• <fieldset id="8imwq"><menu id="8imwq"></menu></fieldset>
  • <bdo id="8imwq"><input id="8imwq"></input></bdo>
    最新文章專題視頻專題問答1問答10問答100問答1000問答2000關鍵字專題1關鍵字專題50關鍵字專題500關鍵字專題1500TAG最新視頻文章推薦1 推薦3 推薦5 推薦7 推薦9 推薦11 推薦13 推薦15 推薦17 推薦19 推薦21 推薦23 推薦25 推薦27 推薦29 推薦31 推薦33 推薦35 推薦37視頻文章20視頻文章30視頻文章40視頻文章50視頻文章60 視頻文章70視頻文章80視頻文章90視頻文章100視頻文章120視頻文章140 視頻2關鍵字專題關鍵字專題tag2tag3文章專題文章專題2文章索引1文章索引2文章索引3文章索引4文章索引5123456789101112131415文章專題3
    問答文章1 問答文章501 問答文章1001 問答文章1501 問答文章2001 問答文章2501 問答文章3001 問答文章3501 問答文章4001 問答文章4501 問答文章5001 問答文章5501 問答文章6001 問答文章6501 問答文章7001 問答文章7501 問答文章8001 問答文章8501 問答文章9001 問答文章9501
    當前位置: 首頁 - 科技 - 知識百科 - 正文

    老生常談ES6中的類

    來源:懂視網 責編:小采 時間:2020-11-27 22:33:46
    文檔

    老生常談ES6中的類

    老生常談ES6中的類:前面的話 大多數面向對象的編程語言都支持類和類繼承的特性,而JS卻不支持這些特性,只能通過其他方法定義并關聯多個相似的對象,這種狀態一直延續到了ES5。由于類似的庫層出不窮,最終還是在ECMAScript 6中引入了類的特性。本文將詳細介紹ES6中的類 ES
    推薦度:
    導讀老生常談ES6中的類:前面的話 大多數面向對象的編程語言都支持類和類繼承的特性,而JS卻不支持這些特性,只能通過其他方法定義并關聯多個相似的對象,這種狀態一直延續到了ES5。由于類似的庫層出不窮,最終還是在ECMAScript 6中引入了類的特性。本文將詳細介紹ES6中的類 ES

    前面的話

    大多數面向對象的編程語言都支持類和類繼承的特性,而JS卻不支持這些特性,只能通過其他方法定義并關聯多個相似的對象,這種狀態一直延續到了ES5。由于類似的庫層出不窮,最終還是在ECMAScript 6中引入了類的特性。本文將詳細介紹ES6中的類

    ES5近似結構

    在ES5中沒有類的概念,最相近的思路是創建一個自定義類型:首先創建一個構造函數,然后定義另一個方法并賦值給構造函數的原型

    function PersonType(name) {
     this.name = name;
    }
    PersonType.prototype.sayName = function() {
     console.log(this.name);
    };
    let person = new PersonType("huochai");
    person.sayName(); // 
    輸出 "huochai" console.log(person instanceof PersonType); // true console.log(person instanceof Object); // true

    這段代碼中的personType是一個構造函數,其執行后創建一個名為name的屬性給personType的原型添加一個sayName()方法,所以PersonType對象的所有實例都將共享這個方法。然后使用new操作符創建一個personType的實例person,并最終證實了person對象確實是personType的實例,且由于存在原型繼承的特性,因而它也是object的實例

    許多模擬類的JS庫都是基于這個模式進行開發,而且ES6中的類也借鑒了類似的方法

    類的聲明

    ES6有一種與其他語言中類似的類特性:類聲明。同時,它也是ES6中最簡單的類形式

    【基本的類聲明語法】

    要聲明一個類,首先編寫class關鍵字,緊跟著的是類的名字,其他部分的語法類似于對象字面量方法的簡寫形式,但不需要在類的各元素之間使用逗號分隔

    class PersonClass {
     // 等價于 PersonType 構造器
     constructor(name) {
     this.name = name;
     }
     // 等價于 PersonType.prototype.sayName
     sayName() {
     console.log(this.name);
     }
    }
    let person = new PersonClass("huochai");
    person.sayName(); // 
    輸出 "huochai" console.log(person instanceof PersonClass); // true console.log(person instanceof Object); // true console.log(typeof PersonClass); // "function" console.log(typeof PersonClass.prototype.sayName); // "function"

    通過類聲明語法定義PersonClass的行為與之前創建PersonType構造函數的過程相似,只是這里直接在類中通過特殊的constructor方法名來定義構造函數,且由于這種類使用簡潔語法來定義方法,因而不需要添加function關鍵字。除constructor外沒有其他保留的方法名,所以可以盡情添加方法

    私有屬性是實例中的屬性,不會出現在原型上,且只能在類的構造函數或方法中創建,此例中的name就是一個私有屬性。建議在構造函數中創建所有私有屬性,從而只通過一處就可以控制類中的所有私有屬性

    類聲明僅僅是基于已有自定義類型聲明的語法糖。typeofPersonClass最終返回的結果是"function",所以PersonClass聲明實際上創建了一個具有構造函數方法行為的函數。此示例中的sayName()方法實際上是PersonClass.prototype上的一個方法;與之類似的是,在之前的示例中,sayName()也是personType.prototype上的一個方法。通過語法糖包裝以后,類就可以代替自定義類型的功能,不必擔心使用的是哪種方法,只需關注如何定義正確的類

    [注意]與函數不同的是,類屬性不可被賦予新值,在之前的示例中,PersonClass.prototype就是這樣一個只可讀的類屬性

    【為何使用類語法】

    盡管類與自定義類型之間有諸多相似之處,但是它們之間仍然有一些差異

    1、函數聲明可以被提升,而類聲明與let聲明類似,不能被提升真正執行聲明語句之前,它們會一直存在于臨時死區中

    2、類聲明中的所有代碼將自動運行在嚴格模式下,而且無法強行讓代碼脫離嚴格模式執行

    3、在自定義類型中,需要通過Object.defineProperty()方法手工指定某個方法為不可枚舉;而在類中,所有方法都是不可枚舉的

    4、每個類都有一個名為[[Construct]]的內部方法,通過關鍵字new調用那些不含[[Construct]]的方法會導致程序拋出錯誤

    5、使用除關鍵字new以外的方式調用類的構造函數會導致程序拋出錯誤

    6、在類中修改類名會導致程序報錯

    了解了這些差異之后,可以用除了類之外的語法為之前示例中的PersonClass聲明編寫等價代碼

    // 直接等價于 PersonClass
    let PersonType2 = (function() {
     "use strict";
     const PersonType2 = function(name) {
     // 確認函數被調用時使用了 new
     if (typeof new.target === "undefined") {
     throw new Error("Constructor must be called with new.");
     }
     this.name = name;
     }
     Object.defineProperty(PersonType2.prototype, "sayName", {
     value: function() {
     // 確認函數被調用時沒有使用 new
     if (typeof new.target !== "undefined") {
     throw new Error("Method cannot be called with new.");
     }
     console.log(this.name);
     },
     enumerable: false,
     writable: true,
     configurable: true
     });
     return PersonType2;
    }());
    

    這段代碼中有兩處personType2聲明:一處是外部作用域中的let聲明,一處是立即執行函數表達式(IIFE)中的const聲明,這也從側面說明了為什么可以在外部修改類名而內部卻不可修改。在構造函數中,先檢查new.target是否通過new調用,如果不是則拋出錯誤;緊接著,將sayName()方法定義為不可枚舉,并再次檢查new.target是否通過new調用,如果是則拋出錯誤;最后,返回這個構造函數

    盡管可以在不使用new語法的前提下實現類的所有功能,但如此一來,代碼變得極為復雜

    【常量類名】

    類的名稱只在類中為常童,所以盡管不能在類的方法中修改類名,但可以在外部修改

    class Foo {
     constructor() {
     Foo = "bar"; // 執行時拋出錯誤
     }
    }
    // 但在類聲明之后沒問題
    Foo = "baz";

    以上代碼中,類的外部有一個Foo聲明,而類構造函數里的Foo則是一個獨立存在的綁定。內部的Foo就像是通過const聲明的,修改它的值會導致程序拋出錯誤;而外部的Foo就像是通過let聲明的,可以隨時修改這個綁定值

    類表達式

    類和函數都有兩種存在形式:聲明形式和表達式形式。聲明形式的函數和類都由相應的關鍵字(分別為function和class)進行定義,隨后緊跟一個標識符;表達式形式的函數和類與之類似,只是不需要在關鍵字后添加標識符

    類表達式的設計初衷是為了聲明相應變量或傳入函數作為參數

    【基本的類表達式語法】

    下面這段代碼等價于之前PersonClass示例的類表達式

    let PersonClass = class {
     // 等價于 PersonType 構造器
     constructor(name) {
     this.name = name;
     }
     // 等價于 PersonType.prototype.sayName
     sayName() {
     console.log(this.name);
     }
    };
    let person = new PersonClass("huochai");
    person.sayName(); // 
    輸出 "huochai" console.log(person instanceof PersonClass); // true console.log(person instanceof Object); // true console.log(typeof PersonClass); // "function" console.log(typeof PersonClass.prototype.sayName); // "function"

    類聲明和類表達式僅在代碼編寫方式略有差異,二者均不會像函數聲明和函數表達式一樣被提升,所以在運行時狀態下無論選擇哪一種方式,代碼最終的執行結果都沒有太大差別

    二者最重要的區別是name屬性不同,匿名類表達式的name屬性值是一個空字符串,而類聲明的name屬性值為類名,例如,通過聲明方式定義一個類PersonClass,則PersonClass.name的值為"PersonClass"

    【命名類表達式】

    類與函數一樣,都可以定義為命名表達式。聲明時,在關鍵字class后添加一個標識符即可

    let PersonClass = class PersonClass2 {
     // 等價于 PersonType 構造器
     constructor(name) {
     this.name = name;
     }
     // 等價于 PersonType.prototype.sayName
     sayName() {
     console.log(this.name);
     }
    };
    console.log(typeof PersonClass); // "function"
    console.log(typeof PersonClass2); // "undefined"
    
    

    上面的示例中,類表達式被命名為PersonClass2,由于標識符PersonClass2只存在于類定義中,因此它可被用在像sayName()這樣的方法中。而在類的外部,由于不存在一個名為PersonClass2的綁定,因而typeof PersonClass2的值為"undefined"

    // 直接等價于 PersonClass 具名的類表達式
    let PersonClass = (function() {
     "use strict";
     const PersonClass2 = function(name) {
     // 確認函數被調用時使用了 new
     if (typeof new.target === "undefined") {
     throw new Error("Constructor must be called with new.");
     }
     this.name = name;
     }
     Object.defineProperty(PersonClass2.prototype, "sayName", {
     value: function() {
     // 確認函數被調用時沒有使用 new
     if (typeof new.target !== "undefined") {
     throw new Error("Method cannot be called with new.");
     }
     console.log(this.name);
     },
     enumerable: false,
     writable: true,
     configurable: true
     });
     return PersonClass2;
    }());
    

    在JS引擎中,類表達式的實現與類聲明稍有不同。對于類聲明來說,通過let定義的外部綁定與通過const定義的內部綁定具有相同名稱;而命名類表達式通過const定義名稱,從而PersonClass2只能在類的內部使用

    盡管命名類表達式與命名函數表達式有不同的表現,但二者間仍有許多相似之處,都可以在多個場景中作為值使用

    一等公民

    在程序中,一等公民是指一個可以傳入函數,可以從函數返回,并且可以賦值給變量的值。JS函數是一等公民(也被稱作頭等函數),這也正是JS中的一個獨特之處

    ES6延續了這個傳統,將類也設計為一等公民,允許通過多種方式使用類的特性。例如,可以將類作為參數傳入函數中

    function createObject(classDef) {
     return new classDef();
    }
    let obj = createObject(class {
     sayHi() {
     console.log("Hi!");
     }
    });
    obj.sayHi(); // "Hi!"
    
    

    在這個示例中,調用createObject()函數時傳入一個匿名類表達式作為參數,然后通過關鍵字new實例化這個類并返回實例,將其儲存在變量obj中

    類表達式還有另一種使用方式,通過立即調用類構造函數可以創建單例。用new調用類表達式,緊接著通過一對小括號調用這個表達式

    let person = new class {
     constructor(name) {
     this.name = name;
     }
     sayName() {
     console.log(this.name);
     }
    }("huochai");
    person.sayName(); // "huochai"
    
    

    這里先創建一個匿名類表達式,然后立即執行。依照這種模式可以使用類語法創建單例,并且不會在作用域中暴露類的引用,其后的小括號表明正在調用一個函數,而且可以傳參數給這個函數


    我們可以通過類似對象字面量的語法在類中創建訪問器屬性

    訪問器屬性

    盡管應該在類構造函數中創建自己的屬性,但是類也支持訪問器屬性。創建getter時,需要在關鍵字get后緊跟一個空格和相應的標識符;創建setter時,只需把關鍵字get替換為set即可

    class CustomHTMLElement {
     constructor(element) {
     this.element = element;
     }
     get html() {
     return this.element.innerHTML;
     }
     set html(value) {
     this.element.innerHTML = value;
     }
    }
    var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, "html");
    console.log("get" in descriptor); // true
    console.log("set" in descriptor); // true
    console.log(descriptor.enumerable); // false
    
    

    這段代碼中的CustomHTMLElement類是一個針對現有DOM元素的包裝器,并通過getter和setter方法將這個元素的innerHTML方法委托給html屬性,這個訪問器屬性是在CustomHTMLElement.prototype上創建的。與其他方法一樣,創建時聲明該屬性不可枚舉。下面這段代碼是非類形式的等價實現

    // 直接等價于上個范例
    let CustomHTMLElement = (function() {
     "use strict";
     const CustomHTMLElement = function(element) {
     // 確認函數被調用時使用了 new
     if (typeof new.target === "undefined") {
     throw new Error("Constructor must be called with new.");
     }
     this.element = element;
     }
     Object.defineProperty(CustomHTMLElement.prototype, "html", {
     enumerable: false,
     configurable: true,
     get: function() {
     return this.element.innerHTML;
     },
     set: function(value) {
     this.element.innerHTML = value;
     }
     });
     return CustomHTMLElement;
    }());
    
    

    由上可見,比起非類等效實現,類語法可以節省很多代碼。在非類等效實現中,僅html訪問器屬性定義的代碼量就與類聲明一樣多

    可計算成員名稱

    類和對象字面量還有更多相似之處,類方法和訪問器屬性也支持使用可計算名稱。就像在對象字面量中一樣,用方括號包裹一個表達式即可使用可計算名稱

    let methodName = "sayName";
     class PersonClass {
     constructor(name) {
     this.name = name;
     }
     [methodName]() {
     console.log(this.name);
     }
    }
    let me = new PersonClass("huochai");
    me.sayName(); // "huochai"
    
    

    這個版本的PersonClass通過變量來給類定義中的方法命名,字符串"sayName"被賦值給methodName變量,然后methodName又被用于聲明隨后可直接訪問的sayName()方法

    通過相同的方式可以在訪問器屬性中應用可計算名稱

    let propertyName = "html";
     class CustomHTMLElement {
     constructor(element) {
     this.element = element;
     }
     get [propertyName]() {
     return this.element.innerHTML;
     }
     set [propertyName](value) {
     this.element.innerHTML = value;
     }
    }
    
    

    在這里通過propertyName變量并使用getter和setter方法為類添加html屬性,并且可以像往常一樣通過.html訪問該屬性

    在類和對象字面量諸多的共同點中,除了方法、訪問器屬性及可計算名稱上的共同點外,還需要了解另一個相似之處,也就是生成器方法

    生成器方法

    在對象字面量中,可以通過在方法名前附加一個星號(*)的方式來定義生成器,在類中亦是如此,可以將任何方法定義成生成器

    class MyClass {
     *createIterator() {
     yield 1;
     yield 2;
     yield 3;
     }
    }
    let instance = new MyClass();
    let iterator = instance.createIterator();
    
    


    這段代碼創建了一個名為MyClass的類,它有一個生成器方法createIterator(),其返回值為一個硬編碼在生成器中的迭代器。如果用對象來表示集合,又希望通過簡單的方法迭代集合中的值,那么生成器方法就派上用場了。數組、Set集合及Map集合為開發者們提供了多個生成器方法來與集合中的元素交互


    盡管生成器方法很實用,但如果類是用來表示值的集合的,那么為它定義一個默認迭代器會更有用。通過Symbol.iterator定義生成器方法即可為類定義默認迭代器

    class Collection {
     constructor() {
     this.items = [];
     }
     *[Symbol.iterator]() {
     yield *this.items.values();
     }
    }
    var collection = new Collection();
    collection.items.push(1);
    collection.items.push(2);
    collection.items.push(3);
    for (let x of collection) {
     // 1
     // 2
     // 3
     console.log(x);
    }
    
    

    這個示例用可計算名稱創建了一個代理this.items數組values()迭代器的生成器方法。任何管理一系列值的類都應該引入默認迭代器,因為一些與特定集合有關的操作需要所操作的集合含有一個迭代器。現在可以將collection的實例直接用于for-of循環中或用展開運算符操作它

    如果不介意在對象的實例中出現添加的方法和訪問器屬性,則可以將它們添加到類的原型中;如果希望它們只出現在類中,那么需要使用靜態成員

    靜態成員

    在ES5中,直接將方法添加到構造函數中來模擬靜態成員是一種常見的模式

    function PersonType(name) {
     this.name = name;
    }
    // 靜態方法
    PersonType.create = function(name) {
     return new PersonType(name);
    };
    // 實例方法
    PersonType.prototype.sayName = function() {
     console.log(this.name);
    };
    var person = PersonType.create("huochai");
    
    

    在其他編程語言中,由于工廠方法PersonType.create()使用的數據不依賴personType的實例,因而其會被認為是一個靜態方法。ES6的類語法簡化了創建靜態成員的過程,在方法或訪問器屬性名前使用正式的靜態注釋即可

    class PersonClass {
     // 等價于 PersonType 構造器
     constructor(name) {
     this.name = name;
     }
     // 等價于 PersonType.prototype.sayName
     sayName() {
     console.log(this.name);
     }
     // 等價于 PersonType.create
     static create(name) {
     return new PersonClass(name);
     }
    }
    let person = PersonClass.create("huochai");
    
    

    PersonClass定義只有一個靜態方法create(),它的語法與sayName()的區別只在于是否使用static關鍵字。類中的所有方法和訪問器屬性都可以用static關鍵字來定義,唯一的限制是不能將static用于定義構造函數方法

    [注意]不可在實例中訪問靜態成員,必須要直接在類中訪問靜態成員

    繼承與派生類

    在ES6之前,實現繼承與自定義類型是一個不小的工作。嚴格意義上的繼承需要多個步驟實現

    function Rectangle(length, width) {
     this.length = length;
     this.width = width;
    }
    Rectangle.prototype.getArea = function() {
     return this.length * this.width;
    };
    function Square(length) {
     Rectangle.call(this, length, length);
    }
    Square.prototype = Object.create(Rectangle.prototype, {
     constructor: {
     value:Square,
     enumerable: true,
     writable: true,
     configurable: true
     }
    });
    var square = new Square(3);
    console.log(square.getArea()); // 9
    console.log(square instanceof Square); // true
    console.log(square instanceof Rectangle); // true
    

    Square繼承自Rectangle,為了這樣做,必須用一個創建自Rectangle.prototype的新對象重寫Square.prototype并調用Rectangle.call()方法。JS新手經常對這些步驟感到困惑,即使是經驗豐富的開發者也常在這里出錯

    類的出現讓我們可以更輕松地實現繼承功能,使用熟悉的extends關鍵字可以指定類繼承的函數。原型會自動調整,通過調用super()方法即可訪問基類的構造函數

    class Rectangle {
     constructor(length, width) {
     this.length = length;
     this.width = width;
     }
     getArea() {
     return this.length * this.width;
     }
    }
    class Square extends Rectangle {
     constructor(length) {
     // 與 Rectangle.call(this, length, length) 相同
     super(length, length);
     }
    }
    var square = new Square(3);
    console.log(square.getArea()); // 9
    console.log(square instanceof Square); // true
    console.log(square instanceof Rectangle); // true
    
    

    這一次,square類通過extends關鍵字繼承Rectangle類,在square構造函數中通過super()調用Rectangle構造函數并傳入相應參數。請注意,與ES5版本代碼不同的是,標識符Rectangle只用于類聲明(extends之后)

    繼承自其他類的類被稱作派生類,如果在派生類中指定了構造函數則必須要調用super(),如果不這樣做程序就會報錯。如果選擇不使用構造函數,則當創建新的類實例時會自動調用super()并傳入所有參數

    class Square extends Rectangle {
     // 沒有構造器
    }
    // 等價于:
    class Square extends Rectangle {
     constructor(...args) {
     super(...args);
     }
    }
    
    

    示例中的第二個類是所有派生類的等效默認構造函數,所有參數按順序被傳遞給基類的構造函數。這里展示的功能不太正確,因為square的構造函數只需要一個參數,所以最好手動定義構造函數

    注意事項

    使用super()時有以下幾個關鍵點

    1、只可在派生類的構造函數中使用super(),如果嘗試在非派生類(不是用extends聲明的類)或函數中使用則會導致程序拋出錯誤

    2、在構造函數中訪問this之前一定要調用super(),它負責初始化this,如果在調用super()之前嘗試訪問this會導致程序出錯

    3、如果不想調用super(),則唯一的方法是讓類的構造函數返回一個對象

    【類方法遮蔽】

    派生類中的方法總會覆蓋基類中的同名方法。比如給square添加getArea()方法來重新定義這個方法的功能

    class Square extends Rectangle {
     constructor(length) {
     super(length, length);
     }
     // 重寫并屏蔽 Rectangle.prototype.getArea()
     getArea() {
     return this.length * this.length;
     }
    }

    由于為square定義了getArea()方法,便不能在square的實例中調用Rectangle.prototype.getArea()方法。當然,如果想調用基類中的該方法,則可以調用super.getArea()方法

    class Square extends Rectangle {
     constructor(length) {
     super(length, length);
     }
     // 重寫、屏蔽并調用了 Rectangle.prototype.getArea()
     getArea() {
     return super.getArea();
     }
    }
    
    

    以這種方法使用Super,this值會被自動正確設置,然后就可以進行簡單的方法調用了

    【靜態成員繼承】

    如果基類有靜態成員,那么這些靜態成員在派生類中也可用。JS中的繼承與其他語言中的繼承一樣,只是在這里繼承還是一個新概念

    class Rectangle {
     constructor(length, width) {
     this.length = length;
     this.width = width;
     }
     getArea() {
     return this.length * this.width;
     }
     static create(length, width) {
     return new Rectangle(length, width);
     }
    }
    class Square extends Rectangle {
     constructor(length) {
     // 與 Rectangle.call(this, length, length) 相同
     super(length, length);
     }
    }
    var rect = Square.create(3, 4);
    console.log(rect instanceof Rectangle); // true
    console.log(rect.getArea()); // 12
    console.log(rect instanceof Square); // false
    
    

    在這段代碼中,新的靜態方法create()被添加到Rectangle類中,繼承后的Square.create()與Rectangle.create()的行為很像

    【派生自表達式的類】

    ES6最強大的一面或許是從表達式導出類的功能了。只要表達式可以被解析為一個函數并且具有[[Construct]屬性和原型,那么就可以用extends進行派生

    function Rectangle(length, width) {
     this.length = length;
     this.width = width;
    }
    Rectangle.prototype.getArea = function() {
     return this.length * this.width;
    };
    class Square extends Rectangle {
     constructor(length) {
     super(length, length);
     }
    }
    var x = new Square(3);
    console.log(x.getArea()); // 9
    console.log(x instanceof Rectangle); // true
    
    

    Rectangle是一個ES5風格的構造函數,Square是一個類,由于Rectangle具有[[Construct]]屬性和原型,因此Square類可以直接繼承它

    extends強大的功能使類可以繼承自任意類型的表達式,從而創造更多可能性,例如動態地確定類的繼承目標

    function Rectangle(length, width) {
     this.length = length;
     this.width = width;
    }
    Rectangle.prototype.getArea = function() {
     return this.length * this.width;
    };
    function getBase() {
     return Rectangle;
    }
    class Square extends getBase() {
     constructor(length) {
     super(length, length);
     }
    }
    var x = new Square(3);
    console.log(x.getArea()); // 9
    console.log(x instanceof Rectangle); // true
    
    

    getBase()函數是類聲明的一部分,直接調用后返回Rectangıe,此示例實現的功能與之前的示例等價。由于可以動態確定使用哪個基類,因而可以創建不同的繼承方法

    let SerializableMixin = {
     serialize() {
     return JSON.stringify(this);
     }
    };
    let AreaMixin = {
     getArea() {
     return this.length * this.width;
     }
    };
    function mixin(...mixins) {
     var base = function() {};
     Object.assign(base.prototype, ...mixins);
     return base;
    }
    class Square extends mixin(AreaMixin, SerializableMixin) {
     constructor(length) {
     super();
     this.length = length;
     this.width = length;
     }
    }
    var x = new Square(3);
    console.log(x.getArea()); // 9
    console.log(x.serialize()); // "{"length":3,"width":3}"
    

    這個示例使用了mixin函數代替傳統的繼承方法,它可以接受任意數量的mixin對象作為參數。首先創建一個函數base,再將每一個mixin對象的屬性值賦值給base的原型,最后minxin函數返回這個base函數,所以Square類就可以基于這個返回的函數用extends進行擴展。由于使用了extends,因此在構造函數中需要調用super()

    Square的實例擁有來自AreaMixin對象的getArea()方法和來自SerializableMixin對象的serialize方法,這都是通過原型繼承實現的,mixin()函數會用所有mixin對象的自有屬性動態填充新函數的原型。如果多個mixin對象具有相同屬性,那么只有最后一個被添加的屬性被保留

    [注意]在extends后可以使用任意表達式,但不是所有表達式最終都能生成合法的類。如果使用null或生成器函數會導致錯誤發生,類在這些情況下沒有[[Consturct]]屬性,嘗試為其創建新的實例會導致程序無法調用[[Construct]]而報錯

    【內建對象的繼承】

    自JS數組誕生以來,一直都希望通過繼承的方式創建屬于自己的特殊數組。在ES5中這幾乎是不可能的,用傳統的繼承方式無法實現這樣的功能

    // 內置數組的行為
    var colors = [];
    colors[0] = "red";
    console.log(colors.length); // 1
    colors.length = 0;
    console.log(colors[0]); // undefined
    // 在 ES5 中嘗試繼承數組
    function MyArray() {
     Array.apply(this, arguments);
    }
    MyArray.prototype = Object.create(Array.prototype, {
     constructor: {
     value: MyArray,
     writable: true,
     configurable: true,
     enumerable: true
     }
    });
    var colors = new MyArray();
    colors[0] = "red";
    console.log(colors.length); // 0
    colors.length = 0;
    console.log(colors[0]); // "red"

    這段代碼最后console.log()的輸出結果與預期不符,MyArray實例的length和數值型屬性的行為與內建數組中的不一致,這是因為通過傳統JS繼承形式實現的數組繼承沒有從Array.apply()或原型賦值中繼承相關功能

    ES6類語法的一個目標是支持內建對象繼承,因而ES6中的類繼承模型與ES5稍有不同,主要體現在兩個方面

    在ES5的傳統繼承方式中,先由派生類型(如MyArray)創建this的值,然后調用基類型的構造函數(如Array.apply()方法)。這也意味著,this的值開始指向MyArray的實例,但是隨后會被來自Array的其他屬性修飾

    ES6中的類繼承則與之相反,先由基類(Array)創建this的值,然后派生類的構造函數(MyArray)再修改這個值。所以一開始可以通過this訪問基類的所有內建功能,然后再正確地接收所有與之相關的功能

    class MyArray extends Array {
     // 空代碼塊
    }
    var colors = new MyArray();
    colors[0] = "red";
    console.log(colors.length); // 1
    colors.length = 0;
    console.log(colors[0]); // undefined

    MyArray直接繼承自Array,其行為與Array也很相似,操作數值型屬性會更新length屬性,操作length屬性也會更新數值型屬性。于是,可以正確地繼承Array對象來創建自己的派生數組類型,當然也可以繼承其他的內建對象

    【Symbol.species屬性】

    內建對象繼承的一個實用之處是,原本在內建對象中返回實例自身的方法將自動返回派生類的實例。所以,如果有一個繼承自Array的派生類MyArray,那么像slice()這樣的方法也會返回一個MyArray的實例

    class MyArray extends Array {
     // 空代碼塊
    }
    let items = new MyArray(1, 2, 3, 4),
     subitems = items.slice(1, 3);
    console.log(items instanceof MyArray); // true
    console.log(subitems instanceof MyArray); // true

    正常情況下,繼承自Array的slice()方法應該返回Array的實例,但是在這段代碼中,slice()方法返回的是MyArray的實例。在瀏覽器引擎背后是通過Symbol.species屬性實現這一行為

    Symbol.species是諸多內部Symbol中的一個,它被用于定義返回函數的靜態訪問器屬性。被返回的函數是一個構造函數,每當要在實例的方法中(不是在構造函數中)創建類的實例時必須使用這個構造函數。以下這些內建類型均己定義Symbol.species屬性

    Array
    ArrayBuffer
    Map
    Promise
    RegExp
    Set
    Typed arrays

    列表中的每個類型都有一個默認的symbol.species屬性,該屬性的返回值為this,這也意味著該屬性總會返回構造函數

    // 幾個內置類型使用 species 的方式類似于此
    class MyClass {
     static get [Symbol.species]() {
     return this;
     }
     constructor(value) {
     this.value = value;
     }
     clone() {
     return new this.constructor[Symbol.species](this.value);
     }
    }

    在這個示例中,Symbol.species被用來給MyClass賦值靜態訪問器屬性。這里只有一個getter方法卻沒有setter方法,這是因為在這里不可以改變類的種類。調用this.constructor[Symbol.species]會返回MyClass,clone()方法通過這個定義可以返回新的實例,從而允許派生類覆蓋這個值

    class MyClass {
     static get [Symbol.species]() {
     return this;
     }
     constructor(value) {
     this.value = value;
     }
     clone() {
     return new this.constructor[Symbol.species](this.value);
     }
    }
    class MyDerivedClass1 extends MyClass {
     // 空代碼塊
    }
    class MyDerivedClass2 extends MyClass {
     static get [Symbol.species]() {
     return MyClass;
     }
    }
    let instance1 = new MyDerivedClass1("foo"),
    clone1 = instance1.clone(),
    instance2 = new MyDerivedClass2("bar"),
    clone2 = instance2.clone();
    console.log(clone1 instanceof MyClass); // true
    console.log(clone1 instanceof MyDerivedClass1); // true
    console.log(clone2 instanceof MyClass); // true
    console.log(clone2 instanceof MyDerivedClass2); // false

    在這里,MyDerivedClass1繼承MyClass時未改變Symbol.species屬性,由于this.constructor[Symbol.species]的返回值是MyDerivedClass1,因此調用clone()返回的是MyDerivedClass1的實例;MyDerivedClass2繼承MyClass時重寫了Symbol.species讓其返回MyClass,調用MyDerivedClass2實例的clone()方法時,返回值是一個MyClass的實例。通過Symbol.species可以定義當派生類的方法返回實例時,應該返回的值的類型

    數組通過Symbol.species來指定那些返回數組的方法應當從哪個類中獲取。在一個派生自數組的類中,可以決定繼承的方法返回何種類型的對象

    class MyArray extends Array {
     static get [Symbol.species]() {
     return Array;
     }
    }
    let items = new MyArray(1, 2, 3, 4),
    subitems = items.slice(1, 3);
    console.log(items instanceof MyArray); // true
    console.log(subitems instanceof Array); // true
    console.log(subitems instanceof MyArray); // false

    這段代碼重寫了MyArray繼承自Array的Symbol.species屬性,所有返回數組的繼承方法現在將使用Array的實例,而不使用MyArray的實例

    一般來說,只要想在類方法中調用this.constructor,就應該使用Symbol.species屬性,從而讓派生類重寫返回類型。而且如果正從一個已定義Symbol.species屬性的類創建派生類,那么要確保使用那個值而不是使用構造函數

    【在類的構造函數中使用new.target】

    new.target及它的值根據函數被調用的方式而改變。在類的構造函數中也可以通過new.target來確定類是如何被調用的。簡單情況下,new.target等于類的構造函數

    class Rectangle {
     constructor(length, width) {
     console.log(new.target === Rectangle);
     this.length = length;
     this.width = width;
     }
    }
    // new.target 就是 Rectangle
    var obj = new Rectangle(3, 4); // 
    輸出 true

    這段代碼展示了當調用new Rectangle(3.4)時等價于Rectangle的new.target。類構造函數必須通過new關鍵字調用,所以總是在類的構造函數中定義new.target屬性,但是其值有時會不同

    class Rectangle {
     constructor(length, width) {
     console.log(new.target === Rectangle);
     this.length = length;
     this.width = width;
     }
    }
    class Square extends Rectangle {
     constructor(length) {
     super(length, length)
     }
    }
    // new.target 就是 Square
    var obj = new Square(3); // 
    輸出 false

    Square調用Rectangle的構造函數,所以當調用發生時new.target等于Square。這一點非常重要,因為每個構造函數都可以根據自身被調用的方式改變自己的行為

    // 靜態的基類
    class Shape {
     constructor() {
     if (new.target === Shape) {
     throw new Error("This class cannot be instantiated directly.")
     }
     }
    }
    class Rectangle extends Shape {
     constructor(length, width) {
     super();
     this.length = length;
     this.width = width;
     }
    }
    var x = new Shape(); // 拋出錯誤
    var y = new Rectangle(3, 4); // 沒有錯誤
    console.log(y instanceof Shape); // true

    在這個示例中,每當new.target是Shape時構造函數總會拋出錯誤,這相當于調用new Shape()時總會出錯。但是,仍可用Shape作為基類派生其他類,示例中的Rectangle便是這樣。super()調用執行了Shape的構造函數,new.target與Rectangle等價,所以構造函數繼續執行不會拋出錯誤

    [注意]因為類必須通過new關鍵字才能調用,所以在類的構造函數中,new.target屬性永遠不會是undefined

    以上這篇老生常談ES6中的類就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支持腳本之家。

    聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com

    文檔

    老生常談ES6中的類

    老生常談ES6中的類:前面的話 大多數面向對象的編程語言都支持類和類繼承的特性,而JS卻不支持這些特性,只能通過其他方法定義并關聯多個相似的對象,這種狀態一直延續到了ES5。由于類似的庫層出不窮,最終還是在ECMAScript 6中引入了類的特性。本文將詳細介紹ES6中的類 ES
    推薦度:
    標簽: ES6 es6的 es6中
    • 熱門焦點

    最新推薦

    猜你喜歡

    熱門推薦

    專題
    Top
    主站蜘蛛池模板: 国产人成精品午夜在线观看| 青青青国产精品国产精品久久久久| 精品午夜福利在线观看| 99久久精品免费看国产免费| 国产精品丝袜黑色高跟鞋| 午夜精品一区二区三区在线视 | 久久精品国产一区二区三区| 日本精品久久久中文字幕| 精品乱码久久久久久久| 一本一本久久a久久精品综合麻豆 一本色道久久88综合日韩精品 | 国产成人综合精品一区| 高清在线亚洲精品国产二区| 日本aⅴ精品中文字幕| 欧美亚洲成人精品| 国产精品无码不卡一区二区三区| 国产伦精品一区二区三区女 | 国产精品一区在线观看你懂的| 久久国产精品久久久| 国产成人A人亚洲精品无码| 久久99热只有频精品8| 老汉精品免费AV在线播放| 亚洲国产精品无码久久久蜜芽| 日韩精品免费一线在线观看| 欧美午夜精品一区二区三区91| 欧美ppypp精品一区二区| 精品人妻V?出轨中文字幕| 国产精品一区二区三区99| 国产午夜亚洲精品理论片不卡| 国产精品乱伦| 国产色精品vr一区区三区| 国产手机在线精品| 国内精品久久久久久久亚洲| 精品第一国产综合精品蜜芽| 精品成人一区二区三区四区| 精品熟女少妇aⅴ免费久久| 国产亚洲精品成人a v小说| 久久91精品综合国产首页| 久久99精品国产99久久6| 免费视频精品一区二区| 亚洲人午夜射精精品日韩| 色一乱一伦一图一区二区精品|