JavaScript Optional Chaining (可選鏈運算子)

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

問題背景

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

var 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

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

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

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

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

Optional Chaining 語法

使用 ?. 可以大幅簡化:

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

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

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

運作原理

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

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

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

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

不同的使用方式

存取物件屬性

var user = { name: 'Mike' };

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

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

呼叫方法

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

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

存取陣列元素

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

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

實用範例

處理 API 回應

async function getUser(id) {
    var response = await fetch('/api/users/' + id);
    var data = await response.json();
    
    // 安全地存取可能不存在的巢狀資料
    var city = data?.user?.address?.city;
    var phone = data?.user?.contact?.phones?.[0];
    
    return {
        city: city || '未提供',
        phone: phone || '未提供'
    };
}

存取 DOM 元素

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

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

設定物件的預設值

var config = getConfig();

var 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 運算子 (??) 使用,提供預設值:

var user = {
    settings: {
        theme: null
    }
};

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

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

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

注意事項

不要過度使用

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

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

不能用於賦值

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

短路求值

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

var count = 0;

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

console.log(count);  // 0

瀏覽器支援

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

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