Skip to content

ECMAScript 6 學習筆記

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

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

Symbol

ES6 引入了一種新的原始型別 Symbol,表示獨一無二的值。 它是 JavaScript 語言的原始型別之一,其他的分別是: undefinednull、布林值(Boolean)、字串(String)、數值(Number)、物件(Object)。

使用 Symbol 作為物件屬性名

由於每一個 Symbol 值都是不相等的,這意味著 Symbol 值可以作為標識符,用於物件的屬性名,就能保證不會出現同名的屬性。 這對於一個物件由多個模組構成的情況非常有用,能防止某一個鍵(key)被不小心改寫或覆蓋。

js
let obj = {
  name: 'sheep',
  getName() {
    console.log(this.name);
  },
};

let name = Symbol();

obj[name] = 'hello';

console.log(obj);
/*
  {
    name: 'sheep', 
    Symbol(): 'hello', 
    getName: ƒ
  }
*/

console.log(obj[name]); // hello
console.log(obj.name); // sheep

作為常數

沒有 Symbol 時:

js
// before
const VIDEO = 1;
const AUDIO = 2;
const IMAGE = 3;

function play(type) {
  switch (type) {
    case VIDEO:
      console.log('播放影片');
      break;
    case AUDIO:
      console.log('播放音樂');
      break;
    case IMAGE:
      console.log('顯示圖片');
      break;
  }
}

play(AUDIO); // prints: 播放音樂
play(2); // prints: 播放音樂

可以發現當傳入 2 時仍然能正常使用這個函式。

改成使用 Symbol:

js
const VIDEO = Symbol();
const AUDIO = Symbol();
const IMAGE = Symbol();

function play(type) {
  switch (type) {
    case VIDEO:
      console.log('播放影片');
      break;
    case AUDIO:
      console.log('播放音樂');
      break;
    case IMAGE:
      console.log('顯示圖片');
      break;
  }
}

play(AUDIO); // prints: 播放音樂

只有約定好的常數才能正常使用這個函式。

常數使用 Symbol 值最大的好處,就是其他任何值都不可能有相同的值了,因此可以保證上面的 switch 語句會按設計的方式工作。

Iterator 迭代器

Iterator 的作用有三個:

一是為各種資料結構,提供一個統一的,簡便的存取介面;

二是使得資料結構的成員能夠按某種次序排列;

三是 ES6 創造了一種新的遍歷寫法 for...of 迴圈,Iterator 主要在提供 for...of 迴圈

js
let arr = ['aaa', 'bbb', 'ccc'];

for (let i of arr) {
  console.log(i); // prints: aaa, bbb, ccc
}

Symbol.iterator 屬性

一個資料結構只要具有 Symbol.iterator 屬性,就可以認為是“可迭代的”(iterable)。 Symbol.iterator 屬性本身是一個函式,就是當前資料結構預設的迭代器生成函式。執行這個函式,就會回傳一個迭代器。

Iterator 的遍歷過程是這樣的:

  1. 建立一個指標(pointer),指向目前資料結構的起始位置。也就是說,Iterator 本質上就是一個 pointer。
  2. 第一次呼叫指標的 next 方法,可以將指標指向資料結構的第一個成員。
  3. 第二次呼叫指標的 next 方法,指標就指向資料結構的第二個成員。
  4. 不斷呼叫指標的 next 方法,直到它指向資料結構的結束位置。
js
let arr = ['aaa', 'bbb', 'ccc'];

let iter = arr[Symbol.iterator]();
// 回傳的 iter 就是 Iterator 物件

console.log(iter.next()); // prints: {value: 'aaa', done: false}
console.log(iter.next()); // prints: {value: 'bbb', done: false}
console.log(iter.next()); // prints: {value: 'ccc', done: false}
console.log(iter.next()); // prints: {value: undefined, done: true}

原生具備 Iterator 的資料結構如下:

  • Array
  • Set
  • Map
  • String
  • arguments 物件
  • NodeList 物件

如何對物件進行 for of 遍歷?

如果是一個線性的物件,可以透過原型將陣列身上的 iterator 賦值給我們手動埋的 Symbol.iterator 中:

js
let obj = {
  0: 'sheep',
  1: 'hitsuji',
  2: '綿羊',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator],
};

for (let i of obj) {
  console.log(i); // prints: sheep, hitsuji, 綿羊
}

進階實作:

js
let obj2 = {
  code: 200,
  name: 'obj2',
  list: ['sheep', 'hitsuji', '綿羊'],
  // 迭代器
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.list.length) {
          return {
            value: this.list[index++],
            done: false,
          };
        }
        return { value: undefined, done: true };
      },
    };
  },
};

let iter = obj2[Symbol.iterator]();

console.log(iter.next()); // prints: {value: 'sheep', done: false}
console.log(iter.next()); // prints: {value: 'hitsuji', done: false}
console.log(iter.next()); // prints: {value: '綿羊', done: false}
console.log(iter.next()); // prints: {value: undefined, done: true}

// 透過 for...of 去遍歷
for (let i of obj2) {
  console.log(i); // prints: sheep, hitsuji, 綿羊
}

