你不知道的JavaScript(中) - 閱讀筆記

你不知道的JavaScript(中)

① 類型和語法

一. 類型

JS有七種內置類型:null,undefined,boolean,number,string,objectsymbol,可使用typeof運算符來查看git

變量沒有類型,但它們持有的值有類型,類型定義了值的行爲特徵github

不少開發人員將undefinedundeclared混爲一談,但在Js中它們是兩碼事.undefined是值的一種,而undeclared則表示變量尚未被聲明過ajax

遺憾的是,JS將它們混爲一談,在咱們試圖訪問"undeclared"變量時這樣報錯:ReferenceErroeL a is not defined, 而且typeof對undefinedundeclared變量都返回"undefined"編程

然而,經過typeof的安全防範機制(阻止報錯)來檢查undeclared變量,有時是個不錯的辦法數組

二. 值

2.1 數組

若是字符串鍵值可以被強制類型轉換爲十進制數字的話,它就會被看成數字的索引來處理promise

var a = []
a['13'] = 42
a.length; // 14
複製代碼

2.2 字符串

JS中字符串是不可變的,而數組是可變的瀏覽器

字符串不可變是指字符串的成員函數不會改變其原始值,而是建立並返回一個新的字符串. 而數組的成員函數都是在其原始值上進行操做的安全

許多數組函數用來處理字符串很方便. 雖然字符串沒有這些函數,但能夠經過"借用"數組的非變動方法來處理字符串多線程

惋惜咱們沒法"借用"數組的可變動成員函數,由於字符串是不可變的,變通辦法是將字符串轉換成數組待處理完再轉換成字符串閉包

2.3 數字

JavaScript數字類型是基於IEEE754標準來實現的,該標準一般也被稱爲"浮點數",JS使用的是"雙精度"格式(即64位二進制)

0.1+0.2 === 0.3
0.1+0.2 === 0.3 //false
複製代碼

簡單的來講,二進制浮點數中的0.1和0.2並非十分精準,它們相加的結果並不是恰好等於0.3,而是一個比較接近的數字0.30000000000000004,因此條件判斷的結果是false

整數檢測

要檢測一個值是不是整數,可使用ES6中的Number.isInteger(...)方法

Number.isInteger(42) //true
Number.isInteger(42.000) //true
Number.isInteger(42.3) //false
複製代碼

2.4 特殊的數值

undefined類型只有一個值,便是undefined

null類型也只有一個值,便是null

  • null指空值
  • undefined指沒有值

或者

  • undefined指從未賦值
  • null指曾賦過值,可是目前沒有值

null是一個特殊關鍵字,不是標識符,咱們不能將其看成變量來使用和賦值. 然而undefined倒是一個標識符,能夠看成變量來使用和賦值

void運算符

表達式 void __沒有返回值,所以返回結果是undefined,void並不改變表達式的結果,只是讓表達式不返回值

var a = 42
console.log(void a , a) // undefined 42
複製代碼

若是要將代碼中的值設爲undefined,就可使用viod

特殊的數字(NaN)

NaN是一個"警惕值",用於指出數字類型中錯誤狀況,即"執行數學運算沒有成功,這是失敗後返回的結果"

NaN惟一一個非自反

NaN是一個特殊值,是惟一一個非自反(自反,即X===X不成立)

特殊的等式

因爲NaN和自身不相等,因此必須使用ES6的Number.isNaN(...)

ES6中新加入了一個工具Object.is(...)來判斷兩個值是否絕對相等

var a = 2 / "foo"
var b = -3 * 0

Object.is(a, NaN) // true
Object.is(0, -0)  // true
Object.is(b, 0) // true
複製代碼

2.5 值和引用

簡單值(基本類型值)老是經過值的方式來賦值/傳遞,包括null,undefined,字符串,數字,布爾值ES6中的symbol

複合值--對象(包括數組和封裝對象)和函數,則老是經過引用複製的方式來賦值/傳遞

咱們沒法自行決定使用值複製仍是引用複製,一切由值的類型來決定

小結

JS中的數組是經過數字索引的一組任意類型的值. 字符串和數組相似,但它們的行爲特徵不一樣,在將字符做爲數組來處理須要特別當心. JS中的數字包括"整數"和"浮點數"

基本類型中定義了幾個特殊的值

null類型只有一個值null,undefined類型也只有一個值undefined . 全部變量在賦值以前默認值都是undefined.

void運算符返回undefined

數字類型有幾個特殊值,包括NaN(invalid number),+Infinity,-Infinity和 -0

簡單標量基本類型值(字符串和數字等)經過值複製來賦值/傳遞,而複合值(對象等)經過值引用來賦值/傳遞. JS中的引用和其餘語言的引用/指針不一樣,它們不能指向別的變量/引用,只能指向值

三. 原生函數

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()

內部屬性[[Class]] - 精準檢查值類型

Object.prototype.toString(...)來查看一個複合值的類型

Object.prototype.toString.call([1,2,3]) // "[object Array]"
Object.prototype.toString.call(/regx-literal/i) // "[object RegExp]"
複製代碼

因爲基本類型值沒有.length.toString()這樣的屬性和方法,須要經過封裝對象才能訪問,此時JavaScript會自動爲基本類型值包裝成一個封裝對象

原生函數做爲構造函數

Array構造函數只帶一個數字參數的時候,該參數會做爲數組的預設長度,而非只充當數組中一個元素

Date(...)和Error(...)

Date(...)主要用來獲取當前的Unix時間戳(從1970年1月1日開始計算),該值能夠經過日期對象的getTime()來得到

Es5引入一個靜態函數Date.now()來獲取當前時間戳

全部的函數(包括內置函數Number,Array等)均可以調用Function.prototype中的apply(...),call(...)bind(...)

小結

JavaScript爲基本數據類型值提供了封裝對象,稱爲原生函數(如String,Number,Boolean等)

它們爲基本數據類型提供了該子類型所特有的方法和屬性

