?与??的7种进阶玩法:代码量减少30%的ES2020语法糖
可选链(?.)和空值合并(??)是ES2020最受欢迎的两个语法糖。大多数人只会用obj?.prop来防止报错,其实它们还有很多高级组合玩法,掌握之后代码量能减少30%。
基础回顾(30秒)
// ?. 可选链:属性不存在时返回 undefined,不报错
const name = user?.profile?.name; // 不用写 user && user.profile && user.profile.name
// ?? 空值合并:只有 null 和 undefined 才触发默认值
const port = config.port ?? 3000; // 注意:0 和 '' 不会触发(区别于 ||)进阶用法1:?.() 调用方法,方法不存在时优雅跳过
// 旧写法:先判断再调用
if (obj.method && typeof obj.method === "function") {
obj.method();
}
// 新写法:方法不存在直接返回 undefined
obj.method?.();
// 实战:插件系统中调用可能不存在的钩子
plugin.onBeforeRender?.();
plugin.onAfterRender?.();
// 带参数也可以
plugin.transform?.(data, options);
// 事件回调
class EventEmitter {
emit(event, data) {
this.listeners[event]?.forEach((fn) => fn(data)); // 没有监听者就跳过
}
}进阶用法2:?. 访问动态下标,数组/Map安全取值
// 数组:安全取元素
const first = arr?.[0]; // arr 为 null/undefined 时返回 undefined
const last = arr?.[arr.length - 1];
// 动态属性名
const key = "username";
const value = user?.[key]; // 等同于 user?.username
// Map:安全取值(变量可能为 null/undefined)
let map = new Map([["a", 1]]);
map = null; // 模拟 map 被清空
const val = map?.get("a"); // map 为 null 时返回 undefined,不报错
// 实战:处理不确定存在的列表
function getFirstItem(list) {
return list?.[0] ?? "暂无数据";
}
getFirstItem(null); // '暂无数据'
getFirstItem([]); // '暂无数据'
getFirstItem(["hello"]); // 'hello'进阶用法3:??= 赋值运算符,只在 null/undefined 时赋值
??= 是 ES2021 的逻辑赋值运算符,只有左边是 null 或 undefined 时,才执行右边的赋值。
let user = { name: "张三", cache: null };
// 旧写法:
if (user.cache === null || user.cache === undefined) {
user.cache = loadFromStorage();
}
// 新写法:
user.cache ??= loadFromStorage();
// 实战:懒加载/惰性初始化
class Config {
#data = null;
get data() {
this.#data ??= this.#loadConfig(); // 只加载一次
return this.#data;
}
#loadConfig() {
console.log("加载配置...");
return { theme: "dark", lang: "zh" };
}
}
const config = new Config();
config.data; // 打印"加载配置..."
config.data; // 不再打印,直接返回缓存进阶用法4:?? 链式使用,多级后备值
// 多级后备:依次尝试,直到找到非 null/undefined 的值
const username = user?.displayName ?? user?.username ?? user?.email ?? "匿名用户";
// 对比 || 的陷阱:|| 会把 0、''、false 也当成假值
const score = user.score || 100; // 错误:score=0 时错误地用了100
const score2 = user.score ?? 100; // 正确:score=0 时正确保留0
// 实战:API 响应数据处理
function formatUser(apiData) {
return {
id: apiData?.id ?? crypto.randomUUID(),
name: apiData?.name ?? apiData?.nickname ?? "用户",
avatar: apiData?.avatar ?? apiData?.photo ?? "/default-avatar.png",
age: apiData?.age ?? null, // 保留 0,只屏蔽 null/undefined
};
}进阶用法5:?. 与解构组合,安全解构深层数据
// 直接解构可能报错
const { name, age } = user?.profile; // 错误:user为null时报错 Cannot destructure
// 安全解构:先确保对象存在
const { name, age } = user?.profile ?? {};
// 带默认值的安全解构
const {
name = "匿名",
role = "user",
settings: { theme = "light" } = {},
} = user?.profile ?? {};
// 实战:处理接口响应
async function fetchUser(id) {
const res = await api.get(`/user/${id}`);
const { data: { name, email, avatar } = {} } = res ?? {};
return { name: name ?? "未知用户", email: email ?? "", avatar };
}进阶用法6:短路特性,右边的副作用不会执行
?. 的短路:左边为 null/undefined 时,右边完全不执行。
let count = 0;
const obj = null;
obj?.method(count++); // count 不会自增
console.log(count); // 0,右边表达式根本没执行
// ?? 的短路:左边为 null/undefined 时,右边不执行
const cache = { data: [] };
const result = cache.data ?? fetchFromAPI(); // data 是 [],fetchFromAPI 不会调用
console.log(result); // []
// 实战:条件性副作用
function logIfExists(user) {
user?.activity && console.log("活跃用户:", user.name); // user为null时不打印
metrics?.record?.("page_view"); // 没有 metrics 或没有 record 方法时静默跳过
}完整可运行 Demo
html
<!DOCTYPE html>
<html>
<head>
<title>?. 和 ?? 进阶 Demo</title>
<style>
body {
font-family: monospace;
padding: 20px;
background: #0f172a;
color: #e2e8f0;
}
button {
margin: 6px;
padding: 10px 16px;
border: none;
border-radius: 6px;
background: #3b82f6;
color: white;
cursor: pointer;
font-weight: bold;
}
button:hover {
background: #2563eb;
}
.result {
background: #1e293b;
padding: 10px 14px;
margin: 8px 0;
border-radius: 6px;
border-left: 3px solid #3b82f6;
font-size: 13px;
}
.label {
color: #60a5fa;
font-weight: bold;
}
.warn {
border-left-color: #f59e0b;
}
.warn .label {
color: #f59e0b;
}
</style>
</head>
<body>
<h2 style="color:#60a5fa">?. 和 ?? 进阶用法演示</h2>
<button onclick="demo1()">?.() 可选调用</button>
<button onclick="demo2()">??.= 惰性赋值</button>
<button onclick="demo3()">??链式后备</button>
<button onclick="demo4()">短路特性</button>
<div id="out"></div>
<script>
const log = (label, value, warn = false) => {
document
.getElementById("out")
.insertAdjacentHTML(
"afterbegin",
`<div class="result${warn ? " warn" : ""}"><span class="label">${label}</span><br>${JSON.stringify(value)}</div>`
);
};
function demo1() {
const plugin = { onLoad: () => "加载完成" }; // 没有 onRender
const r1 = plugin.onLoad?.();
const r2 = plugin.onRender?.();
log("?.() 有方法", r1);
log("?.() 无方法(不报错)", r2);
}
function demo2() {
let config = { port: null };
config.port ??= 3000;
log("??= 赋值后(null→3000)", config.port);
let config2 = { port: 0 };
config2.port ??= 3000;
log("??= 赋值后(0不变)", config2.port); // 0 不触发
}
function demo3() {
const user1 = { score: 0 };
const user2 = { score: null };
log("?? score=0 正确保留0", user1.score ?? 100);
log("?? score=null 用默认100", user2.score ?? 100);
log("|| score=0 的陷阱", user1.score || 100, true); // 0 被错误替换
}
function demo4() {
let sideEffect = 0;
const obj = null;
obj?.method(sideEffect++);
log("短路:右边副作用未执行,count=", sideEffect); // 0
}
</script>
</body>
</html>保存为 .html 文件,直接浏览器打开,点击按钮即可验证每个特性。
速查表
| 语法 | 触发条件 | 典型用途 |
|---|---|---|
| obj?.prop | obj 为 null/undefined | 防止读取属性报错 |
| obj?.method?.() | obj 或 method 不存在 | 可选方法调用 |
| arr?.[0] | arr 为 null/undefined | 安全取数组元素 |
| a ?? b | a 为 null/undefined | 设置默认值(不误伤0和'') |
| a ??= b | a 为 null/undefined | 惰性初始化/懒加载 |
| a ?? b ?? c | 链式后备值 | 多级回退 |
兼容性
?. 和 ??:ES2020,Chrome 80+(2020年2月),Firefox 72+,Safari 13.1+,Edge 80+,Node.js 14+
??=:ES2021,Chrome 85+(2020年8月),Firefox 79+,Safari 14+,Edge 85+,Node.js 15+
现代浏览器已全面支持,可放心使用。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!