在上一篇 打造屬於自己的underscore系列 ( 一 )的文章中,我們介紹了underscore 的一些設計思想和理念,並對框架的結構進行了詳細的介紹,這一節的原始碼打造,我們會深入javascript的資料型別,並會對underscore對各種資料型別的判定方法進行分析。
二, 資料型別診斷
2.1. isArray - 判斷陣列
判斷一個物件是否為陣列的方法常用的有:
- ES5 方法: Array.isArray(obj)
- instanceof: obj instanceof Array
- Object.prototype.toString.call(obj) === "[object Array]"
前面的兩個方法或多或少存在缺陷,低版本瀏覽器不支援ES5 Array.isArray()的新方法,而instanceof 判定規則在跨iframe 中也存在問題。比如,一個頁面(父頁面)有一個框架,框架中引用了一個頁面(子頁面),在子頁面中宣告瞭一個array,並將其賦值給父頁面的一個變數,這時判斷該變數時使用 instanceOf便不準確了。因此最正確的方法是使用Object.prototype.toString.call(obj)來判斷陣列。
// 判斷陣列
_.isArray = function (obj) {
return Array.isArray(obj) || toString.call(obj) === '[object Array]'
}
複製程式碼
2.2. isObject - 判斷物件
如果object是一個物件,返回true。需要注意的是JavaScript陣列和函式是物件,字串和數字不是。
typeof 可以用來判斷資料型別屬於Object,同時,Function 型別的資料同樣屬於物件。而null 雖然是物件,但是需要排除
// 判斷物件
_.isObject = function (obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj
}
複製程式碼
2.3 深入Object.prototype.toString.call()
我們知道在js中,一切都是物件,而Object原型物件上都有一個 toString()方法,toString() 方法呼叫會返回"[object type]", 其中type 是物件的型別,在ES6以前,js內建物件型別 主要有'Arguments', 'Function', 'String', 'Boolean', 'Number', 'Date', 'RegExp', 'Error', ES6之後增加了諸如'Symbol', 'Map', 'WeakMap', 'Set', 'WeakSet' 的資料型別。那既然toString 的方法可以用來判斷物件的具體型別,為什麼還需要通過Object.prototype.toString.call(obj) 的方式來呼叫呢?
原來toString()雖然作為Object原型上的方法,但是Array ,Function等型別作為Object的例項,都重寫了toString方法。因此直接呼叫物件的toString()方法並不會返回資料型別,而是返回重寫後的結果。我們可以舉幾個例子
var a = function(){console.log(2)}
a.toString() // 'function(){console.log(2)}'
var b = [2,5,6];
b.toString() // "2,5,6"
var f = new Date()
f.toString() // "Thu Jan 10 2019 14:33:08 GMT+0800 (中國標準時間)"
var p = /d/g
p.toString() // "/d/g"
var h = new Error('33')
h.toString() // "Error: 33"
var i = Symbol(3);
i.toString() // "Symbol(3)"
···
複製程式碼
因此 Arguments', 'Function', 'String', 'Boolean', 'Number', 'Date', 'RegExp', 'Error', 'Symbol', 'Map', 'WeakMap', 'Set', 'WeakSet'物件型別的判定方法,我們可以統一用Object.prototype.toString.call(obj) 來實現
// 物件型別判斷方法
_.each(['Arguments', 'Function', 'String', 'Boolean', 'Number', 'Date', 'RegExp', 'Error', 'Symbol', 'Map', 'WeakMap', 'Set', 'WeakSet'], function (name) {
_['is' + name] = function (obj) {
return toString.call(obj) === '[object ' + name + ']';
};
});
複製程式碼
2.4 isFinite - 判斷是否是一個有限的數字
javascript原生提供了一個isFinite() 的函式來判斷number(或者可轉成number 的值) 是否為無窮大。注意,判斷條件包括可轉化為number 型別的值,也就是針對 true,false的布林值,以及null的特殊值,可以通過隱式轉換為數字,inFinite(true)返回的是true, 因此我們使用isFinite來判斷一個純的有限數字並不妥當。並且為了避免Symbol型別做型別轉換時報錯,我們需要先排除Symbol的資料型別。
// 判斷數字是否為有限值
_.isFinite = function(obj) {
return !_.isSymbol(obj) && isFinite(obj) && !isNaN(parseFloat(obj));
}
複製程式碼
2.5 _.keys - 物件所擁有的可列舉屬性的集合,不包括原型鏈
往下介紹之前,先介紹一下一個重要的方法:_.keys(), _.keys()是用來列舉 物件中可列舉屬性,並以陣列的形式返回。我們知道,要遍歷物件的屬性可以通過 for in 來遍歷,ES5中也有新增Object.keys方法,Object.getOwnProperty的方法同樣能獲取物件的屬性,那三者的區別在哪裡呢?
- for in 遍歷物件自身和原型上的可列舉屬性
- Object.keys 是ES5新增的方法, 他可以遍歷物件自身的可列舉屬性,但不包括原型鏈上的屬性
- Object.getOwnProperty() 遍歷物件自身可列舉,不可列舉屬性,不包括原型鏈上的屬性
- 在不支援ES5的條件下,只要在for in 基礎上,可以通過 obj.hasOwnProperty(屬性) 便可以來排除原型鏈上的屬性方法。
認清出這幾點後,keys方法的設計就很簡單了
// 遍歷物件自身可列舉屬性
_.keys = function(obj) {
if(!_.isObject(obj)) return []; // 非物件則返回空陣列
if(Object.keys) return Object.keys(obj); // 支援ES5方法,則使用Object.keys()
var keys = []
for(var i in obj) { // 不支援,通過for in 遍歷並排除原型鏈上的屬性
if(obj.hasOwnProperty(i)) keys.push(i)
}
return keys
}
複製程式碼
在underscore原始碼中,我們看到了這樣的一段相容性程式碼。 對於IE9以下而言,forin 遍歷物件存在著某種程度的缺陷,我們知道,諸如 valueof,toString這些定義在Ojbect原型上的方法是不可列舉的,而當我們重寫這些方法後,我們訪問的是這些可列舉的自定義方法。而對於IE9 以下而言。即使重寫了不可列舉的方法後,依然無法在可列舉屬性中遍歷。所以我們需要做對低版本的進行相容。
_.keys = function(obj) {
···
if (hasEnumBug) collectNonEnumProps(obj, keys); // 收集不可列舉屬性
return keys
}
var hasEnumBug = !{
toString: null
}.propertyIsEnumerable('toString'); // 重寫toString 方法,並判斷是否是可列舉的。
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'
]; // 列舉所有Object原型上不可列舉的屬性方法。
var collectNonEnumProps = function (obj, keys) {
var nonEnumIdx = nonEnumerableProps.length;
var constructor = obj.constructor;
var proto = _.isFunction(constructor) && constructor.prototype || ObjProto;
// Constructor is a special case.
var prop = 'constructor';
if (has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
while (nonEnumIdx--) { // 核心: 舉例,判斷物件的toString 方法是否和 物件.constructor.protopye.toString 的記憶體地址是否相同,不相同,則判定重寫了方法。
prop = nonEnumerableProps[nonEnumIdx];
if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
keys.push(prop);
}
}
};
複製程式碼
2.6 _.allKeys - 物件所擁有的可列舉屬性的集合,包括原型鏈
allkeys方法和keys 方法唯一的不同在於,allkeys遍歷的集合包括了原型鏈上的屬性和方法,因此我們可以沿用keys的方法實現,並刪除是否為自身屬性的判斷即可。
// 遍歷物件上可列舉屬性和方法,包括原型鏈
_.allkeys = function(obj) {
if(!_.isObject(obj)) return []; // 非物件則返回空陣列
if(Object.keys) return Object.keys(obj); // 支援ES5方法,則使用Object.keys()
var keys = []
return keys
}
複製程式碼
2.7 _.isEmpty
如果object 不包含任何值(沒有可列舉的屬性),返回true。 對於字串和類陣列(array-like)物件,如果length屬性為 0,那麼_.isEmpty檢查返回true。
//判斷物件是否有可列舉屬性,字串,類陣列屬性length 是否為0
_.isEmpty = function(obj) {
if(_.isArray(obj) || _.isString(obj) || _.isArguments(obj)) return obj.length === 0
return _.keys(obj).length === 0;
}
複製程式碼
2.8 _.isNaN - 判斷obj是否為NaN
javascript 原生提供了一個isNaN() 的函式,該函式用於檢查其引數是否為非數字值。一般情況下,isNaN() 函式用於檢測 parseFloat() 和 parseInt() 的結果,以判斷它們表示的是否是合法的數字。而underscore 的isNaN 方法判斷的唯一標準是NaN 其他情況都會返回false,因此 _.isNaN 方法的實現如下:
// 判斷obj 是否為NaN
_.isNaN = function(obj) {
return _.isNumber(obj) && isNaN(obj) // 必須是數字,且為NaN
}
複製程式碼
2.9 _.isNull - 判斷obj 是否為Null
// 判斷obj 是否為null
_.isNull = function (obj) {
return obj === null
}
複製程式碼
2.10 _.isUndefined - 判斷obj 是否為undefined
//
_.isUndefined = function(obj) {
return obj === void 0
}
複製程式碼
為什麼undefined 我們通過void 0 來判斷, 而不是直接和 "undefined"比較呢?
在ES5之前,undefined 是可以被重寫的,在ES5之後修復了這個問題,但是即使修復了全域性環境下重寫的問題,在區域性環境下,依然可以被重寫
(function() {
var undefined;
undefined = 1
console.log(undefined) // 1
}())
複製程式碼
而void 無論什麼值,返回的都是undefined
void function test() {
console.log('boo!');
// expected output: "boo!"
}();
try {
test();
}
catch(e) {
console.log(e);
// expected output: ReferenceError: test is not defined
}
複製程式碼
2.11 _.isElement 判斷dom元素
// 判斷obj 為一個DOM元素
_.isElement = function(obj) {
return !!(obj && obj.nodeType === 1);
}
複製程式碼