閱讀《深入理解ES6》書籍,筆記整理(上)

汪圖南發表於2020-01-08

由於全部筆記有接近4W的字數,因此分開為上、下兩部分,第二部分內容計劃於明後兩天更新。
如果你覺得寫的不錯請給一個star,如果你想閱讀上、下兩部分全部的筆記,請點選閱讀全文

閱讀《深入理解ES6》書籍,筆記整理(上)
閱讀《深入理解ES6》書籍,筆記整理(下)

前言

ECMAScript6標準定稿之前,已經開始出現了一些實驗性的轉譯器(Transpiler),例如谷歌的Traceur,可以將程式碼從ECMAScript6轉換成ECMAScript5。但它們大多功能非常有限,或難以插入現有的JavaScript構建管道。
但是,隨後出現了的新型轉譯器6to5改變了這一切。它易於安裝,可以很好的整合現有的工具中,生成的程式碼可讀,於是就像野火一樣逐步蔓延開來,6to5也就是現在鼎鼎大名的Babel

ECMAScript6的演變之路

JavaScript核心的語言特性是在標準ECMA-262中被定義,該標準中定義的語言被稱作ECMAScript,它是JavaScript的子集。

演變之路:

  • 停滯不前:逐漸興起的Ajax開創了動態Web引用的新時代,而自1999年第三版ECMA-262釋出以來,JavaScript卻沒有絲毫的改變。
  • 轉折點:2007年,TC-39委員會將大量規範草案整合在了ECMAScript4中,其中新增的語言特性涉足甚廣,包括:模組、類、類繼承、私有物件成員等眾多其它的特性。
  • 分歧:然而TC-39組織內部對ECMAScript4草案產生了巨大的分歧,部分成員認為不應該一次性在第四版標準中加入過多的新功能,而來自雅虎、谷歌和微軟的技術負責人則共同提交了一份ECMAScript3.1草案作為下一代ECMAScript的可選方案,其中此方案只是對現有標準進行小幅度的增量修改。行為更專注於優化屬性特性、支援原生JSON以及為已有物件增加新的方法。
  • 從未面世的ECMAScript4:2008年,JavaScript創始人Brendan Eich宣佈TC-39委員一方面會將合理推進ECMAScript3.1的標準化工作,另一方面會暫時將ECMAScript4標準中提出的大部分針對語法及特性的改動擱置。
  • ECMAScript5:經過標準化的ECMAScript3.1最終作為ECMA-262第五版於2009年正式釋出,同時被命名為ECMAScript5
  • ECMAScript6:在ECMAScript5釋出後,TC-39委員會於2013年凍結了ECMAScript6的草案,不再新增新的功能。2013年ECMAScript6草案發布,在進過12個月各方討論和反饋後。2015年ECMAScript6正式釋出,並命名為ECMAScript 2015

塊級作用域繫結

過去JavaScript中的變數宣告機制一直令我們感到困惑,大多數類C語言在宣告變數的同時也會建立變數,而在以前的JavaScript中,何時建立變數要看如何宣告的變數,ES6引入塊級作用域可以讓我們更好的控制作用域。

var宣告和變數提升機制

問:提升機制(hoisting)是什麼?
答:在函式作用域或全域性作用域中通過關鍵字var宣告的變數,無論實際上是在哪裡宣告的,都會被當成在當前作用域頂部宣告的變數,這就是我們常說的提升機制。
以下例項程式碼說明了這種提升機制:

function getValue (condition) {
  if (condition) {
    var value = 'value'
    return value
  } else {
    // 這裡可以訪問到value,只不過值為undefined
    console.log(value)
    return null
  }
}
getValue(false) // 輸出undefined
複製程式碼

你可以在以上程式碼中看到,當我們傳遞了false的值,但依然可以訪問到value這個變數,這是因為在預編譯階段,JavaScript引擎會將上面函式的程式碼修改成如下形式:

function getValue (condition) {
  var value
  if (condition) {
    value = 'value'
    return value
  } else {
    console.log(value)
    return null
  }
}
複製程式碼

經過以上示例,我們可以發現:變數value的宣告被提升至函式作用域的頂部,而初始化操作依舊留在原處執行,正因為value變數只是宣告瞭而沒有賦值,因此以上程式碼才會列印出undefined

塊級宣告

塊級宣告用於宣告在指定的作用於之外無妨訪問的變數,塊級作用域存在於:函式內部和塊中。

let宣告:

  • let宣告和var宣告的用法基本相同。
  • let宣告的變數不會被提升。
  • let不能在同一個作用域中重複宣告已經存在的變數,會報錯。
  • let宣告的變數作用域範圍僅存在於當前的塊中,程式進入塊開始時被建立,程式退出塊時被銷燬。

根據let宣告的規則,改動上面的程式碼後像下面這樣:

function getValue (condition) {
  if (condition) {
    // 變數value只存在於這個塊中。
    let value = 'value'
    return value
  } else {
    // 訪問不到value變數
    console.log(value)
    return null
  }
}
複製程式碼

const宣告:const宣告和let宣告大多數情況是相同的,唯一的本質區別在於,const是用來宣告常量的,其宣告後的變數值不能再被修改,即意味著:const宣告必須進行初始化。

const MAX_ITEMS = 30
// 報錯
MAX_ITEMS = 50
複製程式碼

我們說的const變數值不可變,需要分兩種型別來說: 值型別:變數的值不能改變。 引用型別:變數的地址不能改變,值可以改變。

const num = 23
const arr = [1, 2, 3, 4]
const obj = {
  name: 'why',
  age: 23
}

// 報錯
num = 25

// 不報錯
arr[0] = 11
obj.age = 32
console.log(arr) // [11, 2, 3, 4]
console.log(obj) // { name: 'why', age: 32 }

// 報錯
arr = [4, 3, 2, 1]
複製程式碼

暫時性死區

因為letconst宣告的變數不會進行宣告提升,所以在letconst變數宣告之前任何訪問(即使是typeof也不行)此變數的操作都會引發錯誤:

if (condition) {
  // 報錯
  console.log(typeof value)
  let value = 'value'
}
複製程式碼

問:為什麼會報錯?
答:JavaScript引擎在掃描程式碼發現變數宣告時,要麼將它們提升至作用域的頂部(var宣告),要麼將宣告放在TDZ(暫時性死區)中(letconst宣告)。訪問TDZ中的變數會觸發錯誤,只有執行變數宣告語句之後,變數才會從TDZ中移出,隨後才能正常訪問。

全域性塊作用域繫結

我們都知道:如果我們在全域性作用域下通過var宣告一個變數,那麼這個變數會掛載到全域性物件window上:

var name = 'why'
console.log(window.name) // why
複製程式碼

但如果我們使用let或者const在全域性作用域下建立一個新的變數,這個變數不會新增到window上。

const name = 'why'
console.log('name' in window) // false
複製程式碼

塊級繫結最佳實踐的進化

ES6早期,人們普遍認為應該預設使用let來代替var,這是因為對於開發者而言,let實際上與他們想要的var一樣,直接替換符合邏輯。但隨著時代的發展,另一種做法也越來越普及:預設使用const,只有確定變數的值會在後續需要修改時才會使用let宣告,因為大部分變數在初始化後不應再改變,而預料以外的變數值改變是很多bug的源頭。

字串

本章節中關於unicode和正則部分未整理。

模組字面量

模板字面量是擴充套件ECMAScript基礎語法的語法糖,其提供了一套生成、查詢並操作來自其他語言裡內容的DSL,且可以免受XSS注入攻擊和SQL注入等等。

ES6之前,JavaScript一直以來缺少許多特性:

  • 多行字串:一個正式的多行字串的概念。
  • 基本的字串格式化:將變數的值嵌入字串的能力。
  • HTML轉義:向HTML插入經過安全轉換後的字串的能力。

而在ECMAScript 6中,通過模板字面量的方式對以上問題進行了填補,一個最簡單的模板字面量的用法如下:

const message = `hello,world!`
console.log(message)        // hello,world!
console.log(typeof message) // string
複製程式碼

一個需要注意的地方就是,如果我們需要在字串中使用反撇號,需要使用\來進行轉義,如下:

const message = `\`hello\`,world!`
console.log(message) // `hello`,world!
複製程式碼

多行字串

JavaScript誕生起,開發者們就一直在嘗試和建立多行字串,以下是ES6之前的方法:
在字串的新行最前方加上\可以承接上一行程式碼,可以利用這個小bug來建立多行字串。

const message = 'hello\
,world!'
console.log(message) // hello,world
複製程式碼

ES6之後,我們可以使用模板字面量,在裡面直接換行就可以建立多行字串,如下:

在模板字面量中,即反撇號中所有空白字元都屬於字串的一部分。

const message = `hello
,world!`
console.log(message) // hello
                     // ,world!
複製程式碼

字串佔位符

模板字面量於普通字串最大的區別是模板字串中的佔位符功能,其中佔位符中的內容,可以是任意合法的JavaScript表示式,例如:變數,運算式,函式呼叫,甚至是另外一個模板字面量。

const age = 23
const name = 'why'
const message = `Hello ${name}, you are ${age} years old!`
console.log(message) // Hello why, you are 23 years old!
複製程式碼

模板字面量巢狀:

const name = 'why'
const message = `Hello, ${`my name is ${name}`}.`
console.log(message) // Hello, my name is why.
複製程式碼

標籤模板

標籤指的是在模板字面量第一個反撇號前方標註的字串,每一個模板標籤都可以執行模板字面量上的轉換並返回最終的字串值。

// tag就是`Hello world!`模板字面量的標籤模板
const message = tag`Hello world!`
複製程式碼

標籤可以是一個函式,標籤函式通常使用不定引數特性來定義佔位符,從而簡化資料處理的過程,就像下面這樣:

