記一次關於js陣列型別判斷及js型別判斷的細節探索

hanmin發表於2019-03-02

一、前言

眾所周知,js是門“動態”、“弱型別”程式語言,這意味著在js中可以任性定義變數,同時,“任性”也意味著需常在js開發中對變數做型別判斷,曾幾何時,對陣列變數的型別的判斷是件很痛苦的事情,開發人員想出多種方案來對陣列做出準確的型別判斷,但效果不佳,直到ES5標準“入主中原”,判斷陣列型別有了標準的isArray()官方利劍,才降伏了陣列型別判斷這條惡龍,世間得一清,但在這此之前開發者是如何判斷陣列型別的?判斷陣列型別為何會如此玄學?js型別判斷有何坑爹問題?為何要判斷陣列型別?帶著這些疑問,吾跋山涉水,探尋各方資料,整理消化後遂成此文,以記之。

二、判斷js陣列型別為何麻煩?

1、語言本身的“缺陷”

js是門“動態”“弱型別”程式語言,這意味著js在定義和使用變數時可以“任性”,在ES6之前,我們定義變數一般使用“var”來定義:

var name = 'jack';
name = 20;
name = ['aa'];
複製程式碼

在上述例子中,name變數初始定義為字串型別,而後變為數字型別,最後搖身一變成為陣列型別,這種任性搖擺的特性這就是其“動態”語言特性,在java中我們定義一個字串變數須如此定義:String name = 'jack',java通過一個String字首“顯式的”、“強制的”指定name變數為字串型別,之後不得對該name變數進行型別變換(如果執行name = 22將會報type型別轉換錯誤),但js採用的是弱型別定義方案,在定義變數時使用var直接宣告瞭一個變數,弱化了型別字首的限制,並沒強制鎖死變數型別,之後可以隨意更改其型別。動態弱型別這種宣告變數的方案用起來可以隨性而為,無須顧慮太多,隨性的程式碼書寫如若不加管制必將招致災難性的程式碼bug。

2、js型別判斷的“不足”

其實動態弱型別的語言特性並不是決定js判斷陣列型別麻煩的必然原因,js語言因為歷史原因其創造者在開發初始將其js定位為簡單的網友小助手語言,為了輕巧、快速的完成小任務開發選擇了“動態弱型別”的語言方案,PHP亦為動態弱型別語言,但在處理型別判斷時,PHP用一個gettype()方法可以輕鬆、精準的搞定(PHP作為世界上世界上最好的語言還是有、東西的),PHP有gettype()這枚銀彈,js有嗎,嗯,算有吧,js最常用的是用typeof操作符來獲取資料型別,看typeof這個名字是不是感覺很厲害?感覺會跟PHP一樣輕鬆簡單?嗯,一開始我也如此認為,但隨後發現:typeof操作符是個很侷限的型別獲取方案,用它對基本資料型別做判斷還算過得去,但在涉及到引用型別判斷這種細活時就顯得很low了(本以為是個王者,沒想到是個青銅),關於js型別判斷的問題,我在隨後的“判斷js陣列型別的幾個“方案”中逐一講述。

三、判斷js陣列型別的幾個“方案”

1、typeof操作符方案(Pass)

前言:typeof操作符本應是解決js型別判斷的合適方案,奈何負了眾望。

眾所周知,js分兩大資料型別:基本資料型別和引用資料型別,typeof操作符可以對兩大資料型別做出基本的判斷,我本以為typeof對基本資料型別做判斷是可以的,但後來發現其實是有問題的,比如用typeof判斷基本資料型別null:typeof null結果就是“object”,typeof在判斷基本資料型別時尚有問題,更別說用來判斷子孫繁多的引用型別了,typeof在判斷引用型別時一刀切的統統返回object

var obj = {};
var arr = [];
var map = new Map();

typeof obj; // object
typeof arr; // object
typeof map; //object
複製程式碼

在上面的例子中可以看到typeof在判斷{}時返回object'、[]亦返回object`,陣列和物件的判斷結果根本沒區別,所以用typeof來判斷陣列型別的判斷,pass!

2、instanceof運算子方案(存在缺陷)

instanceof是js用來判斷繼承關係的運算子(js基於原型鏈實現繼承,故instanceof判斷的就是對應的類是否存在於變數的原型鏈上),根據這個特性可以如此來判斷陣列型別:

['a'] instanceof Array; // true
複製程式碼

列印陣列['a']可以看到如下結果: 記一次關於js陣列型別判斷及js型別判斷的細節探索 從列印的結果可以看到Array存在於陣列['a']的原型鏈上,故['a'] instanceof Array === true;,利用instanceof的這個特性可以判斷陣列型別,但是instanceof運算子有個弊端,就是如果跨越frame會存在問題:

var iframe = document.createElement('iframe');
document.body.append(iframe);
var FrameArray = window.frames[window.frames.length-1].Array;
var array = new FrameArray();
console.log(array instanceof Array); // false
複製程式碼

