Skip to content

ECMAScript 6 學習筆記

參考學習資源:阮一峰 ECMAScript 6 (ES6) 標準入門教程從 ES6 開始的 JavaScript 學習生活

本筆記僅記錄複習 ES6+ 時一些不太熟悉或是比較重要的內容,並非所有的新特性及內容。

Promise

Promise 是非同步程式設計的一種解決方案,比傳統的解決方案 callback 更合理和更強大。ES6 將其寫進了語言標準,統一了用法,原生提供了Promise 物件。

  • 指定 callback 的方式更靈活易懂。
  • 解決非同步 callback hell(回呼地獄) 的問題。

基本用法

Promise 建構函式接收一個函式(executor)作為參數,這個執行器函式又分別接收兩個函式作為參數 resolvereject。這兩個函式由 JavaScript 引擎提供,不需要自己去寫。

js
let pro = new Promise((resolve, reject) => {
  // 執行器函式 (executor)
  if (/* 非同步操作成功 */) {
    resolve('success');
  } else {
    reject('fail');
  }
});

// 可以用 then 方法分別指定 resolved 狀態和 rejected 狀態的 callback
pro.then(
  // 成功 callback
  () => {
    console.log('實現');
  },
  // 失敗 callback
  () => {
    console.log('拒絕');
  }
);

// 更常見的寫法是使用 .catch 去處理 rejected 狀態
pro
  .then((res) => {
    console.log('實現', res);
  })
  .catch((err) => {
    console.log('拒絕', err);
  });

Promise 物件的狀態

Promise 物件透過自身的狀態,來控制非同步操作。Promise 實體具有三種狀態:

  • 非同步操作未完成(pending)
  • 非同步操作成功(fulfilled)
  • 非同步操作失敗(rejected)

這三種狀態的變化途徑只有兩種:

  • 從"未完成"到"成功"
  • 從"未完成"到"失敗"

一旦狀態發生變化,就凝固了,不會再有新的狀態變化。這也是 Promise 這個名字的由來,它的英文意思譯作"承諾",一旦承諾生效,就不得在改變了。Promise 實體的狀態變化只可能發生一次。

因此,Promise 的最終結果只有兩種:

非同步操作成功,Promise 實體拋出一個值(value),狀態變為 fulfilled。
非同步操作失敗,Promise 實體拋出一個錯誤(error),狀態變為 rejected。

案例:假設有個業務需將第一次請求的回傳值當作參數去發第二次請求

json
// 1.json
{
  "data": 11111
}

// 2.json
{
  "data": 22222
}
js
function ajax(url) {
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    xhr.open('get', url, true);
    xhr.send();
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(JSON.parse(xhr.responseText));
        } else {
          reject(xhr.responseText);
        }
      }
    };
  });
}

ajax('1.json')
  .then((res) => {
    console.log(res);
    return ajax('2.json', res);
  })
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log(err);
  });

then 如果回傳非 Promise 的值,則為 pending-fulfilled,如果回傳 Promise,則根據這個新的 Promise 的結果決定 pending-fulfilled 或 pending-rejected,透過鏈式呼叫 .then 可以解決掉過去 callback hell 的問題。

Promise.all()

Promise.all() 方法用於將多個 Promise 實體,包裝成一個新的 Promise 實體。

js
let pro1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1000);
  }, 1000);
});
let pro2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(2000);
  }, 2000);
});
let pro3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(3000);
  }, 3000);
});

Promise.all([pro1, pro2, pro3])
  .then((res) => {
    console.log(res); // prints: [1000, 2000, 3000]
  })
  .catch((err) => console.log(err));

只有 3 個實體的狀態都變成 fulfilled 才會回傳一個由 pro1、pro2、pro3 的回傳值組成的陣列,只要其中一個被 rejected,Promise.all 的狀態就會變成 rejected,此時第一個被 reject 的實體的回傳值,會傳遞給 catch 的 callback。

Promise.race()

Promise.race() 方法同樣是將多個 Promise 實體,包裝成一個新的 Promise 實體。

js
// 接續上方例子

Promise.race([pro1, pro2, pro3])
  .then((res) => {
    console.log(res); // prints: 1000
  })
  .catch((err) => console.log(err));

只要 pro1、pro2、pro3 之中有一個實體先改變狀態,Promise.race 的狀態就會跟著改變並且最先改變的 Promise 實體回傳值就會傳給 Promise.race 的 callback。

應用場景:請求超時處理

js
let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1000);
  }, 30000);
});
let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(2000);
  }, 2000);
});

