美文网首页
《编写可维护的JavaScript》读书笔记之编程实践-避免“空

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

作者: 游学者灬墨槿 | 来源:发表于2019-01-07 14:04 被阅读3次

避免“空比较”

在 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()。

相关文章

网友评论

      本文标题:《编写可维护的JavaScript》读书笔记之编程实践-避免“空

      本文链接:https://www.haomeiwen.com/subject/marurqtx.html