JavaScript 的继承模型与 Java、C++ 这类语言差异很大,它没有传统的类拷贝机制,而是通过对象之间的原型引用实现行为共享。要理解原型链继承,关键不在于背诵 prototype 和 __proto__ 这两个单词,而在于搞清楚对象查找属性的完整路径。

原型链的底层结构:prototype 与 __proto__ 的分工
在 JavaScript 中,每个函数在创建时都会自动获得一个 prototype 属性,这个属性指向一个对象,也就是通常说的原型对象。原型对象里默认带有一个 constructor 属性,指回函数本身。通过 new 调用构造函数创建实例时,实例会隐藏地持有一个内部引用,在多数浏览器中通过 __proto__ 暴露出来,指向构造函数的 prototype 对象。因此,p.__proto__ === Person.prototype 会返回 true,但这只是语法层面的等价,真正标准的方法是 Object.getPrototypeOf(p)。
属性查找的规则可以这样理解:当读取 p.name 时,引擎先看 p 这个对象自身有没有 name 属性;如果有就直接返回,如果没有就沿着 __proto__ 指向的原型对象继续找。原型对象自身也可以有原型,于是会逐级向上,直到 Object.prototype。Object.prototype 的原型是 null,它就是这条链的终点,所以访问一个完全不存在的属性时,最终返回 undefined。
下面这段代码可以验证它们之间的关系:
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
};
const p = new Person('Alice');
console.log(Object.getPrototypeOf(p) === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
从输出可以看到,实例、构造函数原型、Object.prototype 以及 null 正好形成一条完整的查找链路。正是因为所有对象最终都会指向 Object.prototype,所以像 toString、hasOwnProperty 这样的方法才能被任意对象调用。理解这个底层结构后,再看各种继承方案就会清晰很多。
基于原型链的继承实现方式
最早期的原型链继承做法是直接让子类的原型对象等于父类的一个实例,例如 Child.prototype = new Parent()。这种写法确实可以让子类实例访问到父类原型上的方法,但存在两个明显问题:一是父类实例中的引用类型属性会被所有子类实例共享,修改一个实例的数组会影响到另一个实例;二是创建子类实例时无法在不影响其他实例的情况下向父类构造函数传参。正因如此,实际项目中很少单独使用这种继承方式。
为了解决引用类型共享和传参问题,借用构造函数技术被引入进来。它的核心思路是在子类构造函数内部使用 Parent.call(this, ...),把父类的属性复制一份到子类实例上。这样做虽然保证了每个实例拥有独立属性,但父类原型上的方法又无法复用了,因为子类原型没有指向父类原型。将两种思路组合起来,就得到了组合继承,这也是很长一段时间内 JavaScript 继承的主流写法。
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
const c1 = new Child('Tom', 5);
c1.colors.push('black');
console.log(c1.colors); // ['red', 'blue', 'black']
const c2 = new Child('Jerry', 6);
console.log(c2.colors); // ['red', 'blue']
组合继承的不足之处在于父类构造函数被调用了两次:一次是在设置 Child.prototype 时,另一次是在 Child 构造函数内部。这会带来一点性能损耗,并且父类实例的属性会冗余存在于子类原型上。针对这个问题,寄生组合继承通过 Object.create 直接创建一个以父类原型为原型的空对象,避免了多余的一次父类构造调用,成为 ES6 class 出现之前最理想的继承模式。
寄生组合继承的实现通常会把原型替换逻辑封装成一个工具函数,核心代码如下:
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inheritPrototype(Child, Parent);
Child.prototype.sayAge = function() {
console.log(this.age);
};
const child = new Child('Tom', 5);
child.sayName();
child.sayAge();
这段代码只调用了一次 Parent 构造函数,Child.prototype 上也不再包含多余的 name 和 colors 属性,继承关系依然保持正确。由此可见,原型链继承的重点并不在于记住某种固定写法,而是理解对象属性查找和构造函数调用时机。
常见误区与 class 语法糖的真相
使用原型链时,一个常见误区是过度依赖 instanceof 来判断类型。由于 instanceof 检查的是构造函数的 prototype 是否出现在实例的原型链上,跨 iframe 或跨执行上下文时可能会得到与预期不同的结果。更严格的做法是使用 Object.prototype.hasOwnProperty 来判断属性是自身拥有还是继承而来,尤其是在用 for...in 遍历对象时,继承的可枚举属性也会被遍历出来,所以通常需要加上 hasOwnProperty 过滤。
另一个容易被忽略的问题是原型链长度对性能的影响。属性查找是从实例自身开始逐级向上的,如果原型链层级过多,访问一个不存在属性的代价会随之升高。好在实际业务代码中原型链一般不会超过四五层,现代 JavaScript 引擎也做了大量优化,因此只要不刻意构建超长的原型链,通常不需要过度担心。真正值得注意的是不要随意修改内置对象原型,例如向 Object.prototype 添加方法,这会影响所有对象,容易引发不可预期的兼容问题。
ES6 引入的 class 语法让很多从其他语言转过来的开发者感到熟悉,但需要明确的是,class 并不是一套新的继承机制,它只是在原型链之上提供了一层更清晰的表达。下面的代码与前面的寄生组合继承在底层行为上是等价的:
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
const child = new Child('Tom', 5);
child.sayName(); // Tom
child.sayAge(); // 5
console.log(Object.getPrototypeOf(Child.prototype) === Parent.prototype); // true
从输出可以看出,Child.prototype 的原型指向 Parent.prototype,与手动实现的原型链继承完全一致。class 的 extends、super 关键字只是帮助开发者少写一些 Parent.call 和 Object.create 的样板代码,并不会改变 JavaScript 基于原型的本质。理解这一点之后,无论是维护旧代码中的组合继承,还是编写新代码中的 class 继承,都能更准确地判断属性和方法的来源,也能更快定位继承相关的异常。
JavaScript原型链原型继承构造函数修改时间:2026-10-03 15:43:50