7个处理JavaScript值为undefined的技巧
前言
我刚刚开始学习JavaScript时,大约是八年前,当时我对于undefined 和 null 比较困惑 ,因为他们都表示空值。
他们有什么明确的区别吗?他们似乎都可以定义一个空值,而且 当你进行 在做null ===undefined 的比较时,结果是true。
现在的大多数语言,像Ruby, Python or Java,他们有一个单独的空值(nil 或 null),这似乎才是一个合理的方式。
而在JavaScript里,当你要获取一个变量或对象(未初始化)的值时,js引擎会返回 undefined。
let company; company; // => undefined let person = { name: 'John Smith' }; person.age; // => undefined
另一方面,对象引用错误会返回null。JavaScript本身并不会给将变量或者对象属性的值设为 null。
一些js原生的方法会返回null,比如string.prototypt.match() 参数不是对象时,会返回null,来表示对象缺失。
let array = null; array; // => null let movie = { name: 'Starship Troopers', musicBy: null }; movie.musicBy; // => null 'abc'.match(/[0-9]/); // => null
由于JavaScript的宽容特性,开发人员有访问未初始化值的诱惑。我也犯了这种不好的做法。
通常这种冒险行为会产生“未定义”的相关错误,从而快速结束脚本。相关的常见错误消息是:
- TypeError:'undefined'不是函数
- TypeError:无法读取未定义的属性' ''
- 和类似type errors。
JavaScript开发能够理解这个笑话:
function undefined() { // problem solved }
为了减少这种错误的风险,您必须了解产生“undefined”时的情况。
更重要的是抑制其外观并在应用程序中传播,从而提高代码的耐用性。
我们来详细探讨undefined及其对代码安全的影响。
1、 什么是undefined
JavaScript 的 6 基本类型:
- Boolean: true or false
- Number: 1, 6.7, 0xFF
- String: "Gorilla and banana"
- Symbol: Symbol("name") (starting ES2015)
- Null: null
- Undefined: undefined.
And a separated object type: {name: "Dmitri"}, ["apple", "orange"].
从6个基本类型undefined是一个特殊的值,它的类型为Undefined。根据[ECMAScript规范](https://www.ecma-international.org/ecma-262/7.0/#sec-undefined-value):
未定义的值原始值在变量未被赋值时使用。
该标准明确规定,在访问未初始化的变量,不存在的对象属性,不存在的数组元素等时,您将收到未定义的值。
例如:Try in repl.it
let number; number; // => undefined let movie = { name: 'Interstellar' }; movie.year; // => undefined let movies = ['Interstellar', 'Alexander']; movies[3]; // => undefined
ECMAScript规范定义了“未定义”值的类型:
未定义类型是唯一值为“未定义”值的类型。
typeof undefined === 'undefined'; // => true
let nothing; typeof nothing === 'undefined'; // => true
2、 创建未定义的常见场景
2.1 未初始化的变量
一个尚未赋值的声明变量( uninitialized )默认为undefined。
Plain and simple:
let myvariable; myvariable; // => undefined
解决未初始化变量问题的一种有效方法是尽可能分配一个初始值_。
变量在未初始化状态下存在的越少越好。理想情况下,您可以在声明值const myvariable ='初始值'后立即分配一个值,但这并非总是可行。
Tip 1: 赞成const,否则使用let,但是告别var
在我看来,ECMAScript 2015的优秀功能之一是使用const和let声明变量的新方法。这是一个很大的进步,这些声明是块范围的(与旧函数作用域var相反)并存在于[暂时死区](https://rainsoft.io/variables-lifecycle-and-why-let- 没有被吊起/#5letvariableslifecycle)直到宣告行。
当变量只接收一个值时,我建议使用const声明。
它创建一个[不可变绑定](https://mathiasbynens.be/notes/es6-const)。
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
var声明的问题是整个函数范围内的[变量提升](https://rainsoft.io/javascript-hoisting-in-details/#hoistingandvar)。
你可以在函数范围的末尾声明一个var变量,但是它仍然可以在声明之前被访问:并且你会得到一个undefined。
function bigFunction() { // code... myvariable; // => undefined // code... var myVariable = 'Initial value'; // code... myVariable; // => 'Initial value' } bigFunction();
相反,在声明行之前不能访问let(包括const)变量。发生这种情况是因为该变量在声明之前处于[暂时死区](https://rainsoft.io/variables-lifecycle-and-why-let-is-not-hoisted/#5letvariableslifecycle)中。
这很好,因为你访问undefined的机会较少。
上面的例子用let改写后,会出错。
function bigFunction() { // code... myVariable; // => Throws 'ReferenceError: myVariable is not defined' // code... let myVariable = 'Initial value'; // code... myVariable; // => 'Initial value' } bigFunction();
Tip 2: 增强内聚性
[Cohesion](https://en.wikipedia.org/wiki/Cohesion_(computer_science))描述了模块(命名空间,类,方法,代码块)的元素所属的程度。 内聚的测量通常被描述为高内聚或低内聚_。
高内聚是最好的,因为它建议设计模块的元素只专注于单个任务。它使模块:
- Focused and understandable: easier to understand what the module does
- 功能单一且容易理解
- Maintainable and easier to refactor: the change in the module affects fewer modules
- 易于维护和复用
- Reusable: being focusing on a single task, it makes the module easier to reuse
- 重复利用
- Testable: you would easier test a module that's focused on a single task
- 易于测试
高内聚力伴随[松耦合](https://en.wikipedia.org/wiki/Loose_coupling)是设计良好的系统的特点。
一个代码块本身可能被认为是一个小模块。为了从高内聚的好处中受益,您需要尽可能使变量尽可能靠近使用它们的代码块。
例如,如果一个变量完全存在于形成块范围的逻辑,则声明并允许该变量仅存在于该块内(使用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语句中的用法之间,变量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'; }
为什么修改后的版本比最初版本更好?让我们来看看:
- 变量不会暴露于未初始化的状态,因此您没有访问未定义的风险
- 尽可能将变量移动到它们的使用地点增加了代码的可读性
- 高度连贯的代码块在需要时更容易重构并提取为分离的函数
2.2 访问不存在的属性
When accessing a **non-existing object property**, JavaScript returnsundefined`. 当访问不再的属性时,会返回undefined
看例子:
let favoriteMovie = { title: 'Blade Runner' }; favoriteMovie.actors; // => undefined
本身访问不存在的属性不会引发错误。尝试从不存在的属性值获取数据时出现真正的问题。这是最常见的undefined相关陷阱,反映在众所周知的错误消息'TypeError:Can not read propertyof undefined`中。
让我们稍微修改前面的代码片段来说明一个“TypeError”抛出:
let favoriteMovie = { title: 'Blade Runner' }; favoriteMovie.actors[0]; // TypeError: Cannot read property '0' of undefined
允许访问不存在的属性的JavaScript的宽容性质是混淆的来源:该属性可能被设置,或者可能不是。绕过这个问题的理想方法是限制对象始终定义它所拥有的属性。
不幸的是,您经常无法控制您使用的对象。这些对象在不同情况下可能具有不同的属性集。所以你必须手动处理所有这些场景。
让我们实现一个函数append(array,toAppend),它在数组的开始和/或结尾添加新的元素。toAppend参数接受一个具有属性的对象:
- first: element inserted at the beginning of array
- last: element inserted at the end of array.
function append(array, toAppend) { const arrayCopy = array.slice(); if (toAppend.first) { arrayCopy.unshift(toAppend.first); } if (toAppend.last) { arrayCopy.push(toAppend.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]
append([10], { first: 0, last: false }); // => [10]
下面的提示解释了如何正确检查属性是否存在。
Tip 3: 检查属性是否存在
幸运的是,JavaScript提供了很多方法来确定对象是否具有特定属性:
*obj.prop!== undefined:直接与undefined进行比较
- typeof obj.prop!=='undefined':验证属性值的类型 *obj.hasOwnProperty('prop'):验证对象是否拥有自己的属性
- obj`中的'prop':验证对象是否有自己的或继承的属性
我的建议是使用in运算符。它有一个简短而甜美的语法。in操作符存在意味着明确的目的是检查对象是否具有特定的属性,而不访问实际的属性值。
![不要写var,写const并在JavaScript中放置](https://p0.ssl.qhimg.com/t010effea86a232d8a4.png)
obj.hasOwnProperty('prop')也是一个不错的解决方案。它比in运算符略长,并且只在对象自己的属性中进行验证。
涉及与'undefined'比较的两种方式可能会起作用......但在我看来obj.prop!== undefined和typeof obj.prop!=='undefined`看起来冗长而怪异,并且暴露直接处理undefined的怀疑路径。
让我们使用in运算符来改进append(array,toAppend)函数:
function append(array, toAppend) { const arrayCopy = array.slice(); if ('first' in toAppend) { arrayCopy.unshift(toAppend.first); } if ('last' in toAppend) { arrayCopy.push(toAppend.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]
Tip 4: 用对象结构的方式访问对象的属性
访问对象属性时,如果该属性不存在,有时需要指示默认值。
你可以使用in伴随着三元运算符来实现:
const object = { }; const prop = 'prop' in object ? object.prop : 'default'; prop; // => 'default'
当要检查的属性数量增加时,三元运算符语法的使用会变得艰巨。对于每个属性,你必须创建一个新的代码行来处理默认值,增加类似外观的三元运算符的丑陋墙。
为了使用更优雅的方法,让我们熟悉称为object destructuring的一个伟大的ES2015功能。[对象解构](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring)允许直接将对象属性值直接提取到变量中,并设置默认值if 该属性不存在。
避免直接处理undefined的简便语法。
事实上,现在的属性解析看起来简短且明了:
const object = { }; const { prop = 'default' } = object; prop; // => 'default'
为了看到实际情况,让我们定义一个有用的函数,将字符串包装在引号中。quote(subject,config)接受第一个参数作为要包装的字符串。第二个参数config是一个具有以下属性的对象:
char:引号字符,例如 (单引号)或(双引号),默认为`。 skipIfQuoted:如果字符串已被引用,则跳过引用的布尔值。默认为true。
应用对象解构的好处,让我们实现反引号的使用:
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','false'为'skipIfQuoted`。 幸运的是,该功能还有改进的空间。 让我们将解构赋值移到参数部分。并为`config`参数设置一个默认值(一个空对象`{}`),以在默认设置足够时跳过第二个参数。 [Try in repl.it](https://repl.it/HK1b/0) ```javascript 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')`中被指定时使用空对象。
对象解构是一个强大的功能,可以有效地处理从对象中提取属性。我喜欢在访问的属性不存在时指定要返回的默认值的可能性。因此,避免了“未定义”以及与处理它有关的问题。
Tip 5: 用默认属性填充对象
如果不需要像解构分配那样为每个属性创建变量,则缺少某些属性的对象可以用缺省值填充。
ES2015Object.assign(target,source1,source2,...)将所有可枚举属性的值从一个或多个源对象复制到目标对象中。该函数返回目标对象。
例如,您需要访问unsafeOptions对象的属性,该属性并不总是包含其全部属性。
为了在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'
Object.assign()将第一个参数作为目标对象{}。目标对象从unsafeOptions源对象接收fontSize属性的值。并且来自defaults源对象的color属性的值,因为unsafeOptions不包含color。枚举源对象的顺序很重要:稍后的源对象属性将覆盖先前的对象属性。
您现在可以安全地访问options对象的任何属性,包括最初在unsafeOptions中不可用的options.color。
幸运的是,使用默认属性填充对象的方式更简单轻松。我建议使用一个新的JavaScript特性(现在在[stage 3](https://tc39.github.io/process-document/)),它允许[在对象初始化器中传播属性](https://github.com/ TC39/提议对象,其余的扩展)。
代替Object.assign()调用,使用对象扩展语法将来自源对象的所有属性和可枚举属性复制到目标对象中:
const unsafeOptions = { fontSize: 18 }; const defaults = { fontSize: 16, color: 'black' }; const options = { ...defaults, ...unsafeOptions }; options.fontSize; // => 18 options.color; // => 'black'
对象初始值设定项从defaults和unsafeOptions源对象传播属性。指定源对象的顺序很重要:稍后的源对象属性会覆盖先前的对象属性。
使用默认属性值填充不完整的对象是使代码安全和稳定的有效策略。不管情况如何,对象总是包含全部属性:'undefined'不能生成。
2.3 函数的参数
函数参数默认默认为undefined。
通常,应使用相同数量的参数调用使用特定数量的参数定义的函数。在这种情况下,这些参数将获得您期望的值:
function multiply(a, b) { a; // => 5 b; // => 3 return a * b; } multiply(5, 3); // => 15
当您在调用中省略参数时会发生什么?函数内部的参数变成undefined。
让我们稍微修改前面的例子,只用一个参数调用函数:
function multiply(a, b) { a; // => 5 b; // => undefined return a * b; } multiply(5); // => NaN
Tip 6: 给参数默认值
有时函数不需要调用的全套参数。可以简单地为没有值的参数设置默认值。
看例子:
function multiply(a, b) { if (b === undefined) { b = 2; } a; // => 5 b; // => 2 return a * b; } multiply(5); // => 10
The function is invoked with a single argument multiply(5). Initially a parameter is 2 and b is undefined. The conditional statement verifies whether b is undefined. If it happens, b = 2 assignment sets a default value.
尽管提供了分配默认值的方式,但我不建议直接比较'undefined'。它很冗长,看起来像一个黑客。
更好的方法是使用ES2015 [默认参数](https://www.sitepoint.com/es6-default-parameters/)功能。 它很短,很有表现力,并且与'undefined`没有直接的对比。
例子修改,添加默认值:
function multiply(a, b = 2) { a; // => 5 b; // => 2 return a * b; } multiply(5); // => 10 multiply(5, undefined); // => 10
ES2015的默认参数功能非常直观和高效。始终使用它来为可选参数设置默认值。
2.4 函数返回值
隐式地,没有return语句,JavaScript函数返回undefined。
在JavaScript中,没有任何return语句的函数隐式地返回undefined:
function square(x) { const res = x * x; } square(2); // => undefined
square()函数不返回任何计算结果。函数调用结果是'未定义的'。
当return语句存在时会发生同样的情况,但是附近没有表达式:
function square(x) { const res = x * x; return; } square(2); // => undefined
return;语句被执行,但它不返回任何表达式。调用结果也是undefined。
当然,在'return'附近表示要返回的表达式按预期工作:
function square(x) { const res = x * x; return res; } square(2); // => 4
Tip 7: 不要相信自动分号插入
以下JavaScript语句列表必须以分号(;)结尾:
- 空的陈述
- let,const,var,import,export声明
- 表达式语句
- 调试器语句
- 继续语句,break语句
- 抛出声明
- return语句
如果你使用上述语句之一,请务必在末尾指明分号:
function getNum() { // Notice the semicolons at the end let num = 1; return num; } getNum(); // => 1
在let声明和return声明结尾处写了一个强制性分号。
当你不想添加这些分号时会发生什么?例如减少源文件的大小。
在这种情况下,ECMAScript提供了[Automatic Semicolon Insertion](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-automatic-semicolon-insertion)(ASI)机制,该机制可以插入 你丢失的分号。
在ASI的帮助下,你可以从前面的示例中删除分号:
function getNum() { // Notice that semicolons are missing let num = 1 return num } getNum() // => 1
以上文字是有效的JavaScript代码。缺少的分号会自动插入。
乍一看,它看起来很有希望。ASI机制让你跳过不必要的分号。您可以使JavaScript代码更小,更易于阅读。
ASI有一个小而烦人的陷阱。当一个换行符位于return和返回的表达式'return \ n expression之间时,ASI自动在换行符之前插入一个分号; \ n表达式。
在函数内部意味着什么return;语句?该函数返回undefined。如果您不详细了解ASI的机制,那么意外返回的“未定义”是误导性的。
例如,让我们研究getPrimeNumbers()调用的返回值:
function getPrimeNumbers() { return [ 2, 3, 5, 7, 11, 13, 17 ] } getPrimeNumbers() // => undefined
在return语句和数组文字表达式之间存在一个新行。 JavaScript在return后自动插入一个分号,解释代码如下:
function getPrimeNumbers() { return; [ 2, 3, 5, 7, 11, 13, 17 ]; } getPrimeNumbers(); // => undefined
语句return;使getPrimeNumbers()函数返回undefined而不是期望的数组。
通过删除return和数组literal之间的换行符可以解决问题:
function getPrimeNumbers() { return [ 2, 3, 5, 7, 11, 13, 17 ]; } getPrimeNumbers(); // => [2, 3, 5, 7, 11, 13, 17]
我的建议是研究[确切地说](http://www.bradoncode.com/blog/2015/08/26/javascript-semi-colon-insertion/) 自动分号插入的作用,以避免这种情况。
Of course, never put a newline between return and the returned expression.
2.5 void 运算
void运算,计算一个表达式,不返回计算结果,所以返回值为undefined
void 1; // => undefined void (false); // => undefined void {name: 'John Smith'}; // => undefined void Math.min(1, 3); // => undefined
[void use]运算符的[一个用例](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void#JavaScript_URIs)是将表达式评估抑制为undefined,依赖 关于评估的一些副作用。
3、数组中的undefined
You get when accessing an array element with an out of bounds index. 当你试图想要获取一个超出数组界限范围的下标时,会返回 undefined
const colors = ['blue', 'white', 'red']; colors[5]; // => undefined colors[-1]; // => undefined
数组colors 有3个元素array has 3 elements, 对应的下标分别是 0, 1 and 2。 因为在该数组中不存在下标5和-1,所以当你t访问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
sparse1 是通过调用构造函数“Array”构造函数来创建的。它有3个空插槽。sparse2是用字面量的形式来创建了一个第二个元素为空的数组。在任何这些稀疏数组中,访问一个空插槽的结果都是“undefined”。
在处理数组时,为了避免捕获undefined,一定要使用有效的数组索引,并避免创建稀疏数组。
4、undefined and null 之间的不同
这里有个合理的问题:undefined and null他们之间的主要区别是什么?都是一个指定值用来表示一个空状态。
主要的区别是:undefined是用来表示一个变量的值没有被定义。null 这是代表一个对象不存在。
我们来看一下这些区别:
当变量number 被定义,但是没有给它赋值进行初始化:
let number; number; // => undefined
因此变量number的值为 undefined, .这明确表明了则是一个没有初始化的变量
同样的,当你获取一个对象存在的属性时,也会发生这样的情况:该属性未初始化。
const obj = { firstName: 'Dmitri' }; obj.lastName; // => undefined
上面例子,因为 obj没有lastName属性,所以JavaScript会把 obj.lastName 解析为 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
然后,可能会传入一个不是对象的参数:15,null。这种情况下,该函数就不能进行对象复制,所以会返回 null -- 来表示对象缺失
typeof 运算 能够看出两个值之间的区别
typeof undefined; // => 'undefined' typeof null; // => 'object'
The 全等运算符 === 对于undefined 和null,也显示不相等。
let nothing = undefined; let missingObject = null; nothing === missingObject; // => false
5、总结
undefined的存在是JavaScript随意性所造成的,它允许一下任意情况的使用:
- uninitialized variables
- 未初始化的对象
- non-existing object properties or methods
- 对象没有的方法或属性
- out of bounds indexes to access array elements
- 数组的超出长度下标的元素
- the invocation result of a function that returns nothing
- 当方法调用返回空时