ECMAScript

2020-08-09 02:52:32

Study Notes

let 與塊級作用域

let 語句宣告一個塊級作用域的本地變數,並且可選的將其初始化爲一個值。

描述

let 允許你宣告一個作用域被限制在 塊級中的變數、語句或者表達式。與 var 關鍵字不同的是, var 宣告的變數只能是全域性或者整個函數塊的。 var 和 let 的不同之處在於後者是在編譯時才初始化(見下面 下麪)。

作用域規則

let 宣告的變數只在其宣告的塊或子塊中可用,這一點,與 var 相似。二者之間最主要的區別在於 var 宣告的變數的作用域是整個封閉函數。

function varTest() {
  var x = 1;
  {
    var x = 2; // 同樣的變數!
    console.log(x); // 2
  }
  console.log(x); // 2
}

function letTest() {
  let x = 1;
  {
    let x = 2; // 不同的變數
    console.log(x); // 2
  }
  console.log(x); // 1
}
varTest();
letTest();

重複宣告

在同一個函數或塊作用域中重複宣告同一個變數會引起 SyntaxError。

if (x) {
  let foo;
  let foo; // SyntaxError thrown.
}

在 switch 語句中只有一個塊,你可能因此而遇到錯誤。

let x = 1;
switch (x) {
  case 0:
    let foo;
    break;

  case 1:
    let foo; // SyntaxError for redeclaration.
    break;
}

然而,需要特別指出的是,一個巢狀在 case 子句中的塊會建立一個新的塊作用域的詞法環境,就不會產生上訴重複宣告的錯誤。

let x = 1;
switch (x) {
  case 0: {
    let foo;
    break;
  }

  case 1: {
    let foo; // SyntaxError for redeclaration.
    break;
  }
}

暫存死區

與通過 var 宣告的有初始化值 undefined 的變數不同,通過 let 宣告的變數直到它們的定義被執行時才初始化。在變數初始化前存取該變數會導致 ReferenceError。該變數處在一個自塊頂部到初始化處理的「暫存死區」中。

function do_something() {
  console.log(bar); // undefined
  console.log(foo); // ReferenceError
  var bar = 1;
  let foo = 2;
}
do_something();

暫存死區與 typeof

與通過 var 宣告的變數, 有初始化值 undefined 和只是未宣告的變數不同的是,如果使用 typeof 檢測在暫存死區中的變數, 會拋出 ReferenceError 異常:

// prints out 'undefined'
console.log(typeof undeclaredVariable);

// results in a 'ReferenceError'
console.log(typeof i);
let i = 10;

暫存死區和靜態作用域/詞法作用域的相關例子

由於詞法作用域,表達式(foo + 55)內的識別符號 foo 被認爲是 if 塊的 foo 變數,而不是值爲 33 的塊外面的變數 foo。

在同一行,這個 if 塊中的 foo 已經在詞法環境中被建立了,但是還沒有到達(或者終止)它的初始化(這是語句本身的一部分)。

這個 if 塊裡的 foo 還依舊在暫存死區裡。

function test() {
  var foo = 33;
  {
    let foo = foo + 55; // ReferenceError
  }
}
test();

在以下情況下,這種現象可能會使您感到困惑。 let n of n.a 已經在 for 回圈塊的私有範圍內。因此,識別符號 n.a 被解析爲位於指令本身(「let n」)中的「 n」物件的屬性「 a」。

在沒有執行到它的初始化語句之前,它仍舊存在於暫存死區中。

function go(n) {
  // n here is defined!
  console.log(n); // Object {a: [1,2,3]}

  for (let n of n.a) {
    // ReferenceError
    console.log(n);
  }
}

go({ a: [1, 2, 3] });

塊級作用域

用在塊級作用域中時, let 將變數的作用域限制在塊內, 而 var 宣告的變數的作用域是在函數內.

var a = 1;
var b = 2;

if (a === 1) {
  var a = 11; // the scope is global
  let b = 22; // the scope is inside the if-block

  console.log(a); // 11
  console.log(b); // 22
}

console.log(a); // 11
console.log(b); // 2

而這種 var 與 let 合併的宣告方式會報 SyntaxError 錯誤, 因爲 var 會將變數提升至塊的頂部, 這會導致隱式地重複宣告變數.

let x = 1;

{
  var x = 2; // SyntaxError for re-declaration
}

const

常數是塊級作用域,很像使用 let 語句定義的變數。常數的值不能通過重新賦值來改變,並且不能重新宣告。

描述

此宣告建立一個常數,其作用域可以是全域性或本地宣告的塊。 與 var 變數不同,全域性常數不會變爲 window 物件的屬性。需要一個常數的初始化器;也就是說,您必須在宣告的同一語句中指定它的值(這是有道理的,因爲以後不能更改)。

const 宣告建立一個值的只讀參照。但這並不意味着它所持有的值是不可變的,只是變數識別符號不能重新分配。例如,在參照內容是物件的情況下,這意味着可以改變物件的內容(例如,其參數)。

關於「暫存死區」的所有討論都適用於 let 和 const。

一個常數不能和它所在作用域內的其他變數或函數擁有相同的名稱。

// 注意: 常數在宣告的時候可以使用大小寫,但通常情況下全部用大寫字母。

// 定義常數MY_FAV並賦值7
const MY_FAV = 7;

// 報錯
MY_FAV = 20;

// 輸出 7
console.log("my favorite number is: " + MY_FAV);

// 嘗試重新宣告會報錯
const MY_FAV = 20;

//  MY_FAV 保留給上面的常數,這個操作會失敗
var MY_FAV = 20;

// 也會報錯
let MY_FAV = 20;

// 注意塊範圍的性質很重要
if (MY_FAV === 7) {
    // 沒問題,並且建立了一個塊作用域變數 MY_FAV
    // (works equally well with let to declare a block scoped non const variable)
    let MY_FAV = 20;

    // MY_FAV 現在爲 20
    console.log('my favorite number is ' + MY_FAV);

    // 這被提升到全域性上下文並引發錯誤
    var MY_FAV = 20;
}

// MY_FAV 依舊爲7
console.log("my favorite number is " + MY_FAV);

// 常數要求一個初始值
const FOO; // SyntaxError: missing = in const declaration

// 常數可以定義成物件
const MY_OBJECT = {"key": "value"};

// 重寫物件和上面一樣會失敗
MY_OBJECT = {"OTHER_KEY": "value"};

// 物件屬性並不在保護的範圍內,下面 下麪這個宣告會成功執行
MY_OBJECT.key = "otherValue";

// 也可以用來定義陣列
const MY_ARRAY = [];
// It's possible to push items into the array
// 可以向陣列填充數據
MY_ARRAY.push('A'); // ["A"]
// 但是,將一個新陣列賦給變數會引發錯誤
MY_ARRAY = ['B']

解構賦值

解構賦值語法是一種 Javascript 表達式。通過解構賦值, 可以將屬性/值從物件/陣列中取出,賦值給其他變數。

