参考书籍:JavaScript高级程序设计 第三版、第四版
原型链是什么?
首先,我们要清楚原型是个什么东西。根据红宝书的描述:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针(constructor),而构造函数的实例有一个指向原型对象的指针(__proto__)。 通过这句话我们可以推出: 当一个构造函数FA的原型对象是另一个构造函数FB的实例时,我们便可以通过FA.prototype.__proto__(注明:__proto__所代表的指针实际上是不可访问的,这里仅用作理解说明)去找到FB的原型对象,也就可以拿到FB原型对象上的方法。这也就是原型链继承的核心思想。
function SubClass() {}
function SuperClass() {}
SuperClass.prototype.getName = function() {
console.log('我是SuperClass的原型方法')
}
FA.prototype = new SuperClass()
let sub = new SubClass()
sub.getName() // 打印出 我是SuperClass的原型方法
SubClass.prototype.__proto__.getName() // 打印出 我是SuperClass的原型方法
console.log(SubClass.prototype.__proto__ === SuperClass.prototype) // true
console.log(sub.__proto__.__proto__ === SuperClass.prototype) // true
复制代码
万物基于Object的来源:默认原型
我们都知道Javascript中的所有的引用类型都继承了Object,这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型中包含了一个指向Object.prototype的指针(__proto__)。
沿用上面的例子:
// 1,通过 instanceof 方法判断原型链的存在
console.log(sub instanceof SubClass) // true
console.log(sub instanceof SuperClass) // true
console.log(sub instanceof Object) // true
// 2,通过 isPrototypeOf 方法判断
console.log(SubClass.prototype.isPrototypeOf(fa)) // true
console.log(SuperClass.prototype.isPrototypeOf(fa)) // true
console.log(Object.prototype.isPrototypeOf(fa)) // true
复制代码
原型链的问题
原型链能够很方便的实现继承,但是也存在问题:定义在原型上的引用值会被所有实例共享,这也是为什么属性通常在构造函数中定义而不是定义在原型上的原因。当我们使用原型继承时,原型通常会变成另一个构造函数的实例,这时另一个构造函数上的属性也会变成原型上的属性,自然而然也就被所有实例共享了。 并且,子类型实例化时也无法为父类型的构造函数传递参数,再加上之前提到引用值的问题,导致原型链基本不会被单独使用:
function SuperClass() {
this.name = ['super']
}
function SubClass() {}
SubClass.prototype = new SuperClass()
/*
* 注意此时相当于FB的原型等于FA的一个实例了
* 则有 SubClass.prorotype.name = ['super']
*/
let sub1 = new SubClass()
fb1.name.push('sub')
let sub2 = new SubClass()
console.log(sub2.name) // ['super', 'sub'] , 因为他们的name属性在原型上,所以是共享的,但是显然我们不希望这样
复制代码
盗用构造函数(经典继承)
为了解决原型继承的方法,一种名为“盗用构造函数”的技术流行起来。思路很简单:在子类中调用父类构造函数,通过call,apply方法以新创建的对象为上下文执行父类的构造函数。(其实ES6提供的class写法就使用了种思路,在子类构造函数中调用 super 方法,这个super方法其实就是父类的构造函数)
function SuperClass(name) {
this.name = name
this.color = 'red'
}
function SubClass(name, age) {
// 传递 name 作为参数
SuperClass.call(this, name)
this.age = age
}
const sub = new SubClass('sub', 18)
console.log(sub) // SubClass { name: 'sub', color: 'red', age: 18 }
复制代码
盗用构造函数也有其缺点,问题在于方法必须在构造函数中定义,因此函数无法重用,并且子类的实例也不能访问父类原型上的方法,因此盗用构造函数继承一般也不会单独使用。
组合继承
组合继承结合了原型链继承和盗用构造函数继承的优点。思路是使用原型链继承原型上的属性和方法,再通过盗用构造函数继承实例属性,这样既可以实现方法重用,也可以让每个实例拥有自己的属性:
function SuperClass(name) {
this.name = name
this.color = 'red'
}
SuperClass.prototype.sayName = function() {
console.log(this.name)
}
function SubClass(name, age) {
// 继承属性
SuperClass.call(this, name)
this.age = age
}
SubClass.prototype = new SuperClass()
const sub = new SubClass('sub', 18)
console.log(sub) // SuperClass { name: 'sub', color: 'red', age: 18 }
sub.sayName() // sub
复制代码
组合继承弥补了原型继承和盗用构造函数的不足,是JavaScript中使用最多的继承方式。组合继承也保留了 instanceof 和 isProtoetpeOf() 方法识别的能力。
原型式继承
首先观察以下函数:
function object(obj) {
function F() {}
F.prototype = obj
return new F()
}
复制代码
object函数创建一个临时构造函数,将传入的对象作为构造函数的原型,再返回构造函数的实例。实际上是对传入的对象进行了一次浅拷贝:
let originObj = {
name:'origin',
color: ['red', 'yellow']
}
let newObj = object(originObj)
newObj.name = 'new'
newObj.color.push('blue')
console.log(originObj.color) // [ 'red', 'yellow', 'blue' ]
复制代码
我们利用原型式继承可以得到一个新对象,这个对象的__proto__其实就指向了原始对象,这个过程中没有构造函数的参与,也意味着两个对象上共享了属性,实质上相当于克隆了一个原始对象。 原型式继承适用于对象继承对象,而不需要构造函数参与的情况。
ECMAScript5新增了Object.create()方法将原型式继承的思想规范化了。这个方法接收两个参数:作为新对象原型的对象,以及新增额外属性的对象(可选)。在只有一个参数时, Object.create()方法与这里的create()方法效果相同。
需要记住,原型式继承属性中包含的引用值依然会在相关对象间共享,这一点与原型链继承是一样的。
寄生式继承
寄生式继承与原型式继承比较类似。主要思想是通过一个实现继承的函数,以特定方式增强对象,然后返回这个对象。基本的寄生继承模式如下:
function createPerson(origin) {
let clone = Object.create(origin)
clone.sayName = function() {
console.log(this.name)
}
return clone
}
let person = {
name: '张三'
}
let another = createPerson(person)
another.sayName() // 张三
复制代码
上述代码中的 Object.create 不是必须的,任何返回新对象的函数都可以使用。寄生式继承适用于关注对象,而不在乎类型与构造函数的场景。
注意,寄生式继承会导致函数难以服用,这一点与构造函数模式相同。
寄生式组合继承
组合继承其实也有一个缺点:父类构造函数会被调用两次,一次是在子类构造函数中调用,一次是赋值为子类原型时调用。