翻译: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
orfalse
- Number 数值:
1
,6.7
,0xFF
- 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. Aconst
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';
}
变量index
, item
和 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 returnsundefined
.
函数内没有执行 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中,下面这些语句,必需用分号(;)结尾:
- 空语句
let
,const
,var
,import
,export
声明- 表达式
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
- 对于不重要的函数参数使用默认值
- 验证属性存在或填充不安全对象的默认属性
- 避免使用稀疏数组
(译者注:终于翻译完了。感觉有些地方有点啰嗦啊。同样的意思来来去去的讲。)