您的位置:首页 > Web前端 > JavaScript

JavaScript检测原始值、引用值、属性

2016-06-20 01:19 246 查看
上周写过一篇读书笔记《编写可维护的JavaScript》之编程实践,其中 第8章 避免『空比较』是博主在工作中遇坑较多的雷区,所以特此把该章节重新整理分享,希望大家不再坑队友(>﹏<)。

在 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
不相等,进而会导致
process()
方法一旦执行到
sort()
时就会出错。

仅仅和
null
比较并不能提供足够的信息来判断后续代码的执行是否真的安全。好在 JavaScript 为我们提供了很多种方法来检测变量的真实值。

检测原始值

在 JavaScript 中有5种原始类型(也称为简单数据类型):
String
Number
Boolean
Undefined
Null
。如果你希望一个值是
String
Number
Boolean
Undefined
,最佳选择是使用
typeof
运算符,它会返回一个表示类型的字符串。

对于字符串,
typeof
返回
"string"


对于数字,
typeof
返回
"number"


对于布尔值,
typeof
返回
"boolean"


对于undefined,
typeof
返回
"undefined"


typeof
的基本语法是:
typeof variable
,你还可以这样用:
typeof(variable)
,尽管这是合法的 JavaScript 语法,这种用法让
typeof
看起来像一个函数而非运算符。鉴于此,我们更推荐无括号的写法。

使用
typeof
来检测这4种原始类型是非常安全的做法。来看下面这些例子。

// 检测"String"
if (typeof name === "string") {
anotherName = name.substring(3);
}

// 检测"Number"
if (typeof count === "number") {
updateCount(count);
}

// 检测"Boolean"
if (typeof found === "boolean" && found) {
message("Found!");
}

// 检测"Undefined"
if (typeof MyApp === "undefined") {
MyApp = {
// 其他代码
};
}


typeof
运算符的独特之处在于,将其用于一个未声明的变量也不会报错。未定义的变量和值为
undefined
的变量通过
typeof
都将返回
"undefined"


最后一个原始类型
null
,通过
typeof
将返回
"object"
,这看上去很怪异,被认为是标准规范的严重 bug,因此在编程时要 杜绝使用
typeof
来检测
null
的类型


console.log(typeof null);   // "object"


简单地和
null
进行比较通常不会包含足够的信息以判断值的类型是否合法,所以
null
一般不应用于检测语句。

但有一个例外,如果所期望的值真的是
null
,则可以直接和
null
进行比较。例如:

// 如果你需要检测 null,则使用这种方法
var element = document.getElementById("my-div");
if (element !== null) {
element.className = "found";
}


如果 DOM 元素不存在,则通过
document.getElementById()
得到的值为
null
。这个方法要么返回一个节点,要么返回
null
。由于这时
null
是可预见的一种输出,则可以用恒等运算符
===
或非恒等运算符
!==
来检测返回结果。

typeof
运算符的返回值除了上述提到的
string
number
boolean
undefined
object
之外,还有
function
。从技术的角度来讲,函数在 JavaScript 中也是对象,不是一种数据类型。然而,函数也确实有一些特殊的属性,因此通过
typeof
运算符来区分函数和其他对象是有必要的。这一特性将在后面 检测函数 中用到。

检测引用值

在 JavaScript 中除了原始值之外的都是引用值(也称为对象),常用的引用类型有:
Object
Array
Date
RegExp
,这些引用类型都是 JavaScript 的内置对象。
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

// 检测日期
if (value instanceof Date) {
console.log(value.getFullYear);
}

// 检测 Error
if (value instanceof Error) {
throw value;
}

// 检测正则表达式
if (value instanceof RegExp) {
if (value.test(anotherValue)) {
console.log("Matches");
}
}


instanceof
的一个有意思的特性是它不仅检测构造这个对象的构造器,还检测原型链。原型链包含了很多信息,包括定义对象所采用的继承模式。比如,默认情况下,每个对象都继承自
Object
,因此每个对象的
value instanceof Object
都会返回
ture
。比如:

var now = new Date();
console.log(now instanceof Object); // ture
console.log(now instanceof Date);   // ture


instanceof
运算符也可以检测自定义的类型,比如:

function Person(name){
this.name = name;
}
var me = new Person("Nicholas");
console.log(me instanceof Object);  // ture
console.log(me instanceof Person);  // ture


这段示例代码中创建了
Person
类型。变量
me
Person
的实例,因此
me instanceof Person
true
。上文也提到,所有的对象都被认为是
Object
的实例,因此
me instanceof Object
也是
ture


在 JavaScript 中检测 内置类型自定义类型 时,最好的做法就是使用
instanceof
运算符,这也是唯一的方法。

但有一个严重的限制,假设两个浏览器帧(frame)里都有构造函数
Person
,帧A中的
Person
实例
frameAPersonInstance
传入到帧B中,则会有如下结果:

console.log(frameAPersonInstance instanceof frameAPerson)    // ture
console.log(frameAPersonInstance instanceof frameBPerson)    // false


