JavaScript原型与继承实例:从构造函数到ES6 Class的演变
深入解析JavaScript原型与继承机制,通过详尽实例梳理从构造函数原型链、组合继承、寄生组合式继承到ES6 Class语法糖的演化历程与核心原理。
JavaScript 并没有传统基于类(Class-based)的面向对象模型,其底层全部依托原型链(Prototype Chain)实现对象间的属性和方法共享。从早期的构造函数搭配原型对象,到组合继承、寄生组合继承,再到 ECMAScript 2015(ES6)正式推出的 class 语法,其核心演进目标始终是:在消除原型共享污染与多次调用开销的前提下,用更清晰、健壮的语法完成对象属性继承与原型方法复用。本文通过具体的代码实例与内存结构拆解,带你完整掌握这一演化历程。
一、原型与原型链的核心三角关系
理解 JavaScript 继承的前提是理解构造函数、原型对象(prototype)以及实例对象(instance)之间的内在联系。在 JavaScript 中,只要创建了一个函数,解析器就会根据一组内置规则为该函数创建一个 prototype 属性,该属性指向一个包含共享属性与方法的对象,即原型对象。原型对象默认包含一个名为 constructor 的属性,指向该构造函数本身。
当通过 new 运算符实例化该构造函数时,实例内部会生成一个指向该原型对象的隐式指针(在 ECMAScript 规范中记为 [[Prototype]],在主流浏览器中暴露为 __proto__,规范推荐的访问方法为 Object.getPrototypeOf())。
// 基础构造函数与原型方法挂载
function Person(name, age) {
this.name = name; // 实例独享属性
this.age = age;
}
// 共享方法挂载在原型对象上
Person.prototype.sayHi = function() {
return "你好,我是 " + this.name;
};
const p1 = new Person("张三", 25);
console.log(p1.sayHi()); // "你好,我是 张三"
console.log(p1.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
当代码访问 p1.sayHi 时,引擎首先检查 p1 自身是否拥有该属性;如果不存在,则顺着 __proto__ 指针向 Person.prototype 查找;如果在 Person.prototype 上依然找不到,还会进一步沿着原型对象的 __proto__ 向 Object.prototype 查找,直到最顶层的 null 为止。这种层层递进的链条即为原型链。
二、早期继承模式的演变与缺陷剖析
为了让子类对象复用父类对象的属性和方法,开发者在 ECMAScript 规范标准化之前探索了多种继承手段。这一时期的典型尝试包括原型链继承、借用构造函数继承以及伪经典组合继承。
1. 原型链继承
原型链继承的基本做法是直接将子类的原型对象赋值为父类的一个实例:Child.prototype = new Parent()。
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {}
// 核心:子类原型指向父类实例
SubType.prototype = new SuperType();
const instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
const instance2 = new SubType();
console.log(instance2.colors); // ["red", "blue", "green", "black"]
致命缺陷:父类实例属性变成了子类原型上的共享属性。当属性是引用数据类型(如数组或对象)时,一个子类实例的修改会直接影响所有其他实例。此外,在实例化 SubType 时无法在不影响所有实例的情况下向 SuperType 的构造函数传递参数。
2. 借用构造函数继承(经典继承)
为了解决引用类型属性共享与传参问题,开发者借用 call 或 apply 在子类构造函数内部显式调用父类构造函数:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
function SubType(name, age) {
// 核心:在子类上下文中执行父类初始化
SuperType.call(this, name);
this.age = age;
}
const instance = new SubType("李四", 20);
console.log(instance.name); // "李四"
console.log(instance.age); // 20
缺陷:借用构造函数仅实现了实例属性的独立拷贝,但子类无法继承父类原型对象上定义的公共方法。如果所有方法都写在构造函数内,每次实例化都会重复创建函数对象,造成严重的内存浪费。
3. 伪经典组合继承
组合继承综合了原型链继承与借用构造函数的优点:使用原型链继承原型上的公共方法,使用借用构造函数继承实例属性。
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function() {
return this.name;
};
function SubType(name, age) {
// 第一次调用父类构造函数(创建实例属性)
SuperType.call(this, name);
this.age = age;
}
// 第二次调用父类构造函数(重写子类原型)
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
组合继承曾长期是 JavaScript 中最常用的继承模式,但它存在一个性能和结构上的隐患:父类构造函数被调用了两次。在 SubType.prototype 原型对象上,会残留一份无意义的父类实例属性(name: undefined, colors: [...]),虽然它们会被子类实例自身同名属性遮蔽(Shadowing),但仍然造成了内存占用与原型污染。
三、ES5 时代的标准解法:寄生组合式继承
为了解决组合继承中“调用两次父类构造函数”的缺陷,道格拉斯·克罗克福德(Douglas Crockford)等人提出了寄生组合式继承。该方案被《JavaScript 高级程序设计》誉为 ES5 引用类型继承的最佳实践。
其核心原理非常清晰:既然子类原型只需要指向父类原型以复用方法,为什么非要去实例化一个完整的父类对象呢?我们只需要创建一个干净的、以 Parent.prototype 为原型的空对象,将其赋值给 Child.prototype 即可。这正是 Object.create() 的内部机制。
通过一个封装函数集中处理原型继承与 constructor 指针修正,避免任何直接对父构造函数的非必要调用。
// 寄生组合继承的核心继承函数
function inheritPrototype(subType, superType) {
// 1. 创建父类原型的一个干净副本(避免 new SuperType())
const prototype = Object.create(superType.prototype);
// 2. 修正因重写原型而丢失的 constructor 指向
prototype.constructor = subType;
// 3. 将新创建的原型对象挂载到子类
subType.prototype = prototype;
}
// 完整使用示例
function Animal(name) {
this.name = name;
this.traits = ["eat", "sleep"];
}
Animal.prototype.speak = function() {
return this.name + " 正在发声";
};
function Dog(name, breed) {
// 属性继承:借用构造函数
Animal.call(this, name);
this.breed = breed;
}
// 原型方法继承:寄生组合
inheritPrototype(Dog, Animal);
// 挂载子类特有方法
Dog.prototype.bark = function() {
return this.name + " 正在汪汪叫";
};
const d1 = new Dog("旺财", "金毛");
console.log(d1.speak()); // "旺财 正在发声"
console.log(d1.bark()); // "旺财 正在汪汪叫"
console.log(d1 instanceof Dog); // true
console.log(d1 instanceof Animal); // true
四、ES6 Class 语法糖:底层的现代化封装
ECMAScript 2015(ES6)引入了 class 与 extends 关键字。很多从 Java 等语言转过来的开发者误以为 JavaScript 引入了全新的类继承模型,但实际上,ES6 的 Class 本质上依然是基于原型链的语法糖。
1. ES6 Class 继承实例
使用现代语法,前面的 Animal 与 Dog 可以重写为结构更加紧凑直观的形式:
class Animal {
constructor(name) {
this.name = name;
this.traits = ["eat", "sleep"];
}
speak() {
return `${this.name} 正在发声`;
}
// 静态方法(属于构造函数本身,非实例方法)
static getKingdom() {
return "动物界";
}
}
class Dog extends Animal {
constructor(name, breed) {
// 关键:在引用 this 前必须优先调用 super()
super(name);
this.breed = breed;
}
bark() {
return `${this.name} 正在汪汪叫`;
}
}
const dog = new Dog("大黄", "中华田园犬");
console.log(dog.speak()); // "大黄 正在发声"
console.log(dog.bark()); // "大黄 正在汪汪叫"
console.log(Dog.getKingdom()); // "动物界"(静态方法同样被自动继承)
2. super() 关键字的工作原理
在 ES5 的寄生组合继承中,继承机制是“先由子类创建 this 实例对象,然后再通过 Parent.call(this) 为其挂载父类的属性”。而在 ES6 的 extends 中,机制发生了根本改变:它是先将父类实例对象的属性方法加到 this 上(通过 super()),然后再用子类的构造函数修饰 this。因此,在子类的 constructor 中,未调用 super() 之前使用 this 会直接抛出 ReferenceError。
3. 双重继承链(静态方法继承)
ES6 Class 的 extends 实现了两条原型链:
- 实例原型链:
Dog.prototype.__proto__ === Animal.prototype,用于继承父类原型上的常规方法。 - 构造函数原型链:
Dog.__proto__ === Animal(相当于Object.setPrototypeOf(Dog, Animal)),用于继承父类的静态方法与静态属性。这在 ES5 手写继承中经常被忽略。
五、JavaScript 继承演变总结与工程建议
回顾从构造函数到 ES6 Class 的演化历程,每一次改进都在解决前人的工程痛点:
| 继承方案 | 属性继承方式 | 方法继承方式 | 父构造函数调用次数 | 主要不足 / 适用场景 |
|---|---|---|---|---|
| 原型链继承 | 原型共享 | 原型链查找 | 1 次 | 引用类型相互污染,无法传参。已废弃。 |
| 借用构造函数 | 独立拷贝 | 无方法继承 | 1 次 | 无法继承原型方法,内存开销大。已废弃。 |
| 组合继承 | 独立拷贝 | 原型链查找 | 2 次 | 原型产生无用属性镜像,微小性能浪费。旧项目可见。 |
| 寄生组合继承 | 独立拷贝 | Object.create 隔离 | 1 次 | ES5 规范下的黄金标准,Babel 编译产物的核心原理。 |
| ES6 Class | super() 构造 | extends 自动挂载 | 1 次 | 现代项目默认首选。包含静态继承、TDZ 安全检查。 |
什么时候该用JS构造器:与工厂模式、字面量的选择对比
2026-11-05 16:18:22
推荐几款轻量级JS照片墙开源库及对比
2026-11-05 15:56:19
JavaScript递归写法有哪些常见陷阱及避免方法
2026-11-05 14:53:38
JS数组浅拷贝与深拷贝怎么选?场景与方法总结
2026-11-05 14:41:02
p5.js 中 setup() 和 draw() 函数的执行机制详解
2026-11-05 12:32:38








