這些 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 鏈)的定義放在程式碼中的不同位置。如果沒有輔助機制的話,在這種模式下很難這樣實現。
福利時刻
給大家準備幾本書,相信一些小夥伴肯定會喜歡,它長這樣
這本書主要從Vue。js框架技術的基礎概念出發,逐步深入Vue。js進階實戰,並在後配合一個網站專案和一個後臺系統開發實戰案例,重點介紹了使用Vue。js axios ElementUI wangEditor進行前端開發和使用元件進行Vue。js單頁面網頁複用,讓讀者不但可以系統地學習Vue。js前端開發框架的相關知識,而且還能對業務邏輯的分析思路、實際應用開發有更為深入的理解
然後在這裡也免費給大家包郵送出幾本,
點選
下方卡片回覆888
即可參與了,提前祝大家好運
作者:gyx_這個殺手不太冷靜
https://juejin。cn/post/6859133591108976648#heading-10