js中, 常常会看到这种代码: 变量与null的比较(这种用法很有问题), 用来判断变量是否被赋予了一个合理的值. 比如:
const Controller = {
process(items) {
if(!items !== null) { // 不好的写法
items.sort();
items.forEach(item => {
// 执行一些逻辑
});
}
}
};
这段代码中, process()方法显然是希望items是一个数组, 因为我们看到的items拥有sort()和forEach(). 这段代码的意图非常明显: 如果参数items不是一个数组, 则停止接下来的操作. 这种写法的问题在于, 和null的比较不能真正的避免错误的发生. items的值可以是1, 也可以是字符串, 还可以是对象. 这些值都和null不相等, 进而导致process()方法一旦执行到sort()时就会报错.
仅仅和null比较并不能提供足够的信息来判断后续代码的执行是否真的安全. 好在js为我们提供了多种方法来检测变量的真实值.
8.1 检测原始值
typeof的基本语法是:
typeof variable; // 推荐写法
// 或者
typeof(variable);
使用typeof来检测这四种原值类型是非常安全的做法. 例子:
// 检测字符串
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 的变量和值为undefined的变量通过typeof 都将返回'undefined'.
最后一个原始值, null, 一般不应用于检测语句. 正如上文提到的, 简单地和null 比较通畅不会包含足够的信息以判断值得类型是否合法. 但有一个例外, 如果所期望的值真的是null, 则可以直接和null进行比较. 这时应当使用===或者!==来和null进行比较, 比如:
// 如果你需要检测null, 则使用这种方法
const ele = document.getElementById('my-div');
if(ele !== null) {
ele.className = 'found';
}
运行typeof null则返回'object', 这时一种低效的判断null的方法. 如果你需要检测null, 则直接使用恒等运算符(===)或非恒等运算符(!==);
特别注意这里所说的typeof null => 'object', 是因为null是一个空指针对象, 所以在定义变量时如果这个变量将来时对象时, 则定义为null. 在编程时杜绝使用typeof来检测null的类型.
8.2 检测引用值
引用值也称作对象(object). 在js中除了原始值之外的值都是引用. 有这样几种内置的引用类型: Object、Array、Date和Error, 数量不多. typeof运算符在判断这些引用类型时则显得力不从心, 因为所有对象都会返回'object'.
检测某个引用值的类型最好的方法是使用instanceof运算符. instanceof的基本语法是:
value instanceof constructor
这里是一些例子.
// 检测日期
if(value instanceof Date) {
console.log(value.getFullYear());
}
// 检测正则表达式
if(value instanceof RegExp) {
if(value.test(anotherValue)) {
console.log('Mathes');
}
}
// 检测Error
if(value instanceof Error) {
throw value;
}
instanceof的一个有意思的特征是它不仅检测构造这对象的构造器, 还检测原型链. 原型链包含了很多信息, 包括定义对象所采用的继承模式. 比如, 默认情况下, 每个对象都继承Object, 因此每个对象的value instanceof Object都会返回true. 比如:
const now = new Date();
console.log(now instanceof Object); // true
console.log(now instanceof Date); // true
因为这个原因, 使用value instanceof Object来判断对象是否属于某一个特定类型的做法并非最佳.
instanceof运算符也可以检测自定义的类型, 比如:
function Person(name) {
this.name = name;
}
const me = new Person('Nicholas');
console.log(me instanceof Object); // true
console.log(meinstanceof Person); // true
这段示例代码中创建了Person类型. 变量me是Person的实例, 因此me instanceof Person 是true. 上文也提到, 所有的对象都被认为是Oject的实例, 因此me instanceof Object也是true.
在js中检测自定义类型时, 最好的做法就是使用instanceof运算符, 这也是唯一的方法. 同样对于内置js的类型也是如此. 但是有一个严重的限制.
假设一个浏览器帧(frame A)里的一个对象被传入到另一个帧(frame B)中. 两个帧都定义了构造函数Person. 如果帧A的对象是帧A的Person的实例, 则如果规则成立.
// true
frameAPersonInstance instanceof frameAPerson
// false
frameAPersonInstance instanceof frameBPerson
因为每个帧(frame)都拥有Person的一份拷贝, 它被认为是该帧(frame)中的Person的拷贝实例, 尽管两个定义可能是完全一样的.
这个问题不仅出现在自定义类型身上, 其他两个非常重要的内置类型也有这个问题: 函数和数组. 对于这两个类型来说, 一般用不着使用instanceof.
8.2.1 检测函数
从技术上讲, js中的函数是引用类型, 同样存在Function构造函数, 每个函数都是其 实例, 比如:
function maFunc() {
// 不好的写法
console.log(myFunc instanceof Function); // true
}
然而, 这个方法亦不能跨帧(frame)使用, 因为每个帧都有各自的Function构造函数. 好在typeof运算符也是可以用于函数的, 返回'function'.
function myFunc() {
// 好的写法
console.log(typeof myFunc === 'function'); // true
}
检测函数最好的方法是使用typeof, 因为它可以跨帧(frame)使用.
用typeof来检测函数有一个限制. 在IE浏览器中, 使用typeof来检测DOM节点(比如 document.getElementById() 中的函数都返回'object'而不是'function').
之所以出现这种怪异现象是因为浏览器对DOM的实现有差异. 简言之, 这些早版本的IE并没有将DOM实现内置的js方法, 导致内置的typeof运算符将这些函数识别为对象. 因为DOM是有明确定义的, 了解到对象成员如果存在则意味着它是一个方法, 开发者往往通过in运算符来检测DOM的方法, 比如:
// 检测DOM方法
if('querSelectorAll' in document) {
images = document.querySelectorAll('img');
}
这段代码检查querySelectorAll是否定义在了document中, 如果是, 则使用这个方法. 尽管不是最理想的方法, 如果想在IE8以及更早浏览器中检测DOM方是否存在, 这是最安全的做法. 在其他所有的情形中, typeof运算符是检测js函数的最佳选择.
8.2.2 检测数组
js中最古老的的跨域问题之一就是在帧(frame)之间来回传递数组. 开发者很快发现instanceof Array在此场景中不总是返回正确的结果. 正如上文提到的, 每个帧(frame)都有各自的Array构造函数, 因此一个帧(frame)中的实例在另一个帧里不会识别. Douglas Crockford首先使用'鸭式辩型'(duck typing) 来检测器sort()方法是否存在.
// 采用鸭式辩型的方法检测数组
function isArray(value) {
return typeof value.sort === 'function';
}
这种检测方法依赖一个事实, 即数组是唯一包含sort()方法的对象, 它也会返回true.
关于如何在js中检测数组类型已经有很多研究了, 最终, Juriy Zaytsev(也被称作Kangax)给出一种优雅的解决方案.
function isArray(value) {
return Object.prototype.toString.call(value) === '[object Array]';
}
Kangax发现调用某个值的内置toString()方法在所有浏览器中都会返回标准的字符串结果. 对于数组来说, 返回字符串为'[object Array]', 也不用考虑数组实例是在哪个帧(frame)中被构造出来的. Kangax给出的解决方案很快流行起来, 并被大多数js类库所采纳.
这种方法在识别内置对象时往往十分有用, 但对于自定义对象请不要用这种方法. 比如内置JSON对象使用这种方法将返回'[object JSON]'.
从那时起, ECMAScript5将Array.isArray()正式引入js. 唯一的目的就是准确的检测一个值是否为数组. 同Kangax的函数一样, Array.isArray()也可以检测跨帧(frame)传递的值, 因此很多js类库目前都类似的实现了这个方法.
8.3 检测属性
另外一种用到null(以及undefined)的场景是当检测一个属性是否在对象中存在时, 比如:
// 不好的写法: 检测假值
if(object[propertyName]) {
// 一些代码
}
// 不好的写法: 和null相比较
if(object[propertyName] != null) {
// 一些代码
}
// 不好的写法: 和undefined比较
if(object[propertyName] != undefined) {
// 一些代码
}
上面这段代码里的每个判断, 实际上是通过给定的名字来检查属性的值, 而非判断给定的名字所指的属性是否存在, 因为当属性值为假值(falsy value)时结果会出错, 比如0, '', false, null和undefined. 毕竟这些都是属性的合法值. 比如, 如果属性记录了一个数字, 则这个值可以是零, 这样的话, 上段代码中的第一个判断就会导致错误. 以此类推, 如果属性值为null或者undefined时, 三个判断都会导致错误.
判断属性是否存在的最好的方法是使用in运算符. in运算符仅仅会简单的判断属性是否存在, 而不会去读属性的值, 这样就可以避免出现本小节提到的有歧义的语句. 如果实例对象的属性存在、或者继承自对象的原型, in运算符都会返回true. 比如:
const obj = {
count: 0,
related: null
};
// 好的写法
if('count' in obj) {
// do something
}
// 不好的写法: 检测假值
if(obj['count']) {
// 这里的代码不会执行
}
// 好的写法
if('related' in object) {
// 这里的代码会执行
}
// 不好的写法: 检测是否为null
if(object['related'] != null) {
// 这里的代码不会执行
}
如果你只是想检查实例对象的某个属性是否存在, 则使用hasOwnProperty()方法. 所有继承自Object的js对象都有这个方法, 如果实例中存在这个属性则返回ture(如果这个属性只存在于原型里, 则返回false). 需要注意的是, 在IE8以及更早版本的IE中, DOM对象并非继承自Object, 因此也不包含这个方法. 也就是说, 你再调用DOM对象的hasOwnProperty()方法之前应当检测其是否存在(加入你已经知道对象不是DOM, 则可以省略这一步).
// 对于所有非DOM对象来说, 这是好的写法
if(object.hasOwnProperty('related')) {
// do something
}
// 如果你不确定是否为DOM对象, 则这样来写
if('hasOwnProperty' in object && object.hasOwnProperty('related')) {
// do something
}
因为存在IE8以及更早版本的IE的情形, 在判断实例对象是否存在时, 我更倾向于使用in运算符, 只有在需要判断实例属性时才会用到hasOwnProperty().
不管你什么时候需要检测属性的存在性, 请使用in运算符或者hasOwnProperty().
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。