解構陣列

變數宣告並賦值時的解構

const foo = ['one', 'two', 'three'];

const [one, two, three] = foo;
console.log(one); // "one"
console.log(two); // "two"
console.log(three); // "three"

變數先宣告後賦值時的解構

通過解構分離變數的宣告,可以爲一個變數賦值。

let a, b;

[a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2

預設值

爲了防止從陣列中取出一個值爲 undefined 的物件,可以在表達式左邊的陣列中爲任意物件預設預設值。

const [c = 5, d = 7] = [1];
console.log(c); // 1
console.log(d); // 7

交換變數

在一個解構表達式中可以交換兩個變數的值。

沒有解構賦值的情況下,交換兩個變數需要一個臨時變數(或者用低階語言中的 XOR-swap 技巧)。

let e = 1;
let f = 3;

[e, f] = [f, e];
console.log(e); // 3
console.log(f); // 1

解析一個從函數返回的陣列

從一個函數返回一個數組是十分常見的情況。解構使得處理返回值爲陣列時更加方便。

在下面 下麪例子中,要讓 [1, 2] 成爲函數的 f() 的輸出值,可以使用解構在一行內完成解析。

function fn() {
  return [1, 2];
}

const [g, h] = fn();
console.log(g); // 1
console.log(h); // 2

忽略某些返回值

你也可以忽略你不感興趣的返回值:

function fn1() {
  return [1, 2, 3];
}

const [i, , j] = fn1();
console.log(i); // 1
console.log(j); // 3

將剩餘陣列賦值給一個變數

當解構一個數組時,可以使用剩餘模式,將陣列剩餘部分賦值給一個變數。

const [k, ...l] = [1, 2, 3];
console.log(k); // 1
console.log(l); // [2, 3]

用正則表達式匹配提取值

用正則表達式的 exec() 方法匹配字串會返回一個數組,該陣列第一個值是完全匹配正則表達式的字串,然後的值是匹配正則表達式括號內內容部分。解構賦值允許你輕易地提取出需要的部分,忽略完全匹配的字串——如果不需要的話。

function parseProtocol(url) {
  const parsedURL = /^(\w+)\:\/\/([^\/]+)\/(.*)$/.exec(url);
  if (!parsedURL) {
    return false;
  }
  console.log(parsedURL); // ["https://developer.mozilla.org/en-US/Web/JavaScript", "https", "developer.mozilla.org", "en-US/Web/JavaScript"]

  const [, protocol] = parsedURL;
  return protocol;
}

console.log(
  parseProtocol('https://developer.mozilla.org/en-US/Web/JavaScript'),
); // "https"

解構物件

基本賦值

const o = { p: 42, q: true };
const { p, q } = o;

console.log(p); // 42
console.log(q); // true

無宣告賦值

一個變數可以獨立於其宣告進行解構賦值。

let o, p;

({ o, p } = { o: 1, p: 2 });
console.log(o); // 1
console.log(p); // 2

給新的變數名賦值

可以從一個物件中提取變數並賦值給和物件屬性名不同的新的變數名。

const { p: age, q: bar } = { p: 42, q: true };

console.log(age); // 42
console.log(bar); // true

預設值

變數可以先賦予預設值。當要提取的物件沒有對應的屬性,變數就被賦予預設值。

const { q = 10, r = 5 } = { q: 3 };

console.log(q); // 3
console.log(r); // 5

給新的變數命名並提供預設值

一個屬性可以同時 1)從一個物件解構,並分配給一個不同名稱的變數 2)分配一個預設值,以防未解構的值是 undefined。

const { a: aa = 10, b: bb = 5 } = { a: 3 };

console.log(aa); // 3
console.log(bb); // 5

函數參數預設值

function drawES2015Chart({
  size = 'big',
  cords = { x: 0, y: 0 },
  radius = 25,
} = {}) {
  console.log(size, cords, radius);
  // do some chart drawing
}

drawES2015Chart({
  cords: { x: 18, y: 30 },
  radius: 30,
});

解構巢狀物件和陣列

const metadata = {
  title: 'Scratchpad',
  translations: [
    {
      locale: 'de',
      localization_tags: [],
      last_edit: '2014-04-14T08:43:37',
      url: '/de/docs/Tools/Scratchpad',
      title: 'JavaScript-Umgebung',
    },
  ],
  url: '/en-US/docs/Tools/Scratchpad',
};

let {
  title: englishTitle, // rename
  translations: [
    {
      title: localeTitle, // rename
    },
  ],
} = metadata;

console.log(englishTitle); // "Scratchpad"
console.log(localeTitle); // "JavaScript-Umgebung"

For of 迭代和解構

const people = [
  {
    name: 'Mike Smith',
    family: {
      mother: 'Jane Smith',
      father: 'Harry Smith',
      sister: 'Samantha Smith',
    },
    age: 35,
  },
  {
    name: 'Tom Jones',
    family: {
      mother: 'Norah Jones',
      father: 'Richard Jones',
      brother: 'Howard Jones',
    },
    age: 25,
  },
];

for (const {
  name: n,
  family: { father: f },
} of people) {
  console.log('Name: ' + n + ', Father: ' + f);
}

// "Name: Mike Smith, Father: Harry Smith"
// "Name: Tom Jones, Father: Richard Jones"

從作爲函數實參的物件中提取數據

function userId({ id }) {
  return id;
}

function whoIs({ displayName, fullName: { firstName: name } }) {
  console.log(displayName + ' is ' + name);
}

const user = {
  id: 42,
  displayName: 'jdoe',
  fullName: {
    firstName: 'John',
    lastName: 'Doe',
  },
};

console.log('userId: ' + userId(user)); // "userId: 42"
whoIs(user); // "jdoe is John"

物件屬性計算名和解構

let key = 'z';
let { [key]: s } = { z: 'bar' };

console.log(s); // "bar"

物件解構中的 Rest

Rest 屬性收集那些尚未被解構模式拾取的剩餘可列舉屬性鍵。

let { t, u, ...rest } = { t: 10, u: 20, w: 30, x: 40 };
console.log(t); // 10
console.log(u); // 20
console.log(rest); // { c: 30, d: 40 }

解構物件時會查詢原型鏈(如果屬性不在物件自身,將從原型鏈中查詢)

// 宣告物件 和 自身 self 屬性
let obj = { self: '123' };
// 在原型鏈中定義一個屬性 prot
obj.__proto__.prot = '456';
// test
const { self, prot } = obj;
console.log(self); // "123"
console.log(prot); // "456"(存取到了原型鏈)

Template String(模板字串)

模板字面量 是允許嵌入表達式的字串字面量。你可以使用多行字串和字串插值功能。它們在 ES2015 規範的先前版本中被稱爲「模板字串」。

描述