function tag(literals, ...substitutions) {
  // 返回一個字串
}
const name = 'why'
const age = 23
const message = tag`${name} is ${age} years old!`
複製程式碼

其中literals是一個陣列,它包含:

  • 第一個佔位符前的空白字串:""。
  • 第一個、第二個佔位符之間的字串:" is "。
  • 第二個佔位符後的字串:" years old!"

substitutions也是一個陣列:

  • 陣列第一項為:name的值,即:why
  • 陣列第二項為:age的值,即:23

通過以上規律我們可以發現:

  • literals[0]始終代表字串的開頭。
  • literals總比substitutions多一個。

我們可以通過以上這種模式,將literalssubstitutions這兩個陣列交織在一起重新組成一個字串,來模擬模板字面量的預設行為,像下面這樣:

function tag(literals, ...substitutions) {
  let result = ''
  for (let i = 0; i< substitutions.length; i++) {
    result += literals[i]
    result += substitutions[i]
  }

  // 合併最後一個
  result += literals[literals.length - 1]
  return result
}
const name = 'why'
const age = 23
const message = tag`${name} is ${age} years old!`
console.log(message) // why is 23 years old!
複製程式碼

原生字串資訊

通過模板標籤可以訪問到字串轉義被轉換成等價字串前的原生字串。

const message1 = `Hello\nworld`
const message2 = String.raw`Hello\nworld`
console.log(message1) // Hello
                      // world
console.log(message2) // Hello\nworld
複製程式碼

函式

形參預設值

ES6之前,你可能會通過以下這種模式建立函式併為引數提供預設值:

function makeRequest (url, timeout, callback) {
  timeout = timeout || 2000
  callback = callback || function () {}
}
複製程式碼

程式碼分析:在以上示例中,timeoutcallback是可選引數,如果不傳入則會使用邏輯或操作符賦予預設值。然而這種方式也有一定的缺陷,如果我們想給timeout傳遞值為0,雖然這個值是合法的,但因為有或邏輯運算子的存在,最終還是為timeout賦值2000

針對以上情況,我們應該通過一種更安全的做法(使用typeof)來重寫一下以上示例:

function makeRequest (url, timeout, callback) {
  timeout = typeof timeout !== 'undefined' ? timeout : 2000
  callback = typeof callback !== 'undefined' ? callback : function () {} 
}
複製程式碼

程式碼分析:儘管以上方法更安全一些,但我們任然需要額外的撰寫更多的程式碼來實現這種非常基礎的操作。針對以上問題,ES6簡化了為形參提供預設值的過程,就像下面這樣:

對於預設引數而言,除非不傳或者主動傳遞undefined才會使用引數預設值(如果傳遞null,這是一個合法的引數,不會使用預設值)。

function makeRequest (url, timeout = 2000, callback = function () {}) {
  // todo
}
// 同時使用timeout和callback預設值
makeRequest('https://www.taobao.com')
// 使用callback預設值
makeRequest('https://www.taobao.com', 500)
// 不使用預設值
makeRequest('https://www.taobao.com', 500, function (res) => {
  console.log(res)
})
複製程式碼

形參預設值對arguments物件的影響

ES5非嚴格模式下,如果修改引數的值,這些引數的值會同步反應到arguments物件中,如下:

function mixArgs(first, second) {
  console.log(arguments[0]) // A
  console.log(arguments[1]) // B
  first = 'a'
  second = 'b'
  console.log(arguments[0]) // a
  console.log(arguments[1]) // b
}
mixArgs('A', 'B')
複製程式碼

而在ES5嚴格模式下,修改引數的值不再反應到arguments物件中,如下:

function mixArgs(first, second) {
  'use strict'
  console.log(arguments[0]) // A
  console.log(arguments[1]) // B
  first = 'a'
  second = 'b'
  console.log(arguments[0]) // A
  console.log(arguments[1]) // B
}
mixArgs('A', 'B')
複製程式碼

對於使用了ES6的形參預設值,arguments物件的行為始終保持和ES5嚴格模式一樣,無論當前是否為嚴格模式,即:arguments總是等於最初傳遞的值,不會隨著引數的改變而改變,總是可以使用arguments物件將引數還原為最初的值,如下:

function mixArgs(first, second = 'B') {
  console.log(arguments.length) // 1
  console.log(arguments[0])      // A
  console.log(arguments[1])      // undefined
  first = 'a'
  second = 'b'
  console.log(arguments[0])      // A
  console.log(arguments[1])      // undefined
}
// arguments物件始終等於傳遞的值,形參預設值不會反映在arguments上
mixArgs('A')
複製程式碼

預設參數列達式

函式形參預設值,除了可以是原始值的預設值,也可以是表示式,即:變數,函式呼叫也是合法的。

function getValue () {
  return 5
}
function add (first, second = getValue()) {
  return first + second
}
console.log(add(1, 1)) // 2
console.log(add(1))    // 6
複製程式碼

程式碼分析:當我們第一次呼叫add(1,1)函式時,由於未使用引數預設值,所以getValue並不會呼叫。只有當我們使用了second引數預設值的時候add(1)getValue函式才會被呼叫。

正因為預設引數是在函式呼叫時求值,所以我們可以在後定義的參數列達式中使用先定義的引數,即可以把先定義的引數當做變數或者函式呼叫的引數,如下:

function getValue(value) {
  return value + 5
}
function add (first, second = first + 1) {
  return first + second
}
function reduce (first, second = getValue(first)) {
  return first - second
}
console.log(add(1))     // 3
console.log(reduce(1))  // -5
複製程式碼

預設引數的暫時性死區

在前面已經提到過letconst存在暫時性死區,即:在letconst變數宣告之前嘗試訪問該變數會觸發錯誤。相同的道理,在函式預設引數中也存在暫時性死區,如下:

function add (first = second, second) {
  return first + second
}
add(1, 1)         // 2
add(undefined, 1) // 丟擲錯誤
複製程式碼

程式碼分析:在第一次呼叫add(1,1)時,我們傳遞了兩個引數,則add函式不會使用引數預設值;在第二次呼叫add(undefined, 1)時,我們給first引數傳遞了undefined,則first引數使用引數預設值,而此時second變數還沒有初始化,所以被丟擲錯誤。

不定引數

JavaScript的函式語法規定:無論函式已定義的命名引數有多少個,都不限制呼叫時傳入的實際引數的數量。在ES6中,當傳入更少的引數時,使用引數預設值來處理;當傳入更多數量的引數時,使用不定引數來處理。

我們以underscore.js庫中的pick方法為例:

pick方法的用法是:給定一個物件,返回指定屬性的物件的副本。

function pick(object) {
  let result = Object.create(null)
  for (let i = 1, len = arguments.length; i < len; i++) {
    let item = arguments[i]
    result[item] = object[item]
  }
  return result
}
const book = {
  title: '深入理解ES6',
  author: '尼古拉斯',
  year: 2016
}
console.log(pick(book, 'title', 'author')) // { title: '深入理解ES6', author: '尼古拉斯' }
複製程式碼

程式碼分析:

  • 不容易發現這個函式可以接受任意數量的引數。
  • 當需要查詢待拷貝的屬性的時候,不得不從索引1開始。

ES6中提供了不定引數,我們可以使用不定引數的特性來重寫pick函式:

function pick(object, ...keys) {
  let result = Object.create(null)
  for (let i = 0, len = keys.length; i < len; i++) {
    let item = keys[i]
    result[item] = object[item]
  }
  return result
}
const book = {
  title: '深入理解ES6',
  author: '尼古拉斯',
  year: 2016
}
console.log(pick(book, 'title', 'author')) // { title: '深入理解ES6', author: '尼古拉斯' }
複製程式碼

不定引數的限制

不定引數在使用的過程中有幾點限制:

  • 一個函式最多隻能有一個不定引數。
  • 不定引數一定要放在所有引數的最後一個。
  • 不能在物件字面量setter之中使用不定引數。
// 報錯,只能有一個不定引數
function add(first, ...rest1, ...rest2) {
  console.log(arguments)
}
// 報錯,不定引數只能放在最後一個引數
function add(first, ...rest, three) {
  console.log(arguments)
}
// 報錯,不定引數不能用在物件字面量`setter`之中
const object = {
  set name (...val) {
    console.log(val)
  }
}
複製程式碼

展開運算子

ES6的新功能中,展開運算子和不定引數是最為相似的,不定引數可以讓我們指定多個各自獨立的引數,並通過整合後的陣列來訪問;而展開運算子可以讓你指定一個陣列,將它們打散後作為各自獨立的引數傳入函式。
ES6之前,我們如果使用Math.max函式比較一個陣列中的最大值,則需要像下面這樣使用:

const arr = [4, 10, 5, 6, 32]
console.log(Math.max.apply(Math, arr)) // 32
複製程式碼

程式碼分析:在ES6之前使用這種方式是沒有任何問題的,但關鍵的地方在於我們要借用apply方法,而且要特別小心的處理this(第一個引數),在ES6中我們有更加簡單的方式來達到以上的目的:

const arr = [4, 10, 5, 6, 32]
console.log(Math.max(...arr)) // 32
複製程式碼

函式name屬性

問:為什麼ES6會引入函式的name屬性。
答:在JavaScript中有多重定義函式的方式,因而辨別函式就是一項具有挑戰性的任務,此外匿名函式表示式的廣泛使用也加大了除錯的難度,為了解決這些問題,在ESCAScript 6中為所有函式新增了name屬性。

常規name屬性

在函式宣告和匿名函式表示式中,函式的name屬性相對來說是固定的:

function doSomething () {
  console.log('do something')
}
let doAnotherThing = function () {
  console.log('do another thing')
}
console.log(doSomething.name)    // doSomething
console.log(doAnotherThing.name) // doAnotherThing
複製程式碼

