对JS原型链的一些理解
日常写JS代码中,都是写各种函数,每个函数都有一个prototype属性,可以理解为这是一个指针,指向一个对象。prototype属性通过调用构造函数来创建这个函数对象的原型对象,原型对象又会通过自身constructor属性,指向prototype属性所在的指针。
每个函数都有prototype属性,指向函数的原型对象。它的实例对象都可以共享这个属性。
那实例对象如何和原型对象关联?
js中任何对象都有一个属性叫proto([[prototype]]),这也是一个指针,指向构造函数的原型对象,就是我们所能共享信息的那个对象。
当不同的对象要用到不同的属性,PHP中我们一般通过继承,JS中就靠原型链,这就是类似实现继承的方式。JS中每一次获取对象中的属性都是一次查询过程,如果在自有属性中找不到就会去原型对象中查找,如果原型对象中还查不到,就会去原型对象的原型中查找,也就是按照原型链查找,直到查找到原型链的顶端——Object的原型。
下文摘自:
作者:路易斯
链接:http://louiszhai.github.io/2015/12/15/prototypeChain/
prototype
- 每个函数都有一个 prototype 属性;
- 每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型;
- 每一个对象都会从原型”继承”属性;
constructor
- 每个原型都有一个constructor属性指向关联的构造函数,实例原型指向构造函数;
- 每个构造函数(constructor)都有一个原型对象(prototype);
- 原型对象都包含一个指向构造函数的指针,而实例(instance)都包含一个指向原型对象的内部指针;
如果让原型对象指向另一个类型的实例,有趣的事情便发生了:1
constructor1.prototype = instance2
鉴于上述游戏规则生效,如果试图引用constructor1构造的实例instance1的某个属性p1:
1)首先会在instance1内部属性中找一遍;
2)接着会在instance1.proto(constructor1.prototype)中找一遍,
而constructor1.prototype实际上是instance2, 也就是说在instance2中寻找该属性p1;
3)如果instance2中还是没有,此时程序会继续在instance2.proto(constructor2.prototype)中寻找…直至Object的原型对象
搜索轨迹:instance1-->instance2-->constructor2.prototype-->...-->Object.prototype
这种搜索的轨迹,形似一条长链, 又因prototype在这个游戏规则中充当链接的作用,于是我们把这种实例与原型的链条称作原型链;
举个栗子:
1 | function Father(){ |
确定原型和实例的关系
使用原型链后, 我们怎么去判断原型和实例的这种继承关系呢?
方法一般有两种:
- 第一种是使用 instanceof 操作符, 只要用这个操作符来测试实例(instance)与原型链中出现过的构造函数,结果就会返回true;
举栗:
1 | alert(instance instanceof Object);//true |
由于原型链的关系, 我们可以说instance是Object,Father或Son中任何一个类型的实例,因此这三个构造函数的结果都返回了true;
- 第二种是使用isPrototypeOf()方法,同样只要是原型链中出现过的原型isPrototypeOf()方法就会返回true;
举栗:1
2
3alert(Object.prototype.isPrototypeOf(instance));//true
alert(Father.prototype.isPrototypeOf(instance));//true
alert(Son.prototype.isPrototypeOf(instance));//true
原型链的问题
原型链并非十分完美, 它包含如下两个问题.
- 问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
- 问题二: 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数;
有鉴于此, 实践中很少会单独使用原型链,下面将有一些尝试以弥补原型链的不足;
借用构造函数
为解决原型链中上述两个问题, 我们开始使用一种叫做借用构造函数(constructor stealing)的技术(也叫经典继承);
基本思想:即在子类型构造函数的内部调用超类型构造函数.
举栗:
1 | function Father(){ |
借用构造函数一举解决了原型链的两大问题:
- 其一, 保证了原型链中引用类型值的独立,不再被所有实例共享;
- 其二, 子类型创建时也能够向父类型传递参数.
随之而来的是,如果仅仅借用构造函数那么将无法避免构造函数模式存在的问题–方法都在构造函数中定义, 因此函数复用也就不可用了。而且超类型(如Father)中定义的方法对子类型而言也是不可见的,考虑此借用构造函数的技术也很少单独使用。
组合继承
组合继承,有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式。
基本思路: 使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。
这样既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。
举栗:
1 | function Father(name){ |
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点成为JavaScript中最常用的继承模式,而且instanceof和isPrototypeOf()也能用于识别基于组合继承创建的对象。同时我们还注意到组合继承其实调用了两次父类构造函数, 造成了不必要的消耗。
在ECMAScript5中,通过新增object.create()方法规范化了上面的原型式继承。
object.create() 接收两个参数:
- 一个用作新对象原型的对象
- 一个为新对象定义额外属性的对象(可选的)
1
2
3
4
5
6
7
8var person = {
friends : ["Van","Louis","Nick"]
};
var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.friends.push("Style");
alert(person.friends);//"Van,Louis,Nick,Rob,Style"
object.create() 只有一个参数时功能与上述object方法相同, 它的第二个参数与Object.defineProperties()方法的第二个参数格式相同: 每个属性都是通过自己的描述符定义的,以这种方式指定的任何属性都会覆盖原型对象上的同名属性。
举栗:
1 | var person = { |
寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与(寄生)构造函数和工厂模式类似, 即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
举栗:
1 | function createAnother(original){ |
这个例子中的代码基于person返回了一个新对象–anotherPerson。新对象不仅具有person的所有属性和方法,而且还被增强了,拥有了sayH()方法。
注意: 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。
寄生组合式继承
前面讲过组合继承是JavaScript最常用的继承模式; 不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数: 一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。寄生组合式继承就是为了降低调用父类构造函数的开销而出现的。
其背后的基本思路是: 不必为了指定子类型的原型而调用超类型的构造函数;
1 | function extend(subClass,superClass){ |
extend的高效率体现在它没有调用superClass构造函数,因此避免了在subClass.prototype上面创建不必要多余的属性。于此同时原型链还能保持不变;,因此还能正常使用instanceof和isPrototypeOf()方法。
以上寄生组合式继承:集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方法。
下面我们来看下extend的另一种更为有效的扩展:
1 | function extend(subClass, superClass) { |
我一直不太明白的是为什么要 “new F()”, 既然extend的目的是将子类型的 prototype 指向超类型的 prototype,为什么不直接做如下操作呢?subClass.prototype = superClass.prototype;//直接指向超类型prototype
复制代码显然, 基于如上操作, 子类型原型将与超类型原型共用, 根本就没有继承关系。
new 运算符
为了追本溯源, 我顺便研究了new运算符具体干了什么?发现其实很简单,就干了三件事情。
1 | var obj = {}; |
代码第一行,我们创建了一个空对象obj;
第二行,我们将这个空对象的proto成员指向了F函数对象prototype成员对象;
第三行,我们将F函数对象的this指针替换成obj,然后再调用F函数;
我们可以这么理解: 以new操作符调用构造函数的时候,函数内部实际上发生以下变化:
- 1、创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
- 2、属性和方法被加入到 this 引用的对象中。
- 3、新创建的对象由 this 所引用,并且最后隐式的返回this.
proto 属性是指定原型的关键,
以上通过设置 proto 属性继承了父类, 如果去掉new 操作, 直接参考如下写法
subClass.prototype = superClass.prototype;//直接指向超类型prototype
复制代码,那么使用instanceof方法判断对象是否是构造器的实例时, 将会出现紊乱。
假如参考如上写法, 那么extend代码应该为
1 | function extend(subClass, superClass) { |
此时, 请看如下测试:
1 | function a(){} |
c被认为是a的实例可以理解, 也是对的; 但c却被认为也是b的实例, 这就不对了。究其原因instanceof操作符比较的应该是 c.proto 与 构造器prototype(即 b.prototype 或 a.prototype)这两者是否相等, 又extend(b,a); 则b.prototype === a.prototype, 故这才打印出上述不合理的输出。
那么最终原型链继承可以这么实现:
1 | function Father(name){ |
扩展:
属性查找
使用了原型链后, 当查找一个对象的属性时,JavaScript会向上遍历原型链,直到找到给定名称的属性为止,到查找到达原型链的顶部——也就是 Object.prototype,但是仍然没有找到指定的属性,就会返回undefined,此时若想避免原型链查找,建议使用hasOwnProperty方法. 因为 hasOwnProperty是JavaScript中唯一一个处理属性但是不查找原型链的函数。
例如:console.log(instance1.hasOwnProperty('age'));//true
对比:isPrototypeOf 则是用来判断该方法所属的对象是不是参数的原型对象,是则返回true,否则返回false。console.log(Father.prototype.isPrototypeOf(instance1));//true
instanceof && typeof
上面提到几次提到instanceof运算符,那么到底它是怎么玩的呢? 下面让我们来趴一趴它的使用场景:
instanceof运算符是用来在运行时指出对象是否是构造器的一个实例,例如漏写了new运算符去调用某个构造器,此时构造器内部可以通过instanceof来判断。
1 | function f(){ |
以上,this instanceof arguments.callee 的值如果为true表示是作为构造函数被调用的,如果为false则表示是作为普通函数被调用的。
对比: typeof则用以获取一个变量或者表达式的类型,一般只能返回如下几个结果:
number,boolean,string,function(函数),object(NULL,数组,对象),undefined;
原文作者: ybphp
原文链接: https://www.ybphp.com/2019/10/19/对JS原型链的一些理解/
版权声明: 转载请注明出处(必须保留原文作者署名原文链接)