understanding es6


title: 【翻譯】理解 ES6(Understanding ECMAScript 6)
date: <2019-06-16 Sun>
updated: <2019-06-16 Sun>
comments: true
tags:javascript

  • javascript
  • es6

categories: ecmascript
layout: post
permalink:
top: 10
copyright: truehtml


{% note info %}
原書:leanpub.com/understandi…java

該書爲我的爲學習而翻譯的,文中幾乎全部代碼都是來自原書或在原書代碼基礎上修改而來。node

該原文地址:blog.gcl666.com/2019/06/23/… {% endnote %}git

簡介

JavaScript 核心特性在 ECMA-262 標準中被定義,也叫作 ECMAScript ,咱們所熟知的在瀏覽器端和 Node.js 其實是 ECMAScript 的一個超集。程序員

ES6 演變之路

1999 發佈 v3

1999.TC39 年發佈了 ECMA-262 第三版。es6

直到 2007 以前都沒有任何變化。github

2007 發佈 v4, v3.1

2007 年發佈了第四版,包含如下特性:web

  • 新語法(new syntax)
  • 模塊(modules)
  • 類概念(classes)
  • 類繼承概念(classical inheritance)
  • 對象私有屬性(private object members)
  • 更多類型
  • 其餘

因爲第四版涉及的內容太多,所以形成分歧,部分紅員由此建立了正則表達式

3.1 版本,只包含少部分的語法變化,聚焦在:

  • 屬性
  • 原生 JSON 支持
  • 已有對象增長方法

可是兩撥人在 v4 和 v3.1 版本之間並無達成共識,致使最後不了了之。

2008 JavaScript 創始人決定

Brendan Eich 決定將着力於 v3.1 版本。

最後 v3.1 做爲 ECMA-262 的第五個版本被標準化,即: ECMASCript 5

2015 年發佈 ECMAScript 6 也叫 ECMAScript 2015

即本書要講的內容(ES6)。

塊級綁定(var, let, const)

Var 聲明和提高

使用 var 來聲明變量時,在一個做用域內,不管它在哪裏聲明的,都會被升到到該做用域的頂部。

好比:

function getValue(condition) {
  // 好比: var value; // undefined

  if (condition) {
    // 雖然在這裏聲明的,其實會被提高到函數頂端
    var value = 'blue'

    // code

    return value
  } else {
    // 這裏依舊能夠訪問變量 `value` 只不過它的值是 `undefined`
    return null
  }
}

console.log(getValue(false)) // 'null'
複製代碼

上面的 getValue 至關於下面的變量聲明版本(提高以後):

function getValue(condition) {
  var value; // undefined

  if (condition) {
    value = 'blue'

    // code

    return value
  } else {
    return null
  }
}

console.log(getValue(false)) // 'null'

複製代碼

+RESULTS:

null
複製代碼

塊級聲明 let/const 聲明

塊級做用域,如:函數,*{ … }* 大括號,等等都屬於塊級做用域,在該做用域下使用 let 聲明的變量只在

該做用域下可訪問。

聲明提高問題

let 聲明不會被提高,可是也有另外一種說法是 let 會提高,而且在若是在提高處到賦值的中間範圍內使用了該變量,

會使該區域成爲一塊臨時死區(TDZ)。

在聲明以前使用 let 變量:

VM88:4 Uncaught ReferenceError: Cannot access 'value' before initialization

function getValue(cond) {

  if (cond) {
    console.log(value)
    let value = 'blue'

    // code

    return value
  } else {
    // value 在該做用域不存在

    return null
  }

  // value 在該做用域不存在
}

getValue(true)

複製代碼

不能重複聲明

使用 var 的時候是能夠重複聲明的:

var count = 39; var count;

這樣是不會有問題的,只不過它的聲明只會被記錄一次而已,即只會記錄 var count = 39; 這裏聲明,可是不會出現異常。

若是使用 let 就不同了,若是出現重複聲明則會異常:

var count = 39;let count;

異常結果:*SyntaxError: Identifier 'count' has already been declared*

二者差異

let 聲明的值可變,const 聲明的是個常量,值是不能發生改變的。

let name = 'xxx';

name = 'yyy'; // ok

const age = 100;

age = 88; // error
複製代碼

臨時死區(TDZ)

使用 let/const 聲明的變量,任什麼時候候試圖在其聲明以前使用變量都會拋出異常。

即便是在聲明以前使用 typeof 也會出現引用異常(ReferenceError)。

if (true) {
  console.log(typeof value)
  let value = 'blue'
}

複製代碼

img

循環中使用塊級聲明

咱們都知道使用 var 聲明的變量是不存在塊級做用域的,即在 if/for 的 {} 做用域內使用 var

聲明的變量實際上是該全局做用下的全局變量。

好比:咱們常見的 for 循環中的 i 的值

for (var i = 0; i < 10; i++) {
  // ...
}

console.log(i) // 10
複製代碼

+RESULTS:

10
複製代碼

結果爲 10 代表在 console.log(i) 處是能夠訪問 i 變量的,由於 var i = 0; 的聲明

被提高成了全局變量,即循環體中使用的一直是這一份全局變量。

若是是同步代碼,可能沒什麼問題,但要是異步代碼就會出現問題,以下結果:

for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i))
}
複製代碼

+RESULTS:

5
5
5
5
5
複製代碼

很遺憾最後結果都成了 5,由於循環體是個異步代碼 setTimeout

解決方法有:

  • 閉包:

造成一個封閉的做用域,將當前的 i 值傳遞進去。

for (var i = 0; i < 5; i++) {
  (v => {
    // 這裏的 v 值即傳遞進來的當前次循環的 i 的值
    setTimeout(() => console.log(v))
  })(i)
}
複製代碼

+RESULTS:

0
1
2
3
4
複製代碼
  • let

每次循環至關於新建立了一個變量,所以變量的值都得以保存。

for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i))
}
複製代碼

+RESULTS:

0
1
2
3
4
複製代碼

全局做用域聲明

var, let, const 另外一個區別是在全局環境下的聲明做用域也是不同,

咱們都知道在全局做用域下使用 var 聲明的話,瀏覽器端是能夠經過 window.name 來訪問該變量的,可是 let, const 卻不行。

var age = 100

let name = 'xxx'

console.log(window.name)
console.log(window.age)
複製代碼

結果:

img

瀏覽器端做用域:

img

結論:

不管 let 在那裏聲明的它都是個塊級做用域變量,只在其聲明到該做用域以後才能使用。

var 聲明的始終相對於當前做用域下是全局變量。

總結(var, let, const)

在 es6 以後儘可能使用 let 和 const 去聲明變量,嚴格控制變量的做用域。

  1. var 變量聲明會提高,可重複聲明,且在該做用域內爲全局變量
  2. let/const 變量聲明不會提高,不可重複聲明,局部變量,且在 DTZ 範圍內使用即便是 typeof 也會報錯
  3. let/const 區別在於 const 聲明的變量值不能發生改變
關鍵詞 提高 做用域 值屬性
var 有提高,聲明提高(命名函數定義也提高) 範圍內全局 可變
let 無提高 局部變量,做用域內聲明處開始往下 可變
const 無提高 局部變量,做用域內聲明處開始往下 不可變

字符串和正則表達式

更好的 Unicode 編碼支持

UTF-16 編碼

新增 str.codePointAt(n) 和 String.fromCodePoint(str)

已有的編碼查詢函數: str.charCodeAt 和 String.fromCodeAt 用來應對單字符一個字節的狀況。

新增的兩個函數能夠處理單個字符串佔兩個字節的大小,好比一些特殊字符「𠮷」須要用到兩個字節來存儲。

即 2bytes = 16bits 大小。

charCodeAt 和 fromCodeAt 是以一個字節爲單位來處理字符串的,所以若是遇到這些字就無法正常處理。

var name = '𠮷'

console.log(name.charCodeAt(0))
console.log(name.codePointAt(0))
console.log(String.fromCharCode(name.charCodeAt(0)))
console.log(String.fromCodePoint(name.codePointAt(0)))

複製代碼

+RESULTS:

55362
134071
�
𠮷
複製代碼

能夠看到若是咱們還用原來的函數 charCodeAt 和 fromCharCode 去處理這個字獲得結果是不正確的。

normalize() 函數

參考連接:www.cnblogs.com/hahazexia/p…

repeat(n) 函數

將一個字符串重複 n 次後返回。

var c = 'x'

var b = c.repeat(3)

console.log(b, c, b === c)
複製代碼

+RESULTS:

xxx x false
複製代碼

正則表達式

y 標記

模板字符串

基本語法

let msg = `hello world`

console.log(msg)
console.log(typeof msg)
console.log(msg.length)
複製代碼

+RESULTS:

hello world
string
11
複製代碼