name屬性的特殊情況

儘管確定函式宣告和函式表示式的名稱很容易,但還是有一些其他情況不是特別容易識別:

  • 匿名函式表示式顯示提供函式名的情況:函式名稱本身比函式本身被賦值的變數的權重高。
  • 物件字面量:在不提供函式名稱的情況下,取物件字面量的名稱;提供函式名稱的情況下就是提供的名稱
  • 屬性的gettersetter:在物件上存在get + 屬性get或者set方法。
  • 通過bind:通過bind函式建立的函式,name為會帶有bound字首
  • 通過建構函式:函式名稱固定為anonymous
let doSomething = function doSomethingElse () {
  console.log('do something else')
}
let person = {
  // person物件上存在name為get firstName的方法
  get firstName () {
    return 'why'
  },
  sayName: function () {
    console.log('why')
  },
  sayAge: function sayNewAge () {
    console.log(23)
  }
}
console.log(doSomething.name)         // doSomethingElse
console.log(person.sayName.name)      // sayName
console.log(person.sayAge.name)       // sayNewAge
console.log(doSomething.bind().name)  // bound doSomethingElse
console.log(new Function().name)      // anonymous
複製程式碼

函式的多種用途

JavaScript中函式具有多重功能,可以結合new使用,函式內的this值指向一個新物件,函式最終會返回這個新物件,如下:

function Person (name) {
  this.name = name
}
const person = new Person('why')
console.log(person.toString()) // [object Object]
複製程式碼

ES6中,函式有兩個不同的內部方法,分別是:

具有[[Construct]]方法的函式被稱為建構函式,但並不是所有的函式都有[[Construct]]方法,例如:箭頭函式。

  • [[Call]]:如果不通過new關鍵字進行呼叫函式,則執行[[Call]]函式,從而直接執行程式碼中的函式體。
  • [[Construct]]:當通過new關鍵字呼叫函式時,執行的是[[Construct]]函式,它負責建立一個新物件,然後再執行函式體,將this繫結到例項上。

ES6之前,如果要判斷一個函式是否通過new關鍵詞呼叫,最流行的方法是使用instanceof來判斷,例如:

function Person (name) {
  if (this instanceof Person) {
    this.name = name
  } else {
    throw new Error('必須通過new關鍵詞來呼叫Person')
  }
}
const person = new Person('why')
const notPerson = Person('why') // 丟擲錯誤
複製程式碼

程式碼分析:這段程式碼中,首先會判斷this的值,看是否是Person的例項,如果是則繼續執行,如果不是則丟擲錯誤。通常來說這種做法是正確的,但是也不是十分靠譜,有一種方式可以不依賴new關鍵詞也可以把this繫結到Person的例項上,如下:

function Person (name) {
  if (this instanceof Person) {
    this.name = name
  } else {
    throw new Error('必須通過new關鍵詞來呼叫Person')
  }
}
const person = new Person('why')
const notPerson = Person.call(person, 'why') // 不報錯,有效
複製程式碼

為了解決判斷函式是否通過new關鍵詞呼叫的問題,ES6引入了new.target這個元屬性
問:什麼是元屬性?
答:元屬性是指非物件的屬性,其可以提供非物件目標的補充資訊。當呼叫函式的[[Construct]]方法時,new.target被賦值為new操作符的目標,通常是新建立物件的例項,也就是函式體內this的建構函式;如果呼叫[[Call]]方法,則new.target的值為undefined

根據以上new.target的特點,我們改寫一下上面的程式碼:

在函式外使用new.target是一個語法錯誤。

function Person (name) {
  if (typeof new.target !== 'undefined') {
    this.name = name
  } else {
    throw new Error('必須通過new關鍵詞來呼叫Person')
  }
}
const person = new Person('why')
const notPerson = Person.call(person, 'why') // 丟擲錯誤
複製程式碼

塊級函式

ECMAScript 3和早期版本中,在程式碼塊中宣告一個塊級函式嚴格來說是一個語法錯誤,但是所有的瀏覽器任然支援這個特性,卻又因為瀏覽器的差異導致支撐程度稍有不同,所以最好不要使用這個特性,如果要用可以使用匿名函式表示式

// ES5嚴格模式下,在程式碼塊中宣告一個函式會報錯
// 在ES6下,因為有了塊級作用域的概念,所以無論是否處於嚴格模式,都不會報錯。
// 但在ES6中,當處於嚴格模式時,會將函式宣告提升至當前塊級作用域的頂部
// 當處於非嚴格模式時,提升至外層作用域
'use strict'
if (true) {
  function doSomething () {
    console.log('do something')
  }
}
複製程式碼

箭頭函式

ES6中,箭頭函式是其中最有趣的新增特性之一,箭頭函式是一種使用箭頭=>定義函式的新語法,但它和傳統的JavaScript函式有些許不同:

  • 沒有this、super、arguments和new.target繫結:箭頭函式中的thissuperargumentsnew.target這些值由外圍最近一層非箭頭函式所決定。
  • 不能通過new關鍵詞呼叫:因為箭頭函式沒有[[Construct]]函式,所以不能通過new關鍵詞進行呼叫,如果使用new進行呼叫會丟擲錯誤。
  • 沒有原型:因為不會通過new關鍵詞進行呼叫,所以沒有構建原型的需要,也就沒有了prototype這個屬性。
  • 不可以改變this的繫結:在箭頭函式的內部,this的值不可改變(即不能通過callapply或者bind等方法來改變)。
  • 不支援argument物件:箭頭函式沒有arguments繫結,所以必須使用命名引數或者不定引數這兩種形式訪問引數。
  • 不支援重複的命名引數:無論是否處於嚴格模式,箭頭函式都不支援重複的命名引數。

箭頭函式的語法

箭頭函式的語法多變,根據實際的使用場景有多種形式。所有變種都由函式引數、箭頭和函式體組成。

表現形式之一:

// 表現形式之一:沒有引數
let reflect = () => 5
// 相當於
let reflect = function () {
  return 5
}
複製程式碼

表現形式之二:

// 表現形式之二:返回單一值
let reflect = value => value
// 相當於
let reflect = function (value) {
  return value
}
複製程式碼

表現形式之三:

// 表現形式之三:多個引數
let reflect = (val1, val2) => val1 + val2
// 或者
let reflect = (val, val2) => {
  return val1 + val2
}
// 相當於
let reflect = function (val1, val2) {
  return val1 + val2
}
複製程式碼

表現形式之四:

// 表現形式之四:返回字面量
let reflect = (id) => ({ id: id, name: 'why' })
// 相當於
let reflect = function (id) {
  return {
    id: id,
    name: 'why'
  }
}
複製程式碼

箭頭函式和陣列

箭頭函式的語法簡潔,非常適用於處理陣列。

const arr = [1, 5, 3, 2]
// 非箭頭函式排序寫法
arr.sort(function(a, b) {
  return a -b
})
// 箭頭函式排序寫法
arr.sort((a, b) => a - b)
複製程式碼

尾呼叫優化

尾呼叫指的是函式作為另一個函式的最後一條語句被呼叫。

尾呼叫示例:

function doSomethingElse () {
  console.log('do something else')
}
function doSomething () {
  return doSomethingElse()
}
複製程式碼

ECMAScript 5的引擎中,尾呼叫的實現與其他函式呼叫的實現類似:建立一個新的棧幀,將其推入呼叫棧來表示函式呼叫,即意味著:在迴圈呼叫中,每一個未使用完的棧幀都會被儲存在記憶體中,當呼叫棧變得過大時會造成程式問題。

針對以上可能會出現的問題,ES6縮減了嚴格模式下尾呼叫棧的大小,當全部滿足以下條件,尾呼叫不再建立新的棧幀,而是清除並重用當前棧幀:

  • 尾呼叫不訪問當前棧幀的變數(函式不是一個閉包。)
  • 尾呼叫是最後一條語句
  • 尾呼叫的結果作為函式返回

滿足以上條件的一個尾呼叫示例:

'use strict'
function doSomethingElse () {
  console.log('do something else')
}
function doSomething () {
  return doSomethingElse()
}
複製程式碼

不滿足以上條件的尾呼叫示例:

function doSomethingElse () {
  console.log('do something else')
}
function doSomething () {
  // 無法優化,沒有返回
  doSomethingElse()
}
function doSomething () {
  // 無法優化,返回值又新增了其它操作
  return 1 + doSomethingElse()
}
function doSomething () {
  // 可能無法優化
  let result = doSomethingElse
  return result
}
function doSomething () {
  let number = 1
  let func = () => number
  // 無法優化,該函式是一個閉包
  return func()
}
複製程式碼

遞迴函式是其最主要的應用場景,當遞迴函式的計算量足夠大,尾呼叫優化可以大幅提升程式的效能。

// 優化前
function factorial (n) {
  if (n <= 1) {
    return 1
  } else {
    // 無法優化
    return n * factorial (n - 1)
  }
}

// 優化後
function factorial (n, p = 1) {
  if (n <= 1) {
    return 1 * p
  } else {
    let result = n * p
    return factorial(n -1, result)
  }
}
複製程式碼

物件的擴充套件

物件字面量的擴充套件

物件字面量擴充套件包含兩部分:

  • 屬性初始值的簡寫:當物件的屬性和本地變數同名時,不必再寫冒號和值,簡單的只寫屬性即可。
  • 物件方法的簡寫: 消除了冒號和function關鍵字。
  • 可計算屬性名:在定義物件時,物件的屬性值可通過變數來計算。

通過物件方法簡寫語法建立的方法有一個name屬性,其值為小括號前的名稱。

