ES6語言特性的總結(3)

scq000發表於2019-02-08

在ES5中,由於沒有類的概念,所以如果要使用物件導向程式設計,就需要利用原型繼承的方式。通常是建立一個構造器,然後將方法指派到該構造器的原型上。
就像這樣:

function Cat(name) {
  this.name = name;
}

Cat.prototype.speak = function() {
  console.log(`Mew!`);
}複製程式碼

ES6引入了class關鍵字後就不再需要這樣做了。不過需要明白的是ES6中的類僅僅是以上面這種方式作為基礎的一個語法糖而已。
ES6中類宣告已class關鍵字開始,其後是類的名稱;剩餘部分的語法部分看起來就像物件字面量中的方法簡寫,並且在方法之間不需要使用逗號。同時允許你在其中使用特殊的 constructor 方法名稱直接定義一個構造器,而不需要先定義一個函式再把它當作構造器使用。

class Cat {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`Mew!`);
  }
}複製程式碼

類宣告與ES5仿類的區別

雖然ES6的類宣告是ES5方式的一個語法糖,但是與之相比,還是存在一些區別的。

  1. 類宣告不會被提升,這與函式定義不同。類宣告的行為與 let 相似,因此在程式的執行到達宣告處之前,類會存在於暫時性死區內。
  2. 類宣告中的所有程式碼會自動執行在嚴格模式下,並且也無法退出嚴格模式。
  3. 類的所有方法都是不可列舉的,這是對於自定義型別的顯著變化,後者必須用 Object.defineProperty() 才能將方法改變為不可列舉。
  4. 類的所有方法內部都沒有 [[Construct]] ,因此使用 new 來呼叫它們會丟擲錯誤。
  5. 呼叫類構造器時不使用 new ,會丟擲錯誤。
  6. 試圖在類的方法內部重寫類名,會丟擲錯誤。

訪問器屬性

自有屬性需要在類構造器中建立,而類還允許你在原型上定義訪問器屬性。

class Person {
  constructor(name, age) {
    this.age = age;
    this.name = name;
  }

  get firstName() {
    return this.name.split(` `)[0];
  }

  set firstName(value) {
    let lastName = this.name.split(` `)[1];
    this.name = value + ` ` + lastName;
  }
}

let person = new Person(`Michael Jackson`, 35);
console.log(person.firstName); //`Michael`
person.firstName = `Marry`;
console.log(person.name); // `Marry Jackson`複製程式碼

在讀取訪問器屬性的時候,會呼叫getter方法,而寫入值的時候,會呼叫setter方法。這類似於ES5中使用Object.definePropery的方法。

靜態成員

靜態成員在ES5中一般是直接定義在構造器上的,如:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.createAdult = function(name) {
  return new Person(name, 18);
};複製程式碼

而在ES6中提供了static關鍵字簡化了宣告靜態成員的方式:

class Person {
  constructor(name, age) {
    this.age = age;
    this.name = name;
  }

  static createAdult(name) {
    return new Person(name, 18);
  }

}複製程式碼

繼承

ES5中實現繼承的方式有很多種,但是如果要實現嚴格的繼承,步驟較為繁瑣。為了簡化繼承的關係,ES6中使用類讓這項工作變得更簡單。如果你熟悉面嚮物件語言,如java等,那麼extends這個關鍵你一定不會陌生。同樣的,在ES6中使用extends 關鍵字來指定當前類所需要繼承的函式即可。生成的類的原型會被自動調整,而你還能呼叫 super() 方法來訪問基類的構造器。

class Person {
    constructor(country) {
      this.country = country;
    }
}

class Chinese extends Person{
    constructor() {
      super(`China`);
    }

    speak() {
      console.log(`I come from ` + this.country);
    }
}複製程式碼

派生類中的方法總是會遮蔽基類中的同名方法,因此,如果你需要使用父類中定義的方法的話,可以使用super關鍵字來進行訪問。如:

class Person {
    constructor(country) {
      this.country = country;
    }

    speak() {
      console.log(`I come from ` + this.country);
    }
}

class Chinese extends Person{
    constructor() {
      super(`China`);
    }