模板字串使用反引號 (``) 來代替普通字串中的用雙引號和單引號。模板字串可以包含特定語法(${expression})的佔位符。佔位符中的表達式和周圍的文字會一起傳遞給一個預設函數,該函數負責將所有的部分連線起來,如果一個模板字串由表達式開頭,則該字串被稱爲帶標籤的模板字串,該表達式通常是一個函數,它會在模板字串處理後被呼叫,在輸出最終結果前,你都可以通過該函數來對模板字串進行操作處理。在模版字串內使用反引號(`)時,需要在它前面加跳脫符(\)。

`\`` === '`'; // --> true

多行字串

在新行中插入的任何字元都是模板字串中的一部分,使用普通字串,你可以通過以下的方式獲得多行字串:

console.log('string text line 1\n' + 'string text line 2');
// "string text line 1
// string text line 2"

要獲得同樣效果的多行字串,只需使用如下程式碼:

console.log(`string text line 1
string text line 2`);
// "string text line 1
// string text line 2"

插入表達式

在普通字串中嵌入表達式,必須使用如下語法:

const a = 5;
const b = 10;
console.log('Fifteen is ' + (a + b) + ' and\nnot ' + (2 * a + b) + '.');
// "Fifteen is 15 and
// not 20."

現在通過模板字串,我們可以使用一種更優雅的方式來表示:

const a = 5;
const b = 10;
console.log(`Fifteen is ${a + b} and
not ${2 * a + b}.`);
// "Fifteen is 15 and
// not 20."

帶標籤的模板字串

更高階的形式的模板字串是帶標籤的模板字串。標籤使您可以用函數解析模板字串。標籤函數的第一個參數包含一個字串值的陣列。其餘的參數與表達式相關。最後,你的函數可以返回處理好的的字串(或者它可以返回完全不同的東西 , 如下個例子所述)。用於該標籤的函數的名稱可以被命名爲任何名字。

const person = 'Mike';
const age = 28;

function myTag(strings, personExp, ageExp) {
  const str0 = strings[0]; // "that "
  const str1 = strings[1]; // " is a "

  let ageStr;
  if (ageExp > 99) {
    ageStr = 'centenarian';
  } else {
    ageStr = 'youngster';
  }

  return str0 + personExp + str1 + ageStr;
}

const output = myTag`that ${person} is a ${age}`;

console.log(output);
// that Mike is a youngster

正如下面 下麪例子所展示的,標籤函數並不一定需要返回一個字串。

function template(strings, ...keys) {
  return function (...values) {
    const dict = values[values.length - 1] || {};
    const result = [strings[0]];
    keys.forEach(function (key, i) {
      const value = Number.isInteger(key) ? values[key] : dict[key];
      result.push(value, strings[i + 1]);
    });
    return result.join('');
  };
}

const t1Closure = template`${0}${1}${0}!`;
console.log(t1Closure('Y', 'A')); // "YAY!"
const t2Closure = template`${0} ${'foo'}!`;
console.log(t2Closure('Hello', { foo: 'World' })); // "Hello World!"

原始字串

在標籤函數的第一個參數中,存在一個特殊的屬性 raw ,我們可以通過它來存取模板字串的原始字串,而不經過特殊字元的替換。

function tag(strings) {
  return strings.raw[0];
}

console.log(tag`string text line 1 \n string text line 2`);
// logs "string text line 1 \n string text line 2" ,

帶標籤的模版字面量及跳脫序列

自 ES2016 起,帶標籤的模版字面量遵守以下跳脫序列的規則:

  • Unicode 字元以"\u"開頭,例如\u00A9
  • Unicode 碼位用"\u{}"表示,例如\u{2F804}
  • 十六進制以"\x"開頭,例如\xA9
  • 八進制以""和數位開頭,例如\251

這表示類似下面 下麪這種帶標籤的模版是有問題的,因爲對於每一個 ECMAScript 語法,解析器都會去查詢有效的跳脫序列,但是隻能得到這是一個形式錯誤的語法:

function latex(str) {
  return { cooked: str[0], raw: str.raw[0] };
}
latex`\unicode`;
// 在較老的ECMAScript版本中報錯(ES2016及更早)
// SyntaxError: malformed Unicode character escape sequence

ES2018 關於非法跳脫序列的修訂

帶標籤的模版字串應該允許巢狀支援常見跳脫序列的語言(例如 DSLs、LaTeX)。ECMAScript 提議模版字面量修訂(第 4 階段,將要整合到 ECMAScript 2018 標準) 移除對 ECMAScript 在帶標籤的模版字串中跳脫序列的語法限制。

不過,非法跳脫序列在"cooked"當中仍然會體現出來。它們將以 undefined 元素的形式存在於"cooked"之中:

Functions Default(函數預設參數)

函數預設參數允許在沒有值或 undefined 被傳入時使用預設形參。

傳入 undefined vs 其他假值

function test(num = 1) {
  console.log(typeof num);
  console.log(num);
}

test();
/**
 * number
 * 1
 */
test(undefined);
/**
 * number
 * 1
 */

test('');
/**
 * string
 * ''
 */
test(null);
/**
 * object
 * null
 */

呼叫時解析

在函數被呼叫時,參數預設值會被解析,所以不像 Python 中的例子,每次函數呼叫時都會建立一個新的參數物件。

function append(value, array = []) {
  array.push(value);
  return array;
}

console.log(append(1)); //[1]
console.log(append(2)); //[2], 不是 [1, 2]

預設參數可用於後面的預設參數

已經遇到的參數可用於以後的預設參數:

function greet(name, greeting, message = greeting + ' ' + name) {
  return [name, greeting, message];
}

console.log(greet('David', 'Hi')); // ["David", "Hi", "Hi David"]
console.log(greet('David', 'Hi', 'Happy Birthday!')); // ["David", "Hi", "Happy Birthday!"]

位於預設參數之後非預設參數

在 Gecko 26 (Firefox 26 / Thunderbird 26 / SeaMonkey 2.23 / Firefox OS 1.2)之前,以下程式碼會造成 SyntaxError 錯誤。這已經在 bug 1022967 中修復,並在以後的版本中按預期方式工作。參數仍然設定爲從左到右,覆蓋預設參數,即使後面的參數沒有預設值。

function f(x = 1, y) {
  console.log([x, y]);
}

f(); // [1, undefined]
f(2); // [2, undefined]

有預設值的解構參數

你可以通過解構賦值爲參數賦值:

function fn([x, y] = [1, 2], { z: z } = { z: 3 }) {
  console.log(x + y + z);
}

fn(); // 6

Functions Rest Parameters(剩餘參數)

剩餘參數語法允許我們將一個不定數量的參數表示爲一個數組。

剩餘參數和 arguments 物件的區別

剩餘參數和 arguments 物件之間的區別主要有三個:

  • 剩餘參數只包含那些沒有對應形參的實參,而 arguments 物件包含了傳給函數的所有實參。
  • arguments 物件不是一個真正的陣列,而剩餘參數是真正的 Array 範例,也就是說你能夠在它上面直接使用所有的陣列方法,比如 sort,map,forEach 或 pop。
  • arguments 物件還有一些附加的屬性 (如 callee 屬性)。

解構剩餘參數

剩餘參數可以被解構,這意味着他們的數據可以被解包到不同的變數中。請參閱解構賦值

function f(...[a, b, c]) {
  console.log(a + b + c);
}

f(1); // NaN (b 和 c 是 undefined)
f(1, 2, 3); // 6
f(1, 2, 3, 4); // 6

demo

下例中,剩餘參數包含了從第二個到最後的所有實參,然後用第一個實參依次乘以它們:

function multiply(multiplier, ...theArgs) {
  return theArgs.map(function (element) {
    return multiplier * element;
  });
}

console.log(multiply(2, 1, 2, 3)); // [2, 4, 6]

下例演示了你可以在剩餘參數上使用任意的陣列方法,而 arguments 物件不可以:

function sortRestArgs(...theArgs) {
  return theArgs.sort();
}

console.log(sortRestArgs(5, 3, 7, 1)); // 彈出 1,3,5,7

function sortArguments() {
  return arguments.sort(); // 不會執行到這裏
}

console.log(sortArguments(5, 3, 7, 1)); // 拋出TypeError異常:arguments.sort is not a function

爲了在 arguments 物件上使用 Array 方法,它必須首先被轉換爲一個真正的陣列。

function sortArguments1() {
  const args = Array.prototype.slice.call(arguments);
  return args.sort();
}
console.log(sortArguments1(5, 3, 7, 1)); // shows 1, 3, 5, 7

Arrow Functions(箭頭函數)

箭頭函數表達式的語法比函數表達式更簡潔,並且沒有自己的 this,arguments,super 或 new.target。箭頭函數表達式更適用於那些本來需要匿名函數的地方,並且它不能用作建構函式。

描述

引入箭頭函數有兩個方面的作用:更簡短的函數並且不系結 this。

更短的函數

const elements = ['Hydrogen', 'Helium', 'Lithium', 'Beryllium'];

console.log(
  elements.map(function (element) {
    return element.length;
  }),
); // 返回陣列:[8, 6, 7, 9]

// 上面的普通函數可以改寫成如下的箭頭函數
console.log(
  elements.map((element) => {
    return element.length;
  }),
); // [8, 6, 7, 9]

// 當箭頭函數只有一個參數時,可以省略參數的圓括號
console.log(
  elements.map((element) => {
    return element.length;
  }),
); // [8, 6, 7, 9]

// 當箭頭函數的函數體只有一個 `return` 語句時,可以省略 `return` 關鍵字和方法體的花括號
console.log(elements.map((element) => element.length)); // [8, 6, 7, 9]

// 在這個例子中,因爲我們只需要 `length` 屬性,所以可以使用參數解構
// 需要注意的是字串 `"length"` 是我們想要獲得的屬性的名稱,而 `lengthFooBArX` 則只是個變數名,
// 可以替換成任意合法的變數名
console.log(elements.map(({ length: lengthFooBArX }) => lengthFooBArX)); // [8, 6, 7, 9]

沒有單獨的 this

在箭頭函數出現之前,每一個新函數根據它是被如何呼叫的來定義這個函數的 this 值:

  • 如果是該函數是一個建構函式,this 指針指向一個新的物件
  • 在嚴格模式下的函數呼叫下,this 指向 undefined
  • 如果是該函數是一個物件的方法,則它的 this 指針指向這個物件
function Person() {
  // Person() 建構函式定義 `this`作爲它自己的範例.
  this.age = 0;

  setTimeout(function growUp() {
    // 在非嚴格模式, growUp()函數定義 `this`作爲全域性物件,
    // 與在 Person()建構函式中定義的 `this`並不相同.
    this.age++;
    console.log(this.age); // NaN
  }, 1000);
}

new Person();

在 ECMAScript 3/5 中,通過將 this 值分配給封閉的變數,可以解決 this 問題。

function Person() {
  const that = this;
  that.age = 0;

  setTimeout(function growUp() {
    //  回撥參照的是`that`變數, 其值是預期的物件.
    that.age++;
    console.log(that.age); // 1
  }, 1000);
}
new Person();

箭頭函數不會建立自己的 this,它只會從自己的作用域鏈的上一層繼承 this。因此,在下面 下麪的程式碼中,傳遞給 setInterval 的函數內的 this 與封閉函數中的 this 值相同:

function Person() {
  this.age = 0;

  setTimeout(() => {
    this.age++; // |this| 正確地指向 p 範例
    console.log(this.age); // 1
  }, 1000);
}

new Person();

與嚴格模式的關係

鑑於 this 是詞法層面上的,嚴格模式中與 this 相關的規則都將被忽略。

const f = () => {
  return this;
};
function f1() {
  'use strict';
  return this;
}
function f2() {
  return this;
}
console.log(f() === global); // false
console.log(f1() === global); // false
console.log(f2() === global); // true

通過 call 或 apply 呼叫

由於 箭頭函數沒有自己的 this 指針,通過 call() 或 apply() 方法呼叫一個函數時,只能傳遞參數(不能系結 this),他們的第一個參數會被忽略。(這種現象對於 bind 方法同樣成立)

const adder = {
  base: 1,

  add: function (a) {
    const f = (v) => v + this.base;
    return f(a);
  },

  addThruCall: function (a) {
    const f = (v) => v + this.base;
    const b = {
      base: 2,
    };

    return f.call(b, a);
  },
};

console.log(adder.add(1)); // 輸出 2
console.log(adder.addThruCall(1)); // 仍然輸出 2

不系結 arguments

箭頭函數不系結 Arguments 物件。

const foo = (...args) => {
  console.log(arguments[0]); // {}
  console.log(args); // [1,2]
};
foo(1, 2);

箭頭函數不能用作構造器,和 new 一起用會拋出錯誤。

箭頭函數沒有 prototype 屬性。

yield 關鍵字通常不能在箭頭函數中使用(除非是巢狀在允許使用的函數內)。因此,箭頭函數不能用作函數生成器。

Object

Object 建構函式建立一個物件包裝器。

Object 建構函式的方法

Object.assign()

通過複製一個或多個物件來建立一個新的物件。

// 複製一個物件
const obj = { a: 1 };
const copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }

