JavaScript 中的“黑話”,你知道多少?

點擊上方 "程序員小樂"關注, 星標或置頂一起成長

第一時間與你相約

每日英文

Once I pass by your heart, not I don't want to stay, but you refuse to accept.

我曾路過你的心,不是我不想停留,而是你不肯收留。

每日掏心話

願意放棄才不會苦,適度知足才不會悔;記住感恩才不會怨,懂得珍惜才不會愧。

鏈接:segmentfault.com/a/1190000016595592

JavaScript 中的“黑話”,你知道多少?

程序員小樂(ID:study_tech)第 681 次推文 圖片來自網絡

往日回顧:阿里巴巴程序員常用的 15 款開發者工具!你知道幾個?

正文

因為球是圓的,所以不論發生什麼都有可能,對這點我是深信不疑的,但最近我總是在懷疑,JavaScript也是圓的!

什麼是“黑話”

黑話,本指舊時江湖幫會人物的暗語、暗號,往往見於小說,後指流行於某一特殊行業中,非局外人所能瞭解的語言。而本文涉及到的“黑話”,其實是一些利用語言的特徵使用的一些不常見的奇淫技巧,JavaScript的語法是十分簡單靈活的,在項目中建議大家遵從ESLint規範編寫可維護性的代碼,各路神仙們也應該進行自我約束,畢竟“黑話”也並不全是什麼好的東西,如果很多話可以直接講,何必拐彎抹角的去說呢?

“算術”

算術中的位運算已被作者列為禁術,因此希望你在工程中使用位運算時,請確保你有充足的理由使用,並在需要時寫好Hack註釋。

!與!!

!為邏輯非操作符,可以應用於ECMAScript中的任何值,無論這個值是什麼類型,它會被強制轉化為一個布爾值變量,再對其值取反。

!!只是單純的將操作數執行兩次邏輯非,它能將任意類型的值轉化為相應的布爾值,它包含的步驟為:

  • 將一個值轉化為布爾值;

  • 將其取反;

  • 再次取反。

假設你需要通過一個布爾型變量表示是否有id值,以下寫法推薦你使用最後一種方式來進行轉化:

const enable1 = !!id;const enable2 = id ? true : false;const enable3 = Boolean(id);

~ 與 ~~

~表示按位取反,~5的運行步驟為:

  • 轉為一個字節的二進制表示:00000101,

  • 按位取反:11111010

  • 取其反碼:10000101

  • 取其補碼:10000110

  • 轉化為十進制:-6

至於原碼、反碼、補碼原理請看原理篇。

~~它代表雙非按位取反運算符,如果你想使用比Math.floor()更快的方法,那就是它了。需要注意,對於正數,它向下取整;對於負數,向上取整;非數字取值為0,它具體的表現形式為:

~~null; // => 0~~undefined; // => 0~~Infinity; // => 0--NaN; // => 0~~0; // => 0~~{}; // => 0~~[]; // => 0~~(1/0); // => 0~~false; // => 0~~true; // => 1~~1.9; // => 1~~-1.9; // => -1

+

在變量值前使用+的本意是將變量轉換為數字,在一個函數接受數字類型的參數時特別有用:

+'1' // 1+'-1' // '-1+[] // 0+{} // NaN

根據觀察,+a與a * 1結果類似。除此之外,使用+也可以作為立即執行函數:+function() {}(),等效於(function(){})()。

字符串與數字相加時會將數值默認轉為字符串,因此有了一下將數字轉為字符串的快捷方法:'' + 1。

& 與 &&

如何你是從類C語言過來的話,請拋棄之前的刻板印象:&可以充當邏輯操作符號。在JavaScript中,&只能進行位運算。

&,它表示按位與,此運算符需要兩個數字並返回一個數字。如果它們不是數字,則會轉換為數字。如果執行7 & 3, 則會經過以下步驟:

  • 先轉換為2進制:111 & 11

  • 比較結果為:011

  • 將二進制轉回十進制,因此:7 & 3 = 3

它也可用於基偶數判斷:const isOdd = num => !!(num & 1);

