JavaScript 原型和继承
很多面向对象语言都支持两种继承实现方式:接口继承和实现继承 - 接口继承继承方法签名,实现继承则继承方法的实现。但由于 JavaScript 中没有方法签名,因此只支持实现继承者一种方式,而原型链正是实现它的方式。
原型链
JavaScript 中每个对象都有一个被称为原型的持有其它对象引用的私有属性,而原型对象也有其自己的原型,以此类推,这种链条持续到某个原型对象的原型为 null 。根据定义,null 是没有对象的,被作为原型链的最后一环。
当试图访问一个对象的属性时,不仅会搜索对象本身 - 如果对象本身未找到对应的属性,则会沿着对象的原型链层层向上搜索,直到找到匹配的属性或达到原型链的末尾。
每个构造函数都有一个原型对象 ,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。
1 | function Point(x, y) { |
如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函 数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。
1 | function SuperType() { |
下图展示了子类的实例与两个构造函数及其对应的原型之间的关系:
实际上,原型链中还有一环。默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的。 任何函数的默认原型都是一个 Object 的实例, 这意味着这个实例有一个内部指针指向 Object.prototype。这也是为什么自定义类型能够继承包括 toString()、valueOf() 在内的所有默认方法的原因。因此前面的例子还有额外一层继承关系:
继承
原型链虽然是实现继承的强大工具,但它也有问题。主要问题出现在原型中包含引用值的时候。前面在谈到原型的问题时也提到过,原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。在使用原型实现继承时,原型实际上变成了另一个类型的实例。
1 | function SuperType() { |
原型链的第二个问题是,子类型在实例化时不能给父类型的构造函数传参。事实上,我们无法在不影响所有对象实例的情况下把参数传进父类的构造函数。再加上之前提到的原型中包含引用值的问题,就导致原型链基本不会被单独使用。
在子类构造函数中调用父类构造函数,因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用 apply() 和 call() 方法以新创建的对象为上下文执行构造函数,这种实现继承的方法被称为盗用构造函数继承。
这种方法的问题是必须在构造函数中定义方法,因此函数不能重用,而且子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。
使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性,这种实现继承的方法被称为组合继承。
1 | function SuperType(name) { |
组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof 操作符和 isPrototypeOf() 方法识别合成对象的能力。
其它补充
函数的 prototype
每个函数都有 prototype 属性,它默认包含了一个只有 constructor 属性的对象,这个 constructor 属性指向函数自身。
1 | function Foo() {} |
在使用 new 操作符创建对象时,构造函数的 prototype 被赋值给被创建的对象实例的 [[prototype]] 属性。
1 | function Foo() {} |
我们可以使用 constructor 属性来创建一个新对象,该对象使用与现有对象相同的构造器。
1 | function Foo() {} |
JavaScript 原型和继承
https://cocoalei.github.io/blogs/2019/06/13/JavaScript 原型与继承/