5年前,學習 Null 和Undefined ,現在有了新的認知,看看這位人才怎么說?
許多編程語言都有一個稱為null的非值。它指示一個變量當前不指向一個對象,例如,當它還沒有初始化的時候。
相比之下,JavaScript有兩個這樣的非值:undefined和null。在這篇博文中,我們將研究它們有什么不同,以及如何最好地使用或避免它們。
1. undefined vs. null
這兩種值非常相似,經常互換使用。因此,它們的區別是微妙的。
1.1 ECMAScript語言規范:undefined vs. null
ECMAScript語言規范對其描述如下。
- undefined是 "當一個變量沒有被賦值時使用"
- null "表示有意不存在任何對象值"
1.2 兩個非值--一個無法消除的錯誤
在JavaScript中擁有兩個非值現在被認為是一個設計錯誤(甚至被JavaScript的創造者Brendan Eich認為)。
那為什么不從 JavaScript 中刪除其中的一個值呢?JavaScript 的一個核心原則是絕不破壞向后兼容。這個原則有很多好處。它最大的缺點是,設計上的錯誤無法被刪除。
1.3 undefined和null的歷史
在Java中(它啟發了JavaScript的許多方面),初始化值取決于變量的靜態類型。
- 具有對象類型的變量被初始化為null。
- 每個原始類型都有自己的初始化值。例如,int變量被初始化為0。
在JavaScript中,每個變量都可以容納對象值和基本類型值。因此,如果null意味著 "不是一個對象",那么JavaScript也需要一個初始化值,意味著 "既不是一個對象也不是一個原始值"。這個初始化值就是 undefined。
undefined 的出現
如果一個變量myVar還沒有被初始化,它的值是undefined。
- let myVar;
- assert.equal(myVar, undefined);
如果一個屬性.unknownProp丟失,訪問該屬性會產生undefined的值。
- const obj = {};
- assert.equal(obj.unknownProp, undefined);
如果一個函數有一個沒有參數的返回語句,該函數隱含地返回undefined。
- function myFunc() {
- return;
- }
- assert.equal(myFunc(), undefined);
如果一個參數x被省略了,語言將該參數初始化為undefined。
- function myFunc() {
- return;
- }
- assert.equal(myFunc(), undefined);
- myFunc();
通過obj?.someProp的可選鏈路,如果obj未定義或為空,則返回未定義。
- > undefined?.someProp
- undefined
- > null?.someProp
- undefined
3.null 出現情況
一個對象的原型要么是一個對象,要么在原型鏈的末端是null。Object.prototype沒有原型。
- > Object.getPrototypeOf(Object.prototype)
- null
如果我們將一個正則表達式(如/a/)與一個字符串(如'x')相匹配,我們要么得到一個帶有匹配數據的對象(如果匹配成功),要么得到空(如果匹配失敗)。
- > /a/.exec('x')
- null
JSON數據格式不支持undefined,只支持null。
- > JSON.stringify({a: undefined, b: null})
- '{"b":null}'
4.特別處理 undefined 或 null 的運算符
4.1 undefined 和參數默認值
在以下情況下,將使用參數默認值。
- 缺少一個參數。
- 一個參數的值是undefined。
例如:
- function myFunc(arg='abc') {
- return arg;
- }
- assert.equal(myFunc('hello'), 'hello');
- assert.equal(myFunc(), 'abc');
- assert.equal(myFunc(undefined), 'abc');
undefined也觸發了參數的默認值,這說明它是一個元值。
下面的例子說明了這一點的用處。
- function concat(str1='', str2='') {
- return str1 + str2;
- }
- function twice(str) { // (A)
- return concat(str, str);
- }
在A行,我們沒有為str指定一個參數默認值。當這個參數缺失時,我們把這個狀態轉發給concat(),讓它選擇一個默認值。
4.2 undefined 的和解構的默認值
解構中的缺省值與參數缺省值的工作原理類似--如果一個變量在數據中沒有匹配,或者它與undefined的變量匹配,就會使用它們。
- 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.3 undefined 和 null 以及可選鏈
當通過 value?.prop 進行可選的鏈接時
- 如果value是undefined或null,則返回 undefined。也就是說,只要value.prop會拋出一個異常,就會發生這種情況。
- 否則,返回value.prop。
- function getProp(value) {
- // optional static property access
- return value?.prop;
- }
- assert.equal(
- getProp({prop: 123}), 123);
- assert.equal(
- getProp(undefined), undefined);
- assert.equal(
- getProp(null), undefined);
下面兩個操作的效果類似。
- obj?.[«expr»] // optional dynamic property access
- func?.(«arg0», «arg1») // optional function or method call
4.4 undefined 和 null 和 nullish 聚結
nullish 合并算子??如果一個值undefined或為null,則允許我們使用默認值:
- > undefined ?? 'default value'
- 'default value'
- > null ?? 'default value'
- 'default value'
- > 0 ?? 'default value'
- 0
- > 123 ?? 'default value'
- 123
- > '' ?? 'default value'
- ''
- > 'abc' ?? 'default value'
- 'abc'
null合并賦值操作符??=:
- function setName(obj) {
- obj.name ??= '(Unnamed)';
- return obj;
- }
- 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.1 undefined 或 null 都不能作為實際值使用
舉例,我們可能希望一個屬性file.title永遠存在,并且永遠是一個字符串。有兩種常見的方法來實現這一點。
這里,只檢查undefined和null,而不檢查一個值是否是字符串。你必須自己決定是否要把它作為一個額外的安全措施來實施。
5.1 undefined 和 null 都是禁止的
如下所示
- function createFile(title) {
- if (title === undefined || title === null) {
- throw new Error('`title` must not be nullish');
- }
- // ···
- }
為什么選擇這種方法?
我們想把undefined和null當作一樣的東西,因為JavaScript代碼經常這樣做--比如說。
- // 檢測一個屬性是否存在
- if (!obj.requiredProp) {
- obj.requiredProp = 123;
- }
- const myValue = myParameter ?? 'some default';
如果我們的代碼中出現了問題,出現了undefined或null,我們希望它盡可能快地失敗。
5.1.2 undefined 和 null 都會觸發默認值
如下所示
- function createFile(title) {
- title ??= '(Untitled)';
- // ···
- }
我們不能在這里使用參數缺省值,因為它只由undefined 觸發。相反,我們依靠nullish 合并賦值運算符??=。
為什么選擇這種方法?
- 我們想把undefined和null同等對待(見上一節)。
- 我們希望我們的代碼能夠穩健地、無聲地處理 undefined 和null。
5.2 undefined 或 null 都是“關閉”值
例如,我們可能希望一個屬性file.title是一個字符串或 "關閉"(文件沒有標題)。有幾種方法可以實現這一點。
5.2.1 null是 "關閉的 "值
- function createFile(title) {
- if (title === undefined) {
- throw new Error('`title` must not be undefined');
- }
- return {title};
- }
另外,undefined可以觸發一個默認值。
- function createFile(title = '(Untitled)') {
- return {title};
- }
為什么選擇這種方法?
- 我們需要一個表示“關閉”的非值。
- 我們不希望我們的非值觸發參數默認值和解構默認值。
- 我們想把非值字符串化為JSON(這是我們不能用undefined做的)。
5.2.2 undefined 是"關閉的 "值。
- function createFile(title) {
- if (title === null) {
- throw new Error('`title` must not be null');
- }
- return {title};
- }
為什么選擇這種方法?
- 我們需要一個表示“關閉”的非值。
- 我們確實想讓我們的非值觸發參數默認值和解構默認值。
5.2.3 為什么不同時使用undefined和null作為 "關閉 "的值?
當接收一個值時,把undefined和null都當作 "不是一個值 "是有意義的。然而,當我們創建值時,我們希望不含糊,以便處理這些值時保持簡單。
這指向了一種不同的方法。如果我們需要一個 "關閉"的值,但又不想使用undefined或null作為這樣的值,怎么辦?請往下看。
5.3 處理“關閉”的其他方式
5.3.1 特殊值
我們可以創建一個特殊的值,每當屬性.title被關閉時,我們就使用這個值。
- const UNTITLED = Symbol('UNTITLED');
- const file = {
- title: UNTITLED,
- };
5.3.2 空對象模式
空對象模式來自于面向對象的編程。
- 一個共同的超類的所有子類都有相同的接口。
- 每個子類都實現了一個實例運行的不同模式。
- 其中一個模式是 "null"。
在下面的例子中,UntitledFile實現了 "null" 模式。
- // 抽象的超類
- class File {
- constructor(content) {
- if (new.target === File) {
- throw new Error('Can’t instantiate this class');
- }
- this.content = content;
- }
- }
- class TitledFile extends File {
- constructor(content, title) {
- super(content);
- this.title = title;
- }
- getTitle() {
- return this.title;
- }
- }
- class UntitledFile extends File {
- constructor(content) {
- super(content);
- }
- getTitle() {
- return '(Untitled)';
- }
- }
- const files = [
- new TitledFile('Dear diary!', 'My Diary'),
- new UntitledFile('Reminder: pick a title!'),
- ];
- assert.deepEqual(
- files.map(f => f.getTitle()),
- [
- 'My Diary',
- '(Untitled)',
- ]);
我們也可以只對標題使用空對象模式(而不是對整個文件對象)。
5.3.3 Maybe 類型
Maybe 類型是一種函數編程技術。
- function getTitle(file) {
- switch (file.title.kind) {
- case 'just':
- return file.title.value;
- case 'nothing':
- return '(Untitled)';
- default:
- throw new Error();
- }
- }
- const files = [
- {
- title: {kind: 'just', value: 'My Diary'},
- content: 'Dear diary!',
- },
- {
- title: {kind: 'nothing'},
- content: 'Reminder: pick a title!',
- },
- ];
- assert.deepEqual(
- files.map(f => getTitle(f)),
- [
- 'My Diary',
- '(Untitled)',
- ]);
我們可以通過Arrays對 "just "和 "nothing "進行編碼。我們的方法的好處是,它得到了TypeScript的良好支持(通過判別性聯合)。
6.我的方法
我不喜歡用undefined作為 "關閉 "的值,有三個原因。
- undefined經常意外地出現在JavaScript中。
- ndefined會觸發參數和結構化的默認值(出于同樣的原因,有些人喜歡undefined)。
因此,如果我需要一個特殊的值,我會使用以下兩種方法中的一種。
- 我使用null作為一個 "關閉 "的值。(作為一個旁觀者,這種方法在TypeScript中得到了比較好的支持)。)
- 我通過上述的技術之一來避免undefined和null。這樣做的好處是比較干凈,壞處是涉及更多的工作。
作者:Michael Thiessen
譯者:前端小智
來源:dev原文:https://2ality.com/2021/01/undefined-null-revisited.html