Promise.race([p1, p2])
  .then((res) => {
    console.log(res);
  })
  .catch((err) => console.log(err, '超時了'));
// prints: 2000 '超時了'

Generator

Generator 函式是 ES6 提供的一種非同步程式設計的解決方案。

Generator 函式是一個狀態機,封裝了多個內部狀態。

執行 Generator 函式會回傳一個 iterator 物件,也就是說,Generator 函式除了狀態機,還是一個 iterator 物件生成的函式。回傳的 iterator 可以依次遍歷 Generator 函式內部的每一個狀態。

基本用法

js
function* gen() {
  console.log(1);
  yield;
  console.log(2);
  yield;
  console.log(3);
}

let g = gen();

g.next();
g.next();
g.next();

/*
  prints: 1 2 3
*/

yield 表達式是暫停執行的標記,而 next 方法可以恢復執行

非同步流程

用 Generator 去改寫 Promise 筆記中發送兩次請求案例:

  • 手動驅動版本:
js
function* gen() {
  let res = yield ajax('1.json');
  console.log('第一次請求的結果', res);
  let res2 = yield ajax('2.json', res);
  console.log('第二次請求的結果', res2);
}

let g = gen();

g.next().value.then((data) => {
  g.next(data).value.then((res) => {
    g.next(res);
  });
});

/*
  第一次請求的結果 {data: 11111}
  第二次請求的結果 {data: 22222}
*/
  • 自動版本:
js
function* gen() {
  let res = yield ajax('1.json');
  console.log('第一次請求的結果', res);
  let res2 = yield ajax('2.json', res);
  console.log('第二次請求的結果', res2);
}

function AutoRun(gen) {
  let g = gen();

  function next(data) {
    let res = g.next(data);
    if (res.done) return;
    res.value.then((data) => {
      next(data);
    });
  }

  next();
}

AutoRun(gen);
/*
  第一次請求的結果 {data: 11111}
  第二次請求的結果 {data: 22222}
*/

Class 語法

基本上,ES6 的 class 可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的 class 寫法只是讓物件原型的寫法更加清晰、更像物件導向程式設計的語法而已。

類別的寫法

過去 ES5 的原型鏈寫法:

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

Person.prototype.say = function () {
  console.log(this.name, this.age);
};
let obj = new Person('sheep', 25);

console.log(obj);

ES6 Class 寫法:

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

  say() {
    console.log(this.name, this.age);
  }
}

let obj = new Person('sheep', 25);
console.log(obj);

getter 與 setter

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

html
<body>
  <ul id="list"></ul>

  <script>
    class Person {
      constructor(name, age, id) {
        this.name = name;
        this.age = age;
        this.element = document.querySelector(`#${id}`);
      }

      get html() {
        return this.element.innerHTML;
      }

      set html(data) {
        this.element.innerHTML = data.map((item) => `<li>${item}</li>`).join('');
      }
    }

    let obj = new Person('sheep', 25, 'list');

    console.log(obj.html); // prints: ''

    // 在設定 html 的值時,set 會攔截到並將 li 插入到 list 裡
    obj.html = ['aaa', 'bbb', 'ccc'];
    console.log(obj.html); // prints: '<li>aaa</li><li>bbb</li><li>ccc</li>'
  </script>
</body>

靜態方法與靜態屬性

類別相當於實例的原型,所有在類別中定義的方法,都會被實體繼承。如果在一個方法前,加上 static 關鍵字,就表示該方法不會被實體繼承,而是直接透過類別來呼叫,這就稱為“靜態方法”。

靜態屬性指的是 Class 本身的屬性,即 Class.propName,而不是定義在實體物件(this)上的屬性。

js
class Person {
  static myName = 'person類別的名字';
  static myMethod = function () {
    console.log('my method');
  };

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

  say() {
    console.log(this.name, this.age);
  }
}

// 舊寫法
// Person.myName = 'person類別的名字';
// Person.myMethod = function () {
//   console.log('my method');
// };

let obj = new Person('sheep', 25);

console.log(Person.myName);
Person.myMethod();

Class 繼承

Class 可以透過 extends 關鍵字實現繼承,這比 ES5 的透過修改原型鏈實現繼承,要清晰和方便很多。

js
class Person {
  static myName = 'person類別的名字';
  static myMethod = function () {
    console.log('my method');
  };

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

  say() {
    console.log(this.name, this.age);
  }
}

class Student extends Person {
  constructor(name, age, score) {
    super(name, age);
    this.score = score;
  }

  say() {
    super.say();
    console.log(this.score);
  }

