ES6 到 ES11 核心特性精要:只讲对你现代 JavaScript 开发真正常用的那部分

19 阅读15分钟

前言:

ES6(ES2015)到 ES11(ES2020)是 JavaScript 语言现代化的关键阶段,持续引入精巧而实用的新特性,使开发者的开发效率、代码可读性与运行时安全性都大大提升了。

而本文旨在系统梳理 ES6 至 ES11 中最具影响力与实用价值的核心特性,聚焦那些被广泛采纳、深刻改变日常编码习惯的语言革新。理解这些特性背后的动机与应用场景,都将帮助你写出更清晰、健壮且富有表现力的现代 JavaScript 代码

ES6(ES2015)—— 革命性更新

几乎所有现代 JavaScript 开发都建立在 ES6 及其后续版本之上。ES6 不仅是一次语法升级,更是对语言设计哲学的重构,使 JavaScript 更适合大型应用开发。

1. let / const:块级作用域变量

函数作用域:

在 ES6 之前,JavaScript 只有函数作用域,没有块级作用域

什么是函数作用域?

变量的作用域仅由 函数边界 决定。在函数内部声明的变量,在整个函数体内都可见;在函数外部则不可见。

例如:

function foo() { 
    var x = 1; 
    if (true) { 
        var x = 2; // 重新赋值,不是新变量! 
        console.log(x); // 2 
    } 
    console.log(x); // 2 
} 
foo();

