JavaScript專題系列第五篇,講解更加複雜的型別判斷,比如 plainObject、空物件、類陣列物件、Window物件、DOM 元素等
前言
在上篇《JavaScript專題之型別判斷(上)》中,我們抄襲 jQuery 寫了一個 type 函式,可以檢測出常見的資料型別,然而在開發中還有更加複雜的判斷,比如 plainObject、空物件、Window 物件等,這一篇就讓我們接著抄襲 jQuery 去看一下這些型別的判斷。
plainObject
plainObject 來自於 jQuery,可以翻譯成純粹的物件,所謂"純粹的物件",就是該物件是通過 "{}" 或 "new Object" 建立的,該物件含有零個或者多個鍵值對。
之所以要判斷是不是 plainObject,是為了跟其他的 JavaScript物件如 null,陣列,宿主物件(documents)等作區分,因為這些用 typeof 都會返回object。
jQuery提供了 isPlainObject 方法進行判斷,先讓我們看看使用的效果:
function Person(name) {
this.name = name;
}
console.log($.isPlainObject({})) // true
console.log($.isPlainObject(new Object)) // true
console.log($.isPlainObject(Object.create(null))); // true
console.log($.isPlainObject(Object.assign({a: 1}, {b: 2}))); // true
console.log($.isPlainObject(new Person('yayu'))); // false
console.log($.isPlainObject(Object.create({}))); // false複製程式碼
由此我們可以看到,除了 {} 和 new Object 建立的之外,jQuery 認為一個沒有原型的物件也是一個純粹的物件。
實際上隨著 jQuery 版本的提升,isPlainObject 的實現也在變化,我們今天講的是 3.0 版本下的 isPlainObject,我們直接看原始碼:
// 上節中寫 type 函式時,用來存放 toString 對映結果的物件
var class2type = {};
// 相當於 Object.prototype.toString
var toString = class2type.toString;
// 相當於 Object.prototype.hasOwnProperty
var hasOwn = class2type.hasOwnProperty;
function isPlainObject(obj) {
var proto, Ctor;
// 排除掉明顯不是obj的以及一些宿主物件如Window
if (!obj || toString.call(obj) !== "[object Object]") {
return false;
}
/**
* getPrototypeOf es5 方法,獲取 obj 的原型
* 以 new Object 建立的物件為例的話
* obj.__proto__ === Object.prototype
*/
proto = Object.getPrototypeOf(obj);
// 沒有原型的物件是純粹的,Object.create(null) 就在這裡返回 true
if (!proto) {
return true;
}
/**
* 以下判斷通過 new Object 方式建立的物件
* 判斷 proto 是否有 constructor 屬性,如果有就讓 Ctor 的值為 proto.constructor
* 如果是 Object 函式建立的物件,Ctor 在這裡就等於 Object 建構函式
*/
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
// 在這裡判斷 Ctor 建構函式是不是 Object 建構函式,用於區分自定義建構函式和 Object 建構函式
return typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
}複製程式碼
注意:我們判斷 Ctor 建構函式是不是 Object 建構函式,用的是 hasOwn.toString.call(Ctor),這個方法可不是 Object.prototype.toString,不信我們在函式里加上下面這兩句話:
console.log(hasOwn.toString.call(Ctor)); // function Object() { [native code] }
console.log(Object.prototype.toString.call(Ctor)); // [object Function]複製程式碼
發現返回的值並不一樣,這是因為 hasOwn.toString 呼叫的其實是 Function.prototype.toString,畢竟 hasOwnProperty 可是一個函式!
而且 Function 物件覆蓋了從 Object 繼承來的 Object.prototype.toString 方法。函式的 toString 方法會返回一個表示函式原始碼的字串。具體來說,包括 function關鍵字,形參列表,大括號,以及函式體中的內容。
EmptyObject
jQuery提供了 isEmptyObject 方法來判斷是否是空物件,程式碼簡單,我們直接看原始碼:
function isEmptyObject( obj ) {
var name;
for ( name in obj ) {
return false;
}
return true;
}複製程式碼
其實所謂的 isEmptyObject 就是判斷是否有屬性,for 迴圈一旦執行,就說明有屬性,有屬性就會返回 false。
但是根據這個原始碼我們可以看出isEmptyObject實際上判斷的並不僅僅是空物件。
舉個例子:
console.log(isEmptyObject({})); // true
console.log(isEmptyObject([])); // true
console.log(isEmptyObject(null)); // true
console.log(isEmptyObject(undefined)); // true
console.log(isEmptyObject(1)); // true
console.log(isEmptyObject('')); // true
console.log(isEmptyObject(true)); // true複製程式碼
以上都會返回 true。
但是既然 jQuery 是這樣寫,可能是因為考慮到實際開發中 isEmptyObject 用來判斷 {} 和 {a: 1} 是足夠的吧。如果真的是隻判斷 {},完全可以結合上篇寫的 type 函式篩選掉不適合的情況。
Window物件
Window 物件作為客戶端 JavaScript 的全域性物件,它有一個 window 屬性指向自身,這點在《JavaScript深入之變數物件》中講到過。我們可以利用這個特性判斷是否是 Window 物件。
function isWindow( obj ) {
return obj != null && obj === obj.window;
}複製程式碼
isArrayLike
isArrayLike,看名字可能會讓我們覺得這是判斷類陣列物件的,其實不僅僅是這樣,jQuery 實現的 isArrayLike,陣列和類陣列都會返回 true。
因為原始碼比較簡單,我們直接看原始碼:
function isArrayLike(obj) {
// obj 必須有 length屬性
var length = !!obj && "length" in obj && obj.length;
var typeRes = type(obj);
// 排除掉函式和 Window 物件
if (typeRes === "function" || isWindow(obj)) {
return false;
}
return typeRes === "array" || length === 0 ||
typeof length === "number" && length > 0 && (length - 1) in obj;
}複製程式碼
重點分析 return 這一行,使用了或語句,只要一個為 true,結果就返回 true。
所以如果 isArrayLike 返回true,至少要滿足三個條件之一:
- 是陣列
- 長度為 0
- lengths 屬性是大於 0 的陣列,並且obj[length - 1]必須存在
第一個就不說了,看第二個,為什麼長度為 0 就可以直接判斷為 true 呢?
那我們寫個物件:
var obj = {a: 1, b: 2, length: 0}複製程式碼
isArrayLike 函式就會返回 true,那這個合理嗎?
回答合不合理之前,我們先看一個例子:
function a(){
console.log(isArrayLike(arguments))
}
a();複製程式碼
如果我們去掉length === 0 這個判斷,就會列印 false,然而我們都知道 arguments 是一個類陣列物件,這裡是應該返回 true 的。
所以是不是為了放過空的 arguments 時也放過了一些存在爭議的物件呢?
第三個條件:length 是數字,並且 length > 0 且最後一個元素存在。
為什麼僅僅要求最後一個元素存在呢?
讓我們先想下陣列是不是可以這樣寫:
var arr = [,,3]複製程式碼
當我們寫一個對應的類陣列物件就是:
var arrLike = {
2: 3,
length: 3
}複製程式碼
也就是說當我們在陣列中用逗號直接跳過的時候,我們認為該元素是不存在的,類陣列物件中也就不用寫這個元素,但是最後一個元素是一定要寫的,要不然 length 的長度就不會是最後一個元素的 key 值加 1。比如陣列可以這樣寫
var arr = [1,,];
console.log(arr.length) // 2複製程式碼
但是類陣列物件就只能寫成:
var arrLike = {
0: 1,
length: 1
}複製程式碼
所以符合條件的類陣列物件是一定存在最後一個元素的!
這就是滿足 isArrayLike 的三個條件,其實除了 jQuery 之外,很多庫都有對 isArrayLike 的實現,比如 underscore:
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var isArrayLike = function(collection) {
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};複製程式碼
isElement
isElement 判斷是不是 DOM 元素。
isElement = function(obj) {
return !!(obj && obj.nodeType === 1);
};複製程式碼
結語
這一篇我們介紹了 jQuery 的 isPlainObject、isEmptyObject、isWindow、isArrayLike、以及 underscore 的 isElement 實現。我們可以看到,即使是 jQuery 這樣優秀的庫,一些方法的實現也並不是非常完美和嚴密的,但是最後為什麼這麼做,其實也是一種權衡,權衡所失與所得,正如玉伯在《從 JavaScript 陣列去重談效能優化》中講到:
所有這些點,都必須腳踏實地在具體應用場景下去分析、去選擇,要讓場景說話。
專題系列
JavaScript專題系列目錄地址:github.com/mqyqingfeng…。
JavaScript專題系列預計寫二十篇左右,主要研究日常開發中一些功能點的實現,比如防抖、節流、去重、型別判斷、拷貝、最值、扁平、柯里、遞迴、亂序、排序等,特點是研(chao)究(xi) underscore 和 jQuery 的實現方式。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。