JavaScript继承机制:从伪类到函数化模式的深度解析
本文深入解析JavaScript的四种继承模式:伪类、对象说明符、原型继承及函数化构造。重点对比了基于原型的直接继承与通过构造函数模拟类继承的差异,详细阐述了函数化模式如何实现私有变量、特权方法及父类调用,并探讨了其在事件处理和防伪对象中的应用。
本文深入探讨JavaScript继承的核心特性及其在实际开发中的应用场景,旨在为开发者提供清晰的参考与指导。
继承本质上是代码复用的重要模式。JavaScript不仅支持模拟基于类的继承结构,还具备更多表达力强的其他模式。然而,在大多数情况下,保持实现简洁往往是最佳策略。
JavaScript属于基于原型的语言,这意味着对象可以直接继承自其他对象,无需经过中间层。
1 伪类模式
JavaScript的原型机制并非让对象直接继承另一个对象,而是引入了一层间接层:通过构造函数来生成对象实例。
当函数被创建时,Function构造器生成的函数对象会执行类似以下的代码逻辑:
this.prototype = {constructor : this};
新的函数对象会新增一个名为prototype的属性,该属性是一个包含constructor属性且值指向该新函数的对象。
当使用构造器调用模式,即通过new关键字调用函数时,其执行过程如下:
Function.method('new', function (){
var that = Object.create(this.prototype);//创建一个继承了构造器函数的原型对象的新对象
var other = this.apply(that, arguments);//调用构造器函数,绑定 this 到新对象
return (typeof other === 'object' && other) || that;//如果构造器函数的返回值不是对象,就直接返回这个新对象
});
我们可以定义一个构造函数,并在此基础上扩展其原型方法:
//定义构造器并扩充原型
var Mammal = function (name) {
this.name = name;
};
Mammal.prototype.get_name = function () {
return this.name;
};
Mammal.prototype.says = function () {
return this.saying || '';
};
随后,通过该构造函数创建实例:
var myMammal = new Mammal('Herb the mammal');
console.log(myMammal.get_name());//Herb the mammal
接着定义另一个伪类来继承Mammal(即定义构造函数并替换其prototype属性):
var Cat = function (name) {
this.name = name;
this.saying = 'meow';
};
Cat.prototype = new Mammal();
进一步扩展原型以添加新方法:
Cat.prototype.purr = function (n) {
var i, s = '';
for (i = 0; i < n; i += 1) {
if (s) {
s += '-';
}
s += 'r';
}
return s;
};
Cat.prototype.get_name = function () {
return this.says() + ' ' + this.name + ' ' + this.says();
};
var myCat = new Cat('Henrietta');
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow
我们利用method方法定义了inherits方法,从而封装并隐藏了上述复杂的继承细节:
/**
* 为 Function.prototype 新增 method 方法
* @param name 方法名称
* @param func 函数
* @returns {Function}
*/
Function.prototype.method = function (name, func) {
if (!this.prototype[name])//没有该方法时,才添加
this.prototype[name] = func;
return this;
};
Function.method('inherits', function (Parent) {
this.prototype = new Parent();
return this;
});
这两个方法均返回this,从而支持链式编程风格。
var Cat = function (name) {
this.name = name;
this.saying = 'meow';
}.inherits(Mammal).method('purr', function (n) {
var i, s = '';
for (i = 0; i < n; i += 1) {
if (s) {
s += '-';
}
s += 'r';
}
return s;
}).method('get_name', function () {
return this.says() + ' ' + this.name + ' ' + this.says();
});
var myCat = new Cat('Henrietta');
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow
尽管我们拥有了行为类似“类”的构造函数,但这类模式缺乏私有环境,所有属性均为公开状态,且无法直接访问父类的方法。
若在调用构造函数时遗漏new前缀,this将不会被绑定到新对象上,而是错误地绑定到全局变量。这导致新对象未被正确扩充,同时破坏了全局变量环境。
这被视为一种严重的语言设计缺陷。为降低此类错误发生的概率,约定所有构造函数均以首字母大写命名。这样,通过函数名即可识别其为构造函数。
当然,更优的策略是避免使用构造函数模式。
2 对象说明符模式
有时,构造函数需要接收大量参数,这会增加代码复杂度。因此,在编写构造函数时,使其接受一个包含配置信息的对象说明符是更好的做法:
var myObject = maker({
first: f,
middle: m,
last: l
});
此时,参数可以按任意顺序传递,且构造函数能智能地为未传入的参数应用默认值,从而提升代码的可读性。
3 原型模式
基于原型的继承意味着新对象可以直接继承旧对象的属性。首先构建一个基础对象,随后可以创建更多与该对象相似的新对象。
/**
* 原型
*/
var myMammal = {
name: 'Herb the mammal',
get_name: function () {
return this.name;
},
says: function () {
return this.saying || '';
}
};
//创建新实例
var myCat = Object.create(myMammal);
myCat.name = 'Henrietta';
myCat.saying = 'meow';
myCat.purr = function (n) {
var i, s = '';
for (i = 0; i < n; i += 1) {
if (s) {
s += '-';
}
s += 'r';
}
return s;
};
myCat.get_name = function () {
return this.says() + ' ' + this.name + ' ' + this.says();
};
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow
此处使用了create方法来生成新的实例:
Object.create = function (o) {
var F = function () {
};
F.prototype = o;
return new F();
}
4 函数化模式
前文所述的继承模式存在一个主要问题:无法保护隐私,对象的所有属性均可见。部分开发者采用伪装私有的方式,即为私有属性起一个晦涩的名称,期望其他开发者忽略它们。
实际上,有更优的解决方案:应用模块模式。
我们先构建一个用于生成对象的函数,该函数包含以下步骤: ①. 创建新对象。实现方式有四种: 【1】构建对象字面量。 【2】调用构造函数。 【3】基于已存在对象构建新实例。 【4】调用任意返回对象的函数。 ②. 定义私有实例变量与方法。 ③. 为新对象扩展特权方法,这些方法有权访问上述私有变量。 ④. 返回新对象。
函数化构造器的伪代码结构如下:
var constructor = function (spec, my){
var that, 其他私有变量;
my = my || {};
//把共享的变量和函数添加到 my 中
that = 一个新对象
//添加给 that 的特权方法
return that;
};
spec对象包含了构建新实例所需的所有信息,可用于访问私有变量或其他函数。 my对象为继承链中的构造函数提供了共享容器;若未传入,则会自动创建一个新的my对象。
创建特权方法的方式是:先将函数定义为私有方法,随后将其赋值给that对象:
var methodical = function (){
...
};
that.methodical = methodical;
这种分两步定义方式的优势在于:私有的methodical方法不受实例后续变更的影响。
现在,我们将此模式应用于mammal示例中:
var mammal = function (spec) {
var that = {};
that.get_name = function () {
return spec.name;
};
that.says = function () {
return spec.saying || '';
};
return that;
};
var myMammal = mammal({name: 'Herb'});
console.log(myMammal.get_name());//Herb
var cat = function (spec) {
spec.saying = spec.saying || 'meow';
var that = mammal(spec);
that.purr = function (n) {
var i, s = '';
for (i = 0; i < n; i += 1) {
if (s) {
s += '-';
}
s += 'r';
}
return s;
};
that.get_name = function () {
return that.says() + ' ' + spec.name + ' ' + that.says();
};
return that;
};
var myCat = cat({name: 'Henrietta'});
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow
函数化模式还能调用父类的方法。此处我们构建一个superior方法,该方法返回调用特定方法名的函数:
//返回调用某个方法名的函数
Object.method('superior', function (name) {
var that = this,
method = that[name];
return function () {
return method.apply(that, arguments);
};
});
接着创建coolcat对象,其拥有能够调用父类方法的get_name函数:
var coolcat = function (spec) {
var that = cat(spec),
super_get_name = that.superior('get_name');
that.get_name = function (n) {
return 'like ' + super_get_name() + ' baby';
};
return that;
};
var myCoolCat = coolcat({name: 'Bix'});
console.log(myCoolCat.get_name());//like meow Bix meow baby
函数化模式具备极高的灵活性,能更好地实现封装、信息隐藏以及访问父类方法的能力。
若对象的所有状态均为私有,则称为防伪对象。该对象的属性可被替换或删除,但其内部状态不受影响。若使用函数化模式创建对象,且所有方法均不使用this或that,则该对象具有持久性,不会被外部入侵。除非存在特权方法,否则无法访问该持久性对象的内部状态。
5 事件处理函数
我们可以构建一个函数,用于为任何对象添加简单的事件处理特性。此处,我们为对象添加了on方法、fire方法以及私有的事件注册对象:
var eventuality = function (that) {
var registry = {};
/**
* 触发事件
*
* 使用 'on' 方法注册的事件处理程序将被调用
* @param 可以是包含事件名称的字符串,或者是一个拥有 type 属性(值为事件名称)的对象。
*/
that.fire = function (event) {
var array,
func,
handler,
i,
type = typeof event === 'string' ? event : event.type;
//如果这个事件已被注册,则遍历并依序执行
if (registry.hasOwnProperty(type)) {
array = registry[type];
for (i = 0; i < array.length; i += 1) {
handler = array[i];//处理程序包含一个方法和一组可选的参数
func = handler.method;
if (typeof func === 'string') {//如果方法是字符串形式的名称,则寻找它
func = this[func];
}
//调用它。如果处理程序包含参数,则传递过去,否则就传递事件对象
func.apply(this, handler.parameters || [event]);
}
}
return this;
};
/**
* 注册一个事件
* @param type
* @param method
* @param parameters
*/
that.on = function (type, method, parameters) {
var handler = {
method: method,
parameters: parameters
};
if (registry.hasOwnProperty(type)) {//如果已存在,就新增数组项
registry[type].push(handler);
} else {//新增
registry[type] = [handler];
}
return this;
};
return that;
};
可以在任何单独对象上调用eventuality函数,从而赋予其事件处理方法。也可以在构造函数返回that之前,在构造函数内部调用它:
eventuality(that);
JavaScript的弱类型特性在此成为巨大优势,因为我们无需处理对象继承关系中的类型检查问题。
感兴趣的朋友还可以使用本站在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行结果。
更多关于JavaScript相关内容还可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。
ES6模块化中js文件怎么用import和export交互
2026-11-05 17:00:48
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53
