什么是原型?
JavaScript设计之初并没有类似其他语言的"类"(class)和"实例"(instance)的概念,但是javaScript里面到处都是对象。这时候为了将对象连接起来(继承),就有了"原型链"(prototype chain)。
理解原型
首先我们要知道,每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。
默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数。但是因构造函数而异,可能会给原型对象添加其他属性和方法。
原型成链
在自定义构造函数时,原型对象默认只会获得 constructor 属性,其他的所有方法都继承自 Object 。
调用构造函数生成实例,这个实例的内部 [[Prototype]] 指针就会被赋值为构造函数的原型对象。但是javascript的中没有访问这个 [[Prototype]] 特性的标准方式,但Firefox、Safari和Chrome会在每个对象上暴露 "__proto__" 属性,通过这个属性可以访问对象的原型。
可以理解为:构造函数有一个prototype属性引用其原型对象,而这个原型对象也有一个constructor属性,引用这个构造函数换句话说,两者循环引用,而实例的__proto__指向原型属性。 正常的原型链都会终止于Object的原型对象,Object原型的原型是null
觉得太繁琐的话可以理解成下图:
// 上图的内容在js中的意思如下:
console.log("构造函数与原型对象",Person.prototype.constructor === Person ) // true
console.log("实例指向原型对象",instance.__proto__ === Person.prototype) // true
原型寻找层级
在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。 也就是存在如下图的寻找链:
至于tostring方法、call方法等则是在最上层的Object的原型对象找到的,图就不画了,总结就是沿着原型链向上寻找。
值得一说的是在使用 Object.keys() 获得对象属性的keys时,只能获得实例上的key,原型对象的key则可以用 for in 遍历出来。 ( Object.values(), Object.entries() 同理)
继承
构造继承(盗用构造函数)
// 构造继承
function Farther(name) {
this.name = name
this.sayme = function () {
console.log(this.name)
}
}
function Child() {
Farther.call(this,"vinkon")
}
const instance = new Child()
console.log(instance) // > {"name":"vinkon"}
实现思路: 通过在子类中用 call() / apply() 调用父类构造函数实现。
缺点:必须在构造函数中定义方法,因此函数不能重用。此外, 子类也不能访问父类原型上定义的方法,因此所有类型只能使用 构造函数模式。由于存在这些问题,盗用构造函数基本上也不能 单独使用。
组合继承(伪经典继承)
实现思路: 是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。
function Farther(name) {
this.name = name
this.myParams = ['param1', 'param2']
}
Farther.prototype.getMyParams = function () {
console.log("myParams=>",this.myParams)
}
// const fartherInstance = new Farther('陈')
// console.log(fartherInstance.getMyParams()) // > ["param1","param2"]
// 在此组合继承
function Child(name, age) {
// 继承属性
Farther.call(this, name)
this.age = age
}
//
Child.prototype = new Farther()
Child.prototype.getAge = function () {
console.log(this.age)
}
const instance1 = new Child('陈大', 22)
const instance2 = new Child('王二', 33)
优点:既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。而且组合继承也保留了 instanceof 操作符和 isPrototypeOf() 方法识别合成对象的能力
原型式继承
function object(o) {
function Child(){}
Child.prototype = o;
return new Child
}
const originData = {
name:'foo',
list:['aa','bb']
}
const foo2 = object(originData)
foo2.list.push('cc')
const foo1 = object(originData)
console.log(foo1.name,foo1.list) // foo,["aa","bb","cc"]
foo1 和 foo2 实际上是克隆了两份 object 的属性,但是 foo1,foo2 共享了属性,这意味着调用 object 函数返回的新对象的原型上既有原始值属性又有引用值属性。
Es5增加了 Object.create() 方法将原型式继承的概念规范化了。 这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时, Object.create() 与这里的 object() 方法效果相同。
寄生式继承
创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
function createAnother(original){ let clone = object(original); // 通过调用函数创
建一个新对象
clone.sayHi = function() { // 以某种方式增强
这个对象
console.log("hi"); };return clone; // 返回这个对象
}
寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。 object() 函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。
通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
寄生式组合继承
组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用。本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。
寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
function object(o) {
function Child() { }
Child.prototype = o;
return new Child
}
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () {
console.log(this.age);
};
函数接收两个参数:子类构造函数和父类构造函数。在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的prototype 对象设置 constructor 属性,解决由于重写原型导致默 constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。
这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性,因此可以说这个例子的效率更高。而且,原型键仍然保持不变,因此 instanceof操作符和 isPrototypeOf() 方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。
本文参考:《javaScript高级程序设计第四版》