跨越frame導致的判斷失誤屬於意料之中,也很好理解,前面說過instanceof運算子是用來判斷繼承關係的(判斷是否存在血統連線關係),原型鏈好比家族裡面的派系鏈,不同的frame相當於不同的家族,在同一個家族中同一派系上的族人存在著連結關係,但如若家族不同(frame不同),則派系鏈則更不可能相同了。所以利用instanceof運算子判斷陣列型別的方案,pass!

3、Object.prototype.toString()方案

instanceof運算子屬於血統繼承性判斷,這種判斷是基於實物的紐帶性判斷,在早前判斷陣列型別時很多類庫時採用該方案(如早期的jquery),但不同frame導致instanceof出現的侷限性不得不讓開發者放棄該方案,轉而尋求更合理的方案,這時候開發者想到既然這種血統繼承性判斷有弊端,那有沒有含蓄而深入點的方案呢?嗯,有請 Object.prototype.toString()方法:

console.log(Object.prototype.toString.call([])); // "[object Array]"
複製程式碼

改方案能獲取到變數的類目名,在js中萬物皆為物件,萬物皆有類目名,每個變數、物件、陣列等都有一個唯一的類目名(這個類目名類似於人類給各色動植物起的“學名”),該方案通過獲取變數的類目名然後將其和陣列對應的類目名([object Array])進行對比,如果類目名一致則證明“變數”為陣列型別:

var a = [];
Object.prototype.toString.call(a) === "[object Array]"; // true
複製程式碼

關於“Object.prototype.toString()”這個方案,在這裡我來好好叨叨:

  • *1、為何要用Object原型上的toString()方法?*我們都知道js的引用類上都存在著toString()方法,然每個引用類都對toString()方法做了自我實現,比如陣列呼叫toString()方法會產生如下結果:
['aa', 'bb'].toString(); // "aa,bb"
複製程式碼
  • *2、Object.prototype.toString()方法是何方神聖?*該方法可用來獲取變數的類目名,通過該方法可獲取到變數的“[[class]]”屬性值,這個屬性值是js的造物主給所有的類定義的類目名(也可以佐證js中萬物皆是物件的說法,詳情可看Ecma詳情定義)
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call(''); // "[object String]"
Object.prototype.toString.call(1); // "[object Number]"
Object.prototype.toString.call(function f(){}); // "[object Function]"
Object.prototype.toString.call(); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(document); // "[object HTMLDocument]"
複製程式碼
  • 3、如何記憶用Object.prototype.toString()方法來判斷陣列型別的方案?我以前記該方案時總易記錯,後來我這樣理解:該方法獲取的是變數的“類目名”,類目名這種肯定屬於頂級屬性,不能隨便得之,而Object屬於所有類的頂級類,這種頂級屬性肯定屬於頂級類,而為能享用這種頂級屬性,就須用到原型鏈上頂級方法:Object.prototype.toString(),一般的凡物變數肯定不能隨便享用頂級方法的,為能享之,須借神器:call()、apply()方法

4、Array.isArray()方案

isArray()方法是ES5標準規定的判斷陣列型別的標準方法,雖然Object.prototype.toString()方法可用來判斷陣列型別,但未免顯得有點hack,又因自家typeof型別操作符給予厚望,辱沒眾望,如果隨便更改typeof的返回結果勢必會導致天下大亂,instanceof運算子又存在不同frame的侷限性難堪大任,ES5不得不亡羊補牢的設計了isArray()方法來“增量”的解決陣列判斷難題。

四、為何要判斷陣列型別?

前面說過js屬於動態弱型別語言,可能某個變數用著用著就莫名其妙的變了型別(自己不小心更改型別,引入的第三方程式碼庫,因為同名變數改變了型別),如果你設想的是某個變數為陣列型別,但因某個邏輯變成了基本型別,這時如果呼叫陣列的方法註定會報錯,凡此種種導致的問題,數不勝數,具體的問題實踐多了懂得就懂。

五、結語

近幾年前端專案愈發複雜龐大,為更好的構建高效能的前端專案,誕生了“react、angular、vue”等資料驅動型解決方案,大量的資料、大量的元件和類對資料型別的檢測愈發頻繁,但因為js動態弱型別語言特性,加之其型別判斷的坑爹性,所以各路開發者希望完善和升級js,在ES6標準中,新的const變數定義方案能很好的應對變數動態性問題,微軟開發的“typescript”能夠實現強型別變數定義,可應對弱型別定義問題。這些方案極大的減少了早期js變數任性定義帶來的各種問題,雖然判斷陣列型別在未來開發中可能會成為歷史雲煙,但理解其相關的基礎和歷史演變卻是一件很【浪漫】的事情,因為在理解了它的相關坑爹性和進化史有助於我們更好的思考和優化。愛之深,責之切,希望js能在未來變得更加鋒利可靠,也希望少為一些坑爹特性而想出一些hack方案(額,比如——>Object.prototype.toString()方法)。

六、參考

相關文章