1. 原型链继承
// 定义父类
function Parent() {
this.name = 'Jack';
}
// 父类原型添加方法
Parent.prototype.getName = function () {
return this.name;
};
// 子类
function Child() {}
// 子类的原型设置为父类Parent的实例
Child.prototype = new Parent();
// 实例化子类
const child = new Child();
console.log(child.getName()); // Jack
父类Parent
有属性和方法,子类Child
没有属性和方法。实现继承的关键是,子类Child
没有使用默认原型,而是将其替换成了一个新的对象,这个对象恰好是Parent
的实例,这样Child
的实例就能从Parent
的实例中继承属性和方法,而且还与Parent
的原型挂上了钩。
Child
和Parent
的关系如下如所示:
缺陷
(1)原型中包含引用值的时候,会在所有实例间共享
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType() {}
// 继承 SuperType
SubType.prototype = new SuperType();
const instance1 = new SubType();
instance1.colors.push('black'); // 加入新元素
console.log(instance1.colors); // [ 'red', 'blue', 'green', 'black' ]
const instance2 = new SubType();
console.log(instance2.colors); // [ 'red', 'blue', 'green', 'black' ]
SubType
的所有实例都会共享这个colors
属性。这一点通过instance1.colors
上的修改也能反映到instance2.colors
上就可以看出来。
(2)子类型在实例化时不能给父类型的构造函数传参
由于以上两个原因,原型链继承基本不会单独使用。
2. 盗用构造函数
这种技术有时也称作对象伪装或经典继承。在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用 apply()
和call()
方法以新创建的对象为上下文执行构造函数。
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType() {
// 继承 SuperType
SuperType.call(this);
}
const instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // [ 'red', 'blue', 'green', 'black' ]
const instance2 = new SubType();
console.log(instance2.colors); // [ 'red', 'blue', 'green' ]
通过使用call()
或apply()
方法,SuperType
构造函数在为SubType
的实例创建的新对象的上下文中执行了。这相当于新的SubType
对象上运行了 SuperType()
函数中的所有初始化代码。结果就是每个实例都会有自己的colors
属性。
优势
相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参。
function SuperType(name) {
this.name = name;
}
function SubType() {
// 继承 SuperType 并传参
SuperType.call(this, 'Nicholas');
// 实例属性
this.age = 29;
}
const usr = new SubType();
console.log(usr.name, usr.age); // Nicholas 29
缺陷
(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);
};
const usr1 = new SubType('Nicholas', 29);
usr1.colors.push('black');
console.log(usr1.colors); // [ 'red', 'blue', 'green', 'black' ]
usr1.sayName(); // Nicholas
usr1.sayAge(); // 29
const usr2 = new SubType('Greg', 27);
console.log(usr2.colors); // [ 'red', 'blue', 'green' ]
usr2.sayName(); // Greg
usr2.sayAge(); // 27
组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了instanceof
操作符和isPrototypeOf()
方法识别合成对象的能力。
4. 原型式继承
Object.create()
方法接收两个参数:
- 作为新对象原型的对象
- 给新对象定义额外属性的对象(可选)
原型式继承适用于这种情况,有一个对象,想在它的基础上再创建一个新对象。需要把这个对象先传给Object.create()
,然后再对返回的对象进行适当修改。本质上,Object.create()
是对传入的对象执行了一次浅复制。
const person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van'],
};
const anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
const anotherPerson2 = Object.create(person);
anotherPerson2.name = 'Linda';
anotherPerson2.friends.push('Barbie');
console.log(person.friends); // [ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]
person.friends
不仅是person
的属性,也会跟anotherPerson
和anotherPerson2
共享。这里实际上克隆了两个person
。
Object.create()
的第二个参数,每个新增属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。
const person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van'],
};
const anotherPerson = Object.create(person, {
name: {
value: 'Greg',
},
});
anotherPerson.name = 'Jack';
console.log(anotherPerson.name); // Greg
原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
5. 寄生式继承
与原型式继承比较接近的一种继承方式是寄生式继承。
function createAnother(original) {
const clone = Object.create(original); // 通过调用函数创建一个新对象
clone.sayHi = function () {
// 以某种方式增强这个对象
console.log('hi');
};
return clone; // 返回这个对象
}
const person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van'],
};
const anotherPerson = createAnother(person);
anotherPerson.sayHi(); // hi
console.log(anotherPerson.name); // Nicholas
console.log(anotherPerson.friends); // [ 'Shelby', 'Court', 'Van' ]
这个例子基于person
对象返回了一个新对象。新返回的anotherPerson
对象具有person
的所有属性和方法,还有一个新方法sayHi()
。
寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。Object.create()
函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。
6. 寄生式组合继承
组合继承存在效率问题:父类构造函数始终会被调用两次,一次在是创建子类原型时调用,另一次是在子类构造函数中调用。
寄生式组合继承的基本模式如下所示:
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
inheritPrototype()
函数实现了寄生式组合继承的核心逻辑。函数接收两个参数:子类构造函数和父类构造函数。在这个函数内部,第一步是创建父类原型的副本。然后,给返回的prototype
对象设置constructor
属性,解决由于重写原型导致默认constructor
丢失的问题。最后将新创建的对象赋值给子类型的原型。
function inheritPrototype(subType, superType) {
let prototype = Object.create(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);
};
const usr = new SubType('Jack', 18);
usr.sayName(); // Jack
usr.sayAge(); // 18
console.log(usr.colors); // [ 'red', 'blue', 'green' ]
这里只调用了一次SuperType
构造函数,避免了SubType.prototype
上不必要也用不到的属性, 因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此instanceof
操作符和isPrototypeOf()
方法正常有效。
寄生式组合继承可以算是引用类型继承的最佳模式。