對於簡單標量基本類型值,好比abc,若是要訪問它的length屬性或String.prototype方法,JS引擎會自動對該值進行封裝來實現對這些屬性和方法的訪問

四. 強制類型轉換

JS中的強制類型轉換老是返回標量基本類型值,如字符串,數字和布爾值,不會返回對象和函數

然而在JS中一般將它們統稱爲強制類型轉換,分爲"隱式強制類型轉換"和"顯式強制類型轉換"

JSON字符串化

undefined,functionsymbol和包含循環引用的對象都不符合JSON結構標準,其餘支持JSON的語言沒法處理它們

JSON.stringify(..)在對象中遇到undefined,functionsymbol時會自動將其忽略,在數組中則會返回null(以保證單元位置不變)

JSON.stringify(undefined) // undefined
JSON.stringify(function(){}) // undefined
JSON.stringify(
    [1,undefined,function(){},4]
)  // "[1,null,null,4]"
JSON.stringify({
    a:2,b:function(){}
}) // "{"a":2}"
複製代碼
實用功能

若是replace是一個數組,那麼它必須是一個字符串數組,其中包含序列化要處理的對象的屬性名稱,除此以外其餘的屬性則被忽略

var a = {
    b:42,
    c:"42",
    d:[1,2,3]
}
JSON.stringify(a,["b","c"]) //"{"b":42,"c":"42"}
JSON.stringify(a,(k,v)=>{
    if(k !== "c") return v
})
// "{"b":42,"d":[1,2,3]}"
複製代碼

JSON.stringify還有一個可選參數space,用來指定輸出的縮進格式. space爲正整數時是指定每一級縮進的字符數,它還能夠是字符串,此時最前面的十個字符串被用於每一級的縮進

JSON.stringify(...)並非強制類型轉換

  1. 字符串,數字,布爾值和null的JSON.stringify(...)規則與ToString基本相同
  2. 若是傳給JSON.stringify(...)的對象定義了toJSON()方法,那麼該方法會在字符串化前調用,以便轉換成較爲安全的JSON值

4.1 ToNumber

其中true轉換爲1,false轉換爲0.undefined轉換爲NaN,null轉換爲0

將值轉換爲相應的基本類型值

首先檢查該值是否有valueOf()方法. 若是有而且返回基本類型值,就使用該值進行強制類型轉換. 若是沒有就使用toString()的返回值來進行強制類型轉換

若是valueOf()toString()均不返回基本類型值,會產生TypeError錯誤

ES5開始,使用Object.create(null)建立的對象[[Prototype]]屬性爲null,而且沒有valueOf()toString()方法,所以沒法進行強制類型轉換

4.2 ToBoolean

JavaScript中的值能夠分爲如下兩類:

  1. 能夠被強制類型轉換爲false的值
  2. 其餘(被強制轉換爲true的值)

如下假值的布爾強制類型轉換結果爲false:

  • undefined
  • null
  • false
  • +0, -0 和 NaN
  • ""

假值列表之外的都是真值

4.3 顯式強制類型轉換

字符串和數字之間的顯式轉換

String(....)遵循前面講過的ToString規則,將值轉換爲字符串的基本類型. Number(...)遵循前面講過的ToNumber規則,將值轉換成數字的基本類型

一元運算 + 被普通認爲是顯式強制類型轉換

日期顯示轉換爲數字
var timestamp = +new Date()

// 不過最好仍是使用ES5中新加入的靜態方法Date.now()
var timestamp = Date.now()
複製代碼
~運算符

~x大體等同於-(x+1)

~ 和 indexOf( )一塊兒能夠將結果強制類型轉換

var a = "Hello World"
~a.indexOf("lo") // -4 <--真值
if(~a.indexOf("lo")) {
    // 找到匹配
}
~a.indexOf("ol")  // 0 <-- 假值
複製代碼

-(x+1)推斷~ -1的結果應該是-0,然而實際上結果是0,由於它是字位操做而非樹形運算

顯式解析數字字符串
var a = "42"
var b = "42px"
Number(a) // 42
parseInt(a) // 42

Number(b) // NaN
parseInt(b) // 42
複製代碼

解析容許字符串中含有非數字字符,解析按從左到右的順序,若是遇到非數字字符就中止. 而轉換不容許出現非數字字符串,不然會失敗並返回NaN

例外 : parseInt(1/0, 19) // 18

parseInt(1/0, 19)其實是parseInt("Infinity", 19). 第一個字符是"I",以19爲基數時值爲18. 第二個字符"n"不是一個有效的數字字符,解析到此爲止

顯示轉換爲布爾值

建議使用Boolean(a)和!!a 來進行顯式強制類型轉換

4.4 隱式強制類型轉換

隱式強制類型轉換指的是那些隱蔽的強制類型轉換

var a = [1,2]
var b = [3,4]
a + b // "1,23,4"
複製代碼

因數組的valueOf()操做沒法獲得簡單基本類型值,因而它轉而調用toString(). 所以上例中的兩個數組變成了"1,2"和"3,4". + 將它們拼接了

var a = 42
var b = a + ""
b // "42
複製代碼

根據ToPrimitive抽象操做規則,a + ""會對a 調用valueOf()方法,而後經過ToString抽象操做將返回值轉換爲字符串. 而String(a)則是直接調用ToString()

隱式強制類型轉換爲布爾值

下面的狀況會發生布爾值隱式強制類型轉換:

  1. if(...)語句中的條件判斷表達式
  2. for(.. ; .. ; ..)語句中的條件判斷表達式(第二個)
  3. while(..)和do..while(..)循環中的條件判斷表達式
  4. ? : 中的條件判斷表達式
  5. 邏輯運算符 || (邏輯或) 和 && (邏輯與) 左邊的操做數(做爲條件判斷表達式)
|| 和 &&

&& 和 || 運算符的返回值並不必定是布爾類型,而是兩個操做數其中一個的值

|| 和 &&首先會對第一個操做數執行條件判斷,若是其不是布爾值就先進行ToBoolean強制類型轉換,而後再執行條件判斷

4.5 寬鬆相等和嚴格相等

