ES6+ 必备特性复习:解构、展开运算符、Symbol、Proxy

这四个特性在日常开发中出场率极高,但很多人只是"会用",没真正理解背后的设计意图。今天一次性梳理清楚。


前言

ES6(ECMAScript 2015)是 JavaScript 历史上最大的一次更新,后面每年都有新特性陆续放出,我们统称 ES6+。

今天复习四个出场率极高的特性:解构赋值、展开运算符、Symbol、Proxy。它们不是什么花哨的语法糖,而是能真正改变你写代码方式的东西。

我尽量用最少的代码把每个特性讲透,顺便聊聊实际开发中哪些场景最好用。


一、解构赋值:优雅地"拆包"

1.1 本质是什么?

解构赋值就是从数组或对象中提取值,赋给变量。

以前我们取值是这样的:

javascript 复制代码
let obj = { name: 'Alice', age: 25, city: 'Beijing' };
let name = obj.name;
let age = obj.age;
let city = obj.city;

有了解构,一行搞定:

javascript 复制代码
let { name, age, city } = obj;

就这么简单。但别小看它,解构的玩法非常多。

1.2 对象解构

基本用法

javascript 复制代码
let { name, age } = { name: 'Alice', age: 25 };
console.log(name); // 'Alice'
console.log(age);  // 25

重命名

实际开发中,后端返回的字段名经常跟前端不一致,这时候重命名就派上用场了:

javascript 复制代码
let { name: userName, age: userAge } = { name: 'Alice', age: 25 };
console.log(userName); // 'Alice'
console.log(userAge);  // 25
// 注意:name 和 age 在这里不存在,变量名是 userName 和 userAge

默认值

javascript 复制代码
let { name, role = 'user' } = { name: 'Bob' };
console.log(role); // 'user' ------ 没有role属性,使用默认值

这个在函数参数中特别好用,后面会讲到。

嵌套解构

javascript 复制代码
let user = {
    name: 'Alice',
    address: {
        city: 'Beijing',
        district: 'Haidian'
    }
};

let { address: { city, district } } = user;
console.log(city);     // 'Beijing'
console.log(district); // 'Haidian'

注意address 这一层只是用来定位的,并没有创建 address 变量。如果你也想拿到 address,得这样写:

javascript 复制代码
let { address, address: { city } } = user;

1.3 数组解构

数组解构是按位置取值,不是按属性名:

javascript 复制代码
let [a, b, c] = [1, 2, 3];
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

跳过某些元素

javascript 复制代码
let [first, , third] = [1, 2, 3];
console.log(first); // 1
console.log(third); // 3

交换变量

这个用法我经常用,比声明临时变量优雅多了:

javascript 复制代码
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x); // 2
console.log(y); // 1

配合 rest 参数

javascript 复制代码
let [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]

1.4 函数参数解构

这个在实际开发中出场率极高,强烈建议掌握:

javascript 复制代码
// 以前这么写
function createUser(options) {
    let name = options.name || 'unknown';
    let age = options.age || 0;
    let role = options.role || 'user';
}

// 现在这么写
function createUser({ name = 'unknown', age = 0, role = 'user' } = {}) {
    console.log(name, age, role);
}

createUser({ name: 'Alice', age: 25 });
// 'Alice' 25 'user'

注意参数后面的 = {},这是为了防止调用时不传参数导致解构报错。

1.5 常见坑

对象解构对基本类型无效

javascript 复制代码
let { length } = null;      // TypeError!
let { length } = undefined; // TypeError!

解构 nullundefined 会报错,因为它们不能被当作对象来解构。实际开发中可以给个默认值:

javascript 复制代码
let { length } = null || {}; // 安全

已声明的变量解构需要加括号

javascript 复制代码
let x;
{x} = { x: 1}; // SyntaxError!

// 加括号就行
let x;
({ x } = { x: 1 });
console.log(x); // 1