const name = 'why'
const firstName = 'first name'
const person = {
  name,
  [firstName]: 'ABC',
  sayName () {
    console.log(this.name)
  }
  
}
// 相當於
const name = 'why'
const person = {
  name: name,
  'first name': 'ABC',
  sayName: function () {
    console.log(this.name)
  }
}
複製程式碼

新增方法

Object.is

在使用JavaScript比較兩個值的時候,我們可能會習慣使用==或者===來進行判斷,使用全等===在比較時可以避免觸發強制型別轉換,所以深受許多人的喜愛。但全等===也並非是完全準確的,例如: +0===-0會返回trueNaN===NaN會返回false。針對以上情況,ES6引入了Object.is方法來彌補。

// ===和Object.is大多數情況下結果是相同的,只有極少數結果不同
console.log(+0 === -0)            // true
console.log(Object.is(+0, -0))    // false
console.log(NaN === NaN)          // false
console.log(Object.is(NaN, NaN))  // true
複製程式碼

Object.assign

問:什麼是Mixin
答:混合MixinJavaScript中實現物件組合最流行的一種模式。在一個mixin中,一個物件接受來自另一個物件的屬性和方法(mixin方法為淺拷貝)。

// mixin方法
function mixin(receiver, supplier) {
  Object.keys(supplier).forEach(function(key) {
    receiver[key] = supplier[key]
  })
  return receiver
}
const person1 = {
  age: 23,
  name: 'why'
}
const person2 = mixin({}, person1)
console.log(person2) // { age: 23, name: 'why' }
複製程式碼

由於這種混合模式非常流行,所以ES6引入了Object.assign方法來實現相同的功能,這個方法接受一個接受物件和任意數量的源物件,最終返回接受物件。

如果源物件中有同名的屬性,後面的源物件會覆蓋前面源物件中的同名屬性。

const person1 = {
  age: 23,
  name: 'why'
}
const person2 = {
  age: 32,
  address: '廣東廣州'
}
const person3 = Object.assign({}, person1, person2)
console.log(person3) // { age: 32, name: 'why', address: '廣東廣州' }
複製程式碼

Object.assign方法不能複製屬性的get和set。

let receiver = {}
let supplier = {
  get name () {
    return 'why'
  }
}
Object.assign(receiver, supplier)
const descriptor = Object.getOwnPropertyDescriptor(receiver, 'name')
console.log(descriptor.value) // why
console.log(descriptor.get)   // undefined
console.log(receiver)         // { name: 'why' }
複製程式碼

重複的物件字面量屬性

ECMAScript 5嚴格模式下,給一個物件新增重複的屬性會觸發錯誤:

'use strict'
const person = {
  name: 'AAA',
  name: 'BBB' // ES5環境觸發錯誤
}
複製程式碼

但在ECMAScript 6中,無論當前是否處於嚴格模式,新增重複的屬性都不會報錯,而是選取最後一個取值:

'use strict'
const person = {
  name: 'AAA',
  name: 'BBB' // ES6環境不報錯
}
console.log(person) // { name: 'BBB' }
複製程式碼

自有屬性列舉順序

ES5中未定義物件屬性的列舉順序,由瀏覽器廠商自行決定。而在ES6中嚴格規定了物件自有屬性被列舉時的返回順序。

規則

  • 所有數字鍵按升序排序。
  • 所有字元鍵按照它們被加入物件的順序排序。
  • 所有Symbol鍵按照它們被加入物件的順序排序。

根據以上規則,以下這些方法將受到影響:

  • Object.getOwnPropertyNames()
  • Reflect.keys()
  • Object.assign()

不確定的情況:

  • for-in迴圈依舊由廠商決定列舉順序。
  • Object.keys()JSON.stringify()也同for-in迴圈一樣由廠商決定列舉順序。
const obj = {
  a: 1,
  0: 1,
  c: 1,
  2: 1,
  b: 1,
  1: 1
}
obj.d = 1
console.log(Reflect.keys(obj).join('')) // 012acbd
複製程式碼

增強物件原型

ES5中,物件原型一旦例項化之後保持不變。而在ES6中新增了Object.setPrototypeOf()方法來改變這種情況。

const person = {
  sayHello () {
    return 'Hello'
  }
}
const dog = {
  sayHello () {
    return 'wang wang wang'
  }
}
let friend = Object.create(person)
console.log(friend.sayHello())                        // Hello
console.log(Object.getPrototypeOf(friend) === person) // true
Object.setPrototypeOf(friend, dog)
console.log(friend.sayHello())                        // wang wang wang
console.log(Object.getPrototypeOf(friend) === dog)    // true
複製程式碼

簡化原型訪問的Super引用

ES5中,如果我們想重寫物件例項的方法,又需要呼叫與它同名的原型方法,可以像下面這樣:

const person = {
  sayHello () {
    return 'Hello'
  }
}
const dog = {
  sayHello () {
    return 'wang wang wang'
  }
}
const friend = {
  sayHello () {
    return Object.getPrototypeOf(this).sayHello.call(this) + '!!!'
  }
}
Object.setPrototypeOf(friend, person)
console.log(friend.sayHello())                        // Hello!!!
console.log(Object.getPrototypeOf(friend) === person) // true
Object.setPrototypeOf(friend, dog)
console.log(friend.sayHello())                        // wang wang wang!!!
console.log(Object.getPrototypeOf(friend) === dog)    // true
複製程式碼

程式碼分析:要準確記住如何使用Object.getPrototypeOf()xx.call(this)方法來呼叫原型上的方法實在是有點複雜。而且存在多繼承的情況下,Object.getPrototypeOf()會出現問題。
根據以上問題,ES6引入了super關鍵字,其中super相當於指向物件原型的指標,所以以上程式碼可以修改如下:

super關鍵字只出現在物件簡寫方法裡,普通方法中使用會報錯。

const person = {
  sayHello () {
    return 'Hello'
  }
}
const dog = {
  sayHello () {
    return 'wang wang wang'
  }
}
const friend = {
  sayHello () {
    return super.sayHello.call(this) + '!!!'
  }
}
Object.setPrototypeOf(friend, person)
console.log(friend.sayHello())                        // Hello!!!
console.log(Object.getPrototypeOf(friend) === person) // true
Object.setPrototypeOf(friend, dog)
console.log(friend.sayHello())                        // wang wang wang!!!
console.log(Object.getPrototypeOf(friend) === dog)    // true
複製程式碼

正式的方法定義

ES6之前從未正式定義過"方法"的概念,方法僅僅是一個具有功能而非資料的物件屬性。而在ES6中正式將方法定義為一個函式,它會有一個內部[[HomeObject]]屬性來容納這個方法從屬的物件。

const person = {
  // 是方法 [[HomeObject]] = person
  sayHello () {
    return 'Hello'
  }
}
// 不是方法
function sayBye () {
  return 'goodbye'
}
複製程式碼

根據以上[[HomeObject]]的規則,我們可以得出super是如何工作的:

  • [[HomeObject]]屬性上呼叫Object.getPrototypeOf()方法來檢索原型的引用。
  • 搜尋原型找到同名函式。
  • 設定this繫結並且呼叫相應的方法。
const person = {
  sayHello () {
    return 'Hello'
  }
}
const friend = {
  sayHello () {
    return super.sayHello() + '!!!'
  }
}
Object.setPrototypeOf(friend, person)
console.log(friend.sayHello()) // Hello!!!
複製程式碼

程式碼分析:

  • friend.sayHello()方法的[[HomeObject]]屬性值為friend
  • friend的原型是person
  • super.sayHello()相當於person.sayHello.call(this)

解構

解構是一種打破資料結構,將其拆分為更小部分的過程。

為何使用解構功能

ECMAScript 5及其早期版本中,為了從物件或者陣列中獲取特定資料並賦值給變數,編寫了許多看起來同質化的程式碼:

const person = {
  name: 'AAA',
  age: 23
}
const name = person.name
const age = person.age
複製程式碼

程式碼分析:我們必須從person物件中提取nameage的值,並把其值賦值給對應的同名變數,過程極其相似。假設我們要提取許多變數,這種過程會重複更多次,如果其中還包含巢狀結構,只靠遍歷是找不到真實資訊的。

針對以上問題,ES6引入瞭解構的概念,按場景可分為:

  • 物件解構
  • 陣列解構
  • 混合解構
  • 解構引數

物件解構

我們使用ES6中的物件結構,改寫以上示例:

const person = {
  name: 'AAA',
  age: 23
}
const { name, age } = person
console.log(name) // AAA
console.log(age)  // 23
複製程式碼

必須為解構賦值提供初始化程式,同時如果解構右側為null或者undefined,解構會發生錯誤。

// 以下程式碼為錯誤示例,會報錯
var { name, age }
let { name, age }
const { name, age }
const { name, age } = null
const { name, age } = undefined
複製程式碼

解構賦值

我們不僅可以在解構時重新定義變數,還可以解構賦值已存在的變數:

const person = {
  name: 'AAA',
  age: 23
}
let name, age
// 必須新增(),因為如果不加,{}代表是一個程式碼塊,而語法規定程式碼塊不能出現在賦值語句的左側。
({ name, age } = person)
console.log(name) // AAA
console.log(age)  // 23
複製程式碼

解構預設值

使用解構賦值表示式時,如果指定的區域性變數名稱在物件中不存在,那麼這個區域性變數會被賦值為undefined,此時可以隨意指定一個預設值。

const person = {
  name: 'AAA',
  age: 23
}
let { name, age, sex = '男' } = person
console.log(sex) // 男
複製程式碼

為非同名變數賦值

目前為止我們解構賦值時,待解構的鍵和待賦值的變數是同名的,但如何為非同名變數解構賦值呢?

const person = {
  name: 'AAA',
  age: 23
}
let { name, age } = person
// 相當於
let { name: name, age: age } = person
複製程式碼

