Effective JavaScript Item 54 将undefined视为"没有值"

将undefined视为"没有值"

JavaScript中的undefined是一个特殊的值:当JavaScript没有提供具体的值时,它就会产生undefined。

比如:

  1. 未被赋值的变量的初始值就是undefined
  2. 访问对象中不存在的属性会得到undefined
  3. 没有返回值的函数,undefined会作为其返回值
  4. 函数的参数没有提供时,它的值就是undefined
// 情形1
var x;
x; // undefined

// 情形2
var obj = {};
obj.x; // undefined

// 情形3
function f() {
    return;
}
function g() { }

f(); // undefined
g(); // undefined

// 情形4
function f(x) {
    return x;
}
f(); // undefined

将undefined视为任何具体值的缺失是JavaScript语言的一种约定。所以,将它作为其它用途使用就是一种具有风险的行为。比如,一个库中的highlight方法用来改变元素的背景颜色:

element.highlight(); // use the default color
element.highlight("yellow"); // use a custom color

如果我们想让highlight方法具备返回随机颜色的功能,我们也许会尝试使用undefined作为这种情况下需要传入的参数来和其他情况区别开:

element.highlight(undefined); // use a random color

但是,这样做是有风险的。比如,我们可能会向该方法中传入一个对象的属性,如果该属性没有值时,highlight方法就会返回一个随机的颜色,但是这种情况下,用户期望的结果应该是为该元素使用默认的颜色。

var config = JSON.parse(preferences);
// ...
element.highlight(config.highlightColor); // may be random

除了使用undefined之外,有些开发人员可能会选择同样比较特殊的null作为参数传入来进行区分:

element.highlight(null);

但是,这样的代码的可读性比较差。用户第一眼看上去会猜想此方法是要移除element的背景颜色,而不是八竿子打不着的返回随机颜色。

一个更好的API应该是这样的,通过传入字符串来表名意图:

element.highlight("random");

// 或者通过配置对象,关于配置对象可以参考Item 55
element.highlight({ random: true });

另外一个需要注意undefined的地方是拥有可选参数的函数。虽然可以通过arguments对象(关于此对象,可以参考Item 51)对实际传入的参数进行判断,但是对参数进行undefined判断能够让API更加健壮。比如,一个Server对象或许会接受host名作为参数:

var s1 = new Server(80, "example.com");
var s2 = new Server(80); // defaults to "localhost"

function Server(port, hostname) {
    if (arguments.length < 2) {
        hostname = "localhost";
    }
    hostname = String(hostname);
    // ...
}

以上代码使用arguments的length值作为判断依据,来给hostname参数一个默认值。但是,如果hostname被传入了undefined,就会导致默认值不会生效:

// config.hostname为undefined时,就跳过了以上的检查
var s3 = new Server(80, config.hostname);

// 更好的办法是显式地对undefined进行检查
function Server(port, hostname) {
    if (hostname === undefined) {
        hostname = "localhost";
    }
    hostname = String(hostname);
    // ...
}

一种替代方案是进行真值判断(参见Item 3):

function Server(port, hostname) {
    hostname = String(hostname || "localhost");
    // ...
}

依据是undefined在做真值判断时会返回false,因此默认值localhost会生效。

但是需要注意在某些情况下使用真值判断也是不安全的。

当一个函数能够接受空的字符串作为合法参数时,进行真值判断就会将传入的空字符串替换为默认值。类似的,如果一个函数能够接受数字0(或者特殊的NaN)作为合法参数,真值判断也会将它替换成默认值。

比如,下面的API用来通过传入元素的宽度和高度进行创建。如果没有传入,则使用默认值:

var c1 = new Element(0, 0); // width: 0, height: 0
var c2 = new Element(); // width: 320, height: 240

function Element(width, height) {
    this.width = width || 320; // wrong test
    this.height = height || 240; // wrong test
    // ...
}

var c1 = new Element(0, 0);

c1.width; // 320
c1.height; // 240

当我们传入0时,真值判断会将它替换成默认值。然而这并不是我们想要的行为。更好的方式是显式对undefined进行判断:

function Element(width, height) {
    this.width = width === undefined ? 320 : width;
    this.height = height === undefined ? 240 : height;
    // ...
}

var c1 = new Element(0, 0);

c1.width; // 0
c1.height; // 0

var c2 = new Element();
c2.width; // 320
c2.height; // 240

总结

  1. 不要使用undefined来表达除了缺失特定值外的任何其他意义。
  2. 在需要表达特殊情况时,不要使用undefined或者null。而是使用更具表达性的字符串或者对象。
  3. 在函数中显式地对参数进行undefined检查,而不要依赖于诸如arguments.length等检查方法。
  4. 对于能够接受真值判断返回false的特殊值(如0,NaN,null,""),不要使用真值判断。