这是因为 {} 在行首会被JS引擎当作代码块,不是对象字面量。


二、展开运算符:一个点搞定很多事

2.1 本质是什么?

展开运算符(...)把一个可迭代对象"展开"成一个个独立的元素。

javascript 复制代码
let arr1 = [1, 2, 3];
let arr2 = [...arr1, 4, 5];
console.log(arr2); // [1, 2, 3, 4, 5]

就这么简单。但它能做的事情远不止于此。

2.2 数组相关

合并数组

javascript 复制代码
let a = [1, 2];
let b = [3, 4];
let c = [...a, ...b]; // [1, 2, 3, 4]

concat() 直观多了。

数组拷贝(浅拷贝)

javascript 复制代码
let original = [1, 2, 3];
let copy = [...original];

copy.push(4);
console.log(original); // [1, 2, 3] ------ 不受影响

注意 :这是浅拷贝。如果数组里有对象,拷贝的只是引用:

javascript 复制代码
let original = [{ name: 'Alice' }];
let copy = [...original];

copy[0].name = 'Bob';
console.log(original[0].name); // 'Bob' ------ 被改了!

把类数组转成真正的数组

javascript 复制代码
function foo() {
    let args = [...arguments]; // 把arguments转成数组
    console.log(args); // [1, 2, 3]
}
foo(1, 2, 3);

// NodeList转数组
let nodes = [...document.querySelectorAll('div')];

字符串转数组

javascript 复制代码
let chars = [...'hello'];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']

这个比 split('') 好在它能正确处理 Unicode 字符:

javascript 复制代码
[...'😄'].length;     // 1 ✅
'😄'.split('').length; // 2 ❌(把emoji拆成了两个字符)

2.3 对象相关

合并对象

javascript 复制代码
let defaults = { theme: 'light', lang: 'zh', pageSize: 10 };
let userConfig = { theme: 'dark', fontSize: 14 };

let config = { ...defaults, ...userConfig };
console.log(config);
// { theme: 'dark', lang: 'zh', pageSize: 10, fontSize: 14 }

后面的属性会覆盖前面的,这个在合并配置项时特别好用。

对象浅拷贝

javascript 复制代码
let original = { name: 'Alice', address: { city: 'Beijing' } };
let copy = { ...original };

copy.name = 'Bob';
console.log(original.name); // 'Alice' ------ 不受影响

copy.address.city = 'Shanghai';
console.log(original.address.city); // 'Shanghai' ------ 浅拷贝的坑!

剔除某些属性

javascript 复制代码
let { password, ...safeUser } = { name: 'Alice', age: 25, password: '123456' };
console.log(safeUser); // { name: 'Alice', age: 25 }

这个技巧在把用户数据传给前端时特别实用,避免敏感信息泄露。

2.4 展开运算符 vs 剩余参数

它们用的都是 ...,但场景不同:

场景 名称 作用
[...arr] 展开运算符(Spread) 把数组/对象"拆开"
function(...args) 剩余参数(Rest) 把散落的元素"收集"成数组
javascript 复制代码
// 展开:拆开
let arr = [1, 2, 3];
console.log(...arr); // 1 2 3

// 剩余:收集
function sum(...nums) {
    return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3); // 6

一个"拆",一个"收",方向相反。

2.5 实际开发中的高频用法

javascript 复制代码
// 1. React/Vue 中合并 props
<Component {...defaultProps} {...customProps} />

// 2. Redux 中合并 state
return { ...state, loading: true };

// 3. 删除数组某个元素(不可变方式)
let items = [1, 2, 3, 4];
let filtered = items.filter(item => item !== 3); // [1, 2, 4]

// 4. 更新数组某个元素(不可变方式)
let list = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
let updated = list.map(item =>
    item.id === 1 ? { ...item, name: 'AA' } : item
);

三、Symbol:JavaScript 的"隐藏属性"

