深入理解JS原型链prototype、constructor、__proto__之间的关系

没图你说个锤子? 嘿嘿~ 首先来一张图理解JS原型链 :

proto.gif

对于上图的复杂关系其实就来源于3行代码 :

function Foo(){} var f1 = new Foo(); var f2 = new Foo();

对以上3行代码进行概念解释:

实例对象

使用new操作符实例化构造函数, 例如 new Foo() , 创建出的对象被称为实例对象,在这里, 变量f1,f2表示的就是函数Foo的2个实例对象


构造函数

被用来创建实例对象的函数称为构造函数,那么Foo就是f1,f2的构造函数


原型对象

实例对象拥有一个 __proto__ 属性, 该属性的值是一个空对象 {}, 同时, 构造函数也有一个属性, 名为 prototype , 这2个属性是都指向同一个对象, 可以理解为2个指针, 不理解指针相关概念的可以先去看这篇文章

JavaScript基础进阶 : 基本类型与引用类型及其储存方式

如果使用new操作符实例化同一个构造函数, 无论创建出多少个实例对象, 这些实例对象的原型对象都是同一个, 例如这个例子中有2个实例对象f1, f2, 它们的原型对象都是 Foo.prototype 或者用 f1.__proto__ , f2.__proto__来表示, 其实就是几个指针, 但都指向同一个对象, 如果你看了上面那篇文章, 这个地方理解起来就很容易, 下面这个例子就说明这2个属性其实就是同一个对象:

function Foo(){} f1 = new Foo(); f2 = new Foo(); console.log(f1.__proto__ === Foo.prototype); //true console.log(f2.__proto__ === Foo.prototype); //true

讲了这么多, 基本概念终于讲完了, 如果你之前没听说过这些概念, 只是熟练使用JS, 这里肯定会感到疑惑, 那这个原型对象到底是用来干嘛的?其实这个东西, 只要使用JS就会用到它, 下面来举个栗子:

你有没有想过JS的每个数组为什么都有pop(), unshift(), length等属性和方法, 每个对象都有toString(), length等属性和方法? 这些属性和方法是从哪里来的?

1540972103386966.jpg

其实很简单, 这些属性和方法都被挂载在JS内置的构造函数Array, Object的prototype属性上, 众所周知, 我们声明一个对象或者数组有以下2种方式:

let a = {}; // 字面量形式 let a = new Object(); // 实例化Object构造函数

以字面量形式创建对象是new Object()的语法糖, Object是JS内置的构造函数, 它的prototype属性上有各种内置的属性和方法,
例如length, toString等等, JS中每个对象都可以看做是new Object的结果.

当我们每次声明一个对象, 使用该对象的某个属性, JS引擎执行时会先从对象本身的属性列表中查找, 找不到时再从这个对象的原型对象的属性列表中查找, 原型对象是啥? 不就是Object.prototype么? 所以这样JS中所有的对象即使没有声明length, toString等属性, 也能从Object.prototype上找到.

当你明白上面所说的, 后面就很好理解了

自定义的构造函数和JS内置Object, Array等构造函数是不同的, 它的prototype属性是一个空对象, 所以我们可以给该对象添加属性, 使每个实例对象都可以继承到被添加的属性

prototype的用法:

function Foo(){} Foo.prototype.name = 'dalao'; f1 = new Foo(); f2 = new Foo(); console.log(f1.name); // output: dalao console.log(f2.name); // output: dalao

另外, 还有一个概念constructor

原型对象拥有constructor属性,指向相应的构造函数

function Foo(){} f1 = new Foo(); f2 = new Foo(); console.log(Foo.prototype.constructor === Foo) //true console.log(f1.__proto__.constructor === Foo) //true console.log(f2.__proto__.constructor === Foo) //true

实例对象本身不具有constructor属性,但是看了前面的内容你已经知道, 当JS引擎查一个对象的某个属性时, 先从该对象本身的属性列表中查找, 找不到则继续从该对象的原型对象的属性列表中查找, 而此时原型对象拥有constructor,这些实例对象也理所当然地继承到constructor属性, 可以看下面这个例子, 你就懂了:
function Foo(){} f1 = new Foo(); f2 = new Foo(); console.log(f1 === f2); //false console.log(f1.constructor === Foo); //true console.log(f2.constructor === Foo); //true console.log(f1.hasOwnProperty('constructor')) //false console.log(f2.hasOwnProperty('constructor')) //false