正確的解釋是:" == "容許在相等比較中進行強制類型轉換,而" === "不容許

  • 用法:

    若是兩個值的類型不一樣,咱們就須要考慮有沒有強制類型轉換的必要,有就用==,沒有就用===,不用在意性能

抽象相等==

"=="在比較兩個不一樣類型的值時會發生隱式強制類型轉換,會將其中之一或者二者的轉換爲相同的類型後再進行比較

a. 字符串和數字之間的相等比較
var a = 42
var b = "42"
a === b  // fasle
a == b  // true
複製代碼
  1. 若是Type(x)是數字,Type(y)是字符串,則返回x==ToNumber(y)的結果
  2. 若是Type(x)是字符串,Type(y)是數字,則返回ToNumber(x)==y 的結果
b. 其餘類型和布爾類型之間的相等比較
var a = '42'
var b = true
a == b //false
複製代碼
  1. 若是Type(x)是布爾類型,則返回ToNumber(x) == y的結果
  2. 若是Type(y)是布爾類型,則返回x == ToNumber(y)的結果
var x = "42"
var y = false
x == y  // false
複製代碼

解析: Type(y)是布爾值,因此ToNumber(y)將false強制類型轉換爲0,而後"42" == 0 再變成42 == 0,結果是fasle

建議不管什麼狀況下不用使用 == true 和 == false

c.null和undefined之間的相等比較
  1. 若是x爲null,y爲undefined,則結果爲true
  2. 若是x爲undefined,y爲null,則結果爲true

在 == 中null和undefined相等,除此以外其餘值都不存在這種狀況

var a = null
var b 
a == b // true
a == null // true
b == null // true

a == false // false
b == false // false
a == ""
b == ""
a == 0
b == 0
複製代碼
d. 對象和非對象之間相等比較
  1. 若是Type(x)是字符串或者數字,Type(y)是對象,則返回x==ToPrimitive(y)的結果
  2. 若是Type(x)是對象,Type(y)是字符串或者數字,則返回ToPrimitive(x)==y的結果
比較少見的狀況

見中卷P84

使用建議
  • 若是兩邊的值中有true或者false,千萬不要使用"=="
  • 若是兩邊的值中有[],""或者0,儘可能不要使用"=="

4.6 抽象比較

若是比較雙方都是字符串,則按字母順序來進行比較

var a = ["42"]
var b = ["043"]
a < b // false
複製代碼

解析: ToPrimitive返回的是字符串,因此這裏比較的是"42"和"043"兩個字符串,它們分別以"4"和"0"開頭.

var a = {b:42} 
var b = {b:43}
複製代碼

解析: 由於a是[object object],b是[object object],因此按字母順序進行比較

小結

JS的數據類型之間的轉換,即強制類型轉換: 包括顯式和隱式

顯式強制類型轉換明確告訴咱們哪裏發生了類型轉換,有助於提升代碼可讀性和可維護性

隱式強制類型轉換則沒有那麼明顯,是其餘操做的反作用

五. 語法

5.1 語句和表達式

  • 語句 : 語句至關於句子,完整表達某個意思的一組詞
  • 表達式: 表達式相對於短語,JS中表達式能夠返回一個結果值
var a,b
a = if(true) {
    b = 4 + 38
}
複製代碼

上面這段代碼沒法運行,由於語法不容許咱們得到語句的結果值並將其賦值給另外一個變量

ES7規範有一項"do表達式":

var a,b
a = do {
	if(true) {
    	b = 4 + 38
	}
}
a // 42
複製代碼

其目的是將語句看成表達式來處理(語句中能夠包含其餘語句),從而不須要將語句封裝爲函數再調用return來返回值

表達式的反作用
一元運算符

++在前面時,如++a,它的反作用產生在表達式返回結果值以前,而a++的反作用則產生在以後

delete運算符

delete用來刪除對象中的屬性和數組中的單元

若是操做成功,delete返回true,不然返回false. 其反作用是屬性被從對象中刪除(或單元從array中刪除)

上下文規則
if(a) {
    //...
}
else if(b) {
	//... 
}
else {
    //...
}
複製代碼

事實上JS沒有else if,但ifelse只包含單條語句的時候能夠省略代碼塊的 { }

5.2 運算符優先級

運算符 說明
.[ ] ( ) 字段訪問、數組索引、函數調用和表達式分組
++ -- - ~ ! delete new typeof void 一元運算符、返回數據類型、對象建立、未定義的值
* / % 相乘、相除、求餘數
+ - + 相加、相減、字符串串聯
<< >> >>> 移位
< <= > >= instanceof 小於、小於或等於、大於、大於或等於、是否爲特定類的實例
== != === !== 相等、不相等、全等,不全等
& 按位「與」
^ 按位「異或」
| 按位「或」
&& 邏輯「與」
|| 邏輯「或」
?: 條件運算
= OP= 賦值、賦值運算(如 += 和 &=)
, 多個計算

5.5 函數參數

function foo(a = 42,b = a + 1) {
    console.log(
    	arguments.length, a , b,
        arguments[0], arguments[1]
    )
}
foo()    // 0 42 43 undefined undefined
foo(10)  // 1 10 11 10 undefined
foo(10,undefined )  // 2 10 11 10 undefined
foo(10,null )  // 2 10 11 10 undefined
複製代碼

雖然參數a和b都有默認值,可是函數不帶參數時, arguments數組爲空

相反,若是向函數傳遞undefined值,則arguments數組中會出現一個值爲undefined的單元,而不是默認值

5.6 try..finally

function foo() {
    try {
        return 42
    }
    finally {
        console.log("hello")
    }
    console.log("never runs")
}
console.log(foo())
// hello
// 42
複製代碼

這裏return 42先執行,並將foo()函數的返回值設置爲42. 而後try執行完畢,接着執行finally. 最後foo()函數執行完畢.

function foo() {
    try {
        throw 42
    }
    finally {
        console.log("hello")
    }
    console.log("never runs")
}
console.log(foo())
// hello
// Uncaught Exception: 42
複製代碼