let { name: name, age: age } = person含義是:在person物件中取鍵為nameage的值,並分別賦值給name變數和age變數。
那麼,我們根據以上的思路,為非同名變數賦值可以改寫成如下形式:

const person = {
  name: 'AAA',
  age: 23
}
let { name: newName, age: newAge } = person
console.log(newName) // AAA
console.log(newAge)  // 23
複製程式碼

巢狀物件結構

解構巢狀物件任然與物件字面量語法相似,只是我們可以將物件拆解成我們想要的樣子。

const person = {
  name: 'AAA',
  age: 23,
  job: {
    name: 'FE',
    salary: 1000
  },
  department: {
    group: {
      number: 1000,
      isMain: true
    }
  }
}
let { job, department: { group } } = person
console.log(job)    // { name: 'FE', salary: 1000 }
console.log(group)  // { number: 1000, isMain: true }
複製程式碼

let { job, department: { group } } = person含義是:在person中提取鍵為job、在person的巢狀物件department中提取鍵為group的值,並把其賦值給對應的變數。

陣列解構

陣列的解構賦值與物件解構的語法相似,但簡單許多,它使用的是陣列字面量,且解構操作全部在陣列內完成,解構的過程是按值在陣列中的位置進行提取的。

const colors = ['red', 'green', 'blue']
let [firstColor, secondColor] = colors
// 按需解構
let [,,threeColor] = colors
console.log(firstColor)   // red
console.log(secondColor)  // green
console.log(threeColor)   // blue
複製程式碼

與物件一樣,解構陣列也能解構賦值給已經存在的變數,只是可以不需要像物件一樣額外的新增括號:

const colors = ['red', 'green', 'blue']
let firstColor, secondColor
[firstColor, secondColor] = colors
console.log(firstColor)   // red
console.log(secondColor)  // green
複製程式碼

按以上原理,我們可以輕鬆擴充套件一下解構賦值的功能(快速交換兩個變數的值):

let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a); // 2
console.log(b); // 1
複製程式碼

與物件一樣,陣列解構也可以設定解構預設值:

const colors = ['red']
const [firstColor, secondColor = 'green'] = colors
console.log(firstColor)   // red
console.log(secondColor)  // green
複製程式碼

當存在巢狀陣列時,我們也可以使用和解構巢狀物件的思路來解決:

const colors = ['red', ['green', 'lightgreen'], 'blue']
const [firstColor, [secondColor]] = colors
console.log(firstColor)   // red
console.log(secondColor)  // green
複製程式碼

不定引數

在解構陣列時,不定元素只能放在最後一個,在後面繼續新增逗號會導致報錯。

在陣列解構中,有一個和函式的不定引數相似的功能:在解構陣列時,可以使用...語法將陣列中剩餘元素賦值給一個特定的變數:

let colors = ['red', 'green', 'blue']
let [firstColor, ...restColors] = colors
console.log(firstColor) // red
console.log(restColors) // ['green', 'blue']
複製程式碼

根據以上解構陣列中的不定元素的原理,我們可以實現同concat一樣的陣列複製功能:

const colors = ['red', 'green', 'blue']
const concatColors = colors.concat()
const [...restColors] = colors
console.log(concatColors) // ['red', 'green', 'blue']
console.log(restColors)   // ['red', 'green', 'blue']
複製程式碼

解構引數

當我們定一個需要接受大量引數的函式時,通常我們會建立可以可選的物件,將額外的引數定義為這個物件的屬性:

function setCookie (name, value, options) {
  options = options || {}
  let path = options.path,
      domain = options.domain,
      expires = options.expires
  // 其它程式碼
}

// 使用解構引數
function setCookie (name, value, { path, domain, expires } = {}) {
  // 其它程式碼
}
複製程式碼

程式碼分析:{ path, domain, expires } = {}必須提供一個預設值,如果不提供預設值,則不傳遞第三個引數會報錯:

function setCookie (name, value, { path, domain, expires }) {
  // 其它程式碼
}
// 報錯
setCookie('type', 'js')
// 相當於解構了undefined,所以會報錯
{ path, domain, expires } = undefined
複製程式碼

Symbol及其Symbol屬性

ES6之前,JavaScript語言只有五種原始型別:stringnumberbooleannullundefiend。在ES6中,新增了第六種原始型別:Symbol

可以使用typeof來檢測Symbol型別:

const symbol = Symbol('Symbol Test')
console.log(typeof symbol) // symbol
複製程式碼

建立Symbol

可以通過全域性的Symbol函式來建立一個Symbol

const firstName = Symbol()
const person = {}
person[firstName] = 'AAA'
console.log(person[firstName]) // AAA
複製程式碼

可以在Symbol()中傳遞一個可選的引數,可以讓我們新增一段文字描述我們建立的Symbol,其中文字是儲存在內部屬性[[Description]]中,只有當呼叫SymboltoString()方法時才可以讀取這個屬性。

const firstName = Symbol('Symbol Description')
const person = {}
person[firstName] = 'AAA'
console.log(person[firstName]) // AAA
console.log(firstName)         // Symbol('Symbol Description')
複製程式碼

Symbol的使用方法

所有可以使用可計算屬性名的地方,都可以使用Symbol

let firstName = Symbol('first name')
let lastName = Symbol('last name')
const person = {
  [firstName]: 'AAA'
}
Object.defineProperty(person, firstName, {
  writable: false
})
Object.defineProperties(person, {
  [lastName]: {
    value: 'BBB',
    writable: false
  }
})
console.log(person[firstName])  // AAA
console.log(person[lastName])   // BBB
複製程式碼

Symbol共享體系

ES6提供了一個可以隨時訪問的全域性Symbol登錄檔來讓我們可以建立共享Symbol的能力,可以使用Symbol.for()方法來建立一個共享的Symbol

// Symbol.for方法的引數,也被用做Symbol的描述內容
const uid = Symbol.for('uid')
const object = {
  [uid]: 12345
}
console.log(person[uid]) // 12345
console.log(uid)         // Symbol(uid)
複製程式碼

程式碼分析:

  • Symbol.for()方法首先會在全域性Symbol註冊變中搜尋鍵為uidSymbol是否存在。
  • 存在,直接返回已有的Symbol
  • 不存在,則建立一個新的Symbol,並使用這個鍵在Symbol全域性註冊變中註冊,隨後返回新建立的Symbol

還有一個和Symbol共享有關的特性,可以使用Symbol.keyFor()方法在Symbol全域性登錄檔中檢索與Symbol有關的鍵,如果存在則返回,不存在則返回undefined

const uid = Symbol.for('uid')
const uid1 = Symbol('uid1')
console.log(Symbol.keyFor(uid))   // uid
console.log(Symbol.keyFor(uid1))  // undefined
複製程式碼

Symbol與型別強制轉換

其它原始型別沒有與Symbol邏輯相等的值,尤其是不能將Symbol強制轉換為字串和數字。

const uid = Symbol.for('uid')
console.log(uid)
console.log(String(uid))
// 報錯
uid = uid + ''
uid = uid / 1
複製程式碼

程式碼分析:我們使用console.log()方法列印Symbol,會呼叫SymbolString()方法,因此也可以直接呼叫String()方法輸出Symbol。然而嘗試將Symbol和一個字串拼接,會導致程式丟擲異常,Symbol也不能和每一個數學運算子混合使用,否則同樣會丟擲錯誤。

Symbol屬性檢索

Object.keys()Object.getOwnPropertyNames()方法可以檢索物件中所有的屬性名,其中Object.keys返回所有可以列舉的屬性,Object.getOwnPropertyNames()無論屬性是否可以列舉都返回,但是這兩個方法都無法返回Symbol屬性。因此ES6引入了一個新的方法Object.getOwnPropertySymbols()方法。

const uid = Symbol.for('uid')
let object = {
  [uid]: 123
}
const symbols = Object.getOwnPropertySymbols(object)
console.log(symbols.length) // 1
console.log(symbols[0])     // Symbol(uid)
複製程式碼

Symbol暴露內部的操作

ES6通過在原型鏈上定義與Symbol相關的屬性來暴露更多的語言內部邏輯,這些內部操作如下:

  • Symbol.hasInstance:一個在執行instanceof時呼叫的內部方法,用於檢測物件的繼承資訊。
  • Symbol.isConcatSpreadable:一個布林值,用於表示當傳遞一個集合作為Array.prototype.concat()方法的引數時,是否應該將集合內的元素規整到同一層級。
  • Symbol.iterator:一個返回迭代器的方法。
  • Symbol.match:一個在呼叫String.prototype.match()方法時呼叫的方法,用於比較字串。
  • Symbol.replace:一個在呼叫String.prototype.replace()方法時呼叫的方法,用於替換字串中的子串。
  • Symbol.search:一個在呼叫String,prototype.search()方法時呼叫的方法,用於在字串中定位子串。
  • Symbol.split:一個在呼叫String.prototype.split()方法時呼叫的方法,用於分割字串。
  • Symbol.species:用於建立派生物件的建構函式。
  • Symbol.toPrimitive:一個返回物件原始值的方法。
  • Symbol.toStringTag:一個在呼叫Object.prototype.toString()方法時使用的字串,用於建立物件描述。
  • Symbol.unscopables:一個定義了一些不可被with語句引用的物件屬性名稱的物件集合。

重寫一個由well-known Symbol定義的方法,會導致物件內部的預設行為被改變,從而一個普通物件會變為一個奇異物件。

Symbol.hasInstance

每一個函式都有Symbol.hasInstance方法,用於確定物件是否為函式的例項,並且該方法不可被列舉、不可被寫和不可被配置。