  getScore() {
    console.log(this.score);
  }
}

let obj = new Student('sheep', 25, 100);

TIP

父類別的靜態方法和屬性也會被子類別繼承

OOP render 案例

html
<body>
  <div class="box1">
    <h1></h1>
    <ul></ul>
  </div>
  <div class="box2">
    <h1></h1>
    <img src="" alt="" style="width: 100px" />
    <ul></ul>
  </div>

  <script>
    var data1 = {
      title: '體育',
      list: ['體育-1', '體育-2', '體育-3'],
    };

    var data2 = {
      title: '綜藝',
      url: 'https://pic.maizuo.com/usr/movie/5011ee407fb407d47e333a3935ec33d1.jpg?x-oss-process=image/quality,Q_70',
      list: ['綜藝-1', '綜藝-2', '綜藝-3'],
    };

    class CreateBox {
      constructor(selector, data) {
        this.element = document.querySelector(selector);
        this.title = data.title;
        this.list = data.list;
        this.render();
      }

      render() {
        let oh1 = this.element.querySelector('h1');
        let oul = this.element.querySelector('ul');

        oh1.innerHTML = this.title;
        oul.innerHTML = this.list.map((item) => `<li>${item}</li>`).join('');
      }
    }

    new CreateBox('.box1', data1);

    class CreateImgBox extends CreateBox {
      constructor(selector, data) {
        super(selector, data);
        this.imgUrl = data.url;
        this.render();
      }

      render() {
        super.render();
        let oimg = this.element.querySelector('img');
        oimg.src = this.imgUrl;
      }
    }

    new CreateImgBox('.box2', data2);
  </script>
</body>

執行上面的程式碼後,瀏覽器上會出現下圖的樣子:

模組化

JavaScript 現在有兩種模組。一種是 ES6 模組,簡稱 ESM;另一個是 CommonJS 模組,簡稱 CJS。

CommonJS 模組是 Node.js 專用的,與 ES6 模組不相容。語法上面,兩者最明顯的差異是,CommonJS 模組使用 require()module.exports,ES6 模組使用 importexport

ES6 Module 不是物件,而是透過 export 指令顯性指定匯出的程式碼,再透過 import 指令匯入。

ES Module 基本寫法

  • 預設匯出與匯入
js
export default A1;

import a1 from './1.js';
  • 具名匯出與匯入
js
export { A1, A2 };

import { A1, A2 } from './1.js';

import { A1 as a1, A2 as a2 } from './1.js';

import * as obj from './1.js';

案例改寫

將剛剛的案例使用 ES6 Module 來改寫:

首先分別將 CreateBoxCreateImgBox 兩個 Class 拆成兩個 js 模組。

js
// CreateBox.js
export class CreateBox {
  constructor(selector, data) {
    this.element = document.querySelector(selector);
    this.title = data.title;
    this.list = data.list;
    this.render();
  }

  render() {
    let oh1 = this.element.querySelector('h1');
    let oul = this.element.querySelector('ul');

    oh1.innerHTML = this.title;
    oul.innerHTML = this.list.map((item) => `<li>${item}</li>`).join('');
  }
}
js
// CreateImgBox.js
import { CreateBox } from './CreateBox.js';

class CreateImgBox extends CreateBox {
  constructor(selector, data) {
    super(selector, data);
    this.imgUrl = data.url;
    this.render();
  }

  render() {
    super.render();
    let oimg = this.element.querySelector('img');
    oimg.src = this.imgUrl;
  }
}

export default CreateImgBox;

然後再匯入到需要使用的地方:

html
<!-- 必須要是 type module -->
<script type="module">
  import { CreateBox } from './CreateBox.js';
  import CreateImgBox from './CreateImgBox.js';

  var data1 = {
    title: '體育',
    list: ['體育-1', '體育-2', '體育-3'],
  };

  var data2 = {
    title: '綜藝',
    url: '(略)',
    list: ['綜藝-1', '綜藝-2', '綜藝-3'],
  };

  new CreateBox('.box1', data1);
  new CreateImgBox('.box2', data2);
</script>

ES7 新特性

指數運算子 **

js
Math.pow(3, 2) === 3 ** 2; // 9

指數運算子是 right-associative: a ** b ** c 相當於 a ** (b ** c)

陣列的 includes 方法

js
[1, 2, NaN].includes(NaN) // true
[1, 2, NaN].indexOf(NaN) // -1

如果僅僅查詢資料是否在陣列中,建議使用 includes,如果是查詢資料的索引值,建議使用 indexOf 更好一點。