因爲所有筆記有接近4W的字數,所以分開爲上、下兩部分,第二部份內容計劃於明後兩天更新。
若是你以爲寫的不錯請給一個star
,若是你想閱讀上、下兩部分所有的筆記,請點擊全文javascript
在ECMAScript6
標準定稿以前,已經開始出現了一些實驗性的轉譯器(Transpiler)
,例如谷歌的Traceur
,能夠將代碼從ECMAScript6
轉換成ECMAScript5
。但它們大多功能很是有限,或難以插入現有的JavaScript
構建管道。
可是,隨後出現了的新型轉譯器6to5
改變了這一切。它易於安裝,能夠很好的集成現有的工具中,生成的代碼可讀,因而就像野火同樣逐步蔓延開來,6to5
也就是如今鼎鼎大名的Babel
。html
JavaScript核心的語言特性是在標準ECMA-262中被定義,該標準中定義的語言被稱做ECMAScript,它是JavaScript的子集。 java
演變之路:git
停滯不前
:逐漸興起的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引入塊級做用域可讓咱們更好的控制做用域。 es6
問:提高機制(hoisting
)是什麼?
答:在函數做用域或全局做用域中經過關鍵字var
聲明的變量,不管其實是在哪裏聲明的,都會被當成在當前做用域頂部聲明的變量,這就是咱們常說的提高機制。
如下實例代碼說明了這種提高機制:github
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]
複製代碼
由於let
和const
聲明的變量不會進行聲明提高,因此在let
和const
變量聲明以前任何訪問(即便是typeof
也不行)此變量的操做都會引起錯誤:
if (condition) {
// 報錯
console.log(typeof value)
let value = 'value'
}
複製代碼
問:爲何會報錯?
答:JavaScript
引擎在掃描代碼發現變量聲明時,要麼將它們提高至做用域的頂部(var
聲明),要麼將聲明放在TDZ
(暫時性死區)中(let
和const
聲明)。訪問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
插入通過安全轉換後的字符串的能力。而在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
是一個數組,它包含:
substitutions
也是一個數組:
name
的值,即:why
。age
的值,即:23
。經過以上規律咱們能夠發現:
literals[0]
始終表明字符串的開頭。literals
總比substitutions
多一個。咱們能夠經過以上這種模式,將literals
和substitutions
這兩個數組交織在一塊兒從新組成一個字符串,來模擬模板字面量的默認行爲,像下面這樣:
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 () {}
}
複製代碼
代碼分析:在以上示例中,timeout
和callback
是可選參數,若是不傳入則會使用邏輯或操做符賦予默認值。然而這種方式也有必定的缺陷,若是咱們想給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)
})
複製代碼
在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
複製代碼
在前面已經提到過let
和const
存在暫時性死區,即:在let
和const
變量聲明以前嘗試訪問該變量會觸發錯誤。相同的道理,在函數默認參數中也存在暫時性死區,以下:
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: '尼古拉斯' }
複製代碼
代碼分析:
在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
複製代碼
問:爲何ES6
會引入函數的name
屬性。
答:在JavaScript
中有多重定義函數的方式,於是辨別函數就是一項具備挑戰性的任務,此外匿名函數表達式的普遍使用也加大了調試的難度,爲了解決這些問題,在ESCAScript 6
中爲全部函數新增了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
複製代碼
儘管肯定函數聲明和函數表達式的名稱很容易,但仍是有一些其餘狀況不是特別容易識別:
getter
和setter
:在對象上存在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
這些值由外圍最近一層非箭頭函數所決定。[[Construct]]
函數,因此不能經過new
關鍵詞進行調用,若是使用new
進行調用會拋出錯誤。new
關鍵詞進行調用,因此沒有構建原型的須要,也就沒有了prototype
這個屬性。this
的值不可改變(即不能經過call
、apply
或者bind
等方法來改變)。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)
}
}
複製代碼
在使用JavaScript
比較兩個值的時候,咱們可能會習慣使用==
或者===
來進行判斷,使用全等===
在比較時能夠避免觸發強制類型轉換,因此深受許多人的喜好。但全等===
也並不是是徹底準確的,例如: +0===-0
會返回true
,NaN===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
複製代碼
問:什麼是Mixin
?
答:混合Mixin
是JavaScript
中實現對象組合最流行的一種模式。在一個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
複製代碼
在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
對象中提取name
和age
的值,並把其值賦值給對應的同名變量,過程極其類似。假設咱們要提取許多變量,這種過程會重複更屢次,若是其中還包含嵌套結構,只靠遍歷是找不到真實信息的。
針對以上問題,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
對象中取鍵爲name
和age
的值,並分別賦值給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
複製代碼
在ES6
以前,JavaScript
語言只有五種原始類型:string
、number
、boolean
、null
和undefiend
。在ES6
中,添加了第六種原始類型:Symbol
。
可使用typeof
來檢測Symbol
類型:
const symbol = Symbol('Symbol Test')
console.log(typeof symbol) // symbol
複製代碼
能夠經過全局的Symbol
函數來建立一個Symbol
。
const firstName = Symbol()
const person = {}
person[firstName] = 'AAA'
console.log(person[firstName]) // AAA
複製代碼
能夠在Symbol()
中傳遞一個可選的參數,可讓咱們添加一段文本描述咱們建立的Symbol
,其中文本是存儲在內部屬性[[Description]]
中,只有當調用Symbol
的toString()
方法時才能夠讀取這個屬性。
const firstName = Symbol('Symbol Description')
const person = {}
person[firstName] = 'AAA'
console.log(person[firstName]) // AAA
console.log(firstName) // Symbol('Symbol Description')
複製代碼
全部可使用可計算屬性名的地方,均可以使用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
複製代碼
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
註冊變中搜索鍵爲uid
的Symbol
是否存在。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
強制轉換爲字符串和數字。
const uid = Symbol.for('uid')
console.log(uid)
console.log(String(uid))
// 報錯
uid = uid + ''
uid = uid / 1
複製代碼
代碼分析:咱們使用console.log()
方法打印Symbol
,會調用Symbol
的String()
方法,所以也能夠直接調用String()
方法輸出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)
複製代碼
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
方法,用於肯定對象是否爲函數的實例,而且該方法不可被枚舉、不可被寫和不可被配置。
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。
在JavaScript
數組中concat()
方法被用於拼接兩個數組:
const colors1 = ['red', 'green']
const colors2 = ['blue']
console.log(colors1.concat(colors2, 'brown')) // ['red', 'green', 'blue', 'brown']
複製代碼
在concat()
方法中,咱們傳遞了第二個參數,它是一個非數組元素。若是Symbol.isConcatSpreadable
爲true
,那麼表示對象有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']
複製代碼
在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
方法並傳入一個值做爲參數。
對於大多數標準對象,數字模式有如下特性,根據優先級的順序排序以下:
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
。
在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
集合內含多組鍵值對,集合中每一個元素分別存放着可訪問的鍵名和它對應的值,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)
}
複製代碼
Set
集合是一種有序列表,其中含有一些相互獨立的非重複值,在Set
集合中,不會對所存的值進行強制類型轉換。
其中Set
集合涉及到的屬性和方法有:
Set
構造函數:可使用此構造函數建立一個Set
集合。add
方法:能夠向Set
集合中添加一個元素。delete
方法:能夠移除Set
集合中的某一個元素。clear
方法:能夠移除Set
集合中全部的元素。has
方法:判斷給定的元素是否在Set
集合中。size
屬性: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()
方法和數組的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
集合轉換爲數組。
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]
複製代碼
經過以上對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
類型是一種存儲着許多鍵值對的有序列表,其中的鍵名和對應的值支持全部的數據類型,鍵名的等價性判斷是經過調用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'}
複製代碼
與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
集合的時候,也能夠像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
集合自己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集合只支持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
屬性。咱們在平常的開發過程當中,極可能寫過下面這樣的代碼:
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
})
}
複製代碼
問:可迭代對象有什麼特色?
答:可迭代對象具備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
}
複製代碼
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
複製代碼