翻译:JavaScript 中处理 undefined 的 7 个技巧

7 tips to handle undefined in JavaScript

上面是原文链接。今天想尝试来翻译下这篇文章。

------------- 我是正文如下的分割线 ----------------

大约八年前,我刚开始学习JavaScript,我觉得很奇怪的是,undefined和null都代表空值。那么它们之间有什么明显的区别?它们似乎都定义空值,而且在控制台比较null == undefined输出为true。

大多数的现代语言如Ruby,Python或Java都只有一个空值(nil或null),看上去很合理的样子。

而对于JavaScript,当访问尚未初始化的变量或对象属性时,解释器将返回undefined。举个栗子:

let company;  
company;    // => undefined  
let person = { name: 'John Smith' };  
person.age; // => undefined  

null则表示不存在的对象引用。JavaScript本身并不将变量或对象属性设置为null。

一些内部对象的方法比如 String.prototype.match() 可以通过返回 null 来表示一个丢失的对象。具体看下面的例子:

let array = null;  
array;                // => null  
let movie = { name: 'Starship Troopers',  musicBy: null };  
movie.musicBy;        // => null  
'abc'.match(/[0-9]/); // => null 

由于javascript是松散型语言,开发者很容易被访问未初始化的值诱惑。我也曾犯过这样低级的错误。

通常这样的冒险行为会引发undefined的相关错误,并迅速停止脚本运行。相关的常见错误信息是:

  • TypeError: 'undefined' is not a function
  • TypeError: Cannot read property '<prop-name>' of undefined
  • 其他类似的类型错误

一个JavaScript开发者都懂的笑话:

function undefined() {  
  // problem solved
}

为了减少此类错误的风险,您必须了解可能引发 undefined的场景。更重要的是避免它在你的应用程序中出现并引发其他错误,这增加了代码的耐用性。

接下来,让我们详细了解undefined以及其对代码安全的影响。

1. undefined 是什么

JavaScript有6种基本数据类型:

  • Boolean 布尔值: true or false
  • Number 数值: 16.70xFF
  • String 字符串: "Gorilla and banana"
  • Symbol 独一无二的值: Symbol("name") (ES6 引入)
  • Null: null
  • Undefined: undefined.

另外还有一种Object 类型:{name: "Dmitri"}["apple", "orange"].(由键值对组成)

在这6种基本类型中,undefined是undefined类型的唯一的值。

根据ECMAScript标准:

  Undefined value primitive value is used when a variable has not been assigned a value.

当一个变量(声明后)没有被赋值时,这个变量的值会被默认为undefined。

标准明确规定,当您访问未初始化的变量,或者不存在的对象属性、数组元素等等,您会得到一个值undefined。例如:

let number;  
number;     // => undefined  
let movie = { name: 'Interstellar' };  
movie.year; // => undefined  
let movies = ['Interstellar', 'Alexander'];  
movies[3];  // => undefined  

正如上面的例子所示,访问:

  • 一个未初始化的变量 number
  • 对象未定义的属性 movie.year
  • 或者数组中不存在的元素 movies[3]

均会得到一个值:undefined

ECMAScript规范规定了undefined值的类型:

Undefined type is a type whose sole value is the undefined value.

undefined类型只有一个唯一的值:undefined

从这个意义上,typeof运算符为undefined值返回一个字符串undefined:

typeof undefined === 'undefined'; // => true  

当然,typeof 可以很好验证一个变量是否包含了一个未定义的值:

let nothing;  
typeof nothing === 'undefined';   // => true  

2. 引发undefined的常见场景

2.1 未初始化变量

A declared variable that is not yet assigned with a value (uninitialized) is by default undefined.

“声明一个变量,未赋值(未初始化),变量的值默认为undefined。”

举个显而易见的例子:

let myVariable;  
myVariable; // => undefined  

声明了变量 myVariable,未赋值。那么访问这个变量,返回undefined

解决未初始化变量问题的一个有效方法是,尽可能分配一个初始值。变量未初始化情况越少越好。理想情况下,您在声明一个变量后立刻赋值 const myVariable = 'Initial value',但这并不总是如您所愿。

Tip 1: 使用 const,或者 let,不使用 var

在我看来,ECMAScript 2015的最佳特色之一,便是提供了声明变量的新方法:const 和 let。这是一个很大的进步,这些声明的作用域在其代码所在的代码块以内(相反,var声明的作用域在该语句所在的函数体内),并且保存在一个“暂存死区”内直到变量被声明。

