JavaScript 原型链机制
前言
刚开始接触 JavaScript,我十分不愿意去理解原型链,一是觉得很麻烦和很复杂,二是实际应用场景较少,所以之前都只会基本使用,而工作一段时间过后发现偶尔会接触到,就在此总结一下。
继承
在写 Java 的时候,我们都基本上用类的概念,而到 JavaScript,感觉像是如果我需要什么对象我就const person = {...}
,感觉不是那么严谨,比方说我有跟 Java 很类似的需求,需要一个Person
,然后在这个基础上扩展Student
,这感觉就没办法做到了,但其实 JavaScript 在没有class
概念之前都是使用原型链:
(_JavaScript 讲什么严谨,找你的 TypesCript 去_)
1 | function Person() {} |
这样我们就可以扩展的Person
了
prototype
上面例子讲了如何通过继承扩展,但是没有明白什么是prototype
,prototype
实际上就是原型的意思, 在Student.run()
的时候,就会去找Student
下寻找这个方法,如果没有,就往上面找,就是Student.prototype
指向Person
的时候,已经绑定了关系
Student.prototype.run() -> Person.prototype.run()
当调用属性或者方法的时候,就会一直找下去,比如我调用foo.toString()
,由于Student
和Person
都没有这个方法,所以他会继续找下去,就是Object
,此时寻找过程就是
Student.prototype.toString() -> Person.prototype.toString() -> Object.prototype.toString()
到了Object
后面,就没了,只剩下null
验证
怎么验证一下呢?验证方法有几种
__proto__
所有实例里面都会有一个[[prototype]]
指向原型,我们无法直接访问,但是可以通过Object.prototype.__proto__
来访问(实际上他是一个 getter 和 setter function)
1 | console.log(foo.__proto__ === Student.prototype); // true |
Object.getPrototypeOf
由于__proto__
可以获取也可以设置,比较危险而且代码不太雅观,已经不建议使用,存在只为了兼容性,现在在访问的时候已经建议使用Object.getPrototypeOf()
来实现。
1 | Object.getPrototypeOf(foo) === Student.prototype; // true |
instanceof
也可以通过instanceof
来验证,
1 | console.log(foo instanceof Student); // true |
Prototype.isPrototypeOf
或者通过isPrototypeOf
来检查
1 | console.log(Student.prototype.isPrototypeOf(foo)); // true |
判断属性
我们知道如何验证原型链正确与否,那么如何区分实例上的属性是实例的还是原型的呢?
Object.hasOwnProperty
Object.hasOwnProperty()
方法可以知道指定属性是否是实例上的,因为实际上constructor
什么都没做,foo
在实例的角度看就是空对象{}
1 | console.log(foo.hasOwnProperty("run")); // false |
in
in
操作符可以检查所有属性,如果配合for
使用可以循环所有属性
1 | console.log("run" in foo); // true |
如果我们只需要检查是否原型,可以配合hasOwnProperty
使用
1 | function hasPrototypeProperty(obj, name) { |
Object.getOwnPropertyNames
Object.getOwnPropertyNames()
方法可以获取所有实例属性
1 | console.log(Object.getOwnPropertyNames(foo)); // [ 'name' ] |
Object.keys
Object.keys()
和Object.getOwnPropertyNames()
有一点点不同,如果内有属性是不可枚举,那就不会输出,举个例子
1 | const arr = [1, 2, 3]; |
ps: Object.keys()
和for..in
结果一致
关于枚举之后再写一篇文章介绍
操作符实现
上面说到了一些操作符new
,instanceof
,in
,如果我们自己实现一次会是怎样的呢?
instanceof 实现
instanceof
实现是最简单的,不断在原型链上找,找到null
那就是到头了
1 | function instanceofFunction(instance, object) { |
in 实现
in
判断其实存在误区,它虽然的作用是所有属性判断,下意识感觉就可以通过foo.bar !== undefined
就知道有没有,但如果赋值为undefined
就不好弄了
1 | function Foo() { |
这个时候其实思路和instanceof
一致,不断往上调用getOwnPropertyNames
来判断是否拥有,就可以实现
1 | function inFunction(prop, object) { |
new 实现
当new
了一个新的实例出来之后,里面包含的其实有 {__proto__, constructor, ...attribute}
,__proto__
和一些属性能理解,但是为什么还包含constructor
呢?这个也是我刚刚特意还没提到的东西,实际上实例里面包含了constructor
也就是方法本身,用之前的代码来解释就是
1 | console.log(foo.constructor === Person); // true |
为什么不是Student
呢,因为之前写的比较粗糙,继承了之后应该还要把constructor
修正,修正之后就对了
1 | Student.prototype.constructor = Student; |
简单实现
知道这些之后我们就可以实现一边new
了
1 | function createObject(constructor) { |
绑定属性
这样就完成大部分了,为什么是大部分呢…,因为我们的往上的例子里面,都没有参数,也没有一些基于属性的方法,我们来修改一下
1 | function Person(name) { |
但是如果我们这样绑定方法和属性之后,就改成这样
1 | function createObject(constructor, ...args) { |
返回对象
貌似好了,但是其实还有一个问题,我们现在遇到的constructor
全部都没有返回值,貌似这样理所当然的,但其实,是可以拥有返回值的,如果返回值不为null
的Object
或者Function
就可以直接返回,取代this
,其他返回值无效。
如果这样,加一个判断就搞定了
1 | function createObject(constructor, ...args) { |
ES6 class
ES6 增加了class
的概念,其实 JavaScript 历史那么悠久,修修补补一直都用过来了,也不能可能从那么底层的概念更改,所以这其实是一个语法糖,他实际上的并没有和我们用原型链的方法差在哪,用new
的例子,改用class
写一次
1 | // function Person(name) { |
总结
虽然原型链在生产环境上用的还是比较少,但是这却是一道面试很常问的一道问题,学习一下总没坏处,对于自己看源码也有帮助(不过真的好麻烦…),而且从这个过程中也了解到很多不同的东西,比如枚举属性,Array
里面的Symbol.iterator
等等,再继续查下去还会发现有许多不同的继承方式,这一点之后或许学习然后记录一下。