JS 資料型別方面的蹊蹺

永恆君發表於2019-01-14

現在去做前端面試題都是心虛的, 本來可以做對的題,想想好像有坑,然後答錯了。舉個例子:

Number([0]);           // 0
[0] == true;           // false
if ([0]) alert('ok');  // "ok" // 恩? 不都是 false 嗎
複製程式碼

所以本文將盡可能多的去試圖挖一下 javascript 資料型別方面的蹊蹺。

資料相等

等於問題

這張圖大夥應該很熟悉了,但其實這裡面有些很詭異的問題,很迷很迷。

0 == '0';   // true
0 == [];    // true
'0' == [];  // false
複製程式碼

雙等時也許是進行了型別轉換的, 比如都轉為數字或字串後再進行的比較。

個人猜測轉換的順序 可能 如下:

undefined  < null < Boolean < Number < String < Array
複製程式碼

它是一層層向下進行轉換後進行比較的。

'0' == true  // false
// 實則是 0 == true 的比較
複製程式碼

再比如

Boolean([0]);  // true
[0] == true;   // false
// 實際是 '0' == true 最後 0 == true 的比較
複製程式碼

<= 這類數值判斷,也是類似的,但很快就發現, 以上猜測並不完善,還有更多一步前置的 Number 轉換。

2 > true;      // true
'1' > '2';     // false
複製程式碼
undefined == undefined;  // true
undefined <= undefined;  // false
// 因為 Number(undefined) 的結果是 NaN
複製程式碼

注意 [2] == [2] 當然是為 false 啦, 這個可 別混淆 了,以為也要去轉化。

此處稍微提一下 -0 的存在,會造成 Infinity-Infinity 的不同。 但我們多半會做分母不為零的判斷的,恩大概會的吧。

0 === -0;           // true
(1/-0) === (1/0);   // false
複製程式碼

資料型別判斷

if 判斷

一般使用 if 大致會有以下五種情況,三目判斷並或非 也包含其中。

if (a <= b)
if (a) 
if (a())
if (a = 1)
if (!a)
複製程式碼

if 判斷
如圖所示,if 中結果即是 Boolean() 轉化後的結果。

請再回味一番,切實記住 if 判斷與等於判斷的不同喲。

還以為 !a 的判斷會有坑,試驗下來舒了口氣,並沒有什麼特別之處。

typeof 判斷

這章好像要記住的和留意的東西也並不多,

typeof [] === 'object';
typeof NaN === 'number'
typeof null === 'object'
複製程式碼

卻也是判斷中稍有點難判的,所以才出現了 Array.isArrayisNaN 這樣的方法存在。 為啥我試不出 typeof 為 array 的情況呀,很奇怪耶,是我記錯了咩

還有像 Date RegExp arguments 等自然就是物件了,typeof 的坑相對要少很多。

instanceof 判斷

[] instanceof Array 判陣列真的很方便,但這塊也還是有坑的。

'a' instanceof String                // false
(new String('a')) instanceof String  // true
複製程式碼

除此之外,還有原型鏈上的一點問題:

function Foo(){} 
var foo = new Foo(); 
console.log(foo instanceof Foo);  //true

Foo.prototype = new Aoo();
var foo2 = new Foo(); 
console.log(foo2 instanceof Foo)  // true 
console.log(foo2 instanceof Aoo)  // true
複製程式碼

說實話,除了幾個特例,用這個來判原型其實並不是很好的方法。 參考:www.ibm.com/developerwo…

constructor 判斷

constructor 相比 instanceof 有一點優勢,就是它不隨 __proto__ 的改變

function A(){};
var a = new A();
var b = new A();
a.__proto__ = {};

a instanceof A       // false
b.constructor === A  // true
複製程式碼

以往 es5 做繼承時還要自己給 A.prototype.constructor 設新值, 有了 es6 的 class 後已經很簡單了,用 constructor 來判原型也穩妥了起來。 至於基礎資料型別嘛,也不太推薦用此方法。

is 方法判斷

isFinite();
isNaN();
Number.isNaN();
Array.isArray();
複製程式碼

其他判斷

Object.is() 判斷

其實 Object.is() 是類似 === 的,但又有點不一樣,它是真真正正的絕對相等。

+0 === -0           // true
Object.is(+0, -0)   // false

NaN === NaN          // false
Object.is(NaN, NaN)  // true
複製程式碼

key in object 判斷

還需稍微分清一下原型與例項即可,即 for-infor-of 的區別。

'0' in [1, 2];          // true
'now' in Date;          // true
'getFullYear' in Date;  // false
複製程式碼

至於專案是使用以下哪種判斷就見仁見智了。

if (Array.prototype.includes) {}
'includes' in [];
複製程式碼

prototype 判斷

obj.hasOwnProperty(key)obj.isPrototypeOf(obj2) 等相關方法,整理中

強制資料型別轉換

運算式自動轉換

+' 014'   // 14
+'0x12'   // 18

1 + '14'    // '114'
1 + '0x12'  // '10x12'
1 + +'14'   // 15
'14' + 1    // '141'

1 + [1, 1];     // '11,1'
1 + {};         // '1[object Object]'

1 + null;       // 1
1  +undefined;  // NaN
複製程式碼