3.1 本质是什么?

Symbol 是 ES6 引入的一种新的原始数据类型,表示独一无二的值。

javascript 复制代码
let s1 = Symbol();
let s2 = Symbol();
console.log(s1 === s2); // false ------ 每个Symbol都是唯一的

即使描述相同,也不相等:

javascript 复制代码
let s1 = Symbol('foo');
let s2 = Symbol('foo');
console.log(s1 === s2); // false

描述只是用来调试的,不影响唯一性。

3.2 为什么需要 Symbol?

你可能会问,已经有了 string 做属性名,为什么还要搞个 Symbol?

核心原因:Symbol 属性不会被常规方式遍历到。

javascript 复制代码
let obj = {};
let key = Symbol('secret');

obj[key] = '隐藏的值';
obj.name = 'Alice';

Object.keys(obj);    // ['name'] ------ 拿不到Symbol属性
for (let k in obj) { /* 只会输出 name */ }
JSON.stringify(obj); // '{"name":"Alice"}' ------ Symbol属性被忽略了

这就像给对象加了一个"暗格",只有拿到对应的 Symbol key 才能访问。

3.3 全局 Symbol

有时候你可能想在不同的地方使用"同一个" Symbol,可以用 Symbol.for()

javascript 复制代码
let s1 = Symbol.for('shared');
let s2 = Symbol.for('shared');
console.log(s1 === s2); // true ------ 从全局注册表中取的

Symbol.for() 会在全局注册表中查找,有就返回已有的,没有就创建新的。

javascript 复制代码
// Symbol.keyFor() 可以反向查找
let key = Symbol.keyFor(s1);
console.log(key); // 'shared'

3.4 内置 Symbol(Well-known Symbols)

JavaScript 内置了一些 Symbol,用来定制对象的行为。这是 Symbol 最强大的用法。

Symbol.iterator ------ 自定义迭代行为

javascript 复制代码
let range = {
    from: 1,
    to: 5,
    [Symbol.iterator]() {
        let current = this.from;
        let last = this.to;
        return {
            next() {
                return current <= last
                    ? { value: current++, done: false }
                    : { done: true };
            }
        };
    }
};

// 现在可以用 for...of 遍历了
for (let num of range) {
    console.log(num); // 1, 2, 3, 4, 5
}

// 也可以用展开运算符
console.log([...range]); // [1, 2, 3, 4, 5]

实现了 Symbol.iterator,你的对象就"可迭代"了,能被 for...of、展开运算符、解构等使用。

Symbol.toPrimitive ------ 自定义类型转换

javascript 复制代码
let money = {
    amount: 100,
    currency: 'CNY',
    [Symbol.toPrimitive](hint) {
        if (hint === 'number') return this.amount;
        if (hint === 'string') return `${this.amount}${this.currency}`;
        return `${this.amount}${this.currency}`;
    }
};

console.log(Number(money));   // 100
console.log(String(money));   // "100CNY"
console.log(money + '');      // "100CNY"

其他常用内置 Symbol

Symbol 用途
Symbol.toStringTag 自定义 Object.prototype.toString.call() 的返回值
Symbol.hasInstance 自定义 instanceof 的行为
Symbol.toPrimitive 自定义类型转换
Symbol.iterator 自定义迭代行为
javascript 复制代码
// 自定义 toStringTag
class MyClass {
    get [Symbol.toStringTag]() {
        return 'MyClass';
    }
}
Object.prototype.toString.call(new MyClass()); // "[object MyClass]"

3.5 实际应用场景

场景一:防止属性名冲突

在开发库或框架时,给用户对象添加属性,但又不想跟用户自己的属性冲突:

javascript 复制代码
// 库内部使用
const INTERNAL_KEY = Symbol('internal');

function process(obj) {
    obj[INTERNAL_KEY] = { processed: true };
    // 用户遍历对象时不会看到这个属性
}

场景二:定义常量

