JavaScript中7個處理undefined的小技巧

青灯夜游
發布: 2020-10-14 15:45:02
轉載
4067 人瀏覽過

JavaScript中7個處理undefined的小技巧

當原作者開始學習JS時,遇到了一個奇怪的情況,既存在undefined的值,也存在表示空值的null。它們之間的明顯差異是什麼?它們似乎都定義了空值,而且,比較null == undefined的計算結果為true

大多數現代語言,如Ruby、Python或Java都有一個空值(nilnull),這似乎是合理的方式。

對於JavaScript,解釋器在存取尚未初始化的變數或物件屬性時傳回undefined。例如:

let company; company; // => undefined let person = { name: 'John Smith' }; person.age; // => undefined
登入後複製

另一方面,null表示缺少的物件引用,JS本身不會將變數或物件屬性設為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
登入後複製

由於 JS 的寬容特性,開發人員很容易存取未初始化的值,我也犯了這樣的錯誤。

通常,這種危險的操作會產生undefined的相關錯誤,從而快速地結束腳本。相關的常見錯誤訊息有:

  • TypeError: 'undefined' is not a function

  • TypeError: Cannot read property '' of undefined

  • type errors

JS 開發者可以理解這個笑話的諷刺:

function undefined() { // problem solved }
登入後複製

為了降低這類錯誤的風險,必須理解產生undefined的情況。更重要的是抑制它的出現並阻止在應用程式中傳播,從而提高程式碼的持久性。

讓咱們詳細討論undefined# 及其對程式碼安全性的影響。

1、undefined 是什麼鬼

JS 有6種基本型別

  • Boolean:truefalse

  • Number:1,6.7,0xFF

  • String:"Gorilla and banana"

  • Symbol:Symbol("name")(starting ES2015)

  • Null:null

  • #Undefined:undefined

和一個單獨的Object類型:{name: "Dmitri"}["apple", "orange" ]

根據ECMAScript規範,從6種原始類型中,undefined是一個特殊的值,它有自己的Undefined類型。

未為變數賦值時預設值為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是其唯一值為undefined 值的型別。

在這個意義上,typeof undefined傳回「undefined」字串

typeof undefined === "undefined"; // => true
登入後複製

當然typeof可以很好地驗證變數是否包含undefined的值

let nothing; typeof nothing === "undefined"; // => true
登入後複製

2、導致undefined的常見場景

2.1 未初始化變數

尚未賦值(未初始化)的宣告變數預設為undefined

let myVariable; myVariable; // => undefined
登入後複製

myVariable已聲明,但尚未賦值,預設值為undefined

解決未初始化變數問題的有效方法是盡可能分配初始值。變數在未初始化狀態中越少越好。理想情況下,你可以在宣告const myVariable ='Initial value'之後立即指定一個值,但這並不總是可行的。

技巧1:使用 let 和 const 來取代 var

#

在我看来,ES6 最好的特性之一是使用const和let声明变量的新方法。const和let具有块作用域(与旧的函数作用域var相反),在声明行之前都存在于暂时性死区

当变量一次性且永久地接收到一个值时,建议使用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
登入後複製

length 和 half 变量被赋值一次。将它们声明为const似乎是合理的,因为这些变量不会改变。

如果需要重新绑定变量(即多次赋值),请应用let声明。只要可能,立即为它赋一个初值,例如,let index = 0。

那么使用 var 声明呢,相对于ES6,建议是完全停止使用它。

JavaScript中7個處理undefined的小技巧

var 声明的变量提会被提升到整个函数作用域顶部。可以在函数作用域末尾的某个地方声明var变量,但是仍然可以在声明之前访问它:对应变量的值是 undefined。

相反,用let 或者 const 声明的变量之前不能访问该变量。之所以会发生这种情况,是因为变量在声明之前处于暂时死区。这很好,因为这样就很少有机会访问到 undefined 值。

使用let(而不是var)更新的上述示例会引发ReferenceError 错误,因为无法访问暂时死区中的变量。

function bigFunction() { // code... myVariable; // => Throws 'ReferenceError: myVariable is not defined' // code... let myVariable = 'Initial value'; // code... myVariable; // => 'Initial value' } bigFunction();
登入後複製

技巧2:增加内聚性

内聚描述模块的元素(命名空间、类、方法、代码块)内聚在一起的程度。凝聚力的测量通常被称为高凝聚力或低内聚。

高内聚是优选的,因为它建议设计模块的元素以仅关注单个任务,它构成了一个模块。

  • 专注且易懂:更容易理解模块的功能

  • 可维护且更容易重构:模块中的更改会影响更少的模块

  • 可重用:专注于单个任务,使模块更易于重用

  • 可测试:可以更轻松地测试专注于单个任务的模块

JavaScript中7個處理undefined的小技巧

高内聚和低耦合是一个设计良好的系统的特征。

代码块本身可能被视为一个小模块,为了尽可能实现高内聚,需要使变量尽可能接近使用它们代码块位置。

例如,如果一个变量仅存在以形成块作用域内,不要将此变量公开给外部块作用域,因为外部块不应该关心此变量。

不必要地延长变量生命周期的一个典型例子是函数中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'; }
登入後複製

indexitemlength变量在函数体的开头声明,但是,它们仅在最后使用,那么这种方式有什么问题呢?

从顶部的声明到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'; }
登入後複製

indexitem变量仅存在于for语句的作用域内,for之外没有任何意义。length变量也被声明为接近其使用它的位置。

为什么修改后的版本优于初始版本? 主要有几点:

  • 变量未暴露undefined状态,因此没有访问undefined的风险

  • 将变量尽可能地移动到它们的使用位置会增加代码的可读性

  • 高内聚的代码块在必要时更容易重构并提取到单独的函数中

2.2 访问不存在的属性

访问不存在的对象属性时,JS 返回undefined

咱们用一个例子来说明这一点:

let favoriteMovie = { title: 'Blade Runner' }; favoriteMovie.actors; // => undefined
登入後複製

favoriteMovie是一个具有单个属性title的对象。 使用属性访问器favoriteMovie.actors访问不存在的属性actors将被计算为undefined

本身访问不存在的属性不会引发错误, 但尝试从不存在的属性值中获取数据时就会出现问题。 常见的的错误是TypeError: Cannot read property of undefined

稍微修改前面的代码片段来说明TypeError throw

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

favoriteMovie没有属性actors,所以favoriteMovie.actors的值undefined。因此,使用表达式favoriteMovie.actors[0]访问undefined值的第一项会引发TypeError

JS 允许访问不存在的属性,这种允许访问的特性容易引起混淆:可能设置了属性,也可能没有设置属性,绕过这个问题的理想方法是限制对象始终定义它所持有的属性。

不幸的是,咱们常常无法控制对象。在不同的场景中,这些对象可能具有不同的属性集,因此,必须手动处理所有这些场景:

接着我们实现一个函数append(array, toAppend),它的主要功能在数组的开头和/或末尾添加新的元素。 toAppend参数接受具有属性的对象:

  • first:元素插入数组的开头

  • last:元素在数组末尾插入。

函数返回一个新的数组实例,而不改变原始数组(即它是一个纯函数)。

append()的第一个版本看起来比较简单,如下所示:

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]
登入後複製

由于toAppend对象可以省略first或last属性,因此必须验证toAppend中是否存在这些属性。如果属性不存在,则属性访问器值为undefined

检查firstlast属性是否是undefined,在条件为if(toappendix .first){}if(toappendix .last){}中进行验证:

这种方法有一个缺点,undefinedfalsenull0NaN''是虚值。

append()的当前实现中,该函数不允许插入虚值元素:

append([10], { first: 0, last: false }); // => [10]
登入後複製

0false是虚值的。 因为if(toAppend.first){}if(toAppend.last){}实际上与falsy进行比较,所以这些元素不会插入到数组中,该函数返回初始数组[10]而不会进行任何修改。

以下技巧解释了如何正确检查属性的存在。

技巧3: 检查属性是否存在

JS 提供了许多方法来确定对象是否具有特定属性:

  • obj.prop!== undefined:直接与undefined进行比较

  • typeof obj.prop!=='undefined':验证属性值类型

  • obj.hasOwnProperty('prop'):验证对象是否具有自己的属性

  • 'prop' in obj:验证对象是否具有自己的属性或继承属性

我的建议是使用in操作符,它的语法短小精悍。in操作符的存在表明一个明确的意图,即检查对象是否具有特定的属性,而不访问实际的属性值。

JavaScript中7個處理undefined的小技巧

obj.hasOwnProperty('prop')也是一个很好的解决方案,它比in操作符稍长,仅在对象自己的属性中进行验证。

涉及与undefined进行比较剩下的两种方式可能有效,但在我看来,obj.prop!== undefinedtypeof 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]
登入後複製