// 合併物件
const o1 = { a: 1 };
const o2 = { b: 2 };
const o3 = { c: 3 };

const obj1 = Object.assign(o1, o2, o3);
console.log(obj1); // { a: 1, b: 2, c: 3 }
console.log(o1); // { a: 1, b: 2, c: 3 }, 注意目標物件自身也會改變。

Object.create()

使用指定的原型物件和屬性建立一個新物件。

// 用Object.create實現類式繼承
// Shape - 父類別(superclass)
function Shape() {
  this.x = 0;
  this.y = 0;
}

// 父類別的方法
Shape.prototype.move = function (x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};

// Rectangle - 子類(subclass)
function Rectangle() {
  Shape.call(this); // call super constructor.
}

// 子類續承父類別
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

const rect = new Rectangle();

console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?', rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'

Object.defineProperty()

給物件新增一個屬性並指定該屬性的設定。

Object方法等

Proxy(代理)

Proxy 物件用於定義基本操作的自定義行爲(如屬性查詢、賦值、列舉、函數呼叫等)。

參數

target

要使用 Proxy 包裝的目標物件(可以是任何型別的物件,包括原生陣列,函數,甚至另一個代理)。

handler

一個通常以函數作爲屬性的物件,各屬性中的函數分別定義了在執行各種操作時代理 p 的行爲。

