阿里云最近在作活动,低至2折,有兴趣能够看看:
https://promotion.aliyun.com/...
为了保证的可读性,本文采用意译而非直译。javascript
大约8年前,当原做者开始学习JS时,遇到了一个奇怪的状况,既存在undefined
的值,也存在表示空值的null
。它们之间的明显区别是什么?它们彷佛都定义了空值,并且,比较null == undefined
的计算结果为true
。html
大多数现代语言,如Ruby、Python或Java都有一个空值(nil
或null
),这彷佛是一种合理的方式。前端
对于JavaScript,解释器在访问还没有初始化的变量或对象属性时返回undefined
。例如:java
let company; company; // => undefined let person = { name: 'John Smith' }; person.age; // => undefined
另外一方面,null
表示缺乏的对象引用,JS自己不会将变量或对象属性设置为null
。git
一些原生方法,好比String.prototype.match()
,能够返回null
来表示丢失的对象。看看下面的示例:es6
let array = null; array; // => null let movie = { name: 'Starship Troopers', musicBy: null }; movie.musicBy; // => null 'abc'.match(/[0-9]/); // => null
因为 JS 的宽容特性,开发人员很容易访问未初始化的值,我也犯了这样的错误。github
一般,这种危险的操做会生成undefined
的相关错误,从而快速地结束脚本。相关的常见错误消息有:express
TypeError: 'undefined' is not a function
TypeError: Cannot read property '<prop-name>' of undefined
type errors
JS 开发人员能够理解这个笑话的讽刺:数组
function undefined() { // problem solved }
为了下降此类错误的风险,必须理解生成undefined
的状况。更重要的是抑制它的出现并阻止在应用程序中传播,从而提升代码的持久性。安全
让我们详细讨论undefined
及其对代码安全性的影响。
JS 有6种基本类型
true
或 false
1, 6.7, 0xFF
"Gorilla and banana"
Symbol("name")
(starting ES2015)null
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
都会被定义为undefined
。
ECMAScript规范定义了undefined
值的类型
Undefined type是其惟一值为
undefined
值的类型。
在这个意义上,typeof undefined
返回“undefined”字符串
typeof undefined === 'undefined'; // => true
固然typeof
能够很好地验证变量是否包含undefined
的值
let nothing; typeof nothing === 'undefined'; // => true
还没有赋值(未初始化)的声明变量默认为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,建议是彻底中止使用它。
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:增长内聚性
内聚描述模块的元素(命名空间、类、方法、代码块)内聚在一块儿的程度。凝聚力的测量一般被称为高凝聚力或低内聚。
高内聚是优选的,由于它建议设计模块的元素以仅关注单个任务,它构成了一个模块。
高内聚和低耦合是一个设计良好的系统的特征。
代码块自己可能被视为一个小模块,为了尽量实现高内聚,须要使变量尽量接近使用它们代码块位置。
例如,若是一个变量仅存在以造成块做用域内,不要将此变量公开给外部块做用域,由于外部块不该该关心此变量。
没必要要地延长变量生命周期的一个典型例子是函数中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'; }
index
和item
变量仅存在于for
语句的做用域内,for
以外没有任何意义。length
变量也被声明为接近其使用它的位置。
为何修改后的版本优于初始版本? 主要有几点:
undefined
状态,所以没有访问undefined
的风险
访问不存在的对象属性时,JS 返回
undefined
。
我们用一个例子来讲明这一点:
let favoriteMovie = { title: 'Blade Runner' }; favoriteMovie.actors; // => undefined
favoriteMovie
是一个具备单个属性 title
的对象。 使用属性访问器favoriteMovie.actors
访问不存在的属性actors
将被计算为undefined
。
自己访问不存在的属性不会引起错误, 但尝试从不存在的属性值中获取数据时就会出现问题。 常见的的错误是 TypeError: Cannot read property <prop> 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
参数接受具备属性的对象:
函数返回一个新的数组实例,而不改变原始数组(即它是一个纯函数)。
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
。
检查first
或last
属性是不是undefined
,在条件为 if(toappendix .first){}
和if(toappendix .last){}
中进行验证:
这种方法有一个缺点, undefined
,false
,null
,0
,NaN
和''
是虚值。
在append()
的当前实现中,该函数不容许插入虚值元素:
append([10], { first: 0, last: false }); // => [10]
0
和false
是虚值的。 由于 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
操做符的存在代表一个明确的意图,即检查对象是否具备特定的属性,而不访问实际的属性值。
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]
'first' in toAppend
(和'last' in toAppend
)在对应属性存在时为true
,不然为false
。in
操做符的使用解决了插入虚值元素0
和false
的问题。如今,在[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
是一个具备如下属性的对象:
'
(单引号)或“
(双引号),默认为”
。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
对象中提取char
和skipifquote
属性。若是config
对象中有一些属性不可用,那么解构赋值将设置默认值:char
为'"'
,skipifquote
为false
。
该功能仍有改进的空间。让咱们将解构赋值直接移动到参数部分。并为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:用默认属性填充对象
若是不须要像解构赋值那样为每一个属性建立变量,那么丢失某些属性的对象能够用默认值填充。
ES6 Object.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
对象定义属性fontSize
和color
的默认值。
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'
对象初始值设定项从defaults
和unsafeOptions
源对象扩展属性。 指定源对象的顺序很重要,后面的源对象属性会覆盖前面的源对象。
使用默认属性值填充不完整的对象是使代码安全且持久的有效策略。不管哪一种状况,对象老是包含完整的属性集:而且没法生成undefined
的属性。
函数参数隐式默认为
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
。
在调用时省略参数会发生什么?
function multiply(a, b) { a; // => 5 b; // => undefined return a * b; } multiply(5); // => NaN
函数multiply(a, b){}
由两个参数a
和b
定义。调用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
隐式地,没有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 中的如下语句列表必须以分号(;)
结尾:
let,const,var,import,export
声明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 建立了一个小而烦人的陷阱。 当换行符位于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 在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
和返回的表达式之间放置换行符。
void <expression>
计算表达式不管计算结果如何都返回undefined
。
void 1; // => undefined void (false); // => undefined void {name: 'John Smith'}; // => undefined void Math.min(1, 3); // => undefined
void
操做符的一个用例是将表达式求值限制为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
。
JS 中,可能会遇到所谓的稀疏数组。这些数组是有间隙的数组,也就是说,在某些索引中,没有定义元素。
当在稀疏数组中访问间隙(也称为空槽)时,也会获得一个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
使用数组时,为了不获取undefined
,请确保使用有效的数组索引并避免建立稀疏数组。
一个合理的问题出现了:undefined
和null
之间的主要区别是什么?这两个特殊值都表示为空状态。
主要区别在于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()
: 15
或null
(或者一般是一个原始值,null
或undefined
)。在这种状况下,函数不能建立克隆,所以返回null
—— 一个缺失对象的指示符。
typeof
操做符区分了这两个值
typeof undefined; // => 'undefined' typeof null; // => 'object'
严格相等运算符===
能够正确区分undefined
和null
:
let nothing = undefined; let missingObject = null; nothing === missingObject; // => false
undefined
的存在是JS的容许性质的结果,它容许使用:
大多数状况下直接与undefined
进行比较是一种很差的作法。一个有效的策略是减小代码中undefined
关键字的出现:
代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug。
干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。
https://github.com/qq44924588...
我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,便可看到福利,你懂的。