编写可维护的JavaScript
第08章 避免“空比较”
在JavaScript中,我们常常看到这样的代码:变量和null比较(这种做法很有问题),用来判断变量是否被赋予了一个合理的值。比如:
var Controller = {
process: function (items) {
"use strict";
if (items !== null) { // 不好的写法
items.sort();
items.forEach(function (item) {
// 执行逻辑
console.log(item);
});
}
}
};
这段代码中,process()方法显然希望items是一个数组,但是仅仅和null比较并不能提供足够的信息来判断后续代码的执行是否真正安全,上面的代码items可以使用数字 也可以是字符串,这些值都和null不相等。好在JavaScript为我们提供了多种方法来检测变量的真实性。
8.1 检测原始值
在JavaScript中有5中原始类型:字符串、数字、布尔值、null、undefined。如果希望一个值是字符串、数字、布尔值、null、undefined,最佳选择是使用 typeof 运算符。typeof运算符会返回一个表示类型的字符串。
- 字符串 string
- 数字 number
- 布尔值 boolean
- undefined undefined
typeof的基本语法:
var str = "ddd";
typeof str; // 推荐使用这种形式
typeof(sre); // 这种用法让 typeof 看起来像一个函数而非运算符
使用typeof来检测这4中原始类型时非常安全的做法。来看一下下面的例子:
// 检测字符串
if (typeof name === "string") {
anotherName = name.substring(3);
}
// 检测数字
if (typeof count === "number") {
updateCount(count);
}
// 检测布尔值
if (typeof found === "boolean" && found){
message("found!");
}
// 检测undefined
if (typeof MyApp === "undefined"){
MyApp = {
};
}
typeof运算符的独特之处在于,将其用于一个未声明的变量也不会报错。未定义的变量和值为undefined的变量通过typeof都会返回“undefined”。
最后一个原始值,null,一般不应用于检测语句。正如上文提到的,简单地和null比较通常不会包含足够的信息判断值得类型是否合法。
但有一个例外,如果所期望的值真的是null,则可以直接和null进行比较。这时应当使用===或!==来和null进行比较:
var element = document.getElementById("myDiv");
if (element !== null) {
element.className = "found";
}
如果DOM元素不存,则document.getElementById()得到的值为null。这时null就是一个可预见的输出,可以用!==来检测返回结果。
运行 typeof null 返回的是“object”,这是一个低效率的判断null方法。如果需要检测null,请直接使用===和!==。
8.2 检测引用值
引用值也称为对象(*object)。在JavaScript中除了原始值外都是引用值。
其中JavaScript内置的引用类型有:Object、Array、Date、Error。typeof运算符在判断这些引用类型时都会返回“object”。
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof new Date()); // "object"
console.log(typeof new RegExp()); // "object"
检测某个引用值的类型的最好办法是使用 instanceof 运算符。instanceof的基本语法是:
value instanceof constructor
例如:
// 检测日期
var value1 = new Date();
if (value1 instanceof Date) {
console.log(value1.getFullYear());
}
// 检测正则表达式
var value2 = new RegExp();
if (value2 instanceof RegExp) {
console.log(value2);
}
// 检测Error
if (value instanceof Error) {
throw value;
}
instanceof的一个有意思的特性是它不仅检测构造这个对象的构造器,还检测原型链。原型链包含了很多信息,包括定义了对象所采用的继承方式。比如,默认情况下,每个对象都继承自Object,因此每个对象的 value instanceof Object 都会返回 true。
var now = new Date();
console.log(now instanceof Date); // true
console.log(now instanceof Object); // true
因为这个原因,使用 value instanceof Object 来判断对象是否属于某个特定类型的做法并非最佳。
instanceof 运算法也可以检测自定义的类型,比如:
function Person(name) {
"use strict";
this.name = name;
}
var me = new Person("happyking");
console.log(me instanceof Object); // true 任何对象都是Object的实例
console.log(me instanceof Person); // true 因为me是Person的实例
在JavaScript中检测自定义类型时,最好的做法就是使用 instanceof 运算符,这也是唯一的方法。同样对于内置JavaScript类型也是如此。但是,有一个严重的限制。
假设一个浏览器帧(frame A)里的一个对象被传入到另一个帧(frame B)中。两个帧里都定义了构造函数Person。如果来自帧A的对象是帧A的Person实例,则如下规则成立:
frameAPersonInstance instanceof frameAPerson; // true
frameAPersonInstance instanceof frameBPerson; // false
因为每个帧(frame)都拥有Person的一份拷贝,它被认为是该帧(frame)中的Person的拷贝实例,尽管两个定义可能是完全一样的。
这个问题不仅出现在自定义类型上,其他两个非常重要的内置类型也有这个问题:函数和数组、对于这两个类型来说,一般用不着使用instanceof。
8.2.1 检测函数
从技术上讲,JavaScript中的函数是引用类型,同样存在Function构造函数,每个函数都是其实例:
function myFun() {}
// 不好的写法
console.log(myFun instanceof Function); // true
然而,这个方法亦不能跨帧(frame)使用,因为每个帧都有各自的Function构造函数。好在typeof运算符也可以用于函数,返回“function”。
function myFun(){}
// 好的写法
console.log(typeof myFun === "function"); // true
检测函数最好的方法就是使用 typeof ,因为它是可以跨帧(frame)使用的
8.2.2 检测数组
ECMAScript5将Array.isArray()正式引入JavaScript。该函数可以准确的检测一个值是否为数组。Array.isArray()可以检测跨帧(frame)传递的值。
var arr = [1,2,3,4];
var arr32 = 22;
console.log(Array.isArray(arr)); // true
console.log(Array.isArray(arr32)); // false
8.3 检测属性
另外一种用到null(以及undefined)的场景是检测一个属性是否在对象中存在时,比如:
// 不好的写法 检测假值
if(object[propertyName]){
// ...
}
// 不好的写法 和null作比较
if(object[propertyName] != null){
// ...
}
// 不好的写法 和undefined作比较
if(object[propertyName] != undefined){
// ...
}
上面这段代码里的每一个判断,实际上都是通过给定的名字来检测属性的值,而非判断给定的名字所指的属性是否存在。因为当属性的值为假(false)时结果会出错。比如0、""(空字符串)、false、null、undefined。毕竟这些都是属性的合法值。比如属性记录了一个数字,则这个值可以为0,这样的话,上段代码的第一个判断就会出错。
判断属性是否存在的最好办法是使用 in 运算符。in 运算符仅仅会简单地判断属性是否存在,而不会去读属性的值。如果实例对象的属性存在,或者继承自对象的原型,in运算符都会返回true。
var object = {
count: 0,
relate: null
};
// 好的写法
if ("count" in object) {
console.log(object.count);
}
// 不好的写法 检查假值
if (object["count"]) {
console.log(object.count);
}
// 好的写法
if ("relate" in object) {
console.log(object.relate);
}
// 不好的写法 检测是否为null
if (object["relate"] !== null) {
console.log(object.relate);
}
如果你只想检测实例对象的某个属性是否存在,则使用hasOwnProperty()方法,所有继承自Object的JavaScript对象都有这个方法,如果实例中存在这个属性则返回true(如果这个属性只存在于原型里,则返回false)。
var object = {
count: 0,
relate: null
};
// 好的写法
if (object.hasOwnProperty("count")) {
console.log(object.count);
}
// 好的写法
if (object.hasOwnProperty("relate")) {
console.log(object.relate);
}
需要注意的是,在IE8以及更早的版本的IE中,DOM对象并非继承自Object,因此也不包含这个方法。也就是说,你在调用DOM对象的hasOwnProperty()方法之前应当先检查其是否存在(假如你已经知道对象不是DOM,则可以省略这一步)。
// 对于所有非DOM对象来说,这时好的写法
if (object.hasOwnProperty("related")) {
// 执行代码
}
// 如果你不确定是否为DOM对象,则这样来写
if ("hasOwnProperty" in Object && object.hasOwnProperty("related")) {
// 执行代码
}
不管什么时候需要检测属性的存在性,请使用 in 运算符或者 hasOwnProperty()。这样可以避免很多bug。