JavaScript中的「黑話」

由於球是圓的,因此不論發生什麼都有可能,對這點我是深信不疑的,但最近我老是在懷疑,JavaScript也是圓的!javascript

什麼是「黑話」

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

「算術」

算術中的位運算已被做者列爲禁術,所以但願你在工程中使用位運算時,請確保你有充足的理由使用,並在須要時寫好Hack註釋。html

!與!!

!爲邏輯非操做符,能夠應用於ECMAScript中的任何值,不管這個值是什麼類型,它會被強制轉化爲一個布爾值變量,再對其值取反。java

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

  1. 將一個值轉化爲布爾值;
  2. 將其取反;
  3. 再次取反。

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

const enable1 = !!id;
const enable2 = id ? true : false;
const enable3 = Boolean(id);
複製代碼

~ 與 ~~

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

  1. 轉爲一個字節的二進制表示:00000101,
  2. 按位取反:11111010
  3. 取其反碼:10000101
  4. 取其補碼:10000110
  5. 轉化爲十進制:-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
複製代碼

+

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

+'1' // 1
+'-1' // '-1 +[] // 0 +{} // NaN 複製代碼

根據觀察,+aa * 1結果相似。除此以外,使用+也能夠做爲當即執行函數:+function() {}(),等效於(function(){})()數組

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

& 與 &&

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

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

  1. 先轉換爲2進制: 111 & 11
  2. 比較結果爲:011
  3. 將二進制轉回十進制,所以:7 & 3 = 3

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

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

  1. 若第一個表達式爲false,則返回第一個表達式;
  2. 若第一個表達式爲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判斷時,也會使用&&短路邏輯從而節省內存空間:

// before
if (test) { alert('hello') }
// after
test && 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就是避免使用相等運算符,真的是這樣嗎?沒錯,這樣能確保在你不完全熟悉語言的狀況下,儘量的去避免犯錯,可是咱們也應該清楚在哪些狀況下應該使用相等運算符,規則每每只針對於新手,而對聰明的你來講,最重要的是要清楚本身在作什麼。

相等操做符對於不一樣類型的值,進行的比較以下圖所示:

  B
    Undefined Null Number String Boolean Object
A Undefined true true false false false IsFalsy(B)
Null true true false false false IsFalsy(B)
Number false false A === B A === ToNumber(B) A=== ToNumber(B) A=== ToPrimitive(B)
String false false ToNumber(A) === B A === B ToNumber(A) === ToNumber(B) ToPrimitive(B) == A
Boolean false false ToNumber(A) === B ToNumber(A) === ToNumber(B) A === B ToNumber(A) == ToPrimitive(B)
Object false false ToPrimitive(A) == B ToPrimitive(A) == B ToPrimitive(A) == ToNumber(B) A === B

針對於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') // true
console.log(11 === '11') // false
複製代碼

^

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

若要交換a與b的值,若是能夠的話推薦你使用:

[a, b] = [b, a];
複製代碼

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

// 異或運算,相同位取0,不一樣位取1,a ^ b ^ b = a, a ^ a ^ b = b
a = a ^ b
b = a ^ b
a = a ^ b
複製代碼

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

..

在JavaScipt整數和浮點數都屬於Number類型,全部數字都以64位浮點數的形式儲存,所以在解析語句時容許數字後面跟着一個小數點(1. === 1),可這樣其實會引起一個問題,解釋器沒法解析1.toString()這樣的語句,會拋出:Uncaught SyntaxError,此時表達式中的.並無視爲屬性訪問器,而是與1結合爲浮點數1.,因此程序會報錯,1.toString()等同於1toString()

爲了更便於理解,能夠記住這個規則:在解釋器眼中,Number型表達式的出現的第一個.爲浮點數的小數分隔符號,第二個.爲屬性訪問器。好比1.0.toString()1..toString()這樣的語法都能正常執行。須要注意的是變量與表達式的區別,若將Number型表達式賦值給變量,經過變量是能夠直接調用原型方法的,由於此時的.沒有歧義。

這樣的鬆散類型結構確實很令人產生誤解,在程序中咱們都應該規避這樣的歧義性語句,經過括號消除數值表達式的歧義(1).toString(),而不是爲了耍酷使用1..toString()

void

根據MDN中的定義:void對給定的表達式進行求值,而後返回undefined,咱們能夠有不少種方式去理解這句話。

首先它能夠做爲undefined的替代品,因爲undefined不是保留字,它實際上是一個全局變量值,所以咱們能夠對其進行改變,程序可能會出現不穩定的狀態,在ES5中已是一個只讀屬性了,可是在局部做用域中,仍是有被重載的可能(你可能也有被害妄想症):

(function() {
  const undefined = 'hello';
  console.log(undefined); // hello
})();
複製代碼

其次,咱們能夠在函數前面加上void關鍵字,表示函數沒有返回值,可是沒必要在每個函數都加上,這不符合JavaScript的代碼風格,利用此特性咱們能夠用於執行IIFE(當即執行函數),讓咱們來看如下示例:

const arrs = []
(function() {
  console.log('hello')
})()
複製代碼

