JavaScript原型链继承机制到底是如何实现的?

来源:站长素材作者:长沙GEO公司头衔:草根站长
导读:本期聚焦于长沙GEO公司创作的《JavaScript原型链继承机制到底是如何实现的?》,敬请观看详情。每个JavaScript对象内部都维护着一个指向另一个对象的隐藏引用,习惯上把它叫做原型指针。当访问一个属性时,引擎先在对象自身查找,找不到就顺着这个指针去原型对象上找,再找不到就继续往上层原型找,一直查到Object.prototype,最后到达null,这条链路就是原型链。构造函数、prototype属性和实例的__proto__属性分别扮演不同角色:构造函数的prototype指向原型对象,实例的__proto__也指向同一个原型对象,因此实例可以共享原型上的方法。理解了这一点,就能明白为什么直接赋值原型会引发引用类型共享,为什么组合继承可以同时解决属性独立和方法复用,以及class语法为什么本质上只是原型链的一种更清晰的书写方式。需要注意的是,原型链越长属性查找越慢,判断自身属性时应使用hasOwnProperty来避免误读继承来的值。

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

JavaScript原型链继承机制到底是如何实现的?

原型链的底层结构: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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1003/65162.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。