在上一篇 打造属于本身的underscore系列 ( 一 )的文章中,咱们介绍了underscore 的一些设计思想和理念,并对框架的结构进行了详细的介绍,这一节的源码打造,咱们会深刻javascript的数据类型,并会对underscore对各类数据类型的断定方法进行分析。javascript
判断一个对象是否为数组的方法经常使用的有:java
前面的两个方法或多或少存在缺陷,低版本浏览器不支持ES5 Array.isArray()的新方法,而instanceof 断定规则在跨iframe 中也存在问题。好比,一个页面(父页面)有一个框架,框架中引用了一个页面(子页面),在子页面中声明了一个array,并将其赋值给父页面的一个变量,这时判断该变量时使用 instanceOf便不许确了。所以最正确的方法是使用Object.prototype.toString.call(obj)来判断数组。node
// 判断数组
_.isArray = function (obj) {
return Array.isArray(obj) || toString.call(obj) === '[object Array]'
}
复制代码
若是object是一个对象,返回true。须要注意的是JavaScript数组和函数是对象,字符串和数字不是。算法
typeof 能够用来判断数据类型属于Object,同时,Function 类型的数据一样属于对象。而null 虽然是对象,可是须要排除数组
// 判断对象
_.isObject = function (obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj
}
复制代码
咱们知道在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()方法并不会返回数据类型,而是返回重写后的结果。咱们能够举几个例子bash
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 + ']';
};
});
复制代码
javascript原生提供了一个isFinite() 的函数来判断number(或者可转成number 的值) 是否为无穷大。注意,判断条件包括可转化为number 类型的值,也就是针对 true,false的布尔值,以及null的特殊值,能够经过隐式转换为数字,inFinite(true)返回的是true, 所以咱们使用isFinite来判断一个纯的有限数字并不稳当。而且为了不Symbol类型作类型转换时报错,咱们须要先排除Symbol的数据类型。dom
// 判断数字是否为有限值
_.isFinite = function(obj) {
return !_.isSymbol(obj) && isFinite(obj) && !isNaN(parseFloat(obj));
}
复制代码
往下介绍以前,先介绍一下一个重要的方法:_.keys(), _.keys()是用来枚举 对象中可枚举属性,并以数组的形式返回。咱们知道,要遍历对象的属性能够经过 for in 来遍历,ES5中也有新增Object.keys方法,Object.getOwnProperty的方法一样能获取对象的属性,那三者的区别在哪里呢?函数
认清出这几点后,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);
}
}
};
复制代码
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
}
复制代码
若是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;
}
复制代码
javascript 原生提供了一个isNaN() 的函数,该函数用于检查其参数是否为非数字值。通常状况下,isNaN() 函数用于检测 parseFloat() 和 parseInt() 的结果,以判断它们表示的是不是合法的数字。而underscore 的isNaN 方法判断的惟一标准是NaN 其余状况都会返回false,所以 _.isNaN 方法的实现以下:
// 判断obj 是否为NaN
_.isNaN = function(obj) {
return _.isNumber(obj) && isNaN(obj) // 必须是数字,且为NaN
}
复制代码
// 判断obj 是否为null
_.isNull = function (obj) {
return obj === null
}
复制代码
//
_.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
}
复制代码
// 判断obj 为一个DOM元素
_.isElement = function(obj) {
return !!(obj && obj.nodeType === 1);
}
复制代码