当要给一个变量赋一个值并且不修改值的时候,我建议用 const 声明变量。它创建了一个不可变的绑定。

<---------- 插入非翻译文原文的题外话的分割线 START ------>

在查资料的时候发现,有些人认为const声明的是不可变的常量。这是不完全正确的(译者注:作为一个菜鸟,说这句话总有点底气不足)。看原文:

ES6 const does not indicate that a value is ‘constant’ or immutable. A const value can definitely change. The following is perfectly valid ES6 code that does not throw an exception:

const foo = {};
foo.bar = 42;
console.log(foo.bar);
// → 42

这个代码并未抛出异常,说明const声明的变量是可变的。不可变的只是const声明的变量所创建的绑定。(这里就不展开叙述)

<---------- 插入非翻译文原文的题外话的分割线 END ------>

const的一个美妙特性是,你必须给变量赋值一个初始值 const myVariable = 'initial'. 变量不会暴露在初始化状态,也不可能访问到undefined。

下面这个函数,让我们来验证一个词是否一个回文:

function isPalindrome(word) {  
  const length = word.length;
  const half = Math.floor(length / 2);
  for (let index = 0; index < half; index++) {
    if (word[index] !== word[length - index - 1]) {
      return false;
    }
  }
  return true;
}
isPalindrome('madam'); // => true  
isPalindrome('hello'); // => false  

 length 和 half 两个变量被一次性赋值,值也不会被修改,因此用const来声明看上去很合理。

如果您需要重新绑定变量(即多次赋值),那么用 let 来声明变量。只要有可能,立即给它分配一个初始值,例如 let index = 0.

那么老家伙 var 怎么办?基于ES2015,我的建议是把它扫进历史垃圾堆吧。

使用var来声明变量的一个问题是,发生在整个函数作用域变量提升。您可以在函数作用域底部声明一个 var 变量,就可以在函数顶部访问到这个声明的变量,然后得到一个值:undefined。

function bigFunction() {  
  // code...
  myVariable; // => undefined
  // code...
  var myVariable = 'Initial value';
  // code...
  myVariable; // => 'Initial value'
}
bigFunction();  

在这个语句 var myVariable = 'Initial value'之前,变量 myVariable 就可以访问,并且含有一个 undefined 的值。

相反的,let (包括 const) 声明的变量在声明之前无法访问。因为在声明之前,变量保存在一个暂存死区(TDZ = temporl dead zone)内。这很愉快,因为您没有多少机会获取到一个undefined的值。

上诉例子,用 let 来代替 var,会抛出一个 ReferenceError 异常,因为您无法访问在TDZ里的变量。

function bigFunction() {  
  // code...
  myVariable; // => 抛出异常 'ReferenceError: myVariable is not defined'
  // code...
  let myVariable = 'Initial value';
  // code...
  myVariable; // => 'Initial value'
}
bigFunction();  

给不可变的绑定使用 const 或者 let,尽量避免您的代码暴露给未初始化的变量。

Tip 2: 增加聚合度

聚合度是指一个模块内部(命名空间、类、方法、代码块)承担职责之间的相关程度。评估聚合度强度,我们通常称为高内聚或者低内聚。

高内聚略胜一筹,因为高内聚意味着一个模块仅完成一个独立的功能(译者注:模块内部不存在与该功能无关的操作或状态。),它的优点是:

  • 专一性和易于理解性: 更容易理解一个模块的功能。
  • 可维护性和容易重构:减少模块对其他模块内部实现的依赖。
  • 可重用:专注于一个单一的任务,它使模块更容易重用。
  • 测试性:可以更容易地测试集中在单个任务上的模块。

(a. 低耦合高内聚 b. 高耦合低内聚)

好的设计的一个特征,就是高内聚低耦合。

一个代码块本身可以看作是一个小模块。为了获得高内聚的好处,您需要将变量尽可能地靠近使用它们的代码块。

例如,如果一个变量的功能只是用在块作用域内,则声明变量并只允许变量在该块中生存(通过使用 const 或 let 声明)。不要将这个变量暴露在这个块作用域外,因为这个变量和外部无关

一个典型例子是函数内使用for循环导致变量寿命过长:

function someFunc(array) {  
  var index, item, length = array.length;
  // some code...
  // some code...
  for (index = 0; index < length; index++) {
    item = array[index];
    // some code...
  }
  return 'some result';
}

变量indexitem 和 length 在函数体顶部声明,却在底部才被引用。那么这种方法有什么问题呢?

所有在顶部声明变量,在for循环内使用变量的方式,变量 item, index,item 未被初始化并且面临(返回)一个 undefined。它们的生命周期很不讲道理地,长达整个函数作用域。

一个更好的方法是在靠近第一次使用的位置初始化变量,

function someFunc(array) {  
  // some code...
  // some code...
  const length = array.length;
  for (let index = 0; index < length; index++) {
    const item = array[index];
    // some 
  }
  return 'some result';
}

变量 index 和 item 只生存在for循环体内。在for循环外,它们没有任何意义。

变量length也是在引用它的位置附近声明。

为什么修改后的版本比上一个版本更好一些呢。让我们来看看它的优势:

  • 变量并未暴露在未初始化状态,减少您读取到undefined的风险。
  • 尽可能的把变量定义在靠近使用它的地方,增加代码可读性。
  • 高内聚的代码更容易重构、在必要时更容易提取分离功能。

2.1 访问非现有属性

When accessing a non-existing object property, JavaScript returns undefined.

读取不存在的对象属性时JavaScript会返回 undefined。

下面用一个例子来论证:

let favoriteMovie = {  
  title: 'Blade Runner'
};
favoriteMovie.actors; // => undefined  

对象 favoriteMovie 只有一个属性 title,使用属性访问器 favoriteMovie.actors 读取不存在的属性 actors 返回 undefined。

读取不存在的属性并不会报错。真正的问题出现在试图从非现有属性值获取数据时。这是undefined引发的普遍陷阱,比如一个众所周知的报错信息:TypeError: Cannot read property <prop> of undefined.

让我们稍微修改前面的代码片段来表明一个TypeError异常:

let favoriteMovie = {  
  title: 'Blade Runner'
};
favoriteMovie.actors[0];  
// TypeError: Cannot read property '0' of undefined

favoriteMovie 没有 actors 这个属性,因此这个属性是undefined。

结果就是,用 favoriteMovie.actors[0] 读取一个未定义值的第一个元素,抛出一个类型异常:TypeError。

JavaScript的允许访问非现有属性的这个特性是造成这个混淆的来源。这个属性设置了吗,还是未设置。避开这个问题的理想方法是约束对象始终定义它所持有的属性。

然而,您并不总是能控制你所使用的对象。这些对象在不同的场景中可能有不同的属性集。所以你必须手动处理所有这些场景。

假设现在要实现一个append(array, config)函数,它可以在数组的开始和/或结束时添加新元素。参数 config 接受具有以下属性的对象:

  • first: 在数组前插入元素
  • last: 在数组结尾插入元素.

