本文深入解析 JavaScript 作为基于对象语言的独特性,阐述如何通过构造函数与 prototype 原型链模拟传统面向对象中的类与继承。文章详细拆解 new 关键字的执行流程,分析基于原型链继承的常见缺陷,并列举了 Douglas Crockford、John Resig 等知名开发者提供的经典继承实现方案,为后续深入探讨奠定基础。
面向对象与基于对象 绝大多数开发人员都具备使用 C++、C# 或 Java 等传统面向对象语言的开发经验。在这些语言体系中,类(Class)与实例(Instance)是两个核心概念:类用于定义一类事物的公共行为和方法,而实例则是类的具体实现。此外,封装、继承和多态被视为面向对象编程的三大支柱。 然而,在 JavaScript 的语境下,上述特性似乎并不直接存在。JavaScript 并非严格意义上的面向对象语言,而是一种基于对象的语言。其显著特征在于万物皆对象,包括字符串、数组、日期、数字乃至函数。例如:
// 定义一个函数 - add
function add(a, b) {
add.invokeTimes++;
return a + b;
}
// 因为函数本身也是对象,这里为函数add定义一个属性,用来记录此函数被调用的次数
add.invokeTimes = 0;
add(1 + 1);
add(2 + 3);
console.log(add.invokeTimes); // 2
模拟JavaScript中类和继承 在传统面向对象语言中,开发者通过类来创建自定义对象。鉴于 JavaScript 中所有事物均为对象,我们需要寻找一种替代机制来构建自定义对象。为此,引入了原型(prototype)这一概念。可以将 prototype 视为一个模板,新创建的自定义对象均被视为该模板的一个拷贝(实际上,内部并非真正的拷贝,而是建立了一个不可见的链接,从而在视觉和逻辑上呈现出拷贝的效果)。 通过 prototype 创建自定义对象的示例如下:
// 构造函数
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
// 定义Person的原型,原型中的属性可以被自定义对象引用
Person.prototype = {
getName: function() {
return this.name;
},
getSex: function() {
return this.sex;
}
}
在此示例中,我们将函数 Person 称为构造函数,即用于创建自定义对象的函数。可以看出,JavaScript 通过结合构造函数与原型的方式,成功模拟实现了传统面向对象语言中类的功能。 创建自定义对象(即实例化类)的代码如下:
var zhang = new Person("ZhangSan", "man");
console.log(zhang.getName()); // "ZhangSan"
var chun = new Person("ChunHua", "woman");
console.log(chun.getName()); // "ChunHua"
当执行代码 var zhang = new Person("ZhangSan", "man") 时,JavaScript 引擎内部依次执行以下操作: 1. 创建一个空白对象(等效于 new Object())。 2. 将 Person.prototype 中的属性(键值对)复制到该空对象中(如前所述,内部实现实为隐藏链接而非物理拷贝)。 3. 将该对象通过 this 关键字传递给构造函数,并执行构造函数逻辑。 4. 将最终的对象赋值给变量 zhang。 为了验证 prototype 模板并非被物理拷贝到实例化对象中,而是通过链接方式工作,请参考以下代码:
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
Person.prototype.age = 20;
var zhang = new Person("ZhangSan", "man");
console.log(zhang.age); // 20
// 覆盖prototype中的age属性
zhang.age = 19;
console.log(zhang.age); // 19
delete zhang.age;
// 在删除实例属性age后,此属性值又从prototype中获取
console.log(zhang.age); // 20
这种在 JavaScript 内部实现的隐藏 prototype 链接,构成了 JavaScript 运行的基础环境,也是模拟实现继承机制的核心基石。 那么,如何在 JavaScript 中实现简单的继承? 以下示例展示了创建一个雇员类 Employee 的过程,该类从 Person 类继承了原型(prototype)中的所有属性:
function Employee(name, sex, employeeID) {
this.name = name;
this.sex = sex;
this.employeeID = employeeID;
}
// 将Employee的原型指向Person的一个实例
// 因为Person的实例可以调用Person原型中的方法, 所以Employee的实例也可以调用Person原型中的所有属性。
Employee.prototype = new Person();
Employee.prototype.getEmployeeID = function() {
return this.employeeID;
};
var zhang = new Employee("ZhangSan", "man", "1234");
console.log(zhang.getName()); // "ZhangSan
上述关于继承的实现方式较为粗糙,且存在若干问题: 1. 在定义 Employee 构造函数和原型(以下简称类)时,直接对 Person 进行了实例化,这种做法并不恰当。 2. Employee 的构造函数无法调用父类 Person 的构造函数,导致在 Employee 构造函数中对 name 和 sex 属性进行重复赋值。 3. Employee 中的方法会覆盖 Person 中的同名方法,缺乏重载机制(此问题与上一条相关)。 4. 创建 JavaScript 类的语法显得零散,不如 C#/Java 等语言的语法优雅。 5. 实现中存在 constructor 属性指向错误的问题,这将在第二篇文章中详细讨论。 我们将在后续章节完善此示例。
JavaScript继承的实现
由于 JavaScript 本身缺乏完整的类和继承机制,且前文所述的手工实现方式存在诸多弊端,因此针对这一具有挑战性的任务,社区中已涌现出多种实现方案:
- Douglas Crockford - Prototypal Inheritance in JavaScript
- Douglas Crockford - Classical Inheritance in JavaScript
- John Resig - Simple JavaScript Inheritance
- Dean Edwards - A Base Class for JavaScript Inheritance
- Prototype
- Mootools
- Extjs
本系列文章将逐一深入分析上述各种实现方案,旨在最终全面理解 JavaScript 中类和继承的实现原理。 下一章将介绍类实现中的相关知识,包括 this、constructor、prototype 等核心概念。