原型链
原型对象
起源
所有函数在定义时默认生成一个对象,这个对象就是原型对象,函数通过prototype
属性可访问这个对象,即原型对象起源于函数,有函数就一定有原型对象。
原型对象里面有什么
思考如下代码:
function A(){};
console.log(A.prototype);//A的原型对象
我们定义了一个空的函数A,里面没有任何内容,但是有函数就一定有原型对象,哪怕你是空的函数,也会自动生成一个对应的原型对象。这个原型对象可以通过调用函数的````prototype```属性访问,于是我们通过打印 A.prototype
查看A的原型对象,发现其中有两个属性,分别是constructor
和[[Prototype]]
。
constructor
我们通过展开constructor
属性可以发现,这个属性指回了函数A
[[prototype]]]
通过展开[[prototype]]
属性可以发现,它指向了一个对象,这个对象其实还是一个原型对象,证据就是它的constructor
属性,我们知道这个属性是原型对象指向其对应的函数的属性,所以通过这个属性我们知道,这个原型对象是定义函数Object()时产生的,不过[[prototype]]]
属性并不是只会指向Object原型对象,事实上它可以指向任何一个对象,甚至可以指向null,但这需要人为的操作,默认情况下就是指向Object原型对象,同时我们可以看到Object原型对象还有很多其它的方法和属性,但这其实是后面才加上去的,通过A的原型对象我们知道,一个原型对象在出生时只会有两个属性,constructor
,[[prototype]]
,他们分别指向对应的函数和另一个对象,[[prototype[[
默认指向Object原型对象。
为什么Object原型对象没有[[prototype]]属性
眼尖的你应该发现了,Object原型对象只有constructor
属性,却没有[[prototype]]
属性,这是为什么?
普通情况下,所有对象(包括原型对象)都有[[prototype]]
属性,它指向另一个对象,然后这个对象也有[[prototype]]
属性,又指向另一个对象,这种形式就构造出一个链式结构,称之为原型链,而原型链的终点,就是Object原型对象,所以Object原型对象没有[[prototype]]
属性,因为它是[[prototype]]
链的终点。
函数,原型对象和对象的关系
思考如下代码
function A(){};
console.log(typeof A);//function
console.log(typeof A.prototype);//Object
typeof是一个操作符,可以输出某个变量是什么类型,从中我们可以看到A是函数,A.prototype是对象,但这是为了满足开发中需要知道一个变量是不是函数的需求,而制造的一个bug!还记得吗?js基础数据类型里面原始类型有:数值(Number),字符串(String),布尔值(Boolean),null,undefined,Symbol(ES6引入),引用数据类型有对象(Object),而任何不是原始类型的值,都是引用类型,即对象。函数(function)不属于上面任意一种原始数据类型,所以函数,是引用类型,即函数是对象。
函数,原型对象都是对象,
原型对象和普通对象的区别
思考如下代码
function A(){};
var B = {};
console.log(A.prototype);//有constructor 和 [[Prorotype]]两个属性的一个对象
console.log(B);//只有[[Prototype]]一个属性的对象
从中我们可以看到,原型对象与普通对象最显著的区别在于constructor
属性,该属性指向其对应的函数,一般情况,该属性指向哪个函数,该对象就是哪个函数的原型对象。所以如果看到一个对象有constructor
属性,那它十有八九就是一个原型对象。
函数和普通对象的原型
思考如下代码
function A(){};
var B = {};
console.log(A.__proto__);//指向一个函数
console.log(B.__proto__);//指向Object原型对象
__proto__
是一个非标准属性(只有现代浏览器支持,其他运行环境不一定支持,且ES6并没有将其写入正文,只写入了附录中),可以通过这个属性得到当前对象[[prototyep]]
属性指向的原型,从中我们可以发现,函数的[[protoype]]
属性指向另一个函数,而普通的对象[[protoype]]
属性指向Object原型对象。
总结
所有函数在定义时都会自动生成一个伴生的原型对象,该原型对象默认有constructor
和[[prototype]]
两个属性,其中constructor
属性指向对应的函数,[[prototype]]
属性指向另一个对象,默认情况下这个对象是Object原型对象,而且Object原型对象没有[[prototype]]
属性,因为它是原型链的终点。函数和原型对象都属于对象,而原型对象与普通对象最显著的区别就是constructor
属性,函数的[[prototype]]
属性指向另一个函数。
深入原型链
思考如下代码:
var a = {};
console.log(a.fn);//=>undifined
a.fn();//=>报类型错误
这段代码定义了一个空对象a,然后打印一个不存在的属性fn,因为fn不存在,所以默认赋值undefined,然后调用了fn方法,但undefined类型当然无法作为函数调用,于是报了一个类型错误。这当然没有任何问题,但是让我们再思考以下代码。
var a = {};
console.log(a.toString);//ƒ toString() { [native code] }
a.toString();//'[object Object]'
天啦噜!不敢置信,还是一个空对象a,同样没有任何内容,但是神奇的是,当我们打印a.toString属性时,发现竟然不是undefined,而是一个函数,然后我们调用toString方法,它竟然没有报类型错误,而是执行了方法!!那么问题来了,我们明明给a赋值的花括号里面空空如也,那么这个toString这个方法是如何出现在a里面的呢?
真相只有一个,那就是a里面压根没有toString这个方法,这个方法是Object原型对象里面的
那么凭什么a可以调用Object原型对象里面的方法?
a的解释: 因为Object原型对象在我的原型链里面,只要你在我的原型链里面,那么你的属性和方法,都是我的,而我的属性和方法,还是我的[嚣张😎]
查看原型
此时Object原型对象很不服气:凭什么你说我在我就在啊,我还说你在我的原型链里面呢!
a依然一副不可一世的样子😎:“行,要证据是吧,来,我给你!”。
只见a不慌不忙,拿出两个证据。
通过浏览器查看
思考以下代码:
var a = {};
a//=>{}[[Prototype]]: Object
只见a是一个空对象,里面有一个属性[[Prototype]]
,这个属性可不得了,这个属性指向谁,谁就是当前对象的原型,而当前对象是a,[[prototype]] :
旁边是Object,意思是该对象是Object函数的原型对象,即指向Object原型对象。
只见此时Object原型对象额头上流下一滴冷汗。
通过.__proto__属性查看
思考以下代码
var a = {};
console.log(a.__proto__);
此时浏览器没有直接打印出该原型对象对应的函数名,但通过该原型对象的constructor
属性我们依然能够断定,该对象就是函数Object()的原型对象。
但谁知,Object原型对象看到__proto__
属性后竟然喜笑颜开,只见它拿出大名鼎鼎的IE浏览器,然后将版本调整到10,只见__proto__
属性输出了不一样的结果。
IE打印出来的结果竟然是undefined的!!
这是因为__prototype__
属性在ES6之前并不是标准方法,在不支持ES6的浏览器中不能使用(而就算是ES6标准只明确规定只有浏览器需要部署这个属性,所以其他运行环境不一定能够使用),所以存在兼容性问题,那有没有一种ES5也能够证明的方法呢?当然有!那就是Object.getPrototypeOf()方法
Object.getPrototypeOf()
Object.getPrototype()方法能够得到一个对象的[[prototype]]
属性指向的原型对象。
思考如下代码
var a = {};
console.log(a.__proto__);
Object.getPrototypeOf(a) === Object.prototype;//true
我们通过判断a的原型是否是Object函数的prototype
属性指向的Object原型对象,发现其返回true。
我们也可以通过直接打印来查看
只见其得到了一个对象,该对象的constructor
属性指向了Object()函数,这毫无疑问说明了该对象就是Object函数的原型对象,即Object原型对象。
这次使用的是ES5标准里的方法检测出来的,Object原型对象终于无话可说了。