若是須要用到反引號,則須要使用轉義字符: \`

多行字符串

避免一行太長,進行換行書寫,可是不影響最終結果顯示在一行,可使用反斜槓

var msg = `multiline \ string`

console.log(msg)
複製代碼

+RESULTS:

multiline string
複製代碼

多行字符串狀況:

var msg = "multiline \n string"

console.log(msg)
複製代碼

+RESULTS:

multiline
 string
複製代碼

使用模板字符串,會按照模板字符串中的格式原樣輸出,而再也不須要顯示使用 `\n` 來進行換行:

var msg = `multiline string`

console.log(msg)
複製代碼

+RESULTS:

multiline
string
複製代碼

在模板字符串中空格也會是字符串的一部分

var msg1 = `multiline string`

var msg2 = `multiline string`

console.log(`len1: ${msg1.length}`)
console.log(`len2: ${msg2.length}`)
複製代碼

+RESULTS:

len1: 19
len2: 16
複製代碼

因此在書寫模板字符串的時候必須慎重使用縮進。

模板字符串插值

var name = 'xxx'
const getAge = () => 100

console.log(`my name is ${name}`) // 普通字符串
console.log(`3 + 4 = ${3 + 4}`) // 可執行計算
console.log(`call function to get age : ${getAge()}`) // 可調用函數
複製代碼

+RESULTS:

my name is xxx
3 + 4  = 7
call function to get age : 100
複製代碼

標籤模板

容許使用標籤模板,該標籤對應的是一個函數,後面的模板字符串會被解析成參數傳遞給該函數去進行處理,最後返回處理的結果。

好比: let msg = tag`Hello World`

定義標籤:

function tag(literals, ...substitutions) {
  // 返回一個字符串
}
複製代碼

示例:

let count = 10,
    price = 0.25,
    msg = passthru`${count} items cost $${(count * price).toFixed(2)}.`

function passthru(literals, ...subs) {
  console.log(literals.join('--'))
  console.log(subs)

  // 將結果拼起來

  return subs.map((s, i) => literals[i] + subs[i]).join('')
    + literals[literals.length - 1]
}

console.log(msg)
複製代碼

+RESULTS:

-- items cost $--.
[ 10, '2.50' ]
10 items cost $2.50.
複製代碼

從結果能夠看到,標籤函數參數的內容分別爲:

  1. literals 被插值({})分割成的字符串數組,好比上例的結果爲: `["", " items const", "."]`
  2. subs 爲插值計算的結果值做爲第2, … 第 n 個參數傳遞給了 passthru

標籤模板原始值(String.raw())

有時候須要在模板字符串中直接使用帶有轉義字符的內容,好比: `\n` 而不是使用其轉義以後的含義。

這個時候則可使用新增的內置 tag 函數來處理。

好比:

let msg1 = `multiline\nstring`
let msg2 = String.raw`multileline\nstring`

console.log(msg1)
console.log(msg2)
複製代碼

+RESULTS:

multiline
string
multileline\nstring
複製代碼

可看到在咱們使用 String.raw 以後的 \n 並無被轉義成換行符,而是按照其原始的樣子輸出。

若是在不適用內置的 Strng.raw 該怎麼作?

function raw(literals, ...subs) {

  // 將結果拼起來

  return subs.map((s, i) => literals.raw[i] + subs[i]).join('')
    + literals.raw[literals.length - 1]
}

let msg = raw`multiline\nstring`

console.log(msg)

複製代碼

+RESULTS:

multiline\nstring
複製代碼

nodejs 環境可能看起來不直觀,經過下圖咱們來直觀的查看下標籤函數是怎麼處理帶轉義字符的字符串的:

img

會發現其實 literals 的值依舊是轉義以後的,看數組中第一個元素的字符串中是有一個回車標識的。

此外該數組對象自己上面多了一個 raw 屬性,其值爲沒有轉義的內容。

從這裏咱們得出,標籤模板是怎麼處理帶轉義字符串的模板的。

總結

  1. 完整的編碼支持賦予了 JavaScript 處理 UTF-16 字符的能力(經過 codePointAt()String.fromCodePoint() 來轉換)
  2. u 新增的標記使得正則表達式能夠經過碼點來代替 UTF-16 字符
  3. normalize()
  4. 模板字符串,支持原始字符串,插值支持計算表達式或函數調用
  5. 標籤模板,第一個參數爲分割後的字符串列表,後面的參數分別爲插值結果
  6. 轉義標籤模板,轉義標籤的第一個參數數組對象上包含一個 raw 數組,其中包含了原始值列表

函數

參數默認值

function makeRequest(url, timeout = 2000, callback = () => {}) {
  // ...
}
複製代碼

默認參數值是如何影響 arguments 對象的?

嚴格非嚴格模式下的 arguments

只要記住一旦使用了默認值,那麼 arguments 對象的行爲將發生改變。

在 ECMAScript5 的非嚴格模式下,arguments 對象的內容是會隨着函數內部函數參數值得變化而發生變化的,也就是說它

並非在調用函數之初值就固定了,好比:

function maxArgs(first, second) {
  console.log(first === arguments[0])
  console.log(second === arguments[1])
  first = 'c'
  second = 'd'
  console.log(first === arguments[0])
  console.log(second === arguments[1])
}

maxArgs('a', 'b')
複製代碼

+RESULTS:

true
true
true
true
複製代碼

從結果咱們會發現,參數值發生變化也會致使 arguments 對象跟着變化,這種狀況只會在非嚴格模式下產生,

在嚴格模式下, arguments 對象是不會隨着參數值改變而改變的。

function maxArgs(first, second) {
 'use strict';

  console.log(first === arguments[0])
  console.log(second === arguments[1])
  first = 'c'
  second = 'd'
  console.log(first === arguments[0])
  console.log(second === arguments[1])
}

maxArgs('a', 'b')

複製代碼

+RESULTS:

true
true
false
false
複製代碼

喏,後面結果爲 false

帶默認參數值狀況下 arguments

在 es6 以後,arguments 的行爲和以前嚴格模式下是同樣的,即不會映射參數值得變化。

  1. 帶默認值得參數,若是在調用的時候不傳遞,是不會計入到 arguments 對象當中

    即 arguments 的實際個數是根據調用的時候所傳遞的參數個數來決定的。

  2. arguments 對象再也不響應參數值得變化

function mixArgs(first, second = 'b') {
  console.log(arguments.length)
  console.log(first === arguments[0]) // true
  console.log(second === arguments[1]) // false
  first = 'c'
  second = 'd'
  console.log(first === arguments[0]) // false
  console.log(second === arguments[1]) // false
}

mixArgs('a')
複製代碼

+RESULTS:

1
true
false
false
false
複製代碼

默認參數表達式

參數默認值不只可使用靜態值,還能夠賦值爲調用函數的結果

function getValue() {
  console.log('get value...')
  return 5
}

function add(first, second = getValue()) {
  return first + second
}

console.log(add(1, 1)) // 2
console.log(add(1)) // 6
複製代碼

+RESULTS:

2
get value...
6
複製代碼

從結果顯示:

  1. 若是 second 沒傳,會在調用 add() 時候執行 getValue() 獲取默認值
  2. 若是傳遞了 second,那麼 getValue() 是不會被執行的

即在默認參數中調用的函數,是由在調用時該對應的函數參數是否有傳遞來決定是否調用。

而不是傳遞了 second,先調用 getValue() 獲得值,而後用傳遞的 second 值去覆蓋。

也就是說 getValue() 返回的值不用每次都同樣,是能夠在每次調用的時候發生變化的,好比:

var n = 5

function getValue() {
  return n++
}

function add(first, second = getValue()) {
  return first + second
}

console.log(add(1, 1)) // 2
console.log(add(1)) // 6
console.log(add(1)) // 7
複製代碼

+RESULTS:

2
6
7
複製代碼

因爲上面的特性,參數默認值能夠是動態的,所以咱們能夠將前面參數值做爲後面參數的默認值來使用,

好比:

function add(first, second = first) {
  return first + second
}

console.log(add(1, 1)) // 2
console.log(add(1)) // 2
複製代碼

+RESULTS:

2
2
複製代碼

甚至還能夠將 first 做爲參數傳遞給 getValue(first) 獲取新值做爲默認值來用。

默認參數值的臨時死區(TDZ)

這裏臨時死區的意思是指,第二個參數在使用以前未進行聲明,由於參數的聲明至關於使用了 let

根據 let 的特性,在爲聲明以前使用屬於在 TDZ 範圍,會拋異常。

實例:

function add(first = second, second) {
  return first + second
}

console.log(add(1, 1)) // 2

try {
  add(undefined, 1) // error
} catch (e) {
  console.log(e.message)
}
複製代碼

+RESULTS:

2
second is not defined
複製代碼

既然都存在 TDZ 那爲何第一次調用就沒事了,下面來分析下看看:

記住上一節所講的:

默認值的調用(如: getValue() )只有在參數未傳遞的狀況下才會發生,這裏 first=second 的狀況依舊適用。

那麼將這句話應用到這裏:

  1. add(1, 1) 這裏 first 傳遞了 1

    那麼 first 在 add 被調用的時候會被初始化成 1,根據上面那句話,即此時 first=second 這句至關於並無被執行

    所以就不會去檢測 second ,也就不會出現未定義了,從而能得出正確結果:2。

  2. add(undefined, 1) 傳遞了 `undefined` 至關於沒傳這個參數,只是佔了個位

    那麼既然沒傳, first=second 就會被執行, second 就會被檢測是否認義,然而檢測的結果就是「未定義」,

    所以拋出異常。

將 add 函數參數的變化用下來轉聲明來表示,問題就會更明顯了:

// add(1, 1)

let first = 1 // first = second 未執行,不檢測
let second = 1

// add(undefined, 1)
let first = second // 這句被執行,至關於這裏提早使用了 second 變量,let 特性生效
let second = 1
複製代碼

{% note warning %}
函數參數是有它本身的做用域和TDZ的,而且和函數體做用域是區分開的,

這就意味着函數參數是沒法訪問函數體內的任何變量的,由於根據就是兩個不一樣的做用域。
{% endnote %}

未命名參數

爲何會存在未命名參數?

由於 JavaScript 是沒有限制調用函數的時候傳遞參數個數的。

好比:聲明瞭一個函數 function add() {} 沒任何參數,可是調用的時候是能夠這樣的 add(1, 2, 3, ...)

那麼這些調用的時候傳遞給 add 的參數對應的函數參數就叫作未命名參數。

function add() {
  let n = 0
  ;[].slice.call(arguments).forEach(v => n += v)

  return n
}

console.log(add(1, 2, 3, 4, 5))
複製代碼

+RESULTS:

15
複製代碼

參數展開符(…)

未命名參數通常不多使用,由於這讓使用者會很迷惑該函數的做用,所以參數沒任何明顯特徵表示它是幹什麼用的,

在 es6 中增長了一個展開符號(…),在函數參數中的做用是將傳遞進的參數列表合併成一個參數數組。

適用於一個函數參數個數未知的狀況下使用。

好比:

function pick(object, ...keys) {
  // 這裏 keys 會成爲一個包含傳入的其他參數值的數組
  let result = Object.create(null)

  console.log(arguments.length)
  for (let i = 0; i < keys.length; i++) {
    result[keys[i]] = object[keys[i]]
  }

  return result
}

const book = {
  author: 'xxx',
  name: 'yyy',
  pages: 300
}

const res = pick(book, 'author', 'name')

console.log(JSON.stringify(res))
複製代碼

+RESULTS:

3
{"author":"xxx","name":"yyy"}
複製代碼

利用 …keys 將傳入的 ('author', 'name') 合併成了一個數組: ['author', 'name'] ,方便應對

函數參數個數可變的狀況。

參數展開符兩種異常使用狀況

  1. 展開符參數必須是最後一個,不能在其後面還有其餘參數

    好比: function add(n, ...vals, more) {} 這會出現異常

  2. 不能用在對象的 setter 函數上

實例:

const obj = {
  set name(...val) {}
}
複製代碼

img

function add(n, ...vals, more) {

}
複製代碼

img

參數展開符對 arguments 的影響

記住一點:

arguments 老是由函數調用時傳遞進來的參數決定

function checkArgs(...args) {
  console.log(args.length);
  console.log(arguments.length);
  console.log(args[0], arguments[0]);
  console.log(args[1], arguments[1]);
}

checkArgs("a", "b");
複製代碼

+RESULTS:

2
2
a a
b b
複製代碼

函數構造函數能力加強

在實際編碼過程,咱們不多直接使用 Function() 構造函數去建立一個函數。

好比這麼使用:

// 參數:參數一名稱 first, 參數二名稱 second,... 最後一個是函數體
var add = new Function('first', 'second', 'return first + second')

console.log(add(1, 2))
複製代碼

+RESULTS:

3
複製代碼

在 es6 中對構造函數的使用能力加強了,給其賦予了更多的功能,好比

  1. 默認參數值
  2. 展開符
var add = new Function("first", "second = first",
                       "return first + second");

console.log(add(1, 1));     // 2
console.log(add(1));        // 2

var pickFirst = new Function("...args", "return args[0]");

console.log(pickFirst(1, 2));   // 1
複製代碼

+RESULTS:

2
2
1
複製代碼

展開符(…)

在以前咱們在函數參數中用到了展開符,這個時候的用途是將參數合併成數組來用。

普通參數傳遞

咱們通常調用函數的時候都是將參數逐個傳遞:

let v1 = 20,
    v2 = 30

console.log(Math.max(v1, v2))
複製代碼

+RESULTS:

30
複製代碼

這僅僅兩個參數,比較好書寫,一旦參數多了起來就比較麻煩,在 es6 以前的作法能夠利用 Function.prototype.apply 去實現:

apply 傳遞多個參數

let vs = [1, 2, 3, 4, 5]

console.log(Math.max.apply(Math, vs))
複製代碼

+RESULTS:

5
複製代碼

由於 apply 會將數組進行展開做爲函數的參數傳遞個調用它的函數。

es6 以後展開符傳遞

在 es6 以後咱們將使用展開符去完成這項工做,讓代碼更簡潔和便於理解。

let vs = [1, 2, 3, 4]

console.log(Math.max(...vs))
複製代碼

+RESULTS:

4
複製代碼

展開符,傳統方式相結合

let vs = [1, 2, 3, 4]

console.log(Math.max(10, ...vs)) // 10
console.log(Math.max(...vs, 0)) // 4
console.log(Math.max(3, ...vs, 10)) // 10
複製代碼

+RESULTS:

10
4
10
複製代碼

函數名字屬性

以往,因爲函數的各類使用方式使 JavaScript 在識別函數的時候成爲一種挑戰,而且匿名函數的

頻繁使用使得程序的 debugging 過程異常痛苦,常常形成追蹤棧很難理解。

所以在 es6 中給全部函數添加了一個 name 屬性。

{% note info %}
name 屬性只是對函數的一種描述特性,並不會有實際的引用特性,也就是說

在實際編程中不可能經過函數的 name 屬性去幹點啥。
{% endnote %}

選擇合適的名稱

JavaScript 會根據函數的聲明方式去給其選擇合適的名稱,好比:

function doSomething() {
  // ...
}

var doAnotherThing = function() {
  // ...
};

var doThirdThing = function do3rdThing() {

}

console.log(doSomething.name);          // "doSomething"
console.log(doAnotherThing.name);       // "doAnotherThing"
console.log(doThirdThing.name);       // "do3rdThing"
複製代碼

+RESULTS:

doSomething
doAnotherThing
do3rdThing
複製代碼
  1. 若是是命名函數式聲明方式,則使用的就是它的名字做爲 name 屬性值,如: doSomething

  2. 若是是表達式匿名方式聲明函數,則將使用表達式中左邊的變量名稱來做爲 name 屬性值,如: doAnotherThing

  3. 表達式命名方式聲明函數,則將使用命名函數的名稱做爲 name 屬性,如: doThridThing 的名字是: do3rdThing

{% note info %}
經過第三個輸出可知,命名函數的優先級高於表達式的變量名。
{% endnote %}

name 屬性的特殊狀況

  1. 對象的函數名稱,即該函數的名字
  2. 對象的訪問器函數名稱,經過 Object.getOwnPropertyDescriptor(obj, 'keyname') 獲取訪問器對象
  3. 調用 bind() 以後的函數名稱,老是在原始函數名前加上 bound
  4. 使用 new Function() 建立的函數名稱,老是返回 anonymous
var doSth = function() {}

var person = {
  get firstName() {
    return 'Nicholas'
  },

  sayName: function() {
    console.log(this.name)
  }
}

console.log(person.sayName.name) // sayName
// 訪問器屬性,只能經過 getOwnPropertyDescriptor 去獲取
var descriptor = Object.getOwnPropertyDescriptor(person, 'firstName')
console.log(descriptor.get.name) // get firstName

// 調用 bind 以後的函數名稱老是會在原始的函數名稱以前加上 `bound fname`
console.log(doSth.bind().name) // bound doSth
console.log((new Function()).name) // anonymous
複製代碼

+RESULTS:

sayName
get firstName
bound doSth
anonymous
複製代碼

澄清函數雙重目的

函數使用方式

  1. 直接調用,當作函數來使用 Person()

  2. 使用 new 的時候當作構造函數來使用建立一個實例對象

在 es6 以後爲了搞清楚這兩種使用方式,添加了兩個內置屬性: [[Call]][[Constructor]]

噹噹作函數直接調用時,其實內部是調用了 [[Call]] 執行了函數體,

當結合 new 來使用是,調用的是 [[Contructor]] 執行了如下步驟:

  1. 建立一個新的對象 newObj

  2. this 綁定到 newObj

  3. 將 newObj 對象返回做爲該構造函數的一個實例對象

也就是說咱們能夠在構造函數中去改變它的行爲,若是它沒有顯示的 return 一個合法的對象,

則會默認走 #3 步,若是咱們顯示的去返回了一個對象,那麼最後獲得的實例對象即這個顯示返回的對象。

function Person1(name) {
  this.name = name || 'xxx'
}

// 沒有顯示的 return 一個合法對象
// 返回的是新建立的對象,而且 this 被綁定到這個心對象上
const p1 = new Person1('張三')

// 所以這裏訪問的 name 即構造函數中的 this.name
console.log(p1.name)

function Person2(name) {
  this.name = name || 'xxx'

  return {
    name: '李四'
  }
}

// 按照構造函數的使用定義,這裏返回的是
// 顯示 return 的那個對象: { name: '李四' }
const p2 = new Person2('張三')

// 所以這裏輸出的結果爲:李四
console.log(p2.name)
複製代碼

+RESULTS:

張三
李四
複製代碼

{% note warning %}
並非全部的函數都有 [[Constructor]] ,好比箭頭函數就沒有,所以箭頭函數

也就不能被用來 new 對象實例。
{% endnote %}

判斷函數被如何使用?

有時候咱們須要知道函數是如何被使用的,是當作構造函數?仍是單純當作函數直接調用?

這個時候 instanceof 就派上用場了,它的做用是用來檢測一個對象是否在當前對象的

原型鏈上出現過。

好比:在 es5 中強制一個函數只能當作構造函數來使用,通常這麼作

function Person(name) {
  if (this instanceof Person) {
    this.name = name
  } else {
    throw new Error('必須使用 new 來建立實例對象。')
  }
}

var person = new Person('張三')

// 這種調用,內部的 `this` 被綁定到了全局對象
// 而全局對象並不是 Person 原型鏈上的對象,所以會
// 執行 else 拋出異常
var notAPerson = Person('李四')
複製代碼

img

可是有一種直接調用的狀況,不會走 else ,即經過 call 調用指定 person 實例爲調用元。

function Person(name) {
  if (this instanceof Person) {
    this.name = name
  } else {
    throw new Error('必須使用 new 來建立實例對象。')
  }
}

var person = new Person('張三')

// 這樣是合法的,請 this instanceof Person 成立
// 由於 Person.call(person, ...) 指定了做用域爲實例對象 person
// 所以函數內部的 this 會被綁定到這個實例對象 person 上,
// 而 person 確實是 Person 的實例對象,所以不會報錯
var notAPerson = Person.call(person, '李四')

複製代碼

正常運行的結果

+RESULTS:

undefined
複製代碼

所以,若是是 Person.call(person, ...) 這種狀況調用,函數內部一樣沒法判斷它的被使用方式是如何。

new.target 元屬性

爲了解決上一節的「函數調用方式」判斷的問題, es6 中引入了 new.target 元屬性。

{% note info %}
元屬性:一個非對象的屬性,用來爲他的目標(好比: new )提供額外的相關信息。
{% endnote %}

new.target 的取值??

  1. 若是函數當作構造函數

    使用 new 來調用,內部調用 [[Constructor]] 的時候, new.target 會被填充爲 new 操做符

    指定的目標對象,這個目標對象一般是執行內部構造函數的時候新建立的那個對象實例(在函數體重通常是 this )。

  2. 若是函數當作普通函數直接調用,那麼 new.target 的值爲 undefined

從上面兩點,那麼咱們就能夠經過在函數內部判斷 new.target 來判斷函數的使用方式了。

function Person(name) {
  if (typeof new.target !== 'undefined') {
    this.name = name
  } else {
    throw new Error('必須使用 new 建立實例。')
  }
}

var person = new Person('張三')
console.log(person.name, 'new')

var notAPerson = Person.call(person, '李四')
console.log(notAPerson.name, 'call')
複製代碼

img

由圖中的輸出證實上面 #1 和 #2 的結論,也由此結論咱們能夠直接使用 new.target === Person 做爲斷定條件。

函數外部使用 new.target :

function Person() {

}

if (new.target === Person) {
  // ...
}
console.log(new.target)
複製代碼

塊級函數

<= es3 行爲

在 es3 或更早些時候,在塊級做用域中聲明函數會出現語法錯誤,雖然在以後默認容許這樣使用(不會報錯了),可是

各個瀏覽器之間的處理方式依舊不一樣,所以在實際開發過程當中,應該儘可能避免這麼使用,若是非要在塊級做用域聲明函數

能夠考慮使用函數表達式方式。

es5 行爲

另外,爲了嘗試去兼容這種怪異狀況,在 es5 的嚴格模式下若是在塊級做用域聲明函數,會爆出異常。

'use strict';

if (true) {
  // 在 es5 中會報語法錯誤, es6 中不會
  function doSth() {}
}
複製代碼

es6 行爲

在 es6 以後,這種函數聲明將會變的合法,且聲明以後 doSth() 就成了一個局部函數變量,即

只能在 if (true) { ... } 這個做用域內部訪問,外部沒法訪問,好比:

'use strict';

if (true) {
  // 由於有提高,且命名函數的提高包含聲明和定義都會被提高
  console.log(typeof doSth) // function
  function doSth() {}

  doSth()
}

// es6 以後存在塊級做用域,所以 doSth 是個局部變量,在
// 它的做用域範圍以外沒法訪問
console.log(typeof doSth); // undefined
複製代碼

+RESULTS:

function
undefined
複製代碼

決定何時該用塊級函數

4.7.3 一節中使用的是命名式函數聲明方式,這種方式聲明和定義均被提高,所以在

聲明處至上訪問能獲得正常結果。

若是使用表達式 + let 方式,則結果會和用 let 聲明同樣存在 TDZ 的問題。

'use strict';

if (true) {
  // TDZ 區域,訪問會異常
  console.log(typeof doSth) // error

  let doSth = function () {}

  doSth()
}

console.log(typeof doSth) // undefined
複製代碼

img

所以,咱們能夠根據需求去決定該使用哪一種方式去聲明塊級函數,若是須要有提高則應該使用「命名式函數」,

若是不須要提高,只須要在聲明以後的範圍使用應該使用「函數表達式」方式去聲明函數。

非嚴格模式塊級函數

在 es6 中的非嚴格模式下,塊級函數的提高再也不是針對塊級做用域,而是函數體或全局環境。

// 至關於提高到了這裏

if (true) {
  console.log(typeof doSth)

  // 非嚴格模式,全局提高
  function doSth() {}

  doSth()
}

console.log(typeof doSth) // function
複製代碼

+RESULTS:

function
function
複製代碼

結果顯示外面的 typeof doSth 也是 'function' 。

所以,在 es6 以後函數的聲明只須要區分嚴格或非嚴格模式,而再也不須要考慮瀏覽器的兼容問題,至關於統一了標準。

箭頭函數

箭頭函數特性

在 es6 中引入了箭頭函數,大大的簡化了函數的書寫,好比

聲明一個函數: function run() {}

如今: const run = () => {} 或者 const getName = () => '張三'

雖然用起來方便了,可是箭頭函數與普通函數又很大的不一樣,使用的時候必需要注意如下幾點:

特性 說明
1 this 減小問題,便於優化
2 super
3 arguments 箭頭函數必須依賴命名參數或 rest 參數去訪問函數的參數列表
4 new.target 元屬性 不能被實例化,功能無歧義,不須要這個屬性
5 不能 new 實例化
6 無原型 由於不能用 new 所以也不須要原型
7 不能改變 this 指向 此時指向再也不受函數自己限制
8 不能有重複的命名參數 以前非嚴格模式下普通函數是能夠有的

{% note info %}
箭頭函數中若是引用 arguments ,它指向的再也不是該箭頭函數的參數列表,

而是包含該箭頭函數的那個非箭頭函數的參數列表(4.8.6)。
{% endnote %}

沒有 this 綁定主要有兩點理由:

  1. 不易追蹤,易形成未知行爲,衆多錯誤來源

    函數內部 this 的值很是不容易追蹤,常常會形成未知的函數行爲,箭頭函數去掉它能夠避免這些煩惱

  2. 便於引擎優化

    限制箭頭函數內部使用 this 去執行代碼也有利於 JavaScript 引擎更容易去優化內部操做,而不像

    普通函數同樣,函數有可能會當作構造函數使用或其餘用途。

{% note info %}
一樣,箭頭函數也有本身的 name 屬性,用來描述函數的名稱特徵。
{% endnote %}

const print = msg => {
  console.log(arguments.length, 'arguments')
  console.log(this, 'this')
  console.log(msg)
}

console.log(print.name)

print('...end')
複製代碼

+RESULTS:

print
0 'arguments'
Object [global] {
// ... 省略
        { [Function: setImmediate] [Symbol(util.promisify.custom)]: [Function] } } 'this'
...end
undefined
複製代碼

由於是 nodejs 環境,所以 this 被綁定到了 global 對象上。

第二行輸出結果是 0 'arguments' 說明已經不能使用 arguments 去正確獲取傳入的參數了。

箭頭函數語法

箭頭函數語法很是靈活,具體如何使用根據使用場景和實際狀況決定。

好比:

var reflect = value => value; 直接返回原值

至關於

var reflect = function(value) { return value; }

當只有一個參數時刻省略小括號 ()

多個參數時候:

var sum = (n1, n2) => n1 + n2;

函數體更多內容時候:

var sum = (n1, n2) => {
  // do more...
  return n1 + n2;
}
複製代碼

空函數:

var empty = () => {}

返回一個對象:

var getTempItem = id => ({ id: id, name: 'Temp' })

等等。。。

箭頭當即函數表達式

在 es6 以前咱們要實現一個當即執行函數,通常這樣:

let person = function(name) {
  return {
    getName: function() {
      return name
    }
  }
  // 直接在函數後面加上小括號即成爲當即執行函數
}('張三')

console.log(person.getName()) // 張三
複製代碼

+RESULTS:

張三
複製代碼

PS: 可是爲了代碼可讀性,建議給函數加上小括號。

箭頭函數形式的當即執行函數,不能夠直接在 } 後面使用小括號方式:

let person = ((name) => {
  return {
    getName: function() {
      return name
    }
  }
})('張三')


console.log(person.getName()) // 張三
複製代碼

+RESULTS:

張三
複製代碼

沒有 this 對象

在以前咱們常常遇到的一個問題寫法是事件的監聽回調函數中直接使用 this ,這將致使引用錯誤問題,

由於事件的回調屬於被動觸發的,而觸發調用該回調的對象是不肯定的,這就會致使各類問題。

var PageHandler = {

  id: "123456",

  init: function() {
    document.addEventListener("click", function(event) {
      // 這裏用了 this ,意圖是想在點擊事件觸發的時候去調用 PageHandler 的
      // doSomething 這個函數,但實際倒是事與願違的
      // 由於這裏的 this 並不是指向 Pagehandler 而是事件觸發調用回調時候的那個目標對象
      this.doSomething(event.type);     // error
    }, false);
  },

  doSomething: function(type) {
    console.log("Handling " + type  + " for " + this.id);
  }
};
複製代碼

以往解決方法:經過 bind(this) 手動指定函數調用對象

var PageHandler = {

  id: "123456",

  init: function() {
    // 通過 bind 以後,回調函數的調用上下文就被綁定到了 PageHandler 這個對象
    // 真正綁定到 click 事件的函數實際上是執行 bind(this) 以後綁定了上下文的一個函數副本
    // 從而執行能獲得咱們想要的結果
    document.addEventListener("click", (function(event) {
      this.doSomething(event.type);     // no error
    }).bind(this), false);
  },

  doSomething: function(type) {
    console.log("Handling " + type  + " for " + this.id);
  }
};

複製代碼

雖然問題是解決了,可是使用 bind(this) 無疑多建立了一份函數副本,多少都會有些奇怪。

而後,在 es6 以後這個問題就很好的被箭頭函數解決掉:

根據箭頭函數沒有 this 綁定的特性,在其內部使用 this 的時候這個指向將是包含該箭頭函數的非箭頭函數

所在的上下文,即:

var PageHandler = {

  id: "123456",

  init: function() {
    document.addEventListener(
      "click",
      // 箭頭函數無 this 綁定,內部使用 this
      // 這個 this 的上下文將有包含該箭頭函數的上一個非箭頭函數
      // 這裏即 init() 函數,而 init() 函數的上下文爲 PageHandler 對象
      // 也就是說這裏箭頭函數內部的 this 指向的就是 Pagehandler 這個對象
      // 從而讓代碼按照預期運行
      event => this.doSomething(event.type), false);
  },

  doSomething: function(type) {
    console.log("Handling " + type  + " for " + this.id);
  }
};
複製代碼

箭頭函數和數組

在使用數組的一些內置函數時,咱們常常會碰到須要傳遞一個參考函數給他們,好比,排序函數 Array.prototype.sort 就須要

咱們傳遞一個比較函數用來決定是升序仍是降序等等。

若是用箭頭函數將大大簡化代碼:

// es6 以前
const values = [1, 10, 2, 5, 3]

var res1 = values.sort(function(a, b) {
  // 指定爲升序
  return a - b;
})

// es6 以後
var res2 = values.sort((a, b) => a - b)
console.log(res1.toString(), res2.toString())

複製代碼

+RESULTS:

1,2,3,5,10 1,2,3,5,10
複製代碼

或者 map(), reduce() 等等用起來會更方便更簡潔許多。

無參數綁定(arguments)

看實例:

function createArrowFunctionReturningFirstArg() {
  return () => arguments[0]
}

var arrowFunction = createArrowFunctionReturningFirstArg(5)

console.log(arrowFunction()) // 5
複製代碼

+RESULTS:

5
複製代碼

從結果看出,返回的 arrowFunction() 箭頭函數調用的時候並無傳遞任何參數,可是執行結果獲得告終果

這個結果正是包含它的那個非箭頭函數(createArrowFunctionReturingFirstArt())所接受的參數值。

所以箭頭函數內部若是訪問 arguments 對象,此時該對象指向的是包含它的那個非箭頭函數的參數列表對象。

箭頭函數的識別

跟普通函數同樣, typeofinstanceof 對齊依然使用。

var comparator = (a, b) => a - b;

console.log(typeof comparator) // function
console.log(comparator instanceof Function) // true
複製代碼

+RESULTS:

function
true
複製代碼

4.8.1 一節提到過箭頭函數是不能改變 this 指向的,可是

並不表明咱們就徹底不能使用 call, apply, bind

好比:

var sum = (n1, n2) => (this.n1 || 0) + n2

console.log(sum.call(null, 1, 2)) // 3
console.log(sum.call({ n1: 10 }, 1, 2)) // 3
複製代碼

+RESULTS:

2
2
複製代碼

從這個例子中能夠驗證,箭頭函數是沒法修改它的 this 指向的,若是能夠修改

第二個結果值就應該是 12 而不是和第一個同樣爲 2 ,由於在第二個中

咱們手動將 sum 執行上下文綁定到了一個新的對象上 {n1: 10}

{% note warning %}
也就是說,並不是不能使用,而是用了也不會有任何變化而已。
{% endnote %}

使用 bind 保留參數:

var sum = (n1, n2) => n1 + n2

console.log(sum.call(null, 1, 2)) // 3
console.log(sum.apply(null, [1, 2])) // 3

// 產生新的函數,這種和普通函數使用方式同樣
var boundSum = sum.bind(null, 1, 2)

console.log(boundSum())
複製代碼

+RESULTS:

3
3
3
複製代碼

尾調用優化

尾調用:將一個函數的調用放在兩一個函數的最後一行。

或許在 es6 中對於函數相關的最感興趣的改動就是引擎的優化了,它改變了函數的尾調用系統。

function doSth() {
  return doSthElse() // tail call
}
複製代碼

在 es6 以前,它和普通的函數調用同樣被處理:建立一個新的棧幀而後將它推到調用棧的棧頂等待被執行

也就意味着以前的每個棧幀都在內存裏面保留着,若是調用棧過大那這將多是問題的來源。

有什麼不一樣?

在 es6 以後優化了引擎,包含尾調用系統的優化(嚴格模式下,非嚴格模式下依舊未發生改變)。

優化以後,再也不會爲尾部調用建立一個新的棧幀,而是將當前的棧幀狀況,而後將其複用到尾部調用,前提是知足下面幾個條件:

  1. 尾調用函數不須要訪問當前棧幀中的任何變量(即尾調用的函數不能是閉包,閉包的做用就是用來持有變量)

  2. 即在尾調用的函數以後不能有其餘的代碼,即尾調用函數必須是函數體的最後一行

  3. 尾調用函數的調用結果要做爲當前函數的返回值返回

好比:下面的函數就知足尾調用優化的條件

'use strict'; // 1. 嚴格模式

function doSth() {

  // 2. 沒有引用任何內部變量,非閉包

  // 3. 最後一行

  // 4. 調用結果被做爲 doSth 的返回值返回
  return doSthElse()
}
複製代碼

如下狀況不會被優化:

'use strict';

function doSth() {
  doSthElse() // 返回做爲返回值,不會優化
}

function doSth1() {
  return 1 + doSthElse() // 在尾調用函數返回以後不能有其餘操做,不會優化
}

function doSth2() {
  var res = doSthElse()
  return res // 不是最後一行,即不是將結果當即返回,不會優化
}

function doSth3() {
  var num = 1,
      func = () => num

  return func() // 閉包,不會優化
}
複製代碼

如何利用尾調用優化?

尾調用最經典的莫過於遞歸調用了,好比斐波那契數列問題。

function factorial(n) {

  if (n <= 1) {
    return 1;
  } else {

    // 不會被優化,由於函數返回以後還須要進行乘積計算才返回
    return n * factorial(n - 1);
  }
}

console.log(factorial(10))
複製代碼

+RESULTS:

3628800
複製代碼

上面的並不會被優化,由於尾調用函數並非當即返回的,修改以下:

function factorial(n, p = 1) {

  if (n <= 1) {
    return 1 * p;
  } else {

    let res = n * p
    // 被優化
    return factorial(n - 1, res);
  }
}


console.log(factorial(10))
複製代碼

+RESULTS:

3628800
複製代碼

尾調用優化應該是咱們在書寫代碼的時候時常應該考慮的問題,尤爲是書寫遞歸的時候,當使用遞歸涉及到大量的計算的時候,

尾調用優化的優點將會很明顯。

總結

選項 功能 描述 其餘
arguments
ES6以前非嚴格模式 值會隨着函數體內參數的改變而改變
ES6以前嚴格模式 不會響應改變,調用之初就定了
ES6以後行爲統一 不會響應改變,內容由實際調用者傳遞個數決定
函數默認參數 能夠是常量值 function add(f, s = 3) {}
能夠是變量 var n = 10; function add(f, s = n) {}
能夠是函數調用 function getVal() {}; function add(f, s = getVal) {}
默認值參數的執行 調用時有傳遞則不會檢測或執行,未傳遞則會檢測和執行
相互引用 後面的參數能夠引用前面的參數變量 function add(f, s = f) {}
臨時死區(TDZ)
參數 rest 符號 接受多個參數,合併成數組供函數內部使用 function add(f, ...a) {}
異常使用一 不能用在訪問器函數 obj = { set name(...val) {} } 非法。
異常使用二 必須做爲函數最後一個參數使用 function add(f, ...s, t) {} 非法。
對arguments影響 非箭頭函數沒什麼影響 arguments老是由調用者傳遞的參數決定個數
構造函數 new Function() 可使用默認值,rest符號等功能
展開符(…) 普通多參數函數 Math.max(1, 2, 3, 4, ...)
普通多參數函數apply Math.max.apply(Math, [1, 2, 3, 4])
ES6展開符 Math.max(...[1, 2, 3, 4, ...])
name 屬性 函數名稱 僅輔助描述功能,易於跟蹤函數
特殊狀況: 訪問器函數 get fnName
特殊狀況:bind() 函數 bound fnName
特殊狀況:new Function() 匿名函數 anonymous
new.target 函數可直接調用可new構造實例 所以形成函數內部如何識別使用釋放問題?
若是做爲函數調用 [[Call]] new.target = undefined
若是是 new 構造函數 [[Constructor]] new.target = Person 構造函數自己
塊級函數 在 es6之情塊級函數的聲明處理並無統一 嚴格模式必出異常,非嚴格很差說
es6以後統一標準 嚴格模式:塊級函數只是局部函數 只在做用域內有效
非嚴格模式:塊級函數會提高到函數頂部或全局環境 全局或函數體生效
箭頭函數特性 this 不易追蹤,易於引擎優化 內部可使用,可是它指向的是當前箭頭函數所在的非箭頭函數所在的上下文
super 沒有原型,繼承等,不須要 super
arguments 內部訪問的該對象,實際上是當前環境函數的參數,而非箭頭函數自己的參數列表
new.target 不支持 new 就不存在使用方式問題
無原型 不支持 new
不能改變 this 指向 其內部的 this 已經不是它管轄,能夠調用 call, apply, bind 之流,可是不會有任何做用
不能有重複命名參數 非嚴格模式下ES6以前的普通參數能夠用
箭頭函數語法 使用方式靈活多變
當即表達式 必須括號包起來再執行,普通函數可直接在 } 後執行 (() => {})(), function(name){}('xxx')
typeof, instanceof 對箭頭函數依舊有效, typeof fn = 'function', fn instanceof Function (true)
尾調用優化 必須知足三個條件 不知足條件不會優化,典型的遞歸調用
1. 非閉包,尾函數體內不能訪問正函數體內任何變量
2. 結果值必須當即返回,不能參與其餘計算後再返回
3. 必須是正函數的最後一個語句
優化以前 尾函數新建棧幀,放在調用棧頂等待調用
優化以後 清空調用棧,將它做爲尾調用函數的棧幀複用

對象擴展

對象分類

類型 說明
普通對象(Ordinary) 擁有全部對象的默認行爲
異類對象(Exotic) 和默認行爲有所差別
標準對象(Standard) 那些由 ECMAScript 6 定義的,如: Array, Date 等等
內置對象(Built-in) 腳本當前執行環境中的對象,全部標準對象都是內置對象

對象字面量(literal)語法擴展

字面量語法在 JavaScript 中使用很是廣泛

  1. 書寫方便
  2. 簡潔易懂
  3. JSON 就是基於字面量語法演變而來

es6 的來到是的對象字面量語法更增強大簡潔易用。

對象屬性簡寫

<= es5:

function createPerson(name, age) {
  return {
    name: name,
    age: age
  }
}
複製代碼

es6:

function createPerson(name, age) {
  return {
    name,
    age
  }
}
複製代碼

簡潔函數寫法

<= es5:

var person = {
  name: '張三',
  sayName: function() {
    console.log(this.name)
  }
}
複製代碼

es6:

var person = {
  name: '張三',
  sayName() {
    console.log(this.name)
  }
}
複製代碼

計算屬性

在 es6 以前書寫對象字面量的時候,能夠直接使用多個字符串組成的字符串做爲 key ,可是這種方式在實際使用中

是很是不方便的,假如說 key 是個很長的串呢??

var person = {
  'first name': '張三'
}

console.log(person['first name']) // 張三
複製代碼

+RESULTS:

張三
複製代碼

所以, es6 中支持了變量做爲對象屬性名去訪問,根據變量的值動態決定使用什麼 key 去訪問對象的屬性值,

這樣無論 key 多長,只須要使用變量將它存儲起來,直接使用變量名去使用將更加方便。

var person = {},
    lastName = "last name";

person["first name"] = "張三";
person[lastName] = "李四";

console.log(person["first name"]);      // "張三"
console.log(person[lastName]);          // "李四"
複製代碼

+RESULTS:

張三
李四
複製代碼

支持表達式計算屬性名:

var suffix = ' name'

var person = {
  ['first' + suffix]: '張三',
  ['last' + suffix]: '李四'
}

console.log(person['first name']) // 張三
console.log(person['last name']) // 李四
複製代碼

+RESULTS:

張三
李四
複製代碼

新方法

方法 功能 其餘
Object.is(value1, vlaue2) 比較兩個值是不是同一個值 能彌補 === 沒法判斷 (+0, -0), (NaN, NaN) 問題
Object.assign(target, ...sources) 合併拷貝對象屬性 自身且 enumerable: true 的屬性

Object.is(value1, value2)

在以往咱們判斷兩個值是否相等,常用的是 ===== ,通常推薦使用後者

由於前者會有隱式強轉,會在比較以前將兩個值進行強制轉換成同一個類型再比較。

console.log('' == false) // true
console.log(0 == false) // true
console.log(0 == '') // true
console.log(5 == '5') // true
console.log(-0 == +0) // true
console.log(NaN == NaN) // true
複製代碼

+RESULTS:

true
true
true
true
true
false
複製代碼

對於 +0-0 使用 === 的結果是 true ,但實際上他們是有符號的,理論上應該是不相等的。

而兩個 NaN 五路你是 ===== 都斷定他們是不相等的。

爲了解決這些差別, es6 中加入了 Object.is() 接口,意指將等式的判斷更加合理化,它的含義是

兩個值是不是同一個值。

咱們看下各對值使用 Object.is() 比較的結果:

const is = Object.is
const log = console.log

// +0, -0
log('+0 == -0', +0 == -0)
log('+0 === -0', +0 === -0)
log('+0 is -0: ', is(+0, -0))

// NaN
log('NaN == NaN: ', NaN == NaN)
log('NaN === NaN: ', NaN === NaN)
log('NaN is NaN: ', is(NaN, NaN))

// number, string
log('5 == "5": ', 5 == '5')
log('5 == 5: ', 5 == 5)
log('5 === "5": ', 5 === '5')
log('5 === 5: ', 5 === 5)
log('5 is "5": ', is(5, '5'))
log('5 is 5: ', is(5, 5))
複製代碼

+RESULTS:

+0 == -0 true
+0 === -0 true
+0 is -0:  false
NaN == NaN:  false
NaN === NaN:  false
NaN is NaN:  true
5 == "5":  true
5 == 5:  true
5 === "5":  false
5 === 5:  true
5 is "5":  false
5 is 5:  true
複製代碼

所以, Object.is 可以彌補, === 沒法判斷出 +0, -0, NaN, Nan 相等的結果。

Object.assign(target, source, source1, source2, …)

參數:

  1. target 接受拷貝的對象,也將返回這個對象
  2. source 拷貝內容的來源對象
  3. 來源對象參數能夠有多個,若是存在同名屬性值,最後的值由最後一個擁有同名屬性對象中的值爲準

TC39.ECMA262 實現原理圖:

img

合併對象,將 source 中自身的可枚舉的屬性淺拷貝到 target 對象中,返回 target 對象。

混合器(Mixins)在 JavaScript 中被普遍使用,在一個 mixin 中,一個對象能夠從另個對象中

接受他們的屬性和方法,即淺拷貝,許多 JavaScript 庫都會有一個與下面相似的 mixin 函數:

const mixin = (receiver, supplier) => {
  Object.keys(supplier).forEach(
    key => receiver[key] = supplier[key])

  return receiver
}

function EventTarget() {}

EventTarget.prototype = {
  constructor: EventTarget,
  get name() {
    return 'EventTarget.prototype'
  },
  emit: function(msg) {
    console.log(msg, 'in EventTarget.prototype')
  },
  on: function(msg) {
    console.log(msg, 'on EventTarget.prototype')
  }
}


const myObj1 = {}
mixin(myObj1, EventTarget.prototype)

myObj1.emit('something changed from myObj1')
console.log(myObj1.name, 'obj1 name')

const myObj2 = {}
Object.assign(myObj2, EventTarget.prototype)

myObj2.on('listen from myObj1')
console.log(myObj2.name, 'obj2 name')

console.log(EventTarget.prototype, myObj1, myObj2)
複製代碼

+RESULTS:

something changed from myObj1 in EventTarget.prototype
EventTarget.prototype obj1 name
listen from myObj1 on EventTarget.prototype
EventTarget.prototype obj2 name
複製代碼

因爲 mixin(), Object.assign 的實現都是採用的 = 操做符,所以是無法拷貝訪問器屬性的,或者說拷貝過來以後

就不會再是訪問器屬性了,看上面代碼的運行結果對比圖:

img

多個來源對象支持:

const receiver = {}
const res = Object.assign(receiver, {
  name: 'xxx',
  age: 100
}, {
  height: 180
}, {
  color: 'yellow',
  age: 80
})

console.log(receiver === res)
console.log(res)
複製代碼

+RESULTS:

true
{ name: 'xxx', age: 80, height: 180, color: 'yellow' }
複製代碼

最後 age: 80 值是最後一個來源對象中的值,返回值即第一個參數對象。

重複屬性

<= es5 嚴格模式下,重複屬性會出現語法錯誤:

'use strict';

var person = {
  name: 'xxx',
  name: 'yyy' // syntax error in es5 strict mode
}
複製代碼

es6 不管嚴格或非嚴格模式下都屬合法操做,其值爲最後一個指定的值:

'use strict';

var person = {
  name: 'xxx',
  name: 'yyy' // no error
}

console.log(person.name)
複製代碼

+RESULTS:

yyy
複製代碼

自有屬性枚舉順序

<= es5 中是不會定義對象屬性的枚舉順序的,它的枚舉順序是在實際運行時取決於所處的 JavaScript 引擎。

es6 中嚴格定義了枚舉時返回的屬性順序,這將會影響在使用 Objct.getOwnPropertyNames()

Reflect.ownKeys 時屬性該如何返回。a

枚舉時基本順序遵循:

  1. 全部數字類型的 keys 爲升序排序

  2. 全部字符串類型的 keys 按照它添加的時機排序

  3. 全部符號類型(Symbols)的 keys 按照它添加的時機排序

三者的優先級爲: numbers > strings > symbols

var obj = {
  a: 1,
  0: 1,
  c: 1,
  2: 1,
  b: 1,
  1: 1
}

obj.d = 1

console.log(Object.getOwnPropertyNames(obj).join('')) // 012acbd
複製代碼

+RESULTS:

012acbd
複製代碼

{% note warning %}
因爲並不是全部 JavaScript 引擎並不是統一實現方式,致使 for-in 循環依舊沒法肯定枚舉的順序。

而且 Object.keys()JSON.stringify() 採用的枚舉順序和 for-in 同樣。
{% endnote %}

var obj = {
  a: 1,
  0: 1,
  c: 1,
  2: 1,
  b: 1,
  1: 1
}

obj.d = 1

for (let prop in obj) {
  console.log(prop)
}
複製代碼

功能更強的原型對象

原型是 JavaScript 中實現繼承的基石,早起的版本中嚴重限制了原型能作的事情,

而後隨着 JavaScript 的逐漸成熟程序員們開始愈來愈依賴原型,咱們如今能很清晰

地感覺到開發者們對原型控制上和易用性的渴望愈來愈強烈,由此 ES6 對齊進行了增強。

改變對象原型

正常狀況下,對象經過構造函數或 Object.create() 建立的同時原型也就被建立了。

ES5 中能夠經過 Object.getPrototypeof() 方法去獲取對象原型,可是依然

缺乏一個標準的方式去獲取失利以後的對象原型。

ES6 增長了 Object.setPrototypeof(source, target) 用來改變對象的原型指向,

指將 source.prototype 指向 target 對象。

let person = {
  getGreeting() {
    return "Hello";
  }
};

let dog = {
  getGreeting() {
    return "Woof";
  }
};

// prototype is person
let friend = Object.create(person);
console.log(friend.getGreeting());                      // "Hello"
console.log(Object.getPrototypeOf(friend) === person);  // true

// set prototype to dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting());                      // "Woof"
console.log(Object.getPrototypeOf(friend) === dog);     // true
複製代碼

實際上,一個對象的原型是存儲在它的內部屬性 [[Prototype]] 上的, Object.getPrototypeOf()

獲取的也是這個屬性的值, Object.setPrototypeOf() 設置也是改變這個屬性的值。

舊版原型的訪問

好比:若是想在實例中重寫原型的某個方法的時候,須要在重寫的方法內調用原型方法時候,以往是這樣搞

let person = {
  getGreeting() {
    return "Hello";
  }
};

let dog = {
  getGreeting() {
    return "Woof";
  }
};


let friend = {
  getGreeting() {
    return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
  }
};

// set prototype to person
Object.setPrototypeOf(friend, person);
console.log(friend.getGreeting());                      // "Hello, hi!"
console.log(Object.getPrototypeOf(friend) === person);  // true

// set prototype to dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting());                      // "Woof, hi!"
console.log(Object.getPrototypeOf(friend) === dog);     // true
複製代碼

經過 Object.getPrototypeOf(this).getGreeting.call(this) … 去獲取原型中的方法

經過 super 引用簡化原型的訪問

如以前所提,原型是 JavaScript 中一個很重要也很經常使用的一個對象,ES6 對他們的使用進行了簡化。

另外 es6 對原型的另外一個改變是 super 的引用,這讓對象訪問原型對象更加方便。

而在 es6 增長 super 以後就變得異常簡潔了:

let friend = {
  getGreeting() {
    // in the previous example, this is the same as:
    // Object.getPrototypeOf(this).getGreeting.call(this)
    return super.getGreeting() + ", hi!";
  }
};
複製代碼

相似其餘語言的繼承, friend 是實例,它的原型是它的父類,在實例中的 super 實際上是指向父類的引用

所以能夠直接在子類中直接使用 super 去使用父類的方法。

只能在簡寫函數中訪問 super

可是 super 只能在對象的簡寫方法中使用,若是是使用 「function」 關鍵詞聲明的函數中使用會出現

syntax error

好比:下面的方式是非法的

let friend = {
  getGreeting: function() {
    // syntax error
    return super.getGreeting() + ", hi!";
  }
};
複製代碼

由於 super 在這種函數的上下文中中不存在的。

Object.getPrototypeOf() 並非全部場景都能使用的

由於 this 的指向是根據函數的執行上下文來決定了,所以使用 this 是徹底靠譜的。

好比:

let person = {
  getGreeting() {
    return "Hello";
  }
};

// prototype is person
let friend = {
  getGreeting() {
    return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
  }
};
Object.setPrototypeOf(friend, person);


// prototype is friend
let relative = Object.create(friend);

console.log(person.getGreeting());                  // "Hello"
console.log(friend.getGreeting());                  // "Hello, hi!"
console.log(relative.getGreeting());                // error!
複製代碼

上面的 relative.getGreeting()) 會報錯,緣由是 relative 自己是個新的變量,

這個變量指向由 Object.create(friend) 建立的一個空對象,其原型爲 friend

reletive.getGreeting() 的調用首先在 friend 中找但沒找到,最後在

friend 中找到了,也就是說它實際上調用的就是原型上的 getGreeting() 而後原型方法裏面

又是經過 this 去調用了原型的方法(也就自身),因爲 this 始終是根據當前上下文發生變化的,

此時它的指向是 friend ,最終會致使循環調用。

而用 super 就不會有上面的問題,由於 super 指向是固定的,就是指向當前對象的原型對象(父對象),即

這裏指向的是 person

super 引用的過程

通常狀況下是沒什麼區別的,可是在咱們作繼承或者獲取對象的原型的時候就頗有用了,由於 super 的指向是和

[[HomeObject]] 密切相關的, super 獲取指向的過程:

  1. 經過在當前方法的內部屬性 [[HomeObject]] 上面調用 Object.getPrototypeOf() 去獲取這個方法所在對象的原型對象;

  2. 在原型對象上搜與這個函數同名函數;

  3. 最後將這個同名函數綁定當前的 this 執行,而後執行這個函數。

let person = {
  getGreeting() {
    return "Hello";
  }
};

// prototype is person
let friend = {
  getGreeting() {
    return super.getGreeting() + ", hi!";
  }
};
Object.setPrototypeOf(friend, person);

console.log(friend.getGreeting());  // "Hello, hi!"
複製代碼

好比,上面的代碼

  1. person 設置爲 friend 的原型,成爲它的父對象

  2. 調用 friend.getGreeting() 執行以後在其內部使用 super.getGreeting() 這個一開始會

找到 friend.getGreeting 這個方法的 [[HomeObject]] 也就是 friend

  1. 而後根據扎到的 friend ,經過 Object.getPrototypeOf() ,去找到原型對象,即 person ,找到以後再去這裏面找同名函數 getGreeting

  2. 找到以後將該函數執行上下文綁定到 this (即 friend 所在的上下文)。

  3. 執行同名函數,此時這個雖是原型(person)上的函數,可是上下文已經被綁定到了 friend

過程簡單描述就是:

設置繼承
=> 重寫方法
=> super 調用父級方法
=> 找當前函數的 [[HomeObject]]
=> Object.getPrototypeOf([[HomeObject]]) 找原型
=> 找原型上同名函數
=> 綁定找到的同名函數到當前的 this
=> 執行同名函數

var person = {
  fnName: 'person',
  getName() {
    return this.fnName
  }
}
var child = {
  fnName: 'child',
  getName() {
    return super.getName() + ',' + this.fnName
  }
}

Object.setPrototypeOf(child, person)

console.log(child.getName()) // child child
複製代碼

方法定義

在 es6 以前是沒有「方法」這個詞的定義的,但在 es6 以後對方法的定義才正式有了規定。

函數和方法定義

在對象中的函數才叫作方法,非對象中的叫作函數,且 es6 給方法增長了一個 [[HomeObject]] 內置屬性,

它指向的是包含這個方法的那個對象。

好比:

let person = {
  // method
  getGreeting() {
    return 'xxx'
  }
}

// not method
function shareGreeting() {
  return 'yyy'
}
複製代碼

getGreeting 叫作方法,且其有個內部屬性 [[HomeObject]] 指向了 person 說明這個對象擁有它。

shareGreeting 叫作函數,不是方法

總結

更新內容

內容 示例/說明
屬性簡寫 {name, age} <=> {name: name, age: age}
計算屬性 { [first + 'name']: '張三' }, { ['first name']: '張三' }
簡寫方法 { getName() {} }
重複屬性名合法化 { age: 10, age: 100 } <=> { age: 100 }
Object.assign 合併對象 淺拷貝,內部 = 實現拷貝
Object.is 增強判斷,彌補 === 不能判斷 +0, -0NaN, NaN 問題
固定對象屬性枚舉順序 number > string > symbol, string 和 symbol 按照增長前後順序排列
Object.setPrototypeOf 可改變對象原型
super 指向原型對象,可經過它去訪問原型對象中的方法

數據解構

解構優點

在 es5 及以前若是咱們想要從對象中取出屬性的值,只能經過普通的賦值表達式來實現,

一個還好,若是是多個的話就會出現很重複的代碼,好比:

let options = {
  repeat: true,
  save: false
}

let repeat = options.repeat,
    save = options.save


// if more ???
複製代碼

上面只是取兩個對象的屬性,若是不少呢,十幾個二十幾個??

不只代碼量大,還不美觀。

所以 es6 加入瞭解構系統,讓這些操做變的很容易,很簡潔。

對象解構

對象解構的時候,等號右邊不能是 nullundefined ,這樣會報錯,這是由於,不管何時

去讀取 nullundefined 的屬性都會出發運行時錯誤。

聲明式解構

解構的同時聲明解構後賦值的變量:

let node = {
  type: 'Identifier',
  name: 'foo'
}

let { type, name } = node

console.log(type) // Identifier
console.log(name) // foo
複製代碼

在使用解構的過程當中必需要有右邊的初始值,而不能只是用來聲明變量,這是不合法的操做

好比:

// syntax error!
var { type, name };

// syntax error!
let { type, name };

// syntax error!
const { type, name };
複製代碼

先聲明後解構

有時候有些變量早已經存在了,只是後面咱們須要將它的值改變,也正好是須要從對象中去取值,

這個時候就是先聲明後解構:

let node = {
  type: "Identifier",
  name: "foo"
},
    // 這裏變量已經聲明好了
    type = "Literal",
    name = 5;

// assign different values using destructuring
({ type, name } = node);

console.log(type);      // "Identifier"
console.log(name);      // "foo"
複製代碼

這個時候必須用 () 將解構語句包起來,讓其成爲一個執行語句,若是不,左邊就至關於

一個塊級語句,然而塊級語句是不能出如今等式的左邊的。

在這基礎上,另外一種狀況是將 {type, name} = node 做爲參數傳遞給函數的時候,這個時候

傳遞給函數的參數其實就是 node 自己,例如:

let node = {
  type: "Identifier",
  name: "foo"
},
    type = "Literal",
    name = 5;

function outputInfo(value) {
  console.log(value === node);
}

outputInfo({ type, name } = node);        // true

console.log(type);      // "Identifier"
console.log(name);      // "foo"
複製代碼

解構默認值

在解構過程當中,可能左邊聲明的變量在右邊的對象中並不存在或者值爲 undefined 的時候,這個變量的值將會

賦值爲 undefined ,所以這個時候就須要針對這種狀況有個默認處理,即這裏的解構默認值。

let node = {
  type: "Identifier",
  name: "foo"
};

let { type, name, value } = node;

console.log(type);      // "Identifier"
console.log(name);      // "foo"
console.log(value);     // undefined
複製代碼

屬性值爲 undefined 的狀況:

let node = {
  type: "Identifier",
  name: "foo",
  value: undefined
};

let { type, name, value = 0 } = node;

console.log(type);      // "Identifier"
console.log(name);      // "foo"
console.log(value);     // 0
複製代碼

屬性變量重命名

解構出來以後,可能不想沿用右邊對象中的屬性名,所以須要將左邊的變量名稱重命名:

let node = {
  type: "Identifier",
  name: "foo"
};

let { type: localType, name: localName } = node;

console.log(localType);     // "Identifier"
console.log(localName);     // "foo"
複製代碼

重命名 + 默認值:

let node = {
  type: "Identifier",
  name: "foo"
};

let { type: localType, name: localName = 'xxx' } = node;

console.log(localType);     // "Identifier"
console.log(localName);     // "foo"
複製代碼

多級對象解構

右邊對象中的屬性的值不必定是普通類型,多是對象,或對象中包含對象,數組等等類型,次數能夠

使用內嵌對象解構來進行解構:

原則就是左邊的變量的結構要和右邊實際對象中的結構保持一致

let node = {
  type: "Identifier",
  name: "foo",
  loc: {
    start: {
      line: 1,
      column: 1
    },
    end: {
      line: 1,
      column: 4
    }
  }
};

let { loc: { start }} = node;

console.log(start.line);        // 1
console.log(start.column);      // 1
複製代碼

多層解構重命名:

let node = {
  type: "Identifier",
  name: "foo",
  loc: {
    start: {
      line: 1,
      column: 1
    },
    end: {
      line: 1,
      column: 4
    }
  }
};

// 重命名
let { loc: { start: localStart }} = node;

console.log(start.line);        // 1
console.log(start.column);      // 1

複製代碼

{% note info %}

語法陷阱

// no variables declared!
let { loc: {} } = node;
複製代碼

這種形式其實是沒任何做用的,由於左邊的 loc 只是起到了站位的做用,實際起做用的

是在 {} 裏面,可是裏面沒任何東西,也就是說這個不會解構出任何東西,也不會產生任何新的變量。

{% endnote %}

數組解構

數組解構和對象解構用法基本是同樣的,無非就是講 {} 改爲數組的 [] ,和對象同樣,右邊不能夠是 nullundefined

表達式 結果 說明
let [first, second] = [1, 2] first = 1, first = 2 普通解構
let [ , , third] = [1, 2, 3] third = 3 空置解構,只指定某個位置解構
let first = 1, second = 2 => [first, second] = [11, 22] first = 11, second = 22 先聲明再解構
let a = 1, b = 2 => [a, b] = [b, a] a = 2, b = 1 替換值快捷方式
let [a = 1, b] = [11, 22] a = 11, b = 22 默認值
let [a = 1, b] = [, 22] a = 1, b = 22 默認值
let [a, b = 2] = [ 1 ] a = 1, b = 2 默認值
let [a, [b]] = [1, [2]] a = 1, b = 2 嵌套解構
let [a, [b]] = [1, [2, 3], 4] a = 1, b = 2 嵌套解構
let [a, [b], c] = [1, [2, 3], 4] a = 1, b = 2, c = 4 複雜解構
let [a, ...bs] = [1, 2, 3, 4, 5] a = 1, bs = [2, 3, 4, 5] rest 符號解構
[1, 2, 3].concat() => [1, 2, 3] => es6: [...as] = [1, 2, 3] as = [1, 2, 3] 克隆數組

混合解構

混合解構意味着被解構的對象中可能既包含對象由包含數組,也是按照對象和數組的解構原理進行解構就OK。

let node = {
  type: "Identifier",
  name: "foo",
  loc: {
    start: {
      line: 1,
      column: 1
    },
    end: {
      line: 1,
      column: 4
    }
  },
  range: [0, 3]
};

let {
  loc: { start },
  range: [ startIndex ]
} = node;

console.log(start.line);        // 1
console.log(start.column);      // 1
console.log(startIndex);        // 0
複製代碼

參數解構

參數解構,即函數在聲明的時候,參數是採用解構等式左邊的形式書寫,這種就須要要求在調用的時候

這個參數位置必須有個非 null 和 Undefined 值,不然會報錯,緣由同樣解構時候沒法從 null 或 undefined 讀取屬性。

被解構的參數屬性列表

實例:

function setCookie(name, value, { secure, path, domain, expires }) {

  // code to set the cookie
}

setCookie("type", "js", {
  secure: true,
  expires: 60000
})
複製代碼

不傳值得非法操做:

// Error!
setCookie("type", "js");
複製代碼

這樣第三個參數就是 undefined 報錯。

優化參數解構寫法有兩種:

  1. 函數體內解構
  2. 解構體默認值方式(推薦)

函數體內解構:

function setCookie(name, value, options) {

  // 函數體內解構,給個默認值 || {} ,或者在參數那裏這樣: (name, value, options = {})
  let { secure, path, domain, expires } = options || {};

  // code to set the cookie
}
複製代碼

或者:

function setCookie(name, value, options = {}) {

  let { secure, path, domain, expires } = options;

  // code to set the cookie
}

複製代碼

直接參數解構體給默認值:

function setCookie(name, value, { secure, path, domain, expires } = {}) {

  // ...
}
複製代碼

默認值,若是不傳第三個參數,那麼它的默認值就是 {} 避免解構出錯。

解構的參數默認值

和普通對象同樣,解構出來的參數咱們還能夠給他們一個默認值:

function setCookie(name, value, { secure = false, path = "/", domain = "example.com", expires = new Date(Date.now() + 360000000) } = {}
                  ) {

  // ...
}
複製代碼
  1. 第三個參數沒傳,四個參數都取默認值
  2. 第三個參數有傳遞,根據普通對象定義解構

總結

  1. 對象,先聲明再解構,表達式必須用 () 包起來,做爲表達式執行
  2. 對象數組解構均可以給默認值,重命名,多層解構,混合解構
  3. 解構遵循左側最內層的變量聲明,若是左側最內層無任何變量,則解構表達式無任何意義
  4. 參數解構,要麼給當前參數默認值,要麼保證調用時該參數都有傳入非 nullundefined 的值,推薦參數默認值

符號和符號屬性(Symbols)

符號類型值(Symbol())是 es6 新增的一種原始數據類型和 strings, numbers, booleans, nullundefined 屬於原始值類型。

它至關於數字的 42 或字符串的 "hello" 同樣,只是單穿的一些值,所以不能對其使用 new Symbol() 不然會報錯。

img

符號類型是做爲一種建立私有對象成員的類型,在 es6 以前是沒有什麼方法能夠區分普通屬性和私有屬性的。

建立符號

符號類型會建立一個包含惟一值得符號變量,這些變量是沒有實際字面量表示的,也就是說一旦符號變量建立以後,只能經過這個變量

去訪問你所建立的這個符號類型。

建立符號

經過 Symbol([ description ]) 來建立符號,建立過程:

  1. 若是 descriptionundefined, 讓 descString = undefined
  2. 不然 descString = ToString(description)
  3. 讓內部值 [[Description]]descString
  4. 返回一個惟一的 Symbol 值
let firstName = Symbol();
let secondName = Symbol();
let person = {};

person[firstName] = "Nicholas";
console.log(person[firstName]);     // "Nicholas"

console.log(firstName)
console.log(secondName)
console.log(firstName == secondName)
console.log(firstName === secondName)
console.log(Object.is(firstName, secondName))
複製代碼

+RESULTS:

Nicholas
Symbol()
Symbol()
false
false
false
複製代碼

firstName 是存放了一個惟一值得符號類型變量,而且用來做爲 person 對象的一個屬性使用。

所以,若是要訪問對象中的對應的這個屬性的值,每次都必須使用 firstName 這個符號變量去訪問。

{% note info %}
若是須要實在須要符號類型對象,能夠經過 new Object(Symbol()) 去建立一個對象,而不能

直接 new Symbol() 由於 Symbol() 獲得的是一個原始值,就像你不能直接 new 42 一個道理。

img

{% endnote %}

帶參數的 Symbol(arg)

有時候可能須要對建立的符號作一些簡單的區分,或者讓其更加語義化,能夠在建立的時候給 Symbol() 函數

一個參數,參數自己並無實際的用途,可是有利於代碼調試。

let firstName = Symbol("first name");
let person = {};

person[firstName] = "Nicholas";

console.log("first name" in person);        // false
console.log(person[firstName]);             // "Nicholas"
console.log(firstName);                     // "Symbol(first name)"
console.log(firstName.description) // undefined
console.log(Symbol('xxx').description) // undefined
複製代碼

+RESULTS:

false
Nicholas
Symbol(first name)
undefined
undefined
複製代碼

如輸出,參數會一併輸出,所以推薦使用的時候加上參數,這樣在調試的時候你就能區分開哪一個符號

來自哪裏,而不至於輸出都是 Symbol() 沒法區分。

參數做爲符號的一種描述性質特徵被儲存在了內部 [[Description]] 屬性中,這個屬性會在對符號調用 toString()

(隱式或顯示調用)的時候去讀取它的值,除了這個沒有其餘方法能夠直接去訪問 [[Description]]

符號類型檢測(typeof)

因爲符號屬於原始值,所以能夠直接經過 typeof 就能夠去判斷變量是否是符號類型,es6 對 typeof 進行了擴展,

若是是符號類型檢測的結果值是「symbol」

let symbol = Symbol("test symbol")

console.log(typeof symbol) // "symbol"
複製代碼

+RESULTS:

symbol
複製代碼

使用符號

以前的例子中使用變量做爲對象屬性名的,均可以使用符號來替代,而且還能夠對符號類型的屬性

進行定製,讓其變成只讀的。

// 建立符號,惟一
let firstName = Symbol('first name')

let person = {
  // 直接當作計算屬性使用
  [firstName]: '張三'
}

// 讓屬性只讀
Object.defineProperty(person, firstName, { writable: false })

let lastName = Symbol('last name')

Object.defineProperties(person, {
  [lastName]: {
    value: '李四',
    writable: false
  }
})

console.log(person[firstName])
console.log(person[lastName])
複製代碼

+RESULTS:

張三
李四
複製代碼

分享符號

在使用過程當中咱們須要考慮一個問題:

假設某個地方聲明瞭一個符號類型及一個使用了這個符號做爲屬性 key 的對象,哪天

若是我想在其餘地方去使用它,該怎麼辦??

現在模塊化獲得普及,如今常常都是一個文件一個模塊,用的時候導入這個文件獲得相應的對象

但因爲符號值是惟一的,那外部模塊又怎麼知道另外一個模塊內部用了怎樣的符號值做爲對象??

這就是下面要講的「符號分享」問題。

{% note warn %}
全局符號註冊表(Global Symbol Registry) 會在全部代碼執行以前就建立好,且列表爲空。

它和全局對象同樣屬於環境變量,所以不要去假設它是什麼或它不存在之類的,所以它在全部代碼執行以前

就建立好了,因此它是確確實實存在的。
{% endnote %}

Symbol.for()

在以前咱們經過 let firstName = Symbol('first name'); 來建立一個符號變量,可是在使用的時候必須的用

firstName 去使用這個變量,而如今咱們想將符號分享出去須要用到 Symbol.for()

Symbol.for(description) 會針對 description 去建立一個惟一的符號值:

let uid = Symbol.for("uid");
let object = {};

object[uid] = "12345";

console.log(object[uid]);       // "12345"
console.log(uid);               // "Symbol(uid)"
複製代碼

Symbol.for(desc) 在第一次調用的時候,首先會去「全局符號註冊表(global symbol registry)」 中去查找

這個 desc 對應的符號值,找到了就返回這個符號值,若是沒找到會建立一個新的符號值而且將它註冊到全局符號註冊表中,

供下次調用時使用。

-—

Symbol.for(key) 內部實現步驟(僞代碼):

Symbol.for = function (key) {

  // 1 key 轉字符串
  let stringKey = ToString(key);

  // 2. 遍歷 GlobalSymbolRegistryList 註冊表
  for (let e in GlobalSymbolRegistryList) {
    // 符號值已經存在
    if (SameValue(e.[[Key]], stringKey)) {
      return e.[[Symbol]];
    }
  }

  // 3. 註冊表中不含 `stringKey` 的符號值,則建立新的符號值
  // 3.1 新建符號值
  let newSymbol = Symbol(stringKey);
  // 3.1 給 [[Description]] 賦值
  newSymbol.[[Description]] = stringKey;

  // 4. 註冊到符號註冊表中去
  GlobalSymbolRegistryList.push({
    [[Key]]: stringKey,
    [[Symbol]]: newSymbol
  });

  // 5. 返回新建的符號值
  return newSymbol;

}
複製代碼

總結起來爲3個步驟: 查找 -> 新建 -> 註冊

註冊表中的每一個符號片斷是以對象形式存在(對象中包含 KeySymbol 兩個屬性分別表示建立時的描述和符號值)。

使用分享符號

在上一節7.3.1 中咱們描述過了用來建立分享符號的 Symbol.for(desc) 接口,這裏將探討如何具體使用它來分享符號值。

let uid = Symbol.for("uid");
let object = {
  [uid]: "12345"
};

console.log(object[uid]);       // "12345"
console.log(uid);               // "Symbol(uid)"

let uid2 = Symbol.for("uid");

console.log(uid === uid2);      // true
console.log(object[uid2]);      // "12345"
console.log(uid2);              // "Symbol(uid)
複製代碼

在當前代碼運行的全局做用域中均可以分享到一份 Symbol.for("uid") 符號,只須要調用它就能夠拿到那個

惟一的值。

好比:

function createObj1() {
  let uid = Symbol.for("uid");
  let object = {
    [uid]: "12345"
  };

  return object
}

function createObj2() {
  let uid = Symbol.for("uid");
  let object = {
    [uid]: "67890"
  };

  return object
}


let uid1 = Symbol.for("uid");
const obj1 = createObj1()

let uid2 = Symbol.for("uid");
const obj2 = createObj2()

console.log(uid1 === uid2);
console.log(obj1[uid1]);
console.log(obj1[uid2]);
console.log(obj2[uid1]);
console.log(obj2[uid2]);

複製代碼

+RESULTS:

true
12345
12345
67890
67890
複製代碼

Symbol.keyFor(symbolValue)

咱們若是想建立或獲取全局註冊表中的符號是能夠經過 7.3.1 中的 Symbol.for(key) ,可是

若是咱們只知道一個符號值變量的狀況下,使用 Symbol.for(key) 就無法從註冊表中取值了。

所以,這裏將介紹如何使用 Symbol.keyFor(symbolValue) 去根據符號變量查找註冊表中的值。

在這以前須要知道

  1. Symbol.for(key) 建立的符號纔會進入全局註冊表
  2. Symbol() 直接建立的是不會加入全局註冊表的

也就有了下面的代碼及結果:

let uid = Symbol.for("uid");
console.log(Symbol.keyFor(uid));    // "uid"

let uid2 = Symbol.for("uid");
console.log(Symbol.keyFor(uid2));   // "uid"

let uid3 = Symbol("uid");
console.log(Symbol.keyFor(uid3));   // undefined
複製代碼

+RESULTS:

uid
uid
undefined
複製代碼

所以 Symbol("uid"); 結果不會加入註冊表,所以結果是 undefined

符號強制轉換

在 JavaScript 中類型強制轉換是常常會被用到的一個特性,也讓 JavaScript 使用起來會很靈活地能夠將一個

數據類型轉成另外一種數據類型。

可是符號類型不支持強制轉換。

let uid = Symbol.for("uid")

console.log(uid) // Symbol(uid)

// 在輸出的時候其實是調用了 uid.toString()
複製代碼

+RESULTS:

Symbol(uid)
複製代碼

當咱們將符號變量加入計算或字符串操做時會報錯,由於兩個不一樣類型的值進行操做會發生隱式轉換,可是符號類型不支持強轉

的,所以會報異常。

let uid = Symbol.for('uid'),
    desc = '',
    sum = 0

try {
  desc = uid + ""
} catch (e) {
  console.log(e.message)
}

try {
  sum = uid / 1
} catch (e) {
  console.log(e.message)
}
複製代碼

+RESULTS: 異常信息

Cannot convert a Symbol value to a string
Cannot convert a Symbol value to a number
複製代碼

獲取對象符號屬性

獲取對象屬性的方法:

  1. Object.keys() 會獲取全部可枚舉的屬性
  2. Object.getOwnPropertyNames() 獲取全部屬性,忽略可枚舉性

可是爲了兼容 es5 及之前的版本,他們都不會去獲取符號屬性,所以須要使用 Object.getOwnPropertySymbols()

去單獨獲取對象全部的符號屬性,返回一個包含全部符號屬性的數組。

let uid = Symbol.for("uid");
let object = {
  [uid]: "12345",
  [Symbol.for("uid2")]: "67890"
};

let symbols = Object.getOwnPropertySymbols(object);

console.log(symbols.length);        // 1
console.log(symbols[0]);            // "Symbol(uid)"
console.log(object[symbols[0]]);    // "12345"
複製代碼

+RESULTS:

2
Symbol(uid)
12345
複製代碼

符號內部操做(方法)

在 es6 中 JavaScript 的許多特性中其內部的實現都是使用到了符號內部方法。

好比下表涉及到的內容

符號方法 類型 JavaScript 特性 描述
Symbol.hasInstance boolean instanceof 7.6.1 實例(原型鏈)檢測
Symbol.isConcatSpreadable boolean Array.prototype.concat 7.6.2 檢測參數合法性
Symbol.iterator function 調用後獲得迭代器 遍歷對象或數組(等可迭代的對象)的時候會用到
Symbol.asyncIterator function 調用後獲得異步迭代器(返回一個 Promise ) 遍歷對象或數組(等可迭代的對象)的時候會用到
Symbol.match function String.prototype.match 7.6.3 正則表達式對象內部屬性
Symbol.matchAll function String.prototype.matchAll 7.6.3 正則表達式對象內部屬性
Symbol.replace function String.prototype.replace 7.6.3 正則表達式對象內部屬性
Symbol.search function String.prototype.search 7.6.3 正則表達式對象內部屬性
Symbol.split function String.prototype.split 7.6.3 正則表達式對象內部屬性
Symbol.species constructor - 派生對象生成
Symbol.toPrimitive function - 7.6.4 返回一個對象的原始值
Symbol.toStringTag string Object.prototype.toString() 7.6.5 返回一個對象的字符串描述
Symbol.unscopables object with 7.6.8 不能出如今 with 語句中的一個對象

{% note info %}
經過改變對象的上面的內部符號屬性的實現,可讓咱們去修改對象的一些

默認行爲,好比 instanceof 一個對象的時候能夠改變它的行爲讓它返回一個非預期值。
{% endnote %}

Symbol.hasInstance

每一個函數都有一個內部 Symbol.hasInstance 方法用來判斷給定的對象是否是這個函數的一個實例。

這個函數定義在 Function.prototype 上,所以全部的函數都會繼承 instanceof 屬性的默認行爲,

而且這個方法是 nonwritable, nonconfigurable, 和 nonenumerable 的,確保它不會被錯誤的

重寫。

所以下面的中的兩句 obj instanceof ArrayArray[Symbol.hasInstance](obj) 是等價的。

const obj = {}

let v1 = obj instanceof Array;

// 等價於

let v2 = Array[Symbol.hasInstance](obj);

console.log(v1, v2)
複製代碼

+RESULTS:

false false
複製代碼

在 es6 中實際上已經對 instanceof 操做作了重定義,其內部還讓它支持了函數調用方式,即

其內部的 Symbol.hasInstance 再也不限定只是 boolean 類型,它還能夠是函數類型,所以

咱們能夠經過重寫這個方法來改變 instanceof 的默認行爲。

好比:讓一個對象的 instanceof 操做老是返回 false

function MyObj() {
  // ...
}

Object.defineProperty(MyObj, Symbol.hasInstance, {
  value: function(v) {
    console.log('override method')
    return false;
  }
})

let obj = new MyObj();

console.log(obj instanceof MyObj); // false
複製代碼

+RESULTS:

override method
false
複製代碼

因爲 Symbol.hasInstance 屬性是 nonwritable 的所以須要經過 Object.defineProperty

去從新定義這個屬性。

{% note warn %}
雖然 es6 賦予了這種能夠重寫一些 JavaScript 特性的默認行爲的能力,可是依舊不推薦

去這麼作,極可能讓你的代碼變得很不可控,也不容易讓人理解你的代碼。
{% endnote %}

Symbol.isConcatSpreadable

對應着 Array.prototype.concat 的內部使用 Symbol.isConcatSpreadable

concat 使用示例:

let colors1 = [ "red", "green" ],
    colors2 = colors1.concat([ "blue", "black" ]);

console.log(colors2.length);    // 4
console.log(colors2);           // ["red","green","blue","black"]
複製代碼

+RESULTS:

4
[ 'red', 'green', 'blue', 'black' ]
複製代碼

咱們通常用 concat 去擴展一個數組,把他們合併到一個新的數組中去。

根據 Array.prototype.concat(value1, ...valueNs) 的定義,它是能夠接受 n 多個參數的,好比:

[].concat(1, 2, 3, ...) > =[1, 2, 3, ...]

而且並無限定參數的類型,即這些 value1, ...valuesNs 能夠是任意類型的值(數組,對象,純值等等)。

另外,若是參數是數組的話,它會將數組項一一展開合併到源數組中區(且只會作一級展開,數組中的數組不會展開)。

好比:

let colors1 = [ "red", "green" ],
    colors2 = colors1.concat(
      [ "blue", "black", [ "white" ] ], "brown", { color: "red" });

console.log(colors1 === colors2)
console.log(colors2.length);    // 5
console.log(colors2);           // ["red","green","blue","black","brown"]
複製代碼

+RESULTS:

false
7
[ 'red',
  'green',
  'blue',
  'black',
  [ 'white' ],
  'brown',
  { color: 'red' } ]
複製代碼

可是,若是咱們須要的是將 { color: 'red' } 中的屬性值 'red' 合併到數組末尾,該如何作??

->>> Symbol.isConcatSpreadable 就是它

和其餘內置符號不同,這個在全部的對象中默認是不存在的,所以若是咱們須要就得手動去添加,讓這個對象

變成 concatable 只須要將這個屬性值置爲 true 便可:

let collection = {
  0: 'aaa',
  '1': 'bbb',
  length: 2,
  [Symbol.isConcatSpreadable]: true
}

let objNoLength = {
  0: 'xxx',
  1: 'yyy',
  [Symbol.isConcatSpreadable]: true
}


let objNoNumberAttrs = {
  a: 'www',
  b: 'vvv',
  length: 2,
  [Symbol.isConcatSpreadable]: true
}

let words = [ 'somthing' ];

console.log(words.concat(collection).toString())
console.log(words.concat(objNoLength).toString())
console.log(words.concat(objNoNumberAttrs).toString())
複製代碼

+RESULTS:

somthing,aaa,bbb
somthing
somthing,,
複製代碼

分析結果得出,對象要變的能夠被 Array.prototype.concat 使用,

須要知足如下條件:

  1. 必須有 length 屬性,不然對結果沒任何影響,如結果第二行輸出: somthing
  2. 必須有以數字爲 key 的屬性,不然數組中將使用空值代替追加的值追加到數組中去,如第三行輸出: somthing,,
  3. 必須增長符號屬性 Symbol.isConcatSpreadable 且值爲 true

同理,咱們能夠將數組對象的 Symbol.isConcatSpreadable 符號屬性置爲 false 來阻止數組的 concatable 行爲。

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

和字符串,正則表達式有關的一些符號,對應着字符串和正則表達式的方法:

  • match(regex) 字符串是否匹配正則
  • replace(regex, replacement) 字符串替換
  • search(regex) 字符串搜索
  • split(regex) 字符串切割

這些都須要用到正則表達式 regex

在 es6 以前這些方法與正則表達式的交互過程對於開發者而已都是隱藏了其內部細節的,也就是

說開發者沒法經過本身定義的對象去表示一個正則。

在 es6 中定義了四個符號即是用來實現 RegExp 內部實現對象,便可以經過對象的方式去

實現一個正則表達式規則。

這四個符號屬性是在 RegExp.prototype 原型上被定義的,做爲以上方法的默認實現。

{% note info %}
意思就是 math, replace, search, split 這四個方法的 regex 正則

表達式的內部實現基於對應的四個符號屬性函數 Symbol.math, Symbol.replace,

Symbol.search, Symbol.split
{% endnote %}

  • Symbol.match 接受一個字符串參數,若是匹配會返回一個匹配的數組,未匹配返回 null
  • Symbol.replace 接受一個字符串參數和一個用來替換的字符串,返回一個新的字符串。
  • Symbol.search 接受一個字符串,返回匹配到的數字因此呢,未匹配返回 -1。
  • Symbol.split 接受一個字符串,返回以匹配到的字符串位置分割成的一個字符串數組
// 等價於 /^.${10}$/
let hasLengthOf10 = {
  [Symbol.match]: function(value) {
    return value.length === 10 ? [value] : null
  },

  [Symbol.replace]: function(value, replacement) {
    return value.length === 10 ? replacement : value
  },

  [Symbol.search]: function(value) {
    return value.length === 10 ? 0 : -1
  },

  [Symbol.split]: function(value) {
    return value.length === 10 ? ["", ""] : [value]
  }
}

let msg1 = "Hello World", // 11 chars
    msg2 = "Hello John"; // 10 chars


let m1 = msg1.match(hasLengthOf10)
let m2 = msg2.match(hasLengthOf10)

console.log(m1)
console.log(m2)

let r1 = msg1.replace(hasLengthOf10, "Howdy!")
let r2 = msg2.replace(hasLengthOf10, "Howdy!")

console.log(r1)
console.log(r2)


let s1 = msg1.search(hasLengthOf10)
let s2 = msg2.search(hasLengthOf10)

console.log(s1)
console.log(s2)

let sp1 = msg1.split(hasLengthOf10)
let sp2 = msg2.split(hasLengthOf10)

console.log(sp1)
console.log(sp2)
複製代碼

+RESULTS:

null
[ 'Hello John' ]
Hello World
Howdy!
-1
0
[ 'Hello World' ]
[ '', '' ]
複製代碼

經過這幾個正則對象的內部符號屬性,使得咱們有能力根據須要去完成更復雜的正則匹配規則。

Symbol.toPrimitive

在 es6 以前,若是咱們要使用 == 去比較兩個對象的時候,其內部都會講對象轉成原始值以後再去比較,

且此時的轉換屬於內部操做,咱們是沒法知曉更沒法干涉的。

但在 es6 出現以後,這種內部實現經過 Symbol.toPrimitvie 被暴露出來了,從而使得咱們有能力取

改變他們的默認行爲。

Symbol.toPrimitvie 是定義在全部的標準類型對象的原型之上,用來描述在對象被轉換成原始值以前的

都作了些什麼行爲。

當一個對象發生原始值轉換的時候, Symbol.toPrimitive 就會帶上一個參數(hint)被調用,這個參數值爲

"number", "string", "default" 中的一個(值是由 JavaScript 引擎所決定的),分別表示:

  1. "number" :表示 Symbol.toPrimitive 應該返回一個數字。
  2. "string" :表示 Symbol.toPrimitvie 應該返回一個字符串。
  3. "default" : 表示原樣返回。

在大部分的標準對象中, number 模式的行爲按照如下的優先級來返回:

  1. 先調用 valueOf() 若是結果是一個原始值,返回它。
  2. 而後調用 toString() 若是結果是一個原始值,返回它。
  3. 不然,拋出異常。

一樣, string 模式的行爲優先級以下:

  1. 先調用 toString() 若是結果是一個原始值,返回它。
  2. 而後調用 valueOf() 若是結果是一個原始值,返回它。
  3. 不然,拋出異常。

在此,能夠經過重寫 Symbol.toPrimitive 方法,能夠改變以上的默認行爲。

{% note info %}
"default" 模式僅在使用 ==, + 操做符,以及調用 Date 構造函數的時候

只傳遞一個參數的時候纔會用到。大部分的操做都是採用的 "number" 或 "string" 模式。
{% endnote %}

實例:

function Temperature(degrees) {
  this.degrees = degrees
}

let freezing = new Temperature(32)

console.log(freezing + "!") // [object Object]!
console.log(freezing / 2) // NaN
console.log(String(freezing)) // [object Object]
複製代碼

輸出結果:

img

由於默認狀況下一個對象字符串化以後會變成 [object Object] 這是其內部的默認行爲。

經過重寫原型上的 Symbol.toPrimitive 函數能夠改寫這種默認行爲。

好比:

function Temperature(degrees) {
  this.degrees = degrees
}

Temperature.prototype[Symbol.toPrimitive] = function(hint) {
  switch (hint) {
  case 'string':
    return this.degrees + '\u00b0'
  case 'number':
    return this.degrees
  case 'default':
    return this.degrees + " degrees"
  }
}

let freezing = new Temperature(32)

console.log(freezing + "!")
console.log(freezing / 2)
console.log(String(freezing))
複製代碼

+RESULTS:

32 degrees!
16
32°
複製代碼

結果就像咱們以前分析的, 只有 ==+ 執行的是 「default" 模式,

其餘狀況執行的要麼是 "number" 模式(如: freezing / 2)

要麼是 "string" 模式(如: String(freezing))

Symbol.toStringTag 介紹

在 JavaScript 的一個有趣的問題是,能同時擁有多個全局執行上下文的能力。

這個發生在 web 瀏覽器環境下,一個頁面可能包含一個 iframe ,所以當前頁面和這個 iframe 各自

都擁有本身的執行環節。

一般狀況下,這並非什麼問題,由於數據能夠經過一些手段讓其它當前頁和 iframe 之間進行傳遞,

問題是如何去識別這個被傳遞的對象是源自哪一個執行環境??

好比,一個典型的問題是在 pageiframe 之間互相傳遞一個數組。在 es6 的術語中, 頁面和

iframe 每個都表明着一個不一樣的領域(realm, JavaScript 執行環境)。每一個領域都有它本身的全局

做用域包含了它本身的一份全局對象的副本。

不管,數組在哪一個領域被建立,它都很明確的是一個數組對象,當它被傳遞到另外一個領域的時候,使用

instanceof Array 的結果都是 false ,由於數組是經過構造函數在別的領域所建立的,而

Array 表明的僅僅是當前領域下的構造函數,即兩個領域下的 Array 不是一回事。

這就形成了在當前領域下去判斷另外一個領域下的一個數組變量是否是數組,獲得的結果將是 false

Symbol.toStringTag 延伸(不一樣 realm 下的對象識別)

對象識別的應對之策(Object.prototype.toString.call(obj))

function isArray(value) {
  return Object.prototype.toString.call(value) === "[object Array]";
}

console.log(isArray([]));   // true
複製代碼

+RESULTS:

true
複製代碼

這種方式雖然比較麻煩,可是倒是最靠譜的方法。

由於每一個類型的 toString() 可能有本身的實現,返回的值是沒法統一的,可是 Object.prototype.toString

返回的內容始終是 [object Array] 這種,後面是被檢測數據表明的類型的構造函數,它老是能獲得正確且精確的

結果。

Object.prototype.toString 內部實現的僞代碼:

// toString(object)

function toString(obj) {
  // 1. 判斷 undefined 和 null
  if (this === undefined) {
    return '[object Undefined]';
  }

  if (this === null) {
    return '[object Null]';
  }

  let O = ToObject(this); // 上下文變量對象化
  let isArray = IsArray(O); // 先判斷是否是數組類型
  let builtinTag = ''

  let has = builtinName => !!O.builtinName;

  // 2. 根據內置屬性,檢測各對象的類型
  if (isArray === true) { // 數組類型
    builtinTag = 'Array';
  } else if ( has([[ParameterMap]]) ) { // 參數列表,函數參數對象
    // 函數的參數 arguments 對象
    builtinTag = 'Arguments';
  } else if ( has([[Call]]) ) { // 函數
    builtinTag = 'Function';
  } else if ( has([[ErrorData]]) ) { // Error對象
    builtinTag = 'Error';
  } else if ( has([[BooleanData]]) ) { // Boolean 布爾對象
    builtinTag = 'Boolean';
  } else if ( has([[StringData]]) ) { // String 對象
    builtinTag = 'String';
  } else if ( has([[DateValue]]) ) { // Date 對象
    builtinTag = 'Date';
  } else if ( has([[RegExpMatcher]]) ) { // RegExp 正則對象
    builtinTag = 'RegExp';
  } else {
    builtinTag = 'Object' // 其餘
  }

  // 3. 最後檢測 @@toStringTag - Symbol.toStringTag 的值
  let tag = Get(O, @@toStringTag);

  if (Type(tag) !== 'string') {
    tag = builtinTag;
  }

  return `[object ${tag}]`;
}

複製代碼

從僞代碼中咱們知道,最後的實現中使用到了 @@toStringTag 即對應這裏的 Symbol.toStringTag 屬性值,

而且這個放在最後判斷,優先級最高,即若是咱們重寫了 Symbol.toStringTag 那麼重寫以後的返回值將

最優先返回。

Symbol.toStringTag 的 ES6 實現

正如 7.6.6 中的僞代碼所示,在 es6 中對於 Object.prototype.toString.call(obj)

的實現中加入了 @@toStringTag 內部屬性的檢測,即對應着這裏的 Symbol.toStringTag ,那麼咱們便

能夠經過改變這個值來修改它的默認行爲,從而獲得咱們想要的類型值。

好比:咱們有一個 Person 構造函數,咱們但願在使用 toString() 的時候獲得結果是 [object Person]

function Person(name) {
  this.name = name
}

Person.prototype[Symbol.toStringTag] = 'Person'

let me = new Person('xxx')

Person.prototype.toString = () => '[object Test]'

console.log(me.toString()) // [object Person]
console.log(Object.prototype.toString.call(me)) // [object Person]
console.log(me.toString === Object.prototype.toString) // true

複製代碼

+RESULTS: 未重寫 Person.prototype.toString 結果

[object Person]
[object Person]
true
複製代碼

+RESULTS: 重寫 Person.prototype.toString 的結果

[object Test]
[object Person]
false
複製代碼

咱們發現就算重寫了 Person.prototype.toString 也不會影響 Symbol.toStringTag 賦值後的運行結果,

如後面調用 Object.prototype.toString.call(me) 結果依舊是 [object Person]

由於咱們重寫了 Symbol.toStringTag 屬性值,所以7.6.6實現部分:

// 3. 最後檢測 @@toStringTag - Symbol.toStringTag 的值
let tag = Get(O, @@toStringTag); // 這裏的結果就成了 'Person'

if (Type(tag) !== 'string') {
  tag = builtinTag;
}

return `[object ${tag}]`
複製代碼

所以獲得 [object Person] 返回結果。

咱們還能夠經過重寫 Person 自身的 toString() 的實現讓其擁有本身的默認行爲,上面的第三行

結果代表 me.toString() 最終調用的是 Object.prototype.toString

Symbol.unscopables

with 語句在 JavaScript 世界中是最具爭議的一項特性之一。

本來設計的初衷是避免重複書寫同樣的代碼,可是在實際使用過程當中,倒是讓代碼更難理解,很容易出錯,

也有性能上的影響。

雖然,極力不推薦使用它,可是在 es6 中爲了考慮向後兼容性問題,在非嚴格模式下依舊對它作了支持。

好比:

let values = [1, 2, 3],
    colors = ["red", "green"],
    color = "black";

with(colors) {
  push(color);
  push(...values);
}

console.log(colors.toString())
複製代碼

+RESULTS:

red,green,black,1,2,3
複製代碼

上面代碼,在 with 裏面調用的兩次 push 等價於 colors.push 調用,

由於 with 將本地執行上下文綁定到了 colors 上。

values, color 指向的均是在 with 語句外面建立的 valuescolor

可是在 ES6 中給數組增長了一個 values 方法,這個方法會返回當前數組的迭代器對象: Array Iterator {}

這就意味着在 ES6 的環境中, values 指向的將是數組自己的 values() 方法而不是外面聲明的

values = [1, 2, 3] 這個數組,將破壞整個代碼的運行。

這就是 Symbol.unscopables 存在的緣由。

Symbol.unscopables 被用在 Array.prototype 上用來指定那些屬性不能在 with 中建立綁定:

// built into ECMAScript 6 by default
Array.prototype[Symbol.unscopables] = Object.assign(Object.create(null), {
  copyWithin: true,
  entries: true,
  fill: true,
  find: true,
  findIndex: true,
  keys: true,
  values: true
});

複製代碼

上面是默認狀況下 ES6 內置的設定,即數組中的上列屬性不容許在 with 中建立綁定,從列表能發現這些被

置爲 true 的屬性都是 es6 中新贈的方法,這主要是爲了兼容之前的代碼只針對新增的屬性這麼使用。

{% note warn %}
通常狀況下,不須要從新定義 Symbol.unscopables ,除非代碼中存在 with 語句而且

須要作一些特殊處理的時候,可是建議儘可能避免使用 with
{% endnote %}

總結

  1. Symbols 是一種新的原始值類型,用來建立一些屬性,這些屬性只能使用對應的符號或符號變量去訪問。
  2. Symbol([description]) 用來建立一個符號,推薦傳入描述,便於識別。
  3. Symbol.for(key) 首先查找註冊表(GSR),若是 key 對應的符號存在直接返回,若是不存在則建立新符號並加入到註冊表,而後返回新建立的符號。
  4. Symbol.keyFor(symbolValue) 經過符號變量從註冊表中找到對應的符號值,沒有返回 undefined
  5. 符號共享經過 Symbol.for(key)Symbol.keyFor(symbolValue) 可讓符號達到共享的目的,由於全局註冊表在全部代碼運行以前就已經建立好了。
  6. 符號不容許類型轉換(或隱式轉換)。
  7. Object.keys()Object.getOwnPropertyNames() 不能獲取到符號屬性。
  8. Object.getOwnPropertySymbols(obj) 能獲取到對象的全部符號屬性。
  9. Object.defineProperty()Object.defineProperties() 對符號屬性也有效。
  10. 知名符號7.6,以往的內部實現是不對開發者開放的,現在有了這些知名符號屬性,可讓開發者自信改變一些功能和接口的默認行爲。

Sets 和 Maps

  • set 集合是一組沒有重複元素的一個序列。
  • map key 值得集合,指向對應的值

ECMAScript 5 中的 Sets 和 Maps

在 es6 以前會有各類 sets/maps 的實現方式,可是大都或多或少有所缺陷。

背景

好比: 使用對象屬性實現

let st = Object.create(null)

set.foo = true

if (set.foo) {
  // sth
}
複製代碼

在將對象做爲 set 或 map 使用的時候惟一的區別在於:

map 裏面的 key 有存儲對應的具體內容,而不像 set 僅僅用來存儲 true or false,

用來標識 key 是否存在。

let map = Object.create(null)

map.foo = 'bar'

let value = map.foo

console.log(value) // 'bar'
複製代碼

+RESULTS:

bar
複製代碼

潛在問題

使用對象實現 set/map 的問題:

  1. 沒法避免字符串 key 的惟一性問題
  2. 沒法避免對象做爲 key 的惟一性問題

字符串做爲 key :

let map = Object.create(null)

map[5] = 'foo'

console.log(map["5"]) // 'foo'
複製代碼

+RESULTS:

foo
複製代碼

由於對於對象來講,使用數字下表去訪問的時候,其實是將下標數值轉成字符串去訪問了,

即至關於 map[5] 等價於 map['5'] 所以,有上面的結果輸出。

可是,你恰恰想使用 5 和 '5' 去標識兩個 key 的時候就沒法達到目的了。

對象做爲 key :

let map = Object.create(null),
    key1 = {},
    key2 = {}

map[key1] = 'foo'

console.log(map[key2]) // 'foo'
複製代碼

+RESULTS:

foo
複製代碼

對象做爲 key 值得時候,內部會發生類型轉換,將對象轉成 "[object Object]"

所以不管用 key1 仍是 key2 去訪問 map ,最後的結果都是 map["[object Object]"] 去訪問了

所以,結果都是 'foo'。

Sets 集合

  1. 建立使用 new Set() 建立實例。
  2. 添加使用 set.add() 方法。
  3. 集合區分數值的數字類型和字符串類型,不會發生類型強轉。
  4. -0+0 在集合中會被當作同樣處理
  5. 對象能夠做爲 set 的元素,且兩個 {} 會被當作兩個不一樣的元素處理

set 初始化

new Set() 建立了一個空的 set

能夠在初始化的時候傳入一個數組。

{% note info %}
實際上, Set 構造函數能夠接受任意一個 iterable 對象做爲參數。
{% endnote %}

let set = new Set([1, 2, 3, 4])

console.log(set.size) // 4
複製代碼

+RESULTS:

4
複製代碼

添加元素 set.add()

添加的元素區分類型,不會作類型轉換,即 5'5' 是不同的,重複添加也只會執行一次,

set 的元素是不會重複的。

let set = new Set()

set.add(5)
set.add('5')
set.add(5)

console.log(set.size, set)
複製代碼

+RESULTS:

2 Set { 5, '5' }
複製代碼

對象元素:

let set = new Set(),
    key1 = {},
    key2 = {}

set.add(key1)
set.add(key2)
set.add(key1)

console.log(set.size) // 2
複製代碼

+RESULTS:

2
複製代碼

set apis

  1. set.has(v) 判斷 set 中是否有元素 v ,返回 true/false
  2. set.add(v) 添加元素
  3. set.size 集合大小
  4. set.delete(v) 刪除元素
  5. set.clear() 清空集合

集合迭代(forEach)

對集合使用 forEach 和對數組使用的方法同樣,它接受一個函數,抓個函數又三個參數:

  1. 第一個參數:集合的當前值
  2. 第二個參數:和第一個參數同樣是當前元素的值,跟數組不同,數組使用 forEach 抓個參數是當前索引值
  3. 第三個參數:被遍歷的集合自己。

Sets 沒有 Key 值。

let set = new Set(['a', 'b', 'c', 'd', 'e'])

console.log(set[0]) // undefined, 沒有下標值
set.forEach(function(idx, v, ownerSet) {
  console.log(idx, v, ownerSet === set, ownerSet)
})
複製代碼

+RESULTS:

undefined
a a true Set { 'a', 'b', 'c', 'd', 'e' }
b b true Set { 'a', 'b', 'c', 'd', 'e' }
c c true Set { 'a', 'b', 'c', 'd', 'e' }
d d true Set { 'a', 'b', 'c', 'd', 'e' }
e e true Set { 'a', 'b', 'c', 'd', 'e' }
複製代碼

結果所示:

  1. 集合的 key 就是 value。
  2. 遍歷的函數第三個參數 ownerSet 就是被遍歷的 set 集合自己。

在使用 forEach 能夠給它傳遞一個上下文參數,讓綁定回調函數裏面的 this

let set = new Set([1,2])

let processor = {
  output(value) {
    console.log('output from processor: ' + value)
  },

  process(dataSet, scope = 1) {
    const obj = {
      output(value) {
        console.log('output from obj: ' + value)
      }
    }
    dataSet.forEach(function(value) {
      this.output(value)
    }, scope === 1 ? this : obj)
  }
}

processor.process(set) // scope: processor
processor.process(set, 2) // scope: obj
複製代碼

+RESULTS:

output from processor: 1
output from processor: 2
output from obj: 1
output from obj: 2
複製代碼
  1. this 傳遞給回調,從而 output 來自 processor
  2. obj 傳遞給回調,從而 output 來自 obj

結論:*咱們能夠經過給 forEach 傳遞第二個參數來改變回調函數的執行上下文。*

使用箭頭函數解決 this 指向問題:

let set = new Set([1,2])

let processor = {
  output(value) {
    console.log('output from processor: ' + value)
  },

  process(dataSet) {
    // this 老是綁定到 processor
    dataSet.forEach(value => this.output(value), {})
  }
}

processor.process(set) // scope: processor

複製代碼

+RESULTS:

output from processor: 1
output from processor: 2
複製代碼

不管第二個參數 {} 傳或不傳結果都同樣,箭頭函數裏的 this 指向不會發生改變。

{% note warning %}
集合不能直接使用索引訪問元素,若是須要使用到索引訪問元素,那最好將集合轉成數組來使用。
{% endnote %}

Set 和 Array 之間的轉換

  1. 集合轉數組 let set = new Set([1, 2, 3, 2]); ,且會將重複的元素去掉只餘一個。
  2. 數組轉集合,最簡單的就是展開符了 let arr = [...set];

展開符(…)能夠做用域任何 iterable 的對象。即任何可 iterable 的對象均可以經過 ... 轉成數組。

也由於有了 Set... 從而是數組的去重變得異常簡單:

const eleminateDuplicates = items => [...new Set(items)]

let nums = [1, 2, 3, 2, 4, 3, 4]

console.log(eleminateDuplicates(nums).toString())
複製代碼

+RESULTS:

1,2,3,4
複製代碼

弱集(Weak Sets)

由於它存儲對象引用的方式,集合類型也能夠叫作強集合類型。

即集合中對於對象的存儲是存儲了該對象的引用而不是被添加到集合是的那個變量名而已,

相似對象的屬性的值爲對象同樣,就算改變了這個屬性的值,那個對象若是有其餘變量指向它,

那他同樣存在(相似 C 的指針概念,兩個指針同時指向一塊內存,一個指針的指向發生變化並不會

影響另外一個指針指向這塊內存)。

好比:

let animal = {
  dog: {
    name: 'xxx',
    age: 10
  }
}

let dog1 = animal.dog

console.log(dog1.name) // 'xxx'
// 引用發生變化
animal.dog = null

// 並不影響別的變量指向 { name: 'xxx', age: 10 } 這個對象
console.log(dog1.age) // 10

// 指回去,依舊是它原來指向的那個對象
animal.dog = dog1
console.log(animal.dog.name) // 'xxx'
console.log(animal.dog.age) // 10

複製代碼

+RESULTS:

xxx
10
xxx
10
複製代碼

根據引用的特性,對於集合元素也同樣實用:

let set = new Set();
let key = {};

set.add(key) // 實際將對象的引用加到集合中

console.log(set) // 1
console.log(set.size) // 1

key = null // 改變了變量值而已,實際引用的那個對象還在
console.log(set.size) // 1

key = [...set][0]

console.log(key)// {}
複製代碼

+RESULTS:

Set { {} }
1
1
{}
undefined
複製代碼

這種強引用在某些狀況下極可能會出現內存泄漏,好比,在瀏覽器環境中

集合中保存了一些 DOM 元素的引用,而這些元素自己可能會被其餘地方的

代碼從 DOM 樹中移除,同時你也不想再保有這些 DOM 元素的引用了,或者說之後

都不會用到它了,應該被釋放回收纔對,可是實際上集合中仍然保有這些元素的引用(實際已經不存在的東西),

這種狀況就叫作內存泄漏(memory leak)。

爲了解決這種狀況, ECMAScript 6 中增長了一種集合類型: weak sets ,弱引用只會保存對象的弱引用

而不會保存引用的原始值。弱引用不會阻止垃圾回收若是它僅僅只是保存了引用而不是原始值。

建立 Weak Sets(WeakSet)

弱引用集合構造函數: WeakSet

let set = new WeakSet(),
    key = {}, key1 = key

set.add(key)

console.log(set)
key = null
console.log(set.has(key))
console.log(set.has(key1))
console.log(set.has(null))
console.log(set)
複製代碼

+RESULTS:

WeakSet { [items unknown] }
false
true
false
WeakSet { [items unknown] }
undefined
複製代碼

瀏覽器環境輸出結果:

img

Set 和 WeakSet 對比

Set 中添加對象,添加的是對該對象的引用,所以保存該對象的變量值發生變化,並不影響該對象在集合中的事實。

WeakSet 中添加的是該變量的原始值??變量值一旦改變,集合中的內容將隨之改變(由 JavaScript 引擎處理)。

{% note info %}
TODO: Set 保存引用?WeekSet 保存原始值??有啥區別??
{% endnote %}

這裏咱們將對比兩種集合在不一樣形式下的運行結果,經過對比分析來搞清楚集合中引用和原始值的概念。

Set, WeakSet 添加對象的結果

let set = new Set()
let key = { a: 1 }

set.add(key)
console.log(set)
console.log(set.has(key)) // true

let wset = new WeakSet()
let wkey = { a: 1 }

wset.add(wkey)
console.log(wset)
console.log(wset.has(wkey))
複製代碼

+RESULTS:

Set { { a: 1 } }
true
WeakSet { [items unknown] }
true
undefined
複製代碼

這裏 WeakSet 結果不直觀,下面是瀏覽器結果:

img

從瀏覽器端的結果分析:

  1. 二者在內部屬性 Entries 中都有一個咱們添加的 {a : 1} 對象元素。
  2. WeakSet 沒有 size 屬性, Set 有 size 屬性。

改變對象 key/wkey 的值

let set = new Set()
let key = { a: 1 }

set.add(key)
console.log(set) // 改變以前
key = null
console.log(set) // 改變以後
console.log(set.has(key)) // true

let wset = new WeakSet()
let wkey = { a: 1 }

wset.add(wkey)
console.log(wset) // weak key 改變以前
wkey = null
console.log(wset) // weak key 改變以後
console.log(wset.has(wkey))

複製代碼

+RESULTS: emacs nodejs

Set { { a: 1 } }
Set { { a: 1 } }
false
WeakSet { [items unknown] }
WeakSet { [items unknown] }
false
undefined
複製代碼

瀏覽器環境輸出結果:

img

結果:

  1. 對於 Set 對象變量 key 值得改變並不會影響 Set 中 {a:1} 對象

    Set 存放的是對象 {a:1} 的引用,即在 set.add(key) 以後,其實是有兩個引用指向了
    {a:1} 對象,一個是 key 這個變量,一個是集合 set 中的某個位置上的變量(假設爲: fkey)。
    根據引用的特性, key 的釋放並不會影響 {a:1} 這個對象自己在內存中的存在,即不會影響 fkey
    對這個對象的影響,從而並不影響 set 的內容。

  2. WeakSet 中的 {a:1} 沒有了

    WeakSet 咱們說它添加的是 wkey 的原始值,即便直接和 wkey 這個變量的原始值掛鉤的,
    執行 wkey = null 就是講它的原始值發生改變,最終將影響 WeakSet 。

針對 #2 中的 WeakSet 狀況,將程序改造一下:

let set = new Set()
let key = { a: 1 }
let key1 = key

set.add(key)
console.log(set) // 改變以前
key = null
console.log(set) // 改變以後
console.log(set.has(key)) // true

console.log('-------- 楚河漢界 ---------')
let wset = new WeakSet()
let wkey = { a: 1 }
let wkey1 = wkey

wset.add(wkey)
console.log(wset) // weak key 改變以前
wkey = null
console.log(wset) // weak key 改變以後
console.log(wset.has(wkey))
console.log(wset.has(wkey1))
複製代碼

+RESULTS:

Set { { a: 1 } }
Set { { a: 1 } }
false
-------- 楚河漢界 ---------
WeakSet { [items unknown] }
WeakSet { [items unknown] }
false
true
undefined
複製代碼

再來看看輸出結果:

img

咱們獲得了使人意外的結果:

  1. 並無顯示的 wset.add(wkey1) 可是最後的 wset.has(wkey1) 的結果倒是 true
  2. wset 集合中的 {a:1} 依然存在。

要理解這個問題,則須要知道「強引用」和「弱引用」的區別:

強引用和弱引用

咱們都知道 JavaScript 的垃圾回收機制中有一個相關知識點就叫作引用計數,即一個對象若是有被其餘變量

引用那麼這個對象的引用計數就 +1 若是這個變量被釋放該對象的引用計數就 -1 一旦引用計數爲 0

垃圾回收機制就會將這個對象回收掉,由於沒有人再使用它了。

*強引用(Set)*:至關於讓該對象的引用計數 +1 ,如 Set 集合保存了對象的引用導
致引用計數 +1 ,在擁有該對象的變量 key 的值怎麼變化都不會致使引用計數爲 0 從而阻止了垃圾回收器將其回收掉。

弱引用(WeakSet): 對對象的引用不會計入到引用計數中,即將 wkey 加入到 WeakSet 中,並不會引發

wkey 指向的那個對象的引用計數 +1 ,所以只要釋放了 wkey 對其的引用,對象的引用計數就變成 0 了,所以

此時只有 wkey 指向 {a:1} 這個對象,改變 wkey 就會改變 WeakSet 中的內容,由於這個內容已經被

回收掉了。

根據上面的結論,咱們就知道爲何咱們增長了一行 let key1 = key 以後, {a:1} 對象依然會在 wset 中由於此時 {a:1} 引用計數不爲 0 並無被釋放掉。

Maps

es6 的 Map 類型是一個有序的鍵值對列表, key 和 value 能夠是任意類型,而且 key
不會發生類型強轉,也就是說 5"5" 屬於不一樣的兩個鍵,和對象不同(對象把他
們當作一個鍵,由於對象的 key 最終表示形式爲 string 內部有發生強制轉換)。

map.set(key, value)map.get(key)

Map 實例能夠經過 setget 方法去設置鍵值對而後獲取該值。

let map = new Map()
map.set('title', 'u es6')
map.set('year', 2019)

console.log(map)
console.log(map.get('title'))
console.log(map.get('year'))
console.log(map[0])
複製代碼

+RESULTS:

Map { 'tit
u es6
2019
undefined
複製代碼

map 數據的內部存儲格式({ 'key' => value }):

img

新增函數列表

分類 函數名 描述 其餘
Function
Object Object.is(v1, v2) v1 是不是 v2 彌補 === 不能判斷 +0,-0 和 NaN,NaN
Object.assign(target, ...sources) 合併對象,淺拷貝,賦值運算
Object.getPrototypeOf(obj) 取原型對象
Object.setPrototypeOf(obj, protoObj) 設置原型對象
Object.getOwnPropertySymbols(obj) 獲取對象全部符號屬性 Object.keys, Object.getOwnPropertyNames 不能取符號屬性
String str.codePointAt(n) Unicode編碼值 str.charCodeAt(n)
str.fromCodePoint(s) 根據編碼轉字符 str.fromCharCode(s)
str.normalize() 將字符的不一樣表示方式統一成一種表示形式 undefined, "NFC", "NFD", "NFKC", or "NFKD"
str.repeat(n) 將字符串重複 n 遍,做爲新字符串返回 'x'.repeat(3) => 'xxx'
RexExp
相關文章
相關標籤/搜索