深入理解JavaScript代码复用:六种常见模式的缺陷分析

作者:闭包旅人

时间:2026-10-29

浏览:222

本文深入分析JavaScript中六种常见的代码复用与继承模式,包括默认模式、借用构造函数、组合模式、共享原型、临时构造函数及klass模式。通过具体代码示例,详细阐述每种模式在参数传递、原型链维护、执行效率及语法规范方面的缺陷,旨在帮助开发者识别并避免这些潜在问题,选择更稳健的面向对象编程方案。

文章目录

引言

介绍

编程的核心诉求之一是实现代码复用,否则每次开发新功能都需从头编写,效率将极低。然而,代码复用是一把双刃剑,可能引入副作用。接下来的两篇文章将专门探讨代码复用:第一篇为“避免篇”,重点分析那些存在潜在问题的模式,建议尽量避免使用;第二篇为“推荐篇”,介绍稳健且无副作用的推荐模式。

模式1:默认模式

模式1:默认模式

这是代码复用中最常见的默认做法,但往往存在隐患。该模式通过调用父构造函数Parent()创建对象,并将其赋值给子构造函数Child()的原型。具体实现代码如下:

代码如下:

function inherit(C, P) {
C.prototype = new P();
}
// 父构造函数
function Parent(name) {
 this.name = name || 'Adam';
}
// 给原型添加say功能
Parent.prototype.say = function () {
 return this.name;
};
// Child构造函数为空
function Child(name) {
}
// 执行继承
inherit(Child, Parent);
var kid = new Child();
console.log(kid.say()); // "Adam"
var kiddo = new Child();
kiddo.name = "Patrick";
console.log(kiddo.say()); // "Patrick"
// 缺点:不能让参数传进给Child构造函数
var s = new Child('Seth');
console.log(s.say()); // "Adam"

该模式的主要缺陷在于无法向Child构造函数传递参数。由于Child构造函数为空,传入的参数被忽略,导致实例化对象时无法正确初始化属性,这使得该模式在实际应用中基本失效。

模式2:借用构造函数

模式2:借用构造函数

此模式通过调用Child构造函数并借用Parent的构造函数进行apply操作,将Child的this和参数传递给apply方法。具体代码如下:

代码如下:

// 父构造函数
function Parent(name) {
this.name = name || 'Adam';
}
// 给原型添加say功能
Parent.prototype.say = function () {
 return this.name;
};
// Child构造函数
function Child(name) {
 Parent.apply(this, arguments);
}
var kid = new Child("Patrick");
console.log(kid.name); // "Patrick"
// 缺点:没有从构造函数上继承say方法
console.log(typeof kid.say); // "undefined"

该模式的缺点同样明显:虽然成功传递了参数,但由于未建立原型链连接,Child实例无法访问Parent原型上定义的方法(如say方法),导致功能缺失。

模式3:借用构造函数并设置原型

模式3:借用构造函数并设置原型

为了克服上述两种模式的缺陷,尝试将两者结合:既借用构造函数以传递参数,又设置原型以继承方法。具体实现如下:

代码如下:

// 父构造函数
function Parent(name) {
this.name = name || 'Adam';
}
// 给原型添加say功能
Parent.prototype.say = function () {
 return this.name;
};
// Child构造函数
function Child(name) {
 Parent.apply(this, arguments);
}
Child.prototype = new Parent();
var kid = new Child("Patrick");
console.log(kid.name); // "Patrick"
console.log(typeof kid.say); // function
console.log(kid.say()); // Patrick
console.dir(kid);
delete kid.name;
console.log(kid.say()); // "Adam"

虽然该方案在功能上正常运行,但存在一个显著问题:Parent构造函数被执行了两次。一次是在Child构造函数内部通过apply调用,另一次是通过new Parent()设置原型时调用。这导致不必要的性能开销,效率较低。

模式4:共享原型

模式4:共享原型