这个函数返回一个新的数组,不会更改原数组(也就是说,它是一个纯函数)。(译注:纯函数指 不依赖于且不改变它作用域之外的变量状态 的函数。返回值只由它调用时的参数决定。

下面看append()函数的一个简单粗略的例子。

function append(array, config) {  
  const arrayCopy = array.slice();
  if (config.first) {
    arrayCopy.unshift(config.first);
  }
  if (config.last) {
    arrayCopy.push(config.last);
  }
  return arrayCopy;
}
append([2, 3, 4], { first: 1, last: 5 }); // => [1, 2, 3, 4, 5]  
append(['Hello'], { last: 'World' });     // => ['Hello', 'World']  
append([8, 16], { first: 4 });            // => [4, 8, 16]  

因为对象 config 可以省略第一个或最后一个属性,所以必须验证这些属性是否存在于对象 config 中。

属性如果不存在,则返回undefined。条件语句if(config.first){}和if(config.last){},用来验证 first 或 last 属性是否未定义,检查属性是否存在。

先不忙下定论。这个方法有一个严重的缺点。undefined,还有 false,null,0,NaN 和 " " 都是falsy值(译者注:当进行逻辑判断时均为false)。

在这种情况下,参数的属性值为falsy的函数被拒绝执行。

append([10], { first: 0, last: false }); // => [10]  

 由于 0 和 false都是falsy,if(config.first){} 和 if(config.last){}对 falsy进行了对比,这两个元素并不会被插入到数组,函数返回了一个未修改的数组[10]。

下面的提示说明如何正确检查属性是否存在。

Tip 3: 检查属性是否存在

幸运的是,JavaScript提供了一系列方法来确定对象是否具有某种属性:

  • obj.prop !== undefined: 直接和 undefined 对比 
  • typeof obj.prop !== 'undefined': 验证属性的值的类型
  • obj.hasOwnProperty('prop'): (接收一个字符串参数)验证对象是否具有自己的(不是在原型链中的)某个(这个参数名字的)属性。
  • 'prop' in obj: 验证对象是否拥有或者继承某个属性。

我的建议是使用 in 操作符,它是一个语法糖,目的很明确,只检查对象是否具有特定属性,而不访问实际的属性值。

obj.hasOwnProperty('prop') 也是一个比较好的解决办法。它比 in 操作符稍长,只验证对象本身的属性。

以上提到的两个方式,在和 undefined 比较时有用。但是在我看来, obj.prop !== undefined 和 typeof obj.prop !== 'undefined' 显得冗长怪异,并且暴露了一个直接处理 undefined的环境变量(译者注:这句不太理解)。

我们用操作符 in 来改进代码:

function append(array, config) {  
  const arrayCopy = array.slice();
  if ('first' in config) {
    arrayCopy.unshift(config.first);
  }
  if ('last' in config) {
    arrayCopy.push(config.last);
  }
  return arrayCopy;
}
append([2, 3, 4], { first: 1, last: 5 }); // => [1, 2, 3, 4, 5]  
append([10], { first: 0, last: false });  // => [0, 10, false]  

相应的属性只要存在,'first' in config (和  'last' in config) 就是 true, 否则就是false。

操作符 in 解决了属性值为 0 和 false的问题,函数执行得到了我们想要的结果:[0, 10, false].

Tip 4:解构对象属性

读取对象属性时,如果属性不存在,则需要指示默认值。

结合三元运算符和 in 操作符来完成这个目的:

const object = { };  
const prop = 'prop' in object ? object.prop : 'default';  
prop; // => 'default' 

需要检查的属性越多,三元运算符的语法就越难用。对于每一个属性,您必须创建一行新的代码来处理默认值,就像垒砌一堵三元运算符的丑陋的墙。

为了让我们的代码更优雅一些,我们来了解下ES2015的这个超赞的新语法:解构。

对象解构允许直接将对象属性值直接插入变量中,如果属性不存在,则设置默认值(译者注:解构可以用很简洁的方式为未定义属性或值设置默认值)。这个语法避免直接处理undefined。

真正地实现了简短并且意义明确地获取属性:

const object = {  };  
const { prop = 'default' } = object;  
prop; // => 'default'  

为了查看它如何工作,让我们定义一个函数,用引号包一个字符串。quote(subject, config)的第一个参数作为要包装的字符串,第二个参数 config 是一个具有以下属性的对象:

  • char: 符号, 例如  ' (单引号) 或者  " (双引号)。 默认为 ".
  • skipIfQuoted: 字符串如果已有引号,则跳过这个字符串。返回一个布尔值。默认为true。

应用对象解构的优势,我们来实现这个函数 quote():

function quote(str, config) {  
  const { char = '"', skipIfQuoted = true } = config;
  const length = str.length;
  if (skipIfQuoted
      && str[0] === char
      && str[length - 1] === char) {
    return str;
  }
  return char + str + char;
}
quote('Hello World', { char: '*' });        // => '*Hello World*'  
quote('"Welcome"', { skipIfQuoted: true }); // => '"Welcome"'  

const { char = '"', skipIfQuoted = true } = config 一行代码解构赋值,从对象config 提取属性char 和skipIfQuoted 

如果config对象中的个别属性未定义,解构赋值也可以为 char 设置默认值为 "", 为 skipIfQuoted 设置默认值为 true(译者注:原文为false,但我觉得这里应该是true)。

幸运的是,这个函数还有改进空间。

直接把解构赋值作为参数,并把参数config设置默认值为一个空的对象 {},当有足够的默认设置时,省略第二个参数(??)。

function quote(str, { char = '"', skipIfQuoted = true } = {}) {  
  const length = str.length;
  if (skipIfQuoted
      && str[0] === char
      && str[length - 1] === char) {
    return str;
  }
  return char + str + char;
}
quote('Hello World', { char: '*' }); // => '*Hello World*'  
quote('Sunny day');                  // => '"Sunny day"'  

注意,这里用解构赋值代替了参数config来作为函数签名。我更喜欢这样:quote()少了一行。

= {}在解构赋值表达式的右侧,确保如果没有指定第二个参数,则使用空对象。quote('Sunny day').

对象解构是一个强大的功能,更直观清晰地提取对象的属性。我喜欢在访问未定义的属性时指定要返回的默认值。

因为这样可以避免 undefined 和 undefined带来的问题。

Tip 5: 用默认属性填充对象

如果不需要像解构赋值那样给每个属性创建变量,可以用默认值来填充缺失一些属性的对象。

ES2015中, Object.assign(target, source1, source2, ...) 将源对象(source)的所有可枚举属性,复制到目标对象(target)。函数返回目标对象。

(译者注:Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。)

例如,你需要访问unsafeoptions对象的属性,它并不总是包含了完整的属性。

为避免访问不存在的属性时获取undefined,我们来做一些调整:

  • 定义一个对象 defaults 用来保存默认属性值。
  • 调用 Object.assign({ }, defaults, unsafeOptions)创建新对象 options. 新对象从 unsafeOptions接收所有属性, 而缺失的部分则从 defaults 获取.
const unsafeOptions = {  
  fontSize: 18
};
const defaults = {  
  fontSize: 16,
  color: 'black'
};
const options = Object.assign({}, defaults, unsafeOptions);  
options.fontSize; // => 18  
options.color;    // => 'black'  

unsafeOptions 只有一个属性 fontSize。对象 defaults 为属性值 fontSize 和 color 定义了默认值。

Object.assign()第一个参数作为目标对象 {}. 目标对象从源对象unsafeOptions获取属性fontSize的值,由于unsafeOptions不具有属性color, 从源对象 defaults 获取属性 color 的值。

所枚举的源对象的位置很重要:后面的属性会覆盖前面的属性。

现在您可以很安全的访问option对象的任何属性,包括并未在unsafeOptions对象中的 options.color。

其实还有一个更容易和更简洁的方法来填充对象的默认属性。

我推荐使用一个新的JavaScript语法(现在3级),对象字面量的扩展特性。

不调用 Object.assign(),而是使用对象的扩展语法从源对象复制可枚举的属性到目标对象。

(译者注:spread syntax扩展语法:可以使用三个点作为前缀,即 ... 应用于可遍历对象上,访问每个元素。)

const unsafeOptions = {  
  fontSize: 18
};
const defaults = {  
  fontSize: 16,
  color: 'black'
};
const options = {  
  ...defaults,
  ...unsafeOptions
};
options.fontSize; // => 18  
options.color;    // => 'black'  

(译者注:如果您的浏览器在这个例子上报错了,您可以下一个babel插件:babel-plugin-transform-object-rest-spread ,也可以看另外几个简单例子:扩展语法复制一个数组,复制的是引用。这里就不多做展开)

1. 复制数组:
const names = ['Luke','Eva','Phil']; const copiedList = [...names] console.log(copiedList);// ['Luke','Eva','Phil']

2. 连接数组: const concatinated = [...names, ...names]; console.log(concatinated); // ['Luke','Eva','Phil', 'Luke','Eva','Phil']

对象字面量可以把两个源对象的属性扩展到一起。所枚举的源对象的位置很重要:后面的属性会覆盖前面的属性。

使用默认属性值填充不完整的对象是使代码安全持久的有效策略。无论情况如何,对象始终包含完整的属性集:undefined也不会再出现。

2.3 函数参数

The function parameters implicitly default to undefined.

函数参数默认为未定义。

通常,具有特定参数个数的函数调用时,应该具备相同数量的参数。在这种情况下,参数得到你期望的值:

function multiply(a, b) {  
  a; // => 5
  b; // => 3
  return a * b;
}
multiply(5, 3); // => 15  

 multiply(5, 3) 调用时,参数a,b接收相应的值 5 和 3。并按照预期执行: 5 * 3 = 15.

当你忽略了调用的参数时会发生什么?函数参数变成 undefined。让我们稍微修改前面的例子,调用函数只有一个参数:

function multiply(a, b) {  
  a; // => 5
  b; // => undefined
  return a * b;
}
multiply(5); // => NaN

函数function multiply(a, b) { }在参数完整的情况下正常执行。

 multiply(5)函数调用只有一个参数,参数 a 是5,而 b 则为undefined。

Tip 6: 利用默认参数值

有时函数在调用时不需要全部参数集。您可以简单地为没有值的参数设置默认值。

回顾上面的例子,我们来做一些改进。如果参数 b 未定义,那么给它赋值默认为2.

function multiply(a, b) {  
  if (b === undefined) {
    b = 2;
  }
  a; // => 5
  b; // => 2
  return a * b;
}
multiply(5); // => 10  

 multiply(5)调用时候只有一个参数。参数 a 是 5,b 为 undefined。

条件语句验证 b 是否未定义,如果未定义,则设定默认值为2。

虽然这个验证办法有效,但我不推荐使用。冗长并且杂乱无章。

更好的办法是使用 ES6的默认参数 特性(译者注:可以指定任意参数的默认值。)。它简洁、直观,并且无需和undefined直接比较。

继续修改前面的例子,设置默认参数b,看起来更好一些:

function multiply(a, b = 2) {  
  a; // => 5
  b; // => 2
  return a * b;
}
multiply(5);            // => 10  
multiply(5, undefined); // => 10  

b = 2作为函数签名,确保参数 b 如果未提供,参数默认值为 2。

ES2015的默认参数 简洁、直观,接下来都使用它为可选参数设置默认值吧。

2.4 函数返回值

Implicitly, without return statement, a JavaScript function returns undefined.

函数内没有执行 return 语句,则把未定义值赋给当前函数。

function square(x) {  
  const res = x * x;
}
square(2); // => undefined  

square()函数不返回任何计算结果。函数调用结果未定义 undefined。

return;语句执行,但表达式被省略,调用函数的表达式结果依旧是未定义 undefined。

当然,(下面的例子)阐释了 return 语句在返回函数的用法:

function square(x) {  
  const res = x * x;
  return res;
}
square(2); // => 4  

现在函数调用后求值为4,它是2的平方。

Tip 7: 不要相信分号自动插入

 JavaScript中,下面这些语句,必需用分号(;)结尾:

  • 空语句
  • letconstvarimportexport 声明
  • 表达式
  • debugger 
  • continue 语句, break 语句
  • throw 语句
  • return 语句

以上任意一条语句,都要用分号结尾:

function getNum() {  
  // 注意结尾有分号
  let num = 1; 
  return num;
}
getNum(); // => 1  

let 和 return 语句都用分号结尾。

不用分号结尾有什么后果?例如,您要压缩源文件。

 ECMAScript提供了一个 Automatic Semicolon Insertion (ASI) 机制,这个机制会为您插入缺失的分号。

于是上一个例子,您可以省略分号:

function getNum() {  
  // 注意分号不见了。
  let num = 1
  return num
}
getNum() // => 1  

这个代码有效。缺失的分号自动补全。

乍一看,它看起来相当不错。ASI机制让您省略不必要的分号。您的JavaScript代码更简洁易读。

但是ASI依旧有一个恼人的坑。return 和 return 后面的表达式,如果中间换行了,比如这样:return \n expression, ASI机制会自动插入分号,变成这样: return; \n expression.

函数内有一个return;语句,意味着什么?函数返回 undefined。如果您并不知道ASI机制的细节,它会被误导,返回一个意外的undefined。

举个栗子,让我们来调用 getPrimeNumbers() 函数,学习它的返回值:

function getPrimeNumbers() {  
  return 
    [ 2, 3, 5, 7, 11, 13, 17 ]
}
getPrimeNumbers() // => undefined  

在return 语句和 数组表达式之间换行,JS自动插入分号,解释器解析代码为:

function getPrimeNumbers() {  
  return; 
  [ 2, 3, 5, 7, 11, 13, 17 ];
}
getPrimeNumbers(); // => undefined  

表达式return; 导致函数并未按照预期执行,而是返回未定义。

删除新行可以解决这个问题:

function getPrimeNumbers() {  
  return [ 
    2, 3, 5, 7, 11, 13, 17 
  ];
}
getPrimeNumbers(); // => [2, 3, 5, 7, 11, 13, 17]  

我的建议是避免依赖ASI机制,自己加上分号。

EsLint规则的一个小功能就是可检查识别语句结束时需要分号的地方。

2.5 void 操作符

void 表达式会被计算但是返回值永远为undefined。

void 1;                    // => undefined  
void (false);              // => undefined  
void {name: 'John Smith'}; // => undefined  
void Math.min(1, 3);       // => undefined  

void的一个用法是执行表达式但不返回值,这个表达式的执行结果会有副作用。

3. 数组中的undefined 

读取数组界外索引值返回undefined。

const colors = ['blue', 'white', 'red'];  
colors[5];  // => undefined  
colors[-1]; // => undefined 

数组 colors 有3个元素,索引值为 0, 1, 2。

索引 5 和 -1 位置并无元素,colors[5] 和 colors[-1] 返回 undefined.

JavaScript,有一个概念叫所谓的稀疏数组。数组中的元素之间可以有空隙,例如一些索引位置上未定义值。

读取稀疏数组中的空隙(也叫空的内存槽位),返回undefined。

来看下面生成稀疏数组并试图读取空槽数据的例子:

const sparse1 = new Array(3);  
sparse1;       // => [<empty slot>, <empty slot>, <empty slot>]  
sparse1[0];    // => undefined  
sparse1[1];    // => undefined  
const sparse2 = ['white',  ,'blue']  
sparse2;       // => ['white', <empty slot>, 'blue']  
sparse2[1];    // => undefined  

用构造函数创建一个长度为3的数组 sparse1,拥有3个空槽(预分配一个数组空间)。

用字面量创建一个数组 sparse2,省略了第二个元素。(省略的元素在数组中是不存在的,是没有值的。)

读取以上任意稀疏数组的空值,均返回undefined。

当使用数组时,为了避免捕获undefined,请确保使用有效的数组索引并避免创建稀疏数组。

4. undefined 和 null 的区别

undefined 和 null 之间的主要区别是什么?这两个特殊值都意味着“无”。

主要区别在于,undefined 表示一个未初始化的变量的值,而 null 表示不应该有值的不存在的对象。

举几个例子仔细探究下。

定义一个变量 number,但尚未赋值。

let number;  
number; // => undefined

变量 number 为undefined,表明自身就是一个未初始化的变量。

读取一个不存在的对象属性,也会产生同样的未初始化概念。

const obj = { firstName: 'Dmitri' };  
obj.lastName; // => undefined  

属性lastName 不在对象 obj 内,JavaScript正确地解析为undefined。

在其他情况下,对象或者函数可以赋值给一个变量,来返回一个对象。但是您无法实例化这个对象。在这种情况下,null 就是判断一个缺失对象的明确指标。

例如,clone() 函数 用来克隆一个普通的JavaScript对象,并返回一个对象:

function clone(obj) {  
  if (typeof obj === 'object' && obj !== null) {
    return Object.assign({}, obj);
  }
  return null;
}
clone({name: 'John'}); // => {name: 'John'}  
clone(15);             // => null  
clone(null);           // => null  

函数clone()的参数如果不是对象,比如 15 或者 null,(或者一个原始值 null 或者 undefined),函数就不会执行克隆任务,因为看起来很合理,返回null代表一个丢失的对象。

(译者注:null 作为函数的参数,表示该函数的参数不是对象。)

typeof 操作符区分二者如下:

typeof undefined; // => 'undefined'  
typeof null;      // => 'object' 

严格运算符 === 正确区分 undefined 和 null

let nothing = undefined;  
let missingObject = null;  
nothing === missingObject; // => false 

5. 总结

 JavaScript作为松散型语言,它可以用undefined来:

  • 未初始化变量
  • 不存在的对象属性或者方法
  • 读取数组界外元素 
  • 调用无返回值函数

像本文提到的那些虽然可行的方法一样,大多数直接比较undefined不是一个好办法。

有效的策略是在代码中尽可能减少出现关键字undefined。同时,记住并极力避免那些可能出现的意外情况,养成以下这些好习惯:

  • 减少未初始化变量的使用
  • 缩短变量生命周期并接近引用位置
  • 尽可能给变量赋值
  • 使用const,或者 let
  • 对于不重要的函数参数使用默认值
  • 验证属性存在或填充不安全对象的默认属性
  • 避免使用稀疏数组

(译者注:终于翻译完了。感觉有些地方有点啰嗦啊。同样的意思来来去去的讲。)

posted @ 2017-04-26 18:37  kiera  阅读(1143)  评论(0编辑  收藏  举报