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

在 JavaScript 中,我们常常会看到这样的代码:变量与null的比较(这种用法很有问题),用来判断变量是否被赋予了一个合理的值。比如:

  1. var Controller = {
  2. process: function(items) {
  3. if (items !== null) { // 不好的写法
  4. items.sort();
  5. items.forEach(function(item) {
  6. // 执行一些逻辑
  7. });
  8. }
  9. }
  10. }

在这段代码中,process()方法显然希望items是一个数组,因为我们看到items拥有sort()forEach()。这段代码的意图非常明显:如果参数items不是一个组数,则停止接下来的操作。这种写法的问题在于,和null的比较并不能真正避免错误的发生。items的值可以是1,也可以是是字符串,甚至可以是任意对象。这些值都和null不相等,进而会导致process()方法一旦执行到sort()时就会出错。

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

检测原始值

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

  • 对于字符串,typeof返回"string"
  • 对于数字,typeof返回"number"
  • 对于布尔值,typeof返回"boolean"
  • 对于undefined,typeof返回"undefined"

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

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

  1. // 检测"String"
  2. if (typeof name === "string") {
  3. anotherName = name.substring(3);
  4. }
  5. // 检测"Number"
  6. if (typeof count === "number") {
  7. updateCount(count);
  8. }
  9. // 检测"Boolean"
  10. if (typeof found === "boolean" && found) {
  11. message("Found!");
  12. }
  13. // 检测"Undefined"
  14. if (typeof MyApp === "undefined") {
  15. MyApp = {
  16. // 其他代码
  17. };
  18. }

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

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

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

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

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

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

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

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

检测引用值

在 JavaScript 中除了原始值之外的都是引用值(也称为对象),常用的引用类型有:ObjectArrayDateRegExp,这些引用类型都是 JavaScript 的内置对象。typeof运算符在判断这些引用类型时全都返回"object"

  1. console.log(typeof {}); // "object"
  2. console.log(typeof []); // "object"
  3. console.log(typeof new Date()); // "object"
  4. console.log(typeof new RegExp()); // "object"

检测某个引用值类型的最好方法是使用instanceof运算符,instanceof的基本语法是:

  1. value instanceof constructor
  2. // 检测日期
  3. if (value instanceof Date) {
  4. console.log(value.getFullYear);
  5. }
  6. // 检测 Error
  7. if (value instanceof Error) {
  8. throw value;
  9. }
  10. // 检测正则表达式
  11. if (value instanceof RegExp) {
  12. if (value.test(anotherValue)) {
  13. console.log("Matches");
  14. }
  15. }

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

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

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

  1. function Person(name){
  2. this.name = name;
  3. }
  4. var me = new Person("Nicholas");
  5. console.log(me instanceof Object); // ture
  6. console.log(me instanceof Person); // ture

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

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

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

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

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

检测函数

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

  1. function myFunc() {}
  2. // 不好的写法
  3. console.log(myFunc instanceof Function); // true

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

  1. function myFunc() {}
  2. // 好的写法
  3. console.log(typeof myFunc === "function"); // true

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

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

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

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

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

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

检测数组

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

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

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

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

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

  1. function isArray(value) {
  2. if (typeof Array.isArray === "function") {
  3. return Array.isArray(value);
  4. } else {
  5. return Object.prototype.toString.call(value) === "[object Array]";
  6. }
  7. }

IE 9+、FireFox 4+、Safari 5+、Opera 10.5+、Chrome 都实现了Array.isArray()方法。

检测属性

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

  1. // 不好的写法:检测假值
  2. if (object[propertyName]) {
  3. // 一些代码
  4. }
  5. // 不好的写法:和null相比较
  6. if (object[propertyName] != null) {
  7. // 一些代码
  8. }
  9. // 不好的写法:和undefined相比较
  10. if (object[propertyName] != undefined) {
  11. // 一些代码
  12. }

上面这段代码里的每个判断,实际上是通过给定的名字来检查属性的值,而并非判断给定的名字所指的属性是否存在。在第一个判断中,当属性值为假值时结果会出错,比如:0""(空字符串)falsenullundefined,毕竟这些都是属性的合法值。

判断属性是否存在的最好的方法是使用in运算符。in运算符仅仅会简单地判断属性是否存在,而不去读属性的值,如果实例对象的属性存在、或者继承自对象的原型,in运算符都会返回true。比如:

  1. var object = {
  2. count: 0,
  3. related: null
  4. };
  5. // 好的写法
  6. if ("count" in object) {
  7. // 这里的代码会执行
  8. }
  9. // 不好的写法:检测假值
  10. if (object["count"]) {
  11. // 这里的代码不会执行
  12. }
  13. // 好的写法
  14. if ("related" in object) {
  15. // 这里的代码会执行
  16. }
  17. // 不好的写法,检测是否为
  18. if (object["related"] != null) {
  19. // 这里的代码不会执行
  20. }

如果你只想检查实例对象的某个属性是否存在,则使用hasOwnProperty()方法。所有继承自Object的 JavaScript 对象都有这个方法,如果实例中存在这个属性则返回true(如果这个属性只存在于原型里,则返回false)。需要注意的是,在 IE 8 以及更早版本的 IE 中,DOM 对象并非继承自 Object,因此也不包含这个方法。也就是说,你在调用 DOM 对象的hasOwnProperty()方法之前应当先检测其是否存在。

  1. // 对于所有非 DOM 对象来说,这是好的写法
  2. if (object.hasOwnProperty("related")) {
  3. // 执行这里的代码会
  4. }
  5. // 如果你不确定是否为 DOM 对象,则这样来写
  6. if ("hasOwnProperty" in object && object.hasOwnProperty("related")) {
  7. // 执行这里的代码会
  8. }

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

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