function MyObject () {
  // 空函式
}
Object.defineProperty(MyObject, Symbol.hasInstance, {
  value: function () {
    return false
  }
})
let obj = new MyObject()
console.log(obj instanceof MyObject) // false
複製程式碼

程式碼分析:使用Object.defineProperty方法,在MyObject函式上改寫Symbol.hasInstance,為其定義一個總是返回false的新函式,即使obj確實是MyObject的例項,但依然在進行instanceof判斷時返回了false

注意如果要觸發Symbol.hasInstance呼叫,instanceof的左操作符必須是一個物件,如果為非物件則會導致instanceof始終返回false。

Symbol.isConcatSpreadable

JavaScript陣列中concat()方法被用於拼接兩個陣列:

const colors1 = ['red', 'green']
const colors2 = ['blue']
console.log(colors1.concat(colors2, 'brown')) // ['red', 'green', 'blue', 'brown']
複製程式碼

concat()方法中,我們傳遞了第二個引數,它是一個非陣列元素。如果Symbol.isConcatSpreadabletrue,那麼表示物件有length屬性和數字鍵,故它的數值型鍵會被獨立新增到concat呼叫的結果中,它是物件的可選屬性,用於增強作用於特定物件型別的concat方法的功能,有效簡化其預設特性:

const obj = {
  0: 'hello',
  1: 'world',
  length: 2,
  [Symbol.isConcatSpreadable]: true
}
const message = ['Hi'].concat(obj)
console.log(message) // ['Hi', 'hello', 'world']
複製程式碼

Symbol.match,Symbol.replace,Symbol.search,Symbol.split

JavaScript中,字串與正規表示式經常一起出現,尤其是字串型別的幾個方法,可以接受正規表示式作為引數:

  • match:確定給定字串是否匹配正規表示式。
  • replace:將字串中匹配正規表示式的部分替換為給定的字串。
  • search:在字串中定位匹配正則表示位置的索引。
  • split:按照匹配正規表示式的元素將字串進行分割,並將分割結果存入陣列中。

ES6之前,以上幾個方法無法使用我們自己定義的物件來替代正規表示式進行字串匹配,而在ES6之後,引入了與上述幾個方法相對應Symbol,將語言內建的Regex物件的原生特性完全外包出來。

const hasLengthOf10 = {
  [Symbol.match] (value) {
    return value.length === 10 ? [value] : null
  },
  [Symbol.replace] (value, replacement) {
    return value.length === 10 ? replacement : value
  },
  [Symbol.search] (value) {
    return value.length === 10 ? 0 : -1
  },
  [Symbol.split] (value) {
    return value.length === 10 ? [,] : [value]
  }
}
const message1 = 'Hello world'
const message2 = 'Hello John'
const match1 = message1.match(hasLengthOf10)
const match2 = message2.match(hasLengthOf10)
const replace1 = message1.replace(hasLengthOf10)
const replace2 = message2.replace(hasLengthOf10, 'AAA')
const search1 = message1.search(hasLengthOf10)
const search2 = message2.search(hasLengthOf10)
const split1 = message1.split(hasLengthOf10)
const split2 = message2.split(hasLengthOf10)
console.log(match1)     // null
console.log(match2)     // [Hello John]
console.log(replace1)   // Hello world
console.log(replace2)   // AAA
console.log(search1)    // -1
console.log(search2)    // 0
console.log(split1)     // [Hello John]
console.log(split2)     // [,]
複製程式碼

Symbol.toPrimitive

Symbol.toPrimitive方法被定義在每一個標準型別的原型上,並且規定了當物件被轉換為原始值時應該執行的操作,每當執行原始值轉換時,總會呼叫Symbol.toPrimitive方法並傳入一個值作為引數。
對於大多數標準物件,數字模式有以下特性,根據優先順序的順序排序如下:

  • 呼叫valueOf()方法,如果結果為原始值,則返回。
  • 否則,呼叫toString()方法,如果結果為原始值,則返回。
  • 如果再無可選值,則丟擲錯誤。

同樣對於大多數標準物件,字串模式有以下有限級順序:

  • 呼叫toString()方法,如果結果為原始值,則返回。
  • 否則,呼叫valueOf()方法,如果結果為原始值,則返回。
  • 如果再無可選值,則丟擲錯誤。

在大多數情況下,標準物件會將預設模式按數字模式處理(除Date物件,在這種情況下,會將預設模式按字串模式處理),如果自定義了Symbol.toPrimitive方法,則可以覆蓋這些預設的強制轉換行為。

function Temperature (degress) {
  this.degress = degress
}
Temperature.prototype[Symbol.toPrimitive] = function (hint) {
  switch (hint) {
    case 'string':
      return this.degress + '℃'
    case 'number':
      return this.degress
    case 'default':
      return this.deress + ' degress'
  }
}
const freezing = new Temperature(32)
console.log(freezing + '')      // 32 degress
console.log(freezing / 2)       // 16
console.log(String(freezing))   // 32℃
複製程式碼

程式碼分析:我們在物件Temperature原型上重寫了Symbol.toPrimitive,新方法根據引數hint指定的模式返回不同的值,其中hint引數由JavaScript引擎傳入。其中+運算子觸發預設模式,hint被設定為default;/運算子觸發數字模式,hint被設定為number;String()函式觸發字串模式,hint被設定為string

Symbol.toStringTag

JavaScript中,如果我們同時存在多個全域性執行環境,例如在瀏覽器中一個頁面包含iframe標籤,因為iframe和它外層的頁面分別代表不同的領域,每一個領域都有自己的全域性作用域,有自己的全域性物件,在任何領域中建立的陣列,都是一個正規的陣列。然而,如果將這個數字傳遞到另外一個領域中,instanceof Array語句的檢測結果會返回false,此時Array已經是另一個領域的建構函式,顯然被檢測的陣列不是由這個建構函式建立的。

針對以上問題,我們很快找到了一個相對來說比較實用的解決方案:

function isArray(value) {
  return Object.prototype.toString.call(value) === '[object Array]'
}
console.log(isArray([])) // true
複製程式碼

與上述問題有一個類似的案例,在ES5之前我們可能會引入第三方庫來建立全域性的JSON物件,而在瀏覽器開始實現JSON全域性物件後,就有必要區分JSON物件是JavaScript環境本身提供的還是由第三方庫提供的:

function supportsNativeJSON () {
  return typeof JSON !== 'undefined' && Object.prototype.toString.call(JSON) === '[object JSON]'
}
複製程式碼

ES6中,通過Symbol.toStringTag這個Symbol改變了呼叫Object.prototype.toString()時返回的身份標識,其定義了呼叫物件的Object.prototype.toString.call()方法時返回的值:

function Person (name) {
  this.name = name
}
Person.prototype[Symbol.toStringTag] = 'Person'
const person = new Person('AAA')
console.log(person.toString())                        // [object Person]
console.log(Object.prototype.toString.call(person))   // [object Person]
複製程式碼

Set和Map集合

Set集合是一種無重複元素的列表,通常用來檢測給定的值是否在某個集合中;Map集合內含多組鍵值對,集合中每個元素分別存放著可訪問的鍵名和它對應的值,Map集合經常被用來快取頻繁取用的資料。

ES5中的Set和Map集合

ES6還沒有正式引入Set集合和Map集合之前,開發者們已經開始使用物件屬性來模擬這兩種集合了:

const set = Object.create(null)
const map = Object.create(null)
set.foo = true
map.bar = 'bar'
// set檢查
if (set.foo) {
  console.log('存在')
}
// map取值
console.log(map.bar) // bar
複製程式碼

以上程式很簡單,確實可以使用物件屬性來模擬Set集合和Map集合,但卻在實際使用的過程中有諸多的不方便:

  • 物件屬性名必須為字串:
const map = Object.create(null)
map[5] = 'foo'
// 本意是使用數字5作為鍵名,但被自動轉換為了字串
console.log(map['5']) // foo
複製程式碼
  • 物件不能作為屬性名:
const map = Object.create(null)
const key1 = {}
const key2 = {}
map[key1] = 'foo'
// 本意是使用key1物件作為屬性名,但卻被自動轉換為[object Object]
// 因此map[key1] = map[key2] = map['[object Object]']
console.log(map[key2]) // foo
複製程式碼
  • 不可控制的強制型別轉換:
const map = Object.create(null)
map.count = 1
// 本意是檢查count屬性是否存在,實際檢查的確是map.count屬性的值是否為真
if (map.count) {
  console.log(map.count)
}
複製程式碼

ES6中的Set集合

Set集合是一種有序列表,其中含有一些相互獨立的非重複值,在Set集合中,不會對所存的值進行強制型別轉換。

其中Set集合涉及到的屬性和方法有:

  • Set建構函式:可以使用此建構函式建立一個Set集合。
  • add方法:可以向Set集合中新增一個元素。
  • delete方法:可以移除Set集合中的某一個元素。
  • clear方法:可以移除Set集合中所有的元素。
  • has方法:判斷給定的元素是否在Set集合中。
  • size屬性:Set集合的長度。

建立Set集合

Set集合的建構函式可以接受任何可迭代物件作為引數,例如:陣列、Set集合或者Map集合。

const set = new Set()
set.add(5)
set.add('5')
// 重複新增的值會被忽略
set.add(5)
console.log(set.size) // 2
複製程式碼

移除元素

使用delete()方法可以移除集合中的某一個值,使用clear()方法可以移除集合中所有的元素。

const set = new Set()
set.add(5)
set.add('5')
console.log(set.has(5)) // true
set.delete(5)
console.log(set.has(5)) // false
console.log(set.size)   // 1
set.clear()
console.log(set.size)   // 0
複製程式碼

Set集合的forEach()方法

Set集合的forEach()方法和陣列的forEach()方法是一樣的,唯一的區別在於Set集合在遍歷時,第一和第二個引數是一樣的。