javascript 复制代码
const STATUS = {
    PENDING: Symbol('pending'),
    FULFILLED: Symbol('fulfilled'),
    REJECTED: Symbol('rejected')
};

// 比用字符串更安全,不用担心值重复
if (status === STATUS.PENDING) { /* ... */ }

场景三:枚举

javascript 复制代码
const Direction = {
    UP: Symbol('UP'),
    DOWN: Symbol('DOWN'),
    LEFT: Symbol('LEFT'),
    RIGHT: Symbol('RIGHT')
};

四、Proxy:JavaScript 的"拦截器"

4.1 本质是什么?

Proxy 可以拦截并自定义对象的基本操作(读取、赋值、函数调用等)。

javascript 复制代码
let obj = { name: 'Alice', age: 25 };

let proxy = new Proxy(obj, {
    get(target, key) {
        console.log(`读取了 ${key} 属性`);
        return target[key];
    },
    set(target, key, value) {
        console.log(`设置了 ${key} = ${value}`);
        target[key] = value;
        return true;
    }
});

proxy.name;       // 控制台:读取了 name 属性 → 'Alice'
proxy.age = 26;   // 控制台:设置了 age = 26

Proxy 就像给对象套了一层"壳",所有操作都要经过这层壳。

4.2 常用拦截器(Trap)

Proxy 支持的拦截器非常多,这里列出最常用的几个:

拦截器 触发时机
get 读取属性
set 设置属性
has in 操作符
deleteProperty delete 操作
apply 函数调用
construct new 操作
getPrototypeOf Object.getPrototypeOf()
ownKeys Object.keys() 等遍历操作

4.3 实战场景

场景一:数据校验

javascript 复制代码
function createValidator(target, rules) {
    return new Proxy(target, {
        set(obj, key, value) {
            if (rules[key] && !rules[key](value)) {
                throw new TypeError(`${key} 的值不合法: ${value}`);
            }
            obj[key] = value;
            return true;
        }
    });
}

let user = createValidator({}, {
    name: v => typeof v === 'string' && v.length > 0,
    age: v => typeof v === 'number' && v > 0 && v < 150
});

user.name = 'Alice'; // ✅
user.age = 25;       // ✅
user.age = -1;       // TypeError: age 的值不合法: -1

这个在表单处理中特别实用。

场景二:实现响应式(简化版 Vue3 原理)

javascript 复制代码
function reactive(target, callback) {
    return new Proxy(target, {
        set(obj, key, value) {
            let oldValue = obj[key];
            obj[key] = value;
            if (oldValue !== value) {
                callback(key, value, oldValue);
            }
            return true;
        }
    });
}

let state = reactive({ count: 0 }, (key, value, oldValue) => {
    console.log(`${key} 从 ${oldValue} 变成了 ${value}`);
    // 这里可以触发视图更新
});

state.count = 1; // "count 从 0 变成了 1"
state.count = 2; // "count 从 1 变成了 2"

Vue3 的响应式系统就是基于 Proxy 实现的(当然比这个复杂得多)。

场景三:优雅地处理属性不存在的情况

javascript 复制代码
let data = { name: 'Alice' };

let proxy = new Proxy(data, {
    get(target, key) {
        if (key in target) {
            return target[key];
        }
        console.warn(`属性 "${key}" 不存在`);
        return undefined;
    }
});

proxy.name;   // 'Alice'
proxy.age;    // 控制台:属性 "age" 不存在 → undefined

场景四:函数调用拦截(apply)

javascript 复制代码
function sum(a, b) {
    return a + b;
}

let proxy = new Proxy(sum, {
    apply(target, thisArg, args) {
        console.log(`调用了 sum(${args.join(', ')})`);
        let result = target.apply(thisArg, args);
        console.log(`结果: ${result}`);
        return result;
    }
});

proxy(1, 2);
// "调用了 sum(1, 2)"
// "结果: 3"