handler 物件的方法

handler 物件是一個容納一批特定屬性的佔位符物件。它包含有 Proxy 的各個捕獲器(trap)。

所有的捕捉器是可選的。如果沒有定義某個捕捉器,那麼就會保留源物件的預設行爲。

handler.getPrototypeOf()

Object.getPrototypeOf 方法的捕捉器。

handler.setPrototypeOf()

Object.setPrototypeOf 方法的捕捉器。

handler.isExtensible()

Object.isExtensible 方法的捕捉器。

handler.preventExtensions()

Object.preventExtensions 方法的捕捉器。

handler.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor 方法的捕捉器。

handler.defineProperty()

Object.defineProperty 方法的捕捉器。

handler.has()

in 操作符的捕捉器。

handler.get()

屬性讀取操作的捕捉器。

handler.set()

屬性設定操作的捕捉器。

handler.deleteProperty()

delete 操作符的捕捉器。

handler.ownKeys()

Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。

handler.apply()

函數呼叫操作的捕捉器。

handler.construct()

new 操作符的捕捉器。

demo

const handler = {
  get: function (obj, prop) {
    return prop in obj ? obj[prop] : 37;
  },
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37

let validator = {
  set: function (obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // The default behavior to store the value
    obj[prop] = value;

    // 表示成功
    return true;
  },
};

let person = new Proxy({}, validator);

person.age = 100;

console.log(person.age);
// 100

person.age = 'young';
// 拋出異常: Uncaught TypeError: The age is not an integer

person.age = 300;
// 拋出異常: Uncaught RangeError: The age seems invalid

具體使用檢視Proxy

Reflect

Reflect 是一個內建的物件,它提供攔截 JavaScript 操作的方法。這些方法與 proxy handlers 的方法相同。Reflect 不是一個函數物件,因此它是不可構造的。

描述

與大多數內建物件不同,Reflect 不是一個建構函式。你不能將其與一個 new 運算子一起使用,或者將 Reflect 物件作爲一個函數來呼叫。Reflect 的所有屬性和方法都是靜態的(就像 Math 物件)。

靜態方法

Reflect.apply(target, thisArgument, argumentsList)

對一個函數進行呼叫操作,同時可以傳入一個數組作爲呼叫參數。和 Function.prototype.apply() 功能類似。

console.log(Reflect.apply(Math.floor, undefined, [1.75]));
// 1;

console.log(
  Reflect.apply(String.fromCharCode, undefined, [104, 101, 108, 108, 111]),
);
// "hello"

console.log(
  Reflect.apply(RegExp.prototype.exec, /ab/, ['confabulation']).index,
);
// 4

console.log(Reflect.apply(''.charAt, 'ponies', [3]));
// "i"

Reflect.construct(target, argumentsList[, newTarget])

對建構函式進行 new 操作,相當於執行 new target(…args)。

function OneClass() {
  this.name = 'one';
}

function OtherClass() {
  this.name = 'other';
}

// 建立一個物件:
const obj1 = Reflect.construct(OneClass, global, OtherClass);

// 與上述方法等效:
const obj2 = Object.create(OtherClass.prototype);
OneClass.apply(obj2, global);

console.log(obj1.name); // 'one'
console.log(obj2.name); // 'one'

console.log(obj1 instanceof OneClass); // false
console.log(obj2 instanceof OneClass); // false

console.log(obj1 instanceof OtherClass); // true
console.log(obj2 instanceof OtherClass); // true

Reflect.defineProperty(target, propertyKey, attributes)

和 Object.defineProperty() 類似。如果設定成功就會返回 true

const student = {};
console.log(Reflect.defineProperty(student, 'name', { value: 'Mike' })); // true
console.log(student.name); // "Mike"

Reflect.deleteProperty(target, propertyKey)

作爲函數的 delete 操作符,相當於執行 delete target[name]。

const student = {};
console.log(Reflect.defineProperty(student, 'name', { value: 'Mike' })); // true
console.log(student.name); // "Mike"

// Reflect.deleteProperty
const obj = { x: 1, y: 2 };
console.log(Reflect.deleteProperty(obj, 'x')); // true
console.log(obj); // { y: 2 }

const arr = [1, 2, 3, 4, 5];
console.log(Reflect.deleteProperty(arr, '3')); // true
console.log(arr); // [1, 2, 3, , 5]

// 如果屬性不存在,返回 true
console.log(Reflect.deleteProperty({}, 'foo')); // true

// 如果屬性不可設定,返回 false
console.log(Reflect.deleteProperty(Object.freeze({ foo: 1 }), 'foo')); // false

Reflect.get(target, propertyKey[, receiver])

獲取物件身上某個屬性的值,類似於 target[name]。

let obj3 = { x: 1, y: 2 };
console.log(Reflect.get(obj3, 'x')); // 1

// Array
console.log(Reflect.get(['zero', 'one'], 1)); // "one"

// Proxy with a get handler
const x = { p: 1 };
const objProxy = new Proxy(x, {
  get(t, k, r) {
    return k + 'bar';
  },
});
console.log(Reflect.get(objProxy, 'foo')); // "foobar"

Reflect.getOwnPropertyDescriptor(target, propertyKey)

類似於 Object.getOwnPropertyDescriptor()。如果給定屬性存在於物件上,則返回它的屬性描述符,否則返回 undefined。

console.log(Reflect.getOwnPropertyDescriptor({ x: 'hello' }, 'x'));
// {value: "hello", writable: true, enumerable: true, configurable: true}

console.log(Reflect.getOwnPropertyDescriptor({ x: 'hello' }, 'y'));
// undefined

console.log(Reflect.getOwnPropertyDescriptor([], 'length'));
// {value: 0, writable: true, enumerable: false, configurable: false}

Reflect.getPrototypeOf(target)

類似於 Object.getPrototypeOf()。返回指定物件的原型

// 如果參數爲 Object,返回結果相同
console.log(Object.getPrototypeOf({})); // {}
console.log(Reflect.getPrototypeOf({})); // {}

// 在 ES5 規範下,對於非 Object,拋異常
console.log(Object.getPrototypeOf('foo')); // Throws TypeError
console.log(Reflect.getPrototypeOf('foo')); // Throws TypeError

// 在 ES2015 規範下,Reflect 拋異常, Object 強制轉換非 Object
console.log(Object.getPrototypeOf('foo')); // [String: '']
console.log(Reflect.getPrototypeOf('foo')); // Throws TypeError

// 如果想要模擬 Object 在 ES2015 規範下的表現,需要強制型別轉換
console.log(Reflect.getPrototypeOf(Object('foo'))); // [String: '']

Reflect.has(target, propertyKey)

判斷一個物件是否存在某個屬性,和 in 運算子 的功能完全相同。

console.log(Reflect.has({ x: 0 }, 'x')); // true
console.log(Reflect.has({ x: 0 }, 'y')); // false

// 如果該屬性存在於原型鏈中,返回true
console.log(Reflect.has({ x: 0 }, 'toString')); // true

// Proxy 物件的 .has() 控制代碼方法
const obj4Proxy = new Proxy(
  {},
  {
    has(t, k) {
      return k.startsWith('door');
    },
  },
);
console.log(Reflect.has(obj4Proxy, 'doorbell')); // true
console.log(Reflect.has(obj4Proxy, 'dormitory')); // false

Reflect.isExtensible(target)

類似於 Object.isExtensible().判斷一個物件是否可延伸

const empty = {};
console.log(Reflect.isExtensible(empty)); // === true

Reflect.preventExtensions(empty);
console.log(Reflect.isExtensible(empty)); // === false

Reflect.ownKeys(target)

返回一個包含所有自身屬性(不包含繼承屬性)的陣列。(類似於 Object.keys(), 但不會受 enumerable 影響).

console.log(Reflect.ownKeys({ z: 3, y: 2, x: 1 })); // [ "z", "y", "x" ]
console.log(Reflect.ownKeys([1])); // [ "0", "length"]

const sym = Symbol.for('comet');
const sym2 = Symbol.for('meteor');
const obj5 = {
  [sym]: 0,
  str: 0,
  '773': 0,
  '0': 0,
  [sym2]: 0,
  '-1': 0,
  '8': 0,
  'second str': 0,
};
console.log(Reflect.ownKeys(obj5));
// [ "0", "8", "773", "str", "-1", "second str", Symbol(comet), Symbol(meteor) ]

Reflect.preventExtensions(target)

類似於 Object.preventExtensions()。阻止新屬性新增到物件。

const empty = {};
console.log(Reflect.isExtensible(empty)); // === true

Reflect.preventExtensions(empty);
console.log(Reflect.isExtensible(empty)); // === false

Reflect.set(target, propertyKey, value[, receiver])

將值分配給屬性的函數。返回一個 Boolean,如果更新成功,則返回 true。

// Object
const obj6 = {};
Reflect.set(obj6, 'prop', 'value');
console.log(obj6.prop); // "value"

// Array
const arr1 = ['duck', 'duck', 'duck'];
Reflect.set(arr1, 2, 'goose');
console.log(arr1[2]); // "goose"

Reflect.setPrototypeOf(target, prototype)

除了返回型別以外,靜態方法 Reflect.setPrototypeOf() 與 Object.setPrototypeOf() 方法是一樣的。它可設定物件的原型(即內部的 [[Prototype]] 屬性)爲另一個物件或 null,如果操作成功返回 true,否則返回 false。

console.log(Reflect.setPrototypeOf({}, Object.prototype)); // true

// 可以將物件的[[Prototype]]更改爲null。
console.log(Reflect.setPrototypeOf({}, null)); // true

// 如果目標不可延伸,則返回false。
console.log(Reflect.setPrototypeOf(Object.freeze({}), null)); // false

// 如果導致原型鏈回圈,則返回false。
const target = {};
const proto = Object.create(target);
console.log(Reflect.setPrototypeOf(target, proto)); // false

Set

Set 物件允許你儲存任何型別的唯一值,無論是原始值或者是物件參照。

Set 物件是值的集合,你可以按照插入的順序迭代它的元素。 Set 中的元素只會出現一次,即 Set 中的元素是唯一的。

demo

/**
 * add() 方法用來向一個 Set 物件的末尾新增一個指定的值
 */
const mySet = new Set();

mySet.add(1);
mySet.add(5).add('some text').add(1); // 可以鏈式呼叫

console.log(mySet);
// Set {1, 5, "some text"} 重複的值沒有被新增進去

/**
 * delete() 方法可以從一個 Set 物件中刪除指定的元素。
 */
mySet.delete('bar'); // 返回 false,不包含 "bar" 這個元素
mySet.delete(1); // 返回 true,刪除成功
console.log(mySet); // Set {5, "some text"}

/**
 * clear() 方法用來清空一個 Set 物件中的所有元素。
 */
mySet.clear();
console.log(mySet); // Set {}

/**
 * entries() 方法返回一個新的迭代器物件 ,這個物件的元素是類似 [value, value] 形式的陣列,value 是集合物件中的每個元素,迭代器物件元素的順序即集合物件中元素插入的順序。由於集合物件不像 Map 物件那樣擁有 key,然而,爲了與 Map 物件的 API 形式保持一致,故使得每一個 entry 的 key 和 value 都擁有相同的值,因而最終返回一個 [value, value] 形式的陣列。
 */
mySet.add('foobar');
mySet.add(1);
mySet.add('baz');

const setIter = mySet.entries();

console.log(setIter.next().value); // ["foobar", "foobar"]
console.log(setIter.next().value); // [1, 1]
console.log(setIter.next().value); // ["baz", "baz"]

/**
 * forEach 方法會根據集閤中元素的插入順序,依次執行提供的回撥函數。
 */
function logSetElements(value1, value2, set) {
  console.log(`s[ ${value1} ] = ${value2} =>${[...set]}`);
}

new Set(['foo', 'bar', undefined]).forEach(logSetElements);
// s[ foo ] = foo =>foo,bar,
// s[ bar ] = bar =>foo,bar,
// s[ undefined ] = undefined =>foo,bar,

/**
 * has() 方法返回一個布爾值來指示對應的值value是否存在Set物件中。
 */
console.log(mySet.has('foobar')); // 返回 true
console.log(mySet.has('bar')); // 返回 false
const obj1 = { key1: 1 };
mySet.add(obj1);
console.log(mySet.has(obj1)); // 返回 true
console.log(mySet.has({ key1: 1 })); // 會返回 false,因爲其是另一個物件的參照
mySet.add({ key1: 1 }); // 現在 mySet 中有2條(不同參照的)物件了
console.log(mySet);
// Set { 'foobar', 1, 'baz', { key1: 1 }, { key1: 1 } }

/**
 *  values() 方法返回一個 Iterator  物件,該物件按照原Set 物件元素的插入順序返回其所有元素。
 */
const setIter1 = mySet.values();
console.log(setIter1.next().value); // foobar
console.log(setIter1.next().value); // 1
console.log(setIter1.next().value); // baz

/**
 *  keys() 方法是這個方法的別名 (與 Map 物件相似); 它的行爲與 value 方法完全一致,返回 Set 物件的元素。
 */
const setIter2 = mySet.keys();
console.log(setIter2.next().value); // foobar
console.log(setIter2.next().value); // 1
console.log(setIter2.next().value); // baz
console.log('---------------------');
/**
 * @@iterator 屬性的初始值和 values 屬性的初始值是同一個函數。
 */
for (const v of mySet) {
  console.log(v);
}

Map

Map 物件儲存鍵值對,並且能夠記住鍵的原始插入順序。任何值(物件或者原始值) 都可以作爲一個鍵或一個值。

描述

一個 Map 物件在迭代時會根據物件中元素的插入順序來進行 — 一個 for…of 回圈在每次迭代後會返回一個形式爲[key,value]的陣列。

Objects 和 maps 的比較

Objects 和 Maps 類似,它們都允許你按鍵存取一個值、刪除鍵、檢測一個鍵是否系結了值。因此(並且也沒有其他內建的替代方式了)過去我們一直都把物件當成 Maps 使用。不過 Maps 和 Objects 有一些重要的區別,在下列情況裡使用 Map 會是更好的選擇:

Map Object
意外的鍵 Map 預設情況不包含任何鍵。只包含顯式插入的鍵。 一個 Object 有一個原型, 原型鏈上的鍵名有可能和你自己在物件上的設定的鍵名產生衝突。
鍵的型別 一個 Map 的鍵可以是任意值,包括函數、物件或任意基本型別。 一個 Object 的鍵必須是一個 String 或是 Symbol。
鍵的順序 Map 中的 key 是有序的。因此,當迭代的時候,一個 Map 物件以插入的順序返回鍵值。 一個 Object 的鍵是無序的
Size Map 的鍵值對個數可以輕易地通過 size 屬性獲取 Object 的鍵值對個數只能手動計算
迭代 Map 是 iterable 的,所以可以直接被迭代。 迭代一個 Object 需要以某種方式獲取它的鍵然後才能 纔能迭代。
效能 在頻繁增刪鍵值對的場景下表現更好。 在頻繁新增和刪除鍵值對的場景下未作出優化。

demo

/**
 * set() 方法爲 Map 物件新增或更新一個指定了鍵(key)和值(value)的(新)鍵值對。
 */
const myMap = new Map();
myMap.set('bar', 'baz');
myMap.set(1, 'foo');

console.log(myMap); // Map { 'bar' => 'baz', 1 => 'foo' }

/**
 * get() 方法返回某個 Map 物件中的一個指定元素。
 */
console.log(myMap.get('bar')); // foo
console.log(myMap.get('baz')); // undefined

/**
 * forEach() 方法將會以插入順序對 Map 物件中的每一個鍵值對執行一次參數中提供的回撥函數。
 */
function logMapElements(value, key, map) {
  console.log(`'m[ ${key} ] = ${value} => ${map}`);
}
new Map([
  ['foo', 3],
  ['bar', {}],
  ['baz', undefined],
]).forEach(logMapElements);
// m[foo] = 3 => [object Map]
// m[bar] = [object Object] => [object Map]
// m[baz] = undefined => [object Map]

/**
 * entries() 方法返回一個新的包含 [key, value] 對的 Iterator 物件,返回的迭代器的迭代順序與 Map 物件的插入順序相同。
 */
const mapIter = myMap.entries();

console.log(mapIter.next().value); // [ 'bar', 'baz' ]
console.log(mapIter.next().value); // [ 1, 'foo' ]

/**
 * 方法has() 返回一個bool值,用來表明map 中是否存在指定元素.
 */
console.log(myMap.has('bar')); // returns true
console.log(myMap.has('baz')); // returns false

/**
 * keys() 返回一個參照的 Iterator 物件。它包含按照順序插入 Map 物件中每個元素的key值。
 */
const mapIter1 = myMap.keys();

console.log(mapIter1.next().value); // bar
console.log(mapIter1.next().value); // 1

/**
 * values() 方法返回一個新的Iterator物件。它包含按順序插入Map物件中每個元素的value值。
 */
const mapIter2 = myMap.values();

console.log(mapIter2.next().value); // baz
console.log(mapIter2.next().value); // foo

/**
 * @@iterator 屬性的初始值與 entries 屬性的初始值是同一個函數物件。
 */
for (const entry of myMap) {
  console.log(entry);
}
// [ 'bar', 'baz' ]
// [ 1, 'foo' ]

/**
 * delete() 方法用於移除 Map 物件中指定的元素。
 */
myMap.delete('bar');
console.log(myMap); // Map { 1 => 'foo' }

/**
 * clear()方法會移除Map物件中的所有元素。
 */
myMap.clear();

console.log(myMap); // Map { }

Symbol

symbol 是一種基本數據型別 (primitive data type)。Symbol()函數會返回 symbol 型別的值,該型別具有靜態屬性和靜態方法。它的靜態屬性會暴露幾個內建的成員物件;它的靜態方法會暴露全域性的 symbol 註冊,且類似於內建物件類,但作爲建構函式來說它並不完整,因爲它不支援語法:「new Symbol()」。

每個從 Symbol()返回的 symbol 值都是唯一的。一個 symbol 值能作爲物件屬性的識別符號;這是該數據型別僅有的目的。

描述

直接使用 Symbol()建立新的 symbol 型別,並用一個可選的字串作爲其描述。

const sym1 = Symbol();
const sym2 = Symbol('foo');
const sym3 = Symbol('foo');

上面的程式碼建立了三個新的 symbol 型別。 注意,Symbol(「foo」) 不會強制將字串 「foo」 轉換成 symbol 型別。它每次都會建立一個新的 symbol 型別:

console.log(Symbol('foo') === Symbol('foo')); // false

下面 下麪帶有 new 運算子的語法將拋出 TypeError 錯誤:

const sym = new Symbol(); // TypeError

這會阻止建立一個顯式的 Symbol 包裝器物件而不是一個 Symbol 值。圍繞原始數據型別建立一個顯式包裝器物件從 ECMAScript 6 開始不再被支援。 然而,現有的原始包裝器物件,如 new Boolean、new String 以及 new Number,因爲遺留原因仍可被建立。

如果你真的想建立一個 Symbol 包裝器物件 (Symbol wrapper object),你可以使用 Object() 函數:

const sym = Symbol('foo');
console.log(typeof sym); // "symbol"
const symObj = Object(sym);
console.log(typeof symObj); // "object"

全域性共用的 Symbol

上面使用 Symbol() 函數的語法,不會在你的整個程式碼庫中建立一個可用的全域性 symbol 型別。 要建立跨檔案可用的 symbol,甚至跨域(每個都有它自己的全域性作用域) , 使用 Symbol.for() 方法和 Symbol.keyFor() 方法從全域性的 symbol 註冊表設定和取得 symbol。

在物件中查詢 Symbol 屬性

Object.getOwnPropertySymbols() 方法讓你在查詢一個給定物件的符號屬性時返回一個 symbol 型別的陣列。注意,每個初始化的物件都是沒有自己的 symbol 屬性的,因此這個陣列可能爲空,除非你已經在物件上設定了 symbol 屬性。

方法

/**
 * Symbol.for(key) 方法會根據給定的鍵 key,來從執行時的 symbol 註冊表中找到對應的 symbol,如果找到了,則返回它,否則,新建一個與該鍵關聯的 symbol,並放入全域性 symbol 註冊表中。
 */
Symbol('foo'); // 建立一個 symbol 並放入 symbol 註冊表中,鍵爲 "foo"
console.log(Symbol.for('foo')); // 從 symbol 註冊表中讀取鍵爲"foo"的 symbol

console.log(Symbol.for('bar') === Symbol.for('bar')); // true,證明了上面說的
console.log(Symbol('bar') === Symbol('bar')); // false,Symbol() 函數每次都會返回新的一個 symbol

const sym = Symbol.for('mario');
console.log(sym.toString());
// "Symbol(mario)",mario 既是該 symbol 在 symbol 註冊表中的鍵名,又是該 symbol 自身的描述字串

/**
 * Symbol.keyFor(sym) 方法用來獲取 symbol 註冊表中與某個 symbol 關聯的鍵。
 */
// 建立一個 symbol 並放入 Symbol 註冊表,key 爲 "foo"
const globalSym = Symbol.for('foo');
console.log(Symbol.keyFor(globalSym)); // foo

// 建立一個 symbol,但不放入 symbol 註冊表中
const localSym = Symbol();
console.log(Symbol.keyFor(localSym)); // undefined,所以是找不到 key 的

// well-known symbol 們並不在 symbol 註冊表中
console.log(Symbol.keyFor(Symbol.iterator)); // undefined

demo

對 symbol 使用 typeof 運算子

console.log(typeof Symbol() === 'symbol'); // true
console.log(typeof Symbol('foo') === 'symbol'); // true
console.log(typeof Symbol.iterator === 'symbol'); // true

Symbol 型別轉換

當使用 symbol 值進行型別轉換時需要注意一些事情:

  • 嘗試將一個 symbol 值轉換爲一個 number 值時,會拋出一個 TypeError 錯誤 (e.g. +sym or sym | 0).
  • 使用寬鬆相等時, Object(sym) == sym returns true.
  • 這會阻止你從一個 symbol 值隱式地建立一個新的 string 型別的屬性名。例如,Symbol(「foo」) + 「bar」 將拋出一個 TypeError (can’t convert symbol to string).
  • 「safer」 String(sym) conversion 的作用會像 symbol 型別呼叫 Symbol.prototype.toString() 一樣,但是注意 new String(sym) 將拋出異常。

Symbols 與 for…in 迭代

Symbols 在 for…in 迭代中不可列舉。另外,Object.getOwnPropertyNames() 不會返回 symbol 物件的屬性,但是你能使用 Object.getOwnPropertySymbols() 得到它們。

const obj = {};

obj[Symbol('a')] = 'a';
obj[Symbol.for('b')] = 'b';
obj['c'] = 'c';
obj.d = 'd';

for (const i in obj) {
  console.log(i);
}
// c
// d

Symbols 與 JSON.stringify()

當使用 JSON.stringify() 時,以 symbol 值作爲鍵的屬性會被完全忽略:

console.log(JSON.stringify({ [Symbol('foo')]: 'foo' }));
// '{}'

Symbol 包裝器物件作爲屬性的鍵

當一個 Symbol 包裝器物件作爲一個屬性的鍵時,這個物件將被強制轉換爲它包裝過的 symbol 值:

const sym1 = Symbol('foo');
const obj1 = { [sym1]: 1 };
console.log(obj1[sym1]); // 1
console.log(obj1[Object(sym1)]); // 1

Iterator(迭代協定)

作爲 ECMAScript 2015 的一組補充規範,迭代協定並不是新的內建實現或語法,而是協定。這些協定可以被任何遵循某些約定的物件來實現。

迭代協定具體分爲兩個協定:可迭代協定和迭代器協定。

可迭代協定

可迭代協定允許 JavaScript 物件定義或定製它們的迭代行爲,例如,在一個 for…of 結構中,哪些值可以被遍歷到。一些內建型別同時是內建可迭代物件,並且有預設的迭代行爲,比如 Array 或者 Map,而其他內建型別則不是(比如 Object))。