若是finally中拋出異常,函數就會在此終止. 若是此前try中已經有return設置了返回值,則該值會被丟棄

小結

語句和表達式在英語中都能找到類比---語句就像英文中的句子,而表達式就像短語. 表達式能夠是簡單獨立的,不然可能會產生反作用

JS在語法規則上是語義規則. 例如, { } 在不一樣狀況下的意思不盡相同, 能夠是語句塊,對象常量,解構賦值(ES6)或者命名函數參數 (ES6)

ASI(自動分號插入)是JS引擎的代碼解析糾錯機制,它會在須要的地方自動插入分號來糾正解析錯誤. 問題在於這是否意味着大多數的分號都不是必要的,或者因爲分號缺失致使的錯誤是否均可以交給JS引擎來處理

混合環境

JavaScript程序幾乎老是在宿主環境中運行

在建立帶有id屬性的DOM元素時也會建立同名的全局變量


② 異步和性能

一. 異步: 如今與未來

1.1 異步控制檯

並無什麼規範或一組需求指定console.*方法族如何工做--它們並非JavaScript的一部分,而是由宿主環境添加到JavaScript中的

在某些條件下,某些瀏覽器的console.log(....)並不會把傳入的內容當即輸出,在許多程序中,I/O是很是低速的阻塞部分

若是在調試的過程當中遇到對象在console.log(....)語句以後被修改,可你卻看到了意料以外的結果,要意識到這多是這種I/O的異步化形成的

1.2 事件循環

程序一般分紅不少小塊,在事件循環隊列中一個接一個地執行. 嚴格地說,和你的程序不直接相關的其餘事件也可能會插入到隊列中

1.3 並行線程

異步是關於如今和將來的時間限制,而並行是關於可以同時發生的事情

多線程編程是很是複雜的. 由於若是不經過特殊的步驟來防止這種中斷和交錯運行的話,可能會獲得出乎意料的,不肯定的行爲.

JavaScript從不跨線程共享數據,這一味着不須要考慮這一層次的不肯定性. 可是這並不意味着JavaScript老是肯定性的

`示例代碼`
var a = 20
function foo() {
    a = a + 1 
}
function bar() {
    a = a * 2
}
// ajax異步請求的回調
ajax('/get',foo)
ajax('/get2',bar)
複製代碼

在JS的特性中,這種函數順序的不肯定性就是一般所說的競態條件,foo()bar()相互競爭,看誰先運行.

完整性,因爲JS的單線程特性,foo()(以及bar())中的代碼具備原子性. 一旦foo()開始進行,它的全部代碼都會在bar()中的任意代碼進行以前完成,或者相反,這稱爲完整運行特性

1.4 併發

setTimeout(..0)

基本的意思是: 把這個函數插入到當前事件循環隊列的結尾處

嚴格來講,setTimeout(..0)並不直接把項目插入到事件循環隊列. 定時器會在有機會的時候插入事件. 兩個連續的setTimeout(..0)調用不能保證會按照調用順序處理

....

小結

實際上,JavaScript程序老是至少分爲兩個塊: 第一個塊如今運行;下一個塊未來運行,以響應某個事件. 儘管程序是一塊一塊執行的. 可是全部這些塊共享對程序做用域和狀態的訪問,因此對狀態的修改都是在以前累積的修改之上進行的.

一旦有事件須要運行,事件循環就會運行,直到隊列清空. 事件循環的每一輪稱爲一個tick. 用戶交互,IO和定時器會向事件隊列中加入事件

任什麼時候刻,一次只能從隊列中處理一個事件. 執行事件的時候,可能直接或間接地引起一個或多個後續事件

併發是指兩個或多個事件鏈隨時間發展交替執行,以致於從更高的層次來看,就像是同時在運行(儘管在任意時刻只處理一個事件)

一般須要對這些併發執行的"進程"進行某種形式的交互協調,好比須要確保執行或者須要防止競態出現. 這些"進程"也能夠經過把自身分割爲更小的塊,以便其餘"進程"插入進來.

二. 回調

回調是編寫和處理JS程序異步邏輯的最經常使用方式

嵌套回調經常稱爲回調地獄,有時也稱爲毀滅金字塔

2.3 回調的信任問題

  • 調用回調過早(在追蹤以前)
  • 調用回調過晚(或者沒有調用)
  • 調用回調的次數太多或太少
  • 沒有把所需的環境/參數成功傳給你的回調函數
  • 吞掉可能出現的錯誤或異常

2.5 總結

回調函數是JS異步的基本單元

第一,大腦對於事件的計劃方式是線性的,阻塞的,單線程的語義,可是回調錶達異步流程的方式是非線性的,非順序的,這使得正確推導這樣的代碼難度很大. 難於理解的代碼是壞代碼,會致使壞bug

咱們須要一種更同步,更順序,更阻塞的方式來表達異步,就像咱們的大腦同樣

第二,也是更重要的一點,回調會受到控制反轉的影響,由於回調暗中把控制權交給第三方(一般是不受你控制的第三方工具!)來調用你代碼中的continuation. 這種控制轉移致使一系列麻煩的信任問題,好比回調被調用的次數是否會超出預期

能夠發明一些特定邏輯來解決這些信任問題,可是其難度高於應有水平,可能會產生更笨重,更難維護的代碼,而且缺乏足夠的保護,其中的損害要直到你受到bug的影響纔會被發現

咱們須要一個通用的方案來解決這些信任問題. 無論咱們建立多少回調,這一方案都應能夠複用,且沒有重複代碼的開銷

咱們須要比回調更好的機制. 到目前爲止,回調提供了很好的服務,可是將來的JS須要更高級,功能更強大的異步模式

三. Promise

經過回調錶達程序異步和管理併發的兩個主要缺陷: 缺少順序性和可信任性

一旦Promise決議,它就永遠保持在這個狀態.此時它就成爲了避免變值(immutablevalue),能夠根據需求屢次查看