场景五:只读对象

javascript 复制代码
function readonly(obj) {
    return new Proxy(obj, {
        set() {
            throw new TypeError('这个对象是只读的');
        },
        deleteProperty() {
            throw new TypeError('不能删除属性');
        }
    });
}

let config = readonly({ theme: 'dark', lang: 'zh' });
config.theme = 'light'; // TypeError: 这个对象是只读的

4.4 Proxy vs Object.defineProperty

面试经常问这两个的区别:

对比项 Proxy Object.defineProperty
拦截范围 13种操作,全面 只有 get/set
数组支持 ✅ 原生支持 ❌ 需要重写数组方法
动态新增属性 ✅ 自动拦截 ❌ 需要手动 Observe
嵌套对象 递归代理即可 需要深度遍历
性能 整体代理,懒处理 初始化时就要遍历所有属性

这也是 Vue3 从 Object.defineProperty 切换到 Proxy 的原因。

4.5 注意事项

Proxy 的 this 指向问题

javascript 复制代码
let target = {
    m() {
        console.log(this === proxy); // true
    }
};

let proxy = new Proxy(target, {});
proxy.m(); // this 指向 proxy,不是 target!

如果你在 get 拦截器里返回方法,要注意 this 的指向。可以用 Reflect.get() 来保持正确的行为。

Proxy 不能代理基本类型

javascript 复制代码
new Proxy(1, {}); // TypeError

Proxy 只能代理对象。如果需要代理基本类型,可以包装成对象。


五、总结速查表

解构赋值

用法 示例
对象解构 let { name, age } = obj
重命名 let { name: userName } = obj
默认值 let { role = 'user' } = obj
嵌套解构 let { a: { b } } = obj
数组解构 let [a, b] = arr
交换变量 [x, y] = [y, x]
函数参数 function fn({ a, b } = {})

展开运算符

用法 示例
合并数组 [...a, ...b]
合并对象 { ...defaults, ...config }
浅拷贝 [...arr] / { ...obj }
剔除属性 let { pwd, ...rest } = obj
类数组转数组 [...arguments] / [...nodeList]

Symbol

用法 说明
Symbol() 创建唯一值
Symbol.for() 全局注册 Symbol
Symbol.iterator 自定义迭代行为
Symbol.toPrimitive 自定义类型转换
Symbol.toStringTag 自定义 toString 标签

Proxy

拦截器 用途
get 属性读取拦截
set 属性设置拦截
apply 函数调用拦截
has in 操作拦截
deleteProperty delete 操作拦截

写在最后

这四个特性在日常开发中出场率极高,建议每个都动手写几遍,比看十遍文章都有用。

特别是 Proxy,理解了它就理解了 Vue3 响应式的核心原理,面试的时候也能讲出深度。

有问题欢迎评论区交流

相关推荐
半兽先生1 小时前
vue高性能下拉组件 支持上万数据不卡顿
前端·javascript·vue.js
invicinble1 小时前
前端框架使用vue-cli( 第二层:工程配置层--路由页面配置)
javascript·vue.js·前端框架
四岁爱上了她1 小时前
自定义标签切换动画
javascript·css·css3
坤盾科技2 小时前
Docker 离线地图服务器搭建实战:Node.js + OpenLayers + MBTiles
linux·javascript·arcgis·docker·node.js
念你那丝微笑2 小时前
2026年Vue前端面试准备
前端·vue.js·面试
Copy_Paste_Coder2 小时前
小程序失败后,换个方向,终于成功搞到收益
前端·javascript·后端
Purple Coder2 小时前
储能项目一操作记录
面试
im_AMBER2 小时前
Browser Agent 开发:从浏览器插件到Electron CDP
前端·javascript·架构·electron·agent
逻辑驱动的ken2 小时前
Java高频面试考点场景题27
java·开发语言·面试·职场和发展·求职招聘