這些JavaScript 細節(jié),你未必知道
大廠技術(shù) 高級前端 Node進階
點擊上方 程序員成長指北,關(guān)注公眾號
回復(fù)1,加入高級Node交流群
前言
本文主要給大家?guī)硪恍┪易x《你不知道的 JavaScript(中卷)》中遇到的一些有意思的內(nèi)容,可以說是打開新世界的大門的感覺。希望能在工作之余,給大家?guī)硪稽c樂趣。
JavaScript 是一門優(yōu)秀的語言。只學(xué)其中一部分內(nèi)容很容易,但是要全面掌握則很難。開發(fā)人員遇到困難時往往將其歸咎于語言本身,而不反省他們自己對語言的理解有多匱乏。《你不知道的 JavaScript》旨在解決這個問題,使讀者能夠發(fā)自內(nèi)心地喜歡上這門語言。
強制類型轉(zhuǎn)換
值類型轉(zhuǎn)換
var a = 42;
var b = a + ""; // 隱式強制類型轉(zhuǎn)換
var c = String(a); // 顯式強制類型轉(zhuǎn)換
抽象值操作
document.all是假值對象。也就是!!document.all值為false。
顯示強制類型轉(zhuǎn)換
日期顯示轉(zhuǎn)換為數(shù)字:
使用 Date.now() 來獲得當前的時間戳,使用 new Date(..).getTime() 來獲得指定時間的時間戳。
奇特的 ~ 運算符:
~x 大致等同于 -(x+1)。很奇怪,但相對更容易說明問題:~42; // -(42+1) ==> -43
JavaScript 中字符串的 indexOf(..) 方法也遵循這一慣例,該方法在字符串中搜索指定的子 字符串,如果找到就返回子字符串所在的位置(從 0 開始),否則返回 -1。
~ 和 indexOf() 一起可以將結(jié)果強制類型轉(zhuǎn)換(實際上僅僅是轉(zhuǎn)換)為真 / 假值:
var a = "Hello World";
~a.indexOf("lo"); // -4 <-- 真值!
if (~a.indexOf("lo")) { // true
// 找到匹配!
}
解析非字符串:
曾經(jīng)有人發(fā)帖吐槽過 parseInt(..) 的一個坑:
parseInt( 1/0, 19 ); // 18
parseInt(1/0, 19) 實際上是 parseInt("Infinity", 19)。第一個字符是 "I",以 19 為基數(shù) 時值為 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
隱式強制類型轉(zhuǎn)換
字符串和數(shù)字之間的隱式強制類型轉(zhuǎn)換
例如:
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"
根據(jù) ES5 規(guī)范 11.6.1 節(jié),如果某個操作數(shù)是字符串或者能夠通過以下步驟轉(zhuǎn)換為字符串的話,+ 將進行拼接操作。如果其中一個操作數(shù)是對象(包括數(shù)組),則首先對其調(diào)用 ToPrimitive 抽象操作(規(guī)范 9.1 節(jié)),該抽象操作再調(diào)用 [[DefaultValue]](規(guī)范 8.12.8 節(jié)),以數(shù)字作為上下文。
你或許注意到這與 ToNumber 抽象操作處理對象的方式一樣(參見 4.2.2 節(jié))。因為數(shù)組的 valueOf() 操作無法得到簡單基本類型值,于是它轉(zhuǎn)而調(diào)用 toString()。因此上例中的兩個數(shù)組變成了 "1,2" 和 "3,4" 。+ 將它們拼接后返回 "1,23,4" 。
簡單來說就是,如果 + 的其中一個操作數(shù)是字符串(或者通過以上步驟可以得到字符串),則執(zhí)行字符串拼接;否則執(zhí)行數(shù)字加法。
符號的強制類型轉(zhuǎn)換
ES6 允許從符號到字符串的顯式強制類型轉(zhuǎn)換,然而隱式強制類型轉(zhuǎn)換會產(chǎn)生錯誤,具體的原因不在本書討論范圍之內(nèi)。
例如:
var s1 = Symbol("cool");
String(s1); // "Symbol(cool)"
var s2 = Symbol("not cool");
s2 + ""; // TypeError
符號不能夠被強制類型轉(zhuǎn)換為數(shù)字(顯式和隱式都會產(chǎn)生錯誤),但可以被強制類型轉(zhuǎn)換為布爾值(顯式和隱式結(jié)果都是 true)。
由于規(guī)則缺乏一致性,我們要對 ES6 中符號的強制類型轉(zhuǎn)換多加小心。
好在鑒于符號的特殊用途,我們不會經(jīng)常用到它的強制類型轉(zhuǎn)換。
寬松相等和嚴格相等
常見的誤區(qū)是“== 檢查值是否相等,=== 檢查值和類型是否相等”。聽起來蠻有道理,然而還不夠準確。很多 JavaScript 的書籍和博客也是這樣來解釋的,但是很遺憾他們都錯了。
正確的解釋是:“== 允許在相等比較中進行強制類型轉(zhuǎn)換,而 === 不允許。”
字符串和數(shù)字之間的相等比較:
-
如果 Type(x) 是數(shù)字,Type(y) 是字符串,則返回 x == ToNumber(y) 的結(jié)果。 -
如果 Type(x) 是字符串,Type(y) 是數(shù)字,則返回 ToNumber(x) == y 的結(jié)果。
其他類型和布爾類型之間的相等比較:
-
如果 Type(x) 是布爾類型,則返回 ToNumber(x) == y 的結(jié)果; -
如果 Type(y) 是布爾類型,則返回 x == ToNumber(y) 的結(jié)果。
null 和 undefined 之間的相等比較:
-
如果 x 為 null,y 為 undefined,則結(jié)果為 true。 -
如果 x 為 undefined,y 為 null,則結(jié)果為 true。
對象和非對象之間的相等比較:
-
如果 Type(x) 是字符串或數(shù)字,Type(y) 是對象,則返回 x == ToPrimitive(y) 的結(jié)果; -
如果 Type(x) 是對象,Type(y) 是字符串或數(shù)字,則返回 ToPromitive(x) == y 的結(jié)果。
語法
錯誤
提前使用變量
ES6 規(guī)范定義了一個新概念,叫作 TDZ(Temporal Dead Zone,暫時性死區(qū))。
TDZ 指的是由于代碼中的變量還沒有初始化而不能被引用的情況。
對此,最直觀的例子是 ES6 規(guī)范中的 let 塊作用域:
{
a = 2; // ReferenceError!
let a;
}
a = 2 試圖在 let a 初始化 a 之前使用該變量(其作用域在 { .. } 內(nèi)),這里就是 a 的 TDZ,會產(chǎn)生錯誤。
有意思的是,對未聲明變量使用 typeof 不會產(chǎn)生錯誤(參見第 1 章),但在 TDZ 中卻會報錯:
{
typeof a; // undefined
typeof b; // ReferenceError! (TDZ)
let b;
}
回調(diào)
省點回調(diào)
構(gòu)造一個超時驗證工具:
function timeoutify(fn, delay) {
var intv = setTimeout(function() {
intv = null
fn(new Error('Timeout!'))
}, delay)
return function() {
// 還沒有超時?
if (intv) {
clearTimeout(intv)
fn.apply(this, arguments)
}
}
}
以下是使用方式:
// 使用 ‘error-first 風(fēng)格’ 回調(diào)設(shè)計
function foo(err, data) {
if (err) {
console.error(err)
}
else {
console.log(data)
}
}
ajax('http://some.url.1', timeoutify(foo, 500))
如果你不確定關(guān)注的 API 會不會永遠異步執(zhí)行怎么辦呢?可以創(chuàng)建一個類似于這個“驗證概念”版本的 asyncify(..) 工具:
function asyncify(fn) {
var orig_fn = fn,
intv = setTimeout(function() {
intv = null
if (fn) fn()
}, 0)
fn = null
return function() {
// 觸發(fā)太快,在定時器intv觸發(fā)指示異步轉(zhuǎn)換發(fā)生之前?
if (intv) {
fn = orig_fn.bind.apply(
orig_fn,
// 把封裝器的this添加到bind(..)調(diào)用的參數(shù)中,
// 以及克里化(currying)所有傳入?yún)?shù)
[this].concat([].slice.call(arguments))
)
}
// 已經(jīng)是異步
else {
// 調(diào)用原來的函數(shù)
orig_fn.apply(this, arguments)
}
}
}
可以像這樣使用 asyncify(..):
function result(data) {
console.log(a)
}
var a = 0
ajax('..pre-cached-url..', asyncify(result))
a++
不管這個 Ajax 請求已經(jīng)在緩存中并試圖對回調(diào)立即調(diào)用,還是要從網(wǎng)絡(luò)上取得,進而在將來異步完成,這段代碼總是會輸出 1,而不是 0——result(..) 只能異步調(diào)用,這意味著 a++ 有機會在 result(..) 之前運行。
關(guān)于回調(diào)地獄的可以看:JS中優(yōu)雅的使用async await
Promise
Promise 信任問題
回調(diào)未調(diào)用
提供一個超時處理的解決方案:
// 用于超時一個Promise的工具
function timeoutPromise(delay) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
reject('Timeout!')
}, delay)
})
}
// 設(shè)置foo()超時
Promise.race([
foo(),
timeoutPromise(3000)
])
.then(
function() {
// foo(..)及時完成!
},
function(err) {
// 或者foo()被拒絕,或者只是沒能按時完成
// 查看err來了解是哪種情況
}
)
鏈式流
為了進一步闡釋鏈接,讓我們把延遲 Promise 創(chuàng)建(沒有決議消息)過程一般化到一個工具中,以便在多個步驟中復(fù)用:
function delay(time) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, time)
})
}
delay(100) // 步驟1
.then(function STEP2() {
console.log("step 2 (after 100ms)")
return delay(200)
})
.then(function STEP3() {
console.log("step 3 (after another 200ms)")
})
.then(function STEP4() {
console.log("step 4 (next Job)")
return delay(50)
})
.then(function STEP5() {
console.log("step 5 (after another 50ms)")
})
調(diào)用 delay(200) 創(chuàng)建了一個將在 200ms 后完成的 promise,然后我們從第一個 then(..) 完成回調(diào)中返回這個 promise,這會導(dǎo)致第二個 then(..) 的 promise 等待這個 200ms 的 promise。
Promise 局限性
順序錯誤處理
Promise 的設(shè)計局限性(鏈式調(diào)用)造成了一個讓人很容易中招的陷阱,即 Promise 鏈中的錯誤很容易被無意中默默忽略掉。
關(guān)于 Promise 錯誤,還有其他需要考慮的地方。由于一個 Promise 鏈僅僅是連接到一起的成員 Promise,沒有把整個鏈標識為一個個體的實體,這意味著沒有外部方法可以用于觀察可能發(fā)生的錯誤。
如果構(gòu)建了一個沒有錯誤處理函數(shù)的 Promise 鏈,鏈中任何地方的任何錯誤都會在鏈中一直傳播下去,直到在某個步驟注冊拒絕處理函數(shù)。在這個特定的例子中,只要有一個指向鏈中最后一個 promise 的引用就足夠了(下面代碼中的 p),因為你可以在那里注冊拒絕處理函數(shù),而且這個處理函數(shù)能夠得到所有傳播過來的錯誤的通知:
// foo(..), STEP2(..)以及STEP3(..)都是支持promise的工具
var p = foo(42)
.then(STEP2)
.then(STEP3);
雖然這里可能令人迷惑,但是這里的 p 并不指向鏈中的第一個 promise(調(diào)用 foo(42) 產(chǎn)生的那一個),而是指向最后一個 promise,即來自調(diào)用 then(STEP3) 的那一個。
還有,這個 Promise 鏈中的任何一個步驟都沒有顯式地處理自身錯誤。這意味著你可以在 p 上注冊一個拒絕錯誤處理函數(shù),對于鏈中任何位置出現(xiàn)的任何錯誤,這個處理函數(shù)都會得到通知:
p.catch(handleErrors);
但是,如果鏈中的任何一個步驟事實上進行了自身的錯誤處理(可能以隱藏或抽象的不可見的方式),那你的 handleErrors(..) 就不會得到通知。這可能是你想要的——畢竟這是一個“已處理的拒絕”——但也可能并不是。不能清晰得到(對具體某一個“已經(jīng)處理”的拒絕的)錯誤通知也是一個缺陷,它限制了某些用例的功能。
基本上,這等同于 try..catch 存在的局限:try..catch 可能捕獲一個異常并簡單地吞掉它。所以這并不是 Promise 獨有的局限性,但可能是我們希望繞過的陷阱。
遺憾的是,很多時候并沒有為 Promise 鏈序列的中間步驟保留的引用。因此,沒有這樣的引用,你就無法關(guān)聯(lián)錯誤處理函數(shù)來可靠地檢查錯誤。
關(guān)于Promise你還可以看這個:一道讓人失眠的 Promise 試題深入分析
單一值
根據(jù)定義,Promise 只能有一個完成值或一個拒絕理由。在簡單的例子中,這不是什么問題,但是在更復(fù)雜的場景中,你可能就會發(fā)現(xiàn)這是一種局限了。
一般的建議是構(gòu)造一個值封裝(比如一個對象或數(shù)組)來保持這樣的多個信息。這個解決方案可以起作用,但要在 Promise 鏈中的每一步都進行封裝和解封,就十分丑陋和笨重了。
-
分裂值
有時候,你可以把這一點,當作提示你應(yīng)該把問題分解為兩個或更多 Promise 的信號。
設(shè)想你有一個工具 foo(..),它可以異步產(chǎn)生兩個值(x 和 y):
function getY(x) {
return new Promise(function(resolve, reject){
setTimeout(function(){
resolve((3 * x) - 1);
}, 100);
});
}
function foo(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(..) 返回的內(nèi)容,這樣就不再需要把 x 和 y 封裝到一個數(shù)組值中以通過 promise 傳輸。取而代之的是,我們可以把每個值封裝到它自己的 promise:
function foo(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 數(shù)組真的要優(yōu)于傳遞給單個 promise 的一個值數(shù)組嗎?從語法的角度來說,這算不上是一個改進。
但是,這種方法更符合 Promise 的設(shè)計理念。如果以后需要重構(gòu)代碼把對 x 和 y 的計算分開,這種方法就簡單得多。由調(diào)用代碼來決定如何安排這兩個 promise,而不是把這種細節(jié)放在 foo(..) 內(nèi)部抽象,這樣更整潔也更靈活。這里使用了 Promise.all([ .. ]),當然,這并不是唯一的選擇。
-
傳遞參數(shù)
var x = .. 和 var y = .. 賦值操作仍然是麻煩的開銷。我們可以在輔助工具中采用某種函數(shù)技巧:
function spread(fn) {
return Function.apply.bind(fn, null);
}
Promise.all(
foo(10, 20)
).then(spread(function(x, y){
console.log(x, y); // 200 599
}))
這樣會好一點!當然,你可以把這個函數(shù)戲法在線化,以避免額外的輔助工具:
Promise.all(
foo(10, 20)
).then(Function.apply.bind(
function(x, y){
console.log(x, y); // 200 599
},
null
));
這些技巧可能很靈巧,但 ES6 給出了一個更好的答案:解構(gòu)。數(shù)組解構(gòu)賦值形式看起來是這樣的:
Promise.all(
foo(10, 20)
).then(function(msgs){
var [x, y] = msgs;
console.log(x, y); // 200 599
});
不過最好的是,ES6 提供了數(shù)組參數(shù)解構(gòu)形式:
Promise.all(
foo(10, 20)
)
.then(function([x, y]){
console.log(x, y); // 200 599
});
現(xiàn)在,我們符合了“每個 Promise 一個值”的理念,并且又將重復(fù)樣板代碼量保持在了最小!
單決議
Promise 最本質(zhì)的一個特征是:Promise 只能被決議一次(完成或拒絕)。在許多異步情況中,你只會獲取一個值一次,所以這可以工作良好。
但是,還有很多異步的情況適合另一種模式——一種類似于事件或數(shù)據(jù)流的模式。在表面上,目前還不清楚 Promise 能不能很好用于這樣的用例,如果不是完全不可用的話。如果不在 Promise 之上構(gòu)建顯著的抽象,Promise 肯定完全無法支持多值決議處理。
設(shè)想這樣一個場景:你可能要啟動一系列異步步驟以響應(yīng)某種可能多次發(fā)生的激勵(就像是事件),比如按鈕點擊。
這樣可能不會按照你的期望工作:
// click(..) 把"click"事件綁定到一個 DOM 元素
// request(..) 是前面定義的支持 Promise 的 Ajax
var p = new Promise(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);
});
只有在你的應(yīng)用只需要響應(yīng)按鈕點擊一次的情況下,這種方式才能工作。如果這個按鈕被點擊了第二次的話,promise p 已經(jīng)決議,因此第二個 resolve(..) 調(diào)用就會被忽略。
因此,你可能需要轉(zhuǎn)化這個范例,為每個事件的發(fā)生創(chuàng)建一整個新的 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 序列。
由于需要在事件處理函數(shù)中定義整個 Promise 鏈,這很丑陋。除此之外,這個設(shè)計在某種程度上破壞了關(guān)注點與功能分離(SoC)的思想。你很可能想要把事件處理函數(shù)的定義和對事件的響應(yīng)(那個 Promise 鏈)的定義放在代碼中的不同位置。如果沒有輔助機制的話,在這種模式下很難這樣實現(xiàn)。
感謝
如果本文對你有幫助,就點個贊支持下吧!感謝閱讀。
關(guān)于本文
作者:gyx_這個殺手不太冷靜
https://juejin.cn/post/6859133591108976648
- EOF -
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
“分享、點贊、在看” 支持一波??
