本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
如何理解JS对象中的[[prototype]]实为紧握JS原理的必备知识架构,笔者曾经觉得在实际工作中对原型的探索知之甚少,实际上是自己不会去基于对象应用[[prototype]]原型链,本篇主要用于学习和总结JS原型的总结。
希望能通过本篇周详的博文,带着大家彻底搞懂JavaScript的原型。
1.What's prototype?
JavaScript中的对象,有一个名为[[prototype]]的原型链,它是一个抽象的查找链。 除了Object.create(null)创建对象以外,其他的对象在创建时,[[prototype]]查找链都是非空的。
var a = new Object();
console.log(a);
打印结果:
我们在试图引用对象的属性时,如果没有触发对象的访问描述符,那么会默认执行[[Get]]操作,比如我们创建了一个新对象:var newObj={a:1}在讨论newObj.a时,实际上就是执行了[[Get]]操作。
对于默认的[[Get]]操作,我们会先在对象本体进行属性查询,如果本体找不到对应的属性,那就继续在[[prototype]]原型链上寻找,一直寻找到JS内置对象Object.prototype,Object.prototype即为查找链上的顶层。
这里要注意两点:
1.在对象本体找不到属性,跳到[[prototype]]上继续寻找,如果找遍了整个[[prototype]]原型链上也找不到属性,[[Get]]操作的查询结果将返回一个undefined。
2.如果对象本体上的属性名的属性值为undefined,[[Get]]操作找到属性时,返回的属性值也会是undefined,如果需要对两者进行区分,请使用对象的in查询或者对象.hasOwnProperty()操作。
这里不再赘述,关于这类的详细介绍请看笔者所写的这篇:面试宝典-让你全方位搞懂JS中的对象!
我们看以下代码:
var newObj = {
a: 2,
};
var myObj = Object.create(newObj);
console.log(myObj.a); //2
console.log(myObj); //{}
Object.create(newObj)创建一个对象,并将新创建对象的[[prototype]]原型链关联到由Object.create()调用的对象newObj,myObj虽然不存在属性a,但是[[Get]]操作成功的在myObj的[[prototype]]原型链上找到了相关联的newObj,并在newObj上找到了属性a。
使用for...in循环和in查找,和在对象上执行[[Get]]操作的效果都是类似的,它们都会在对象本身不存在该属性时,跑到对象的[[prototype]]原型链上去查找。
var newObj = {
a: 2,
};
var myObj = Object.create(newObj);
for (var i in myObj) {
console.log('find' + i); //finda
}
console.log('a' in myObj); //true
1.1 Object.prototype
所有的普通对象中的[[prototype]]原型链的查找结果,最终都会指向内置的Object.prototype,所有的JS内置对象的起源均为Object.prototype,它包含了JS中许多的通用功能。
1.2 属性的查找和屏蔽
我们在查询一个属性,或者修改一个属性的时候,都会执行我们上述代码所阐述的查找顺序,事实上,我们在进行查找的时候,会遵照更为详细的步骤进行查询,我们围绕一个栗子来看对象属性的查找newObj.a=2;
- 我们现在newObj基于[[Get]]进行查找,这一层是最简单的,如果newObj本体有属性a,那我们直接对a的值进行修改操作。如果此时对象的[[prototype]]原型链上同时也存在同名属性a,那么[[prototype]]上找到的属性a会默认被屏蔽,并不会被查到后进行修改。
查找和修改操作会在newObj本体上停止,不会继续向上查询,这就是属性的屏蔽。
实际上在这里起作用的分别是JS的编译器和引擎,JS编译器在发现存在a时,就不会再创建一个新的属性a,当JS引擎来执行这段代码的时候,会在作用域里面再找一次属性a,如果找到了那就直接对属性a进行赋值,若无则继续查找。
我们下面会举一些关联原型链的属性查找方式的案例,我们会借助利用Object.create()函数结合newObj对象参数而生成的新对象realObj来问大家讲解,realObj本身在创建出来后为一个空对象,但其[[prototype]]关联newObj对象。
2.如果newObj本身不存在属性a,那我们继续去newObj的[[prototype]]原型链上穷追不舍地继续查找,如果我们在[[prototype]]的原型链上找到了属性a,且属性a的数据描述符中的writable的值为true(可写特性),那我们就会在newObj本体创建一个属性a并进行赋值,但不会变更原型链上的属性a,上层的[[prototype]]原型链上找到的属性a就会被屏蔽。
var newObj = {};
Object.defineProperty(newObj, 'a', {
writable: true,
value: 1,
});
var realObj = Object.create(newObj);
newObj.a = 2;
console.log(realObj.a); //2
3.如果newObj的[[prototype]]原型链上查到了属性a,newObj本身不存在属性a,而且newObj的[[prototype]]原型链上的属性a的数据描述符的writable的值为false(不可写),那么对newObj.a的创建会默认失效。若是在严格模式下会抛出一个TypeError的操作错误。
var newObj = {};
Object.defineProperty(newObj, 'a', {
writable: false,
value: 1,
});
var realObj = Object.create(newObj);
newObj.a = 2;
console.log(realObj.a); //1 -> 这是在realObj原型链上的属性a的值1,realObj是一个空对象
4.如果newObj的[[prototype]]原型链上有属性a,newObj本身不存在属性a,而且属性a的由访问描述符的setter构建,那么属性a不会被创建到newObj,尽管我们可以从newObj查到该属性a。
var newObj = {};
Object.defineProperty(newObj, 'a', {
get: function () {
return this.aNum;
},
set: function (val) {
this.aNum = val;
},
});
newObj.a = 3;
var realObj = Object.create(newObj);
realObj.a = 4;
console.log(realObj); //{aNum: 4} a并不存在
5.如果原型链上都找不到这个属性,该属性会直接在newObj本体上创建,并完成赋值操作。
如果我们希望在2和3的情况下依然能创建一个属性屏蔽,那我们可以利用Object.defineProperty()来创建属性,无论用数据描述符直接创建还是访问描述符创建都是等效的。
var newObj = {};
Object.defineProperty(newObj, 'a', {
writable: false,
value: 1,
});
var realObj = Object.create(newObj);
Object.defineProperty(realObj, 'a', {
value: 3,
});
console.log(realObj); //{a:3}
综上来看,其实这个属性屏蔽的设计非常奇怪,那就是普通赋值修改同名属性的方式(XXX=XXX)在步骤2中不能实现属性的屏蔽,如果说对象的原型链中中存在一个只读属性或者setter构建的属性就不让改了,可是用Object.defineProperty()却可以实现属性屏蔽。
这简直是离谱的妈妈给离谱开门——离谱到家了,但是JS就是这么设计的。
1.3 隐式赋值产生的属性屏蔽
var newObj = { a: 2 };
var realObj = Object.create(newObj);
console.log(realObj.hasOwnProperty('a')); //false
realObj.a++; //隐式屏蔽
console.log(realObj.a); //3
console.log(realObj.hasOwnProperty('a')); //true
realObj.a++等同于realObj.a=realObj.a+1,那么进行赋值操作的realObj.a(左2)就会进行查询工作,在realObj本身搜索无果后,就会跑去[[prototype]]原型链上找到属性a,进行+1操作后会以赋值(a=3 [[Put]]操作)的形式创建屏蔽属性a,太危险了!
2.JavaScript的“类”
JavaScript中只有对象,我们任何模仿类的行为(继承、混入)只不过是基于对象的复制,对象才是JavaSript的“王道”,对象直接定义自己的行为。
2.1 “类”的沿用
所有在JS中沿用类的行为,都是基于函数的prototype属性,函数也属于对象,prototype属性不可枚举,其会指向另外一个对象。
function Apple() {}
console.log(apple.prototype);
函数上的prototype所构成的对象通常被称之为函数的原型,我们可以通过上例的apple.prototype来访问这个对象。
而基于new构造函数的调用所生成的对象,对象的[[prototype]]原型链和函数的prototype关联的实现原理为:
通过调用new Apple()新建了一个对象,该新建对象的[[prototype]]被"链接(关联)"到"Apple.prototype"这个对象上。
我们可以通过Object.getPrototypeOf()来验证一下上述结论:
function Apple() {}
var fnA = new Apple();
console.log(Object.getPrototypeOf(fnA) === Apple.prototype); //true
上述操作生成了new Apple()生成的对象,其内部的[[prototype]]关联着函数Apple.prototype。
这里隐藏着一个小细节:new Apple()和Apple.prototype的关联是无意间产生的,两者并不存在直接创建的必然联系,new Apple()生成的对象内部[[prototype]]原型链上间接关联了Apple.prototype,这并不是针对“类”的复制,只是两个对象意外的相互关联。
传统计算机语言的继承意味着复制,但在JavaScript并不会“默认”复制对象属性,JavaScript必须要通过特定的行为来复制对象属性(混入操作),就算如此,对象内部的函数也并非真正的复制,复制的不过是函数的引用。
function mixin(sourceObj, tagetObj) {
for (var key in sourceObj) {
if (!(key in tagetObj)) {
tagetObj[key] = sourceObj[key];
}
}
return tagetObj;
}
上述代码即为JS的显式混入的mixin函数,真正的复制只是对象内部的基本数据类型的复制,对象内部的函数和对象所进行的浅拷贝操作,只是针对对象内部的函数和对象的引用。
而我们在讨论“类”的继承也是同理,“类”的继承是通过一个对象的委托去访问另一个对象的属性和函数,这在Object.create()和new构造函数的调用上也是基于上述原理实现的。
2.2 new的构造函数调用
function Apple() {}
var fnA = new Apple();
在其他面向类的语言里,一旦我们看到new,就会认为这是在构造一个类的实例。
但是在JS里面,并非如此。
一个函数的prototype有一个共有且不可枚举的属性constructor,这个属性的属性引用是对象关联的函数,你可以理解为它指向函数本身。
function Apple() {}
console.log(Apple.prototype.constructor === Apple); //true
var objA = new Apple();
console.log(objA.constructor === Apple); //true
由new Apple()调用所创建的对象,“看起来“包含”了一个constructor属性,然而实际上对象上的这个.constructor属性引用,是基于[[Get]]查询在对象的[[prototype]]原型链一路查到了关联函数上才找到的。
严格来说objA.constructor是被委托给了对象的关联函数的prototype.constructor,也就是Apple.prototype,当我们在执行objA.constructor时,实际是执行了默认委托Apple.prototype.constructor,最终指向创建这个对象的函数。
你要理解这一段话,那就要抓住上一段总结的核心:
new Apple()生成的对象objA,其内部的[[prototype]]原型链关联着函数Apple.prototype。
objA里面实际上是没有constructor的,我们针对constructor的查找,是从对象本体到对象的[[prototype]]依次查询,并在对象的[[prototype]]上找到这个constructor,并执行了委托操作。
那么一来二去就可以说清了,所谓的constructor也就是:
1.如果函数没有被new所调用,那么会指向对象关联的函数,也就是函数本身。
2.如果函数被new调用了,那么也会通过对象的[[prototype]]原型链查到创建这个对象的构造函数,当然就是指向“我”(函数)自己啦!
好家伙,你说的这个“朋友”(constructor)指来指去都是你自己啊!
另外插句题外话,用new调用的函数其实压根就是普通函数,如果你的领导或者同事看到你在用new调用时不用首字母大写来写对应“类”的函数,他们借此指责你不够专业你不懂JS的“类”的话,我建议你直接问他为什么在JS里面用new构建所谓的“类”首字母要用大写,他们十有八九答不上来。
因为在JS里面用new构建函数首字母大写根本就没有意义好嘛!!!你用大写小写根本没有实际功效上的差别。
这是因为在JS里面实际上根本没有类,这只不过是大家约定俗成的“自导自演”——我看起来好像真的在JS里面操作“类”,实则为皇帝的新装罢了。
同学们,要记住JavaScript是一门什么语言?面向对象的语言!这就是最直白的解释,接下来我们会继续粉碎JavaScript存在“类”这一谎言。
2.3 JS的“构造函数”真的是“构造函数”吗?
function Apple() {}
console.log('我是类,我是类你信吗?');
var fnA = new Apple(); //'我是类,我是类你信吗?'
console.log(fnA); //{}
我们可以从这一段代码看出,我们在使用new Apple()时,实际上会执行以下步骤:
1.new Apple()被基于当前词法作用域先被调用,然后再被new所劫持生成一个新对象。
2.新对象会立刻执行[[prototype]]原型链与函数的prototype相关联。
3.新对象会绑定函数调用时的this。
4.如果函数内部有返回(return)一个对象,那么我们优先调用这个返回的对象,如果没有返回则返回这个新对象。
Apple本质上就只是一个平平无奇的普通函数,不要再把它往类方面想象了。
上述的话可以总结为:new会劫持普通函数,然后用构造对象的形式来调用它。
Apple函数压根就不是一个所谓“构造函数”,这个“构造函数”只不过是我们强加给它的概念,所谓的构造函数只不过基于new实现的函数调用。
实际上,JS中的构造函数(这里不用引号了,大家明白其实际的行为定义就好)是支持携带参数的,本质就是函数调用时的参数传递,如果内部存在this指定,会基于新生成对象的对象上下文绑定this指向。
function Apple(param) {
this.num = param;
}
Apple.prototype.showNum = function () {
return this.num;
};
var fruit = new Apple(3);
console.log(fruit.showNum()); //3
我们来看一下这段代码:
1.首先我们要记住所总结的概论:
构造函数所生成的新对象,其内部的[[prototype]]关联着函数.prototype这个对象,这绝非复制关系,而是关联关系,函数的prototype对象上的修改会影响对象的[[prototype]]原型链,是因为我们本就通过对象的[[prototype]]原型链去查找到函数的prototype对象本体。
2.Apple.prototype这个对象不是在构建函数的过程中复制给fruit的,fruit所接收的是由new Apple()调用所生成的新对象,新对象关联了函数的[[prototype]]原型链(注意是关联不是包含!一定要注意!)我们的查找顺序链是这样的:
I.首先查找fruit本体上有没有函数showNum(),没有,下一层。
II.然后顺着fruit的[[prototype]]原型链一路往上查,噢,它是关联到到函数Apple.prototype对象上的,那通过委托去Apple函数的prototype上找!
III.在Apple.prototype上找到,好!继续执行调用步骤!基于对象上下文锁定fruit,找到了!返回对应结果!
全程都是在对象的[[Get]]操作上完成的。
如果对查找的原理有疑问,欢迎过来看这篇面试宝典-让你全方位搞懂JS中的对象!
2.3.1 不可靠的constructor
前面我们讲过,对象的.constructor属性引用会通过委托到函数的prototype.constructor,最终指向函数本身。
然而实际上,我们假设Apple是一个函数,那么Apple.prototype内部的constructor只是Apple函数在声明时的默认属性,而非固定不可变属性,如果我们重新定义了Apple.prototype,又没有重新定义Apple.prototype里面的constructor,那么在由new构造的函数调用里面,关联函数的new生成对象的constructor不会再指向函数。
可能这么看有点绕口,我们不如直接看代码:
function Apple() {}
Apple.prototype = { a: 2 }; /* 创建新的原型对象,原本的默认原型对象被覆盖 */
var objA = new Apple();
console.log(objA.constructor === Apple); //false
console.log(objA.constructor === Object); //true
objA并没有.constructor属性,这一点我们在之前已经讲过了,所以针对constructor属性的查询回来到[[prototype]]原型链上,按照默认定义,objA会通过自身的[[prototype]]原型链委托到Apple.prototype去查询,然而Apple.prototype被改写了,里面根本不存在constructor。
在这里,我们会沿着委托链继续查找,直到来到对象的顶端——Object.prototype,然后会委托给Object.prototype,其内部的constructor会指向Object函数自身。
如果你想在函数的新原型对象上重新定义constructor属性,也没问题,它一样也可以使对象的委托查询最后指向函数自身,但这需要你手动地进行重新赋值。
function Apple() {}
Apple.prototype = { a: 2 }; /* 创建新的原型对象,原本的默认原型对象被抹去 */
Object.defineProperty(Apple.prototype, 'constructor', {
enumerable: false,
writable: true,
value: Apple,
});
var objA = new Apple();
console.log(objA.constructor === Apple); //true
console.log(objA.constructor === Object); //true
综上结论,构造函数所生成的对象和构造函数所调用的函数本身的关系可以说是非常“塑料”的,构造函数所调用的函数的prototype和对象的[[prototype]]链并不必然相联。
我们不能够通过对象的constructor属性武断地判断其由哪个函数所构造,函数内部的prototype和对象的[[prototype]]原型链的关系只不过是委托关系,并没有必然的关联。
3.“类”的继承
3.1 实现原型的继承
我们可以通过Object.create()实现模拟“类”的继承,Object.create()会创建一个新的对象,并将新对象(下文则是Orange.prototype对象)内部的[[prototype]]原型链关联到指定的对象(Apple.prototype),前提是我们需要使用函数的原型来完成对应的继承,下面要实现的并非复制操作,而是利用原型实现的委托关联。
function Apple(num) {
this.num = num;
}
Apple.prototype.showNum = function () {
return this.num;
};
function Orange(num, name) {
Apple.call(this, num);
this.name = name;
}
/* 将Orange.prototype对象关联到Apple.prototype对象,这里可以直接创建两个函数之间的Prototype对象关联 */
Orange.prototype = Object.create(Apple.prototype);
/* Orange的prototype属性被改写,内部的constructor属性不再存在 */
Orange.prototype.myName = function () {
return this.name;
};
var fruit = new Orange(4, 'isOrange');
console.log(fruit);
console.log(fruit.constructor);
console.log(fruit.myName());
console.log(fruit.showNum());
打印结果如下:
我们之所以这么做,是因为我们希望在声明function Orange(){}以后,手动把Orange.prototype关联到Apple.prototype,并且抛弃了默认的Orange.prototype。
通过[[Get]]查询fruit.constructor的时候,自然也就导致了fruit.constructor指向了被委托的Apple.prototype.constructor。
那么我们为什么不用以下两种关联方法呢?
1.Orange.prototype=Apple.prototype
2.Orange.prototype=new Apple()
第一种方法导致Orange.prototype接收的并不是一个新对象,只是一个简单的浅拷贝,这就导致了如果在Orange.prototype上修改函数(比如修改MyLabel),将会同步修改Apple.prototype本身。
第二种方法因为利用new进行构造函数的调用时,会自动将基于Apple新生成的对象整个赋值给Orange.prototype,而不是我们想要的单纯关联Orange.prototype和Apple.prototype。
我们可以得出一个结论:由new生成的构造函数调用,其构造函数.prototype和新生成的对象的[[prototype]]只是关联委托关系,并没有进行一个所谓的复制行为。
因此,我们必须要使用Object.create(),创建一个新对象,然后将新对象的[[prototype]]原型链关联到对应对象的[[prototype]]原型链,从某种意义上我们在Orange.prototype“复制”了Apple.prototype,但并非实际的复制,而只是单纯的关联。
如果你改变了构造函数的定义(例如,添加或删除属性和方法),那么这将影响通过 new 构造函数调用生成的新对象。新对象将按照新的构造函数定义来创建。
然而,已经存在的对象不会受到构造函数改变的影响,因为它们在创建时已经获取了构造函数当时的状态。换句话说,它们不会动态地获取构造函数的更新。
function MyConstructor() {
this.a = 1;
}
var myObject = new MyConstructor(); // myObject.a 是 1
MyConstructor.prototype.a = 2; // 改变了构造函数的原型
var myNewObject = new MyConstructor(); // myNewObject.a 仍然是 1,而不是 2
MyConstructor.prototype = { a: 3 }; // 完全改变了构造函数的原型
var myThirdObject = new MyConstructor(); // myThirdObject.a 是 1,而不是 3
在这个例子中,尽管我们改变了 MyConstructor 的原型,但是通过 new MyConstructor() 创建的新对象的 a 属性的值仍然是 1,而不是 2 或 3。这是因为 a 属性是在构造函数内部定义的,而不是在原型上定义的。
9
有没有相对更减少性能消耗的方法来解决这个问题?毕竟肉眼可见Object.create()的实现过程是存在一定的过程消耗的。
ES6的Object.setPrototypeOf()给出了我们一个完美的解决方案。
Object.setPrototypeOf(Orange.prototype, Apple.prototype);
我们可以直接基于Apple.prototype修改现有的Orange.prototype,它会自动识别两者的差异增加属性,无需先抛弃原有的默认对象Orange.prototype再接收新的对象。
3.2 检查“类”的委托关联
我们把检查一个“类”(也就是普通对象)的继承称之为内省,所谓的继承就是委托关联,常用的方法为instanceof。
我们常常喜欢把instanceof称之为查找对象的“祖先”构造函数,但是这种说法是不对的。
instanceof只是为了测试一个对象是否在其原型链上继承自一个构造函数的 prototype。
function Apple() {}
function Orange() {}
Orange.prototype = Object.create(Apple.prototype);
fruit = new Orange();
console.log(fruit);
console.log(fruit instanceof Orange); //true
console.log(fruit instanceof Apple); //true
检查对象fruit的[[prototype]]原型链是否有相关函数的关联,如果Orange.prototype关联了Apple.prototype,它同样能判定fruit的[[prototype]]原型链和Apple.prototype存在关联关系,证明了fruit的[[prototype]]原型链上关联着Orange.prototype,以及Orange.prototype所指向的Apple.prototype。
言简意赅说明instanceof的作用:判定对象的[[prototype]]原型链上有没有函数的prototype。
instanceof用来预判断对象和函数之间的关系,如果我们需要用来判断对象之间的关系,则存在三种通用的方法(__proto__单独开小一节介绍,这里不介绍),我们在下面进行讨论,但是第一种方法是非常不推荐的。
3.2.1 instanceof
使用instanceof来判定对象之间的关系,必须需要创建一个辅助函数。
function connectObj(objA, objB) {
function Fn() {}
Fn.prototype = objB;
return objA instanceof Fn;
}
var apple = { a: 1 };
var orange = Object.create(apple);
console.log(connectObj(orange, apple)); //true
其实这种判断是非常“尴尬”的,我们使用instanceof的目的是为了判断对象的[[prototype]]原型链是否有关联函数.prototype所指向的对象,但是在这里我们直接通过修改函数Fn的prototype属性引用,将其指向了objB,使其充当间接代理人的角色。
这样子非常容易造成误解,因为objA并不是由Fn所构造的,虽然我们可以通过这个辅助函数间接地判断出两个对象之间的关系。
3.2.2 isPrototypeOf()
我们既可以使用这种方法代替instanceof的对象和函数之间是否关联原型链的判定,也可以使用这种方法来判定对象之间的[[prototype]]原型链是否关联,这就避免了借助辅助函数来进行判定,而是直接用对象来判定。
function Apple(param) {
this.num = param;
}
var fruit = new Apple(3);
console.log(Apple.prototype.isPrototypeOf(fruit)); //true
Apple.prototype.isPrototypeOf(fruit)的意义在于判断在fruit的[[prototype]]原型链上,是否存在Apple.prototype?本质上和instanceof是等效的。
同样,我们可以拿来做对象之间的关系判定:
var apple = {};
var orange = {};
apple = Object.create(orange);
console.log(orange.isPrototypeOf(apple)); //true
本段代码打印的判定目标是:orange是否出现在apple的[[prototype]]原型链之中?根据效果可见,能实现!
3.2.3 Object.getPrototypeOf()
我们可以使用ES5里面的Object.getPrototypeOf()来执行对象之间或者对象和函数之间原型链的关系判定。
function Apple(param) {
this.num = param;
}
var fruit = new Apple(3);
var pineApple = {};
var orange = {};
pineApple = Object.create(orange);
console.log(orange === Object.getPrototypeOf(pineApple)); //true
console.log(Object.getPrototypeOf(fruit) === Apple.prototype); //true
3.3 追本溯源:__proto__
__proto__属于可设置的属性,实际上并不存在于对象之中,而是内置于Object.prototype里面,基于在对象上执行的[[Get]]操作所找到的,和consturctor有异曲同工的原理。
你完全可以将__proto__看作是一个访问描述符所生成的产物,请看以下代码:
Object.defineProperty(Object.prototype, '__proto__', {
get: function () {
return Object.getPrototypeOf(this);
},
set: function (obj) {
Object.setPrototypeOf(this, obj);
return obj;
},
});
我们使用Object.setPrototypeOf(this,obj),this指向调用的对象的对象上下文,obj为携带的设置值的参数,实际上就是关联函数的prototype对象,然后在对象内部属性get引用的函数里面利用Object.getPrototypeOf()返回对象的[[prototype]]原型链所关联的函数的prototype对象。
function Apple() {}
function Orange() {}
Orange.prototype = Object.create(Apple.prototype);
fruit = new Orange();
console.log(fruit instanceof Orange); //true
console.log(fruit instanceof Apple); //true
console.log(fruit);
console.log(fruit.__proto__===Orange.prototype); //true->Orange.prototype和Apple.prototype是相关联的
打印输出结果:
当我们调用pineApple.__proto__的时候,对应的正是getter函数返回的pineApple的[[prototype]],指向关联的Apple.prototype,我们会指向对象的[[prototype]]原型链上所查找到的目标Orange.prototype。
一般情况下,我们最好不要随便改一个函数的默认prototype对象,随便变更prototype,不但会使得new调用函数构造生成的对象产生意想不到的查找效果,更可能会导致抹去函数prototype上的constructor,令对象的.constructor属性引用指向错乱,毕竟constructor是“脆弱”的函数内置prototype对象的属性。
4.对象的关联
OK,通过上面的篇幅介绍,我们已经有了一个通识:[[prototype]]原型链机制就是对象上的一个内部链接,一般情况下会通过委托引用构造函数的prototype对象。
当我们没有在对象本体上查找到对应属性时,就会继续跑到[[prototype]]原型链去查它的关联对象,如果还没有找到,则继续沿着[[prototype]]原型链向上去查找,直到查到顶层Object.prototype方才罢休。
对象的一连串紧密关联的链接正是“原型链”。
4.1 从Object.create()探讨对象关联
我们通过Object.create()创建了一个新对象,并将其关联到我们的指定对象,指定对象的[[prototype]]原型链关联了这个新创建的对象的[[prototype]]原型链。
var orange = {
buyFruit: function () {
console.log('buy orange!');
},
};
var apple = Object.create(orange);
console.log(apple); //{}
apple.buyFruit(); //'buy orange!'
我们通过Object.create()创建,可以隔绝因为new构造函数调用,而导致函数本体和对象的[[prototype]]原型链由于关联而造成的相互影响。
Object.create()在这里就是利用委托创建两个对象的关联。
Object.create(null)会创建一个空的[[prototype]]原型链,因为这个对象没有原型链,所以instanceof操作符返回的结果将会是false,因为压根就没有原型链,新生成的对象也无法进行委托操作。
我们通过书写一段简单的polyfill代码来看一下Object.create()功能的实现。
if (!Object.create) {
Object.create = function (obj) {
function Fn() {}
Fn.prototype = obj;
return new Fn();
};
我们通过一次性代码Fn的prototype属性,引用了传递进来的obj对象,并且在最后执行了new Fn()构造函数的调用,返回了一个新对象,新对象的[[Prototype]]原型链关联着Fn的prototype对象。
由此我们可以得出两个知识点:
1.Object.create()主要针对的是对象之间的关联。
2.prototype属性引用只在函数上可以用,对象上是没有prototype属性的,只有[[prototype]]原型链,[[prototype]]原型链本身是不能被直接访问(毕竟根本不是一个属性,只是一个抽象的链概念),想访问对象的关联函数的prototype可以通过"对象.__proto__"来进行访问。
4.2 原型链的备用
一般来说,如果你是为了把一个对象当做另一个对象的备用选项时,你最好通过内部委托的方式,较为显式地调用这个对象内部的方法。
var objA = {
showProFn: function () {
console.log('我是你的备用方法');
},
};
var objB = Object.create(objA);
/* 最好不要这么像下面注释代码那样用,不利于团队开发的代码维护工作 */
// objA.showProFn();
/* 而是要利用内部委托来使用 */
objB.showFn = function () {
this.showProFn(); //此为内部委托,利用对象上下文绑定this指向
};
objB.showFn(); //'我是你的备用方法'
这样的设计会更加清晰。
总结
1.对象本身没有constructor属性,它实际上是通过委托到构造函数的prototype.constructor找到的,意味着在没有对构造函数的prototype进行重写的情况下,对象的constructor通过[[Get]]查找默认指向构造函数本身。
2.如果对象本身不存在一个属性,那么在访问这个对象的时候,会利用[[Get]]查找沿着对象内部的[[prototype]]原型链,去查找关联的构造函数的prototype或者对象上是否有这个属性,对象的[[prototype]]原型链并不是一个具体的值,而是一个抽象的查找链。
3.所有对象都有内置的Object.prototype,其指向原型链的顶端,是对象的“集大成者”,我们当然也可以在Object.prototype中定义对应的属性,如果在Object.prototype都找不到属性,查找就会停止,并会基于LHS查询和RHS查询,分别在严格和非严格模式下返回不一样的结果。
4.利用new进行的构造函数的调用会生成一个新对象,并且将新对象的[[prototype]]原型链关联到构造函数的prototype对象上。
5.对象之间使用Object.create()进行操作,var Apple=Object.create(Orange)会使得新生成的对象Apple的[[prototype]]原型链会关联到Orange对象,进一步调用Orange对象内部的属性和方法。
6.我们假设Apple和Orange都是函数,那么Apple.prototype=Object.create(Orange.prototype),会基于Orange.prototype创建一个新的对象,覆盖原本的Apple.prototype,并将Apple.prototype的[[prototype]]原型链关联到Orange.prototype对象。
7.JavaScript中的“类”继承和传统语言不一样,所谓的继承只是对象之间通过[[prototype]]原型链进行的关联操作,而非复制操作。
8.我们希望通过原型链做属性或者函数的备用时,最好通过内部委托的方式来实现,方便团队开发代码的后续维护工作,使得API更加清晰明了。
9.随便的变更函数默认的.prototype很容易丢失constructor属性,除非你在定义的时候重新将constructor的值指回函数本身,一般我们不要随便改函数内部默认的prototype对象。
10.Object.setPrototypeOf()比Object.create()的开销更小,会基于目标函数的prototype对象以“合并”的方式关联另一个指定对象的prototype,不用抛弃函数内部原有的prototype对象。