即使 var x = 2写在if块内部,但是其仍然属于foo函数作用域(ifforwhile 等语句块不会创建新的作用域

常见陷阱:循环中的闭包问题

for (var i = 0; i < 3; i++) { 
    setTimeout(() => { 
        console.log(i); // 输出 3, 3, 3(而不是 0,1,2) 
    }, 100); 
}

i 是函数作用域变量,三个 setTimeout 回调共享同一个 i,当它们执行时,循环早已结束,i 的值已是 3

ES5 时代解决方案:用 立即执行函数(IIFE) 创建新作用域


变量提升:

变量提升 是 JavaScript 引擎在代码执行前的一个“预处理”行为。

什么是变量提升?

使用 var 声明的变量和 function 声明的函数,会被自动移动到其所在作用域的顶部(仅声明被提升,赋值不会)。

示例1:变量提升

console.log(a); // undefined(不是 ReferenceError!) 
var a = 10; 
console.log(a); // 10

就等价于:

var a; // 声明被提升到顶部 
console.log(a); // undefined(此时未赋值) 
a = 10; // 赋值留在原地 
console.log(a); // 10

示例2:函数表达式 vs 函数声明

// 函数声明 → 完整提升 
foo(); // 正常执行 
function foo() { } 

// 函数表达式 → 只有变量名提升 
bar(); // TypeError: bar is not a function 
var bar = function() { };

常见陷阱:遮蔽

var name = 'xxl'; 
function test() { 
    console.log(name); // undefined(不是 'xxl'!) 
    var name = 'Bob';
    console.log(name); // 'Bob' 
} 
test();

包裹在函数test内部的变量 var name变量提升了导致屏蔽了外部的var name = 'xxl'


🔍 新特性 :

  • let:声明可变的块级作用域变量。
  • const:声明不可重新赋值的块级作用域常量(注意:对象/数组内容仍可修改)。

虽然会声明提升,但不初始化(存在“暂时性死区”),并且不允许重复声明

{
  console.log(x); // ReferenceError!
  let x = 1;
  const y = 2;
  x = 3;      // 允许
  // y = 4;   // 报错:Assignment to constant variable
}
console.log(x); // ReferenceError: x is not defined

let x执行之前,x处于 “死区”,访问会报错ReferenceError(而不是undefined

⚠️ 关键点

  • 块级作用域{} 内部即为一个作用域(如 iffor、函数体)。
  • const 并非“完全不可变”:
const obj = { a: 1 };
obj.a = 2; // 允许(修改属性)
// obj = {}; // 不允许(重新赋值)

使用建议

  • 默认使用 const,只有需要重新赋值时才用 let
  • 完全避免使用 var

2. 箭头函数(Arrow Functions)

🔍 语法简化

// 普通函数
const add1 = function(a, b) { 
    return a + b; 
};

// 箭头函数
const add2 = (a, b) => a + b;

// 单参数可省略括号
const square = x => x * x;

// 无参数
const sayHi = () => console.log('Hi');

核心优势:不绑定自己的 this

箭头函数的 this 继承自外层作用域(词法作用域),解决了回调中 this 指向丢失的问题:

class Timer {
  constructor() {
    this.seconds = 0;
  }
  start() {
    // 普通函数:this 指向 window(非严格模式)或 undefined
    // setInterval(function() { this.seconds++; }, 1000); // 不好

    // 箭头函数:this 指向 Timer 实例
    setInterval(() => {
      this.seconds++; // 正确
    }, 1000);
  }
}

⚠️ 注意事项

  • 不能用作构造函数(无 new.target,无 prototype)。
  • 没有 arguments 对象。
  • 不适合需要动态 this 的场景(如事件处理器需访问 DOM 元素时)。

3. 模板字符串(Template Literals)

🔍 多行字符串 + 字符串插值

使用反引号 ` 包裹,支持:

  • 换行(保留格式)
  • ${expression} 插入变量或表达式
const name = 'Alice';
const age = 30;

// 传统方式(繁琐且易错)
const msg1 = 'Hello ' + name + '!\nYou are ' + age + ' years old.';

// 模板字符串
const msg2 = `
Hello ${name}!
You are ${age} years old.
`;

console.log(msg2);
// 输出:
// Hello Alice!
// You are 30 years old.

4. 解构赋值(Destructuring Assignment)

🔍 从 数组或对象 中提取数据并赋值

数组解构:
const [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1 2 3

// 跳过元素
const [x, , z] = [1, 2, 3]; // x=1, z=3

// 默认值
const [p = 10, q = 20] = [5]; // p=5, q=20
对象解构
const user = { name: 'Alice', age: 30 };

// 基本解构
const { name, age } = user;

// 重命名
const { name: userName, age: userAge } = user;

// 默认值
const { email = 'unknown@qq.com' } = user; // 如果user中包含email,那么这里的默认值就会被忽略
// 即 user.email === undefined ? 'unknown@qq.com' : user.email

常见应用场景

  • 函数参数解构:

    function createUser({ name, age }) {
      return { name, age };
    }
    createUser({ name: 'Bob', age: 25 });
    
  • 交换变量:

    [a, b] = [b, a];
    

5. 模块化(Modules)

🔍 使用 import / export 实现代码分割与复用

导出(export)
// utils.js
export const PI = 3.14;

export function add(a, b) {
  return a + b;
}

export default class Calculator { } // 默认导出(一个文件只能有一个)
导入(import)
// main.js
import Calculator, { PI, add } from './utils.js';

// 重命名
import { add as sum } from './utils.js';

// 导入所有
import * as utils from './utils.js';

优势

  • 明确依赖关系
  • 避免全局污染

6. Promise:统一异步处理

🔍 解决“回调地狱”(Callback Hell)

// 回调地狱(难以维护)
getData(function(a) {
  getMoreData(a, function(b) {
    getEvenMoreData(b, function(c) {
      // ...
    });
  });
});

// Promise 链式调用
getData()
  .then(a => getMoreData(a))
  .then(b => getEvenMoreData(b))
  .then(c => { /* ... */ })
  .catch(err => console.error(err));

核心方法

  • .then():处理成功结果
  • .catch():捕获错误
  • .finally()(ES2018):无论成功/失败都执行

7. 类(Class)

🔍 语法糖,更清晰的面向对象写法

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal { // extends 负责继承
  constructor(name, breed) {
    super(name); // 调用父类构造函数
    this.breed = breed;
  }

  speak() {
    console.log(`${this.name} barks.`); // 方法重写
  }
}

优势

  • 代码更易读、更接近其他 OOP 语言。
  • 避免手动操作 prototype 的复杂性。

8. 默认参数 & 剩余/展开运算符(Rest/Spread)

默认参数

function greet(name = 'Guest', greeting = 'Hello') {
  return `${greeting}, ${name}!`;
}

greet(); // "Hello, Guest!"
greet('Alice'); // "Hello, Alice!"

🔍 剩余参数(Rest Parameters)

将多个参数收集为数组:

function sum(...numbers) {
  return numbers.reduce((a, b) => a + b, 0);
}

sum(1, 2, 3, 4); // numbers = [1, 2, 3, 4]

🔍 展开运算符(Spread Syntax)

将数组/对象展开:

// 数组
const arr1 = [1, 2];
const arr2 = [...arr1, 3, 4]; // [1, 2, 3, 4]

// 函数调用
Math.max(...arr1); // 等价于 Math.max(1, 2)

应用场景

  • 浅拷贝数组/对象
  • 合并数据
  • 灵活处理函数参数

ES7(ES2016)—— 精简而实用的增强

ES7 并没有像 ES6 那样带来革命性变化,而是聚焦于解决日常开发中的小痛点,提升代码可读性与表达力。

1. Array.prototype.includes():判断数组是否包含某个值

背景:为什么需要它?

在 ES7 之前,我们通常用 indexOf 判断元素是否存在:

if ([1, 2, 3].indexOf(2) !== -1) { 
    // 存在 
}

但是这种方式:

  • 语义不清晰indexOf 返回索引,需额外判断是否为 -1
  • 无法正确处理 NaN
    [NaN].indexOf(NaN); // -1(错误!)
    

🔍 includes() 的优势:

  • 语义明确:直接返回 true/false
  • 能正确识别 NaN
  • 支持 fromIndex 参数(从指定位置开始搜索)
// 基本用法 
[1, 2, 3].includes(2); // true 
['a', 'b', 'c'].includes('d'); // false 

// 处理 NaN(这是 includes() 最大的亮点) 
[NaN].includes(NaN); // true
[1, NaN, 3].includes(NaN); // true 

// fromIndex:从索引 2 开始查找 
[1, 2, 3, 4].includes(3, 2); // true 
[1, 2, 3, 4].includes(3, 3); // false(索引 3 是 4) 

// 类型严格相等(===),不会类型转换 
[1, 2, 3].includes('2'); // false(字符串 '2' ≠ 数字 2)

⚠️ 注意事项

  • 使用 SameValueZero 算法 比较(与 === 几乎相同,唯一例外是 NaN === NaN 成立
  • 不会跳过空槽(empty slots):
    const arr = [, ,];
    arr.includes(undefined); // true(空槽被视为 undefined)
    

2. 指数运算符 **:用于计算幂运算(x 的 y 次方)

背景:之前的写法

在 ES7 之前,只能使用 Math.pow()

Math.pow(2, 3); // 8

但这种方式:

  • 不够直观:数学表达式  无法直接书写
  • 可读性差:嵌套复杂表达式时难以理解

🔍 ** 的优势

  • 符合数学直觉2 ** 3 就是 “2 的 3 次方”
  • 支持赋值运算符 **=
  • 右结合性(符合数学规则):2 ** 3 ** 2 = 2 ** (3 ** 2) = 2 ** 9
// 基本幂运算
2 ** 3;    // 8
5 ** 2;    // 25
2 ** -1;   // 0.5(负指数)

// 结合变量
const x = 3;
x ** 2;    // 9

// 赋值运算符
let a = 2;
a **= 3;   // a = a ** 3 → a = 8

// 右结合性(重要!)
2 ** 3 ** 2;   // 512(因为 3**2=9,再 2**9=512)
// 等价于:
2 ** (3 ** 2); // 512
// 而不是:
(2 ** 3) ** 2; // 64

⚠️ 注意事项

  • 不能有空格2 * * 3 会报错(必须写成 2**3
  • 优先级高于一元运算符
    -2 ** 2; // -4(等价于 -(2 ** 2))
    // 若想计算 (-2)²,需加括号:
    (-2) ** 2; // 4
    

ES8(ES2017)—— 异步编程的里程碑

ES8 聚焦于提升开发体验:让异步代码更清晰、对象遍历更直观、字符串处理更便捷。

1. async / await:让异步代码像同步一样书写

背景:回调地狱与 Promise 链

在 async/await 出现前,处理多个异步操作非常繁琐:

// 回调地狱(难以维护)
getData((err, data) => {
  getMoreData(data, (err, more) => {
    getEvenMore(more, (err, final) => { /* ... */ });
  });
});

// Promise 链(稍好,但嵌套逻辑仍复杂)
fetch(url)
  .then(res => res.json())
  .then(data => fetch(data.url))
  .then(res => res.json())
  .catch(err => console.error(err));

🔍 async/await 的核心优势

  • 语法简洁:用同步风格写异步逻辑
  • 错误处理统一:可用 try/catch
  • 调试友好:断点可正常步入 await 行
  • 基于 Promiseawait 后面必须是 Promise
// 1. 基本用法
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;
}

// 2. 错误处理
async function safeFetch() {
  try {
    const res = await fetch('/api/data');
    if (!res.ok) throw new Error('Network error');
    return await res.json();
  } catch (err) {
    console.error('Failed to fetch:', err);
    return null;
  }
}

// 3. 并行请求(避免串行等待)
async function fetchAll() {
  const [user, posts] = await Promise.all([
    fetchUser(1),
    fetchPosts()
  ]);
  return { user, posts };
}

⚠️ 注意事项

  • await 只能在 async 函数内使用
  • 顶层 await :仅在模块(type="module")或现代 Node.js 中支持
  • 不要滥用串行:无关的异步操作应并行(用 Promise.all

最佳实践

  • 用 async/await 替代 .then() 链
  • 总是配合 try/catch 处理错误
  • 对独立请求使用 Promise.all 提升性能

2. Object.values() / Object.entries():对象遍历像数组一样简单

背景:传统对象遍历的痛点

过去遍历对象需结合 for...in + hasOwnProperty,或先取键再映射:

const obj = { a: 1, b: 2 };

// 旧方式:获取值
const values = [];
for (let key in obj) {
  if (obj.hasOwnProperty(key)) {
    values.push(obj[key]);
  }
}

// 或
const values = Object.keys(obj).map(key => obj[key]);

🔍 新方法的优势

方法返回值用途
Object.keys(obj)['a', 'b']获取所有可枚举属性名
Object.values(obj)[1, 2]直接获取所有值
Object.entries(obj)[['a',1], ['b',2]]获取键值对数组
const user = { name: 'Alice', age: 30, city: 'Paris' };

// 获取所有值
console.log(Object.values(user)); 
// ['Alice', 30, 'Paris']

// 获取键值对(常用于 map 渲染)
console.log(Object.entries(user));
// [['name', 'Alice'], ['age', 30], ['city', 'Paris']]

// 在 React/Vue 中遍历对象
Object.entries(user).map(([key, value]) => (
  // 方法
));

// 计算对象总和(如统计)
const scores = { math: 90, english: 85 };
const total = Object.values(scores).reduce((a, b) => a + b, 0); // 175

⚠️ 注意事项

  • 只包含可枚举属性(不包括 Symbol 键,除非用 Object.getOwnPropertySymbols
  • 不保证顺序(但现代引擎通常按创建顺序)

3. String.prototype.padStart() / padEnd():字符串补全(左填充 / 右填充)

背景:格式化需求常见但实现繁琐

格式化时间(5 → 05)、对齐文本、生成固定长度 ID 等,过去需手动拼接或正则处理。

🔍 新方法的优势

  • 语义清晰padStart(左补)、padEnd(右补)
  • 自动截断:若原字符串长度 ≥ 目标长度,直接返回原字符串
  • 支持多字符填充
// 时间格式化
'7'.padStart(2, '0');     // '07'
'12'.padStart(2, '0');    // '12'(长度已够,不变)

// 生成固定长度编号
'123'.padStart(6, '0');   // '000123'

// 右对齐文本(控制台输出)默认填充空
'Price'.padEnd(10) + '$10'; // 'Price     $10'

// 多字符填充
'abc'.padStart(8, 'xy');    // 'xyxyxabc'(重复填充 'xy')

// 安全处理:目标长度小于原长
'hello'.padStart(3, '0');   // 'hello'(不会截断!)

⚠️ 注意事项

  • padString 默认是空格 ' '
  • 填充字符串会被截断以适应目标长度
    'a'.padStart(5, 'xyz'); // 'xyzxa'(不是 'xyzxy')
    

ES9(ES2018)—— 对象操作与异步收尾的优雅升级

ES9 延续了“小而美”的更新风格,聚焦开发者日常痛点,让对象处理更灵活,异步逻辑更完整。

1. 对象展开/剩余运算符(ES6中 数组展开/剩余语法的延展)

🔍 特性一:对象展开(Object Spread)—— 合并/克隆对象

// 1. 浅拷贝对象
const user = { name: 'Alice', age: 30 };
const clone = { ...user }; // { name: 'Alice', age: 30 }

// 2. 合并多个对象(后覆盖前)
const defaults = { theme: 'light', lang: 'en' };
const settings = { lang: 'zh', fontSize: 14 };
const config = { ...defaults, ...settings };
// { theme: 'light', lang: 'zh', fontSize: 14 }

// 3. 添加新属性
const person = { name: 'Bob' };
const employee = { ...person, role: 'developer', id: 101 };
// { name: 'Bob', role: 'developer', id: 101 }

⚠️ 注意事项:

  • 浅拷贝:嵌套对象仍共享引用(引用式赋值)
    const original = { a: { b: 1 } };
    const copy = { ...original };
    copy.a.b = 2;
    console.log(original.a.b); // 2(被修改了!)
    
  • 属性覆盖顺序:后者属性会覆盖前者同名属性
  • 非对象值会被忽略(但不会报错):
    { ...null, ...undefined, x: 1 } // { x: 1 }
    

🔍 特性二:对象剩余(Object Rest)—— 提取“其余属性”

// 1. 提取部分属性,其余打包
const user = { id: 1, name: 'Alice', email: 'a@example.com', role: 'admin' };
const { id, ...userInfo } = user;
// id = 1
// userInfo = { name: 'Alice', email: 'a@example.com', role: 'admin' }

// 2. 函数参数中移除特定字段
function omit(obj, key) {
  const { [key]: _, ...rest } = obj; // 使用计算属性名 + 忽略变量 _
  return rest;
}
omit({ a: 1, b: 2, c: 3 }, 'b'); // { a: 1, c: 3 }

// 3. React 中分离 props
function MyComponent({ className, children, ...restProps }) {
  return <div className={className} {...restProps}>{children}</div>;
}

⚠️ 注意事项:

  • 只能用在解构的最后
    const { ...rest, name } = user; // SyntaxError
    
  • 不会包含原型链上的属性(只处理自身可枚举属性)

2. Promise.prototype.finally():Promise 都执行的回调

背景:为什么需要 finally

在 ES9 之前,要实现“无论成功失败都执行”的逻辑,必须在 .then() 和 .catch() 中重复写代码

// ES8 及之前(冗余)
showLoading();
fetch('/api/data')
  .then(data => {
    hideLoading(); // ← 重复
    return processData(data);
  })
  .catch(err => {
    hideLoading(); // ← 重复
    showError(err);
  });

🔍 finally() 的优势

  • 消除重复代码
  • 语义清晰:明确表示“收尾操作”
  • 不改变 Promise 状态finally 中的返回值会被忽略,错误会继续抛出
// 1. 隐藏 loading 指示器
showLoading();
fetchUserData()
  .then(user => renderUser(user))
  .catch(err => showErrorMessage(err))
  .finally(() => hideLoading()); // 无论成功失败都隐藏

// 2. 清理资源
let dbConnection;
openDB()
  .then(conn => {
    dbConnection = conn;
    return conn.query('SELECT * FROM users');
  })
  .finally(() => {
    if (dbConnection) dbConnection.close(); // 安全关闭
  });

// 3. finally 中抛出错误会传递下去
Promise.resolve(1)
  .finally(() => {
    throw new Error('Oops!');
  })
  .catch(err => console.log(err.message)); // "Oops!"

⚠️ 关键行为:

场景finally回调是否执行?
Promise 成功
Promise 失败
finally 中返回值被忽略(不影响链式结果)
finally 中抛出错误会中断后续 .then(),进入 .catch()

💡 finally 不接收参数(因为它不知道 Promise 是成功还是失败):

promise.finally(result => { /* result 是 undefined! */ });

ES10(ES2019)—— 精巧实用的工具方法

ES10 聚焦于“补齐生态”,提供对称、一致、标准化的工具函数,让开发者告别手动实现或依赖第三方库。

1. Array.prototype.flat() / flatMap():轻松处理嵌套数组

背景:手动扁平化很麻烦

过去要展平嵌套数组,需用 reduce + 递归或 concat.apply

// 手动展平一层
const arr = [[1, 2], [3, 4]];
const flat = [].concat(...arr); // [1, 2, 3, 4]

// 展平多层?更复杂!

🔍 flat():按需展平指定层数

// 展平一层(默认)
[[1, 2], [3, [4]]].flat(); 
// → [1, 2, 3, [4]]

// 展平两层
[[1, 2], [3, [4]]].flat(2); 
// → [1, 2, 3, 4]

// 展平任意深度
[1, [2, [3, [4, [5]]]]].flat(Infinity); 
// → [1, 2, 3, 4, 5]

// 自动跳过空槽(empty slots)
[1, , [2, 3]].flat(); 
// → [1, 2, 3](中间的空位被忽略)

🔍 flatMap():先 map 再 flat(一层)

// 场景:将句子拆分为单词数组并合并
const sentences = ['Hello world', 'JS is fun'];
const words = sentences.flatMap(sentence => sentence.split(' '));
// → ['Hello', 'world', 'JS', 'is', 'fun']

// 对比传统写法:
sentences.map(s => s.split(' ')).flat(); // 效果相同,但 flatMap 更高效(只遍历一次)

// 过滤 + 映射(返回空数组可过滤掉元素)
[1, 2, 3, 4].flatMap(n => 
  n % 2 === 0 ? [n * 2] : [] 
);
// → [4, 8](奇数被过滤)
⚠️ 注意事项:
  • flatMap 只能展平一层(内部调用 flat(1)
  • 回调函数必须返回数组(否则会报错或行为异常)

2. Object.fromEntries():将键值对列表转回对象

背景:对象 ↔ 键值对 的转换不对称

ES8 引入了 Object.entries(obj),但没有对应的反向方法,导致链式操作中断:

// 想过滤对象属性?需手动重建对象
const obj = { a: 1, b: 2, c: 3 };
const filtered = Object.keys(obj)
  .filter(k => obj[k] > 1)
  .reduce((acc, k) => ({ ...acc, [k]: obj[k] }), {});
// 复杂且低效!

🔍 fromEntries() 的优势

  • 完美对称Object.fromEntries(Object.entries(obj)) === obj(浅拷贝)
  • 支持链式操作:结合 entries() 实现函数式对象变换
// 1. 过滤对象属性(函数式风格)
const obj = { a: 1, b: 2, c: 3 };
const filtered = Object.fromEntries(
  Object.entries(obj).filter(([key, val]) => val > 1)
);
// → { b: 2, c: 3 }

// 2. 转换 Map 到普通对象
const map = new Map([['name', 'Alice'], ['age', 30]]);
const user = Object.fromEntries(map);
// → { name: 'Alice', age: 30 }

// 3. 修改键名(如 API 字段重命名)
const apiData = { user_id: 123, user_name: 'Bob' };
const cleanData = Object.fromEntries(
  Object.entries(apiData).map(([key, val]) => 
    [key.replace('user_', ''), val]
  )
);
// → { id: 123, name: 'Bob' }
⚠️ 注意事项:
  • 输入必须是可迭代对象(如 ArrayMap
  • 每个元素必须是长度 ≥2 的数组或类似结构(取前两个值作为 [key, value]

3. trimStart() / trimEnd():标准化字符串首尾空白清理

背景:浏览器兼容性问题

早期浏览器提供了非标准方法:

  • trimLeft() / trimRight()(Chrome、Firefox 支持)
  • 但 Safari 和 IE 不一致

ES10 引入了标准化名称,并让旧方法成为别名:

标准方法(ES10+)非标准别名(仍可用)
str.trimStart()str.trimLeft()
str.trimEnd()str.trimRight()

所有现代浏览器现在都同时支持新旧两种写法,但推荐使用标准名称

const str = '   Hello World!   ';

console.log(str.trimStart()); // 'Hello World!   '
console.log(str.trimEnd());   // '   Hello World!'
console.log(str.trim());      // 'Hello World!'(移除两端)

// 别名仍然有效(但不推荐)
console.log(str.trimLeft() === str.trimStart());  // true
console.log(str.trimRight() === str.trimEnd());   // true

ES11(ES2020)—— 安全、精确与跨环境统一

ES11 聚焦于 空值安全处理大整数支持 和 跨平台一致性,极大提升了代码的健壮性与可移植性。

1. 空值合并运算符 ??:仅在值为 null 或 undefined 时提供默认值

背景:|| 的陷阱

传统使用逻辑或 || 设置默认值存在严重问题:

const name = input || 'default';

但 || 会将所有 falsy 值(如 0''falseNaN)都视为“无效”,导致意外覆盖:

// 问题示例
const count = 0;
console.log(count || 10); // 10(但 0 是有效值!)

const title = '';
console.log(title || 'Untitled'); // 'Untitled'(但空字符串可能是用户故意输入的)

🔍 ?? 的核心优势

  • 只响应 null 和 undefined
  • 保留其他 falsy 值0''falseNaN 等)
// 正确处理边界值
0 ?? 10;        // 0(保留 0)
'' ?? 'text';   // ''(保留空字符串)
false ?? true;  // false(保留布尔 false)
NaN ?? 0;       // NaN(保留 NaN)

// 仅当真正“无值”时用默认值
null ?? 'default';      // 'default'
undefined ?? 'default'; // 'default'

// 实际场景:配置项默认值
const config = {
  timeout: userConfig.timeout ?? 5000, // 若用户没设 timeout 才用默认
  retries: userConfig.retries ?? 3
};

⚠️ 注意事项

  • 不能与 && 或 || 混用(需加括号):
    // SyntaxError
    a || b ?? c;
    
    // 必须加括号
    (a || b) ?? c;
    a || (b ?? c);
    
  • 与可选链 ?. 天然搭配(见下文)

2. 可选链操作符 ?.:安全访问嵌套属性

背景:深层属性访问风险高

传统写法需层层检查:

// 冗长且易错
const city = user && user.address && user.address.city;

// 或使用 try/catch(过度)

🔍 ?. 的核心优势

  • 自动短路:若左侧为 null/undefined,立即返回 undefined 而不报错
  • 支持多种访问形式:属性、方法调用、动态属性
const user = {
  name: 'Alice',
  address: { city: 'Paris' },
  getAge: () => 30
};

// 安全读取属性
user?.address?.city;        // 'Paris'
user?.profile?.email;       // undefined(不报错)

// 安全调用方法
user?.getAge?.();           // 30
user?.getProfile?.();       // undefined(方法不存在)

// 动态属性
const key = 'city';
user?.address?.[key];       // 'Paris'

// 与空值合并搭配(完美组合)
const email = user?.contact?.email ?? 'no-email@example.com';

⚠️ 注意事项

  • 不会跳过 falsy 值(如 0''):
    const obj = { a: { b: 0 } };
    obj.a?.b; // 0(正常返回,因为 a 存在)
    
  • 不能用于赋值左侧
    user?.address = {}; // SyntaxError
    

3. BigInt:表示任意精度的整数(突破 Number.MAX_SAFE_INTEGER 限制)

背景:JavaScript 数字的精度限制

  • JavaScript 的 Number 类型基于 IEEE 754 双精度浮点数
  • 安全整数范围-(2^53 - 1) 到 2^53 - 1(即 ±9007199254740991
  • 超出此范围会丢失精度
    9007199254740992 === 9007199254740993; // true!(精度丢失)
    

🔍 BigInt 的核心优势

  • 任意精度整数(仅受内存限制)
  • 原生支持大数运算
// 超大整数运算
const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
console.log(maxSafe + 1n); // 9007199254740992n(精确!)

// 比较
10n === 10;    // false(类型不同)
10n == 10;     // true(类型转换)

// 不支持与 Number 混合运算
// 10n + 10;   //  TypeError
10n + BigInt(10); // 20n 

// 常见用途:加密、ID(如 Twitter Snowflake ID)、金融计算

⚠️ 注意事项

  • 不能与 Number 直接运算或比较(严格相等)
  • 不支持 Math 对象方法(如 Math.sqrt(16n) 会报错)
  • JSON 不支持序列化 BigInt(需自定义 replacer):
    JSON.stringify({ id: 123n }); // TypeError
    // 解决方案:
    JSON.stringify({ id: 123n }, (k, v) => typeof v === 'bigint' ? v.toString() : v);
    

4. globalThis:统一获取全局对象(跨环境兼容)

背景:不同环境全局对象名称不同

环境全局对象
浏览器window
Web Workerself
Node.jsglobal
DenoglobalThis

过去需写兼容代码:

// 丑陋的兼容写法
const getGlobal = () => {
  if (typeof window !== 'undefined') return window;
  if (typeof global !== 'undefined') return global;
  if (typeof self !== 'undefined') return self;
};

🔍 globalThis 的核心优势

  • 单一标准属性,在所有 JavaScript 环境中可用
  • 指向当前环境的全局对象
// 任何环境下都安全
globalThis.myApp = { version: '1.0' };

// 在浏览器中:
console.log(globalThis === window); // true

// 在 Node.js 中:
console.log(globalThis === global); // true

// 用于 polyfill 或库开发
if (!globalThis.Promise) {
  // 提供 Promise polyfill
}

💡 使用场景

  • 编写跨平台库(如 Lodash、Axios)
  • 全局状态管理(谨慎使用)
  • 检测环境特性(如 globalThis.crypto

ES6–ES11 极简总结

  • ES6(2015) :现代化基石
    let/const、箭头函数、模块、Promise、Class、解构、模板字符串 —— 奠定现代 JS 开发范式。
  • ES7(2016) :小而实用
    includes()(安全查数组)、**(幂运算)—— 提升语义清晰度。
  • ES8(2017) :异步革命
    async/await 让异步如同步;Object.entries/valuespadStart 简化对象与字符串操作。
  • ES9(2018) :对象与 Promise 完善
    对象展开/剩余({...obj})、Promise.finally() —— 操作更对称,收尾更统一。
  • ES10(2019) :工具补齐
    flat()/flatMap() 扁平数组,Object.fromEntries() 逆转 entries,trimStart/End 标准化。
  • ES11(2020) :安全与跨平台
    ?.(可选链)、??(空值合并)防崩溃;BigInt 支持大整数;globalThis 统一全局对象。