    speak() {
        super.speak();
        console.log(`I am a Chinese`);
    }
}

const chinese = new Chinese();
chinese.speak();
//I come from China.
//I am a Chinese.複製程式碼

從表示式中派生類

另一個在ES6中比較高階的地方是,可以從表示式中派生出類來:

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}"複製程式碼

繼承內建物件

利用extends繼承內建物件的時候,容易出現的一個問題是會返回內建物件例項的方式,在繼承後會返回子類的例項。如:

class SubArray extends Array {

}

const subArr = new SubArray(1,2,3);
const filteredArr = subArr.filter(value => value > 1); 
console.assert(filteredArr instanceof SubArray);  //true複製程式碼

如果需要想讓其返回例項型別是Array可以利用Symbol.species這個符號來處理:

class SubArray extends Array {
//這裡使用static,表明是靜態訪問器屬性
  static get [Symbol.species]() {
    return Array;
  }
}複製程式碼

定義抽象類

利用之前介紹的new.target可以實現一個抽象類,原理就是當使用者呼叫new直接建立例項的時候,丟擲錯誤。:

class BaseClass {
  constructor() {
    if(new.target === BaseClass) {
      throw new Error(`該類不能直接例項化`)
    }
  }
}複製程式碼

模組

隨著專案的規模越來越大,現在模組化已經成為開發過程中必備的流程。之前,我們可能借助RequireJS等工具進行模組化管理,而現在ES6已經提供了模組系統。

先來了解一下基本語法:

基本的匯出匯入

模組( Modules )本質上就是 包含JS 程式碼的檔案。在一個js檔案中,你可以使用export關鍵字,將程式碼公開給其他模組。

// sayHello.js
export function sayHello() {
  console.log(`hello`);
}

// funcs.js
export function fun1() { .... }
export function func2() { .... }
export const value1 = `value1`;複製程式碼

如上面的例子中所示,你可以在檔案中匯出所有的最外層函式以及varletconst宣告的變數。而這些匯出的變數或公開部分則可以被其他檔案利用import語法進行匯入後引用。

//單個匯入
import {sayHello} from `./sayHello.js`;
//多個匯入
import {func1, func2} from `./funs.js`;
sayHello(); // hello複製程式碼

為了確保瀏覽器與Node.js之間保持良好的相容性,建議使用相對路徑的寫法。

如果需要將整個模組當做單一的物件進行匯入,可以使用*萬用字元:

//使用as關鍵字為匯出物件設定別名,模組中所有匯出都將作為屬性存在
import * as funcs from `./funcs.js`;

funcs.func1();
funsc.func2();複製程式碼

重新命名匯出與匯入

如果不想用原來模組中的命名,可以通過as關鍵字來指定別名。

//as前面為模組原先的名稱,後面是別名,使用別名後sayHello為undefined
import { sayHello as say } from `./sayHello.js`;

say();複製程式碼

預設值

你可以使用export關鍵字來匯出預設模組:

// sayHello.js
export default function() {
  console.log(`hello`);
}

// main.js
import sayHello from `./sayHello.js`;
sayHello();複製程式碼

可以注意到,這裡預設匯出的時候,不需要使用花括號,而直接為其命名即可。這種寫法也較為簡潔。當一個檔案中,同時存在預設匯出模組和非預設匯出模組的時候,匯出的時候,預設匯出模組需要寫在前面,例如:

import sayHello,{ func1 } from `./sayHello.js`; //此處略去匯出過程
//或者使用如下方式
import {default as sayHello, func1} from `./sayHello.js`;複製程式碼

無繫結匯出

當一個檔案中沒有使用export語句進行匯出的時候,其實我們還是可以import進行匯入的。通常是被用於建立polyfill與shim的時候。

//sayHello.js
const name = `scq000`;
function sayHello() {
    console.log(`hello`);
}

// main.js
import `./sayHello.js`;

sayHello();
console.log(name);複製程式碼

載入模組

雖然說現在在專案中通常都使用webpack來處理模組程式碼,但也需要知道其他載入模組的方式。

你可以使用<script type="module">的方式進行模組的載入,預設瀏覽器會採用defer屬性,一旦頁面文件完全被解析後,模組就會按次序執行。如果需要非同步載入的話,可以加上async關鍵字。