'first' in toAppend(和'last' in toAppend)在对应属性存在时为true,否则为falsein操作符的使用解决了插入虚值元素0false的问题。现在,在[10]的开头和结尾添加这些元素将产生预期的结果[0,10,false]

技巧4:解构访问对象属性

在访问对象属性时,如果属性不存在,有时需要指示默认值。可以使用in和三元运算符来实现这一点。

const object = { }; const prop = 'prop' in object ? object.prop : 'default'; prop; // => 'default'
登入後複製

当要检查的属性数量增加时,三元运算符语法的使用变得令人生畏。对于每个属性,都必须创建新的代码行来处理默认值,这就增加了一堵难看的墙,里面都是外观相似的三元运算符。

为了使用更优雅的方法,可以使用 ES6 对象的解构。

对象解构允许将对象属性值直接提取到变量中,并在属性不存在时设置默认值,避免直接处理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 = '", skipifquote = true} = config解构赋值在一行中从config对象中提取charskipifquote属性。如果config对象中有一些属性不可用,那么解构赋值将设置默认值:char'"'skipifquotefalse

该功能仍有改进的空间。让我们将解构赋值直接移动到参数部分。并为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()缩短了一行。

={}在解构赋值的右侧,确保在完全没有指定第二个参数的情况下使用空对象。