Promise決議後就是外部不可變的值,咱們能夠安全地把這個值傳遞給第三方,並確信它不會被有意無心地修改.

3.1 Promise"事件"

代碼:

function foo(x) {
    // 開始作一些可能耗時的工做
    
    //構造並返回一個promise
    return new Promise((resolve,reject)=>{
        // 最終調用resolve(...)或者reject(...)
    })
}
複製代碼

這些是promise的決議函數. resolve(...)一般標識完成,而reject(...)則標識拒絕

3.2 具備then方法的鴨子類型

識別Promise就是定義某種稱爲thenable的東西,將其定義爲任何具備then(...)方法的對象和函數

thenable值的鴨子類型檢測就大體相似於:

if(
	p !== null &&
    (
    	typeof p === 'object' ||
        typeof p === 'function'
    )&&
    typeof p.then === 'function'
){
    // 假定這是一個thenable
}else{
	// 不是thenable
}
複製代碼

若是有任何其餘代碼無心或者惡意地給Object.prototypeArray.prototype或者其餘原生原型添加then(..),

你沒法控制也沒法預測,而且,若是指定的是不調用起參數做爲回調的函數,那麼若是有Promise決議到這樣的值,就會永遠掛住!

3.3 Promise的信任問題

回調的信任問題
  • 調用回調過早(在追蹤以前)
  • 調用回調過晚(或者沒有調用)
  • 調用回調的次數太多或太少
  • 沒有把所需的環境/參數成功傳給你的回調函數
  • 吞掉可能出現的錯誤或異常
3.3.1 調用過早

在這類問題中,一個任務有時同步完成,有時異步完成,這可能會致使競態條件

對一個Promise調用then(..)的時候,由於即便這個Promise已經決議,提供給then(..)的回調也總會被異步調用

3.3.2 調用過晚

Promisethen(..)註冊的觀察就會被自動調度. 能夠確信,這些被調度的回調在下一個異步事件點上必定會被觸發. 也就是說, 一個Promise決議後,這個Promise上全部的經過then(...)註冊的回調都會在下一個異步時機點上 依次被馬上調用.

p.then(function() {
    p.then(function() {
        console.log("C")
    })
    console.log("A")
})
p.then(function() {
        console.log("B")
})
// A B C
// 這裏,"C"沒法打斷或搶佔"B",這就是Promise的運做方式
複製代碼
3.3.3 回調未調用

沒有任何東西能阻止Promise像你通知它的決議. 若是你對一個Promise註冊了一個完成回調和一個拒絕回調,那麼Promise在決議時總會調用其中的一個

3.3.4 調用次數過少或過多

因爲Promise只能被決議一次,因此任何經過then(..)註冊的回調就只會調用一次

固然,若是你把同一個回調註冊了不止一次(如: p.then(...); p.then(...)),那它被調用的次數就會和註冊次數相同. 響應函數只會被調用一次.

3.3.5 未能傳遞參數/環境值

Promise至多隻能有一個決議值(完成或拒絕)

若是使用多個參數調用resolve(..)或者reject(..),第一個參數以後的因此參數都會被忽略.

JS中的函數老是保持其定義所在的做用域的閉包

3.3.6 吞掉錯誤或異常(重要)

若是Promise的建立過程當中或在查看其決議結果過程當中的任什麼時候間點上出現一個JS異常錯誤,好比一個TypeErrorReferenceError,那這個異常就會被捕捉,而且會使這個Promise被拒絕

var p = new Promise(function(resolve,reject){
    foo.bar() // foo未定義,會報錯
    resolve(42) // 永遠不會到這裏
})
p.then(\
    function fulfilled() {
        // 永遠不會到達這裏
    },
    function rejected(err) {
        // err將會是一個TypeError異常對象來自foo.bar()這一行
    }
)
複製代碼

由於其有效解決了另一個潛在的Zalgo風險,即出錯可能會引發同步響應,而不出錯則會是異步的. Promise甚至把JS異常也變成了異步行爲,進而極大下降了競態條件出現的可能.

var p = new Promise(function(resolve,reject){
    resolve(42)
})
p.then(
	 function fulfilled(msg) {
        foo.bar()
        console.log(msg) // 永遠不會到達這裏
    },
    function rejected(err) {
        // 永遠不會到達這裏
    }
)
複製代碼

這看起來foo.bar()這一行產生的異常被吞掉了,實際上不是這樣的,其實是咱們沒有偵聽到它

p.then(..)調用自己返回了另外一個Promise,正是這個Promise將會因這個TypeError異常而拒絕

問: 爲何它不是簡單地調用咱們定義的錯誤處理函數?

答: 若是這樣的話就違背了Promise的一條基本原則, Promise一旦決議就不可再變. p已經完成爲值42,因此以後查看p的決議是,並不能由於出錯就把p再變成一個拒絕

3.3.7 是可信任的promise嗎

若是向Promise.resolve(..)傳遞一個非Promise或非thenable的當即值,就會獲得一個用這個值填充的promise.

var p1 = new Promise(function(resolve,reject){
    resolve(42)
})
var p2 = Promise.resolve(42)
// 以上兩個promise的行爲徹底是一致的
複製代碼

而若是向Promise.resolve(...)傳遞一個真正的Promise,就會返回一個Promise

Promise.resolve(...)能夠接受任何thenable,將其解封爲它的非thenable值. 從Promise.resolve(...)獲得的是一個真正的Promise,是一個能夠信任的值. 若是你傳入的已是真正的Promise,那麼你獲得的就是自己,因此經過Promise.resolve(...)過濾來得到可信任性徹底沒有壞處.

對於用Promise.resolve(...)爲全部函數的返回值都封裝一層. 另外一個好處是,這樣作容易把函數調用規範爲定義良好的異步任務. 若是一個函數有時會返回一個當即值,有時會返回Promise,那麼Promise.resolve(...)就能保證總會返回一個Promose結果

3.3.8 創建信任

Promise這種模式經過可信任的語義把回調做爲參數傳遞,使得這種行爲更可靠更合理.經過把回調的控制反轉反轉回來,咱們把控制權放到一個可信任的系統(Promise)中,這種系統的設計目的就是爲了使異步編碼更清晰

