浅谈原型链
上题
先上前菜看段代码,思考以下几个问题
1 | # test.ts |
以下几个的值分别是什么
Object.getOwnPropertyNames(cat)
Object.getOwnPropertyNames(dog)
Object.getOwnPropertyNames(Object.getPrototypeOf(cat))
Object.getOwnPropertyNames(Object.getPrototypeOf(dog))
Object.getOwnPropertyNames(Animal)
Object.getOwnPropertyNames(Thing)
解题
1. Object.getOwnPropertyNames(cat)
cat
对象由Animal
类实例化而来,继承Animal类(及其父类及父类的父类…)的所有属性,即[ 'born', 'eat', 'name' ]
2. Object.getOwnPropertyNames(dog)
同理1,dog对象继承Thing,拥有的属性是[ 'born' ]
3. Object.getOwnPropertyNames(Object.getPrototypeOf(cat))
cat
对象的原型对象即Animal.prototype
,原型对象默认有一个constructor属性指向构造函数本身,即Animal.prototype.constructor = Animal
,再加上Animal
类本身定义的say方法,所以结果是[ 'constructor', 'say' ]
4. Object.getOwnPropertyNames(Object.getPrototypeOf(dog))
同理3,结果是[ 'constructor', 'die' ]
5. Object.getOwnPropertyNames(Animal)
这个有点绕,在一切皆对象的JavaScript
中,Animal
类也是一个函数对象,实例化内置的Function
对象而来,底层的定义类似于let Animal = new Function('name', { ...Animal构造函数的方法体... })
,所以继承了Function的所有属性,即[ 'length', 'name', 'arguments', 'caller', 'prototype' ]
(ES5, 新标准中Function的属性有所出入,此处暂不考虑)
6. Object.getOwnPropertyNames(Thing)
同理5,结果一样是[ 'length', 'name', 'arguments', 'caller', 'prototype' ]
深入
上面出现了三类数据
- 类/class,即
Animal
、Thing
- 类的实例,即
cat
、dog
- 原型对象,即
Object.getPrototypeOf(cat)
、Object.getPrototypeOf(dog)
这三者之间的关联说简单也简单,说难也难,对于从其他语言转过来的筒子来说概念上很容易绕晕,首先看第3个本文重点关注的原型对象
原型对象
原型对象相关的概念有4个:__proto__
、prototype
、Object.setPrototypeOf
和Object.getPrototypeOf
,我们看下它们的作用及历史渊源:
构造函数的 “prototype” 属性自古以来就起作用。这是使用给定原型创建对象的最古老的方式。
之后,在 2012 年,Object.create 出现在标准中。它提供了使用给定原型创建对象的能力。但没有提供 get/set 它的能力。一些浏览器实现了非标准的 __proto__ 访问器,以为开发者提供更多的灵活性。
之后,在 2015 年,Object.setPrototypeOf 和 Object.getPrototypeOf 被加入到标准中,执行与 __proto__ 相同的功能。由于 __proto__ 实际上已经在所有地方都得到了实现,但它已过时,所以被加入到该标准的附件 B 中,即:在非浏览器环境下,它的支持是可选的。
之后,在 2022 年,官方允许在对象字面量 {…} 中使用 __proto__(从附录 B 中移出来了),但不能用作 getter/setter obj.__proto__(仍在附录 B 中)。
引用自https://zh.javascript.info/prototype-methods
简单来讲,目前的标准有两种获取原型对象的方式,prototype
和Object.getPrototypeOf
,而且有使用限制:
prototype
只能用于构造函数,如本文中的Animal
、Thing
这句话除了含有非构造函数只能用Object.getPrototypeOf
获取原型对象的意义外,还隐藏着另外一个原则,即构造函数也可以用Object.getPrototypeOf
获取原型对象,这含义真的是九曲十八弯,脑袋冒烟不打紧,我们继续看,根据JavaScript
原型链的定义,可得出以下结论:
Object.getPrototypeOf(cat)
=Animal.prototype
Object.getPrototypeOf(dog)
=Thing.prototype
Object.getPrototypeOf(Animal.prototype)
=Thing.prototype
Object.getPrototypeOf(Thing.prototype)
=Object.prototype
Object.getPrototypeOf(Object.prototype)
=null
这个就是经典的原型链,这也是JavaScript
一切都从对象继承而来 的原因,上面刚说了构造函数也可以用Object.getPrototypeOf
获取原型对象,那Animal
和Thing
的原型对象分别是啥?Object.getPrototypeOf(Thing) = ?
类
class
关键字非严谨层面可以看作一个语法糖,通过编译后的js关键代码可见一斑:
1 | # test.js |
class中定义的属性及constructor
方法逻辑构成了同名的函数(即构造函数),定义的方法被加在构造函数的原型链上(prototype
);
而实例化对象的关键字new
底层的逻辑大致是以下三步:
- 创建一个新对象
- 执行构造函数,并将this指向当前的新对象
- 将新对象的原型对象设置为构造函数的原型对象
第2步使实例化的对象继承了类的所有属性,可以解释Object.getOwnPropertyNames(cat)
和Object.getOwnPropertyNames(dog)
的值;
第3步原型对象的设置,使实例化的对象继承了原型链上的所有方法,可以解释Object.getOwnPropertyNames(Object.getPrototypeOf(cat))
和Object.getOwnPropertyNames(Object.getPrototypeOf(dog))
的值;
那上小节提及的Animal
构造函数对象本身的原型链(Object.getPrototypeOf(Animal)
)和Thing
构造函数对象本身的原型链(Object.getPrototypeOf(Thing)
)的值又是什么呢?
其实上面稍为提到过,构造函数对象本身是由内建的类型Function
实例化而来,以此可推论出Object.getPrototypeOf(Thing)
= Function.prototype
,简单来讲这里的Thing
和Function
类比于前面的dog
和Thing
,这里的绕在于JavaScript
的类型系统,class Thing
表面是一个类,实际上是一个构造函数,而底层又是Function
类的实例,Thing
是别人(Function)的实例,它又可以再实例出别的实例(dog),实例实例无穷尽也~ 其实站在JavaScript
一切皆对象的角度看,没有那些眼花缭乱的类呀、实例呀、函数呀、字符串呀、整数呀、布尔值呀等等,对v8来讲都是object
,等等,是不是还有漏了个Animal
,Object.getPrototypeOf(Animal)
= ?
这里又不得不提extends
关键字了,和class
类似,也是一个语法糖,咱直接看编译后的js关键代码:
1 | # test.js |
有两个关键点:
extendStatics(d, b)
其实执行了Object.setPrototypeOf(d, b)
,在本文中即Object.setPrototypeOf(Animal, Thing)
,这一下子不就找到了答案?Object.getPrototypeOf(Animal)
=Thing
;d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
这行代码大致可以拆分为以下几个流程:- 覆盖__的原型链: __.prototype = b.prototype
- 使用__构造函数创建对象abc: let abc = { constructor: d; }
- 设置abc的原型对象: abc.__proto__ = b.prototype;
- 设置d的原型对象: d.prototype = abc
这个流程中,第一次看到整篇都在谈却一直没见踪影的的原型对象本尊,上面通过
prototype
和Object.getPrototypeOf
获取到的就是abc这样的原型对象,结合我们的实例可得出:Object.getPrototypeOf(cat)
=Animal.prototype
=abc
,Object.getPrototypeOf(Animal.prototype)
=abc.__proto__
=Thing.prototype
,跟我们上面谈到的也对上了号
献祭
最后祭出这张神图!!
结尾
完结撒花🎉,原型链在日常开发中直接碰到的不多,但每次遇到相关的问题都会很难绕出来,所以大致做了一下总结,有纰漏及错误之处还请勇猛指出
本文完整示例代码:https://github.com/tashuo/note/tree/master/typescript/prototype