对象解构是一个强大的功能,可以有效地处理从对象中提取属性。 我喜欢在被访问属性不存在时指定要返回的默认值的可能性。因为这样可以避免undefined以及与处理它相关的问题。

技巧5: 用默认属性填充对象

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

ES6Object.assign(target,source1,source2,...)将所有可枚举的自有属性的值从一个或多个源对象复制到目标对象中,该函数返回目标对象。

例如,需要访问unsafeOptions对象的属性,该对象并不总是包含其完整的属性集。

为了避免从unsafeOptions访问不存在的属性,让我们做一些调整:

定义包含默认属性值的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对象定义属性fontSizecolor的默认值。

Object.assign()将第一个参数作为目标对象{}。 目标对象从unsafeOptions源对象接收fontSize属性的值。 并且人defaults对象的获取color属性值,因为unsafeOptions不包含color属性。

枚举源对象的顺序很重要:后面的源对象属性会覆盖前面的源对象属性。

现在可以安全地访问options对象的任何属性,包括options.color在最初的unsafeOptions中是不可用的。

还有一种简单的方法就是使用ES6中展开运算符:

const unsafeOptions = { fontSize: 18 }; const defaults = { fontSize: 16, color: 'black' }; const options = { ...defaults, ...unsafeOptions }; options.fontSize; // => 18 options.color; // => 'black'
登入後複製

对象初始值设定项从defaultsunsafeOptions源对象扩展属性。 指定源对象的顺序很重要,后面的源对象属性会覆盖前面的源对象。

使用默认属性值填充不完整的对象是使代码安全且持久的有效策略。无论哪种情况,对象总是包含完整的属性集:并且无法生成undefined的属性。

2.3 函数参数

函数参数隐式默认为undefined

通常,用特定数量的参数定义的函数应该用相同数量的参数调用。在这种情况下,参数得到期望的值