要成爲可迭代物件, 一個物件必須實現 @@iterator 方法。這意味着物件(或者它原型鏈上的某個物件)必須有一個鍵爲 @@iterator 的屬性,可通過常數 Symbol.iterator 存取該屬性:

屬性
[Symbol.iterator] 返回一個符合迭代器協定的物件的無參數函數。

當一個物件需要被迭代的時候(比如被置入一個 for…of 回圈時),首先,會不帶參數呼叫它的 @@iterator 方法,然後使用此方法返回的迭代器獲得要迭代的值。

值得注意的是呼叫此零個參數函數時,它將作爲對可迭代物件的方法進行呼叫。 因此,在函數內部,this 關鍵字可用於存取可迭代物件的屬性,以決定在迭代過程中提供什麼。

此函數可以是普通函數,也可以是生成器函數,以便在呼叫時返回迭代器物件。 在此生成器函數的內部,可以使用 yield 提供每個條目。


迭代器協定

迭代器協定定義了產生一系列值(無論是有限個還是無限個)的標準方式。當值爲有限個時,所有的值都被迭代完畢後,則會返回一個預設返回值。

只有實現了一個擁有以下語意(semantic)的 next() 方法,一個物件才能 纔能成爲迭代器:

屬性
next 一個無參數函數,返回一個應當擁有以下兩個屬性的物件:

done(boolean)
如果迭代器可以產生序列中的下一個值,則爲 false。(這等價於沒有指定 done 這個屬性。)

如果迭代器已將序列迭代完畢,則爲 true。這種情況下,value 是可選的,如果它依然存在,即爲迭代結束之後的預設返回值。

value
迭代器返回的任何 JavaScript 值。done 爲 true 時可省略。
next() 方法必須返回一個物件,該物件應當有兩個屬性: done 和 value,如果返回了一個非物件值(比如 false 或 undefined),則會拋出一個 TypeError 異常(「iterator.next() returned a non-object value」)。

demo

// 簡單迭代器
const makeIterator = (array) => {
  this.index = 0;
  return {
    next: () => {
      return {
        value: array[this.index++],
        done: this.index > array.length,
      };
    },
  };
};
let mIterator = makeIterator([1, 2]);
console.log(mIterator.next());
console.log(mIterator.next());
console.log(mIterator.next());
// { value: 1, done: false }
// { value: 2, done: false }
// { value: undefined, done: true }

// 物件迭代器
const obj = {
  names: ['tom', 'jack', 'jeer'],
  age: [22, 11, 33],
  [Symbol.iterator]() {
    let array = [...this.names, ...this.age];
    return {
      index: 0,
      next() {
        return {
          value: array[this.index++],
          done: this.index > array.length,
        };
      },
    };
  },
};
for (let item of obj) {
  console.log(item);
}
// tom
// jack
// jeer
// 22
// 11
// 33