整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          JavaScript:class繼承

          上面的章節中我們看到了JavaScript的對象模型是基于原型實現的,特點是簡單,缺點是理解起來比傳統的類-實例模型要困難,最大的缺點是繼承的實現需要編寫大量代碼,并且需要正確實現原型鏈。

          有沒有更簡單的寫法?有!

          新的關鍵字class從ES6開始正式被引入到JavaScript中。class的目的就是讓定義類更簡單。

          我們先回顧用函數實現Student的方法:

          function Student(name) {
           this.name = name;
          }
          Student.prototype.hello = function () {
           alert('Hello, ' + this.name + '!');
          }
          

          如果用新的class關鍵字來編寫Student,可以這樣寫:

          class Student {
           constructor(name) {
           this.name = name;
           }
           hello() {
           alert('Hello, ' + this.name + '!');
           }
          }
          

          比較一下就可以發現,class的定義包含了構造函數constructor和定義在原型對象上的函數hello()(注意沒有function關鍵字),這樣就避免了Student.prototype.hello = function () {...}這樣分散的代碼。

          最后,創建一個Student對象代碼和前面章節完全一樣:

          var xiaoming = new Student('小明');
          xiaoming.hello();
          

          class繼承

          用class定義對象的另一個巨大的好處是繼承更方便了。想一想我們從Student派生一個PrimaryStudent需要編寫的代碼量。現在,原型繼承的中間對象,原型對象的構造函數等等都不需要考慮了,直接通過extends來實現:

          class PrimaryStudent extends Student {
           constructor(name, grade) {
           super(name); // 記得用super調用父類的構造方法!
           this.grade = grade;
           }
           myGrade() {
           alert('I am at grade ' + this.grade);
           }
          }
          

          注意PrimaryStudent的定義也是class關鍵字實現的,而extends則表示原型鏈對象來自Student。子類的構造函數可能會與父類不太相同,例如,PrimaryStudent需要name和grade兩個參數,并且需要通過super(name)來調用父類的構造函數,否則父類的name屬性無法正常初始化。

          PrimaryStudent已經自動獲得了父類Student的hello方法,我們又在子類中定義了新的myGrade方法。

          ES6引入的class和原有的JavaScript原型繼承有什么區別呢?實際上它們沒有任何區別,class的作用就是讓JavaScript引擎去實現原來需要我們自己編寫的原型鏈代碼。簡而言之,用class的好處就是極大地簡化了原型鏈代碼。

          你一定會問,class這么好用,能不能現在就用上?

          現在用還早了點,因為不是所有的主流瀏覽器都支持ES6的class。如果一定要現在就用上,就需要一個工具把class代碼轉換為傳統的prototype代碼,可以試試Babel這個工具。

          需要瀏覽器支持ES6的class,如果遇到SyntaxError,則說明瀏覽器不支持class語法,請換一個最新的瀏覽器試試。

          面向對象的編程中,class 是用于創建對象的可擴展的程序代碼模版,它為對象提供了狀態(成員變量)的初始值和行為(成員函數或方法)的實現。

          Wikipedia

          在日常開發中,我們經常需要創建許多相同類型的對象,例如用戶(users)、商品(goods)或者任何其他東西。

          正如我們在 構造器和操作符 "new" 一章中已經學到的,new function 可以幫助我們實現這種需求。

          但在現代 JavaScript 中,還有一個更高級的“類(class)”構造方式,它引入許多非常棒的新功能,這些功能對于面向對象編程很有用。

          一、“class” 語法

          基本語法是:

          class MyClass {
            // class 方法
            constructor() { ... }
            method1() { ... }
            method2() { ... }
            method3() { ... }
            ...
          }

          然后使用 new MyClass() 來創建具有上述列出的所有方法的新對象。

          new 會自動調用 constructor() 方法,因此我們可以在 constructor() 中初始化對象。

          例如:

          class User {
          
            constructor(name) {
              this.name = name;
            }
          
            sayHi() {
              alert(this.name);
            }
          
          }
          
          // 用法:
          let user = new User("John");
          user.sayHi();

          new User("John") 被調用:

          1. 一個新對象被創建。
          2. constructor 使用給定的參數運行,并為其分配 this.name

          ……然后我們就可以調用對象方法了,例如 user.sayHi

          類的方法之間沒有逗號

          對于新手開發人員來說,常見的陷阱是在類的方法之間放置逗號,這會導致語法錯誤。

          不要把這里的符號與對象字面量相混淆。在類中,不需要逗號。

          二、什么是 class?

          所以,class 到底是什么?正如人們可能認為的那樣,這不是一個全新的語言級實體。

          讓我們揭開其神秘面紗,看看類究竟是什么。這將有助于我們理解許多復雜的方面。

          在 JavaScript 中,類是一種函數。

          看看下面這段代碼:

          class User {
            constructor(name) { this.name = name; }
            sayHi() { alert(this.name); }
          }
          
          // 佐證:User 是一個函數
          alert(typeof User); // function

          class User {...} 構造實際上做了如下的事兒:

          1. 創建一個名為 User 的函數,該函數成為類聲明的結果。該函數的代碼來自于 constructor 方法(如果我們不編寫這種方法,那么它就被假定為空)。
          2. 存儲類中的方法,例如 User.prototype 中的 sayHi

          new User 對象被創建后,當我們調用其方法時,它會從原型中獲取對應的方法,正如我們在 F.prototype 一章中所講的那樣。因此,對象 new User 可以訪問類中的方法。

          我們可以將 class User 聲明的結果解釋為:

          下面這些代碼很好地解釋了它們:

          class User {
            constructor(name) { this.name = name; }
            sayHi() { alert(this.name); }
          }
          
          // class 是一個函數
          alert(typeof User); // function
          
          // ...或者,更確切地說,是 constructor 方法
          alert(User === User.prototype.constructor); // true
          
          // 方法在 User.prototype 中,例如:
          alert(User.prototype.sayHi); // alert(this.name);
          
          // 在原型中實際上有兩個方法
          alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

          三、不僅僅是語法糖

          人們常說 class 是一個語法糖(旨在使內容更易閱讀,但不引入任何新內容的語法),因為我們實際上可以在沒有 class 的情況下聲明相同的內容:

          // 用純函數重寫 class User
          
          // 1. 創建構造器函數
          function User(name) {
            this.name = name;
          }
          // 函數的原型(prototype)默認具有 "constructor" 屬性,
          // 所以,我們不需要創建它
          
          // 2. 將方法添加到原型
          User.prototype.sayHi = function() {
            alert(this.name);
          };
          
          // 用法:
          let user = new User("John");
          user.sayHi();

          這個定義的結果與使用類得到的結果基本相同。因此,這確實是將 class 視為一種定義構造器及其原型方法的語法糖的理由。

          盡管,它們之間存在著重大差異:

          1. 首先,通過 class 創建的函數具有特殊的內部屬性標記 [[FunctionKind]]:"classConstructor"。因此,它與手動創建并不完全相同。編程語言會在許多地方檢查該屬性。例如,與普通函數不同,必須使用 new 來調用它:class User { constructor() {} } alert(typeof User); // function User(); // Error: Class constructor User cannot be invoked without 'new'此外,大多數 JavaScript 引擎中的類構造器的字符串表示形式都以 “class…” 開頭class User { constructor() {} } alert(User); // class User { ... }還有其他的不同之處,我們很快就會看到。
          2. 類方法不可枚舉。 類定義將 "prototype" 中的所有方法的 enumerable 標志設置為 false。這很好,因為如果我們對一個對象調用 for..in 方法,我們通常不希望 class 方法出現。
          3. 類總是使用 use strict。 在類構造中的所有代碼都將自動進入嚴格模式。

          此外,class 語法還帶來了許多其他功能,我們稍后將會探索它們。

          四、類表達式

          就像函數一樣,類可以在另外一個表達式中被定義,被傳遞,被返回,被賦值等。

          這是一個類表達式的例子:

          let User = class {
            sayHi() {
              alert("Hello");
            }
          };

          類似于命名函數表達式(Named Function Expressions),類表達式可能也應該有一個名字。

          如果類表達式有名字,那么該名字僅在類內部可見:

          // “命名類表達式(Named Class Expression)”
          // (規范中沒有這樣的術語,但是它和命名函數表達式類似)
          let User = class MyClass {
            sayHi() {
              alert(MyClass); // MyClass 這個名字僅在類內部可見
            }
          };
          
          new User().sayHi(); // 正常運行,顯示 MyClass 中定義的內容
          
          alert(MyClass); // error,MyClass 在外部不可見

          我們甚至可以動態地“按需”創建類,就像這樣:

          function makeClass(phrase) {
            // 聲明一個類并返回它
            return class {
              sayHi() {
                alert(phrase);
              }
            };
          }
          
          // 創建一個新的類
          let User = makeClass("Hello");
          
          new User().sayHi(); // Hello

          五、Getters/setters

          就像對象字面量,類可能包括 getters/setters,計算屬性(computed properties)等。

          這是一個使用 get/set 實現 user.name 的示例:

          class User {
          
            constructor(name) {
              // 調用 setter
              this.name = name;
            }
          
            get name() {
              return this._name;
            }
          
            set name(value) {
              if (value.length < 4) {
                alert("Name is too short.");
                return;
              }
              this._name = value;
            }
          
          }
          
          let user = new User("John");
          alert(user.name); // John
          
          user = new User(""); // Name is too short.

          從技術上來講,這樣的類聲明可以通過在 User.prototype 中創建 getters 和 setters 來實現。

          六、計算屬性名稱 […]

          這里有一個使用中括號 [...] 的計算方法名稱示例:

          class User {
          
            ['say' + 'Hi']() {
              alert("Hello");
            }
          
          }
          
          new User().sayHi();

          這種特性很容易記住,因為它們和對象字面量類似。

          七、Class 字段

          舊的瀏覽器可能需要 polyfill

          類字段(field)是最近才添加到語言中的。

          之前,我們的類僅具有方法。

          “類字段”是一種允許添加任何屬性的語法。

          例如,讓我們在 class User 中添加一個 name 屬性:

          class User {
            name = "John";
          
            sayHi() {
              alert(`Hello, ${this.name}!`);
            }
          }
          
          new User().sayHi(); // Hello, John!

          所以,我們就只需在表達式中寫 " = ",就這樣。

          類字段重要的不同之處在于,它們會在每個獨立對象中被設好,而不是設在 User.prototype

          class User {
            name = "John";
          }
          
          let user = new User();
          alert(user.name); // John
          alert(User.prototype.name); // undefined

          我們也可以在賦值時使用更復雜的表達式和函數調用:

          class User {
            name = prompt("Name, please?", "John");
          }
          
          let user = new User();
          alert(user.name); // John

          八、使用類字段制作綁定方法

          正如 函數綁定 一章中所講的,JavaScript 中的函數具有動態的 this。它取決于調用上下文。

          因此,如果一個對象方法被傳遞到某處,或者在另一個上下文中被調用,則 this 將不再是對其對象的引用。

          例如,此代碼將顯示 undefined

          class Button {
            constructor(value) {
              this.value = value;
            }
          
            click() {
              alert(this.value);
            }
          }
          
          let button = new Button("hello");
          
          setTimeout(button.click, 1000); // undefined

          這個問題被稱為“丟失 this”。

          我們在 函數綁定 一章中講過,有兩種可以修復它的方式:

          1. 傳遞一個包裝函數,例如 setTimeout(() => button.click(), 1000)
          2. 將方法綁定到對象,例如在 constructor 中。

          類字段提供了另一種非常優雅的語法:

          class Button {
            constructor(value) {
              this.value = value;
            }
            click = () => {
              alert(this.value);
            }
          }
          
          let button = new Button("hello");
          
          setTimeout(button.click, 1000); // hello

          類字段 click = () => {...} 是基于每一個對象被創建的,在這里對于每一個 Button 對象都有一個獨立的方法,在內部都有一個指向此對象的 this。我們可以把 button.click 傳遞到任何地方,而且 this 的值總是正確的。

          在瀏覽器環境中,它對于進行事件監聽尤為有用。

          九、總結

          基本的類語法看起來像這樣:

          class MyClass {
            prop = value; // 屬性
          
            constructor(...) { // 構造器
              // ...
            }
          
            method(...) {} // method
          
            get something(...) {} // getter 方法
            set something(...) {} // setter 方法
          
            [Symbol.iterator]() {} // 有計算名稱(computed name)的方法(此處為 symbol)
            // ...
          }

          技術上來說,MyClass 是一個函數(我們提供作為 constructor 的那個),而 methods、getters 和 settors 都被寫入了 MyClass.prototype

          在下一章,我們將會進一步學習類的相關知識,包括繼承和其他功能。

          類是用于創建對象的模板。JavaScript中生成對象實例的方法是通過構造函數,這跟主流面向對象語言(java,C#)寫法上差異較大,如下:

          function Point(x, y) {
            this.x = x;
            this.y = y;
          }
          
          Point.prototype.toString = function () {
            return '(' + this.x + ', ' + this.y + ')';
          };
          
          var p = new Point(1, 1);

          ES6 提供了更接近Java語言的寫法,引入了 Class(類)這個概念,作為對象的模板。通過class關鍵字,可以定義類。

          如下:constructor()是構造方法,而this代表實例對象:

          class Point {
            constructor(x, y) {
              this.x = x;
              this.y = y;
            }
          
            toString() {
              return '(' + this.x + ', ' + this.y + ')';
            }
          }

          類的數據類型就是函數,它本身就是指向函數的構造函數:

          // ES5 函數聲明
          function Point() {
              //...
          }
          
          // ES6 類聲明
          class Point {
            //....
            constructor() {
            }
          }
          typeof Point // "function"
          Point === Point.prototype.constructor // true

          在類里面定義的方法是掛到Point.prototype,所以類只是提供了語法糖,本質還是原型鏈調用。

          class Point {
            constructor(x, y) {
              this.x = x;
              this.y = y;
            }
          
            toString() {
              return '(' + this.x + ', ' + this.y + ')';
            }
          }
          
          Point.prototype = {
            //....
            toString()
          }
          var p = new Point(1, 1);
          p.toString() // (1,1)

          類的另一種定義方式類表達式

          // 未命名/匿名類
          let Point = class {
            constructor(x, y) {
              this.x = x;
              this.y = y;
            }
          };
          Point.name // Point

          函數聲明和類聲明有個重要區別,函數聲明會提升,類聲明不會提升。

          constructor()

          constructor()方法是類的默認方法,new生成實例對象時會自動調用該方法。

          一個類必須有constructor()方法,如果沒有顯式定義,引擎會默認添加一個空的constructor()

          constructor()方法默認返回實例對象(即this)。

          class Point {
          }
          
          // 自動添加
          class Point {
            constructor() {}
          }

          getter和setter

          與 ES5 一樣,在類的內部可以使用getset關鍵字,對某個屬性設置存值函數和取值函數,攔截該屬性的存取行為。

          class User {
            constructor(name) {
              this.name = name;
            }
          
            get name() {
              return this.name;
            }
          
            set name(value) {
              this.name = value;
            }
          }
          
          

          this

          類的方法內部的this,它默認指向類的實例,在調用存在this的方法時,需要使用 obj.method()方式,否則會報錯。

          class User {
            constructor(name) {
              this.name = name;
            }
            printName(){
              console.log('Name is ' + this.name)
            }
          }
          const user = new User('jack')
          user.printName() // Name is jack
          const { printName } = user;
          printName()     // 報錯 Cannot read properties of undefined (reading 'name')

          如果要單獨調用又不報錯,一種方法可以在構造方法里調用bind(this)

          class User {
            constructor(name) {
              this.name = name;
              this.printName = this.printName.bind(this);
            }
            printName(){
              console.log('Name is ' + this.name)
            }
          }
          const user = new User('jack')
          const { printName } = user;
          printName()     // Name is jack

          bind(this) 會創建一個新函數,并將傳入的this作為該函數在調用時上下文指向。

          另外可以使用箭頭函數,因為箭頭函數內部的this總是指向定義時所在的對象。

          class User {
            constructor(name) {
              this.name = name;
            }
            printName = () => {
              console.log('Name is ' + this.name)
            }
          }
          const user = new User('jack')
          const { printName } = user;
          printName()     // Name is jack

          靜態屬性

          靜態屬性指的是類本身的屬性,而不是定義在實例對象this上的屬性。

          class User {
          }
          
          User.prop = 1;
          User.prop // 1

          靜態方法

          可以在類里面定義靜態方法,該方法不會被對象實例繼承,而是直接通過類來調用。

          靜態方法里使用this是指向類。

          class Utils {
            static printInfo() {
               this.info();
            }
            static info() {
               console.log('hello');
            }
          }
          Utils.printInfo() // hello

          關于方法的調用范圍限制,比如:私有公有,ES6暫時沒有提供,一般是通過約定,比如:在方法前面加下劃線_print()表示私有方法。

          繼承

          Java中通過extends實現類的繼承。ES6中類也可以通過extends實現繼承。

          繼承時,子類必須在constructor方法中調用super方法,否則新建實例時會報錯。

          class Point3D extends Point {
            constructor(x, y, z) {
              super(x, y); // 調用父類的constructor(x, y)
              this.z = z;
            }
          
            toString() {
              return super.toString() + '  ' + this.z ; // 調用父類的toString()
            }
          }

          父類的靜態方法,也會被子類繼承。

          class Parent {
            static info() {
              console.log('hello world');
            }
          }
          
          class Child extends Parent {
          }
          
          Child.info()  // hello world
          
          

          super關鍵字

          在子類的構造函數必須執行一次super函數,它代表了父類的構造函數。

          class Parent {}
          
          class Child extends Parent {
            constructor() {
              super();
            }
          }

          在子類普通方法中通過super調用父類的方法時,方法內部的this指向當前的子類實例。

          class Parent {
            constructor() {
              this.x = 1;
              this.y = 10
            }
            printParent() {
              console.log(this.y);
            }
            print() {
              console.log(this.x);
            }
          }
          
          class Child extends Parent {
            constructor() {
              super();
              this.x = 2;
            }
            m() {
              super.print();
            }
          }
          
          let c = new Child();
          c.printParent() // 10
          c.m() // 2

          _proto_和prototype

          初學JavaScript時,_proto_prototype 很容易混淆。首先我們知道每個JS對象都會對應一個原型對象,并從原型對象繼承屬性和方法。

          • prototype 一些內置對象和函數的屬性,它是一個指針,指向一個對象,這個對象的用途就是包含所有實例共享的屬性和方法(我們把這個對象叫做原型對象)。
          • _proto_ 每個對象都有這個屬性,一般指向對應的構造函數的prototype屬性。

          下圖是一些擁有prototype內置對象。

          prototype

          根據上面描述,看下面代碼

          var obj = {} // 等同于 var obj = new Object()
          
          // obj.__proto__指向Object構造函數的prototype
          obj.__proto__ === Object.prototype // true 
          
          // obj.toString 調用方法從Object.prototype繼承
          obj.toString === obj.__proto__.toString // true
          
          // 數組
          var arr = []
          arr.__proto__ === Array.prototype // true 

          對于function對象,聲明的每個function同時擁有prototype__proto__屬性,創建的對象屬性__proto__指向函數prototype,函數的__proto__又指向內置函數對象(Function)的prototype

          function Foo(){}
          var f = new Foo();
          f.__proto__ === Foo.prototype // true
          Foo.__proto__ === Function.prototype // true
          
          

          繼承中的__proto__

          類作為構造函數的語法糖,也會同時有prototype屬性和__proto__屬性,因此同時存在兩條繼承鏈。

          1. 子類的__proto__屬性,表示構造函數的繼承,總是指向父類。
          2. 子類prototype屬性的__proto__屬性,表示方法的繼承,總是指向父類的prototype屬性。
          class Parent {
          }
          
          class Child extends Parent {
          }
          
          Child.__proto__ === Parent // true
          Child.prototype.__proto__ === Parent.prototype // true

          繼承實例中的__proto__

          子類實例的__proto__屬性,指向子類構造方法的prototype

          子類實例的__proto__屬性的__proto__屬性,指向父類實例的__proto__屬性。也就是說,子類的原型的原型,是父類的原型。

          class Parent {
          }
          
          class Child extends Parent {
          }
          
          var p = new Parent();
          var c = new Child();
          
          c.__proto__ === p.__proto__ // false
          c.__proto__ === Child.prototype // true
          c.__proto__.__proto__ === p.__proto__ // true
          
          

          小結

          JavaScript中的Class更多的還是語法糖,本質上繞不開原型鏈。歡迎大家留言交流。


          主站蜘蛛池模板: 国产亚洲福利精品一区| 国产AV一区二区三区传媒| 无码人妻精品一区二区三18禁| 中文字幕无线码一区2020青青| 成人H动漫精品一区二区| 天码av无码一区二区三区四区 | 国产在线精品一区二区不卡麻豆| 中日av乱码一区二区三区乱码| 中文字幕乱码一区久久麻豆樱花| 曰韩精品无码一区二区三区| 在线免费视频一区| 日韩一区二区在线观看| 玩弄放荡人妻一区二区三区| 日韩精品一区二区三区色欲AV| 精品黑人一区二区三区| 国产一区二区三区免费观看在线| 日本免费一区尤物| 色偷偷av一区二区三区| 中文字幕一区二区三区视频在线| 在线免费视频一区二区| AV无码精品一区二区三区| 久久精品视频一区| 国产美女av在线一区| 一区二区三区精品高清视频免费在线播放 | 无码人妻一区二区三区在线视频 | 国模大尺度视频一区二区| 无码一区二区三区在线| 97久久精品无码一区二区天美| 另类免费视频一区二区在线观看| 国产一区二区三区不卡在线看 | 亚洲国产成人精品无码一区二区| 台湾无码AV一区二区三区| 亚洲国产精品一区二区九九| 国产aⅴ一区二区| 色一乱一伦一区一直爽| 国产精品综合一区二区| 濑亚美莉在线视频一区| 欧美激情一区二区三区成人| 51视频国产精品一区二区| 日韩免费无码一区二区三区| 久久中文字幕一区二区|