JS中的原型和原型链详解

JS中的原型和原型链是大家彻底搞懂JS面向对象及JS中继承相关知识模块非常重要的一个模块,一旦突破这块知识点,相信大家对JS会有一个更新、更全面的认识。

一、 什么是原型?

任何对象都有一个原型对象,这个原型对象由对象的内置属性_proto_指向它的构造函数的prototype指向的对象,即任何对象都是由一个构造函数创建的,但是不是每一个对象都有prototype,只有方法才有prototype。

二、 为什么要使用原型

试想如果我们要通过Foo()来创建很多很多个对象,如果我们是这样子写的话。

那么我们创建出来的每一个对象,里面都有showName和showAge方法,这样就会占用很多的资源。

而通过原型来实现的话,只需要在构造函数里面给属性赋值,而把方法写在Foo.prototype属性(这个属性是唯一的)里面。这样每个对象都可以使用prototype属性里面的showName、showAge方法,并且节省了不少的资源。如下图所示:

要想彻底了解原型,我们必须从对象的创建过程开始。就像你要彻底了解一个人,要从他的原生家庭,从他的出生环境了解起。有些基因是从出生就开始植入到对象内的。所以,我们首先来看对象的创建过程。

三、 创建对象的过程

1. 声明方法的过程

首先,当我们声明一个function关键字的方法时,会为这个方法添加一个prototype属性,指向默认的原型对象,并且此prototype的constructor属性也指向方法对象。此二个属性会在创建对象时被对象的属性引用。

2. 通过构造函数创建对象

实践证明,new出来的对象,它的constructor指向了方法对象,它的_proto_和prototype相等。即new一个对象,它的_proto_属性指向了方法的prototype属性,并且constructor指向了prototype的constructor属性。

为什么会导致上述结果?这涉及到对象创建的过程,一旦明白对象创建的过程这个问题就迎刃而解了。下面我们来看

3. 创建一个对象的过程

JS中创建对象的常见方法有三种:①通过字面量创建对象;②通过构造函数创建对象;③通过Object.create方法创建对象。

方式一:通过字面量创建对象

这种形式就是对象字面量,通过对象字面量构造出的对象,其__proto__指向Object.prototype。

所以,其实Object是一个函数也不难理解了。Object、Function都是是JS自带的函数对象。我们可以通过下面的代码验证:

方式二:通过构造函数创建对象

new一个构造函数,相当于实例化一个对象,这期间其实进行了这三个步骤:

【1】创建对象,设为f,即: var  f = {};

【2】上文提到了,每个对象都有__proto__属性,该属性指向一个对象,这里,将f对象的__Proto__指向构造函数Foo的原型对象(Foo.prototype);

【3】将f作为this去调用构造函数Foo,从而设置f的属性和方法并初始化。

当这3步完成,这个f对象就与构造函数Foo再无联系,这个时候即使构造函数Foo再加任何成员,都不再影响已经实例化的f对象了。

此时,f对象具有了name和age属性,同时具有了构造函数Foo的原型对象的所有成员,当然,此时该原型对象是没有成员的。

上述过程可以用下列代码表述:

简单的总结下就是:

【1】js在创建对象的时候,都有一个叫做__proto__的内置属性,用于指向创建它的函数对象的原型对象prototype;

【2】那么一个对象的__proto__属性究竟怎么决定呢?答案显而易见了:是由构造该对象的方法决定的。

方式三:通过Object.create方法创建对象

这种情况下,person2的__proto__指向person1。在没有Object.create函数的时候,人们大多是这样做的:

4. 延伸

从上面说明的过程中,我们发现只要是对象是由构造函数来创建的,并且内部二个属性是从构造函数的prototype衍生的一个指向,而构造函数的prototype也是一个对象,那么它应该肯定也有一个构造函数,首先它是一个Object {} 对象,那么它的构造函数肯定是Object,所以就会有一个指针_proto_指向Object.prototype。最后Object.prototype因为没有_proto_,指向null,这样就构成了一个原型链。

四、 原型链

1.  什么是原型链?

原型链的核心就是依赖对象的_proto_的指向,当自身不存在的属性时,就一层层的扒出创建对象的构造函数,直至到Object时,就没有_proto_指向了。

也就是说,当试图得到一个对象的属性时,如果这个对象本身不存在这个属性,那么就会去它构造函数的’prototype’属性中去寻找。那又因为’prototype’属性是一个对象,所以它也有一个’_ _ proto_ _'属性。

上述代码运行结果,直观地用下图表示,更便于大家理解:

首先,fn的构造函数是Foo()。所以:fn._ _ proto _ _ ===  Foo.prototype

又因为Foo.prototype是一个普通的对象,它的构造函数是Object,所以:

Foo.prototype._ _ proto _ _ ===  Object.prototype

通过上面的代码,我们知道这个toString()方法是在Object.prototype里面的,当调用这个对象的本身并不存在的方法时,它会一层一层地往上去找,一直到null为止。

所以当fn调用toString()时,JS发现fn中没有这个方法,于是它就去Foo.prototype中去找,发现还是没有这个方法,然后就去Object.prototype中去找,找到了,就调用Object.prototype中的toString()方法。

这就是原型链,fn能够调用Object.prototype中的方法正是因为存在原型链的机制。

另外,在使用原型的时候,一般推荐将需要扩展的方法写在构造函数的prototype属性中,避免写在_ _ proto _ _属性里面。(上述为什么使用原型有解释原因)

5.  如何分析原型链?

因为_proto_实质找的是prototype,所以我们只要找这个链条上的构造函数的prototype。其中Object.prototype是没有_proto_属性的,它==null。

属性搜索原则:

【1】当访问一个对象的成员的时候,会现在自身找有没有,如果找到直接使用。

【2】如果没有找到,则去原型链指向的对象的构造函数的prototype中找,找到直接使用,没找到就返回undifined或报错。

始终牢记:JS在创建对象的时候,都有一个叫做__proto__的内置属性,用于指向创建它的函数对象的原型对象prototype。

而原型链的基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法。

五、 总结

1、所有的引用类型(数组、函数、对象)可以自由扩展属性(除null以外)。

2、所有的引用类型都有一个’_ _ proto_ _'属性(也叫隐式原型,它是一个普通的对象)。

3、所有的函数都有一个’prototype’属性(这也叫显式原型,它也是一个普通的对象)。

4、所有引用类型,它的’_ _ proto_ _'属性指向它的构造函数的’prototype’属性。

5、当试图得到一个对象的属性时,如果这个对象本身不存在这个属性,那么就会去它的’_ _ proto_ _'属性(也就是它的构造函数的’prototype’属性)中去寻找。

给大家推荐个cocos学习交流群 点击链接即可加群  加群链接

推荐阅读更多精彩内容