原型链
原型链是 JS 的一个非常重要的概念,首先从对象说起:
在 JS 中,除了基本类型之外,其他的类型都是对象,我们看看对象的结构:
const a = {
b: 1,
c: 2,
}
这就是一个普通对象,只不过上面显示的是对象中可枚举的属性,对象中还有一个不可枚举的属性 __proto__
,在控制台输出对象可以看到这个属性:
在 JS 中,所有对象都存在 __proto__
属性,那么 __proto__
是用来干什么的呢?继续看:
__proto__
下面有一个 constructor
属性,指向的是 Object
的 prototype
,这里要说一下函数对象和普通对象的区别:
函数对象 | 普通对象 | |
---|---|---|
prototype | 有 | 无 |
__proto__ | 有 | 有 |
也就是说只有函数才具有 prototype 属性
既然 constructor
指向的是 Object
的构造函数,那么 constructor
下必然有 prototype
属性,prototype
属性指向的仍然是 constructor
本身,也就是说:a.__proto__ === Object.prototype
:
我们可以得出以下结论:
- 实例对象的
__proto__
主动指向构造函数的prototype
,这里的构造函数是Object
我们现在再使用构造函数创建一个对象:
function Animal() {
// ...
}
const dog = new Animal()
Animal
就是一个构造函数,通过 new
操作符可以得到一个新的 Animal
实例,Animal
是构造函数,也就一定有 prototype
,因为 prototype
是一个对象,所以一定拥有 __proto__
属性,我们来看看:
结果不出所料,因为 prototype
本身是一个对象,所以它的 __proto__
理应指向它的构造函数 Object
的 prototype
:
Animal.prototype.__proto__ === Object.prototype
那么当我们创建构造函数的时候,是如何自动生成 __proto__
和 prototype
的呢?
function Animal() {
// 定义函数时自动执行的代码
Animal.__proto__ = Function.prototype
Animal.prototype = {
constructor: Animal,
__proto__: Object.prototype,
}
}
就是这么简单!
prototype
称为显式原型对象,__proto__
称为隐式原型对象
我们知道实例的第一层除了我们自己定义的属性之外只有一个 __proto__
,那么如果我想调用实例的原生对象方法,JS 是如何找到并执行的呢?
答案就是原型链
假设有如下代码:
function Animal() {
this.name = 'Lee'
}
Animal.prototype.getName = function () {
return this.name
}
const dog = new Animal()
dog.toString()
dog.getName()
输出 dog
,我们可以看到 dog
中是没有 toString
和 getName
方法的:
当在第一层找不到相应属性的时候,就回去找自身的 __proto__
属性继续向上查找,__proto__
指向构造他函数的原型,也就是 Animal.prototype
,这上面就有我们定义的 getName
方法,于是停止了寻找,而 Animal.prototype
上并没有 toString
,沿着 Animal.prototype.__proto__
继续向上寻找,终于找到了 Object.prototype
,上面有 tostring
方法
这个过程可以用一张图来解释:
明白了原型和原型链我们就知道 new
是如何实现的了:
function new(parent, ...args) {
let obj = {};
obj.__proto__ = parent.prototype;
let res = parent.call(obj, args);
return typeof res === "object" ? res : obj;
}
instance
的实现也是基于原型链:
function instance(left, right) {
let rightProto = right.prototype
let leftValue = left.__proto__
while (true) {
if (leftValue === null) {
return false
}
if (leftValue === rightProto) {
return true
}
leftValue = leftValue.__proto__
}
}
__proto__
与 prototype
的区别
说了这么多,既然 __proto__
和 prototype
都能访问到原型,那么它们的区别是什么呢?有以下几点:
__proto__
是每个对象都有的一个属性,而prototype
是函数才会有的属性。__proto__
指向的是当前对象的原型对象,而prototype
指向的,是以当前函数作为构造函数构造出来的对象的原型对象。
按照 JavaScript 的继承机制,对于实例来说可以通过 __proto__
来访问所构造它的函数的 prototype
,因此 __proto__
就是对象用于访问原型链的桥梁。
另外 __proto__
实际上指的是对象的内部属性 [[Prototype]]
,之所以使用 [[]]
括起来是因为它是词法环境中的东西;官方不建议我们使用 __proto__
访问对象的原型,而应该使用 Object.getPrototypeOf()
/Reflect.getPrototypeOf()
访问。
修改对象的 __proto__
是一个非常浪费性能的事情,所以尽量使用 Object.create
来实现继承。