JS原型链、__proto__和prototype
不知道你有没有想过这样一个问题
- 为什么我定义一个数组,它就有push、join、pop、shift等方法,我明明什么也没写啊?
- 为什么我定义一个函数,它就有call、apply、length等属性/方法,我也什么都没有做呀?!
- 为什么我定义一个对象,它就有toString、valueOf等方法,我更是什么都没有做呀?!
1、我们先来说说对象。
当我们定义一个对象时
我们发现obj下面有一个属性名叫__proto__
,(它是个对象)obj.__proto__
里面又有很多属性,包括 valueOf、toString、constructor 等。当我们需要找obj.valueOf
这个属性时,发现obj本身没有没有,那么它就会去查找obj.__proto__
是否有这个属性,如果还没有,它去找obj.__proto__.__proto__
,直到找到这个属性或null为止,在这个读取属性的过程中,是沿着__proto__
组成的链子来搜索的,这个链子我们称为原型链。
如果obj自身定义了一个valueOf属性,那么它找到自身的valueOf之后就不再沿着__proto__
来找,因为已经找到了,没有必要继续找了,也就是说
- 新增的属性不会沿着
__proto__
查找 - 读取属性会沿着
__proto__
,直到找到这个属性,或者是null为止。
2、那么obj.__proto__
到底是什么呢?
__proto__
是一个简单的访问器属性,它总是指向它的构造函数的prototype。即原型对象。
所有的对象都继承了Object.prototype的属性和方法,
它们可以被覆盖(除了以null为原型的对象,如 Object.create(null))。
例如,新的构造函数的原型覆盖原来的构造函数的原型,提供它们自己的 toString() 方法.。对象的原型的改变会传播到所有对象上,除非这些属性和方法被其他对原型链更里层的改动所覆盖。
所有的对象会动态生成一个__proto__
指向它构造函数的原型(prototype )
当我们去查找obj.valueOf
这个属性时,他会沿着原型链去查找obj.__proto__.valueOf
,而obj.__proto__
指向obj.constructor.prototype
。即
我们知道obj的构造函数就是Object,那么我们也可以这么写
3、对于数组
以push方法为例,我们知道当我们定义一个空数组时,我们可以直接调用push方法,根据上面的解释,他会沿着这个数组的__proto__
去查找这个方法。
array.__proto__
指向它构造函数的prototype,那么
终于找到push方法了。
问题来了,我们知道array也是对象,那么JS是怎么知道array也是对象的呢?
答:通过__proto__
我们先看Array.__proto__
指向谁
你也许会纳闷,怎么Array.__proto__
指向的怎么是函数的原型对象呢?因为Array的构造函数就是函数,不信你console.log(Array.constructor)试试?而函数的原型对象的__proto__
最终指向Object
或者我们也可以这样写
最终归宿都是Object。
至此我们看下一个小小的array都经历了什么
这样也就不难理解为什么数组(函数)也是个对象了。
4、一切皆对象?
也许你会迷惑,既然Array.__proto__.__proto__ = Object.prototype
,那么
为什么我们不可以说数字/布尔/字符串也是对象呢?
这要看这个数字/布尔/字符串是怎么创建的了。
以数字为例
我们知道基本类型是没有属性的,即便可以访问到这个属性,也是访问的临时对象的属性,访问完就销毁了,即使你发现a.__proto__.__proto__
指向是Object,也是new Number指向的,跟a没有半毛钱关系,因为a就是个number。
b就不一样了,b是构造函数Number构造出来的一个对象,只不过他的值是1,它可是有__proto__
属性的,那么b就可以愉快的指来指去了。
所以a是一个number,而b是一个object。
同理字符串和布尔也是如此。
来跟我一起大声念JS的七种数据类型:
number , string , boolean , undefined , null , object , symbol
说JS一切皆对象的,你们当其他类型是吃干饭的么?