因為球是圓的,所以不論發生什麼都有可能,對這點我是深信不疑的,但最近我總是在懷疑,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&&false0(both are false-y, but 0is the first)true&&falsefalse(second one isfalse-y)true&&truetrue(both are true-y)true&&2020(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語句,比如:
// beforelet res;if(a){ res = a;}elseif(b){ res = b;}elseif(c){ res = c;}else{ res =1;}// afterconst res = a || b || c ||1;== 與 ===
==為相等運算符,操作符會先將左右兩邊的操作數強制轉型,轉換為相同的操作數,再進行相等性比較。
===為全等運算符,它除了在比較時不會將操作數強制轉型,其餘相等判斷與==一致。
簡單而言, ==用於判斷值是否相等, ===判斷值與類型是否都相等,因此使用全等運算符判斷操作數會更準確,新手也在學習JavaScript接收到的前幾條Tips就是避免使用相等運算符,真的是這樣嗎?沒錯,這樣能確保在你不徹底熟悉語言的情況下,儘可能的去避免犯錯,但是我們也應該清楚在哪些情況下應該使用相等運算符,規則往往只針對於新手,而對聰明的你來說,最重要的是要清楚自己在做什麼。
相等操作符對於不同類型的值,進行的比較如下圖所示:
針對於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;// -3000Number對象有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 instanceofArray)){ result =Array.prototype.concat.apply([], result);}return result;}經過測試,效率與lodash對比如下:
對上述方法中的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];// 長度依然為3length還有一個重要的特性,那就是允許你修改它的值,若修改值小於數組本身的最大索引,則會對數組進行部分截取:
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 isundefined,return"[object Undefined]".If the this value isnull,return"[object Null]".Let O be !ToObject(this value).Let isArray be ?IsArray(O).If isArray istrue,let builtinTag be "Array".Elseif O is a String exotic object,let builtinTag be "String".Elseif O has a [[ParameterMap]]internal slot,let builtinTag be "Arguments".Elseif O has a [[Call]]internal method,let builtinTag be "Function".Elseif O has an [[ErrorData]]internal slot,let builtinTag be "Error".Elseif O has a [[BooleanData]]internal slot,let builtinTag be "Boolean".Elseif O has a [[NumberData]]internal slot,let builtinTag be "Number".Elseif O has a [[DateValue]]internal slot,let builtinTag be "Date".Elseif O has a [[RegExpMatcher]]internal slot,let builtinTag be "RegExp".Else,let builtinTag be "Object".Let tag be ?Get(O,@@toStringTag).IfType(tag)isnotString,set tag to builtinTag.Return the string-concatenation of "[object ", tag,and"]".Thisfunctionis the %ObjProto_toString% intrinsic object.NOTEHistorically,thisfunction 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-inor program defined objects.In addition, programs can use@@toStringTagin 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=00000001-1=10000001假如計算機要對兩個數相加:1 + (-1),使用原碼相加的運算結果為:10000010,很明顯-2並不是我們想要的結果,因此出現了反碼,若使用反碼進行運算會有什麼結果呢,讓我們來看一下:
1[反碼]+(-1)[反碼]=00000001+11111110=11111111[反碼]=10000000[原碼]此時運算結果是正確的,可是這樣還存在一個問題,有兩個值可以表示0:1000 0000、0000 0000,對於計算機來說,0帶符號是沒有任何意義的,人們為了優化0的存在,設計出了補碼:
1[補碼]+(-1)[補碼]=00000001+11111111=00000000[原碼]這樣一來,-0的問題就可以解決了。
參考資料
https://modernweb.com/45-useful-javascript-tips-tricks-and-best-practices/https://dmitripavlutin.com/the-magic-behind-array-length-property/https://medium.freecodecamp.org/9-neat-javascript-tricks-e2742f2735c3https://stackoverflow.com/questions/7310109/whats-the-difference-between-and-in-javascripthttp://javascript.ruanyifeng.com/grammar/number.html
關於本文作者:@vv13原文:https://segmentfault.com/a/1190000016595592