一、创建对象有几种方法
请大家尽可能多的找到创建对象的方法,如有补充欢迎在评论区留言讨论。更多参考详见:JavaScript创建对象的7种方法
第一种方式:对象字面量表示法
var o1 = {
name: 'o1'
};
var o2 = new Object({
name: 'o2'
});
缺点:字面量对象中默认原型链指向Object,用同一个接口创建很多对象会产生大量冗余代码。
第二种方式:使用显式构造函数
var M = function (name) {
this.name = name;
};
var o3 = new M('o3');
第三种方式:Object.create
var P = {
name: 'p'
};
var o4 = Object.create(P);
那么,在控制台输入o1
, o2
, o3
, o4
,会发生什么?
-
o1
,o2
输出 Object 对象{ name: 'o1' }
、{ name: 'o2' }
-
o3
输出 M 对象{ name: 'o3' }
-
o4
是个空对象{}
-
o1
,o2
,o3
,o4
均有name属性
二、原型系统的“复制操作”的实现思路
浅拷贝
并不是真的去复制一个原型对象,而是使得新对象持有一个原型的引用;
深拷贝
另一个是切实地复制对象,从此两个对象再无关联。
三、理解原型、构造函数、实例、原型链
原型
JavaScript 是基于原型的编程语言的代表,它利用原型来描述对象。JavaScript 并非第一个使用原型的语言,在它之前,self、kevo 等语言已经开始使用原型来描述对象了。 Brendan 最初的构想是将 JavaScript 设计成一个拥有基于原型的面向对象能力的 scheme 语言,而基于原型系统的独特之处是提倡运行时的原型修改。
对象可以有两种成员类型:
- 实例成员:直接存在于对象实例中
- 原型成员:从对象原型继承
”基于类“和”基于原型“的编程之间的比较:
基于类 | 基于原型 |
---|---|
提倡使用一个关注分类和类之间关系的开发模型 | 提倡编码人员关注一系列对象实例的行为 |
先有类,再从类去实例化一个对象 | 将对象划分到最近的使用方式相似的原型对象 |
类之间的关系:继承、组合等 | 通过复制的方式创建新对象 |
往往与语言的类型系统整合,形成一定编译时的能力 | 一些语言中,复制一个空对象,就是创建一个全新的对象 |
JavaScript的原型:
抛开JavaScript模拟Java类的复杂语法,如 new、Function Object、函数的 prototype 属性等。其原型系统可以用以下两条概括:
- 如果所有对象都有私有字段 [[prototype]],就是对象的原型;
- 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。
判断对象是否包含特定的实例成员:hasOwnProperty("成员的名称")
确定对象是否包含特定的属性:使用in
操作符(既搜索实例又搜索原型)
ES6中,JavaScript提供能直接访问操纵原型的三个方法:
-
Object.create
根据指定的原型创建新对象,原型可以是null
-
Object.getPrototypeOf
获得一个对象的原型 -
Object.setPrototypeOf
设置一个对象的原型
利用以上三个方法,我们可以完全抛开类的思维,利用原型来实现抽象和复用。
var cat = {
say(){
console.log("miao~miao~");
},
play(){
console.log("ball")
},
}
//布偶猫
var Ragdoll = Object.create(cat, {
say: {
writable: true,
configurable: true,
enumerable: true,
value: function () {
console.log("iao~iao~")
}
}
})
var anotherCat = Object.create(cat);
anotherCat.say(); //miao~miao~
var anotherRagdollCat = Object.create(Ragdoll);
Ragdoll.say(); //iao~iao~
构造函数和实例
实例:对象就是一个实例。
构造函数:任何一个函数只要被new操作了,该函数就可以被叫做构造函数。
上图对应关系阐述:
函数都有prototype属性,指的就是原型对象。(声明一个函数时JS引擎会为这个构造函数自动添加prototype属性,该属性会初始化一个空对象,也就是原型对象。)
-
原型对象怎么区分出被哪一个构造函数所引用?
原型对象中有一个构造器constructor,会默认声明的函数,即通过constructor来确定是被哪一个构造函数引用。
上图工作原理代码演示:
上面的例子中o3
是实例,M
是构造函数
//构造函数和原型对象的关系
M.prototype.constructor === M //true
//实例和原型对象之间的关系
o3.__proto__ === M.prototype //true
原型链
原型链就是js中数据继承的继承链。在访问一个实例的属性的时候,先在实例本身中找,如果没找到,再从这个实例对象向上找构造这个实例的相关联的对象,还没找到就再往上找,这个相关联的对象又有创造它的上一级的原型对象。以此类推,到Object.prototype终止(整个原型链的顶端),原型链通过prototype
和__proto__
属性进行向上查找。
原型对象和原型链之间起的作用:
构造函数中增加很多属性和方法。当有多个实例,想共用一个方法,不能每个实例都拷贝一份(若每个实例都要拷贝一份,会占内存,没有必要),多个实例之间有相同的方法时要考虑存到共同的东西上,这个共同的东西就是原型对象。这就是JS引擎支持的原型链的功能,任何一个实例对象通过原型链找到它上面的原型对象,上面的实例和方法都可以被共享。
var M = function(name){this.name = name;};
M.prototype.say = function() {
console.log('say hi');
}
var o5 = new M('o5');
控制台输出:
o3.say()
say hi
o5.say()
say hi
注意:
构造函数才会有prototype,对象是没有prototype 的。
-
实例对象才有
__proto__
属性。特殊的,函数即是函数也是对象,也有
__proto__
属性:M.__proto__ === Function.prototype //true
其他的:
Array.__proto__ === Function.prototype //true
let arr = [1,2,3,4] arr.__proto__ === Array.prototype //true
Array.prototype.__proto__ === Object.prototype //true
Object.prototype.__proto__ === null //true
四、instanceof 原理
原理:实例对象的__proto__
属性和构造函数没什么关联,其实是引用的原型对象。instanceof用来判断实例对象的属性和构造函数的属性是不是同一个引用。
原型对象上可能还会有原型链,用实例对象instanceof判断原型的构造函数,这条原型链上的函数返回都是 true:
o3 instanceof M //true
//只要是原型链上都可以看作instanceof的构造函数
o3 instanceof Object //true
解释:
o3.__proto__=== M.prototype //true
M.prototype.__proto___=== Object.prototype //true
//判断是哪个构造函数直接生成的,比instanceof更严谨
o3.__proto__.constructor === M //true
o3.__proto__.constructor === Object //false
五、new运算符
定义:JavaScript 的 new 运算符创建一个继承于其运算数的原型的新对象,然后调用该运算数,把新创建的对象绑定给this。
按照惯例,打算与 new 结合使用的函数命名应该首字母大写,并谨慎使用 new 。
new运算接收一个构造器和一组调用参数,其工作原理为:
new 后面加上构造函数(构造器),一个新对象被创建,它继承自构造函数原型对象Foo.prototype属性
-
将this和调用参数传给构造器:
构造函数 Foo 被执行的时候,相应的传参会被传入,同时上下文this 会被指定为这个新实例。(new Foo() 在不传递任何参数的时候可以写成 new Foo。)
如果构造函数返回了一个对象,那么这个对象会取代整个 new 出来的结果。换句话说,如果构造函数没有任何返回对象,那么new出来的结果为第一步创建的新对象;有返回对象,直接返回。
实现一个new运算符效果:
var newFunc = function (func) {
//1.创建空对象,关联指定构造函数原型对象
var a = Object.create(func.prorotype);
//2.把上下文转移给b对象
var b = func.call(a);
//3.判断执行之后的结果是不是对象类型
if (typeof b === 'object') {
return b;
} else {
return a;
}
}
控制台输出:
o6 = newFunc(M)
o6 instanceof M //true
o6 instanceof Object //true
o6.__proto__.constructor === M //true
M.prototype.walk = function() {
console.log('walk')
}
o6.walk()
walk
o3.walk()
walk
new 这样的行为,试图让函数对象在语法上跟类变得相似,但是,它客观上提供了两种方式,一是在构造器中添加属性,二是在构造器的 prototype 属性上添加属性。
用构造器模拟类的两种方法:
//第一种方法:直接在构造器中修改this,给this添加属性
function Cls1(){
this.p1 = 1;
this.p2 = function(){
console.log(this.p1);
}
}
var o1 = new Cls1;
o1.p2();
//第二种方法:修改构造器的 prototype 属性指向的对象,它是从这个构造器构造出来的所有对象的原型。
function Cls2(){
}
Cls2.prototype.p1 = 1;
Cls2.prototype.p2 = function(){
console.log(this.p1);
}
var o2 = new Cls2;
o2.p2();
没有 Object.create、Object.setPrototypeOf 的早期版本中,new 运算是唯一一个可以指定 [[prototype]] 的方法(当时的 mozilla 提供了私有属性 proto,但是多数环境并不支持),所以,当时已经有人试图用它来代替后来的 Object.create,我们甚至可以用它来实现一个 Object.create 的不完整的 pollyfill,见以下代码:
Object.create = function(prototype){
var cls = function(){}
cls.prototype = prototype;
return new cls;
}
这段代码创建了一个空函数作为类,并把传入的原型挂在了它的 prototype上,最后创建了一个它的实例,根据 new 的行为,这将产生一个以传入的第一个参数为原型的对象。
六、补充问题
为什么o4直接拿不到name属性?
Object.create方法创建的对象是用原型链连接的,当JS引擎查找o4是一个空对象,这个空对象上是没有name属性的,name属性在它的原型对象上,也就是说,Object.create方法是把参数中的对象作为一个新对象的原型对象赋给o4的,o4本身不具备这个属性。
o4.__proto__ === p
true
总结:原型链真的很重要,值得反复理解推敲。