本文系统梳理JavaScript中七种主流继承模式的实现原理与局限性。从原型链和构造函数继承的基础用法,到组合继承、原型式及寄生式继承的演进,重点剖析寄生组合式继承为何被视为引用类型最理想的范式。通过代码示例对比各模式在参数传递、属性共享及原型链维护上的差异,帮助开发者根据场景选择最优方案。
1. 原型链继承
function Parent () {
this.name = 'kevin';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.getName()) // kevin
该方式存在以下主要问题:
首先,引用类型的属性会被所有实例共享。例如:
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy", "yayu"]
其次,在创建 Child 实例时,无法向 Parent 构造函数传递参数。
2. 借用构造函数(经典继承)
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
Parent.call(this);
}
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy"]
其优势在于:
- 避免了引用类型属性被所有实例共享的问题。
- 允许在 Child 构造函数中向 Parent 传递参数。
具体示例如下:
function Parent (name) {
this.name = name;
}
function Child (name) {
Parent.call(this, name);
}
var child1 = new Child('kevin');
console.log(child1.name); // kevin
var child2 = new Child('daisy');
console.log(child2.name); // daisy
然而,该方法也存在明显缺点:
- 所有方法均在构造函数内部定义,导致每次创建实例时都会重复创建一遍方法,造成资源浪费。
3. 组合继承
这是原型链继承与构造函数继承的结合体。
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
var child1 = new Child('kevin', '18');
child1.colors.push('black');
console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]
var child2 = new Child('daisy', '20');
console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]
它融合了上述两种模式的优点,是目前 JavaScript 中最常用的继承模式。
4. 原型式继承
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
这实际上是 ES5 Object.create 的模拟实现,其核心是将传入的对象作为新创建对象的原型。
其缺点与原型链继承类似:包含引用类型的属性值始终会被共享。
var person = {
name: 'kevin',
friends: ['daisy', 'kelly']
}
var person1 = createObj(person);
var person2 = createObj(person);
person1.name = 'person1';
console.log(person2.name); // kevin
person1.firends.push('taylor');
console.log(person2.friends); // ["daisy", "kelly", "taylor"]
需要注意的是,修改
person1.name的值时,person2.name的值并未随之改变。这并非因为person1和person2拥有独立的name值,而是因为执行了person1.name = 'person1',从而给person1添加了name值,这并未修改原型上的 name 属性。
5. 寄生式继承
该方法创建一个仅用于封装继承过程的函数,内部以某种形式增强对象,最后返回该对象。
function createObj (o) {
var clone = object.create(o);
clone.sayName = function () {
console.log('hi');
}
return clone;
}
其缺点与借用构造函数模式相同:每次创建对象时都会重新创建一遍方法。
6. 寄生组合式继承
为了便于理解,此处再次展示组合继承的代码:
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
var child1 = new Child('kevin', '18');
console.log(child1)
组合继承最大的缺陷在于会调用两次父构造函数。
第一次调用发生在设置子类型实例的原型时:
Child.prototype = new Parent();
第二次调用发生在创建子类型实例时:
var child1 = new Child('kevin', '18');
回顾 new 操作的模拟实现,在执行该语句时,我们会执行:
Parent.call(this, name);
在此过程中,我们再次调用了一次 Parent 构造函数。
因此,在示例中打印 child1 对象时,会发现 Child.prototype 和 child1 均拥有一个名为 colors 的属性,其值为 ['red', 'blue', 'green']。
那么,如何进一步优化以避免这次重复调用呢?
如果我们不使用 Child.prototype = new Parent() ,而是通过间接方式让 Child.prototype 访问到 Parent.prototype 呢?
具体实现方式如下:
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
// 关键的三步
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
var child1 = new Child('kevin', '18');
console.log(child1);
最后,我们将此继承方法封装起来:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 当我们使用的时候:
prototype(Child, Parent);
引用《JavaScript高级程序设计》对寄生组合式继承的评价:
此种方式的高效性在于仅调用一次Parent构造函数,从而避免了在 Parent.prototype 上创建多余属性。同时,原型链得以维持,使得instanceof 与isPrototypeOf功能正常。业界视寄生组合式继承为引用类型最理想的继承范式。
至此,关于深入讲解JavaScript之继承的多种方式和优缺点的文章介绍完毕。更多相关JavaScript各种继承方式和优缺点内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!