&&,表示邏輯與,通常用於if條件判斷,可跟你想象的不太一樣,&&並不是單純的返回true或者false,而是依據:

  • 若第一個表達式為false,則返回第一個表達式;

  • 若第一個表達式為true,返回第二個表達式。

在這裡舉幾個例子:

0 && false 0 (both are false-y, but 0 is the first)true && false false (second one is false-y)true && true true (both are true-y)true && 20 20 (both are true-y)

&&可以連接多個操作符,如:a && b && c && d,返回值的規則與上面一樣。除此以外,它還經常被作為短路邏輯使用:若前面表達式不是truthy,則不會繼續執行之後的表達式。如在取一個對象的屬性,我們需要先判斷是否為空才能進行取值,否則會拋出Uncaught TypeError,這種情況下一般我們也會通過邏輯或,給與表達式一個默認值:

const value = obj && obj.value || false

當JavaScript壓縮工具遇到if判斷時,也會使用&&短路邏輯從而節省內存空間:

// beforeif (test) { alert('hello') }// aftertest && alert('hello')

| 與 ||

它們與&和&&使用方法很相似,不同的是它們表示的是邏輯或,因此使用|會進行按位或運算,而||會返回第一個Truthy值。

使用||進行默認值賦值在JavaScript中十分常見,這樣可以省略很多不必要的if語句,比如:

  • // before

  • let res;

  • if (a) {

  • res = a;

  • } else if (b) {

  • res = b;

  • } else if (c) {

  • res = c;

  • } else {

  • res = 1;

  • }

  • // after

  • const res = a || b || c || 1;

== 與 ===

==為相等運算符,操作符會先將左右兩邊的操作數強制轉型,轉換為相同的操作數,再進行相等性比較。

===為全等運算符,它除了在比較時不會將操作數強制轉型,其餘相等判斷與==一致。

簡單而言, ==用於判斷值是否相等, ===判斷值與類型是否都相等,因此使用全等運算符判斷操作數會更準確,新手也在學習JavaScript接收到的前幾條Tips就是避免使用相等運算符,真的是這樣嗎?沒錯,這樣能確保在你不徹底熟悉語言的情況下,儘可能的去避免犯錯,但是我們也應該清楚在哪些情況下應該使用相等運算符,規則往往只針對於新手,而對聰明的你來說,最重要的是要清楚自己在做什麼。

相等操作符對於不同類型的值,進行的比較如下圖所示:

JavaScript 中的“黑話”,你知道多少?

針對於undefined與null:undefined與null互等,與其餘任意對象都不相等,因此在某些lib裡,你可能會看到如下寫法:

if (VAR == undefined) {}if (VAR == null) {}

它等效於:

if (VAR === undefined || VAR === null) {}

對於 '', false, 0而言,他們都屬於Falsy類型,通過Boolean對象都會轉換為假值,而通過==判斷三者的關係,他們總是相等的,因為在比較值時它們會因為類型不同而都被轉換為false值:

console.log((false == 0) && (0 == '') && ('' == false)) // true

或者有時候我們希望利用強轉特性比較字符串與數字:

console.log(11 == '11') // trueconsole.log(11 === '11') // false

^

按位異或運算符,對比每一個比特位,當比特位不相同時則返回1,否則返回0。很少人在Web開發中使用此運算符吧,除了傳說中的一種場景:交換值。

若要交換a與b的值,如果可以的話推薦你使用:

[a, b] = [b, a];

或者新建一個c,用於存儲臨時變量,如果你遇到有人這樣書寫:

// 異或運算,相同位取0,不同位取1,a ^ b ^ b = a, a ^ a ^ b = ba = a ^ bb = a ^ ba = a ^ b

這樣通過異或運算進行交換兩個數字型變量,請原諒他並忽視它,他只可能是一個醉心於魔法的初心者,並祝願他早日發現,簡潔易讀的函數才是最佳實踐。

數值表示法

3e9

科學計數法是一種數學術語,將一個數表示為a乘以10的n次方,如光速30萬公里每秒,在計算中通常將米做單位,則記為:300000000m/s,而在JavaScript中我們可使用科學計數法 3e9表示。

在這裡舉幾個科學計數法的示例:

1e5; // 1000002e-4; // 0.0002-3e3; // -3000

