《编写可维护的JavaScript》读书笔记之编程实践-避免“空比较”

避免“空比较”

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

var Controller = {
    process : function(items) {
        // 不好的写法
        if(items !== null) {
            items.sort();
            items.forEach(function(item) {
                // 执行一些逻辑
            });
        }
    }
};
  • 意图:如果参数 items 不是一个数组,则停止接下来的操作。
  • 问题:和 null 的比较并不能真正避免错误的发生。
  • 原因:items 的值可以是 1,也可以是字符串,甚至可以是任意对象。这些值都和 null 不想等,进而会导致 process() 方法一旦执行到 sort() 时就会出错。
  • 结论:仅仅和 null 比较并不能提供足够的信息来判断后续代码的执行是否真的安全。

检测原始值

如果你希望一个值是字符串、数字、布尔值或 undefined,最佳选择是使用 typeof 运算符。typeof 运算符会返回一个表示值的类型的字符串。

  • 字符串:typeof 返回 “string”。
  • 数字:typeof 返回 “number”。
  • 布尔值:typeof 返回 “boolean”。
  • undefined:typeof 返回 “undefined”。

typeof 语法:

typeof variable
/*
 * 尽管这是合法的 JavaScript 语法,这种用法让 typeof
 * 看起来像一个函数而非运算符。因此,更推荐无括号的写法
 */
typeof(variable)

检测 4 种原始值的做法:

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

// 检测数字
if(typeof count === "number") {
    unpdateCount(count);
}

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

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

注意:

typeof 运算符用于一个未声明的变量也不会报错。未定义的变量和值为 undefined 的变量通过 typeof 都将返回 “undefined”。

关于 null:

  • 概述:原始值 null 一般不应用于检测,因为简单地和 null 比较通常不会包含足够的信息以判断值的类型是否合法。但有个例子,如果所期望的值真的是 null,则可以直接和 null 进行比较。这时应当使用 === 或者 !== 来和 null 进行比较。
  • 示例
// 如果你需要检测 null,则使用这种方法
var element = document.getElementById("my-div");
if(element !== null) {
    element.className = "found";
}
/*
 * 如果 DOM 元素不存在,则获得的节点的值为 null。这个方法要么返回一个节点,要么返回 null。
 * 由于这时 null 是可预见的一种输出,则可以使用 !== 来检测返回结果。
  • 注意:运行 typeof null 则返回 “object”,这是一种低效的判断 null 的方法,甚至被认为是标准规范的严重 bug,因此在编程时杜绝使用 typeof 来检测 null 的类型。如果你需要检测 null,则直接使用恒等运算符(===)或非恒等运算符(!==)。

检测引用值

引用值也称作对象(object),在 JavaScript 中除了原始值之外的值都是引用。

常见的内置引用类型:
  • Object
  • Array
  • Date
  • RegExp
  • Error

对引用值的检测:

  • typeof 运算符在判断这些引用类型以及 null 时都会返回 “object”,因此 typeof 不适合检测引用值。
var arr = [1, 2],
	obj = {name: "clvsit"},
	date = Date(),
	reg = RegExp(),
	err = Error();
console.log(typeof arr === "object"); // true
console.log(typeof obj === "object"); // true
console.log(typeof date === "object"); // false
console.log(typeof reg === "object"); // true
console.log(typeof err === "object"); // true

从上述示例可以发现,typeof date === “object” 返回 false,为什么会这样?

console.log(typeof date); // 'string'
console.log(date.toString()); // "Sat Jan 05 2019 15:06:28 GMT+0800 (中国标准时间)"
console.log(date); // "Sat Jan 05 2019 15:06:28 GMT+0800 (中国标准时间)"

实践可知,在使用 Date 对象时会自动调用 Date 对象的 toString() 方法,因此 typeof date 得到的结果为 ‘string’。

  • 检测某个引用值的类型的最好方法是使用 instanceof 运算符。

instanceof:

  • 语法
value instanceof constructor
  • 示例
// 检测日期
if(value instanceof Date) {
    console.log(value.getFullYear());
}

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

// 检测 Error
if(value instanceof Error) {
    throw value;
}
  • 注意
  1. instanceof 不仅检测构造这个对象的构造器,还检测原型链。而原型链包含了很多信息,包括定义对象所采用的继承模式。比如,默认情况下,每个对象都继承来自 Object,因此每个对象的 value instanceof Object 都会返回 true(使用 value instanceof Object 来判断对象是否属于某个特定类型的做法并非最佳)。
var now = new Date();

console.log(now instanceof Object);  // true
console.log(now instanceof Date);    // true
  1. instanceof 运算符也可以检测自定义的类型(最好的做法)。
function Person(name) {
    this.name = name;
}

var me = new Person("Nicholas");

console.log(me instanceof Object);  // true
console.log(me instanceof Person);  // true
存在的限制:
  • 情景:假设一个浏览器帧(frameA) 里的一个对象被传入到另一个浏览器帧(frameB)中,两个帧里都定义了构造函数 Person。如果来自帧 A 的对象是 帧 A 的 Person 的实例,则如下规则成立:
// true
frameAPersonInstance instanceof frameAPerson;

// false
frameAPersonInstance instanceof frameBPerson;
  • 原因
    每个帧都拥有 Person 的一份拷贝,被认为是该帧中 Person 的拷贝的实例,尽管两个定义可能完全一样。
  • 注意
    这个问题不仅出现在自定义类型上,其他两个非常重要的内置类型也存在这个问题:函数和数组。对于这两个类型来说,一般用不着使用 instanceof。

检测函数

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

function myFunc() {}

// 不好的写法
console.log(myFunc instanceof Function);  // true
  • 注意
  1. instanceof 不能跨帧(frame)使用,因为每个帧都有各自的 Function 构造函数。
  2. typeof 运算符也是可以用于函数的,返回 “function”。检测函数最好的方法是使用 typeof,因为它可以跨帧使用。
function myFunc() {}

// 好的写法
console.log(typeof myFunc === "function");  // true
  1. typeof 检测函数的限制:在 IE 8 和更早版本的 IE 浏览器中,使用 typeof 来检测 DOM 节点(比如 document.getElementById())中的函数都返回 “object” 而不是 “function”。之所以出现这种现象是因为浏览器对 DOM 的实现有差异。简言之,早期的 IE 并没有将 DOM 实现为内置的 JavaScript 方法,导致内置 typeof 运算符将这些函数识别为对象。
  2. 在 IE 8 及早期版本,开发者往往通过 in 运算符来检测 DOM 的方法。
// 检测 DOM 方法
if("querySelectorAll" in document) {
    images = document.querySelectorAll("img");
}
  1. 尽管使用 in 检测 DOM 方法不是最理想的方法,但如果想在 IE 8 及更早浏览器中检测 DOM 方法是否存在,这是最安全的做法。在其他所有情形中,typeof 运算符是检测 JavaScript 函数的最佳选择。

检测数组

JavaScript 最古老的跨域问题之一就是在帧之间来回传递数组。因为每个帧都有各自的 Array 构造函数,因此一个帧中的实例在另外一个帧里面不会被识别。

  • 检测方法
  1. 鸭式辨型(duck typing)
// 采用鸭式辨型的方法检测数组
function isArray(value) {
    return typeof value.sort === "function";
}
/*
 * 缺陷:数组是唯一包含 sort() 方法的对象。
 * 如果传入 isArray() 的参数是一个包含 sort() 
 * 方法的对象,也会返回 true。
 */
var obj = {
    sort: function () {
    
        // ...
    }
};
isArray(obj) // true; 
  1. Kangax 方案
function isArray(value) {
    return Object.prototype.toString.call(value) === "[object Array]";
}
  • 解释
    调用某个值的内置 toString() 方法在所有浏览器中都会返回标准的字符串结果。对于数组来说,返回的字符串为 “[object Array]”,也不用考虑数组实例实在哪个帧中被构造出来。
  • 缺陷
    对于自定义对象使用这种方法会存在问题,比如内置 JSON 对象将返回 “[object JSON]”。
  • 后续
    ECMAScript5 将 Array.isArray() 正式引入 JavaScript。唯一的目的就是准确地检测一个值是否为数组。Array.isArray() 可以检测跨帧传递的值。同样很多 JavaScript 类库都实现了这个方法。
function isArray(value) {
    if(typeof Array.isArray === "function") {
        return Array.isArray(value);
    } else {
        return Object.prototype.toString.call(value) === "[object Array]";
    }
}

检测属性

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

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

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

// 不好的写法:和 undefined 比较
if(object[propertyName] !== undefined) {
    // 一些代码
}
  • 解释
    上面这段代码里的每个判断,实际上是通过给定的名字来检查属性的值,而非判断给定的名字所指的属性是否存在。因为当属性值为假值(false value)时结果会出错,比如 0、""、false、null 和 undefined。毕竟,这些都是属性的合法值。比如,如果属性记录了一个数字,则这个值可以是零,这样的话,上段代码中的第一个判断就会导致错误。以此类推,如果属性值为 null 或者 undefined 时,三个判断都会导致错误。

  • 合适的方式
    判断属性是否存在的最好方法是使用 in 运算符。因为 in运算符仅仅会简单地判断属性是否存在,而不会去读属性的值,这样就可以避免解释部分提到的问题。如果实例对象的属性存在、或者继承自对象的原型,in 运算符都会返回 true。

var object = {
    count : 0,
    related : null
}

// 好的写法
if("count" in object) {
    // 这里的代码会执行
}

// 不好的写法
if(object["count"]) {
    // 这里的代码不会执行
}

// 好的写法
if("related" in object) {
    // 这里的代码会执行
}

// 不好的写法:检测是否为 null
if(object["related"] !== null) {
    // 这里代码不会执行
}

hasOwnProperty():

  • 概述
    如果只想检查实例对象的某个属性是否存在,可以使用 hasOwnProperty() 方法,所有继承自 Object 的 JavaScript 对象都有这个方法。如果实例中存在这个属性则返回 true(如果这个属性只存在于原型里,则返回 false)。
  • 注意
    在 IE 8 及早期版本,DOM 对象并非继承自 Object,因此也不包含这个方法。也就是说,你在调用 DOM 对象的 hasOwnProperty() 方法之前应当先检测其是否存在(假如你已经知道对象不是 DOM,则可以省略这一步)。
// 对于所有非 DOM 对象来说,这是好的写法
if(object.hasOwnProperty("related")) {
    // 执行这里的代码
}

// 如果你不确定是否为 DOM 对象,则这样来写
if("hasOwnProperty" in object && object.hasOwnProperty("related")) {
    // 执行这里的代码
}
  • 总结
    因为存在 IE 8 以及更早版本 IE 的情形,在判断实例对象的属性是否存在时,推荐使用 in 运算符。只有在需要判断实例属性时才会用到 hasOwnProperty()。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
编写维护JavaScript》向开发人员阐述了如何在团队开发中编写具备高可维护性的JavaScript代码,书中详细说明了作为团队一分子,应该怎么写JavaScript。本书内容涵盖了编码风格、编程技巧、自动化、测试等几方面,既包括具体风格和原则的介绍,也包括示例和技巧说明,最后还介绍了如何通过自动化的工具和方法来实现一致的编程风格。   《编写维护JavaScript》作者Nicholas C. Zakas是顶级的Web技术专家,也是《JavaScript高级程序设计》一书的作者。他曾是Yahoo!的首席前端开发工程师,在完成了从一名“独行侠”到“团队精英”的蜕变后,他站在前端工程师的角度提炼出众多的最佳编程实践,其中包括很多业内权威所推崇的最佳法则,而这些宝贵经验正是本书的核心内容。   《编写维护JavaScript》适合前端开发工程师、JavaScript程序员和学习JavaScript编程的读者阅读,也适合开发团队负责人、项目负责人阅读。运用本书中讲述的技巧和技术,可以使JavaScript团队编程从侠义的个人偏好的阴霾走出来,走向真正的高可维护性、高效能和高水准。 第一部分 编程风格 第1章 基本的格式化 1.1 缩进层级 1.2 语句结尾 1.3 行的长度 1.4 换行 1.5 行 1.6 命名 1.6.1 变量和函数 1.6.2 常量 1.6.3 构造函数 1.7 直接量 1.7.1 字符串 1.7.2 数字 1.7.3 null 1.7.4 undefined 1.7.5 对象直接量 1.7.6 数组直接量 第2章 注释 2.1 单行注释 2.2 多行注释 2.3 使用注释 2.3.1 难于理解的代码 2.3.2 可能被误认为错误的代码 2.3.3 浏览器特性hack 2.4 文档注释 第3章 语句和表达式 3.1 花括号的对齐方式 3.2 块语句间隔 3.3 switch语句 3.3.1 缩进 3.3.2 case语句的“连续执行” 3.3.3 default 3.4 with语句 3.5 for循环 3.6 for-in循环 第4章 变量、函数和运算符 4.1 变量声明 4.2 函数声明 4.3 函数调用间隔 4.4 立即调用的函数 4.5 严格模式 4.6 相等 4.6.1 eval() 4.6.2 原始包装类型 第二部分 编程实践 第5章 UI层的松耦合 5.1 什么是松耦合 5.2 将JavaScript从CSS中抽离 5.3 将CSS从JavaScript中抽离 5.4 将JavaScript从HTML中抽离 5.5 将HTML从JavaScript中抽离 5.5.1 方法1:从服务器加载 5.5.2 方法2:简单客户端模板 5.5.3 方法3:复杂客户端模板 第6章 避免使用全局变量 6.1 全局变量带来的问题 6.1.1 命名冲突 6.1.2 代码的脆弱性 6.1.3 难以测试 6.2 意外的全局变量 避免意外的全局变量 6.3 单全局变量方式 6.3.1 命名间 6.3.2 模块 6.4 零全局变量 第7章 事件处理 7.1 典型用法 7.2 规则1:隔离应用逻辑 7.3 规则2:不要分发事件对象 第8章 避免比较” 8.1 检测原始值 8.2 检测引用值 8.2.1 检测函数 8.2.2 检测数组 8.3 检测属性 第9章 将配置数据从代码中分离出来 9.1 什么是配置数据 9.2 抽离配置数据 9.3 保存配置数据 第10章 抛出自定义错误 10.1 错误的本质 10.2 在JavaScript中抛出错误 10.3 抛出错误的好处 10.4 何时抛出错误 10.5 try-catch语句 10.6 错误类型 第11章 不是你的对象不要动 11.1 什么是你的 11.2 原则 11.2.1 不覆盖方法 11.2.2 不新增方法 11.2.3 不删除方法 11.3 更好的途径 11.3.1 基于对象的继承 11.3.2 基于类型的继承 11.3.3 门面模式 11.4 关于Polyfill的注解 11.5 阻止修改 第12章 浏览器嗅探 12.1 User-Agent检测 12.2 特性检测 12.3 避免特性推断 12.4 避免浏览器推断 12.5 应当如何取舍 第三部分 自动化 第13章 文件和目录结构 13.1 最佳实践 13.2 基本结构 第14章 Ant 14.1 安装 14.2 配置文件 14.3 执行构建 14.4 目标操作的依赖 14.5 属性 14.6 Buildr项目 第15章 校验 15.1 查找文件 15.2 任务 15.3 增强的目标操作 15.4 其他方面的改进 15.5 Buildr任务 第16章 文件合并和加工 16.1 任务 16.2 行尾结束符 16.3 文件头和文件尾 16.4 加工文件 第17章 文件精简和压缩 17.1 文件精简 17.1.1 使用YUI Compressor精简代码 17.1.2 用Closure Compiler精简 17.1.3 使用UglifyJS精简 17.2 压缩 17.2.1 运行时压缩 17.2.2 构建时压缩 第18章 文档化 18.1 JSDoc Toolkit 18.2 YUI Doc 第19章 自动化测试 19.1 YUI Test Selenium引擎 19.1.1 配置一台Selenium服务器 19.1.2 配置YUI Test Selenium引擎 19.1.3 使用YUI Test Selenium引擎 19.1.4 Ant的配置写法 19.2 Yeti 19.3 PhantomJS 19.3.1 安装及使用 19.3.2 Ant的配置写法 19.4 JsTestDriver 19.4.1 安装及使用 19.4.2 Ant的配置写法 第20章 组装到一起 20.1 被忽略的细节 20.2 编制打包计划 20.2.1 开发版本的构建 20.2.2 集成版本的构建 20.2.3 发布版本的构建 20.3 使用CI系统 20.3.1 Jenkins 20.3.2 其他CI系统 附录A JavaScript编码风格指南 附录B JavaScript工具集

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值