我刚刚开始学习JavaScript时,大约是八年前,当时我对于undefined 和 null 比较困惑 ,因为他们都表示空值。他们有什么明确的区别吗?他们似乎都可以定义一个空值,而且 当你进行 在做null ===undefined 的比较时,结果是true。
现在的大多数语言,像Ruby, Python or Java,他们有一个单独的空值(nil 或 null),这视乎才是一个合理的方式。
而在JavaScript里,当你要获取一个变量或对象(未初始化)的值时,js引擎会返回 undefined。
另一方面,对象引用错误会返回null。Javascript本身并不会给将变量或者对象属性的值设为 null。一些js原生的方法会返回null,比如string.prototypt.match() 参数不是对象时,会返回null,来表示对象缺失
由于JavaScript的宽容特性,开发人员有访问未初始化值的诱惑。 我也犯了这种不好的做法。
通常这种冒险行为会产生“未定义”的相关错误,从而快速结束脚本。 相关的常见错误消息是:
为了减少这种错误的风险,您必须了解产生“undefined”时的情况。 更重要的是抑制其外观并在应用程序中传播,从而提高代码的耐用性。
我们来详细探讨undefined
及其对代码安全的影响。
未定义的值原始值在变量未被赋值时使用。
该标准明确规定,在访问未初始化的变量,不存在的对象属性,不存在的数组元素等时,您将收到未定义的值。 例如:
ECMAScript规范定义了“未定义”值的类型:
未定义类型是唯一值为“未定义”值的类型。
2. 创建未定义的常见场景`
2.1 未初始化的变量
解决未初始化变量问题的一种有效方法是尽可能分配一个初始值_。 变量在未初始化状态下存在的越少越好。 理想情况下,您可以在声明`const myVariable ='初始值'后立即分配一个值,但这并非总是可行。
在我看来,ECMAScript 2015的最佳功能之一是使用const
和let
声明变量的新方法。 这是一个很大的进步,这些声明是块范围的(与旧函数作用域var
相反)并存在于[暂时死区]( 没有被吊起/#5letvariableslifecycle)直到宣告行。
当变量只接收一个值时,我建议使用const
声明。 它创建一个[不可变绑定](
const
的一个很好的特性是 - 你必须给初始值赋予变量const myVariable ='initial'
。 变量不会暴露于未初始化的状态,并且访问undefined
根本不可能。
让我们检查一下验证单词是否是回文的函数:
var
声明的问题是整个函数范围内的[变量提升]( 你可以在函数范围的末尾声明一个var
变量,但是它仍然可以在声明之前被访问:并且你会得到一个undefined
。
相反,在声明行之前不能访问let
(包括const
)变量。 发生这种情况是因为该变量在声明之前处于[暂时死区]( 这很好,因为你访问undefined
的机会较少。
上面的例子用let改写后,会出错。
内聚的测量通常被描述为高内聚或低内聚_。
高内聚是最好的,因为它建议设计模块的元素只专注于单个任务。 它使模块:
高内聚力伴随[松耦合](
一个代码块本身可能被认为是一个小模块。 为了从高内聚的好处中受益,您需要尽可能使变量尽可能靠近使用它们的代码块。
例如,如果一个变量完全存在以形成块范围的逻辑,则声明并允许该变量仅存在于该块内(使用const
或let
声明)。 不要将这个变量暴露给外部块作用域,因为外部块不应该关心这个变量。
不必要的扩展变量生命周期的一个典型例子是在函数内使用for
循环:
index
,item
和length
变量在函数体的开头声明。 然而,它们只用于接近尾声。 那么这种方法有什么问题?
在顶部的声明和for
语句中的用法之间,变量index
,item
都是未初始化的并且暴露给undefined
。 它们在整个功能范围内的生命周期不合理。
更好的方法是将这些变量尽可能靠近他们的使用地点:
为什么修改后的版本比最初版本更好? 让我们来看看:
-
变量不会暴露于未初始化的状态,因此您没有访问未定义的风险
-
尽可能将变量移动到它们的使用地点增加了代码的可读性
-
高度连贯的代码块在需要时更容易重构并提取为分离的函数
2.2 访问不存在的属性
让我们稍微修改前面的代码片段来说明一个“TypeError”抛出:
允许访问不存在的属性的JavaScript的宽容性质是混淆的来源:该属性可能被设置,或者可能不是。 绕过这个问题的理想方法是限制对象始终定义它所拥有的属性。
不幸的是,您经常无法控制您使用的对象。 这些对象在不同情况下可能具有不同的属性集。 所以你必须手动处理所有这些场景。
让我们实现一个函数append(array,toAppend),它在数组的开始和/或结尾添加新的元素。 toAppend
参数接受一个具有属性的对象:
下面的提示解释了如何正确检查属性是否存在。
Tip 3: 检查属性是否存在
幸运的是,JavaScript提供了很多方法来确定对象是否具有特定属性:
我的建议是使用in
运算符。它有一个简短而甜美的语法。 in
操作符存在意味着明确的目的是检查对象是否具有特定的属性,而不访问实际的属性值。
obj.hasOwnProperty('prop')
也是一个不错的解决方案。它比in
运算符略长,并且只在对象自己的属性中进行验证。
Tip 4: 用对象结构的方式访问对象的属性
访问对象属性时,如果该属性不存在,有时需要指示默认值。
你可以使用in
伴随着三元运算符来实现:
当要检查的属性数量增加时,三元运算符语法的使用会变得艰巨。 对于每个属性,你必须创建一个新的代码行来处理默认值,增加类似外观的三元运算符的丑陋墙。
为了使用更优雅的方法,让我们熟悉称为object destructuring的一个伟大的ES2015功能。 [对象解构]( 该属性不存在。 避免直接处理undefined的简便语法。
事实上,现在的属性解析看起来简短且明了:
为了看到实际情况,让我们定义一个有用的函数,将字符串包装在引号中。 quote(subject,config)
接受第一个参数作为要包装的字符串。 第二个参数config
是一个具有以下属性的对象:
char
:引号字符,例如 (单引号)或
(双引号),默认为`
。 skipIfQuoted
:如果字符串已被引用,则跳过引用的布尔值。 默认为true
。
应用对象解构的好处,让我们实现反引号的使用:
幸运的是,该功能还有改进的空间。 让我们将解构赋值移到参数部分。 并为`config`参数设置一个默认值(一个空对象`{}`),以在默认设置足够时跳过第二个参数。
请注意,解构赋值将替换函数签名中的“config”参数。 我喜欢这样:quote()
变成一行更短。 在解构赋值右侧的= {}
确保在第二个参数没有在quote('Sunny day')`中被指定时使用空对象。
对象解构是一个强大的功能,可以有效地处理从对象中提取属性。 我喜欢在访问的属性不存在时指定要返回的默认值的可能性。 因此,避免了“未定义”以及与处理它有关的问题。
如果不需要像解构分配那样为每个属性创建变量,则缺少某些属性的对象可以用缺省值填充。
例如,您需要访问unsafeOptions
对象的属性,该属性并不总是包含其全部属性。
为了在unsafeOptions
中访问一个不存在的属性时避免undefined
,让我们做一些调整:
并且来自defaults
源对象的color
属性的值,因为unsafeOptions
不包含color
。 枚举源对象的顺序很重要:稍后的源对象属性将覆盖先前的对象属性。
幸运的是,使用默认属性填充对象的方式更简单轻松。 我建议使用一个新的JavaScript特性(现在在[stage 3]( TC39/提议对象,其余的扩展)。
代替Object.assign()调用,使用对象扩展语法将来自源对象的所有属性和可枚举属性复制到目标对象中:
对象初始值设定项从defaults
和unsafeOptions
源对象传播属性。 指定源对象的顺序很重要:稍后的源对象属性会覆盖先前的对象属性。
使用默认属性值填充不完整的对象是使代码安全和稳定的有效策略。 不管情况如何,对象总是包含全部属性:'undefined'不能生成。
函数参数默认默认为undefined
。
通常,应使用相同数量的参数调用使用特定数量的参数定义的函数。 在这种情况下,这些参数将获得您期望的值:
当您在调用中省略参数时会发生什么? 函数内部的参数变成undefined
。
让我们稍微修改前面的例子,只用一个参数调用函数:
有时函数不需要调用的全套参数。 可以简单地为没有值的参数设置默认值。
尽管提供了分配默认值的方式,但我不建议直接比较'undefined'。 它很冗长,看起来像一个黑客。
更好的方法是使用ES2015 [默认参数]( 它很短,很有表现力,并且与'undefined`没有直接的对比。
例子修改,添加默认值:
ES2015的默认参数功能非常直观和高效。 始终使用它来为可选参数设置默认值。
square()
函数不返回任何计算结果。 函数调用结果是'未定义的'。
当return
语句存在时会发生同样的情况,但是附近没有表达式:
return;
语句被执行,但它不返回任何表达式。 调用结果也是undefined
。
当然,在'return'附近表示要返回的表达式按预期工作:
Tip 7: 不要相信自动分号插入
以下JavaScript语句列表必须以分号(;
)结尾:
如果你使用上述语句之一,请务必在末尾指明分号:
在let
声明和return
声明结尾处写了一个强制性分号。
当你不想添加这些分号时会发生什么? 例如减少源文件的大小。
在ASI的帮助下,你可以从前面的示例中删除分号:
以上文字是有效的JavaScript代码。 缺少的分号会自动插入。
乍一看,它看起来很有希望。 ASI机制让你跳过不必要的分号。 您可以使JavaScript代码更小,更易于阅读。
ASI有一个小而烦人的陷阱。 当一个换行符位于return
和返回的表达式'return \ n expression之间时,ASI自动在换行符之前插入一个分号
; \ n表达式。
在函数内部意味着什么return;
语句? 该函数返回undefined
。 如果您不详细了解ASI的机制,那么意外返回的“未定义”是误导性的。
例如,让我们研究getPrimeNumbers()
调用的返回值:
在return
语句和数组文字表达式之间存在一个新行。 JavaScript在return
后自动插入一个分号,解释代码如下:
通过删除return
和数组literal之间的换行符可以解决问题:
我的建议是研究[确切地说]( 自动分号插入的作用,以避免这种情况。
void运算,计算一个表达式,不返回计算结果,所以返回值为undefined
关于评估的一些副作用。
在JavaScript中你可能遇到所谓的稀疏数组。 这些是有间隙的数组,即在某些索引中没有定义元素。
当在一个稀疏数组中访问一个间隙(又名空槽)时,你也会得到一个'undefined`。
以下示例将生成稀疏数组并尝试访问其空插槽:
sparse1
是通过调用构造函数“Array”构造函数来创建的。 它有3个空插槽。 sparse2
是用字面量的形式来创建了一个第二个元素为空的数组。 在任何这些稀疏数组中,访问一个空插槽的结果都是“undefined”。
在处理数组时,为了避免捕获undefined
,一定要使用有效的数组索引,并避免创建稀疏数组。
这里有个合理的问题: undefined
and null
他们之间的主要区别是什么?都是一个指定值用来表示一个空状态。
主要的区别是:undefined
是用来表示一个变量的值没有被定义。 null
这是代表一个对象不存在。
我们来看一下这些区别:
当变量number
被定义,但是没有给它赋值进行初始化:
因此变量number
的值为 undefined
, .这明确表明了则是一个没有初始化的变量
同样的,当你获取一个对象存在的属性时,也会发生这样的情况:该属性未初始化。
还有另一种情况,当一个变量期待是一个对象或者是一个方法返回一个对象时,但是由于某些原因,你不能实例化一个对象。。那么这样的情况下,null就会是一个有意义的指示器,来表示对象缺失。
例如:clone()` 是一个用来复制JavaScript对象的 函数,这个函数期望能够返回的是一个对象。
然后,可能会传入一个不是对象的参数:15,null。这种情况下,该函数就不能进行对象复制,所以会返回 null
-- 来表示对象缺失
typeof
运算 能够看出两个值之间的区别
undefined的存在是JavaScript随意性所造成的,他允许一下任意情况的使用:
大多数情况下,直接与'undefined'进行比较是一种不好的做法,因为你可能依赖于上面提到的允许但不鼓励的做法。
一个有效的策略是减少代码中未定义关键字的出现。 在此期间,请总是以令人惊讶的方式记住它的潜在外观,并通过应用下列有益习惯来防止这种情况发生:
-
减少未初始化变量的使用
-
使变量生命周期变短并接近其使用的来源
-
尽可能为变量分配一个初始值
-
使用默认值作为无意义的函数参数
-
验证属性的存在或用缺省属性填充不安全的对象