另外,如果是使用Web Worker或Server Worker之類的worker的話,可以通過下面這種方式載入模組:

let worker = new Worker(`module.js`, { type: `module` });複製程式碼

迭代器與生成器

迭代器和生成器通常是一起來使用的。迭代器的目的是為了更加方便地遍歷物件,而生成器用來生成可迭代的物件。使用迭代器的過程中,你可以結合for...of語句以及...擴充套件符來遍歷物件的值。

迭代器

在ES6中,迭代器是專門用來設計迭代的物件,帶有特殊的介面。所有的迭代器都帶有next方法,用來返回一個結果。這裡我們來手工實現一個迭代器:

function createIterator() {
    var i = 0;

    return {
        next() {
            var done = false;
            var value;
            if (i < 3) {
                value = i * 2;
                i++;
            } else {
                done = true;
                value = undefined;
            }
            return { value: value, done: done }
        }
    }
}

let iterator = new createIterator();
iterator.next(); // {value: 0, done: false}
iterator.next(); // {value: 0, done: false}
iterator.next(); // {value: 4, done: false}
iterator.next(); // {value: undefined, done: true}複製程式碼

集合物件(Set、Map、Array)提供了三種內建的迭代器:entries,keys,values,這三個方法都會返回一個迭代器,用來方便地獲取鍵值對等資訊。ES6中定義了可迭代物件(iterable object),如Set、Map、Array以及字串等都可以利用for...of語法來進行遍歷操作。原理其實就是呼叫它們內建的預設迭代器。對於使用者自定義的物件,如果也要讓它們支援for...of語法,則需要去定義Symbol.iterator屬性。具體例子,可以檢視符號那一部分的內容。

生成器

生成器(generator)是能夠返回迭代器的函式。通常定義的時候,我們會利用function關鍵字之後的(*)號表示,使用yield語句輸出每一次的資料。

function *getNum() {
  yield 1;
  yield 2;
  yield 3;
}

const nums = getNum();

for(let num of nums) {
  console.log(num);
}
//1,2,3複製程式碼

Promise與非同步程式設計

這部分的內容我在前端的非同步解決方案之Promise和Await/Async中有詳細的闡述,如果感興趣的可以看一下。

代理與反射介面

為了讓開發者能夠建立內建物件,ES6通過代理proxy )的方式暴露了物件上的內部工作。使用代理能夠攔截並改變 JS 引擎的底層操作,如日誌、物件虛擬化等。而反射reflect )則是反映了對底層的預設行為操作。

接下來這個例子,將演示如何利用代理和反射的方式對物件的內建行為做修改:

//要修改的預設物件
let target = {
    name: `scq000`,
    age: 23
};

//代理物件
let proxy = new Proxy(target, {
  has(trapTarget, key) {
    if(key === `age`) {
      return false;
    }else {
    //呼叫預設的行為
      return Reflect.has(trapTarget, key);
    }
  }
});

console.log(`value` in proxy); //true
console.log(`age` in proxy); //false複製程式碼

可以看到,上面這個例子使用代理物件攔截了in操作符的預設行為並作出了修改。has這個方法稱作陷阱函式,它能夠響應對in操作的訪問操作。trapTarget則是這個函式的目標物件,has方法接受一個額外的引數key是對應著需要檢查的屬性。一旦檢查到屬性名為age,則返回false,這樣就能隱藏這個屬性。

以下是一些常用的代理陷阱以及反射所對應的預設行為:

代理陷阱 被重寫的行為 預設行為
get/set 讀取/寫入一個屬性值 Reflect.get/Reflect.set
has in運算子 Reflect.has
deleteProperty delete運算子 Reflect.deleteProperty
getPropertyOf/setPropertyOf Object.getPropertyOf/setPropertyOf Reflefct.getPropertyOf/setPropertyOf

目前,反射和代理在瀏覽器上還不支援,主要還是用在NodeJS程式設計上。這一部分的功能在實際開發中並不是特別常用,因此,這裡不做過多介紹。如果感興趣的話,可以自行查詢相關文件。

相關文章