JavaScript 是一门基于对象(Object-based)的语言,但在很长一段时间里,它并没有像 Java 或 C++ 那样完善的类(Class)机制。这导致初学者在处理对象的生成与继承时,往往会感到困惑。也不能怪大家,因为在JavaScript在创造之初,本来就是一个赶鸭子上架的kpi项目,创造者为了可以快速完成,更多的是考虑它的便捷,简单易上手(对比其他语言这是不得不承认的一点),这就是为什么大多数人在刚学习这门语言时,会有很强的不适应性,也是为什么JavaScript之前有重大更新的原因之一
本文将剥开语法的表象,沿着 JavaScript 语言设计的演进脉络,从最原始的对象字面量出发,一步步推导至现代的 ES6 Class 语法,并重点解析原型链继承的核心机制。
第一阶段:混沌初开(对象字面量)
在最早期,如果我们想要描述一只猫,我们可能会直接创建一个对象。这是最直观的方式,被称为“对象字面量”。
JavaScript
// 描述一只猫
var cat1 = {
name: "加菲猫",
color: "橘色"
};
// 描述另一只猫
var cat2 = {
name: "小白",
color: "白色"
};
这种写法虽然简单,但在实际开发中存在两个严重的痛点:
- 代码冗余:如果我们需要创建 100 只猫,就必须重复写 100 次结构相同的代码。
- 缺乏类型关联:cat1 和一个描述用户的对象 user1 在结构上没有任何本质区别,我们无法从代码层面看出它们属于同一个“类别”(即缺乏模板的概念)。
第二阶段:工业化生产(构造函数模式)
为了解决代码重复和缺乏模板的问题,我们可以借鉴工厂模式的思维,使用函数来封装实例化的过程。在 JavaScript 中,这种函数被称为构造函数。按照开发约定,构造函数的首字母应当大写。
JavaScript
// 定义一个“猫”的构造函数(类模板)
function Cat(name, color) {
// 这里的 this 指向即将在 new 过程中创建的新对象
this.name = name;
this.color = color;
// 这是一个实例方法
this.eat = function() {
console.log("吃东西");
}
}
// 使用 new 关键字实例化对象
var cat1 = new Cat("加菲猫", "橘色");
var cat2 = new Cat("黑猫警长", "黑色");
console.log(cat1.constructor === cat2.constructor); // true
核心知识点:new 关键字到底做了什么?
当我们在调用函数前加上 new 关键字时,JavaScript 引擎会在后台执行以下操作:
- 创建一个全新的空对象。
- 将构造函数中的 this 指向这个新对象。
- 执行构造函数中的代码(为这个新对象添加属性)。
- 返回这个新对象。
痛点分析:
虽然构造函数解决了属性的封装问题,但它带来了一个严重的内存浪费问题。在上面的代码中,eat 方法被定义在构造函数内部。这意味着,每当我们 new 一个实例,都会在内存中重新创建一遍 eat 函数。如果有成千上万个实例,这将消耗大量不必要的内存。
第三阶段:共享经济(原型模式 Prototype)
为了解决方法重复创建的问题,JavaScript 引入了 prototype(原型) 机制。我们将特有的属性(如名字、颜色)放在构造函数中,而将公有的属性和方法放在原型对象上。
JavaScript
function Cat(name, color) {
this.name = name;
this.color = color;
}
// 将公共方法挂载到原型上
// 所有 Cat 的实例将共享同一个 eat 方法,不会重复创建
Cat.prototype.type = "猫科动物";
Cat.prototype.eat = function() {
console.log("eat jerry");
}
var cat1 = new Cat("Tom", "蓝色");
// 验证属性来源
console.log(cat1.hasOwnProperty("name")); // true,这是实例自己的属性
console.log(cat1.hasOwnProperty("type")); // false,这是原型上的属性
console.log("type" in cat1); // true,in 操作符会查找原型链
核心辨析
- constructor:原型对象上有一个 constructor 属性,指向构造函数本身。这让我们知道 cat1 是由 Cat 生产出来的。
- hasOwnProperty:用于区分一个属性是属于实例自身,还是继承自原型。
- 原型链查找原则:当我们访问 cat1.eat() 时,引擎首先在 cat1 自身查找;如果没有找到,就会顺着 proto 找到 Cat.prototype 上的 eat 方法。
第四阶段:继承的难题(核心重点)
在面向对象编程中,继承是最核心但也最复杂的概念。假设我们有一个父类 Animal,如何让 Cat 继承 Animal 的属性和方法?
JavaScript
// 父类
function Animal() {
this.species = '动物';
}
Animal.prototype.sayHi = function() {
console.log('我是动物');
}
尝试一:借用构造函数(继承属性)
我们可以利用 call 或 apply 方法,在子类构造函数中调用父类构造函数,并强行绑定 this。
JavaScript
function Cat(name, color) {
// 核心操作:将 Animal 的 this 指向当前的 Cat 实例
// 这样 Cat 实例就拥有了 species 属性
Animal.call(this);
this.name = name;
this.color = color;
}
var cat = new Cat("加菲猫", "橘色");
console.log(cat.species); // "动物" —— 属性继承成功
// cat.sayHi(); // 报错!—— 方法继承失败
缺陷:这种方式只能继承父类构造函数内部定义的属性,无法访问父类原型(Animal.prototype)上定义的方法。
尝试二:原型链继承(继承方法)
为了继承方法,我们需要将子类的原型指向父类的一个实例。
JavaScript
// 核心操作:打通原型链
// Cat.prototype 变成了 Animal 的一个实例
// 所以 Cat 的实例可以访问 Animal.prototype 上的方法
Cat.prototype = new Animal();
// 修正 constructor 指向(虽然不修正也能跑,但为了严谨性建议修正)
Cat.prototype.constructor = Cat;
var cat = new Cat("加菲猫", "橘色");
cat.sayHi(); // "我是动物" —— 方法继承成功
终极方案:组合继承
在 ES6 之前,最标准的继承方式是结合上述两种方法:在构造函数中借用 call 继承属性,利用原型链继承方法。
JavaScript
function Cat(name, color) {
// 1. 继承属性
Animal.call(this);
this.name = name;
this.color = color;
}
// 2. 继承方法
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat = new Cat("Tom", "Blue");
// 既有属性,又有方法,完美!
第五阶段:返璞归真(ES6 Class 语法糖)
到了 ES6,JavaScript 终于引入了 class 和 extends 关键字,让写法看起来更像传统的面向对象语言。
JavaScript
// 现代写法
class Cat extends Animal {
constructor(name, color) {
super(); // 相当于 Animal.call(this)
this.name = name;
this.color = color;
}
eat() {
console.log("eat jerry");
}
}
const cat1 = new Cat('tom', '蓝色');
cat1.eat();
本质揭秘:只是语法糖
千万不要被 class 迷惑。在底层,JavaScript 依然是基于原型的语言。我们可以通过打印代码来验证这一点:
JavaScript
console.log(typeof Cat); // "function" —— Cat 本质上还是一个函数
console.log(cat1.__proto__ === Cat.prototype); // true —— 原型机制依然存在
console.log(cat1.__proto__.constructor === Cat); // true
ES6 的 class 只是将我们在第四阶段手动实现的“组合继承”逻辑进行了语法层面的封装,使其更易读、更易写。
总结
从对象字面量的复制粘贴,到构造函数的封装,再到利用原型链实现资源共享与继承,JavaScript 的 OOP 之路展示了其独特的设计哲学。
理解原型链不仅是为了应对面试,更是为了深刻理解 JavaScript 对象系统的本质:它不是通过复制模板来生成对象,而是通过“链条”将对象关联起来,实现属性和方法的查找与复用。