const set = new Set([1, 2])
set.forEach((value, key, arr) => {
  console.log(`${value} ${key}`)
  console.log(arr === set)
})
// 1 1
// true
// 2 2
// true
複製程式碼

Set集合轉換為陣列

因為Set集合不可以像陣列那樣通過索引去訪問元素,最好的做法是將Set集合轉換為陣列。

const set = new Set([1, 2, 3, 4])
// 方法一:展開運算子
const arr1 = [...set]
// 方法二:Array.from方法
const arr2 = Array.from(set)
console.log(arr1) // [1, 2, 3, 4]
console.log(arr2) // [1, 2, 3, 4]
複製程式碼

Weak Set集合

通過以上對Set集合的梳理,我們可以發現:只要Set例項中的引用存在,垃圾回收機制就不能釋放該物件的記憶體空間,所以我們把Set集合看作是一個強引用的集合。為了更好的處理Set集合的垃圾回收,引入了一個叫Weak Set的集合:

Weak Set集合只支援三種方法:add、has和delete。

const weakSet = new WeakSet()
const key = {}
weakSet.add(key)
console.log(weakSet.has(key)) // true
weakSet.delete(key)
console.log(weakSet.has(key)) // false
複製程式碼

Set集合和Weak Set集合有許多共同的特性,但它們之間還是有一定的差別的:

  • Weak Set集合只能儲存物件元素,向其新增非物件元素會導致丟擲錯誤,同理has()delete()傳遞非物件也同樣會報錯。
  • Weak Set集合不可迭代,也不暴露任何迭代器,因此也不支援forEach()方法。
  • Weak Set集合不支援size屬性。

ES6中的Map集合

ES6中的Map型別是一種儲存著許多鍵值對的有序列表,其中的鍵名和對應的值支援所有的資料型別,鍵名的等價性判斷是通過呼叫Object.is方法來實現的。

const map = new Map()
const key1 = {
  name: 'key1'
}
const key2 = {
  name: 'key2'
}
map.set(5, 5)
map.set('5', '5')
map.set(key1, key2)
console.log(map.get(5))     // 5
console.log(map.get('5'))   // '5'
console.log(map.get(key1))  // {name:'key2'}
複製程式碼

Map集合支援的方法

Set集合類似,Map集合也支援以下幾種方法:

  • has:判斷指定的鍵名是否在Map集合中存在。
  • delete:在Map集合中移除指定鍵名及其對應的值。
  • clear:移除Map集合中所有的鍵值對。
const map = new Map()
map.set('name', 'AAA')
map.set('age', 23)
console.log(map.size)        // 2
console.log(map.has('name')) // true
console.log(map.get('name')) // AAA
map.delete('name')
console.log(map.has('name')) // false
map.clear()
console.log(map.size)        // 0
複製程式碼

Map集合的初始化方法

在初始化Map集合的時候,也可以像Set集合傳入陣列,但此時陣列中的每一個元素都是一個子陣列,子陣列中包含一個鍵值對的鍵名和值兩個元素。

const map = new Map([['name', 'AAA'], ['age', 23]])
console.log(map.has('name'))  // true
console.log(map.has('age'))   // true
console.log(map.size)         // 2
console.log(map.get('name'))  // AAA
console.log(map.get('age'))   // 23
複製程式碼

Map集合的forEach()方法

Map集合中的forEach()方法的回撥引數和陣列類似,每一個引數的解釋如下:

  • 第一個引數是鍵名
  • 第二個引數是值
  • 第三個引數是Map集合本身
const map = new Map([['name', 'AAA'], ['age', 23]])
map.forEach((key, value, ownMap) => {
  console.log(`${key} ${value}`)
  console.log(ownMap === map)
})
// name AAA
// true
// age 23
// true
複製程式碼

Weak Map集合

Weak Map它是一種儲存著許多鍵值對的無序列表,集合中的鍵名必須是一個物件,如果使用非物件鍵名會報錯。

Weak Map集合只支援set()、get()、has()和delete()。

const key1 = {}
const key2 = {}
const key3 = {}
const weakMap = new WeakMap([[key1, 'AAA'], [key2, 23]])
weakMap.set(key3, '廣東')

console.log(weakMap.has(key1)) // true
console.log(weakMap.get(key1)) // AAA
weakMap.delete(key1)
console.log(weakMap.has(key)) // false
複製程式碼

Map集合和Weak Map集合有許多共同的特性,但它們之間還是有一定的差別的:

  • Weak Map集合的鍵名必須為物件,新增非物件會報錯。
  • Weak Map集合不可迭代,因此不支援forEach()方法。
  • Weak Map集合不支援clear方法。
  • Weak Map集合不支援size屬性。

迭代器(Iterator)和生成器(Generator)

迴圈語句的問題

我們在日常的開發過程中,很可能寫過下面這樣的程式碼:

var colors = ['red', 'gree', 'blue']
for(var i = 0, len = colors.length; i < len; i++) {
  console.log(colors[i])
}
// red
// green
// blue
複製程式碼

程式碼分析:雖然迴圈語句的語法簡單,但是如果將多個迴圈巢狀則需要追蹤多個變數,程式碼複雜度會大大增加,一不小心就會錯誤使用了其它for迴圈的跟蹤變數,從而造成程式出錯,而ES6引入迭代器的宗旨就是消除這種複雜性並減少迴圈中的錯誤。

什麼是迭代器

問:什麼是迭代器?
答:迭代器是一種特殊的物件,它具有一些專門為迭代過程設計的專有介面,所有迭代器都有一個叫next的方法,每次呼叫都返回一個結果物件。結果物件有兩個屬性,一個是value表示下一次將要返回的值;另外一個是done,它是一個布林型別的值,當沒有更多可返回的資料時返回true。迭代器還會儲存一個內部指標,用來指向當前集合中值的位置,每呼叫一次next方法,都會返回下一個可用的值。

在瞭解迭代器的概念後,我們使用ES5語法來建立一個迭代器:

function createIterator (items) {
  var i = 0
  return {
    next: function () {
      var done = i >= items.length
      var value = !done ? items[i++] : undefined
      return {
        done: done,
        value: value
      }
    }
  }
}
var iterator = createIterator([1, 2, 3])
console.log(iterator.next())  // { value: 1, done: false }
console.log(iterator.next())  // { value: 2, done: false }
console.log(iterator.next())  // { value: 3, done: false }
console.log(iterator.next())  // { value: undefined, done: true }
複製程式碼

正如上面那樣,我們使用了ES5語法來建立我們自己的迭代器,它的內部實現很複雜,而ES6除了引入了迭代器的概念還引入了一個叫生成器的概念,使用它我們可以讓建立迭代器的過程更加簡單一點。

什麼是生成器

問:什麼是生成器?
答:生成器是一種返回迭代器的函式,通過function關鍵字後的*號來表示,函式中會用到新的關鍵詞yield

function * createIterator () {
  yield 1
  yield 2
  yield 3
}
const iterator = createIterator()
console.log(iterator.next().value)  // 1
console.log(iterator.next().value)  // 2
console.log(iterator.next().value)  // 3
複製程式碼

正如我們上面的輸出結果一樣,它和我們使用ES5語法建立的迭代器輸出結果是一致的。

生成器函式最重要的一點是:每執行完一條yield語句,函式就會自動終止:我們在ES6之前,函式一旦開始執行,則一直會向下執行,一直到函式return語句都不會中斷,但生成器函式卻打破了這一慣例:當執行完一條yield語句時,函式會自動停止執行,除非程式碼手動呼叫迭代器的next方法。

我們也可以在迴圈中使用生成器:

function * createIterator (items) {
  for(let i = 0, len = items.length; i < len; i++) {
    yield items[i]
  }
}
const it = createIterator([1, 2, 3])
console.log(it.next())  // { done: false, value: 1 }
console.log(it.next())  // { done: false, value: 2 }
console.log(it.next())  // { done: false, value: 3 }
console.log(it.next())  // { done: true, value: undefined }
複製程式碼

yield關鍵字只能在生成器內部使用,在其他地方使用會導致丟擲錯誤,即使是在生成器內部的函式中使用也是如此。

function * createIterator (items) {
  items.forEach(item => {
    // 丟擲錯誤
    yield item + 1
  })
}
複製程式碼

可迭代物件和for-of迴圈

問:可迭代物件有什麼特點?
答:可迭代物件具有Symbol.iterator屬性,是一種與迭代器密切相關的物件。Symbol.iterator通過指定的函式可以返回一個作用於附屬物件的迭代器。在ES6中,所有的集合物件(陣列、Set集合以及Map集合)和字串都是可迭代物件,這些物件中都有預設的迭代器。由於生成器預設會為Symbol.iterator屬性賦值,因此所有通過生成器建立的迭代器都是可迭代物件。

ES6新引入了for-of迴圈每執行一次都會呼叫可迭代物件的next方法,並將迭代器返回的結果物件的value屬性儲存在一個變數中,迴圈將持續執行這一過程直到返回物件的done屬性的值為true

const value = [1, 2, 3]
for (let num of value) {
  console.log(num);
}
// 1
// 2
// 3
複製程式碼

訪問預設的迭代器

可以通過Symbol.iterator來訪問物件的預設迭代器

const values = [1, 2, 3]
const it = values[Symbol.iterator]()
console.log(it.next())  // {done:false, value:1}
console.log(it.next())  // {done:false, value:2}
console.log(it.next())  // {done:false, value:3}
console.log(it.next())  // {done:true, value:undefined}
複製程式碼

由於具有Symbol.iterator屬性的物件都有預設的迭代器物件,因此可以用它來檢測物件是否為可迭代物件:

function isIterator (object) {
  return typeof object[Symbol.iterator] === 'function'
}