扩展阅读

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


JavaScript检测原始值、引用值、属性的更多相关文章

  1. js运行机制 值引用 值传递

    1.js是单线程的  为什么是单线程的呢  因为js作为浏览器脚本语言,会有很多和用户的互动,以及操作dom,多个线程会出问题. 2.js有同步任务,异步任务(ajax,用户点击等,settimeou ...

  2. C++ 右值引用与一级指针

    将右值引用用于一级指针,在初始化时等号右边必须为右值,有以下几种用法: //方式一:引用一级指针,常规用法 int a = 5; int * &&rrpa = &a; //右值 ...

  3. JavaScript检测之basevalidate.js

    上篇文章「JavaScript检测原始值.引用值.属性」中涉及了大量有用的代码范例,为了让大家更方便的使用这些代码,博主特意把这些代码重新整理并托管到 GitHub,项目地址是:https://git ...

  4. JavaScript数据操作--原始值和引用值的操作本质

    我的一句话总结:原始值不管是变量赋值还是函数传递都不会改变原值,引用值不管是变量赋值还是函数传递,如果新变量重新赋值,则不会影响原引用值,如新变量是直接操作,就会影响原引用值. 首先明确,值和类型是两 ...

  5. javascript原始值和引用值类型及区别

    原始值和引用值类型及区别 首先,原始值和引用值类型都是js中的数据类型,为了充分利用存储空间,定义了不同的数据类型,而且js是弱类型,动态语言,数据类型可变. 原始值(简单数据类型) 存储在栈中的简单 ...

  6. Javascript检测值

    检测原始值用typeof javascript有五种原始类型,分别为字符串.数字.布尔值.null和undefined 判断一个值是什么类型的字符串,可以通过typeof typeof variabl ...

  7. javascript中值传递与值引用的研究

    今天重新看了一下<javascript高级程序设计>,其中讲到了javascript中的值传递和值引用,所以就自己研读了一下,但是刚开始没有明白函数中的参数只有值传递,有的场景好像参数是以 ...

  8. JavaScript 函数参数传递到底是值传递还是引用传递

    tips:这篇文章是听了四脚猫的js课程后查的,深入的理解可以参看两篇博客: JavaScript数据类型--值类型和引用类型 JavaScript数据操作--原始值和引用值的操作本质 在传统的观念里 ...

  9. javascript检测基本类型值或引用类型值的类型方法

    首先javascript的数据类型分为两种数据类型:基本数据数据类型和引用数据类型 基本数据类型:Number,String,Boolean,Undefined,Null.原始值,是简单的数据段,可按 ...

随机推荐

  1. 怎样为virtualbox添加新的分辨率

    virtualbox是个相当NB的开源跨平台虚拟机软件,只是新创建的虚拟机仅仅支持几种分辨率.比如.安装win8.1,仅仅有例如以下图的几种分辨率. 只是我的显示器是5K哦,这么点分辨率,简直是搞笑. ...

  2. 1 游戏逻辑架构,Cocos2d-x游戏项目创建,HelloWorld项目创建,HelloWorld程序分析,(CCApplicationProtocol,CCApplication,AppDeleg

     1 游戏逻辑架构 具体介绍 A 一个导演同一时间仅仅能执行一个场景,场景其中,能够同一时候载入多个层,一个层能够可载多个精灵.层中亦能够加层. B  场景切换 sceneàaddChild(la ...

  3. CCCardinalSplineBy概念

    cardianl 红衣主教 这个类是样条曲线动作, 其创建函数是CCCardinalSplineBy::create(float duration, cocos2d::CCPointArray *po ...

  4. mysql binlog参数设置

    1.mysql有许多系统变量,可以设置,系统变量设置不同,不同的系统将导致执行状态. 故mysql提供两组命令,分别查看系统设置和执行状态. 1.系统设置: SHOW [GLOBAL | SESSIO ...

  5. 易语言转C#小试牛刀

    呵呵,用了几年的易语言,太郁闷了,玩过E的童鞋们懂得,偶然机会尝试C#,现正式投入C#门下. 我会把我学习C#的一些知识和重点,实时发不到我的BLOG中,同想学习C#的童鞋一起成长起来.

  6. javascript动态创建对象

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  7. HDU 2458 - Kindergarten

    有一堆男孩和女孩,男孩和男孩之间,女孩和女孩之间互相认识,给出一堆男孩女孩之间认识的关系, 问一个组里最多多少人相互都认识 那么 二分图里 将不认识的连线 那么 相互认识的人最多 就为 最大独立点集 ...

  8. outlook 2007 IMAP设置和配置

    以Outlook2007为例(Outlook2003操作基本类似).  1.依次点击“工具”>“帐户设置”. 2.在“帐户设置”页中点击“新建”> 不需要做任何选择,点击下一步. 3.填写 ...

  9. jquery ajax 跨域提交(附IE浏览器解决方案)

    后台输出内容之前需要指定header("Access-Control-Allow-Origin: *"); post 之前 jQuery.support.cors = true; ...

  10. Ajax XMLHttpRequest对象的三个属性以及open和send方法

    (1)onreadystatechange 属性onreadystatechange 属性存有处理服务器响应的函数.下面的代码定义一个空的函数,可同时对 onreadystatechange 属性进行 ...