function multiply(a, b) { a; // => 5 b; // => 3 return a * b; } multiply(5, 3); // => 15
登入後複製

调用multiply(5,3)使参数ab接收相应的53值,返回结果:5 * 3 = 15

在调用时省略参数会发生什么?

function multiply(a, b) { a; // => 5 b; // => undefined return a * b; } multiply(5); // => NaN
登入後複製

函数multiply(a, b){}由两个参数ab定义。调用multiply(5)用一个参数执行:结果一个参数是5,但是b参数是undefined

技巧6: 使用默认参数值

有时函数不需要调用的完整参数集,可以简单地为没有值的参数设置默认值。

回顾前面的例子,让我们做一个改进,如果b参数未定义,则为其分配默认值2

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

虽然所提供的分配默认值的方法有效,但不建议直接与undefined值进行比较。它很冗长,看起来像一个hack .

这里可以使用 ES6 的默认值:

function multiply(a, b = 2) { a; // => 5 b; // => 2 return a * b; } multiply(5); // => 10 multiply(5, undefined); // => 10
登入後複製

2.4 函数返回值

隐式地,没有return语句,JS 函数返回undefined

在JS中,没有任何return语句的函数隐式返回undefined

function square(x) { const res = x * x; } square(2); // => undefined
登入後複製

square()函数没有返回计算结果,函数调用时的结果undefined

return语句后面没有表达式时,默认返回undefined

function square(x) { const res = x * x; return; } square(2); // => undefined
登入後複製

return;语句被执行,但它不返回任何表达式,调用结果也是undefined

function square(x) { const res = x * x; return res; } square(2); // => 4
登入後複製

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

JS 中的以下语句列表必须以分号(;)结尾:

  • 空语句

  • letconstvarimportexport声明

  • 表达语句

  • debugger语句

  • continue语句,break语句

  • throw语句

  • return语句

如果使用上述声明之一,请尽量务必在结尾处指明分号:

function getNum() { let num = 1; return num; } getNum(); // => 1
登入後複製

let声明和return语句结束时,强制性写分号。

当你不想写这些分号时会发生什么? 例如,咱们想要减小源文件的大小。

在这种情况下,ECMAScript 提供自动分号插入(ASI)机制,为你插入缺少的分号

ASI 的帮助下,可以从上一个示例中删除分号

function getNum() { // Notice that semicolons are missing let num = 1 return num } getNum() // => 1
登入後複製

上面的代码是有效的JS代码,缺少的分号ASI会自动为我们插入。

乍一看,它看起来很 nice。 ASI 机制允许你少写不必要的分号,可以使JS代码更小,更易于阅读。

ASI 创建了一个小而烦人的陷阱。 当换行符位于returnreturn \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 在return后自动插入分号,解释代码如下:

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

return;使函数getPrimeNumbers()返回undefined而不是期望的数组。

这个问题通过删除return和数组文字之间的换行来解决:

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

我的建议是研究自动分号插入的确切方式,以避免这种情况。

当然,永远不要在return和返回的表达式之间放置换行符。

2.5 void 操作符

void 计算表达式无论计算结果如何都返回undefined

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

void操作符的一个用例是将表达式求值限制为undefined,这依赖于求值的一些副作用。

3、 未定义的数组

访问越界索引的数组元素时,会得到undefined

const colors = ['blue', 'white', 'red']; colors[5]; // => undefined colors[-1]; // => undefined
登入後複製

colors数组有3个元素,因此有效索引为012

因为索引5-1没有数组元素,所以访问colors[5]colors[-1]值为undefined

JS 中,可能会遇到所谓的稀疏数组。这些数组是有间隙的数组,也就是说,在某些索引中,没有定义元素。

当在稀疏数组中访问间隙(也称为空槽)时,也会得到一个undefined

下面的示例生成稀疏数组并尝试访问它们的空槽

const sparse1 = new Array(3); sparse1; // => [, , ] sparse1[0]; // => undefined sparse1[1]; // => undefined const sparse2 = ['white', ,'blue'] sparse2; // => ['white', , 'blue'] sparse2[1]; // => undefined
登入後複製

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

4、 undefined和null之间的区别

一个合理的问题出现了:undefinednull之间的主要区别是什么?这两个特殊值都表示为空状态。

主要区别在于undefined表示尚未初始化的变量的值,null表示故意不存在对象。

让咱们通过一些例子来探讨它们之间的区别。

number 定义了但没有赋值。

let number; number; // => undefined
登入後複製

number变量未定义,这清楚地表明未初始化的变量。

当访问不存在的对象属性时,也会发生相同的未初始化概念

const obj = { firstName: 'Dmitri' }; obj.lastName; // => undefined
登入後複製

因为obj中不存在lastName属性,所以JS正确地将obj.lastName计算为undefined

在其他情况下,你知道变量期望保存一个对象或一个函数来返回一个对象。但是由于某些原因,你不能实例化该对象。在这种情况下,null是丢失对象的有意义的指示器。

例如,clone()是一个克隆普通JS对象的函数,函数将返回一个对象

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():15null(或者通常是一个原始值,nullundefined)。在这种情况下,函数不能创建克隆,因此返回null—— 一个缺失对象的指示符。

typeof操作符区分了这两个值

typeof undefined; // => 'undefined' typeof null; // => 'object'
登入後複製

严格相等运算符===可以正确区分undefinednull

let nothing = undefined; let missingObject = null; nothing === missingObject; // => false
登入後複製

总结

undefined的存在是JS的允许性质的结果,它允许使用:

  • 未初始化的变量

  • 不存在的对象属性或方法

  • 访问越界索引的数组元素

  • 不返回任何结果的函数的调用结果

大多数情况下直接与undefined进行比较是一种不好的做法。一个有效的策略是减少代码中undefined关键字的出现:

  • 减少未初始化变量的使用

  • 使变量生命周期变短并接近其使用的位置

  • 尽可能为变量分配初始值

  • 多敷衍 const 和 let

  • 使用默认值来表示无关紧要的函数参数

  • 验证属性是否存在或使用默认属性填充不安全对象

  • 避免使用稀疏数组

更多编程相关知识,请访问:编程入门!!

以上是JavaScript中7個處理undefined的小技巧的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:dmitripavlutin.com
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!