Set

ES6 提供了新的資料結構 Set。 它類似於陣列,但是成員的值都是唯一的,沒有重複的值。

基本用法

js
let s1 = new Set([1, 2, 3, 2, 3]);

console.log(s1); // prints: Set(3) {1, 2, 3}

// 將 Set 轉回普通陣列
console.log([...s1]); // prints: [1, 2, 3]
console.log(Array.from(s1)); // [prints: 1, 2, 3]

通過 add() 方法可以向 Set 添加新元素,且不會添加重複的值:

js
let s2 = new Set();
s2.add(1);
s2.add(2);
s2.add(2);
s2.add(3);
console.log(s2); // Set(3) {1, 2, 3}

Set 實體物件的屬性和方法

Set.prototype.size:回傳 Set 實體的成員總數。

js
let s1 = new Set([1, 2, 3, 2, 3]);
console.log(s1.size); // prints: 3

Set.prototype.add(value):添加某個值,回傳 Set 結構本身。

js
// 可以鏈式呼叫
s1.add(4).add(5);
console.log(s1); // prints: Set(5) {1, 2, 3, 4, 5}

Set.prototype.has(value):回傳一個 boolean,表示該值是否為 Set 的成員。

js
console.log(s1.has(8)); // false
console.log(s1.has(5)); // true

Set.prototype.delete(value):刪除某個值,回傳一個 boolean,表示刪除是否成功。

js
s1.delete(5);
console.log(s1.has(5)); // false

Set.prototype.clear():清除所有成員,沒有回傳值。

js
s1.clear();
console.log(s1); // prints: Set(0) {size: 0}

迭代(遍歷) Sets

Set.prototype.keys():回傳鍵名的迭代器

Set.prototype.values():回傳鍵值的迭代器

Set.prototype.entries():回傳鍵值對的迭代器

Set.prototype.forEach():迭代每個成員

js
let s2 = new Set([11, 22, 33]);

for (let i of s2) {
  console.log(i); // prints: 11, 22, 33
}

for (let i of s2.keys()) {
  console.log(i); // prints: 11, 22, 33
}

for (let i of s2.values()) {
  console.log(i); // prints: 11, 22, 33
}

for (let i of s2.entries()) {
  console.log(i); // prints: [11, 11], [22, 22], [33, 33]
}

// entries 在陣列上的應用
let arr = ['aa', 'bb', 'cc'];

for (let [index, item] of arr.entries()) {
  console.log(index, item); // prints: 0 'aa', 1 'bb', 2 'cc'
}

s2.forEach((item, index) => {
  console.log(item, index); // prints: 11 11, 22 22, 33 33
});

刪除重複項的應用

有一個複雜的陣列結構如下,如何做到去除重複的成員?

js
let list = [
  1,
  2,
  2,
  'sheep',
  'sheep',
  [1, 2],
  [3, 4],
  [1, 2],
  { name: 'sheep' },
  { age: 25 },
  { name: 'sheep' },
  undefined,
  undefined,
  NaN,
  NaN,
];

利用 Set 實作:

js
function uni(arr) {
  const res = new Set();
  return arr.filter((item) => {
    const id = JSON.stringify(item);
    // 判斷 has return false
    if (res.has(id)) return false;

    // 沒有 return true
    res.add(id);
    return true;
  });
}

Map

類似於物件,也是鍵值對(key-value pairs)的集合,但是 key 的範圍不限於字串,各種型別的值(包括物件)都可以當作 key。

基本用法

js
let m1 = new Map([
  ['name', 'sheep'],
  ['age', 25],
  [{ a: 1 }, 'hello'],
]);

console.log(m1);
// prints: Map(3) {'name' => 'sheep', 'age' => 25, {…} => 'hello'}

let m2 = new Map();
m2.set('name', 'sheep');
m2.set('age', 100);
m2.set({ a: 1 }, 'hello');

console.log(m2);
// prints: Map(3) {'name' => 'sheep', 'age' => 25, {…} => 'hello'}

Map 同時也內建 iterator 在裡面,可以使用 spread 讓它轉成一個二維陣列。

例如:[...m2] 會轉為 [['name', 'sheep'], ['age', 100], [{ a: 1 }, 'hello']]

Map 實體物件的屬性和方法

Map 的屬性和方法與 Set 幾乎一樣。

  • 屬性:

Map.prototype.size:回傳 Map 結構的成員總數。

  • 方法:

Map.prototype.set(key, value):設置鍵名 key 對應的鍵值為 value,然後回傳整個 Map 結構。 如果 key 已經有值,則鍵值會被更新,否則就新生成該鍵。

Map.prototype.get(key):讀取 key 對應的鍵值,如果找不到 key,回傳 undefined

Map.prototype.has(key):回傳一個布林值,表示某個鍵是否在當前 Map 物件之中。

Map.prototype.delete(key):刪除某個鍵,回傳 true。 如果刪除失敗,回傳 false

Map.prototype.clear():清除所有成員,沒有回傳值。

迭代 Map

Map.prototype.keys():回傳鍵名的迭代器

Map.prototype.values():回傳鍵值的迭代器

