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等等,再继续查下去还会发现有许多不同的继承方式,这一点之后或许学习然后记录一下。