许多编程语言都有一个非值,叫做null。它表示一个变量当前没有指向一个对象,例如,当它还没有被初始化时。相反,JavaScript有两个这样的非值:undefined和null。在这篇博文中,我们将研究它们的不同之处以及如何最好地使用或避免它们。1.undefinedvs.null这两个值非常相似,经常互换使用。因此,它们的区别是微妙的。1.1ECMAScript语言规范:undefinedvs.nullECMAScript语言规范对它的描述如下。undefined是“当一个变量还没有被赋值时使用“null”来表示故意不存在任何对象值”1.2两个非值——一个不可逆的bugJavaScript中有两个非值是现在被认为是一个设计错误(甚至被JavaScript的创造者BrendanEich认为)。那么为什么不从JavaScript中删除其中一个值呢?JavaScript的核心原则是永不破坏向后兼容性。这个原则有很多好处。它最大的缺点是无法消除设计错误。1.3undefined和null的历史在Java中(它启发了JavaScript的许多方面),初始化值取决于变量的静态类型。对象类型的变量被初始化为空。每个基本类型都有自己的初始化值。比如int变量被初始化为0。在JavaScript中,每一个变量都可以保存对象值和原始类型值。所以,如果null表示“不是对象”,那么JavaScript还需要一个初始化值,意思是“既不是对象也不是原始值”。此初始化值未定义。undefined的存在如果一个变量myVar还没有被初始化,它的值就是undefined。letmyVar;assert.equal(myVar,undefined);如果属性.unknownProp缺失,访问该属性将产生未定义的值。constobj={};assert.equal(obj.unknownProp,undefined);如果一个函数有一个没有参数的return语句,则该函数隐式返回undefined。函数myFunc(){return;}assert.equal(myFunc(),undefined);如果省略参数x,则语言会将参数初始化为未定义。函数myFunc(){return;}assert.equal(myFunc(),undefined);myFunc();通过obj?.someProp的可选链,如果obj未定义或为空,则返回未定义。>undefined?.somePropundefined>null?.somePropundefined3.null发生对象的原型是对象或原型链末尾的null。Object.prototype没有原型。>Object.getPrototypeOf(Object.prototype)null如果我们将正则表达式(如/a/)与字符串(如'x')相匹配,我们要么得到一个具有匹配数据的对象(如果匹配成功),要么得到如果匹配失败则为空。>/a/.exec('x')nullJSON数据格式不支持undefined,只支持null。>JSON.stringify({a:undefined,b:null})'{"b":null}'4.undefined或null运算符的特殊处理4.1undefined和参数默认值在以下情况下,参数默认值将被使用。缺少一个参数。一个参数的值未定义。例如:functionmyFunc(arg='abc'){returnarg;}assert.equal(myFunc('hello'),'hello');assert.equal(myFunc(),'abc');assert.equal(myFunc(未定义),'abc');undefined还会触发参数的默认值,这意味着它是一个元值。下面的例子说明了这个的用处。functionconcat(str1='',str2=''){returnstr1+str2;}functiontwice(str){//(A)returnconcat(str,str);}在A行,我们没有为str指定参数默认值.当缺少此参数时,我们将此状态转发给concat()以让它选择一个默认值。4.2未定义和解构的默认值解构中的默认值与参数默认值类似——如果变量在数据中没有匹配项,或者它与未定义的变量匹配,则使用它们。const[a='a']=[];assert.equal(a,'a');const[b='b']=[undefined];assert.equal(b,'b');const{prop:c='c'}={};assert.equal(c,'c');const{prop:d='d'}={prop:undefined};assert.equal(d,'d');4.3undefinedandnull和optionalchain当通过value?.prop进行optionalchaining时,如果value为undefined或null,则返回undefined。也就是说,只要value.prop抛出异常,就会发生这种情况。否则,返回value.prop。functiongetProp(value){//optionalstaticpropertyaccessreturnvalue?.prop;}assert.equal(getProp({prop:123}),123);assert.equal(getProp(undefined),undefined);assert.equal(getProp(null),不明确的);以下两个操作具有类似的效果。obj?.[?expr?]//optionaldynamicpropertyaccessfunc?.(?arg0?,?arg1?)//optionalfunctionormethodcall4.4undefinedandnullandnullishcoalescingoperator??如果值未定义或为空,则允许我们使用默认值:>undefined??'defaultvalue''defaultvalue'>null??'defaultvalue''defaultvalue'>0??'defaultvalue'0>123??'defaultvalue'123>''??'defaultvalue'''>'abc'??'defaultvalue''abc'空合并赋值运算符??=:functionsetName(obj){obj.name??='(Unnamed)';returnobj;}assert.deepEqual(setName({}),{name:'(Unnamed)'});assert.deepEqual(setName({name:undefined}),{name:'(Unnamed)'});assert.deepEqual(setName({name:null}),{name:'(Unnamed)'});assert.deepEqual(setName({name:'Jane'}),{name:'Jane'});5.处理undefined和null下面分享一下我们自己的代码来处理undefined和null最常用的方法。5.1undefined和null都不能用作实际值。例如,我们可能希望一个属性file.title永远存在并且永远是一个字符串。有两种常见的方法可以实现这一点。这里只检查undefined和null,而不检查值是否为字符串。您必须自己决定是否要将其作为附加安全措施来实施。5.1undefined和null都被禁止如下图functioncreateFile(title){if(title===undefined||title===null){thrownewError('`title`mustnotbenullish');}//···}为什么选择这种方法?我们希望将undefined和null视为同一事物,因为JavaScript代码经常这样做——例如。//检测属性是否存在if(!obj.requiredProp){obj.requiredProp=123;}constmyValue=myParameter??'somedefault';如果我们的代码有问题,有undefined或者null,我们希望它可能很快失败。5.1.2undefined和null都会触发默认值如下图functioncreateFile(title){title??='(Untitled)';//···}这里不能使用参数默认值,因为它只会被undefined触发.相反,我们依赖于无效合并赋值运算符??=。为什么采用这种方法?我们希望平等对待undefined和null(参见上一节)。我们希望我们的代码能够稳健且安静地处理undefined和null。5.2undefined或null是“closed”值例如,我们可能希望一个属性file.title是一个字符串或“closed”(文件没有标题)。有几种方法可以实现这一点。5.2.1null是一个“关闭”值默认值。functioncreateFile(title='(Untitled)'){return{title};}为什么采用这种方法?我们需要“关闭”的非值。我们不希望我们的非值触发参数默认值和解构默认值。我们想将非值字符串化为JSON(我们不能用undefined做)。5.2.2undefined是一个“off”值。functioncreateFile(title){if(title===null){thrownewError('`title`mustnotbenull');}return{title};}为什么采用这种方法?我们需要一个表示“关闭”的NOT值。我们确实希望我们的非值触发参数默认值和解构默认值。5.2.3为什么不同时使用undefined和null作为“封闭”值?接收值时,将undefined和null都视为“不是值”是有意义的。但是,当我们创建值时,我们希望是明确的,以便使用这些值保持简单。这指向一种不同的方法。如果我们需要一个“关闭”值,但又不想使用undefined或null怎么办?见下文。5.3其他处理“closed”的方法5.3.1特殊值我们可以创建一个特殊值,每当属性.title关闭时我们都可以使用它。constUNTITLED=符号('无标题');constfile={标题:无标题,};5.3.2空对象模式空对象模式来自面向对象编程。公共超类的所有子类都具有相同的接口。每个子类实现实例运行的不同模式。其中一种模式是“空”。在下面的示例中,UntitledFile实现了“空”模式。//抽象超类classFile{constructor(content){if(new.target===File){thrownewError('Can'instantiatethisclass');}this.content=content;}}classTitledFileextendsFile{constructor(content,title){super(内容);this.title=title;}getTitle(){returnthis.title;}}classUntitledFileextendsFile{constructor(content){super(content);}getTitle(){return'(Untitled)';}}constfiles=[newTitledFile('Deardiary!','MyDiary'),newUntitledFile('提醒:pickatitle!'),];assert.deepEqual(files.map(f=>f.getTitle()),['MyDiary','(无标题)',]);我们也可以只对标题使用空对象模式(而不是整个文件对象)。5.3.3Maybe类型Maybe类型是一种函数式编程技术。functiongetTitle(file){switch(file.title.kind){case'just':returnfile.title.value;case'nothing':return'(Untitled)';default:thrownewError();}}constfiles=[{title:{kind:'just',value:'MyDiary'},content:'Deardiary!',},{title:{kind:'nothing'},content:'Reminder:pickatitle!',},];断言。deepEqual(files.map(f=>getTitle(f)),['MyDiary','(Untitled)',]);我们可以通过数组编码“just”和“nothing”。我们的方法的好处在于它得到了TypeScript的良好支持(通过判别联合)。6.我的方法由于三个原因,我不喜欢使用undefined作为“关闭”值。undefined经常意外出现在JavaScript中。ndefined触发参数和结构化默认值(有些人出于同样的原因喜欢undefined)。所以如果我需要一个特定的值,我使用以下两种方法之一。我使用null作为“关闭”值。(作为旁观者,这种方式在TypeScript中支持得比较好)。)我通过上述技术之一避免了undefined和null。这具有更清洁的优点,涉及更多工作的缺点。作者:MichaelThiessen译者:前端小智来源:dev原文:https://2ality.com/2021/01/undefined-null-revisited.html