时间: 2024-08-04 23:06:25

Effective JavaScript Item 54 将undefined视为"没有值"的相关文章

Effective JavaScript Item 25 使用bind方法来得到一个固定了this指向的方法

本系列作为Effective JavaScript的读书笔记. 当需要将方法抽取出来作为回调函数使用的时候,常常会因为this的指向不明而发生错误,比如: var buffer = { entries: [], add: function(s) { this.entries.push(s); }, concat: function() { return this.entries.join(""); } }; 如果想利用其中的add作为回调函数对一组数据进行添加: var source

Effective JavaScript Item 23 永远不要修改arguments对象

本系列作为Effective JavaScript的读书笔记. arguments对象只是一个类似数组的对象,但是它并没有数组对象提供的方法,比如shift,push等.因此调用诸如:arguments.shift(),arguments.push()是错误的. 在Item 20和Item 21中,知道了函数对象上存在call和apply方法,那么是不是可以利用它们来让arguments也能够利用数组的方法呢: function callMethod(obj, method) { var shi

Effective JavaScript Item 13 使用即时调用的函数表达式(IIFE)来创建局部域

本系列作为Effective JavaScript的读书笔记. 所谓的即时调用的函数表达式,这个翻译也许不太准确,它对应的英文原文是Immediately Invoked Function Expression (IIFE).下文也使用IIFE来表达这一概念. 首先看一个程序: function wrapElements(a) { var result = [], i, n; for (i = 0, n = a.length; i < n; i++) { result[i] = function

Effective JavaScript Item 31 优先使用Object.getPrototypeOf,而不是__proto__

本系列作为Effective JavaScript的读书笔记. 在ES5中引入了Object.getPrototypeOf作为获取对象原型对象的标准API.可是在非常多运行环境中.也提供了一个特殊的__proto__属性来达到相同的目的. 由于并非全部的环境都提供了这个__proto__属性,且每一个环境的实现方式各不同样,因此一些结果可能不一致: // 在某些环境中 var empty = Object.create(null); // object with no prototype "__

Effective JavaScript Item 55 接受配置对象作为函数参数

接受配置对象作为函数参数 虽然保持函数接受的参数的顺序很重要,但是当函数能够接受的参数达到一定数量时,也会让用户很头疼: var alert = new Alert(100, 75, 300, 200, "Error", message, "blue", "white", "black", "error", true); 随着函数的不断重构和进化,它能够接受的参数也许会越来越多,最终就像上面的例子那样. 对

Effective JavaScript Item 20 使用call方法来绑定this变量

本系列作为Effective JavaScript的读书笔记. 通常而言,一个函数中this的指向和该函数的调用类型相关,比如当函数直接作为函数被调用时,this一般指向的是全局对象(StrictMode时指向undefined):当函数作为方法被调用时(即x.method()这种形式),this指向的是x:当函数作为构造方法被调用时,this指向的是一个新创建的对象. 但是在一些场合,需要指定this的指向,比如下面的代码需要将this指向一个对象obj,一个简单的办法如下: obj.temp

Effective JavaScript Item 11 掌握闭包

本系列作为Effective JavaScript的读书笔记. 掌握闭包,需要知道以下几个关键点: JavaScript允许在当前的function中访问该function外部的变量. function makeSandwich() { var magicIngredient = "peanut butter"; function make(filling) { return magicIngredient + " and " + filling; } return

Effective JavaScript Item 26 使用bind来进行函数的柯里化(Curry)

本系列作为Effective JavaScript的读书笔记. 在上一个Item中介绍了bind的一种用法:用来绑定this对象.但是实际上,bind含有另一种用法,就是帮助函数进行柯里化.关于柯里化,这里有一份百科可以参考: http://zh.wikipedia.org/wiki/%E6%9F%AF%E9%87%8C%E5%8C%96 但是实际上,关于柯里化只需要记住一点就够了:柯里化是把接受多个参数的函数变换成接受一个单一参数(通常是最初函数的第一个参数,但是并无限制)的函数,并且返回这个

Effective JavaScript Item 24 使用一个变量来保存arguments的引用

本系列作为Effective JavaScript的读书笔记. 假设需要一个API用来遍历若干元素,像下面这样: var it = values(1, 4, 1, 4, 2, 1, 3, 5, 6); it.next(); // 1 it.next(); // 4 it.next(); // 1 相应的实现可以是: function values() { var i = 0, n = arguments.length; return { hasNext: function() { return