如何深入理解JavaScript原型与原型链继承机制?

来源:站长平台作者:缅甸程序员头衔:程序员
导读:本期聚焦于缅甸程序员创作的《如何深入理解JavaScript原型与原型链继承机制?》,敬请观看详情。为什么普通对象能直接调用toString、hasOwnProperty这些方法?它们并不在对象自身定义,答案藏在原型链里。本文从prototype、constructor、__proto__三个关键属性入手,梳理对象属性查找的完整路径,并对比原型链继承、构造函数继承、组合继承、寄生组合继承等方案的优缺点。同时还会剖析instanceof和Object.create的真实原理,以及ES6 class语法与原型继承之间的对应关系。理解这条隐藏的委托链,你会发现类继承不过是原型链的语法糖,而原型链才是JavaScript对象模型的真正核心。

想真正理解JavaScript的对象模型,绕不开原型与原型链。即便你每天都在用class和extends,如果说不清一个对象为什么能访问自己没有的方法,遇到继承相关的性能问题和调试难题时就会非常被动。下面从函数与对象之间的隐藏联系开始,逐步拆解原型链的工作方式。

如何深入理解JavaScript原型与原型链继承机制?

一、原型对象与三个关键属性

在JavaScript中,每一个函数被创建时都会自动获得一个prototype属性,这个属性指向一个对象,通常称为原型对象。默认情况下,原型对象内部只有一个constructor属性,并且该属性指回构造函数本身。通过构造函数创建实例时,实例的内部属性[[Prototype]]会指向构造函数的prototype。浏览器环境提供了__proto__访问器来暴露这个内部指针,不过更规范的做法是使用Object.getPrototypeOf。

下面这段代码演示了prototype、constructor和__proto__之间的关系。理解这个三角形结构是掌握原型链的第一步。

function Person(name) {
  this.name = name;
}
console.log(Person.prototype.constructor === Person); // true
var p = new Person('Alice');
console.log(Object.getPrototypeOf(p) === Person.prototype); // true
console.log(p.__proto__ === Person.prototype); // true(浏览器环境)
console.log(p.constructor === Person); // true

需要注意的是,constructor属性并不是实例自身的属性,而是通过原型链找到的。如果我们手动重写了构造函数的prototype对象,但没有重新指定constructor,那么实例的constructor可能指向Object而不是原构造函数。这也是很多代码中会出现SubType.prototype.constructor = SubType的原因。

二、属性查找:原型链如何决定取值与赋值

当读取一个对象的属性时,JavaScript引擎会先检查对象自身的可枚举属性,如果没找到,就沿着[[Prototype]]向上查找,直到找到属性或者到达原型链的末端null。这条由对象、原型对象、原型的原型串联起来的路径,就是原型链。所有普通对象最终都会指向Object.prototype,而Object.prototype的原型是null,因此像toString、hasOwnProperty这些方法能被任何对象调用。

赋值操作的行为与读取不同。如果给一个对象设置一个它自身没有的属性,JavaScript会直接在对象上创建该属性,而不会去修改原型链上的同名属性。但如果原型上的属性是一个引用类型,并且实例直接修改了这个引用类型的内部结构,那么所有共享该原型的实例都会受到影响,这是早期原型链继承中非常典型的坑。

function Animal() {}
Animal.prototype.type = 'mammal';
var a1 = new Animal();
var a2 = new Animal();
console.log(a1.type); // 'mammal'
a1.type = 'bird';
console.log(a1.type, a2.type); // 'bird' 'mammal'
delete a1.type;
console.log(a1.type); // 'mammal' 再次沿原型链查找

区分属性是实例自身的还是从原型链继承的,可以使用hasOwnProperty方法。它只会检查对象自身,不会沿原型链查找。而in操作符则不同,只要属性在原型链上存在就会返回true。这种差异在遍历对象属性时尤为重要,配合Object.keys和for...in可以避免误操作原型成员。

三、六种继承方案对比与实现

理解了原型链的查找机制后,再来分析继承的几种实现方式。原型链继承是最直接的一种:把子类的prototype设置为父类实例。这样做能让子类实例访问父类原型上的方法,但父类实例自身的引用类型属性会被所有子类实例共享,而且子类构造函数无法向父类传递参数。

function SuperType() {
  this.colors = ['red', 'blue'];
}
function SubType() {}
SubType.prototype = new SuperType();
var s1 = new SubType();
s1.colors.push('green');
var s2 = new SubType();
console.log(s2.colors); // ['red', 'blue', 'green']

为了解决共享和传参问题,出现了盗用构造函数继承。在子类构造函数内部通过SuperType.call(this)调用父类构造函数,这样每个实例都会拥有独立的属性副本。但缺点也很明显:方法只能在构造函数中定义,无法通过原型复用,每次创建实例都会重新创建一遍方法。

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
function SubType(name) {
  SuperType.call(this, name);
}
var s1 = new SubType('Alice');
console.log(s1.name); // 'Alice'

组合继承结合了原型链继承和盗用构造函数继承的优点:使用原型链继承方法,使用盗用构造函数继承实例属性。不过它有一个明显的效率问题,父类构造函数会被调用两次,一次是在设置子类原型时,另一次是在子类构造函数内部。子类原型上因此存在多余的父类实例属性。

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
};
function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

更优雅的寄生组合继承通过Object.create创建父类原型的一个副本,避免调用父类构造函数,同时还能正确维护原型链。它被认为是ES5环境下最理想的继承方案,也是很多类库内部实现继承的基础。

function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype);
  prototype.constructor = subType;
  subType.prototype = prototype;
}
function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
};
function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

原型式继承利用Object.create直接以某个对象为原型创建新对象,适合不需要构造函数的场景。寄生式继承则在原型式继承的基础上对返回的对象进行增强。这两种方式常见于临时对象或轻量级继承需求中。

四、class语法与原型继承的关系

ES6引入的class和extends让继承写法更加清晰,但底层仍然使用原型链。一个类本质上是构造函数加原型对象的组合,extends背后做了两件事:让子类构造函数的[[Prototype]]指向父类构造函数以实现静态成员继承,同时让子类prototype的[[Prototype]]指向父类prototype以实现实例方法继承。

class Parent {
  constructor(name) {
    this.name = name;
  }
  say() {
    return 'I am ' + this.name;
  }
}
class Child extends Parent {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
}
var c = new Child('Bob', 10);
console.log(c.say());
console.log(Object.getPrototypeOf(Child) === Parent); // true
console.log(Object.getPrototypeOf(Child.prototype) === Parent.prototype); // true

因此,class继承本质上就是经过语法封装的寄生组合继承。理解这一点后,就不会把JavaScript的类继承与Java、C++等静态语言的类继承混淆。JavaScript没有真正的类复制行为,继承的本质是对象之间的委托,实例通过原型链找到共享的方法。

掌握原型与原型链不仅能帮你写出更健壮的继承结构,也能在阅读框架源码、排查内存泄漏、理解instanceof原理时提供清晰的思维模型。原型链是JavaScript语言的核心支柱,值得花时间彻底搞懂。

JavaScript原型原型链继承构造函数修改时间:2026-09-27 23:26:01

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