Number對象有toExponential(fractionDigits)方法以科學計數法返回該數值的字符串表示形式,參數fractionDigits可選,用於用來指定小數點後有幾位數字,例如:(179000).toExponential(); // "1.79e+5"。

以下情況JavaScript會自動將數值轉為科學計數法表示:

  • 小數點前的數字多於21位。

  • 小數點後的零多於5個。

.5px

通常某些人習慣省略0.開頭的數字,常見於數值計算、css屬性中,比如0.5px可直接寫為.5px,0.2 * 0.3可寫為:.2 * .3

0x、0o和0b

在十進制的世界裡呆久了,請不要忘記還有其他進制的存在,在計算機中它們是同地位的。JavaScript提供了以下進制的表示方法:

  • 二進制:只用0和1兩個數字,前綴為0b,十進制13可表示為0b1101

  • 八進制:只用0到7八個數字,前綴為0o、0,十進制13可表示為0o15、015

  • 十六進制:只用0到9的十個數字,和a到f六個字母,前綴為0x,十進制13可表示為0xd

默認情況下,JavaScript 內部會自動將八進制、十六進制、二進制轉為十進制再進行運算。從十進制轉其他進制請查閱toString方法,從其他進制轉十進制請查閱parseInt方法,從其他進制轉其他進制請先轉為十進制再轉為其他方法。

“話術”

Array.prototype.sort

Array.prototype.sort()默認根據字符串的Unicode編碼進行排序,具體算法取決於實現的瀏覽器,在v8引擎中,若數組長度小於10則使用從插入排序,大於10使用的是快排。

而sort支持傳入一個compareFunction(a, b)的參數,其中a、b為數組中進行比較的兩個非空對象(所有空對象將會排在數組的最後),具體比較規則為:

  • 返回值小於0,a排在b的左邊

  • 返回值等於0,a和b的位置不變

  • 返回值大於0,a排在b的右邊

因此利用sort即可寫一個打亂數組的方法:

[1,2,3,4].sort(() => .5 - Math.random())

但是以上的實現並不是完全隨機的,究其原因,還是因為排序算法的不穩定性,導致一些元素沒有機會進行比較,具體請參考問題,在抽獎程序中若要實現完全隨機,請使用 Fisher–Yates shuffle 算法,以下是簡單實現:

function shuffle(arrs) { for (let i = arrs.length - 1; i > 0; i -= 1) { const random = Math.floor(Math.random() * (i + 1)); [arrs[random], arrs[i]] = [arrs[i], arrs[random]]; }}

Array.prototype.concat.apply

apply接收數組類型的參數來調用函數,而concat接收字符串或數組的多個參數,因此可使用此技巧將二維數組直接展平:

Array.prototype.concat.apply([], [1, [2,3], [4]])

而通過此方法也可以寫一個深層次遍歷的方法:

function flattenDeep(arrs) { let result = Array.prototype.concat.apply([], arrs); while (result.some(item => item instanceof Array)) { result = Array.prototype.concat.apply([], result); } return result;}

經過測試,效率與lodash對比如下:

JavaScript 中的“黑話”,你知道多少?

對上述方法中的Array.prototype.concat.apply([], target)亦可以寫成:[].concat(...target)。

Array.prototype.push.apply

在es5中,若想要對數組進行拼接操作,我們習慣於使用數組中的concat方法:

let arrs = [1, 2, 3];arrs = arrs.concat([4,5,6]);

但還有酷的方法,利用apply方法的數組傳參特性,可以更簡潔的執行拼接操作:

const arrs = [1, 2, 3];arrs.push.apply(arrs, [4, 5, 6]);

Array.prototype.length

它通常用於返回數組的長度,但是也是一個包含有複雜行為的屬性,首先需要說明的是,它並不是用於統計數組中元素的數量,而是代表數組中最高索引的值:

const arrs = [];arrs[5] = 1;console.log(arrs.length); // 6

另外,length長度隨著數組的變化而變化,但是這種變化僅限於:子元素最高索引值的變化,假如使用delete方法刪除最高元素,length是不會變化的,因為最高索引值也沒變:

const arrs = [1, 2, 3];delete arrs[2]; // 長度依然為3

