這些 JavaScript 細節,你知道不?

「來源: |Vue中文社群 ID:vue_fe」

本文主要給大家帶來一些我讀《你不知道的 JavaScript(中卷)》中遇到的一些

有意思

的內容,可以說是

開啟新世界的大門

的感覺。希望能在工作之餘,給大家帶來一點樂趣。

在文末小編也給大家準備了一點小福利

JavaScript 是一門優秀的語言。只學其中一部分內容很容易,但是要全面掌握則很難。開發人員遇到困難時往往將其歸咎於語言本身,而不反省他們自己對語言的理解有多匱乏。《你不知道的 JavaScript》旨在解決這個問題,使讀者能夠發自內心地喜歡上這門語言。

強制型別轉換

值型別轉換

var a = 42;

var b = a + “”; // 隱式強制型別轉換

var c = String(a); // 顯式強制型別轉換

複製程式碼

抽象值操作

document。all 是假值物件。也就是 !!document。all 值為 false。

顯示強制型別轉換

日期顯示轉換為數字:

使用 Date。now() 來獲得當前的時間戳,使用 new Date(。。)。getTime() 來獲得指定時間的時間戳。

奇特的 ~ 運算子:

~x 大致等同於 -(x+1)。很奇怪,但相對更容易說明問題: ~42; // \-(42+1) ==> \-43

JavaScript 中字串的 indexOf(。。) 方法也遵循這一慣例,該方法在字串中搜索指定的子 字串,如果找到就返回子字串所在的位置(從 0 開始),否則返回 -1。

~ 和 indexOf() 一起可以將結果強制型別轉換(實際上僅僅是轉換)為真 / 假值:

var a = “Hello World”;

~a。indexOf(“lo”); // -4 <—— 真值!

if (~a。indexOf(“lo”)) { // true

// 找到匹配!

}

複製程式碼

解析非字串:

曾經有人發帖吐槽過 parseInt(。。) 的一個坑:

parseInt( 1/0, 19 ); // 18

複製程式碼

parseInt(1/0, 19) 實際上是 parseInt(“Infinity”, 19)。第一個字元是 “I”,以 19 為基數 時值為 18。

此外還有一些看起來奇怪但實際上解釋得通的例子:

parseInt(0。000008); // 0 (“0” 來自於 “0。000008”)

parseInt(0。0000008); // 8 (“8” 來自於 “8e-7”)

parseInt(false, 16); // 250 (“fa” 來自於 “false”)

parseInt(parseInt, 16); // 15 (“f” 來自於 “function。。”)

parseInt(“0x10”); // 16

parseInt(“103”, 2); // 2

複製程式碼

隱式強制型別轉換

字串和數字之間的隱式強制型別轉換

例如:

var a = “42”;

var b = “0”;

var c = 42;

var d = 0;

a + b; // “420”

c + d; // 42

複製程式碼

再例如:

var a = [1,2];

var b = [3,4];

a + b; // “1,23,4”

複製程式碼

根據 ES5 規範 11。6。1 節,如果某個運算元是字串或者能夠透過以下步驟轉換為字串的話,+ 將進行拼接操作。如果其中一個運算元是物件(包括陣列),則首先對其呼叫 ToPrimitive 抽象操作(規範 9。1 節),該抽象操作再呼叫 [[DefaultValue]](規範 8。12。8 節),以數字作為上下文。

你或許注意到這與 ToNumber 抽象操作處理物件的方式一樣(參見 4。2。2 節)。因為陣列的 valueOf() 操作無法得到簡單基本型別值,於是它轉而呼叫 toString()。因此上例中的兩個陣列變成了 “1,2” 和 “3,4” 。+ 將它們拼接後返回 “1,23,4” 。

簡單來說就是,如果 + 的其中一個運算元是字串(或者透過以上步驟可以得到字串),則執行字串拼接;否則執行數字加法。

符號的強制型別轉換

ES6 允許從符號到字串的顯式強制型別轉換,然而隱式強制型別轉換會產生錯誤,具體的原因不在本書討論範圍之內。

例如:

var s1 = Symbol(“cool”);

String(s1); // “Symbol(cool)”

var s2 = Symbol(“not cool”);

s2 + “”; // TypeError

複製程式碼

符號不能夠被強制型別轉換為數字(顯式和隱式都會產生錯誤),但可以被強制型別轉換為布林值(顯式和隱式結果都是 true)。

由於規則缺乏一致性,我們要對 ES6 中符號的強制型別轉換多加小心。

好在鑑於符號的特殊用途,我們不會經常用到它的強制型別轉換。

寬鬆相等和嚴格相等

常見的誤區是“== 檢查值是否相等,=== 檢查值和型別是否相等”。聽起來蠻有道理,然而還不夠準確。很多 JavaScript 的書籍和部落格也是這樣來解釋的,但是很遺憾他們都錯了。