尽管两个
Person
的定义是完全一样的,但在不同帧(frame)里,他们被认为是不同类型。有两个非常重要的内置类型也有这个问题:
Array
Function
,所以检测它们一般不使用
instanceof


检测函数

从技术上讲,JavaScript 中的函数是引用类型,同样存在
Function
构造函数,每个函数都是其实例,比如:

function myFunc() {}

// 不好的写法
console.log(myFunc instanceof Function); // true


然而,这个方法亦不能跨帧(frame)使用,因为每个帧都有各自的
Function
构造函数,好在
typeof
运算符也是可以用于函数的,返回
"function"


function myFunc() {}

// 好的写法
console.log(typeof myFunc === "function"); // true


检测函数最好的方法是使用
typeof
,因为他可以跨帧(frame)使用。

typeof
来检测函数有一个限制。在 IE 8 和更早版本的 IE 浏览器中,使用
typeof
来检测 DOM 节点中的函数都返回
"object"
而不是
"function"
。比如:

// IE8 及更早版本的IE
console.log(typeof document.createElement);         // "object"
console.log(typeof document.getElementById);        // "object"
console.log(typeof document.getElementByTagName);   // "object"


之所以出现这种怪异的现象是因为浏览器对 DOM 的实现有差异。简言之,这些早版本的 IE 并没有将 DOM 实现为内置的 JavaScript 方法,导致内置
typeof
运算符将这些函数识别为对象。因为 DOM 是有明确定义的,了解到对象成员如果存在则意味着它是一个方法,开发者往往通过
in
运算符来检测 DOM 的方法,比如:

// 检测 DOM 方法
if ("querySelectorAll" in document) {
var images = document.querySelectorAll("img");
}


这段代码检查
querySelectorAll
是否定义在
document
中,如果是,则使用这个方法。尽管不是最理想的方法,如果想在 IE 8 及更早浏览器中检测 DOM 方法是否存在,这是最安全的做法。在其他所有的情形中,
typeof
运算符是检测 JavaScript 函数的最佳选择。

检测数组

JavaScript 中最古老的跨域问题之一就是在帧(frame)之间来回传递数组。开发者很快发现
instanceof Array
在此场景中不能返回正确的结果。正如上文提到的,每个帧都有各自的
Array
构造函数,因此一个帧中的实例在另外一个帧里不会被识别。

关于如何在 JavaScript 中检测数组类型已经有狠多研究了,最终 Kangax 给出了一种优雅的解决方案:

function isArray(value) {
return Object.prototype.toString.call(value) === "[object Array]";
}


Kangax 发现调用某个值的内置
toString()
方法在所有浏览器中都会返回标准的字符串结果。对于数组来说,返回的字符串为
"[object Array]"
,也不用考虑数组实例实在哪个帧(frame)中被构造出来的。这种方法在识别内置对象时往往十分有用,但对于自定义对象请不要用这种方法。

ECMAScript5 将
Array.isArray()
正式引入 JavaScript。唯一的目的就是准确地检测一个值是否为数组。同 Kangax 的函数一样,
Array.isArray()
也可以检测跨帧(frame)传递的值,因此很多 JavaScript 类库目前都类似地实现了这个方法。

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()
方法。

检测属性

另外一种用到
null
(以及
undefined
)的场景是当检测一个属性是否在对象中存在时,比如:

// 不好的写法:检测假值
if (object[propertyName]) {
// 一些代码
}

// 不好的写法:和null相比较
if (object[propertyName] != null) {
// 一些代码
}

// 不好的写法:和undefined相比较
if (object[propertyName] != undefined) {
// 一些代码
}


上面这段代码里的每个判断,实际上是通过给定的名字来检查属性的值,而并非判断给定的名字所指的属性是否存在。在第一个判断中,当属性值为假值时结果会出错,比如:
0
""(空字符串)
false
null
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
的 JavaScript 对象都有这个方法,如果实例中存在这个属性则返回
true
(如果这个属性只存在于原型里,则返回
false
)。需要注意的是,在 IE 8 以及更早版本的 IE 中,DOM 对象并非继承自 Object,因此也不包含这个方法。也就是说,你在调用 DOM 对象的
hasOwnProperty()
方法之前应当先检测其是否存在。

// 对于所有非 DOM 对象来说,这是好的写法
if (object.hasOwnProperty("related")) {
// 执行这里的代码会
}

// 如果你不确定是否为 DOM 对象,则这样来写
if ("hasOwnProperty" in object && object.hasOwnProperty("related")) {
// 执行这里的代码会
}


因为存在 IE 8 以及更早版本的 IE 的情形,在判断实例对象的属性是否存在时,我更倾向于使用
in
运算符,只有在需要判断实例属性时才会用到
hasOwnProperty()


不管你什么时候需要检测属性的存在性,请使用
in
运算符或者
hasOwnProperty()
。这样做可以避免很多 bug。

扩展阅读

《编写可维护的JavaScript》

《JavaScript权威指南(第6版)》

《JavaScript高级程序设计(第3版)》

欢迎来到 石佳劼的博客,如有疑问,请在「原文」评论区 留言,我会尽量为您解答。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  javascript