编程实践 - 第 8 章 避免“空比较”
《编写可维护的 JavaScript》—— Nicholas C. Zakas
在 JavaScript 中,我们常常会看到这种代码:变量与 null
的比较(这种用法很有问题),用来判断变量是否被赋予了一个合理的值。比如:
var Controller = {
process: function(items) {
if (items !== null) { // 不好的写法
items.sort();
items.forEach(function(item) {
// 执行一些逻辑
});
}
}
};
process()
方法显然希望 items
是一个数组,因为 items
拥有 sort()
和 forEach()
方法。
这段代码的意图非常明显:如果参数 items
不是一个数组,则停止接下来的操作。
但这种写法的问题在于,和 null
的比较并不能真正避免错误的发生。
items
的值可以是 1
、字符串、任意对象。
仅仅和 null
比较并不能提供足够的信息来判断后续代码的执行是否真的安全。
1. 检测原始值
在 JavaScript 中有 5 种原始类型:字符串、数字、布尔值、undefined
、null
。
可以用 typeof
验证前 4 种,用 ===
验证 null
。
typeof 'a'; //=> "string"
typeof 1; //=> "number"
typeof true; //=> "boolean"
typeof undefined; //=> "undefined"
// 可用于未声明的变量
typeof undeclaredVar; //=> "undefined"
typeof null; //=> "object"
简单地和 null
比较通常不会包含足够的信息以判断值的类型是否合法。
如果所期望的值是 null
,则可以直接和 null
进行比较。
2. 检测引用值
引用值也称作对象(object),在 JavaScript 中除了原始值之外的值都是引用。
内置的引用类型: Object
、 Array
、 Date
、 Error
、 RegExp
。
对引用类型使用 typeof
运算符返回的都是 "object"
。
typeof {}; //=> "object"
typeof []; //=> "object"
typeof new Date(); //=> "object"
typeof new RegExp(); //=> "object"
// 这看上去很怪异,被认为是标准规范的严重 bug
typeof null; //=> "object"
通过 instanceof
运算符检测某个引用值的类型,基本语法:
value instanceof constructor
示例:
// 检测日期
if (value instanceof Date) {
console.log(value.getFullYear());
}
// 检测正则表达式
if (value instanceof RegExp) {
if (value.test(anotherValue)) {
console.log("Matches");
}
}
// 检测 Error
if (value instanceof Error) {
throw value;
}
instanceof
不仅检测构造函数还检测原型链。
所有对象都继承自 Object
,因此 引用类型值 instanceof Object; //=> true
。
instanceof
运算符也可以检测自定义的类型,比如:
function Person(name) {
this.name = name;
}
var me = new Person("Nicholas");
console.log(me instanceof Object); //=> true
console.log(me instanceof Person); //=> true
instanceof
不能跨上下文。
比如,一个页面里有个 <iframe>
,父级中的数组对象不能用子级的 Array
构造函数检测。
2.1. 检测函数
JavaScript 中的函数是引用类型,是 Function
构造函数的实例,但 instanceof
不能跨上下文检测。
检测函数最好的方法时使用 typeof
,它可以跨上下文使用。
function myFunc() {}
typeof myFunc === "function"; //=> true
在 IE 8- 中,使用 typeof
检测 DOM 节点中的函数都返回 "object"
而不是 "function"
。比如:
console.log(typeof document.getElementById); //=> "object"
console.log(typeof document.createElement); //=> "object"
console.log(typeof document.getElementsByTagName); //=> "object"
IE 8- 没有将 DOM 函数实现为 JavaScript 方法,导致 typeof
运算符将这些函数识别为对象,通常使用 in
运算符来检测 DOM 的方法。比如
if ("querySelectorAll" in document) {
images = document.querySelectorAll("img");
}
2.2. 检测数组
JavaScript 中最古老的跨域问题之一是在 <iframe>
之间来回传递数组,
而使用 instanceof Array
不总是返回正确的结果,因为每个 <iframe>
都有各自的 Array
构造函数,相互之间不能识别。
Douglas Crockford 首先推荐使用“鸭式辩型”(duck typing)来检测其 sort()
存在与否,以判断是否为数组对象。
function isArray(value) {
return typeof value.sort === "function";
}
这种检测方法依赖一个事实,即数组是唯一包含 sort()
方法的对象。
最终,Juriy Zaytsev(也称为 Kangax)给出了一种更优雅的解决方案,如下
function isArray(value) {
return Object.prototype.toString.call(value) === "[object Array]";
}
Kangax 发现调用某个值的内置 toString()
方法在所有浏览器中都会返回标准的字符串结果,对于数组来说,返回 "[object Array]"
。
这个解决方案很快流行起来,并被大多数 JavaScript 类库所采纳。
这种方法在识别内置对象时往往十分有用,但对于自定义对象请不要使用这种方法。
ECMAScript 5 将 Array.isArray()
正式引入 JavaScript,唯一的目的是准确的检测一个值是否为数组。
同 Kangax 的函数一样,Array.isArray()
也可以检测跨 <iframe>
传递的值。很多类库都做了相应的兼容处理,如下
function isArray(value) {
if (typeof Array.isArray === "function") {
return Array.isArray(value);
} else {
return Object.prototype.toString.call(value) === "[object Array]";
}
}
IE 9+、Firefox 4+、Safari 5+、Opera 10.5+、Chrome 都实现了 Array.isArray()
。
3. 检测属性
另一种用到 null
/undefined
的场景是当检测一个属性是否在对象中存在时,比如:
// 不好的写法:不能检测为假值(0、""、false)的属性
if (object[propertyName]) {
// ...
}
// 不好的写法:不能检测属性值为 null 时会出错
if (object[prototypeName] != null) {
// ...
}
// 不好的写法
if (object[prototypeName] != undefined) {
// ...
}
上面这段代码里的每个判断,实际上是通过给定的名字来检测属性的值,而非判断给定的名字所指的属性是否存在。
判断属性是否存在的最好的方法是使用 in
运算符,in
运算符仅仅会简单地判断属性是否存在,而不会去读属性的值。
如果实例对象的属性存在,或者存在对象的原型链上的某个对象上,in
运算符都会返回 true
。
var object = {
count: 0,
related: null
};
// 好的写法
if ("count" in object) {
// ...
}
// 不好的写法:检测假值
if (object["count"]) {
// ...
}
// 好的写法
if ("related" in object) {
// ...
}
// 不好的写法
if (object["related"] != null) {
// ...
}
如果只想检测实例自身的属性(而非原型链对象上的属性)是否存在,则使用 hasOwnProperty()
方法,
此方法存在于 Object.prototype
,所以任何对象都可以使用。