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';
}
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';
}
引言
undefined
是 JavaScript 中的一个特殊值,另一个特殊值则是null
,这两个特殊值还分别对应了一种数据类型:Undefined 和 Null。这两个值都定义了“空值”,但两个又不是完全相同。对 JavaScript 而言,当访问一个尚未初始化的变量或不存在的对象属性时,会返回
undefined
。例如:而
null
则代表一个缺失的对象引用。JavaScript 本身不将变量或对象属性设置为null
。例如:将
undefined
和null
作比较时,也会返回不同的结果:为什么会出现这种结果呢?简单查阅下 Abstract Equality Comparison 和 Strict Equality Comparison 在规范中的定义便可。
由于 JavaScript 的语法比较宽松,开发人员是可以访问未初始化的值,我也犯过这样的错误。通常这样的危险行为会产生
undefined
的相关错误,终止脚本的执行。常见的相关错误消息是:JavaScript 开发者可以理解这个笑话的讽刺之处:
为了减少此类错误的风险,必须先了解产生
undefined
的情况,然后在应用程序中阻止这种情况的出现,提高代码的稳定性。1.什么是
undefined
JavaScript 的数据类型可以简单分为两类:简单数据类型和复杂数据类型。简单数据类型包含6种基本值类型:
复杂数据类型即为 JavaScript 中的对象类型:{name: "Dmitri"}, ["apple", "orange"]等。
根据 ECMAScript规范,
undefined
是类型 Undefined 的唯一值:根据标准定义,当访问任何未被赋值的变量时均将返回
undefined
值。例如访问未初始化的变量、对象未定义的属性或者不存在的数组元素等:上面的例子演示了访问:
并且都返回了
undefined
。在这种情况下,对于undefined
值,typeof 操作符会返回字符串 'undefined':因为可以使用
typeof
来验证一个变量值是否是undefined
:2.产生 undefined 值的常见场景
2.1 未初始化的变量
一个简单的示例:
声明的变量
myVariable
没有赋值,访问myVariable
时则返回默认的值undefined
。避免这种情况的有效方式则是在声明变量赋予一个默认值。Tip 1: 优先使用 const,其次是 let,对 var 说再见
在我看来,ECMAScript 2015的一个最好的特性是使用
const
和let
来声明变量。这是向前迈出的一大步,这些声明是块范围的(var 的声明是函数作用域的),并且存在于一个暂时性死区。当变量仅接收一次赋值时,我建议使用
const
声明,它创建了一个不可变的绑定。另外,使用const
声明变量时,你必须指定一个初始值。看一个验证一个单词是否为回文的函数:
在上述代码中,变量
length
和half
仅在声明的时候赋值了一次,因而使用const
进行声明,因为之后没有再次进行赋值。如果你需要对变量进行多次赋值,可以使用
let
进行声明。但无论何时都给变量定义一个初始值,如let index = 0
。那对于
var
声明呢?如果你是使用 ES2015 进行开发,我的建议是完全停止使用它。var
声明的变量是基于函数作用域的,并存在变量(作用域)提升。你可以在函数范围的末尾处声明一个 var 变量,但是它仍然可以在声明之前访问:你将得到一个 undefined 值。而使用
let
或者const
进行变量声明则不会存在作用域提升的问题,因为变量在声明之前处于一个暂时性死区,在声明之前访问变量会抛出一个 ReferenceError。Tip 2: 增强(代码的)内聚性
内聚性描述了模块的元素(名称空间、类、方法、代码块)属于同一类的程度。内聚性的测量通常被描述为高内聚或低内聚。
高内聚是更好的选择,因为它建议设计模块的元素,只关注单个任务。它使得模块:
高内聚和低耦合是设计良好的系统的特点。
代码块本身可以被认为是一个小模块。为了从高内聚的好处中获益,你需要尽可能地将变量保持在使用它们的代码块中。
例如我们要遍历一个数组,代码可能如下:
在函数的顶部声明了三个:
index
、item
和length
,前两个变量未初始化,则默认值是 undefined,第三个变量的初始值是数组长度。然而,这三个变量只在函数尾才进行了调用。那么这种方法的问题是什么呢?如果在
var
和for
语句之间还有很多代码,会有一些潜在的问题:一种更好的方法是将这些变量尽可能地移到它们的使用位置:
修改后的代码则能有效避免上述潜在的问题,并增强了代码的内聚性,使得代码块更容易重构和提取到分离的函数中。
2.2 访问对象不存在的属性
让我们在一个例子中演示一下:
favoriteMovie
是一个只有 title 属性的对象,当访问一个不存在的属性actors
时,结果返回了undefined
。访问一个自身不存在的属性,它不会抛出一个错误;但试图从一个不存在的属性值获取数据时,就会抛出 TypeError,得到的常见错误类型:
TypeError: Cannot read property of undefined.
让我们稍微修改一下前面的代码片段,以说明一个类型错误抛出:
favoriteMovie
没有actors
属性,所以favoriteMovie.actors
返回undefined
,因此,通过favoriteMovie.actors[0]
表达式访问undefined
的第一项就会抛出 TypeError。Tip 3: 检查属性的存在
幸运的是,JavaScript提供了一组方法来确定对象是否具有特定的属性:
obj.prop !== undefined
: 和undefined
直接比较typeof obj.prop !== 'undefined'
: 验证属性值类型obj.hasOwnProperty('prop')
: 验证对象是否有自己的属性'prop' in obj
: 验证对象是否有自己的或继承的属性我的推荐是使用
in
操作符,不仅语法简介,其也表明了一个明确的意图:即检查一个对象是否具有特定的属性,而不需要访问实际的属性值。obj.hasOwnProperty('prop')
也是一个不错的解决方案,它比in
运算符稍微长一点,但只在对象的自身属性中验证。Tip 4: 使用解构来访问对象属性
当访问对象属性时,如果属性不存在的话,有时必须指出一个默认值。你可以使用
in
和三元运算符来完成这个任务:随着对象属性数量的增加,这种方式就会变得不优雅了。因为需要有一种更优雅的方式来代替
in
和三元运算符,那就是使用 ES2015 的新特性:对象解构。对象解构 允许将对象属性值直接提取到变量中,并在属性不存在的情况下设置默认值。这是一种非常方便的语法,避免直接处理
undefined
的问题。现在使用对象结构来提取属性,使得代码看起来更短,也很有意义:
Tip 5: 使用默认属性来填充对象
使用对象解构时,需要声明一些属性变量;如果想省去变量声明,则可以使用
Object.assign(target, source1, source2, ...)
方法,该方法会将所有可枚举的属性值从一个或多个源对象复制到目标对象中,并返回目标对象。例如,你可能需要访问一个未包含所有属性集的
unsafeOptions
对象,为避免访问unsafeOptions
对象中不存在的属性返回的undefined
值,可以先定义一个包含所有属性集的默认对象defaults
:unsafeOptions
对象仅包含fontSize
属性,defaults
对象则定义了fontSize
和color
属性的默认值。这样,返回的options
对象则可以安全的访问color
属性了。如果不想使用
Object.assign
,则可以使用更简洁的对象扩展运行符:2.3 函数的参数
通常情况下,函数定义的参数个数和调用函数时传入的参数个数应该保持一致,以得到正确的返回结果:
但是,当调用函数省略了一些参数时,则省略的参数的默认值就是
undefined
了:函数
multiply
定义了两个参数 a 和 b,但是调用的时候只传入了一个参数(a=5),因为忽略的参数(b)的值就是undefined
。Tip 6: 使用默认参数值
有时调用函数时并不需要传递所有的参数列表,此时,给没有传参的参数设置一个默认值即可:
对于之前的示例简单修改了一下,如果
b
是undefined
,则赋予默认值为 2。如果你使用 ES2015,则可进一步简化如下:2.4 函数的返回值
在 JavaScript 中,一个没有
return
语句的函数会隐式返回undefined
:square()
函数没有返回任何计算结果,当其被调用时,就默认返回了undefined
。Tip 7: 不要信任分号自动插入
在 JavaScript 中,下列语句必须以分号结尾:
如果你使用了上述语句,确保在语句末尾添加分号:
如果没有添加分号,ECMAScript 提供了 ASI 机制( Automatic Semicolon Insertion),会这这些语句后面自动插入分号。
由于 ASI,你也可以删掉上述代码中的分号:
这看上去可以减小源码文件的大小,但同时也埋了隐患。看如下代码:
在
return
语句和数组字面量表达式之间存在一个换行,由于 ASI 机制,JavaScript 会在return
语句后插入一个分号,于是就等同于如下代码:此时,当调用函数
getPrimeNumbers
,得到的结果是undefined
而不是期望的数组。解决这个问题的方式也比较简单,移除换行就行:谨记,不要在 return 语句及其之后的表达式语句之间插入换行。
2.5 void 操作符
void
表达式会对表达式进行求值,但无论求值结果是什么,它均是返回undefined
:3. 数组中的 undefined
进行数组访问时,如果索引越界了,返回的结果也是
undefined
:数组
colors
仅三个元素,合法的索引值是 0,1,2,索引值 5 和 -1 则是越界的,得到的值会是undefined
。总结
本文讨论了会出现
undefined
值的情形,总结如下:欢迎纠正、评论和补充。