3.4 鏈式流

鏈式流得於實現關鍵在於如下兩個Promise固有行爲特徵:

  1. 每次對promise調用then(..),它都會建立並返回一個新的Promise,咱們能夠將其連接起來
  2. 無論從then(..)調用的完成回調返回值是什麼,它都會自動設置爲被連接Promise(第一點中的)的完成
function delay(time) {
    return new Promise(function (resolve, reject) {
        setTimeout(resolve, time)
    })
}

delay(100) // 步驟1
    .then(function STEP2() {
        console.log('step2 after 100ms')
        return delay(200)
    })
    .then(function STEP3() {
        console.log('step3 after another 200ms')
    })
    .then(function STEP4() {
        console.log('step4')
        return delay(50)
    })
    .then(function STEP5() {
        console.log('step5 after another 50ms')
    })
複製代碼

嚴格來講: 這個交互過程當中有兩個promise: 200ms延遲promise和第二個then(..)連接到的那個連接promise.

Promise機制已經自動把它們的狀態合併在一塊兒,能夠把return delay(200)看做是建立了一個promise,並用其替換了前面返回的連接promise

從本質來講,這使得錯誤能夠繼續沿着Promise鏈傳播下去,直到遇到顯示定義的拒絕處理函數

總結
  • 調用Promisethen(..)會自動建立一個新的Promise從調用返回
  • 在完成或拒絕處理函數的內部,若是返回一個值或拋出一個異常,新返回的Promise就相應地決議
  • 若是完成或拒絕處理函數返回一個Promise,它將會被展開,這樣一來,無論它的決議值是什麼,都會成爲當前then(..)返回的連接Promise的決議值
決議,完成以及拒絕

決議(resolve),完成(fulfill)和拒絕(reject)

3.5 錯誤處理

錯誤處理最天然的形式就是同步的try...catch結構

任何Promise鏈的最後一步,無論是什麼,老是存在着未被查看的Promise中出現未捕獲錯誤的可能性

3.5.2 處理未捕獲的狀況(未實現)

瀏覽器有一個特有的功能:

它們能夠跟蹤並瞭解全部對象被丟棄以及被垃圾回收的時機. 因此,瀏覽器能夠追蹤Promise對象. 若是在它被垃圾回收的時候其中拒絕,瀏覽器就能確保這是一個真正未被捕獲的錯誤,進而能夠肯定應該將其報告到開發者終端.

3.5.3 成功的坑
  • 默認狀況下,Promise在下一個任務或時間循環tick上報告全部拒絕麼若是在這個時間點上該Promise上尚未註冊錯誤處理函數
  • 若是想要一個被拒絕的Promise在查看以前的某個時間段被保持被拒絕狀態,能夠調用defer(..),這個函數優先級高於該Promise的自動錯誤報告
var p = Promise.reject('Oops').defer()
foo(42)
.then(
    function fulfilled(){
        return p
    },
    function rejected(err) {
        // 處理foo(...)錯誤
    }
)
// 調用defer(),這樣就不會有全局報告出現. 爲了便於連接,defer()只是返回這同一個promise
複製代碼

默認狀況下,全部的錯誤要麼被處理要麼被報告,調用defer()的危險是,若是defer()了一個Promise,但以後沒有成功查看或處理它的拒絕結果,這樣就有可能存在未被捕獲的狀況

3.6 Promise模式

3.6.1 Promise.all([..])

Promise.all([...])須要一個參數,是一個數組,一般由Promise實例組成. 從Promise.all([..])調用返回的promise會收到一個完成消息. 這是一個由全部傳入promise的完成消息組成的數組,與指定的順序一致(與完成順序無關)

  • 嚴格說來,傳給Promise.all([..])的數組中的值能夠是Promise, thenable,甚至是當即值.

  • 就本質而言,列表中的每一個值都會經過Promise.resolve(..)過濾,以確保要等待的是一個真正的Promise,因此當即值會規範化爲爲這個值構建的Promise.

  • 若是數組是空的,主Promise就會當即完成

  1. Promise.all([..])返回的主promise在且僅在全部成員promise都完成後纔會完成.
  2. 若是這些promise中有任何一個被拒絕的話,主Promise.all([..])promise就會當即被拒絕,並拋棄來自其餘全部promise的所有結果
  3. 永遠要記住爲每一個promise關聯一個拒絕/錯誤處理函數,特別是從Promise.all([..])返回的那一個
3.6.2 Promise.race([..])

Promise.race([..])也接受單個數組參數. 這個數組由一個或多個Promise,thenable或 當即值組成. 可是當即值之間的競爭在實踐中沒有太大的意義

  • Promise.all([..])相似,一旦有任何一個Promise決議爲完成,Promise.race([..])就會完成;一旦有任何一個Promise決議爲拒絕,它就會拒絕
  • 若是你傳入一個空數組,主race([..])Promise永遠不會決議,而不是當即決議

3.7 Promise API概述

3.7.1 new Promise(..)構造器

有啓示的構造器Promise(..)必須和new一塊兒使用,而且必須提供一個函數回調. 這個回調是同步的或當即調用的

var p = new Promise(function(resolve,reject){
    // resolve(..) 用於決議/完成這個promise
    // reject(..) 用於拒絕這個promise
})
複製代碼
  1. reject(..)就是拒絕這個promise;但resolve(..)便可能完成promise,也可能拒絕,要根據傳入參數而定.
  2. 若是傳給resolve(..)的是一個非Promise,非thenable的當即值,這個promise就會用這個值完成
  3. 若是傳給resolve(..)的是一個真正的promise或thenable值,這個值就會被遞歸展開,而且promise將取用其最終決議值或狀態
3.7.2 Promise.resolve(...)Promise.reject(...)

Promise.resolve(...)建立一個已完成的Promise的快捷方式

Promise.reject(...)建立一個已被拒絕的Promise的快捷方式

