JavaScript 的原型链机制与继承详解

 更新时间:2026年05月23日 14:19:20   作者:HeyCeleste  
这段文章详细解释了JavaScript中的原型和原型链机制,以及它们如何实现属性和方法的共享,文章还探讨了通过原型链查找属性的方法,并对比了多种实现继承的方式,包括原型链继承、构造函数继承、组合机制和寄生组合机制,感兴趣的朋友一起看看吧

JavaScript 的原型和原型链,本质是一种基于对象的继承机制。通过原型链实现属性查找和方法复用,使多个对象能够共享同一份数据和行为,同时避免重复创建,提高性能。

原型

官方定义(MDN):

当使用 new 调用函数时,构造函数的 prototype 属性将成为结果对象的原型。

原型是几乎每个对象实例都有的内部属性 [[Prototype]](可以通过定义在 Object.prototype 上的访问器属性 .__proto__访问),在通过 new 创建对象时这个属性就指向构造函数的 prototype 属性。

访问器属性: 这是与普通属性(数据属性)不同的概念,数据属性用于存储值,而访问器属性通过 getter/setter 定义属性的读取和赋值行为。普通属性例如 obj.name = "G.E.M." 就像是一个箱子将对应的值存储起来,而访问器属性并不是一个储物箱而是访问器

从底层来看,.__proto__ 其实是通过 getter/setter 这两个函数触发了引擎内部方法 [[GetPrototypeOf]][[SetPrototypeOf]]从而间接读写内部属性 [[Prototype]]。ES5 标准更推荐 Object.getPrototypeOf()Object.setPrototypeOf() 这两个方法读写对象实例的原型,.__proto__ 仅为保证浏览器向下兼容而存在。

这里值得注意的是,构造函数也有原型,即内部属性 [[Prototype]],但这和它的 prototype 属性并不是一个东西。构造函数本质上是对象,所以它自然也有原型,而 prototype 属性是普通函数(非箭头函数)在创建时自带的、用来为未来实例当作公共池子的普通属性,用于实现对象的继承。

而原型对象上有个 constructor 属性指向构造函数本身。也就是说,在默认情况下,如果假设 const obj = new Foo(),那么就存在 Object.getPrototypeof(obj).constructor === Foo

综上所述,每个对象身上都有一个内部属性 [[Prototype]],指向一个对象,即原型对象。想要访问到这个对象可以通过 Object.getPrototypeof() 或者 .__proto__,这两个方法都会指向原型对象。并且,在实例被创建时,会将其原型指向构造函数的普通属性 prototype,而在这个原型对象上又有一个 constructor 属性指向构造函数本身。关系如下图所示:

核心作用:

原型的本质作用就是实现属性和方法的共享。原型机制通过“引用共享”,避免了函数的重复创建,提高了内存的利用率。

原型链

当访问一个对象属性时,JavaScript 引擎会先查对象本身,如果本身没有这个属性就会查它的原型,如果还没有就继续往上找直到找到 null,这条路径就是原型链。

原型链上最后一个真实存在的对象是 Object.prototype,它的内部属性 [[Prototype]] 指向 null,即原型链的终点,因为绝大多数对象都是从 Object.prototype 继承而来的。

继承

传统面向对象语言(如 Java/C++)中继承意味着复制,派生类会将基类的属性和方法硬拷贝一份到自己身上,而 JavaScript 是利用原型链机制模拟了一种“假继承”。在 new 一个对象的时候,JavaScript 并没有像传统类那样复制蓝图上的共享方法,而是通过原型机制将对象实例的原型链接到构造函数的 prototype 上。

《You Don’t Know JS》中的观点是 JS 底层并没有类的概念,所谓的“继承”是将两个对象通过 [[Prototype]] 建立的一条单向引用链条(也就是原型链)。作者认为这种行为应该称之为“行为委托”而不是继承:当我们访问一个对象 A 身上不存在的属性/方法时,它并不是从其他地方复制或继承了这个属性/方法,而是顺着原型链找到另一个对象 B,并将这个行为委托给 B 让它帮执行,此时 A 的执行上下文(this)依旧绑定在它身上。

但由于普遍的用语习惯,下面还是将这一委托行为称为继承。

JS 实现继承的几种方式:

原型链继承

function Base() {
    this.names = ['G.E.M.', 'Gloria'];
}
Base.prototype.say = function() {}
function Derived() {}
Derived.prototype = new Base();
const d1 = new Derived();
const d2 = new Derived();
d1.say(); // 能够访问到

这是早起最容易想到的办法,直接将派生类的原型换成基类的实例。但这样实现的一个痛点就是引用类型的数据会被所有派生类实例共享,例如此处 d1.names.push('Tang') 会导致 d2.names 也跟着改变。

构造函数继承

function Base(name) {
    this.name = name;
    this.say = function() {console.log(this.name);}
}
function Derived(name, age) {
    Base.call(this, name); // 借用基类的构造函数并把 this 绑定到当前实例上
    this.age = age;
}

