类型判断广泛应用于Web开发中。 简单的包括判断数字或字符串。 更高级的包括判断领域或物体。 更高级的包括判断日期、规律性、错误类型。 更高级的包括判断日期、规律性和错误类型。 还有诸如确定plainObject、空对象、Window对象等。
在上一章中,我们也介绍了 JavaScript 类型判断,但只讲了几个基本方法。 此次整理的内容更广泛,实用价值更高。
类型
最常用的是 typeof。 请注意,尽管我们会看到例如:
console.log(typeof('yayu')) // string
的写法,但是typeof是一个纯粹的运算符,就像加减乘除一样! 这就可以解释为什么下面的写法也是可行的:
console.log(typeof 'yayu') // string
typeof 是一个一元运算符,位于其单个操作数之后,可以是任何类型。 返回值是一个表示操作数类型的字符串。
嗯,我们都知道,在 ES6 之前,JavaScript 有六种数据类型,分别是:
未定义、空、布尔值、数字、字符串、对象
然而,当我们使用typeof对这种数据类型的值进行操作时,返回的结果并不是一一对应的,分别是:
未定义、对象、布尔值、数字、字符串、对象
注意jquery 循环数组,上面都是大写字符串。 Null 和 Object 类型都返回对象字符串。
虽然没有一一对应,但 typeof 可以衡量函数类型:
function a() {} console.log(typeof a); // function
所以typeof可以测量六种类型的值,但除此之外,Object下还有很多细分的类型,比如Array、Function、Date、RegExp、Error等。
如果你使用 typeof 来衡量这些类型,下面是一个反例:
var date = new Date(); var error = new Error(); var symbol = new Symbol('a'); console.log(typeof date); // object console.log(typeof error); // object console.log(typeof a); // symbol
返回的所有对象都是对象。 怎么区分呢~那么有没有更好的方法呢?
对象.prototype.toString
是的当然! 这是Object.prototype.toString!
那么Object.protototype.toString到底是什么? 英文版:
当调用toString方法时,会执行以下步骤:
如果 this 值未定义,则返回“[object Undefined]”。
如果该值为空,则返回“[object Null]”。
令 O 为调用 ToObject 并传递 this 值作为参数的结果。
令 class 为 O 的 [[Class]] 内部属性的值。
返回由三个字符串“[object”、class 和“]”连接而成的字符串值。
如果你不明白,不妨看看我的理解:
当调用toString方法时,会执行以下步骤:
如果 this 的值未定义,则返回 [object Undefined]。 如果该值为 null,则返回 [object Null]。 让 O 成为 ToObject(this) 的结果。 让 class 成为 O 的内部属性 [[Class]] 的值。 最后返回一个由三部分组成的字符串:“[object”、class和“]”
通过规范jquery 循环数组,我们至少知道调用Object.prototype.toString会返回一个由“[object”和class和“]”组成的字符串,而class是要确定的对象的内部属性。
console.log(Object.prototype.toString.call(undefined)) // [object Undefined] console.log(Object.prototype.toString.call(null)) // [object Null] var date = new Date(); console.log(Object.prototype.toString.call(date)) // [object Date] var a = Symbol('a'); console.log(Object.prototype.toString.call(Symbol)) // [object Symbol]
正是因为这些特性,我们可以使用Object.prototype.toString方法来识别更多的类型!
那么可以识别多少种类型呢?
至少12种!
// 以下是11种: var number = 1; // [object Number] var string = '123'; // [object String] var boolean = true; // [object Boolean] var und = undefined; // [object Undefined] var nul = null; // [object Null] var obj = {a: 1} // [object Object] var array = [1, 2, 3]; // [object Array] var date = new Date(); // [object Date] var error = new Error(); // [object Error] var reg = /a/g; // [object RegExp] var func = function a(){}; // [object Function] function checkType() { for (var i = 0; i < arguments.length; i++) { console.log(Object.prototype.toString.call(arguments[i])) } }
checkType(数字、字符串、布尔值、und、nul、obj、数组、日期、错误、reg、func)
除上述11种外,还有:
console.log(Object.prototype.toString.call(Math)); // [object Math] console.log(Object.prototype.toString.call(JSON)); // [object JSON] function a() { console.log(Object.prototype.toString.call(arguments)); // [object Arguments] } a();
类型API
现在我们有了强大的工具Object.prototype.toString! 那我们就写一个类型函数来帮助我们后面识别各种类型的值吧!
我的想法:
编写一个类型函数,可以测量各种类型的值。 如果是基本类型,则使用typeof,如果是引用类型,则使用toString。 另外,由于 typeof 的结果是大写的,我也希望所有结果都是大写的。
考虑到实际情况中不检查Math和JSON,所以去掉了这两类检查。
让我们编写一个版本的代码:
// 第一版 var class2type = {}; // 生成class2type映射 "Boolean Number String Function Array Date RegExp Object Error Null Undefined".split(" ").map(function(item, index) { class2type["[object " + item + "]"] = item.toLowerCase(); }) function type(obj) { return typeof obj === "object" || typeof obj === "function" ? class2type[Object.prototype.toString.call(obj)] || "object" : typeof obj; }
嗯,看起来很完美~~但是请注意,在IE6中,null和undefined会被Object.prototype.toString识别为[object Object]!
放开我,还有这个契合度! 有什么简单的方法可以解决吗? 那么我们将重写它,它一定会让你震惊!
第二版
var class2type = {}; // 生成class2type映射 "Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) { class2type["[object " + item + "]"] = item.toLowerCase(); }) function type(obj) { // 一箭双雕 if (obj == null) { return obj + ""; } return typeof obj === "object" || typeof obj === "function" ? class2type[Object.prototype.toString.call(obj)] || "object" : typeof obj; }
是函数
有了type function,我们就可以直接封装常用的判断,比如isFunction:
function isFunction(obj) { return type(obj) === "function"; }
大批
jQuery 确定链表的类型。 旧版本判断Array.isArray方法是否存在。 如果存在,请使用此技术。 如果不存在,则使用类型函数。
var isArray = Array.isArray || function( obj ) { return type(obj) === "array"; }
但Array.isArray在jQuery v3.0中已经被完全采用。
在下一篇文章中,我们抄袭了jQuery,写了一个可以测量常见数据类型的类型函数。 然而开发中的判断越来越复杂,比如plainObject、空对象、Window对象等等,在这篇文章中,让我们再抄袭jQuery来看看那些类型的判别。
普通对象
plainObject 来自 jQuery,可以翻译为纯对象。 所谓“纯对象”是指通过“{}”或“new Object”创建对象,并且该对象包含零个或多个键名称对。
之所以需要判断是否是plainObject,是为了与其他JavaScript对象如null、数组、宿主对象(文档)等区分开来,因为使用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方法返回一个字符串,代表该函数的源代码。具体包括函数关键字、形参列表、大括号以及函数体中的内容。
空对象
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 对象是客户端 JavaScript 的全局对象。 它有一个指向自身的窗口属性。 这在《JavaScript 深入变量对象》中提到过。 我们可以利用这个特性来判断是否是一个Window对象。
function isWindow( obj ) { return obj != null && obj === obj.window; }
类似于数组
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; }
重点分析回线。 使用or 语句。 只要一个人是真的,那么结果就是真的。
因此,如果 isArrayLike 返回 true,则必须至少满足三个条件之一:
链表的宽度为0lengths。 该属性是小于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。
那么为了放开空论,是否意味着一些有争议的对象也被放开呢?
第三个条件:length 是一个数字,length > 0 并且最后一个元素存在。
为什么只要求最后一个元素存在?
我们先想一下该字段是否可以这样写:
var arr = [,,3] var arrLike = { 2: 3, length: 3 }
也就是说,当我们在链表中直接用冒号跳过的时候,感觉该元素不存在,类字段对象中就不需要写这个元素了,但是最后一个元素一定要写,否则 length 和 width 不会是最后一个元素的键值加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); };