var p1 = new Promise(function(resolve,reject){
    reject('Oops')
})
var p2 = Promise.reject("Oops")
// 以上兩個promise是等價的
複製代碼
3.7.3 then(...)catch(...)
  1. then(..)接受一個或者兩個參數;第一個用於完成回調,第二個用於拒絕回調. 若是二者中的任何一個被省略或者做爲非函數值傳入的話,就會替換爲相應的默認回調. 默認完成回調只是把消息傳遞下去,而默認拒絕回調則只是從新拋出其接收到的出錯緣由.
  2. catch(..)只接受一個拒絕回調做爲參數,並自動替換默認完成回調. 它等價於then(null,...)
p.then(fulfilled)
p.then(fulfilled,rejected)
p.catch(rejected); // 等價於.then(null,rejected)
複製代碼

then(..)和catch(..)也會建立並返回一個新的promise,這個promise能夠用於實現Promise鏈式流程控制

3.7.4 Promise.all(...)Promise.race(...)

詳情見3.6

Promise.all([..])傳入空數組,它會當即完成,但Promise.race([..])會掛住,且永遠不會決議

3.8 Promise侷限性

3.8.1 順序錯誤處理

因爲一個Promise鏈僅僅是連接到一塊兒成員Promise,沒有把整個鏈標識爲一個個個體的實體,這意味着沒有外部方法能夠用於觀察可能發生的錯誤

咱們能夠在Promise鏈中註冊一個拒絕錯誤處理函數,對於鏈中任何位置出現的任何錯誤,這個處理函數都會獲得通知:p.catch(handleErrors);

可是,若是鏈中的任何一個步驟事實上進行了自身的錯誤處理,那麼handleErrors(..)就不會獲得通知. 徹底不能獲得錯誤通知也是一個缺陷. 基本上,這等同於try..catch存在的侷限:

try...catch可能捕獲一個異常並簡單地吞掉它. 因此這不是Promise獨有的侷限性,但多是咱們但願繞過的陷阱

3.8.2 單一值

Promise只能有一個完成值或一個拒絕理由.

3.8.3 單決議

Promise最本質的特徵是:

一個promise只能被決議一次(不管完成仍是拒絕)

3.8.4 慣性
3.8.5 沒法取消的Promise

一旦建立了一個Promise併爲其註冊了完成和拒絕處理函數,若是出現某種狀況使得這個任務掛起的話,你也沒有辦法從外部中止它的進程

3.8.6 Promise的性能

Promise使全部一切都成爲異步的了,即有一些當即完成的步驟仍然會延遲到任務的下一步. 這意味着一個Promise任務序列可能比徹底經過回調鏈接的一樣的任務序列運行的稍慢一點.

Promise稍慢一些,但做爲交換,你獲得的是大量內建的可信任,對Zalgo的避免及可組合性.

請使用它!

四. ES6生成器(generator)

生成器是一類特殊的函數,能夠一次或屢次啓動和中止,並不必定非得要完成

var x = 1
function *foo() {
    x++
    yield
    console.log('x':x)
}
function bar() {
    x++
}

// 構造一個迭代器it來控制這個生成器
var it = foo()
// 這裏啓動foo()
it.next()
x // 2
bar()
x // 3
it.next() // x:3
複製代碼

解析:

  1. it = foo()運算並無執行生成器*foo(),而只是構造一個迭代器,這個迭代器會控制它的執行
  2. 第一個it.next()啓動了生成器 *foo(),而並運行了 *foo()第一行的x++
  3. *foo() 在yield語句處暫停,在這一點上第一個it.next()調用結束. 此時 *foo()仍然在運行而且是活躍的,但處於暫停狀態
  4. 咱們查看x的值,此時是2
  5. 咱們調用bar(),它經過x++再次遞增x
  6. 咱們再次查看x的值是3
  7. 最後的it.next()調用從暫停處恢復了生成器*foo()的執行,並運行console.log(..)語句,這句語句使用當前x的值是3

4.1 打破完整運行

4.1.1 輸入和輸出
function *foo(x,y) {
    return x * y
}
var it = foo(6,7)
var res = it.next()
res.valur // 42
複製代碼

咱們只是建立一個迭代器對象,把它賦給一個變量it,用於控制生成器*foo(..). 而後調用it.next(),指示生成器 *foo(..)從當前位置開始繼續運行,停在下一個yield處或者直到生成器結束

這個next(..)調用的結果是一個對象,它有一個value屬性,持有*foo(..)返回的值. 換句話說,yield會致使生成器在執行過程當中發送出一個值,這有點相似於中間的return

根據你的視角不一樣,yieldnext(...)調用有一個不匹配. 通常來講,須要的next(..)調用要比yield語句多一個

由於第一個next(..)老是啓動一個生成器,並運行到第一個yield處. 不過,是第二個next(..)調用完第一個被暫停的yield表達式,第三個next(..)調用完成第二個yield,以此類推

消息是雙向傳遞的---yield...做爲一個表達式能夠發出消息響應next(..)調用,next(..)也能夠向暫停的yield表達式發送值

代碼:

function *foo(x) {
    var y = x * (yield "hello") // yield一個值
    return y
}
var it = foo(6)
var res = it.next() // 第一個next(),並不傳入任何東西
res.value; 			// "hello"
res = it.next(7) 	// 向等待的yield傳入7
res.value			// 42
複製代碼

yield.. 和 next(..)這一對組合起來,在生成器中的執行過程當中構成一個雙向消息傳遞的系統

注意

var res = it.next()	// 第一個next(),並不會傳入任何東西
res.value; 			// "hello"
res = it.next(7) 	// 向等待的yield傳入7
res.value			// 42 
複製代碼

​ 咱們並無向第一個next()調用和發送值,這是有意爲之. 只有暫停的yield才能接受一個經過next(..)傳遞的值,而在生成器的起始處咱們調用第一個next()時,尚未暫停的yield來接受這樣一個值. 規範和全部兼容瀏覽器都會默默丟棄傳遞第一個next()的任何東西. 傳值過去仍然不是個好思路,由於你建立了沉默無效代碼,這會讓人迷惑. 所以,啓動生成器時必定要用不帶參數的next()