Map.prototype.entries():回傳所有成員的迭代器

Map.prototype.forEach():迭代 Map 的每個成員

js
const map = new Map([
  ['F', 'no'],
  ['T', 'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同於使用 map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

Proxy

Proxy 如其名,它的作用是在物件和物件的屬性值之間設置一個代理,獲取該物件的值或者設置該物件的值,以及實例化等等多種操作,都會被攔截住,經過這一層我們可以統一處理,我們可以認為它就是"代理器"

如果我們想要在修改一個物件屬性時,能夠去攔截到這個操作時,在 ES6 之前我們會使用 Object.defineProperty() 來實作:

html
<body>
  <div id="box"></div>

  <script>
    let obj = {};

    Object.defineProperty(obj, 'data', {
      get() {
        console.log('get');
        return box.innerHTML;
      },
      set(val) {
        console.log('set', val);
        // 設置 DOM
        box.innerHTML = val;
      },
    });
  </script>
</body>

利用 getset 去讓我們在修改 data 的同時也可以讓畫面響應式更新。

基本使用

到了 ES6 出現了一個更高級的 Proxy,它可以替代 Object.defineProperty,並且不會直接去改動到原始資料而是透過一層“代理”。

接著將剛剛的案例改成 Proxy 寫法:

html
<body>
  <div id="box"></div>

  <script>
    let obj = {};

    let proxy = new Proxy(obj, {
      get(target, key) {
        console.log('get', target[key]);
        return target[key];
      },
      set(target, key, value) {
        console.log('set', target, key, value);
        if (key === 'data') {
          box.innerHTML = value;
        }
        target[key] = value;
      },
    });
  </script>
</body>

Proxy 的 this 指向問題

在對 Set 或是 Map 進行代理時,需要注意 this 指向:

js
let s = new Set();

let proxy = new Proxy(s, {
  get(target, key) {
    let value = target[key];
    // 判斷如果是方法,修正 this 指向
    if (value instanceof Function) {
      return value.bind(target);
    }
    return value;
  },
  set() {
    console.log('set');
  },
});

Proxy 本質上屬於元程式設計(Metaprogramming)非破壞性資料攔截,在原物件的基礎上進行了功能的衍生而又不影響原物件,符合低耦合高內聚的設計理念。

Reflect

Reflect 可以用於獲取目標物件的行為,它與 Object 類似,但是更易讀,為操作物件提供了一種更優雅的方式。 它的方法與 Proxy 是對應的。

代替 Object 的某些方法

js
let obj = {};

Reflect.defineProperty(obj, 'name', {
  value: 'sheep',
  writable: false,
  enumerable: false,
  configurable: false,
});

修改某些 Object 的回傳結果

原本 Object 寫法在遇到無法定義屬性時會直接報錯:

js
let obj = {};

Object.defineProperty(obj, 'name', {
  value: 'sheep',
  writable: false,
  enumerable: false,
});

Object.defineProperty(obj, 'name', {
  value: 'hitsuji',
});
// Uncaught TypeError: Cannot redefine property: name at Function.defineProperty

Reflect 遇到錯誤時則只會回傳一個 false

js
Reflect.defineProperty(obj, 'name', {
  value: 'sheep',
  writable: false,
  enumerable: false,
});

const res = Reflect.defineProperty(obj, 'name', {
  value: 'hitsuji',
});

console.log(res); // prints: false

因此使用 Reflect 時就不用像過去一樣必須用 trycatch 去處理:

js
// 舊寫法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}
// 新寫法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

指令式變為函式行為

某些 Object 操作是指令式,比如 name in objdelete obj[name],而 Reflect.has(obj, name)Reflect.deleteProperty(obj, name) 讓它們變成了函式行為。

js
let obj = {
  name: 'sheep',
};
// 舊寫法
console.log('name' in obj); // true
// 新寫法
console.log(Reflect.has(obj, 'name')); // true

// 舊寫法
delete obj.name;
// 新寫法
Reflect.deleteProperty(obj, 'name');

配合 Proxy

將前面 Proxy 的案例結合 Reflect 進行改造:

js
let s = new Set();

let proxy = new Proxy(s, {
  get(target, key) {
    // 判斷如果是方法,修正 this 指向
    let value = Reflect.get(target, key);
    if (value instanceof Function) {
      return value.bind(target);
    }
    return target[key];
  },
  set(target, key, value) {
    Reflect.set(...arguments);
  },
});

有了 Proxy + Reflect 就可以輕鬆地去攔截一個陣列,這是過去 Object.defineProperty 所無法做到的事情。(Vue3 將響應式資料改成 Proxy 後就不需要在對陣列方法進行一些改裝,直接用原生即可。)

js
let arr = [1, 2, 3];

let proxy = new Proxy(arr, {
  get(target, key) {
    console.log('get', key);
    return Reflect.get(...arguments);
  },
  set(target, key, value) {
    console.log('set', key, value);
    return Reflect.set(...arguments);
  },
});

// 直接使用原生方法,Reflect 會去攔截到
proxy.push(4);
console.log(proxy); // Proxy {0: 1, 1: 2, 2: 3, 3: 4}