很鮮明,當有單獨的運算子存在時(單加括號是不行滴), 會幫忙 Number 轉換,否則 String 轉換。 還請注意上例中後 4 種特殊的情況。

進行 ++ 運算時並不會幫忙轉換為數字,還容易報錯。 所以使用時這裡得留個心眼喲。

++'14'    // ReferenceError
複製程式碼

還有兩個特立獨行的數字運算,即 Infinity0 的正負號。

Infinity+Infinity;      // Infinity
-Infinity+(-Infinity);  // -Infinity
Infinity+(-Infinity);   // NaN

+0+(+0);     // 0
(-0)+(-0);   // -0
(+0)+(-0);   // 0
複製程式碼

再看一個絕對不會遇到的特例, {} + [] 理論上應該是 '[object Object]' + '' 才對, 就算不是也應該是 NaN + 0 吧,結果我又猜錯了。 遇事不決問百度,結果震驚了,這裡的 {} 被當成空程式碼塊了,+[] 自然就是 0 了。

[] + {}; // '[object Object]'
{} + []; // 0
複製程式碼

物件式轉換

物件式轉換一覽

Number() 轉換

NumberparseInt 的不同,將於下文 parseInt 系列方法 講述

String() 轉換

探討一下 StringtoString 的不同吧。

一方面是部分資料型別沒有 toString 方法:

String(null);        // 'null'
(null).toString();        // Uncaught TypeError
(undefined).toString();   // Uncaught TypeError
複製程式碼

另一方面是 toString 可以傳個進位制數的參(僅對數字型別有用

(30).toString(16);    // "1e"
('30').toString(16);  // "30"
複製程式碼

至於 Date Error RegRxp 的字串化,基本不會出啥么蛾子。

用原型的 toString 來判資料型別也是種很巧妙常用的方法。

function typeOf(obj) {
  var typeStr = Object.prototype.toString.call(obj).split(" ")[1];
  return typeStr.substr(0, typeStr.length - 1).toLowerCase();
}
typeOf([]);    // 'array'
複製程式碼

函式式轉換

原型方法

toString 在上文已有介紹,但還得再區分一下陣列的。

[1,[2,"abc","",0,null,undefined,false,NaN],3].toString();
// "1,2,abc,,0,,,false,NaN,3"
複製程式碼

也即是下例想表達的意思:

(null).toString();   // Uncaught TypeError
[null].toString();   // ''
複製程式碼

toStringvalueOf 大致是相同的,但是否有不同,整理中...

再則 (1).toFixed Date.parse 等,應該不會有啥常見錯誤。 只需注意那些是會 對入參進行隱形轉換 的,下文 引數的隱形轉換 將介紹

parseInt 系列方法

window.parseIntNumber.parseInt 是全等的,即完全相同。

主要來看 NumberparseInt 的不同,挺迷的, 它們並不是單純的資料型別轉化那麼簡單,舉個例子:

Number('');     // 0
parseInt('');   // NaN
複製程式碼

parseInt 就很花哨,還會再多進行一些暗箱操作來判斷和適配成數字。 可見,用 Number 轉非整數時會是更好的選擇。

parseInt(' 10 ');   // 10  // 自動去空格,通用
parseInt('10.2');   // 10  // 數字後的全剔除,Number 和 parseFloat 沒問題
parseInt('1e2');    // 1   // 區分不出科學計數法,Number 和 parseFloat 沒問題
parseFloat('0x5');  // 0   // 區分不出進位制,Number 和 parseInt 沒問題
複製程式碼

當引數為陣列時,當然也是先轉 String 的咯, 而 parseInt 又能去除 , 後的字元,所以就有下面的情況。

Number([1, 2]);    // NaN
parseInt([1, 2]);  // 1
複製程式碼

引數的隱形轉換

比較典型的 isNaN 是先用 Number 轉了一次,但 Number.isNaN 就沒有。

isNaN('1x');          // true
Number.isNaN('1x');   // false
複製程式碼

這方面沒做什麼整理,遇到了再補吧。

'12'.replace(1, '');    // "2"
複製程式碼

其他強行轉換

JSON.stringify()

JSON.parse(JSON.strigify()) 深拷貝時可得注意了喲。 其實遞迴加物件解構來做深拷貝要更好一些喲。

JSON.stringify(Infinity);   // 'null'
JSON.stringify(NaN);        // 'null'
JSON.stringify(undefined);        // undefined
JSON.stringify({a: undefined});   // '{}'
JSON.stringify({a: null});        // '{"a":null}'
JSON.stringify(() => {});         // 'undefined'
複製程式碼
encode 系列

encodeURI 方法不會對下列字元編碼 ASCII字母、數字、~!@#$&*()=:/,;?+'

encodeURIComponent 方法不會對下列字元編碼 ASCII字母、數字、~!*()'

所以 encodeURIComponentencodeURI 編碼的範圍更大。

其他
Array.from('foo');          // ["f", "o", "o"]
Object.assign([1], [2,3]);  // [2, 3]
複製程式碼

大致就是這些了,寫完要自閉一會,整個過程充滿了懷疑與揣測。 雖然做了較為系統的拆分,但還是得承認沒寫好,敬請期待後續吧。

我還有一個 BUG 庫,不妨也分享出來一起看看吧。

相關文章