null 和 undefined 的區別!

前端小智發表於2022-03-18
作者:Michael Thiessen
譯者:前端小智
來源:dev

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

許多程式語言都有一個稱為null的非值。它指示一個變數當前不指向一個物件,例如,當它還沒有初始化的時候。

相比之下,JavaScript有兩個這樣的非值:undefinednull。在這篇博文中,我們將研究它們有什麼不同,以及如何最好地使用或避免它們。

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 出現情況

一個物件的原型要麼是一個物件,要麼在原型鏈的末端是nullObject.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 進行可選的連結時

  • 如果valueundefinednull,則返回 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

下面分享一下我們自己的程式碼中處理undefinednull的最常見方法。

5.1 undefined 或 null 都不能作為實際值使用

舉例,我們可能希望一個屬性file.title永遠存在,並且永遠是一個字串。有兩種常見的方法來實現這一點。

這裡,只檢查undefinednull,而不檢查一個值是否是字串。你必須自己決定是否要把它作為一個額外的安全措施來實施。

5.1 undefined 和 null 都是禁止的

如下所示

function createFile(title) {
  if (title === undefined || title === null) {
    throw new Error('`title` must not be nullish');
  }
  // ···
}

為什麼選擇這種方法?

我們想把undefinednull當作一樣的東西,因為JavaScript程式碼經常這樣做--比如說。

// 檢測一個屬性是否存在
if (!obj.requiredProp) {
  obj.requiredProp = 123;
}

const myValue = myParameter ?? 'some default';

如果我們的程式碼中出現了問題,出現了undefinednull,我們希望它儘可能快地失敗。

5.1.2 undefined 和 null 都會觸發預設值

如下所示

function createFile(title) {
  title ??= '(Untitled)';
  // ···
}

我們不能在這裡使用引數預設值,因為它只由undefined 觸發。相反,我們依靠nullish 合併賦值運算子??=

為什麼選擇這種方法?

  • 我們想把undefinednull同等對待(見上一節)。
  • 我們希望我們的程式碼能夠穩健地、無聲地處理 undefinednull

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作為 "關閉 "的值?

當接收一個值時,把undefinednull都當作 "不是一個值 "是有意義的。然而,當我們建立值時,我們希望不含糊,以便處理這些值時保持簡單。

這指向了一種不同的方法。如果我們需要一個 "關閉"的值,但又不想使用undefinednull作為這樣的值,怎麼辦?請往下看。

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中得到了比較好的支援)。)
  • 我通過上述的技術之一來避免undefinednull。這樣做的好處是比較乾淨,壞處是涉及更多的工作。

~ 完,我是小智,刷碗去了,現在唯有刷碗才能體驗到生活中的一點樂趣,逃 ~


程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

原文:https://2ality.com/2021/01/un...

交流

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq44924588... 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

相關文章