三分钟折纸 發表於 2022-12-24 13:23:00

JavaScript:类(class)

<p>在JS中,类是后来才出的概念,早期创造对象的方式是<code>new Function()</code>调用构造函数创建函数对象;</p>
<p>而现在,可以使用<code>new className()</code>构造方法来创建类对象了;</p>
<p>所以在很多方面,类的使用方式,很像函数的使用方式:</p>
<p>但是类跟函数,还是有本质区别的,这在<strong>原型</strong>那里已经说过,不再赘述;</p>
<h2 id="如何定义一个类">如何定义一个类</h2>
<p>如下所示去定义一个类:</p>
<pre><code class="language-javascript">class className {
    // 属性properties
    property1 = 1;
    property2 = [];
    peoperty3 = {};
    property4 = function() {};
    property5 = () =&gt; {};
   
    // 构造器
    constructor(...args) {
      super();
      // code here
    };
   
    // 方法methods
    method1() {
      // code here
    };
    method2(...args) {
      //code here
    };
}
</code></pre>
<p>可以定义成员属性和成员方法以及构造器,他们之间都有封号<code>;</code>隔开;</p>
<p>在通过<code>new className()</code>创建对象<code>obj</code>的时候,会立即执行构造器方法;</p>
<p>属性会成为<code>obj</code>的属性,句式为赋值语句,就算等号右边是函数,它也依然是一个属性,注意与方法声明语句区别开;</p>
<p>方法会成为<code>obj</code>的原型里的方法,即放在<code>className.prototype</code>属性里;</p>
<h2 id="像使用function一样使用class关键字">像使用<code>function</code>一样使用<code>class</code>关键字</h2>
<p>正如函数表达式一样,类也有类表达式:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221221213522228-1790026191.png" alt="image-20221221213521212" loading="lazy"></p>
<p>还可以像传递一个函数一样,去传递一个类:</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221221213653606-2101391042.png" alt="image-20221221213652757" loading="lazy"></p>
<p>这在Java中是不可想象的,但是在JS中,就是这么灵活;</p>
<h2 id="静态属性和静态方法">静态属性和静态方法</h2>
<p>静态属性和静态方法,不会成为对象的属性和方法,永远都属于类本身,只能通过类去调用;</p>
<ul>
<li>
<p>定义语法</p>
<pre><code class="language-javascript">// 直接在类中,通过static关键字定义
class className {
    static property = ...;
    static methoed() {};
}

// 通过类直接添加属性和方法,即为静态的
class className {};
className.property = ...;
className.method = function() {};
</code></pre>
</li>
<li>
<p>调用语法</p>
<p>类似于对象调用属性和方法,直接通过类名去调用</p>
<pre><code>className.property;
className.method();
</code></pre>
</li>
</ul>
<p>静态属性/方法,可以和普通属性/方法同名,这不会被弄混,因为他们的调用者不一样,前者是类,后者是类对象;</p>
<h2 id="私有属性和私有方法">私有属性和私有方法</h2>
<p>JS新增的私有特性,在属性和方法之前添加<code>#</code>号,使其只在类中可见,对象无法调用,只能通过类提供的普通方法去间接访问;</p>
<ul>
<li>
<p>定义和调用语法</p>
<pre><code class="language-javascript">class className {
    // 定义,添加#号
    #property = ...;
    #method() {};
   
    // 只能在类中可见,调用也需要加#号
    getProperty() {
      return this.#property;
    }
    set property(value) {
      this.#property = value;
    }
}
</code></pre>
</li>
</ul>
<p>注意,<code>#property</code>是一个总体作为属性名,与<code>property</code>是不同的,<code>#method</code>同理;</p>
<p>在这个私有特性之前,JS采用人为约定的方式,去间接实现私有;</p>
<p>在属性和方法之前添加下划线<code>_</code>,约定这样的属性和方法,只能在类中可见,只能靠人为遵守这样的约定;</p>
<h2 id="类检查instanceof">类检查instanceof</h2>
<p>我们知道,可以用<code>typeof</code>关键字来获取一个变量是什么数据类型;</p>
<p>现在可以用<code>instanceof</code>关键字,来判断一个对象是什么类的实例;</p>
<p>语法<code>obj instanceof className</code>,会返回一个布尔值:</p>
<ul>
<li>如果<code>className</code>是<code>obj</code>原型链上的类,返回true;</li>
<li>否则,返回false;</li>
</ul>
<p>它是怎么去判断的呢?假设现在有如下几个类:</p>
<pre><code class="language-javascript">class A {};
class B extends A {};
class C extends B {};
let c = new C();
</code></pre>
<p>c的原型是<code>C.prototype</code>;</p>
<p><code>C.prototype</code>的原型是<code>B.prototype</code>;</p>
<p><code>B.prototype</code>的原型是<code>A.prototype</code>;</p>
<p><code>A.prototype</code>的原型是<code>Object.prototype</code>;</p>
<p><code>Object.prototype</code>的原型是null;</p>
<p>原型链如上所示;</p>
<p>当我们执行<code>c instanceof A</code>的时候,它是这样的过程:</p>
<p><code>c.__proto__ === A.prototype</code>?否,则继续;</p>
<p><code>c.__proto__.__proto__ === A.prototype</code>?否,则继续;</p>
<p><code>c.__proto__.__proto__.__proto__ === A.prototype</code>?是,返回true;</p>
<p>如果一直否的话,这个过程会持续下去,直到将<code>c</code>的原型链溯源到null,全都不等于<code>A.prototype</code>,则返回false;</p>
<p>也就是说,instanceof关键字,比较的是对象的原型链上的原型和目标类的prototype是否相等(原型和prototype里有<code>constructor</code>,但是instanceof不会比较构造器是否相等,只会比较隐藏属性<code>[]</code>);</p>
<h3 id="静态方法symbolhasinstance">静态方法Symbol.hasInstance</h3>
<p>大多数类是没有实现静态方法<code></code>的,如果有一个类实现了这个静态方法,那么instanceof关键字会直接调用这个静态方法;</p>
<p>如果类没有实现这个静态方法,那么则会按照上述说的流程去检查;</p>
<pre><code class="language-javascript">class className {
    static () {};
}
</code></pre>
<h3 id="objaisprototypeofobjb">objA.isPrototypeOf(objB)</h3>
<p><code>isPrototypeOf()</code>方法,会判断<code>objA</code>的原型是否处在<code>objB</code>的原型链中,如果在则返回true,否则返回false;</p>
<p><code>objA.isPrototypeOf(objB)</code>就相当于<code>objB instanceof classA</code>;</p>
<p>反过来,<code>objB instanceof classA</code>就相当于<code>classA.prototype.isPrototypeOf(objB)</code>;</p>
<h2 id="继承">继承</h2>
<p>我们知道,JS的继承,是通过原型来实现的,现在结合原型来说一下类的继承相关内容。</p>
<h3 id="关键字extends">关键字extends</h3>
<p>JS中表示继承的关键字是<code>extends</code>,如果<code>classA extends classB</code>,则说明<code>classA</code>继承<code>classB</code>,<code>classA</code>是子类,<code>classB</code>是父类;</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>
<h3 id="基类和派生类">基类和派生类</h3>
<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>
<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="类对象的原型和类的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-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>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221223221757230-604944082.png" alt="image-20221223221756177" loading="lazy"></p>
<p>从结果上可以看到,虽然子类直接将父类的普通属性作为自己的普通属性,但是当出现同名属性,属性值会进行覆盖,最终的值采用子类自己定义的值;</p>
<h3 id="同名方法的重写">同名方法的重写</h3>
<p>与属性一样,子类和父类也可能会出现同名方法;</p>
<p>当然大多数情况下,是我们自己要拓展方法功能而故意同名,从而重写父类的方法;</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221223222234399-342545278.png" alt="image-20221223222233351" loading="lazy"></p>
<p>如上所示,我们重写了父类的静态方法和普通方法;</p>
<p>如果是重写构造器的话,分两种情况:</p>
<pre><code class="language-javascript">// 基类重写构造器
class A {
    constructor() {
      code...
    }
}
   
// 派生类重写构造器
class B extends A() {
    constructor() {
      // 一定要先写super()
      super();
      code...
    }
}
</code></pre>
<h3 id="子类的调用顺序">子类的调用顺序</h3>
<p>从上图还可以看出来,子类调用方法的顺序:</p>
<ul>
<li>先从自己的方法里调用,发现没有可调用的方法时;</li>
<li>再沿着原型链,先从父类开始寻找方法,一直往上溯源,直到找到可调用的方法,或者没有而出错;</li>
</ul>
<h3 id="super关键字">super关键字</h3>
<p>类的方法里,有一个特殊的、专门用于<code>super</code>关键字的特殊属性<code>[]</code>,这个属性绑定<code>super</code>语句所在的类的对象,不会改变;</p>
<p>而<code>super</code>关键字,则指向<code>[]</code>绑定的对象的类的父类的<code>prototype</code>;</p>
<p>这要求,<code>super</code>关键字用于派生类类的方法里,基类是不可以使用<code>super</code>的,因为没有父类;</p>
<p>当我们使用<code>super</code>关键字时,借助于<code>[]</code>,总是能够正确重用父类方法;</p>
<p><img src="https://img2023.cnblogs.com/blog/2576484/202212/2576484-20221223225447236-386048013.png" alt="image-20221223225446030" loading="lazy"></p>
<p>如上,<code>super</code>语句所在的类为<code>B</code>,其对象为<code>b</code>,即<code>[]</code>绑定<code>b</code>;</p>
<p>而<code>super</code>则指向<code>b</code>的类的父原型,即<code>A</code>的prototype属性;</p>
<p>而<code>super.show()</code>就类似于<code>A.prototype.show()</code>,故而最终结果如上所示;</p>
<p>可以简单理解成,<strong>super指向子类对象的父类的<code>prototype</code></strong>;</p>
<h3 id="构造器constructor">构造器constructor</h3>
<p>终于说到构造器了,理解了构造器的具体创建对象的过程,我们就能理解关于继承的很多内容了;</p>
<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>
<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>
<p>通过基类和派生类创建对象的流程对比,可以发现主要区别在于类的属性的赋值上;</p>
<p>属性值从一开始就已经暂存好:</p>
<ul>
<li>如果构造器<code>constructor</code>中有赋值,则暂存这个值;</li>
<li>如果构造器没有,则暂存类定义中的值;</li>
<li>不管父类及其原型链上同名的属性在中间进行过几次赋值,最终都会重新覆盖为最开始就暂存好的值;</li>
</ul><br><br>
来源:https://www.cnblogs.com/Journing/p/17002793.html
頁: [1]
查看完整版本: JavaScript:类(class)