1.原型链
ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。实现原型链涉及如下代码模式:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
// 继承 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true
由上述的代码不难看出将SubType的原型指向了SuperType的实例化对象这样SubType 的实例不仅能从SuperType 的实例中继承属性和方法,而且还与 SuperType 的原型挂上了钩。如下图所示JavaScript中的原型链关系图:
原型链实现继承特点
-
- 实例可继承的属性有: 实例的构造函数属性, 父类构造函数属性, 父类原型的属性. (新实例不会继承父类实例属性) 原型链实现继承存在的问题:
-
1.原型中包含的引用值会在所有实例间共享
-
2.子类型在实例化时不能给父类型的构造函数传参
2.盗用构造函数
在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用 apply()和 call()方法以新创建的对象为上下文执行构造函数。如下列所示
function SuperType(name){
this.name = name;
}
function SubType() {
// 继承 SuperType 并传参
SuperType.call(this, "Nicholas");
// 实例属性
this.age = 29;
}
let instance = new SubType();
console.log(instance.name); // "Nicholas";
console.log(instance.age); // 29
相对于原型链继承的优点
- 1.可以在子类构造函数中向父类构造函数传参 构造函数实现继承的问题
- 1.使用构造函数模式自定义类型的问题:必须在构造函数中定义方法,因此函数不能重用
- 2.子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。由于存在这些问题,盗用构造函数基本上也不能单独使用。
3.组合继承
组合继承(有时候也叫伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来。基 本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方 法定义在原型上以实现重用,又可以让每个实例都有自己的属性。如下例子所示:
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;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27
组合继承的优点
- 1.结合了两种模式的优点, 传参和复用 组合继承的问题
- 1.调用了两次父类构造函数(耗内存), 子类的构造函数会代替原型上的那个父类构造函数。
4.原型式继承
用一个函数包装一个对象, 然后返回这个函数的调用, 这个函数就变成了个可以随意增添属性的实例或对象Object.create() 就是这个原理。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
这个 object()函数会创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例。本质上,object()是对传入的对象执行了一次浅复制。
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
原型式继承的优点
- 原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。 原型式继承的不足
- 属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
5.寄生式继承
寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。基本的寄生继承模式如下:
function createAnother(original){
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
寄生式继承的优点
- 没有创建自定义类型, 因为只是套了个壳子返回对象, 这个函数顺理成章就成了创建的新对象 寄生式继承的不足
- 通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
6.寄生组合式继承
组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是 创建子类原型时调用,另一次是在子类构造函数中调用。寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。寄生式组合继承的基本模式如下所示:
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);
};
寄生式继承的特点
- 这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性,因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此 instanceof 操作符和isPrototypeOf()方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。
7.类
前面几种继承方式只讲解了如何只使用 ECMAScript 5 的特性来模拟类似于类(class-like)的行为。不难看出,各种策略都有自己的问题,也有相应的妥协。正因为如此,实现继承的代码也显得非常冗长和混乱。为解决这些问题ECMAScript 6 新引入的 class 关键字具有正式定义类的能力。类(class)是ECMAScript 中新的基础性语法糖结构,因此刚开始接触时可能会不太习惯。虽然 ECMAScript 6 类表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。
实例化
使用 new 操作符实例化某个类的操作等于使用 new 调用其构造函数。唯一可感知的不同之处就是JavaScript 解释器知道使用 new 和类意味着应该使用 constructor 函数进行实例化。使用 new 调用类的构造函数会执行如下操作。
-
- 在内存中创建一个新对象
-
- 这个新对象内部的 [[Prototype]] 指针被赋值为构造函数的 prototype 属性。
-
- 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
-
- 执行构造函数内部的代码(给新对象添加属性)。
-
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。 理解了实例化某个类的过程,那么就可以尝试自己手动实现一个new方法:
// 1. 方式一
function _new(Obj, ...args) {
let obj = Object.create(Obj.prototype)
let result = Obj.apply(obj, args)
return result instanceof Obj ? result : obj
}
// 2.方式二
function _new2() {
// 创建一个对象
let obj = {}
// 获得构造参数
let Con = [].shift.call(arguments)
// 链接到原型(给obj这个新生对象的原型指向它的构造函数的原型)
obj.__proto__ = Con.prototype
// 绑定this
let result = Con.apply(obj, arguments)
// 确保new出来的是一个对象
return result instanceof Con ? result : obj
}