而且前面还说过, JS中的每个对象都可以看做是new Object的结果, 所以它们能继承内置构造函数Object的prototype上的属性和方法, 在这里我们new了一个自定义的构造函数Foo, Foo.prototype虽然它是f1, f2的原型对象, 但它同时也是JS内置构造函数Object的实例对象, Foo.prototype的原型对象就是Object.prototype (这句话有点拗口, 但是用代码理解起来应该很简单) :
function Foo(){} console.log(Foo.prototype.__proto__ === Object.prototype); //true

再来看一个问题, Object.prototype的constructor属性是指向JS内置构造函数Object的,那么作为实例对象的Foo.prototype会继承Object.prototype的constructor也指向Object吗?

答案是不会, 因为Foo.prototype本身就是f1, f2的原型对象,拥有constructor属性,所以会覆盖掉继承的constructor属性

function Foo(){} f1 = new Foo(); f2 = new Foo(); console.log(Object.prototype.constructor === Object); //true console.log(Foo.prototype.constructor === Foo); //true console.log(Foo.prototype.hasOwnProperty('constructor')) //true

按照这个逻辑,我们再想想原型对象Object.prototype作为实例对象时,它的原型对象又会是什么呢?
console.log(Object.prototype.__proto__); //null

答案是竟然是null,那么我们可以由以上结果总结出:

所有原型对象作为实例对象时,它们的原型对象都指向Object.prototype,

所有对象都是Object类型的实例,因此都可以继承Object.prototype上的属性和方法,Object.prototype的原型对象只能是null,> null是一个空对象指针 ,

使Object.prototype不能再向上引用,不然Object.prototype的原型对象继续指向Object.prototype就是循环引用了,这也是一切皆对象的一个论点。


一切皆对象,所有函数也都是对象,无论是Foo()这类自定义的普通函数,还是Object(),Array() , String()这类原生函数都可以看成是由new Function()创造出的对象
function Function(){} Foo = new Function(); Object = new Function();

所以这些函数作为实例对象时,对应的原型对象便是Funtion.prototype,前面也说过实例对象并不具有constructor属性,会继承原型对象的constructor属性

function Foo(){} f1 = new Foo(); f2 = new Foo(); console.log(Foo.__proto__ === Function.prototype); //true console.log(Object.__proto__ === Function.prototype); //true console.log(Array.__proto__ === Function.prototype); //true console.log(Foo.hasOwnProperty('constructor')); //false console.log(Foo.__proto__.hasOwnProperty('constructor')) //true

所以当一个函数作为实例对象时 , 它的constructor属性指向构造函数Function :
function Foo(){} f1 = new Foo(); f2 = new Foo(); console.log(Foo.__proto__.constructor === Function); //true console.log(Foo.constructor === Function); //true console.log(Object.constructor === Function); //true console.log(Array.constructor === Function); //true

同时Function自身也是原生函数,那么它作为实例对象时便可看做是实例化自身的结果,Funciton的原型对象很明显就是Function.prototype,构造函数是Function :
function Function(){} //举例 Function = new Function(); //举例 console.log(Function.__proto__ === Function.prototype); //true console.log(Function.__proto__.constructor === Function); //true console.log(Function.constructor === Function); //true

当然上面的2条举例写法显然是不符合语法规范的,这么理解即可,下面3条输出的结果是正确的。

那么结论出来了,所有函数都可以看做是new Function()的结果,作为实例对象,它们对应的原型对象都是Function.prototype,

而且前面提过每个原型对象都可以看做是new Object()的结果,它的原型对象是Object.prototype,构造函数是Object,

所以当原型对象Function.prototype作为实例对象时,它也不例外,它的原型对象是Object.prototype,但因自己拥有constructor属性

constructor会覆盖掉继承的Object.prototype的属性,指向构造函数Function:

console.log(Function.prototype.__proto__ === Object.prototype); //true console.log(Function.prototype.constructor === Function) //true

经过了以上的详细的分析理解,再回过头来看第一张图,是不是有一种豁然开朗的感觉!最后再归纳总结3条:

所有原型对象被当做实例对象时,它们所对应的原型对象是Object.prototype

所有函数皆对象,它们所对应的原型对象是Function.prototype,构造函数是Function

Object.prototype作为实例对象时,它的__proto__属性指向null,也是一切皆对象的论点

发布评论
还没有评论,快来抢沙发吧!