console.log(isIterator([1, 2, 3]))  // true
console.log(isIterator('hello'))    // true,字串也可以迭代,原理等同於陣列
console.log(isIterator(new Set()))  // true
console.log(isIterator(new Map))    // true
複製程式碼

建立可迭代物件

預設情況下,我們自己定義的物件都是不可迭代物件,但如果給Symbol.iterator屬性新增一個生成器,則可以將其變為可迭代物件。

let collection = {
  items: [1, 2, 3],
  *[Symbol.iterator] () {
    for (let item of this.items) {
      yield item
    }
  }
}
for (let value of collection) {
  console.log(value)
}
// 1
// 2
// 3
複製程式碼

內建迭代器

集合物件迭代器

ES6中有三種型別的集合物件:陣列、Set集合和Map集合,它們都內建瞭如下三種迭代器:

  • entries:返回一個迭代器,其值為多個鍵值對。
  • values:返回一個迭代器,其值為集合的值。
  • keys:返回一個迭代器,其值為集合中的所有鍵名。

entries()迭代器:

const colors = ['red', 'green', 'blue']
const set = new Set([1, 2, 3])
const map = new Map([['name', 'AAA'], ['age', 23], ['address', '廣東']])

for (let item of colors.entries()) {
  console.log(item)
  // [0, 'red']
  // [1, 'green']
  // [2, 'blue']
}
for (let item of set.entries()) {
  console.log(item)
  // [1, 1]
  // [2, 2]
  // [3, 3]
}
for (let item of map.entries()) {
  console.log(item)
  // ['name', 'AAA']
  // ['age', 23]
  // ['address', '廣東']
}
複製程式碼

values迭代器:

const colors = ['red', 'green', 'blue']
const set = new Set([1, 2, 3])
const map = new Map([['name', 'AAA'], ['age', 23], ['address', '廣東']])

for (let item of colors.values()) {
  console.log(item)
  // red
  // green
  // blue
}
for (let item of set.values()) {
  console.log(item)
  // 1
  // 2
  // 3
}
for (let item of map.values()) {
  console.log(item)
  // AAA
  // 23
  // 廣東
}
複製程式碼

keys迭代器:

const colors = ['red', 'green', 'blue']
const set = new Set([1, 2, 3])
const map = new Map([['name', 'AAA'], ['age', 23], ['address', '廣東']])

for (let item of colors.keys()) {
  console.log(item)
  // 0
  // 1
  // 2
}
for (let item of set.keys()) {
  console.log(item)
  // 1
  // 2
  // 3
}
for (let item of map.keys()) {
  console.log(item)
  // name
  // age
  // address
}
複製程式碼

不同集合型別的預設迭代器:每一個集合型別都有一個預設的迭代器,在for-of迴圈中,如果沒有顯示的指定則使用預設的迭代器:

  • 陣列和Set集合:預設迭代器為values
  • Map集合:預設為entries
const colors = ['red', 'green', 'blue']
const set = new Set([1, 2, 3])
const map = new Map([['name', 'AAA'], ['age', 23], ['address', '廣東']])
for (let item of colors) {
  console.log(item)
  // red
  // green
  // blue
}
for (let item of set) {
  console.log(item)
  // 1
  // 2
  // 3
}
for (let item of map) {
  console.log(item)
  // ['name', 'AAA']
  // ['age', 23]
  // ['address', '廣東']
}
複製程式碼

解構和for-of迴圈:如果要在for-of迴圈中使用解構語法,則可以簡化編碼過程:

const map = new Map([['name', 'AAA'], ['age', 23], ['address', '廣東']])
for (let [key, value] of map.entries()) {
  console.log(key, value)
  // name AAA
  // age 23
  // address 廣東
}
複製程式碼

字串迭代器

ES6釋出以來,JavaScript字串的行為慢慢變得更像陣列了:

let message = 'Hello'
for(let i = 0, len = message.length; i < len; i++) {
  console.log(message[i])
  // H
  // e
  // l
  // l
  // o
}
複製程式碼

NodeList迭代器

DOM標準中有一個NodeList型別,代表頁面文件中所有元素的集合。ES6為其新增了預設的迭代器,其行為和陣列的預設迭代器一致:

let divs = document.getElementByTagNames('div')
for (let div of divs) {
  console.log(div)
}
複製程式碼

展開運算子和非陣列可迭代物件

我們在前面的知識中已經知道,我們可以使用展開運算子把一個Set集合轉換成一個陣列,像下面這樣:

let set = new Set([1, 2, 3, 4])
let array = [...set]
console.log(array) // [1, 2, 3, 4]
複製程式碼

程式碼分析:在我們所用...展開運算子的過程中,它操作的是Set集合的預設可迭代物件(values),從迭代器中讀取所有值,然後按照返回順序將他們依次插入到陣列中。

const map = new Map([['name', 'AAA'], ['age', 23], ['address', '廣東']])
const array = [...map]
console.log(array) // [['name', 'AAA'], ['age', 23], ['address', '廣東']]
複製程式碼

程式碼分析:在我們使用...展開運算子的過程中,它操作的是Map集合的預設可迭代物件(entries),從迭代器中讀取多組鍵值對,依次插入陣列中。

const arr1 = ['red', 'green', 'blue']
const arr2 = ['yellow', 'white', 'black']
const array = [...arr1, ...arr2]
console.log(array) // ['red', 'green', 'blue', 'yellow', 'white', 'black']
複製程式碼

程式碼分析:在使用...展開運算子的過程中,同Set集合一樣使用的都是其預設迭代器(values),然後按照返回順序依次將他們插入到陣列中。

高階迭代器功能

給迭代器傳遞引數

如果給迭代器next()方法傳遞引數,則這個引數的值就會替代生成器內部上一條yield語句的返回值。

function * createIterator () {
  let first = yield 1
  let second = yield first + 2
  yield second + 3
}
let it = createIterator()
console.log(it.next(11)) // {done: false, value: 1}
console.log(it.next(4))  // {done: false, value: 6}
console.log(it.next(5))  // {done: false, value: 8}
console.log(it.next())   // {done: true, value: undefined}
複製程式碼

程式碼分析:除了第一個迭代器,其它幾個迭代器我們能很快計算出結果,但為什麼第一個next()方法的引數無效呢?這是因為傳給next()方法的引數是替代上一次yield的返回值,而在第一次呼叫next()方法之前不會執行任何yield語句,因此給第一次呼叫的next()方法傳遞引數,無論傳遞任何值都將被捨棄。

在迭代器中丟擲錯誤

除了給迭代器傳遞資料外,還可以給他傳遞錯誤條件,讓其恢復執行時丟擲一個錯誤。

function * createIterator () {
  let first = yield 1
  let second = yield first + 2
  // 不會被執行
  yield second + 3
}
let it = createIterator()
console.log(it.next())                    // {done: false, value: 1}
console.log(it.next(4))                   // {done: false, value: 6}
console.log(it.throw(new Error('break'))) // 丟擲錯誤
複製程式碼

正如我們上面看到的那樣,我們想生成器內部傳遞了一個錯誤物件,迭代器恢復執行時會丟擲一個錯誤,我們可以使用try-catch語句來捕獲這種錯誤:

function * createIterator () {
  let first = yield 1
  let second
  try {
    second = yield first + 2
  } catch(ex) {
    second = 6
  }
  yield second + 3
}
let it = createIterator()
console.log(it.next())                    // {done: false, value: 1}
console.log(it.next(4))                   // {done: false, value: 6}
console.log(it.throw(new Error('break'))) // {done: false, value: 9}
console.log(it.next())                    // {done: true, value: undefined}
複製程式碼

生成器返回語句

由於生成器也是函式,因此可以通過return語句提前退出函式執行,對於最後一次next()方法呼叫,可以主動為其指定一個返回值。

function * createIterator () {
  yield 1
  return 2
  // 不會被執行
  yield 3
  yield 4
}
let it = createIterator()
console.log(it.next())  // {done: false, value: 1}
console.log(it.next())  // {done: false, value: 2}
console.log(it.next())  // {done: true, value: undefined}
複製程式碼

程式碼分析:在生成器中,return語句表示所有的操作都已經完成,屬性值done會被設定成true,如果同時提供了響應的值,則屬性value會被設定為這個值,並且return語句之後的yield不會被執行。

展開運算子和for-of迴圈會直接忽略通過return語句指定的任何返回值,因為只要done被設定為true,就立即停止讀取其他的值。

const obj = {
  items: [1, 2, 3, 4, 5],
  *[Symbol.iterator] () {
    for (let i = 0, len = this.items.length; i < len; i++) {
      if (i === 3) {
        return 300
      } else {
        yield this.items[i]
      }
    } 
  }
}
for (let value of obj) {
  console.log(value)
  // 1
  // 2
  // 3
}
console.log([...obj]) // [1, 2, 3]
複製程式碼

委託生成器

我們可以將兩個迭代器合二為一,這樣就可以建立一個生成器,再給yield語句新增一個星號,以達到將生成資料的過程委託給其他迭代器。

function * createColorIterator () {
  yield ['red', 'green', 'blue']
}
function * createNumberIterator () {
  yield [1, 2, 3, 4]
}
function * createCombineIterator () {
  yield * createColorIterator();
  yield * createNumberIterator();
}
let it = createCombineIterator()
console.log(it.next().value)  // ['red', 'green', 'blue']
console.log(it.next().value)  // [1, 2, 3, 4]
console.log(it.next().value)  // undefined
複製程式碼

如果你覺得寫的不錯請給一個star,如果你想閱讀上、下兩部分全部的筆記,請點選閱讀全文

閱讀《深入理解ES6》書籍,筆記整理(上)
閱讀《深入理解ES6》書籍,筆記整理(下)

相關文章