若你不習慣於寫分號,那就極有可能遇到過這種報錯:Uncaught TypeError: [] is not a function,這是因爲編輯器在進行minify的時候沒法進行正確的分詞,這時經過void就能夠解決此類問題,解決了分詞的問題,也使當即執行函數調用更加優雅:

const arrs = []
void function() {
  console.log('hello')
}()
複製代碼

在有時咱們不但願a標籤進行跳轉,如下是一些經常使用方法:

<!-- 使用preventDefault -->
<a id="a" href="">hello</a>
<script> a.addEventListener('click', e => e.preventDefault()); </script>

<!-- 使用return false -->
<a href="" onclick="return false;">hello</a>
複製代碼

當咱們給href值設置爲undefined,也能夠避免a標籤的默認跳轉行爲:

<a href="javascript: void 0;" onclick="return false;">hello</a>
複製代碼

>>> 0

無符號右移運算,對最高位的符號位不做特殊處理,將總體二進制碼向右移動,捨棄低位,高位以 0 進行補充,咱們以 1 byte 大小的的整數進行舉例:

  • 3 >>> 1等價於0000 0011向右移動 1 位:000 0001,而後高位補0:0000 0001,即3 >>> 1 = 1

在 js 中,咱們總能發現一些使用>>> 0的 Hack 代碼,其中核心特性就是無符號右移 0 位能夠將因此類型的值都轉化爲 number 類型作統一處理,且非 number 類型轉換爲 0,如下大概列了一些基本用法:

類型 操做 示例
正整數 不作任何操做 1 >>> 0 = 1
負整數 取負數的補碼的值 -1 >>> 0 = 4294967295
布爾值 轉化爲 1 or 0 true >>> 0 = 1false >>> 0 = 0
字符串 若isNaN(str),值爲 0;不然值爲parseInt(str) >>> 0 '1' >>> 0 = 1's' >>> 0 = 0
浮點數 捨棄掉小數位,而後再執行整數移位操做 1.1 >>> 0 = 1 >>> 0-1.1 >>> 0 = -1 >>> 0
其餘對象 統一值爲0 [] >>> 0 = 0undefined >>> 0 = 0、...

數值表示法

3e9

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

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

1e5; // 100000
2e-4; // 0.0002
-3e3; // -3000
複製代碼

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

如下狀況JavaScript會自動將數值轉爲科學計數法表示:

  1. 小數點前的數字多於21位。
  2. 數值小於1且小數點後的零多於5個,如0.0000001

.5px

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

0x、0o和0b

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

  • 二進制:只用0和1兩個數字,前綴爲0b,十進制13可表示爲0b1101
  • 八進制:只用0到7八個數字,前綴爲0o、0,十進制13可表示爲0o1五、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對好比下:

對上述方法中的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這個包或是別的吧。

生成[0, 1, ..., N-1]

依稀記得在Python中生成列表的語法是多麼簡潔:[ x for x in range(1, 10) ],那麼在JavaScript如何進行初始化1~10的有序序列呢?

行車有規範,直接使用new Array(10)進行初始化並.map是不可取的,由於這樣只設置了數組的length字段:

Object.getOwnPropertyNames([1, 2, 3]) // ["0", "1", "2", "length"]

const a = new Array(3) // [undefined, undefined, undefined]
Object.getOwnPropertyNames(a) // ["length"]
複製代碼

這樣會致使mapfilter等迭代方法無效,固然使用fill填充後便可正常操做數組項,可是在這裏會用其餘方法解決。

在之前,你們喜歡使用這樣的Hack技巧去初始化固定長度的數組:Array.apply(null, { length: 3 }),須要特地說明的是,{ length: 3 }實際上是一個類數組對象,Array.prototype.apply內部取參數多是這樣實現的:

for (let index = 0; i < arguments[1].length; index++) {
  // pass arguments[1][index]
}
複製代碼

正因如此,若是你基礎沒問題的話,就會發現上面的語句其實等效於:Array(undefined, undefined, undefined),綜上,生成0~10的序列語句可寫爲:

Array.apply(null, { length: 10 }).map((v, k) => k)
複製代碼

而對於ES6來說,徹底能夠用Array.from來替代以上的語句的:

Array.from(new Array(10), (k, v) => v)
複製代碼

Array.from不只接受字符串、Set、Map、類數組對象做爲參數,凡是可迭代對象均可以,好比咱們出於娛樂的目的使用生成器實現:

function* range(start, end) {
  for (let i = start; i < end; i++) {
    yield i
  }
}
Array.from(range(1, 10)) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
[...range(1, 10)] // [1, 2, 3, 4, 5, 6, 7, 8, 9]
複製代碼

「理論」

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) // false
console.log(NaN === NaN) // false
console.log(NaN == NaN) // false
複製代碼

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

Object.is(NaN, NaN) // true
Object.is(+0, -0) // false
複製代碼

而除了Falsy值,全部值都是Truthy值,在Boolean上下文中表現爲true。

其餘

上文主要總結了 JavaScript 中一些本身接觸過的奇技淫巧,但其中只有少數知識點是值得深挖的,咱們更應關注其中的原理而不是語法,以不變應萬變。

參考資料

相關文章
相關標籤/搜索