length還有一個重要的特性,那就是允許你修改它的值,若修改值小於數組本身的最大索引,則會對數組進行部分截取:

const arrs = [1, 2, 3, 4];arrs.length = 2; // arrs = [1, 2]arrs.length = 0; // arrs = []

若賦予的值大於當前最大索引,則會得到一個稀疏數組:

const arrs = [1, 2];arrs.length = 5; // arrs = [1, 2,,,,]

若將值賦為0,則執行了清空數組的操作:

const arrs = [1, 2, 3, 4];arrs.length = 0; // arrs = []

使用此方法會將數組中的所有索引都刪除掉,因此也會影響其他引用此數組的值,這點跟使用arrs = []有很大的區別:

let a = [1,2,3];let b = [1,2,3];let a1 = a;let b1 = b;a = [];b.length = 0;console.log(a, b, a1, b1); // [], [], [1, 2, 3], []

在對length進行修改的時候,還需要注意:

  • 值需要為正整數

  • 傳遞字符串會被嘗試轉為數字類型

Object.prototype.toString.call

每個對象都有一個toString(),用於將對象以字符串方式引用時自動調用,如果此方法未被覆蓋,toString則會返回[object type],因此Object.prototype.toString.call只是為了調用原生對象上未被覆蓋的方法,call將作用域指向需要判斷的對象,這樣一來就可以通過原生的toString方法打印對象的類型字符串:Object.prototype.toString.call([]) => "[object Array]" ,利用這個特性,可以較為精確的實現類型判斷。

在ES3中,獲取到的type為內部屬性[[Class]]屬性,它可以用來判斷一個原生屬性屬於哪一種內置的值;在ES5中新增了兩條規則:若this值為null、undefined分別返回:[object Null]、[object Undefined];在ES6中不存在[[Class]]了,取而代之的是一種內部屬性:[[NativeBrand]],它是一種標記值,用於區分原生對象的屬性,具體的判斷規則為:

  • 19.1.3.6Object.prototype.toString ( )

  • When the toString method is called, the following steps are taken:

  • If the this value is undefined, return "[object Undefined]".

  • If the this value is null, return "[object Null]".

  • Let O be ! ToObject(this value).

  • Let isArray be ? IsArray(O).

  • If isArray is true, let builtinTag be "Array".

  • Else if O is a String exotic object, let builtinTag be "String".

  • Else if O has a [[ParameterMap]] internal slot, let builtinTag be "Arguments".

  • Else if O has a [[Call]] internal method, let builtinTag be "Function".

  • Else if O has an [[ErrorData]] internal slot, let builtinTag be "Error".

  • Else if O has a [[BooleanData]] internal slot, let builtinTag be "Boolean".

  • Else if O has a [[NumberData]] internal slot, let builtinTag be "Number".

  • Else if O has a [[DateValue]] internal slot, let builtinTag be "Date".

  • Else if O has a [[RegExpMatcher]] internal slot, let builtinTag be "RegExp".

  • Else, let builtinTag be "Object".

  • Let tag be ? Get(O, @@toStringTag).

  • If Type(tag) is not String, set tag to builtinTag.

  • Return the string-concatenation of "[object ", tag, and "]".

  • This function is the %ObjProto_toString% intrinsic object.

  • NOTE

  • Historically, this function was occasionally used to access the String value of the [[Class]] internal slot that was used in previous editions of this specification as a nominal type tag for various built-in objects. The above definition of toString preserves compatibility for legacy code that uses toString as a test for those specific kinds of built-in objects. It does not provide a reliable type testing mechanism for other kinds of built-in or program defined objects. In addition, programs can use @@toStringTag in ways that will invalidate the reliability of such legacy type tests.

Object.create(null)

用於創建無“副作用”的對象,也就是說,它創建的是一個空對象,不包含原型鏈與其他屬性。若使用const map = {}創建出來的對象相當於Object.create(Object.prototype),它繼承了對象的原型鏈。

JSON.parse(JSON.stringify(Obj))

很常用的一種深拷貝對象的方式,將對象進行JSON字符串格式化再進行解析,即可獲得一個新的對象,要注意它的性能不是特別好,而且無法處理閉環的引用,比如:

const obj = {a: 1};obj.b = obj;JSON.parse(JSON.stringify(obj)) // Uncaught TypeError: Converting circular structure to JSON

這樣通過JSON解析的方式其實性能並不高,若對象可通過淺拷貝複製請一定使用淺拷貝的方式,不管你使用{...obj}還是Object.assign({}, obj)的方式,而如果對性能有要求的情況下,請不要再造輪子了,直接使用npm:clone這個包或是別的吧。

“理論”

Truthy與Falsy

對每一個類型的值來講,它每一個對象都有一個布爾型的值,Falsy表示在Boolean對象中表現為false的值,在條件判斷與循環中,JavaScript會將任意類型強制轉化為Boolean對象。以下這些對象在遇到if語句時都表現為Falsy:

if (false)if (null)if (undefined)if (0)if (NaN)if ('')if ("")if (document.all)

document.all屬於歷史遺留原因,所以為false,它違背了JavaScript的規範,可以不管它,而NaN這個變量,千萬不要用全等或相等對其進行判斷,因為它發起瘋來連自己都打:

console.log(NaN === 0) // falseconsole.log(NaN === NaN) // falseconsole.log(NaN == NaN) // false

但是我們可以使用Object.is方法進行判斷值是否為NaN,它是ES6新加入的語法,用於比較兩個值是否相同,它可以視為比全等判斷符更為嚴格的判斷方法,但是不可混為一談:

Object.is(NaN, NaN) // trueObject.is(+0, -0) // false

而除了Falsy值,所有值都是Truthy值,在Boolean上下文中表現為true。

原碼, 反碼, 補碼

在JavaScript進行位運算時,採用32位有符號整型,即數字5有以下表示方式:

  • 原碼:00000000 00000000 00000000 00000101

  • 反碼:00000000 00000000 00000000 00000101

  • 補碼:00000000 00000000 00000000 00000101

而數字-5的表示方式為:

  • 原碼:10000000 00000000 00000000 00000101

  • 反碼:11111111 11111111 11111111 11111010

  • 補碼:11111111 11111111 11111111 11111011

綜上所述,有以下規律:

  • 正數的原碼、反碼、補碼都是它本身

  • 負數的反碼:在其原碼的基礎上, 符號位不變,其餘各個位取反

  • 負數的補碼:負數的反碼 + 1

那麼它們到底有什麼用呢?其實位運算就是用計算機底層電路所有運算的基礎,為了讓計算機的運算更加簡單,而不用去辨別符號位,所有值都採用加法運算,因此,人們設計了原碼,通過符號位來標識數字的正負:

1 = 0000 0001-1 = 1000 0001

假如計算機要對兩個數相加:1 + (-1),使用原碼相加的運算結果為:10000010,很明顯-2並不是我們想要的結果,因此出現了反碼,若使用反碼進行運算會有什麼結果呢,讓我們來看一下:

1[反碼] + (-1)[反碼] = 0000 0001 + 1111 1110 = 11111111[反碼] = 10000000[原碼]

此時運算結果是正確的,可是這樣還存在一個問題,有兩個值可以表示0:1000 0000、0000 0000,對於計算機來說,0帶符號是沒有任何意義的,人們為了優化0的存在,設計出了補碼:

1[補碼] + (-1)[補碼] = 0000 0001 + 1111 1111 = 00000000[原碼]

這樣一來,-0的問題就可以解決了。

modernweb.com/45-useful-javascript-tips-tricks-and-best-practices/

dmitripavlutin.com/the-magic-behind-array-length-property/

medium.freecodecamp.org/9-neat-javascript-tricks-e2742f2735c3

stackoverflow.com/questions/7310109/whats-the-difference-between-and-in-javascript

javascript.ruanyifeng.com/grammar/number.html

歡迎在留言區留下你的觀點,一起討論提高。如果今天的文章讓你有新的啟發,學習能力的提升上有新的認識,歡迎轉發分享給更多人。

猜你還想看

阿里、騰訊、百度、華為、京東最新面試題彙集

談談最近的面試感悟和人生

優化你的SpringBoot,看這篇文章就對了!

Linux 命令行的藝術,看這篇文章就對了!

嘿,你在看嗎?


分享到:


相關文章: