资讯专栏INFORMATION COLUMN

<<编写可维护的javascript>> 笔记8(避免'空比较&#

young.li / 1514人阅读

摘要:中常常会看到这种代码变量与的比较这种用法很有问题用来判断变量是否被赋予了一个合理的值比如不好的写法执行一些逻辑这段代码中方法显然是希望是一个数组因为我们看到的拥有和这段代码的意图非常明显如果参数不是一个数组则停止接下来的操作这种写法的问题在

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().

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90702.html

相关文章

  • &lt;&lt;编写维护javascript&gt;&gt; 笔记1(基本格式化)

    摘要:程序是写给人读的只是偶尔让计算机执行一下当你刚刚组建一个团队时团队中的每个人都各自有一套编程习惯毕竟每个成员都有着不同的背景有些人可能来自某个皮包公司身兼数职在公司里面什么事都做还有些人会来自不同的团队对某种特定的做事风格情有独钟或恨之入骨 程序是写给人读的,只是偶尔让计算机执行一下. Donald Knuth 当你刚刚组建一个团队时,团队中的每个人都各自有一套编程习惯.毕竟,...

    wfc_666 评论0 收藏0
  • &lt;&lt;编写维护javascript&gt;&gt; 笔记5(UI层松耦合)

    摘要:由于第四章太稀松平常了于是就直接跳到第五章了这里我就草草的说一下第四章的几个点吧在严格模式的应用下不推荐将用在全局作用域中相等推荐尽量使用和守则如果是在没有别的方法来完成当前任务这时可以使用原始包装类型不推荐创建类型时用等创建类型从这一章节 由于第四章太稀松平常了, 于是就直接跳到第五章了.这里我就草草的说一下第四章的几个点吧 在严格模式的应用下 不推荐将use strict;用在全...

    saucxs 评论0 收藏0
  • &lt;&lt;编写维护javascript&gt;&gt; 笔记9(将配置数据从代码中分离出来

    摘要:代码无非是定义一些指令的集合让计算机来执行我们常常将数据传入计算机由指令对数据进行操作并最终产生一个结果当不得不修改数据时问题就来了任何时候你修改源代码都会有引入的风险且值修改一些数据的值也会带来一些不必要的风险因为数据时不应当影响指令的正 代码无非是定义一些指令的集合让计算机来执行. 我们常常将数据传入计算机, 由指令对数据进行操作, 并最终产生一个结果. 当不得不修改数据时问题就来...

    xbynet 评论0 收藏0
  • &lt;&lt;编写维护javascript&gt;&gt; 笔记3(语句和表达式)

    摘要:所有的块语句都应当使用花括号包括花括号的对齐方式第一种风格第二种风格块语句间隔第一种在语句名圆括号和左花括号之间没有空格间隔第二种在左圆括号之前和右圆括号之后各添加一个空格第三种在左圆括号后和右圆括号前各添加一个空格我个人喜欢在右括号之后添 所有的块语句都应当使用花括号, 包括: if for while do...while... try...catch...finally 3....

    OBKoro1 评论0 收藏0
  • &lt;&lt;编写维护javascript&gt;&gt; 笔记2(注释)

    摘要:注释是代码中最常见的组成部分它们是另一种形式的文档也是程序员最后才舍得花时间去写的但是对于代码的总体可维护性而言注释是非常重要的一环打开一个没有任何注释的文件就好像趣味冒险但如果给你的时间有限这项任务就变成了折磨适度的添加注释可以解释说明代 注释是代码中最常见的组成部分.它们是另一种形式的文档,也是程序员最后才舍得花时间去写的.但是,对于代码的总体可维护性而言,注释是非常重要的一环.打...

    renweihub 评论0 收藏0

发表评论

0条评论

young.li

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<