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原型链机制与继承内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
JavaScript中call、apply、bind实现原理详解
其实在很多文章都会写call,apply,bind,但个人觉着如果不弄懂原理,是很难理解透的,所以这篇文章主要介绍了JavaScript中call、apply、bind实现原理的相关资料,需要的朋友可以参考下2021-06-06
DOM操作原生js 的bug,使用jQuery 可以消除的解决方法
下面小编就为大家带来一篇DOM操作原生js 的bug,使用jQuery 可以消除的解决方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧2016-09-09
JavaScript 双向链表操作实例分析【创建、增加、查找、删除等】
这篇文章主要介绍了JavaScript 双向链表操作,结合实例形式分析了JavaScript双向链表的创建、增加、查找、删除等相关操作技巧,需要的朋友可以参考下2020-04-04


最新评论