JavaScript Optional Chaining (可選鏈運算子)

Optional Chaining(可選鏈)是 ES2020 引入的語法,使用 ?. 運算子。它讓你可以安全地存取可能不存在的巢狀物件屬性,不用擔心中間某個屬性是 nullundefined 而拋出錯誤。

問題背景

在存取深層巢狀物件時,經常需要檢查每一層是否存在:

const user = {
  name: 'Mike',
  address: {
    city: 'Taipei',
  },
};

// 如果 user.address 不存在,會報錯
console.log(user.address.city); // 'Taipei'
console.log(user.address.street); // undefined
console.log(user.contact.phone); // TypeError: Cannot read property 'phone' of undefined

傳統的解決方式是逐層檢查:

// 方法一:逐層檢查
let phone;
if (user && user.contact && user.contact.phone) {
  phone = user.contact.phone;
}

// 方法二:使用 && 短路求值
const phone = user && user.contact && user.contact.phone;

這樣的程式碼冗長且難以閱讀。

Optional Chaining 語法

使用 ?. 可以大幅簡化:

const user = {
  name: 'Mike',
  address: {
    city: 'Taipei',
  },
};

// 如果 contact 不存在,直接回傳 undefined,不會報錯
console.log(user?.contact?.phone); // undefined

// 如果存在就正常取值
console.log(user?.address?.city); // 'Taipei'

運作原理

?. 會檢查左邊的值是否為 nullundefined

  • 如果是,立即回傳 undefined,不繼續往右執行
  • 如果不是,繼續正常存取屬性
const a = null;
console.log(a?.b); // undefined(a 是 null)

const b = undefined;
console.log(b?.c); // undefined(b 是 undefined)

const c = { d: 1 };
console.log(c?.d); // 1(c 存在,正常存取)

不同的使用方式

存取物件屬性

const user = { name: 'Mike' };

// 點語法
user?.name; // 'Mike'
user?.address; // undefined

// 中括號語法
const prop = 'name';
user?.[prop]; // 'Mike'

呼叫方法

const user = {
  greet: function () {
    return 'Hello!';
  },
};

// 如果方法存在就呼叫
user.greet?.(); // 'Hello!'
user.sayBye?.(); // undefined(不會報錯)

存取陣列元素

const users = [{ name: 'Mike' }, { name: 'John' }];

users?.[0]?.name; // 'Mike'
users?.[5]?.name; // undefined

實用範例

處理 API 回應

async function getUser(id) {
  const response = await fetch('/api/users/' + id);
  const data = await response.json();

  // 安全地存取可能不存在的巢狀資料
  const city = data?.user?.address?.city;
  const phone = data?.user?.contact?.phones?.[0];

  return {
    city: city || '未提供',
    phone: phone || '未提供',
  };
}

存取 DOM 元素

// 如果元素不存在,不會報錯
var value = document.querySelector('#myInput')?.value;

// 安全地呼叫方法
document.querySelector('#myButton')?.addEventListener('click', handler);

設定物件的預設值

const config = getConfig();

const settings = {
  theme: config?.appearance?.theme || 'light',
  language: config?.locale?.language || 'zh-TW',
  fontSize: config?.appearance?.fontSize ?? 16,
};

處理回呼函式

function processData(data, options) {
  // 如果有 onSuccess callback 就呼叫
  options?.onSuccess?.(data);

  // 等同於
  if (options && options.onSuccess) {
    options.onSuccess(data);
  }
}

processData(
  { id: 1 },
  {
    onSuccess: function (data) {
      console.log(data);
    },
  }
);

processData({ id: 2 }); // 沒有 options 也不會報錯

搭配 Nullish Coalescing

Optional Chaining 經常搭配 Nullish Coalescing 運算子 (??) 使用,提供預設值:

const user = {
  settings: {
    theme: null,
  },
};

// ?? 只在 null 或 undefined 時使用預設值
const theme = user?.settings?.theme ?? 'light';
console.log(theme); // 'light'

// 和 || 的差異
let count = user?.settings?.count ?? 0; // 0
count = user?.settings?.count || 0; // 0

// 但如果值是 0
user.settings.count = 0;
user?.settings?.count ?? 10; // 0(保留 0)
user?.settings?.count || 10; // 10(0 被視為 falsy)

注意事項

不要過度使用

// 如果你確定某個屬性一定存在,不需要用 ?.
const name = user?.name; // 如果 user 一定存在,直接用 user.name

// 只在真的可能是 null/undefined 的地方使用
const city = user.address?.city;

不能用於賦值

var obj = {};
obj?.prop = 'value'; // SyntaxError

短路求值

?. 後面的程式碼在短路時不會執行:

let count = 0;

const obj = null;
obj?.foo(count++); // 不會執行 count++

console.log(count); // 0

瀏覽器支援

Optional Chaining 是 ES2020 的功能,現代瀏覽器都已支援。如果需要支援舊瀏覽器,可以使用 Babel 等工具轉譯。

瀏覽器支援版本
Chrome80+
Firefox74+
Safari13.1+
Edge80+
Node.js14+