正確的解釋是:“== 允許在相等比較中進行強制型別轉換,而 === 不允許。”

字串和數字之間的相等比較:

如果 Type(x) 是數字,Type(y) 是字串,則返回 x == ToNumber(y) 的結果。

如果 Type(x) 是字串,Type(y) 是數字,則返回 ToNumber(x) == y 的結果。

其他型別和布林型別之間的相等比較:

如果 Type(x) 是布林型別,則返回 ToNumber(x) == y 的結果;

如果 Type(y) 是布林型別,則返回 x == ToNumber(y) 的結果。

null 和 undefined 之間的相等比較:

如果 x 為 null,y 為 undefined,則結果為 true。

如果 x 為 undefined,y 為 null,則結果為 true。

物件和非物件之間的相等比較:

如果 Type(x) 是字串或數字,Type(y) 是物件,則返回 x == ToPrimitive(y) 的結果;

如果 Type(x) 是物件,Type(y) 是字串或數字,則返回 ToPromitive(x) == y 的結果。

語法

錯誤

提前使用變數

ES6 規範定義了一個新概念,叫作 TDZ(Temporal Dead Zone,暫時性死區)。

TDZ 指的是由於程式碼中的變數還沒有初始化而不能被引用的情況。

對此,最直觀的例子是 ES6 規範中的 let 塊作用域:

{

a = 2; // ReferenceError!

let a;

}

複製程式碼

a = 2 試圖在 let a 初始化 a 之前使用該變數(其作用域在 { 。。 } 內),這裡就是 a 的 TDZ,會產生錯誤。

有意思的是,對未宣告變數使用 typeof 不會產生錯誤(參見第 1 章),但在 TDZ 中卻會報錯:

{

typeof a; // undefined

typeof b; // ReferenceError! (TDZ)

let b;

}

複製程式碼

回撥

省點回調

構造一個超時驗證工具:

functiontimeoutify(fn, delay) {

var intv = setTimeout(function() {

intv = null

fn(newError(‘Timeout!’))

}, delay)

returnfunction() {

// 還沒有超時?

if (intv) {

clearTimeout(intv)

fn。apply(this, arguments)

}

}

}

複製程式碼

以下是使用方式:

// 使用 ‘error-first 風格’ 回撥設計

functionfoo(err, data) {

if (err) {

console。error(err)

}

else {

console。log(data)

}

}

