大家好,我是前端西瓜哥。今天讲一道经典的原型链面试题。
原型链是什么?
JavaScript 中,每当创建一个对象,都会给这个对象提供一个内置对象 [[Prototype]]
。这个对象就是原型对象,[[Prototype]]
的层层嵌套就形成了原型链。
当我们访问一个对象的属性时,如果自身没有,就会通过原型链向上追溯,找到第一个存在该属性原型对象,取出对应值。
当然原型链不是无止境的,和单链表一样,最后一个原型对象的值是 null,原型链的所有对象都找不到指定的属性时,我们会拿到 undefined
。
[[Prototype]]
虽然无法通过脚本进行访问,但大多数浏览器提供了 __proto__
属性来访问这个内置对象,但它并不是标准,无法兼容所有浏览器。
下面来举几个例子,让读者对原型链有一个直观的认识:
-
通过对象字面量声明
a = {}
时, a 的[[prototype]]
就是Object.prototype
。此时的原型链是:a -> Object.prototype -> null
。这里有个易错点,就是以为 a 的上一个原型对象是Object
,其实并不对,Object
其实只是一个构造函数。 -
声明数组
arr = [1, 2, 4]
,它的原型链则是arr -> Array.prototype -> Object.prototype -> null
。 -
Object.create(null)
甚至能够创建一个连[[prototype]]
都没有的真正的空对象,一般用于做字符串哈希表,比如 vue 源码里就能经常看到。
通过构造函数创建实例对象
在 JavaScript 中,一个函数会在 new 关键字的配合下成为构造函数。也就是说,任何一个函数都可以成为构造函数。
当声明一个构造函数时,它会有一个属性名为 prototype
的对象(和 [[prototype]]
是不同的东西),这个对象就是 原型对象。这个对象的 constructor
又反过来指向构造函数。
当我们对使用 new 关键字创建对象,被创建的对象的 [[prototype]]
会指向这个 prototype。
function Rect() {}
const rect = new Rect()
rect.__proto__ === Rect.prototype // true
Rect.prototype.constructor === Rect // true
只要是通过 new Rect()
创建的对象,无论多少次,它的 [[prototype]]
都是指向 Rect.prototype
。另外,Rect.prototype.prototype
指向的是 Object.prototype
。
这样,通过给构造函数的原型对象(Rect.prototype
)添加一些方法(如 Rect.prototype.draw
),就能让创建的多个实例对象共享同一个方法,减少内存的使用。
用原型链的方式实现继承
理解了构造函数如何影响创建的实例的原型链后,我们来探讨一下核心问题,如何使用原型链来实现继承。
假设我们有一个 Shape 构造函数(父类)和 Rect 构造函数(子类)。代码如下:
// 父类
function Shape() {}
Shape.prototype.draw = function() {
console.log('Shape Draw')
}
Shape.prototype.clear = function() {
console.log('Shape Clear')
}
// 子类
function Rect() {}
/**
实现继承的代码放这里**/
Rect.prototype.draw = function() {
console.log('Rect Draw')
}
通过前面的学习,我们知道,正常情况下使用 new Rect
创建的实例对象,它的原型链是这样的:
rect -> Rect.prototype -> Object.protoype -> null
现在我们要实现的继承,其实就是在原型链中间再加一个原型对象 Shape.prototype
。对此我们需要对 Rect.prototype
进行特殊的处理。
方法1:Object.create
Rect.prototype = Object.create(Shape.prototype)
Rect.prototype.constructor = Rect // 选用,如果要用到 constructor
Object.create(proto)
是个神奇的方法,它能够创建一个空对象,并设置它的 [[prototype]]
为传入的对象。
因为我们无法通过代码的方式给 [[prototype]]
属性赋值,所以使用了 Object.create
方法作为替代。
因为 Rect.prototype 指向了另一个新的对象,所以把 constructor 给丢失了,可以考虑把它放回来,如果你要用到的话。
缺点是替换掉了原来的对象。
方法2:直接修改 [[prototype]]
如果就是不想使用新对象,只想修改原对象,可以使用 废弃 的 __proto__
属性,但不推荐。
不过另外还有一个方法 Object.setPrototypeOf()
可以修改对象的 [[prototype]]
,但因为性能的问题,也不推荐使用。
Object.setPrototypeOf(Rect.prototype, Shape.prototype)
// 或
Rect.prototype.__proto__ = Shape.prototype
都不推荐使用,但确实能用。
方法3:使用父类的实例
Rect.prototype = new Shape()
形成的原型链为:
rect -> shape(替代掉原来的 Rect.prototype) -> Shape.prototype -> Object.prototype -> null
基本能用,缺点是会产生副作用,就是执行 new Shap()
可能会出现副作用,比如给创建的对象添加了一些属性、发送了请求之类的,完全取决于构造函数内的代码。
某种意义上,这个缺点是致命的。不推荐使用。
总结
用原型链的方式实现一个 JS 继承,其实就是希望构造函数 Son
创建出来的对象 son
,它的原型链上加上父类 Parent.prototype
,所以最后就是要修改 Son.prototype
的 [[prototype]]
。
鉴于性能、兼容性、副作用等考虑,推荐使用方法 1,即通过 Object.create(Parent.prototype)
创建一个指定了 [[prototype]]
的新对象,替换掉原来的 Son.prototype
指向的对象。
总结几个核心知识点:
- 任何对象都有
[[prototype]]
属性,读写对象属性发现当前对象不存在时,会访问[[prototype]]
指向的对象尝试访问属性,于是 原型链 形成了。 - 函数创建时,它的
prototype
属性会拿到一个 原型对象。当函数作为构造函数,通过 new 创建一个新对象时,这个新对象的[[prototype]]
会指向这个原型对象。 - JS 要实现 “类” 继承,本质是通过处理构造函数的
prototype
对象来修改原型链。
本文使用 文章同步助手 同步
网友评论