若是你的生成器中沒有return的話---在生成器中和普通函數中同樣,return固然不是必需的---總有一個假定的/隱式的return(也就是return undefined),它會在默認狀況下回答最後的it.next(7)提出的問題.

4.1.2 多個迭代器

每次構建一個迭代器,實際上就隱式構建了生成器的一個實例,經過這個迭代器來控制的是這個生成器實例.

同一個生成器的多個實例能夠同時運行,他們甚至能夠彼此交互:

function *foo() {
    var x = yield 2
    z++
    var y = yield(x * z)
    console.log(x,y,z)
}
var z = 1
var it1 = foo()
var it2 = foo()
var val1 = it.next().value	// 2 <--yield 2 
var val2 = it.next().value	// 2 <--yield 2
var1 = it1.next(val2 * 10 ).value	//40 <-- x:20 , z:2
var2 = it2.next(val1 * 10 ).value	//600 <-- x:200 , z:3

it1.next(val2/2) // y:300
				// 20 300 3
it2.next(val1/4) // y:10
				//200 10 3
複製代碼

執行流程:

​ (1) *foo()的兩個實例同時啓動,兩個next()分別從yield2語句獲得值2

​ (2) val2 * 10也就是2 * 10,發送到第一個生成器實例it1,所以x獲得的值20. z從1增長到2,而後20 * 2經過yield發出,將val1設置40

​ (3) val15也就是40 * 5,發送到第二個生成器實例it2,所以x獲得值200. z再次從2遞增到3,而後2003經過yield發出,將val2設置爲600

​ (4) val2/2也就是600/2,發送到第一個生成器實例it1,所以y獲得值300,而後打印出x y z的值分別是20 300 3

​ (5) val1/4也就是40/4,發送到第二個生成器實例it2,所以y獲得值10,而後打印出x y z的值分別爲200 10 3

4.3 同步錯誤處理

yield暫停也使得生成器可以捕獲錯誤

function *main(){
  var x = yield "hello world"
  yield x.toLowerCase() // 引起一個異常
}
var it = main()
it.next().value // hello world
try{
  it.next(42)
}catch(err) {
  console.error(err) // TypeError
}
複製代碼

4.4 生成器+Promise

ES6中最完美的世界就是生成器和Promise的結合

迭代器應該對這個promise作什麼呢?

它應該偵聽這個promise的決議,而後要麼使用完成消息恢復生成器運行,要麼向生成器拋出一個帶有拒絕緣由的錯誤.

獲取Promise和生成器最大效用的最天然的方法就是yield出來一個Promise,而後經過這個Promise來控制生成器的迭代器.

代碼示例:

function foo(x,y) {
  return request("http://some.url.1/?x="+ x + "&y="+ y)
}
function *main() {
  try{
    var text = yield foo(11,31)
    console.log(text)
  }catch(e) {
    console.log(e)
  }
}
複製代碼

在生成器內部,無論什麼值yield出來,都只是一個透明的實現細節,因此咱們甚至沒有意識到其發生,也不須要關心,接下來實現接收和鏈接yield出來的promise,使它可以在決議以後恢復生成器.先從手工實現開始:

var it = main();
var p = it.next().value;
// 等待promise p決議
p.then(function(text){
  it.next(text)
},function(err){
  it.throw(err)
})
複製代碼
async...await

生成器+promise的語法糖

若是,你await了一個Promise,async函數就會自動獲知要作什麼,它會暫停這個函數,直到Promise決議

生成器中的Promise併發

最簡單的方法:

function *foo() {
  // 讓兩個請求"並行"
  var p1 = request("http://some.url.1")
  var p2 = request("http://some.url.2")
  
  // 等待兩個Promise都決議
  var r1 = yield p1
  var r2 = yield p2
  
  var v3 = yield request(
  	"http://some.url.3/?v="+ r1 + "," + r2
  )
  console.log(r3)
}
// 工具函數run
run(foo)
複製代碼

p1和p2都會併發執行,不管完成順序如何,二者都要所有完成,而後纔會發出r3 = yield request...Ajax請求

固然,咱們也可使用Promise.all([...])完成

function *foo() {
  // 讓兩個請求"並行"
  var results = yield Promise.all([
    request("http://some.url.1"),
    request("http://some.url.2")
  ])
  
  // 等待兩個Promise都決議
  var r1 = results[0]
  var r2 = results[1]
  
  var v3 = yield request(
  	"http://some.url.3/?v="+ r1 + "," + r2
  )
  console.log(r3)
}
// 工具函數run
run(foo)
複製代碼

.....華麗的略過線.....

小結

​ 生成器是ES6的一個新的函數類型,它並不像普通函數那樣老是運行到結束. 取而代之的是,生成器能夠運行當中暫停,而且未來再從暫停的地方恢復運行.

​ 這種交替的暫停和恢復是合做性的而不是搶戰式的,這意味着生成器具備獨一無二的能力來暫停自身,這是經過關鍵字yield實現的. 不過,只有控制生成器的迭代器具備恢復生成器的能力(經過next(..))

​ yield/next(..)這一對不僅是一種控制機制,實際上也是一種雙向消息機制. yield..表達式本質上是暫停下來等待某個值,接下來的next(...)調用會向被暫停的yield表達式傳回一個值(或者是隱式的undefined)

​ 在異步控制流程方面,生成器的關鍵優勢是:

​ 生成器內部的代碼是天然的同步/順序方式表達任務的一系列步驟. 其技巧在於,咱們把可能的異步隱藏在了關鍵字yield的後面,把異步移動到控制生成器的迭代器的代碼部分.

換句話說,生成器爲異步代碼保持了順序,同步,阻塞的代碼模式,這使大腦能夠更天然地追蹤代碼,解決了基於回調的異步的兩個關鍵字缺陷之一.


原文地址: 傳送門

Github歡迎Star: wq93

相關文章
相關標籤/搜索