JavaScript:原型(prototype)
<p>面向对象有一个特征是继承,即重用某个已有类的代码,在其基础上建立新的类,而无需重新编写对应的属性和方法,继承之后拿来即用;</p><p>在其他的面向对象编程语言比如Java中,通常是指,子类继承父类的属性和方法;</p>
<p>我们现在来看看,JS是如何实现继承这一个特征的;</p>
<p>要说明这个,我们首先要看看,每个对象都有的一个隐藏属性<code>[]</code>;</p>
<h2 id="对象的隐藏属性prototype">对象的隐藏属性[]</h2>
<p>在JS中,每个对象<code>obj</code>,都有这样一个隐藏属性<code>[]</code>,它的值要么是null,要么是对另一个对象<code>anotherObj</code>的引用(不可以赋值为其他类型值),这另一个对象<code>anotherObj</code>,就叫做对象<code>obj</code>的原型;</p>
<p>通常说一个对象的原型,就是在说这个隐藏属性<code>[]</code>,也是在说它引用的那个对象,毕竟二者一致;</p>
<p>现在来创建一个非常简单的字面量对象,来查看一下这个属性:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221219202045253-776243168.png" alt="image-20221219202044247" loading="lazy"></p>
<p>可以看到,对象<code>obj</code>没有自己的属性和方法,但是它还有一个隐藏属性<code>[]</code>,数据类型是<code>Object</code>,说明它指向了一个对象(即原型),这个原型对象里面,有很多方法和一个属性;</p>
<p>其他的暂且不论,我们先重点看一下,红框的<code>constructor()</code>方法和<code>__proto__</code>属性;</p>
<h2 id="访问器属性__proto__">访问器属性(<code>__proto__</code>)</h2>
<h3 id="访问prototype">访问[]</h3>
<p>从红框可以看到,属性<code>__proto__</code>是一个访问器属性,有getter/setter特性(这个属性名前后各两个下划线);</p>
<p>问题是,它是用来访问哪个属性的?</p>
<p>我们来调用一下看看:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221219205331844-521471380.png" alt="image-20221219205330882" loading="lazy"></p>
<p>可以看到,<code>__proto__</code>访问器属性,访问的正是隐藏属性<code>[]</code>,或者说,它指向的正是原型对象;</p>
<p>值得一提的是,这是一个老式的访问原型对象的方法,现代编程语言建议使用<code>Object.getPrototypeOf/setPrototypeOf</code>来访问原型对象;</p>
<p>但是考虑兼容性,使用<code>__proto__</code>也是可以的;</p>
<p>请注意,<code>__proto__</code>不能代表<code>[]</code>本身,它只是其一个访问器属性;</p>
<h3 id="设置prototype">设置[]</h3>
<p>正因为它是访问器属性,也即具有getter和setter功能,我们现在可以控制对象的原型对象的指向了(并不建议这样做):</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221219210506730-112014121.png" alt="image-20221219210505820" loading="lazy"></p>
<p>如上图,现在将其赋值为null,好了,现在<code>obj</code>对象没有原型了;</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221219212733342-1275858650.png" alt="image-20221219212732241" loading="lazy"></p>
<p>如上图,创建了两个对象,并且让<code>obj1</code>没有了原型,让<code>obj2</code>的原型是<code>obj1</code>;</p>
<p>看看,此时<code>obj2.name</code>读取到<code>obj1</code>的属性<code>name</code>了,首先<code>obj2</code>在自身属性里找<code>name</code>没有找到,于是去原型上去找,于是找到了<code>obj1</code>的<code>name</code>属性了,换句话说,<code>obj2</code>继承了<code>obj1</code>的属性了;</p>
<p>这就是JS实现继承的方式,通过原型这种机制,后面会在<strong>继承</strong>详细说明继承和原型的关系;</p>
<p>让我们看看下面的代码:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221219214740884-242569657.png" alt="image-20221219214739842" loading="lazy"></p>
<p>正常的<code>obj2.name = 'Jerry'</code>的添加属性的语句,会成为<code>obj2</code>对象自己的属性,而不会去覆盖原型的同名属性,这是再正常不过了,继承得来的东西。只能读取,不能修改(访问器属性<code>__proto__</code>除外);</p>
<p>现在的问题是,为什么<code>obj2.__proto__</code>是<code>undefined</code>?上面不是刚刚赋值为<code>obj1</code>了吗?</p>
<p>原因就在于<code>__proto__</code>是访问器属性,我们读取它实际上是在调用对应的getter/setter方法,而现在<code>obj2</code>的原型(即<code>obj1</code>)并没有对应的getter/setter方法,自然是<code>undefined</code>了;</p>
<p>现在综合一下,看下面代码,与上图做比较:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221219220645285-1822107764.png" alt="image-20221219220644268" loading="lazy"></p>
<p>为什么最后<code>obj2.__proto__</code>输出的是<code>hello world</code>,为什么<code>__proto__</code>成了<code>obj2</code>自己的属性了?</p>
<p>关键就在于红框的三句代码:</p>
<p>第一句<code>let obj2 = {}</code>,此时<code>obj2</code>有原型,有访问器属性<code>__proto__</code>,一切正常;</p>
<p>第二句<code>obj2.__proto__ = obj1</code>,这句调用<code>__proto__</code>的setter方法,将<code>[]</code>的引用指向了<code>obj1</code>;</p>
<p>这一句完成以后,<code>obj2</code>因为<code>obj1</code>这个原型而没有访问器属性<code>__proto__</code>了;</p>
<p>所以第三句<code>obj2.__proto__ = 'hello world'</code>的<code>__proto__</code>已经不再是访问器属性了,而是一个普通的属性名了,所以这句就是一个普通的添加属性的语句了;</p>
<h2 id="构造器constructor">构造器(constructor)</h2>
<p>在隐藏属性<code>[]</code>那里,看到其有一个<code>constructor()</code>方法,顾名思义,这就是构造器了;</p>
<h3 id="类对象与函数对象">类对象与函数对象</h3>
<ul>
<li>类对象</li>
</ul>
<p>在其他编程语言比如Java中,构造方法通常是和类名同名的函数,里面定义了对象的一些初始化代码;</p>
<p>当需要一个对象时,就通过<code>new</code>关键字去调用构造方法创建一个对象;</p>
<p>那在JS中,当我们<code>let obj = {}</code>去创建一个字面量对象的时候,发生了什么?</p>
<p>上面这句代码,其实就是<code>let obj = new Object()</code>的简写,也是通过<code>new</code>关键字去调用一个和类名同名的构造方法去创建一个对象,在这里就是构造方法<code>Object()</code>;</p>
<p>这种通过<code>new className()</code>调用构造方法创造的对象,称为类对象;</p>
<ul>
<li>函数对象</li>
</ul>
<p>但是,再等一下,JS早期是没有类的概念的,那个时候大家又是怎么去创建对象的呢?</p>
<p>想一下,创建对象是不是需要一个构造方法(即一个函数),本质上是不是<code>new Function()</code>的形式去创建对象?</p>
<p>对咯,早期就是<code>new Function()</code>去创建对象的,这个<code>Function</code>就叫做构造函数;</p>
<p>这种通过<code>new Function()</code>调用构造函数创造的对象,称为函数对象;</p>
<p>构造函数和普通函数又有什么区别呢?除了要求是用<code>function</code>关键字声明的函数,并且命名建议大驼峰以外,几乎是没有区别的:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221219230238848-644576173.png" alt="image-20221219230237600" loading="lazy"></p>
<p>看,我们声明了一个构造函数<code>Cat()</code>,并通过<code>new Cat()</code>创造了一个对象<code>tom</code>;</p>
<p>打印<code>tom</code>发现,它有一个原型,这个原型和字面量对象的原型不一样,它有一个方法一个属性;</p>
<p>方法是<code>constructor()</code>构造器,指向的正是<code>Cat()</code>函数;</p>
<p>属性是另一个隐藏属性<code>[]</code>,暂时不去探究它是谁;</p>
<p>也就是说,函数对象的原型,是由另一个原型和<code>constructor()</code>方法组成的对象;</p>
<p>我们可以用代码来验证一下,类对象和函数对象的原型的异同点:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221220180835496-284420967.png" alt="image-20221220180834768" loading="lazy"></p>
<p>如上所示,创建了一个函数对象<code>tom</code>和一个类对象<code>obj</code>;</p>
<p>可以看出:</p>
<p>函数对象的原型的方法<code>constructor()</code>指向构造函数本身;</p>
<p>函数对象的原型的隐藏属性<code>[]</code>和字面量对象(Object对象)的隐藏属性,他们两的引用相同,指向的是同一个对象,暂时不去探究这个对象是什么,就认为它是字面量对象的原型即可;</p>
<p>还可以看到,无论是类对象,还是函数对象,其原型都有<code>constructor()</code>构造器;</p>
<p>这个构造器在创建对象的过程中,具体起了什么样的作用呢?</p>
<p>让我们先看看函数对象<code>tom</code>的这个原型是怎么来的?我们之前一直都是在说对象有一个隐藏属性<code>[]</code>指向原型对象,究竟是哪一步,让这个隐藏属性指向了原型对象呢?</p>
<h3 id="函数的普通属性prototype">函数的普通属性prototype</h3>
<p>事实上,每个函数都有一个属性<code>prototype</code>,默认情况下,这个属性<code>prototype</code>是一个对象,其中只含有一个方法<code>constructor</code>,而这个<code>constructor</code>指向函数本身(还有一个隐藏属性<code>[]</code>,指向字面量对象的原型);</p>
<p>可以用代码佐证,如下所示:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221220122723728-2115385816.png" alt="image-20221220122721686" loading="lazy"></p>
<p>注意,<code>prototype</code>要么是一个对象类型,要么是null,不可以是其他类型,这听起来很像隐藏属性<code>[]</code>,不过<code>prototype</code>只是函数的一个普通属性,对象是没有这个属性的;</p>
<p>来看下这个属性的特性吧:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221220183641780-1854141126.png" alt="image-20221220183641004" loading="lazy"></p>
<p>可以看到,它不是一个访问器属性,只是一个普通属性,但是它不可配置不可枚举,只能修改值;</p>
<p>它的<code>value</code>值,眼熟吗?正是构造函数创建的函数对象的原型啊;</p>
<p><strong>它居然还有一个特性<code>[]</code>,不要把它和<code>value</code>值里面的属性<code>[]</code>弄混,前者是<code>prototype</code>属性的特性,后者是<code>prototype</code>属性的一个隐藏属性,虽然此刻他们都指向字面量对象的原型,但是前者始终指向字面量对象的原型,后者则始终指向原型(而原型是会变的);</strong></p>
<p>这里也不再去追究为什么它会有这样一个特性了,让我们把重点放在<code>prototype</code>属性本身;</p>
<h3 id="new-function的时候发生了什么">new Function()的时候发生了什么</h3>
<p>事实上,只有在调用<code>new Function()</code>作为构造函数的时候,才会使用到这个<code>prototype</code>属性;</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221220185834492-953995893.png" alt="image-20221220185833703" loading="lazy"></p>
<p>我们来仔细分析一下上面代码具体发生了什么:</p>
<p><code>let tom = new Cat()</code>这句代码的执行流程如下:</p>
<ul>
<li>先调用<code>Cat.prototype</code>属性的特性<code>[]</code>(我们知道它指向字面量对象的原型)里面的<code>constructor()</code>构造器,创建一个字面量对象,当然此时这个对象的隐藏属性<code>[]</code>也都已经存在了,将这个对象分配给<code>this</code>指针;</li>
<li>然后将<code>Cat.prototype</code>属性值<code>value</code>,<strong>复制</strong>(注意,这里是复制,不是赋值,这意味着这里不是传引用,而是传值)给字面量对象的隐藏属性<code>[]</code>,即<code>tom.__proto__ = Cat.prototype的value值</code>;</li>
<li>然后执行构造函数<code>Cat()</code>本身的语句,即<code>this.name = "Tom"</code>;</li>
<li>然后返回<code>this</code>指针给<code>tom</code>,即<code>tom</code>引用了这个字面量空对象,同时<code>this</code>指向了<code>tom</code>;</li>
</ul>
<p>现在已经说清楚了<code>new Function()</code>发生的具体过程,上面代码的输出结果也佐证了我们所说的:</p>
<p>函数对象<code>tom</code>的原型正是<code>Cat</code>函数的属性<code>prototype</code>的值<code>value</code>,可以看到他们的<code>constructor()</code>构造器都指向<code>Cat</code>函数本身,并且<code>tom.name</code>的值为<code>Tom</code>;</p>
<p>然后我们修改了<code>Cat</code>函数的<code>prototype</code>的值<code>value</code>,<code>Cat.prototype = Dog.prototype</code>语句将其设置成了<code>Dog</code>函数的<code>prototype</code>的值<code>value</code>;</p>
<p>让我们顺着刚刚说的流程,看看<code>let newTom = new Cat()</code>的执行过程:</p>
<ul>
<li>先创建字面量空对象,与<code>this</code>绑定;</li>
<li>然后将<code>Cat.prototype</code>的<code>value</code>值(此时等于<code>Dog.prototype</code>),复制给字面量对象的隐藏属性;</li>
<li>然后执行构造函数<code>Cat()</code>本身的语句,即<code>this.name = "Tom"</code>;</li>
<li>然后返回<code>this</code>指针给<code>newTom</code>;</li>
</ul>
<p>输出结果佐证了我们的执行过程,对象<code>newTom</code>的原型正是<code>Dog</code>函数的属性<code>prototype</code>的值<code>value</code>,他们的<code>constructor()</code>构造器都指向了<code>Dog</code>函数本身,但是<code>newTom.name</code>的值依然是"Tom";</p>
<p>从上面前后两个输出结果也可以看出来,最后一步的<code>tom.__proto__ = Cat.prototype</code>确实是复制而不是赋值,否则在<code>Cat.prototype = Dog.prototype</code>语句之后,<code>tom.__proto__ = Cat.prototype = Dog.prototype</code>了(即<code>tom</code>的原型也会是<code>Dog.prototype</code>的<code>value</code>值),但是输出结果表面并没有改变,依然是<code>Cat.prototype</code>;</p>
<p>现在我们已经明白了函数对象的原型为什么是这个样子的,也明白了函数对象的<code>constructor()</code>构造器指向了构造函数本身;</p>
<p>现在让我们像下面这样,使用一下函数对象的<code>constructor()</code>构造器吧:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221220204244410-1820909179.png" alt="image-20221220204243512" loading="lazy"></p>
<p>看上面的代码,我们现在已经知道<code>let tom = new Cat()</code>的时候都发生了什么,也知道此时<code>tom</code>的原型的<code>constructor()</code>构造器指向的是<code>Dog</code>函数;</p>
<p>所以<code>let spike = new tom.constructor()</code>这句代码,当<code>tom</code>去自己的属性里没有找到<code>constructor()</code>方法的时候,就去原型里面去找,于是找到了指向<code>Dog</code>函数的<code>constructor()</code>构造器,所以这句代码就等于<code>let spike = new Dog()</code>;</p>
<p>通过这段代码,好好体会一下函数对象的构造器吧。</p>
<h3 id="构造函数和普通函数的区别">构造函数和普通函数的区别</h3>
<p>其实从技术上来讲,构造函数和普通函数没有区别;</p>
<p>只是默认构造函数采用大驼峰命名法,并通过<code>new</code>操作符去创建一个函数对象;</p>
<ul>
<li>
<p>new.target</p>
<p>我们怎样去判断一个函数的调用是普通调用,还是<code>new</code>操作符调用的呢?</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221221152945823-1297155573.png" alt="image-20221221152944768" loading="lazy"></p>
<p>如上所示,通过<code>new.target</code>,可以判断该函数是被普通调用的还是通过<code>new</code>关键字调用的;</p>
</li>
<li>
<p>构造函数的返回值</p>
<p>构造函数从技术上说,就是一个普通函数,所以当然也可能有<code>return</code>返回值(通常构造函数于情于理都是不会有<code>return</code>语句的);</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221221153626318-1634723543.png" alt="image-20221221153625673" loading="lazy"></p>
<p>之前说过<code>new Function()</code>的时候的具体流程,我们来看一下:</p>
<ul>
<li>
<p>先创建一个字面量对象,绑定<code>this</code>;</p>
</li>
<li>
<p>将<code>Cat.prototype</code>的<code>value</code>值复制给字面量对象的隐藏属性;</p>
</li>
<li>
<p>执行<code>Cat()</code>函数本身,让字面量对象有属性<code>name</code>;</p>
<p>但是遇到了<code>return</code>语句,本来应该返回<code>this</code>指针的,现在返回了一个空对象<code>{}</code>;</p>
</li>
<li>
<p>所以最后<code>tom</code>指向的是<code>return</code>语句的空对象,而不是最开始创建的空对象;</p>
</li>
</ul>
</li>
</ul>
<h2 id="字面量对象的原型">字面量对象的原型</h2>
<h3 id="new-object的时候发生了什么">new Object()的时候发生了什么</h3>
<p>我们刚刚说了<code>new Function()</code>创建函数对象的时候,具体发生了什么,现在来看看创建字面量对象的时候,具体发生了什么;</p>
<p>之所以单拿出字面量对象来说,是因为<code>Object</code>是JS其他所有类的祖先,要想创建一个对象,基础便是先<code>new Object()</code>;</p>
<p>我们先看一下<code>Object</code>的<code>prototype</code>属性吧,是的,类和函数一样,也有这个属性(注意,是类有这个属性,而不是类的实例即对象有这个属性);</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221220205758032-906714285.png" alt="image-20221220205757060" loading="lazy"></p>
<p>看上图,是不是很眼熟,这不就是字面量对象的原型吗?</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221220210051377-1195043232.png" alt="image-20221220210050576" loading="lazy"></p>
<p>是的,如上图所示,就是它;</p>
<p>那么这个原型对象还有原型吗?</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221220221600330-727566584.png" alt="image-20221220221559466" loading="lazy"></p>
<p>如上所示,没有了,指向null了,看样子我们已经走到了原型链的原点了,为了方便,我们就称呼<code>Object.prototype</code>为原始原型吧;</p>
<p>看看它的特性吧:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221220210425406-506152710.png" alt="image-20221220210424571" loading="lazy"></p>
<p>和函数的<code>prototype</code>属性的特性,如出一辙,但是注意,它的<code>writable</code>属性是<code>false</code>了,这意味着我们再也无法对这个属性做任何操作了;</p>
<p>这是当然,它可是所有类的祖先,怎么能随意更改呢,事实上,所有类的<code>prototype</code>属性都是不可操作的,这是为了确保原型链的完整继承;</p>
<p>这下我们就能明白<code>new Object()</code>的时候大概流程是什么样子了;</p>
<p>以<code>let obj = {}</code>为例(其实就是<code>let obj = new Object()</code>):</p>
<ul>
<li>先调用<code>Objecet.prototype</code>属性的特性<code>[]</code>里面的<code>constructor()</code>构造器(不再继续深究这个构造器了),创建一个字面量对象,绑定<code>this</code>;</li>
<li>然后将<code>Object.prototype</code>属性值<code>value</code>,复制给字面量对象的隐藏属性<code>[]</code>,即<code>this.__proto__ = Object.prototype</code>;</li>
<li>然后执行构造方法<code>Object()</code>本身的语句(不再进一步去研究这个构造方法了),总之此时字面量对象已经有着很多内置方法了;</li>
<li>然后返回<code>this</code>赋值给<code>obj</code>,即<code>obj</code>引用了这对象,同时<code>this</code>指针也就指向了<code>obj</code>;</li>
</ul>
<p>注意,其实<code>new className()</code>的流程不完全是上面这样子,与构造函数的流程还有一点点区别,主要是第二步和第三步,这和类的继承有关系,详细的在后面<strong>new className()的时候发生了什么</strong>里面具体说明;</p>
<h3 id="更改原始原型">更改原始原型</h3>
<p>我们刚刚说了,<code>Object.prototype</code>属性的所有特性都是<code>false</code>,意味着我们对这个属性无法再做任何操作了;</p>
<p>这只是再说,我们不能对其本身做任何删改的操作了,但是它本身依然是一个对象,这意味着我们可以正常的向其添加属性和方法;</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221220223233927-54761140.png" alt="image-20221220223232974" loading="lazy"></p>
<p>如上图所示,我们向<code>Object.prototype</code>属性对象里添加了<code>hello()</code>方法,并且由<code>obj</code>对象通过原型调用了这个方法;</p>
<h2 id="类对象的原型">类对象的原型</h2>
<h3 id="类的prototype属性">类的prototype属性</h3>
<p>在创建类对象的时候,会将类的prototype属性值复制给类对象的原型;</p>
<p>所以说,类对象的原型等于类的prototype属性值;</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221223214053769-1347607767.png" alt="image-20221223214052596" loading="lazy"></p>
<p>而类的prototype属性,默认就有两个属性:</p>
<ul>
<li>构造器constructor:指向类本身;</li>
<li>原型[]:指向父类的prototype属性;</li>
</ul>
<p>以及</p>
<ul>
<li>类的普通方法;</li>
</ul>
<p>从上图中可以看出,<code>A</code>的prototype属性里,除构造器和原型以外,就只有一个普通方法<code>show()</code>;</p>
<p>这说明,只有类的普通方法,会自动进入类的<code>prototype</code>属性参与继承;</p>
<p>也就是说,一个类对象的数据结构,如下:</p>
<ul>
<li>普通属性</li>
<li>(原型)prototype属性
<ul>
<li>构造器</li>
<li>父类的prototype属性(父原型)</li>
<li>方法</li>
</ul>
</li>
</ul>
<p>另外,类的<code>prototype</code>属性是不可写的,但是类对象的原型则是可以修改的;</p>
<h3 id="类的原型">类的原型</h3>
<p>类本身也是有原型的,就像类对象有原型一样;</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221223212000442-1289943207.png" alt="image-20221223211958950" loading="lazy"></p>
<p>可以看到,<code>B</code>的原型就是其父类<code>A</code>,而<code>A</code>作为基类,基类的原型是本地方法;</p>
<p>正因如此,<code>B</code>可以通过原型去调用<code>A</code>的静态方法/属性;</p>
<p>也就是说,静态方法/属性,也是可以继承的,通过类的原型去继承;</p>
<h3 id="继承了哪些东西">继承了哪些东西</h3>
<p>当子类去继承父类的时候,到底继承到了父类的哪些东西,也即子类可以用父类的哪些内容;</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221223220503279-1751529122.png" alt="image-20221223220502179" loading="lazy"></p>
<p>从结果上来看,我们可以确定如下:</p>
<ul>
<li>子类继承父类的静态属性/方法(基于类的原型);</li>
<li>子类对象继承父类的普通方法和构造器(基于类的prototype);</li>
<li>子类直接将父类的普通属性作为自己的普通属性(普通属性不参与继承);</li>
</ul>
<p>由于原型链的存在,这些继承会一路沿着原型链回溯,继承到所有祖宗类;</p>
<h3 id="子类的调用顺序">子类的调用顺序</h3>
<p>子类调用方法的顺序:</p>
<ul>
<li>先从自己的方法里调用,发现没有可调用的方法时;</li>
<li>再沿着原型链,先从父类开始寻找方法,一直往上溯源,直到找到可调用的方法,或者没有而出错;</li>
</ul>
<h3 id="类与构造函数的区别">类与构造函数的区别</h3>
<p>我们已经了解了函数对象的原型,原始原型,类对象的原型;</p>
<p>我们把这三种放一起做个比较吧:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221220225535998-1743015463.png" alt="image-20221220225535034" loading="lazy"></p>
<p>我们自定义了类<code>classA</code>,自定义了函数<code>functionA</code>,并创建了类对象<code>clsA</code>和函数对象<code>funcA</code>,以及字面量对象;</p>
<p>可以看出,类对象与函数对象的原型的形式,是一致的,都是两个属性(一个隐藏属性一个构造器),只是各自原型里的<code>constructor()</code>指向各自的类/函数,即红框部分不同;</p>
<p>而他们的原型的原型则是一致的,和字面量对象的原型一样,都指向了原始原型,即绿框部分相同;</p>
<p>上面的输出结果佐证了这一点;</p>
<p>从这也可以看出来,其他类都是继承自原始类<code>Object</code>的,只是原型链的长短罢了,最终都可以溯源到原始类<code>Object</code>;</p>
<p>很显然,类与构造函数,很类似;</p>
<p>尽管类对象和函数对象有相似的原型,但是不代表类与构造函数就完全一样了,他们之间的区别还是很大的:</p>
<ul>
<li>
<p>类型不同,定义形式不同</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221221160908747-974819244.png" alt="image-20221221160907543" loading="lazy"></p>
<p>类名后不需要括号,构造函数名后需要加括号;</p>
<p>类的方法声明形式和构造函数的方法不一样;</p>
<p>打印类和构造函数,类前的类型是<code>class</code>,构造函数前的类型是<code>f</code>,即<code>function</code>;</p>
<p>注意,不能使用<code>typeof</code>操作符,它会认为类和构造函数都是<code>function</code>,应该使用<code>instanceof</code>操作符;</p>
</li>
<li>
<p>prototype不一样</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221221161608248-1578691713.png" alt="image-20221221161607599" loading="lazy"></p>
<p>如上所示,类的方法,会成为<code>prototype</code>的方法,但是构造函数的方法不会成为<code>prototype</code>的方法;</p>
<p>函数对象如果想要调用<code>method1()</code>方法,就不能写成<code>let method1 = function(){}</code>,而是<code>this.method1 = function(){}</code>,将其变为函数对象自己的方法;</p>
</li>
<li>
<p>prototype的特性不一样</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221221162206720-1182360703.png" alt="image-20221221162205970" loading="lazy"></p>
<p>类的<code>prototype</code>是不可写的,但是构造函数的<code>prototype</code>是可写的;</p>
</li>
<li>
<p>方法的特性不一样</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221221163659475-29514082.png" alt="image-20221221163658267" loading="lazy"></p>
<p>由于函数对象不能通过原型继承方法,这里只展示类的方法的特性,如上所示,类的方法,是不可枚举的,也即不会被<code>for-in</code>语法遍历到;</p>
</li>
<li>
<p>模式不同</p>
<p>由于类是后来才有的概念,所以类总是使用严格模式,即不需要显示使用<code>use strict</code>,类总是在严格模式下执行;</p>
<p>而构造函数则不同,默认是普通模式,需要显式使用<code>use strict</code>才会在严格模式下执行;</p>
</li>
<li>
<p>[]</p>
<p>类有隐藏属性<code>[]</code>,其值为true;</p>
<p>这要求必须使用<code>new</code>关键字去调用它,像普通函数一样调用会出错:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221221164941257-168616658.png" alt="image-20221221164940516" loading="lazy"></p>
<p>但是很显然,构造函数本身就是一个函数,是可以像普通函数一样去调用的;</p>
</li>
<li>
<p>构造器<code>constructor</code></p>
<p>由于函数对象不能通过原型继承方法,所以无法自定义构造器;</p>
<p>但是类对象可以继承啊,所以可以自定义构造器并在<code>new</code>的时候调用;</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221221170615879-732182643.png" alt="image-20221221170615072" loading="lazy"></p>
<p>从图上可以看出,我们是无法去自定义构造函数的构造器的,它会成为函数对象自己的一个方法;</p>
</li>
</ul>
<h3 id="new-classname的时候发生了什么">new className()的时候发生了什么</h3>
<p>在说明<code>new className()</code>具体流程之前,先了解一下必要的概念:</p>
<h4 id="基类和派生类">基类和派生类</h4>
<pre><code>class classA {};
class classB extends classA {};
</code></pre>
<p>像<code>classA</code>这样没有继承任何类(实际上父原型是<code>Object.prototype</code>)的类称为基类;</p>
<p>像<code>classB</code>这样继承<code>classB</code>的类,称为<code>classB</code>的派生类;</p>
<p>为什么要分的这么细,是因为在创建类对象时,他们两个的行为不同,后面会说到;</p>
<h4 id="重写构造器">重写构造器</h4>
<p>事实上,如果我们不显式自定义构造器,JS也会默认提供一个构造器:</p>
<pre><code class="language-javascript">// 基类
constructor() {}
// 派生类
constructor() {
super();
}
</code></pre>
<p>所以我们要重写构造器,也是分两种情况:</p>
<pre><code class="language-javascript">// 基类重写构造器
class A {
constructor() {
code...
}
}
// 派生类重写构造器
class B extends A() {
constructor() {
// 一定要先写super()
super();
code...
}
}
</code></pre>
<h4 id="基类new-classname时的流程">基类new className()时的流程</h4>
<p>先来看一下基类创建对象的过程:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221224002126851-305259967.png" alt="image-20221224002125631" loading="lazy"></p>
<p>执行<code>let a = new A()</code>时,大致流程如下:</p>
<ul>
<li>首先调用<code>A.prototype</code>的特性<code>[]</code>创建一个字面量对象,同时<code>this</code>指针指向这个字面量对象;</li>
<li>然后执行类<code>A()</code>的定义,<code>A</code>定义的普通属性成为字面量对象的属性并初始化,<code>A.prototype</code>的<code>value</code>值复制给字面量对象的隐藏属性<code>[]</code>;</li>
<li>然后再执行<code>constructor</code>构造器,没有构造器就算了;</li>
<li>返回<code>this</code>指针给变量<code>a</code>,即<code>a</code>此时引用该字面量对象了;</li>
</ul>
<p>从结果上看,在执行构造器时,字面量对象就已经有原型了,以及属性<code>name</code>,且值初始化为<code>tomA</code>;</p>
<p>然后才对属性<code>name</code>重新赋值为<code>jerryA</code>;</p>
<p>然而,构造器中对属性的重新赋值,从一开始就决定好了,只是在执行到这句赋值语句之前,暂存在字面量对象中;</p>
<h4 id="派生类new-classname时的流程">派生类new className()时的流程</h4>
<p>现在再来看一下派生类创建对象的过程;</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221224005352852-1388400372.png" alt="image-20221224005351505" loading="lazy"></p>
<p>执行<code>let b = new B()</code>的大致流程如下:</p>
<ul>
<li>首先调用<code>B.prototype</code>的特性<code>[]</code>创建一个字面量对象,同时<code>this</code>指针指向这个字面量对象;</li>
<li>然后执行类<code>B()</code>的定义,<code>B</code>定义的普通属性成为字面量对象的属性并初始化,<code>B.prototype</code>的<code>value</code>值复制给字面量对象的隐藏属性<code>[]</code>;</li>
<li>然后再执行<code>constructor</code>构造器(没有显式定义构造器会提供默认构造器),第一句<code>super()</code>,开始进入类<code>A()</code>的定义;
<ul>
<li>暂存<code>B</code>的属性值,转而赋值为<code>A</code>定义的值,<code>A.prototype</code>的<code>value</code>值复制给<code>B.__proto__</code>的隐藏属性<code>[]</code>;</li>
<li>然后执行<code>constructor</code>构造器(基类没有构造器就算了);</li>
<li>返回<code>this</code>指针;</li>
<li>丢弃<code>A</code>赋值的属性值,重新使用暂存的<code>B</code>的属性值;</li>
</ul>
</li>
<li>继续执行<code>constructor</code>构造器剩下的语句;</li>
<li>返回<code>this</code>指针给变量<code>b</code>,即<code>b</code>引用该字面量对象了;</li>
</ul>
<h4 id="基类与派生类创建对象时的不同点">基类与派生类创建对象时的不同点</h4>
<p>通过基类和派生类创建对象的流程对比,可以发现主要区别在于类的属性的赋值上;</p>
<p>属性值从一开始就已经暂存好:</p>
<ul>
<li>如果构造器<code>constructor</code>中有赋值,则暂存这个值;</li>
<li>如果构造器没有,则暂存类定义中的值;</li>
<li>不管父类及其原型链上同名的属性在中间进行过几次赋值,最终都会重新覆盖为最开始就暂存好的值;</li>
</ul>
<h2 id="原型链与继承">原型链与继承</h2>
<p>现在应该已经理解了原型是一个什么样的概念,以及如何去访问原型;</p>
<h3 id="原型链">原型链</h3>
<p>正如继承有儿子继承父亲,父亲继承爷爷一样,有这样一个往上溯源的关系,原型也可以这样往上溯源,这就是原型链的概念;</p>
<p>用代码去理解一下吧:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221219222155208-791117248.png" alt="image-20221219222154114" loading="lazy"></p>
<p>我们定义了三个对象A/B/C,并且设置C的原型是B,B的原型是A;</p>
<p>读取<code>C.nameA</code>的时候,首先在C自己的属性里去找,没有找到;</p>
<p>于是去原型B的属性里去找,没有找到;</p>
<p>再去B的原型A的属性里去找,找到并输出;</p>
<p>可以看C展开的一层层结构,可以很清晰的看到原型链的存在;</p>
<p>由此也可以看出,JS是单继承的,同Java一致;</p>
<p>但是正常的继承,肯定不是这样手动去设置对象的原型的,而是自动去设置的;</p>
<h3 id="继承">继承</h3>
<p>JS中表示继承的关键字是<code>extends</code>,如果<code>classA extends classB</code>,则说明<code>classA</code>继承<code>classB</code>,<code>classA</code>是子类,<code>classB</code>是父类;</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221221194843089-275836244.png" alt="image-20221221194842123" loading="lazy"></p>
<p>上面代码,<code>classC</code>继承<code>classB</code>,而<code>classB</code>继承<code>classA</code>;</p>
<p>所以<code>classC</code>的对象,继承了他们的属性,便有了三个属性<code>nameA/nameB/nameC</code>,这也说明,属性是不放在原型里的,而是会在创建对象的时候,直接成为<code>classC</code>的属性;</p>
<p><code>classC</code>的原型,有一个属性一个方法,方法是<code>constructor()</code>构造器指向自己,属性是另一个原型;</p>
<p>注意,打印出来的原型后面标注的<code>classX</code>,原型指的是对象,不是类,所以<code>c</code>的原型不是指<code>classB</code>这个类本身,而是指其来源于<code>classB</code>;</p>
<p>紫色框:对象<code>c</code>的原型,即<code>c.__proto__ == classC.prototype</code>;</p>
<p>橘色框:<code>classB.prototype</code>,即对象<code>c</code>的原型的原型<code>c.__proto__.__proto__ == classB.prototype</code>;</p>
<p>绿色框:<code>classA.prototype</code>,即对象<code>c</code>的原型的原型的原型<code>c.__proto__.__proto__.__proto__ == classA.prototype</code>;</p>
<p>红色框:<code>Object.prototype</code>,也即原始原型<code>c.__proto__.__proto__.__proto__.__proto__ == Object.prototype</code>;</p>
<p>这是一条完整的原型链,从中也能看出继承是什么样的一个形式;</p>
<h3 id="原型高于extends">原型高于extends</h3>
<p>时刻记住,JS的继承,是依靠原型来实现的;</p>
<p>关键字<code>extends</code>虽然确立了两个类的父子关系,但是这只是一开始确立子类的父原型;</p>
<p>但是父原型是可以中途被修改的,此时子类调用方法,是沿着原型链去寻找的,而不是沿着子类父类的关键字声明去寻找的,这和Java是不一样的:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221223233527597-1204674716.png" alt="image-20221223233526394" loading="lazy"></p>
<p>如图所示,<code>C extends A</code>确立了C一开始的父原型是<code>A.prototype</code>,<code>c.show()</code>调用的也是父类<code>A</code>的方法;</p>
<p>但是后面修改<code>c</code>的父原型为<code>B.prototype</code>,<code>c.show</code>调用的就不是父类<code>A</code>的方法,而是父原型的方法;</p>
<p>也就是说,原型才是核心,高于<code>extends</code>关键字;</p><br><br>
来源:https://www.cnblogs.com/Journing/p/17000813.html
頁:
[1]