?与??的7种进阶玩法:代码量减少30%的ES2020语法糖

更新日期: 2026-04-17 阅读: 13 标签: 语法糖

可选链(?.)和空值合并(??)是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?.propobj 为 null/undefined防止读取属性报错
obj?.method?.()obj 或 method 不存在可选方法调用
arr?.[0]arr 为 null/undefined安全取数组元素
a ?? ba 为 null/undefined设置默认值(不误伤0和'')
a ??= ba 为 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+

现代浏览器已全面支持,可放心使用。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13632

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!