ajax(‘http://some。url。1’, timeoutify(foo, 500))

複製程式碼

如果你不確定關注的 API 會不會永遠非同步執行怎麼辦呢?可以建立一個類似於這個“驗證概念”版本的 asyncify(。。) 工具:

functionasyncify(fn) {

var orig_fn = fn,

intv = setTimeout(function() {

intv = null

if (fn) fn()

}, 0)

fn = null

returnfunction() {

// 觸發太快,在定時器intv觸發指示非同步轉換髮生之前?

if (intv) {

fn = orig_fn。bind。apply(

orig_fn,

// 把封裝器的this新增到bind(。。)呼叫的引數中,

// 以及克里化(currying)所有傳入引數

[this]。concat([]。slice。call(arguments))

}

// 已經是非同步

else {

// 呼叫原來的函式

orig_fn。apply(this, arguments)

}

}

}

複製程式碼

可以像這樣使用 asyncify(。。):

functionresult(data) {

console。log(a)

}

var a = 0

ajax(‘。。pre-cached-url。。’, asyncify(result))

a++

複製程式碼

不管這個 Ajax 請求已經在快取中並試圖對回撥立即呼叫,還是要從網路上取得,進而在將來非同步完成,這段程式碼總是會輸出 1,而不是 0——result(。。) 只能非同步呼叫,這意味著 a++ 有機會在 result(。。) 之前執行。

Promise

Promise 信任問題

回撥未呼叫

提供一個超時處理的解決方案:

// 用於超時一個Promise的工具

functiontimeoutPromise(delay) {

returnnewPromise(function(resolve, reject) {

setTimeout(function() {

reject(‘Timeout!’)

}, delay)

})

}

// 設定foo()超時

Promise。race([

foo(),

timeoutPromise(3000)

])

。then(

function() {

// foo(。。)及時完成!

},

function(err) {

// 或者foo()被拒絕,或者只是沒能按時完成

// 檢視err來了解是哪種情況

}

複製程式碼

鏈式流

為了進一步闡釋連結,讓我們把延遲 Promise 建立(沒有決議訊息)過程一般化到一個工具中,以便在多個步驟中複用:

functiondelay(time) {

returnnewPromise(function(resolve, reject) {

setTimeout(resolve, time)

})

}

delay(100) // 步驟1

。then(functionSTEP2() {

console。log(“step 2 (after 100ms)”)

return delay(200)

})

。then(functionSTEP3() {

console。log(“step 3 (after another 200ms)”)

})

。then(functionSTEP4() {

console。log(“step 4 (next Job)”)

return delay(50)

})

。then(functionSTEP5() {

console。log(“step 5 (after another 50ms)”)

})

複製程式碼

呼叫 delay(200) 建立了一個將在 200ms 後完成的 promise,然後我們從第一個 then(。。) 完成回撥中返回這個 promise,這會導致第二個 then(。。) 的 promise 等待這個 200ms 的 promise。

Promise 侷限性

順序錯誤處理

Promise 的設計侷限性(鏈式呼叫)造成了一個讓人很容易中招的陷阱,即 Promise 鏈中的錯誤很容易被無意中默默忽略掉。

關於 Promise 錯誤,還有其他需要考慮的地方。由於一個 Promise 鏈僅僅是連線到一起的成員 Promise,沒有把整個鏈標識為一個個體的實體,這意味著沒有外部方法可以用於觀察可能發生的錯誤。

如果構建了一個沒有錯誤處理函式的 Promise 鏈,鏈中任何地方的任何錯誤都會在鏈中一直傳播下去,直到在某個步驟註冊拒絕處理函式。在這個特定的例子中,只要有一個指向鏈中最後一個 promise 的引用就足夠了(下面程式碼中的 p),因為你可以在那裡註冊拒絕處理函式,而且這個處理函式能夠得到所有傳播過來的錯誤的通知:

// foo(。。), STEP2(。。)以及STEP3(。。)都是支援promise的工具

var p = foo(42)

。then(STEP2)

。then(STEP3);

複製程式碼

雖然這裡可能令人迷惑,但是這裡的 p 並不指向鏈中的第一個 promise(呼叫 foo(42) 產生的那一個),而是指向最後一個 promise,即來自呼叫 then(STEP3) 的那一個。

還有,這個 Promise 鏈中的任何一個步驟都沒有顯式地處理自身錯誤。這意味著你可以在 p 上註冊一個拒絕錯誤處理函式,對於鏈中任何位置出現的任何錯誤,這個處理函式都會得到通知:

p。catch(handleErrors);

複製程式碼

但是,如果鏈中的任何一個步驟事實上進行了自身的錯誤處理(可能以隱藏或抽象的不可見的方式),那你的 handleErrors(。。) 就不會得到通知。這可能是你想要的——畢竟這是一個“已處理的拒絕”——但也可能並不是。不能清晰得到(對具體某一個“已經處理”的拒絕的)錯誤通知也是一個缺陷,它限制了某些用例的功能。

基本上,這等同於 try。。catch 存在的侷限:try。。catch 可能捕獲一個異常並簡單地吞掉它。所以這並不是 Promise 獨有的侷限性,但可能是我們希望繞過的陷阱。

遺憾的是,很多時候並沒有為 Promise 鏈序列的中間步驟保留的引用。因此,沒有這樣的引用,你就無法關聯錯誤處理函式來可靠地檢查錯誤。

單一值

根據定義,Promise 只能有一個完成值或一個拒絕理由。在簡單的例子中,這不是什麼問題,但是在更復雜的場景中,你可能就會發現這是一種侷限了。

一般的建議是構造一個值封裝(比如一個物件或陣列)來保持這樣的多個資訊。這個解決方案可以起作用,但要在 Promise 鏈中的每一步都進行封裝和解封,就十分醜陋和笨重了。

分裂值

有時候,你可以把這一點,當作提示你應該把問題分解為兩個或更多 Promise 的訊號。

設想你有一個工具 foo(。。),它可以非同步產生兩個值(x 和 y):

functiongetY(x) {

returnnewPromise(function(resolve, reject){

setTimeout(function(){

resolve((3 * x) - 1);

}, 100);

});

}

functionfoo(bar, baz) {

var x = bar * baz;

return getY(x)。then(function(y){

// 把兩個值封裝到容器中

return [x, y];

});

}

foo(10, 20)。then(function(msgs){

var x = msgs[0];

var y = msgs[1];

console。log(x, y); // 200 599

});

複製程式碼

首先,我們重新組織一下 foo(。。) 返回的內容,這樣就不再需要把 x 和 y 封裝到一個數組值中以透過 promise 傳輸。取而代之的是,我們可以把每個值封裝到它自己的 promise:

functionfoo(bar, baz) {

var x = bar * baz;

// 返回兩個 promise

return [

Promise。resolve(x),

getY(x)

];

}

Promise。all(

foo(10, 20)

)。then(function(msgs){

var x = msgs[0];

var y = msgs[1];

console。log(x, y);

});

複製程式碼

一個 promise 陣列真的要優於傳遞給單個 promise 的一個值陣列嗎?從語法的角度來說,這算不上是一個改進。

但是,這種方法更符合 Promise 的設計理念。如果以後需要重構程式碼把對 x 和 y 的計算分開,這種方法就簡單得多。由呼叫程式碼來決定如何安排這兩個 promise,而不是把這種細節放在 foo(。。) 內部抽象,這樣更整潔也更靈活。這裡使用了 Promise。all([ 。。 ]),當然,這並不是唯一的選擇。

傳遞引數

var x = 。。 和 var y = 。。 賦值操作仍然是麻煩的開銷。我們可以在輔助工具中採用某種函式技巧:

functionspread(fn) {

returnFunction。apply。bind(fn, null);

}

Promise。all(

foo(10, 20)

)。then(spread(function(x, y){

console。log(x, y); // 200 599

}))

複製程式碼

這樣會好一點!當然,你可以把這個函式戲法線上化,以避免額外的輔助工具:

Promise。all(

foo(10, 20)

)。then(Function。apply。bind(

function(x, y){

console。log(x, y); // 200 599

},

null

));

複製程式碼

這些技巧可能很靈巧,但 ES6 給出了一個更好的答案:解構。陣列解構賦值形式看起來是這樣的:

Promise。all(

foo(10, 20)

)。then(function(msgs){

var [x, y] = msgs;

console。log(x, y); // 200 599

});

複製程式碼

不過最好的是,ES6 提供了陣列引數解構形式:

Promise。all(

foo(10, 20)

。then(function([x, y]){

console。log(x, y); // 200 599

});

複製程式碼

現在,我們符合了“每個 Promise 一個值”的理念,並且又將重複樣板程式碼量保持在了最小!

單決議

Promise 最本質的一個特徵是:Promise 只能被決議一次(完成或拒絕)。在許多非同步情況中,你只會獲取一個值一次,所以這可以工作良好。

但是,還有很多非同步的情況適合另一種模式——一種類似於事件或資料流的模式。在表面上,目前還不清楚 Promise 能不能很好用於這樣的用例,如果不是完全不可用的話。如果不在 Promise 之上構建顯著的抽象,Promise 肯定完全無法支援多值決議處理。

設想這樣一個場景:你可能要啟動一系列非同步步驟以響應某種可能多次發生的激勵(就像是事件),比如按鈕點選。

這樣可能不會按照你的期望工作:

// click(。。) 把“click”事件繫結到一個 DOM 元素

// request(。。) 是前面定義的支援 Promise 的 Ajax

var p = newPromise(function(resolve, reject){

click(“#mybtn”, resolve);

});

p。then(function(evt){

var btnID = evt。currentTarget。id;

return request(“http://some。url。1/?id=” + btnID);

})。then(function(text){

console。log(text);

});

複製程式碼

只有在你的應用只需要響應按鈕點選一次的情況下,這種方式才能工作。如果這個按鈕被點選了第二次的話,promise p 已經決議,因此第二個 resolve(。。) 呼叫就會被忽略。

因此,你可能需要轉化這個範例,為每個事件的發生建立一整個新的 Promise 鏈:

click(“#mybtn”, function(evt){

var btnID = evt。currentTarget。id;

request(“http://some。url。1/?id=” + btnID)。then(function(text){

console。log(text);

});

});

複製程式碼

這種方法可以工作,因為針對這個按鈕上的每個 “click” 事件都會啟動一整個新的 Promise 序列。

由於需要在事件處理函式中定義整個 Promise 鏈,這很醜陋。除此之外,這個設計在某種程度上破壞了關注點與功能分離(SoC)的思想。你很可能想要把事件處理函式的定義和對事件的響應(那個 Promise 鏈)的定義放在程式碼中的不同位置。如果沒有輔助機制的話,在這種模式下很難這樣實現。

福利時刻

給大家準備幾本書,相信一些小夥伴肯定會喜歡,它長這樣

這些 JavaScript 細節,你知道不?

這本書主要從Vue。js框架技術的基礎概念出發,逐步深入Vue。js進階實戰,並在後配合一個網站專案和一個後臺系統開發實戰案例,重點介紹了使用Vue。js axios ElementUI wangEditor進行前端開發和使用元件進行Vue。js單頁面網頁複用,讓讀者不但可以系統地學習Vue。js前端開發框架的相關知識,而且還能對業務邏輯的分析思路、實際應用開發有更為深入的理解

然後在這裡也免費給大家包郵送出幾本,

點選

下方卡片回覆888

即可參與了,提前祝大家好運

作者:gyx_這個殺手不太冷靜

https://juejin。cn/post/6859133591108976648#heading-10