这样实现继承会使得每次实例化派生类都会把方法重新创建一遍,没有实现函数引用的复用,造成内存严重浪费。

组合机制

这是将上述二者结合了起来:

function Base(name) {
    this.name = name;
    this.names = ['G.E.M.', 'Gloria'];
}
Base.prototype.say = function() {} // 将方法挂在原型上实现复用
function Derived(name, age) {
    Base.call(this, name); // 拿到独立的属性
    this.age = age;
}
Derived.prototype = new Base(); // 将原型链拉通
Derived.prototype.constructor = Derived; // 手动修复 constructor

组合机制的痛点是调用了两次基类的构造函数,执行 Base.call(this, name) 后派生类实例就创建了自己独立的属性,但 Derived.prototype = new Base()Derived.prototype 上也有了 Base 的那份属性,但这上面的属性并不会用到,造成内存浪费。

寄生组合机制

这是 ES5 时代最完美的手写方案。

function Base(name) {
    this.name = name;
}
Base.prototype.say = function() {}
function Derived(name, age) {
    Base.call(this, name);
    this.age = age;
}
// 最优方案是利用 Object.create() 直接建立干净的原型链
// 这个方法实现了 Derived.prototype.[[Prototype]] = Base.prototype
Derived.prototype = Object.create(Base.prototype);
Derived.prototype.constructor = Derived;

ES6 的 class/extends

class 和 extends 是 ES6 官方推出的基于寄生组合机制继承的语法糖。

class Base {
    constructor(name) {
        this.name = name;
    }
    say() {}
}
class Derived extends Base {
    constructor(name, age) {
        super(name); // 这句相当于 Base.call(this, name)
        this.age = age;
    }
}

ES5 寄生组合机制中存在静态属性的丢失,也就是没有建立起基类和派生类两个函数对象之间的直接连接,ES6 推出的 extends 补全了这一点,底层实现了 Derived.__proto__ = Base

到此这篇关于JavaScript 的原型链机制与继承详解的文章就介绍到这了,更多相关js原型链机制与继承内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 原生js实现随机点名功能

    原生js实现随机点名功能

    这篇文章主要为大家详细介绍了原生js实现随机点名功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-11-11
  • 详解ES6语法之可迭代协议和迭代器协议

    详解ES6语法之可迭代协议和迭代器协议

    这篇文章主要介绍了详解ES6语法之可迭代协议和迭代器协议,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-01-01
  • JavaScript中call、apply、bind实现原理详解

    JavaScript中call、apply、bind实现原理详解

    其实在很多文章都会写call,apply,bind,但个人觉着如果不弄懂原理,是很难理解透的,所以这篇文章主要介绍了JavaScript中call、apply、bind实现原理的相关资料,需要的朋友可以参考下
    2021-06-06
  • uni-app实现数据上拉加载更多功能实例

    uni-app实现数据上拉加载更多功能实例

    数据列表在很多时候,经常会用到,下面这篇文章主要给大家介绍了关于uni-app实现数据上拉加载更多功能的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-08-08
  • DOM操作原生js 的bug,使用jQuery 可以消除的解决方法

    DOM操作原生js 的bug,使用jQuery 可以消除的解决方法

    下面小编就为大家带来一篇DOM操作原生js 的bug,使用jQuery 可以消除的解决方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-09-09
  • JavaScript 双向链表操作实例分析【创建、增加、查找、删除等】

    JavaScript 双向链表操作实例分析【创建、增加、查找、删除等】

    这篇文章主要介绍了JavaScript 双向链表操作,结合实例形式分析了JavaScript双向链表的创建、增加、查找、删除等相关操作技巧,需要的朋友可以参考下
    2020-04-04
  • js canvas实现擦除效果示例代码

    js canvas实现擦除效果示例代码

    擦除效果在我们日常开发中也是时有见到的,通过擦除效果大大加强了与用户的交互性,所以下面这篇文章主要给大家介绍了利用js和canvas实现擦除效果的相关资料,文中给出了详细的介绍和示例代码,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-04-04
  • Web Components入门教程示例详解

    Web Components入门教程示例详解

    这篇文章主要为大家介绍了Web Components入门教程示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • 前端开发中常见的数据结构优化问题与解决

    前端开发中常见的数据结构优化问题与解决

    在实际前端开发中,后端返回的数据结构往往不能直接满足前端展示或业务逻辑的需求,需要进行各种优化处理,下面我们来看看常见的几个优化问题及解决方案吧
    2025-04-04
  • for of 和 for in 的区别介绍

    for of 和 for in 的区别介绍

    这篇文章主要介绍了for of 和 for in 的区别,for of 和 for in都是用来遍历的属性,本文重点介绍下for of 和 for in 的区别,需要的朋友可以参考下
    2022-12-12

最新评论