共享原型模式让Child和Parent直接共享同一个原型对象。具体代码如下:

代码如下:

function inherit(C, P) {
C.prototype = P.prototype;
}
// 父构造函数
function Parent(name) {
 this.name = name || 'Adam';
}
// 给原型添加say功能
Parent.prototype.say = function () {
 return this.name;
};
// Child构造函数
function Child(name) {
}
inherit(Child, Parent);
var kid = new Child('Patrick');
console.log(kid.name); // undefined
console.log(typeof kid.say); // function
kid.name = 'Patrick';
console.log(kid.say()); // Patrick
console.dir(kid);

该模式同样存在Child无法正确接收参数的问题。由于Child构造函数为空,实例化时无法初始化name属性,导致name为undefined,除非手动赋值。

模式5:临时构造函数

模式5:临时构造函数

该模式首先借用构造函数,然后将Child的原型设置为一个临时构造函数的实例,最后恢复Child原型的构造函数。具体代码如下:

代码如下:

/* 闭包 */
var inherit = (function () {
var F = function () {
    };
return function (C, P) {
F.prototype = P.prototype;
C.prototype = new F();
C.uber = P.prototype;
C.prototype.constructor = C;
    }
} ());
function Parent(name) {
 this.name = name || 'Adam';
}
// 给原型添加say功能
Parent.prototype.say = function () {
 return this.name;
};
// Child构造函数
function Child(name) {
}
inherit(Child, Parent);
var kid = new Child();
console.log(kid.name); // undefined
console.log(typeof kid.say); // function
kid.name = 'Patrick';
console.log(kid.say()); // Patrick
var kid2 = new Child("Tom");
console.log(kid.say()); 
console.log(kid.constructor.name); // Child
console.log(kid.constructor === Parent); // false

尽管该模式试图解决参数传递和原型继承问题,但Child仍无法正常接收参数。由于Child构造函数未调用Parent.apply,实例化时无法初始化属性,导致name为undefined。

模式6:klass

模式6:klass

该模式提供了一种更复杂的继承机制,代码如下:

代码如下:

var klass = function (Parent, props) {
var Child, F, i;
// 1.
 // 新构造函数
 Child = function () {
 if (Child.uber && Child.uber.hasOwnProperty("__construct")) {
 Child.uber.__construct.apply(this, arguments);
 }
 if (Child.prototype.hasOwnProperty("__construct")) {
 Child.prototype.__construct.apply(this, arguments);
 }
 };
// 2.
 // 继承
 Parent = Parent || Object;
 F = function () {
 };
 F.prototype = Parent.prototype;
 Child.prototype = new F();
 Child.uber = Parent.prototype;
 Child.prototype.constructor = Child;
// 3.
 // 添加实现方法
 for (i in props) {
 if (props.hasOwnProperty(i)) {
 Child.prototype[i] = props[i];
 }
 }
// return the "class"
 return Child;
};
var Man = klass(null, {
 __construct: function (what) {
 console.log("Man's constructor");
 this.name = what;
 },
 getName: function () {
 return this.name;
 }
});
var first = new Man('Adam'); // logs "Man's constructor"
first.getName(); // "Adam"
var SuperMan = klass(Man, {
 __construct: function (what) {
 console.log("SuperMan's constructor");
 },
 getName: function () {
 var name = SuperMan.uber.getName.call(this);
 return "I am " + name;
 }
});
var clark = new SuperMan('Clark Kent');
clark.getName(); // "I am Clark Kent"
console.log(clark instanceof Man); // true
console.log(clark instanceof SuperMan); // true

该模式语法复杂,规范与其他语言类似,但可读性和易用性较差,开发者需谨慎评估是否采用。

总结

总结

上述六种模式虽然在特定场景下实现了部分功能,但各自存在明显缺陷,如参数传递失败、原型链断裂、性能低下或语法复杂。因此,在一般开发中,建议避免使用这些模式,转而采用更稳健的继承方案。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章