03-Symbol类型与SetMap
分类:02-ES6+现代特性
发布于:
阅读时间:113 分钟
Symbol类型与Set/Map
📋 学习目标
- 掌握Symbol的基本概念和创建方式
- 理解Symbol的唯一性和不可变性
- 学会使用Set集合进行唯一值管理
- 掌握Map映射的键值对操作
- 了解WeakSet和WeakMap的弱引用特性
- 学会这些数据结构的实际应用场景
🔤 Symbol类型
1. Symbol基础概念
// Symbol是JavaScript的第七种原始数据类型
// 它表示一个唯一的、不可变的值
// 创建Symbol
const sym1 = Symbol();
const sym2 = Symbol('description'); // 带描述的Symbol
const sym3 = Symbol('description');
console.log(sym1 =<mark> sym2); // false (每个Symbol都是唯一的)
console.log(sym2 </mark>= sym3); // false (即使描述相同,也是不同的Symbol)
// Symbol的描述
console.log(sym2.description); // 'description'
console.log(sym2.toString()); // 'Symbol(description)'
// Symbol不能使用new关键字
// const badSym = new Symbol(); // TypeError: Symbol is not a constructor
2. Symbol作为对象属性
// Symbol作为对象属性键
const obj = {};
const nameSymbol = Symbol('name');
const ageSymbol = Symbol('age');
obj[nameSymbol] = 'Alice';
obj[ageSymbol] = 25;
console.log(obj[nameSymbol]); // 'Alice'
console.log(obj[ageSymbol]); // 25
// Symbol属性不会出现在for...in循环中
for (const key in obj) {
console.log(key); // 不会输出Symbol属性
}
// Object.keys也不会返回Symbol属性
console.log(Object.keys(obj)); // []
// 获取Symbol属性的方法
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(name), Symbol(age)]
// Reflect.ownKeys可以获取所有类型的属性键
console.log(Reflect.ownKeys(obj)); // [Symbol(name), Symbol(age)]
// 使用Symbol作为私有属性
class Person {
constructor(name) {
this._name = Symbol('name');
this[this._name] = name;
}
getName() {
return this[this._name];
}
}
const person = new Person('Bob');
console.log(person.getName()); // 'Bob'
// console.log(person[Symbol('name')]); // 无法直接访问
3. 全局Symbol注册表
// Symbol.for() - 在全局注册表中创建或获取Symbol
const globalSym1 = Symbol.for('global');
const globalSym2 = Symbol.for('global');
console.log(globalSym1 === globalSym2); // true (相同的描述返回同一个Symbol)
// Symbol.keyFor() - 从Symbol获取描述
console.log(Symbol.keyFor(globalSym1)); // 'global'
// 本地Symbol不会被注册
const localSym = Symbol('local');
console.log(Symbol.keyFor(localSym)); // undefined
// 实际应用:创建共享常量
const EVENTS = {
USER_LOGIN: Symbol.for('user.login'),
USER_LOGOUT: Symbol.for('user.logout'),
DATA_LOADED: Symbol.for('data.loaded')
};
function emitEvent(eventType, data) {
console.log(`Event: ${Symbol.keyFor(eventType)}`, data);
}
emitEvent(EVENTS.USER_LOGIN, { userId: 1 });
// Event: user.login { userId: 1 }
4. 内置Symbol
// Symbol.iterator - 定义对象的迭代器
const myObject = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
const data = this.data;
return {
next() {
if (index < data.length) {
return { value: data[index++], done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};
for (const value of myObject) {
console.log(value); // 1, 2, 3
}
// Symbol.toStringTag - 自定义对象标签
class MyClass {
constructor(name) {
this.name = name;
}
get [Symbol.toStringTag]() {
return 'MyClass';
}
}
const instance = new MyClass('test');
console.log(instance.toString()); // [object MyClass]
// Symbol.hasInstance - 自定义instanceof行为
class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
console.log([] instanceof MyArray); // true
console.log({} instanceof MyArray); // false
// Symbol.species - 指定创建衍生对象时的构造函数
class MyCollection extends Array {
static get [Symbol.species]() {
return Array;
}
}
const collection = new MyCollection(1, 2, 3);
const derived = collection.map(x => x * 2);
console.log(derived instanceof Array); // true
console.log(derived instanceof MyCollection); // false
🏗️ Set集合
1. Set基础操作
// 创建Set
const set1 = new Set();
const set2 = new Set([1, 2, 3, 4, 5]);
const set3 = new Set('hello'); // Set {'h', 'e', 'l', 'l', 'o'}
// 基本操作
const fruits = new Set();
fruits.add('apple');
fruits.add('banana');
fruits.add('orange');
fruits.add('apple'); // 重复添加无效
console.log(fruits); // Set {'apple', 'banana', 'orange'}
console.log(fruits.has('banana')); // true
console.log(fruits.has('grape')); // false
fruits.delete('banana');
console.log(fruits.has('banana')); // false
console.log(fruits.size); // 2
fruits.clear();
console.log(fruits.size); // 0
2. Set的遍历
const numbers = new Set([1, 2, 3, 4, 5]);
// for...of遍历
for (const num of numbers) {
console.log(num);
}
// forEach遍历
numbers.forEach((value, key, set) => {
console.log(`${key}: ${value}`);
});
// keys() - 获取键的迭代器
for (const key of numbers.keys()) {
console.log(key); // 0, 1, 2, 3, 4
}
// values() - 获取值的迭代器
for (const value of numbers.values()) {
console.log(value); // 1, 2, 3, 4, 5
}
// entries() - 获取键值对的迭代器
for (const [key, value] of numbers.entries()) {
console.log(`${key}: ${value}`); // 0: 1, 1: 2, ...
}
3. Set与数组转换
// 数组转Set
const arrayWithDuplicates = [1, 2, 2, 3, 4, 4, 5];
const uniqueSet = new Set(arrayWithDuplicates);
console.log(uniqueSet); // Set {1, 2, 3, 4, 5}
// Set转数组
const uniqueArray = [...uniqueSet];
const uniqueArray2 = Array.from(uniqueSet);
console.log(uniqueArray); // [1, 2, 3, 4, 5]
console.log(uniqueArray2); // [1, 2, 3, 4, 5]
// 实际应用:数组去重
function removeDuplicates(arr) {
return [...new Set(arr)];
}
console.log(removeDuplicates([1, 2, 2, 3, 4, 4, 5])); // [1, 2, 3, 4, 5]
4. Set的集合操作
// 交集
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log(intersection); // Set {3, 4}
// 并集
const union = new Set([...setA, ...setB]);
console.log(union); // Set {1, 2, 3, 4, 5, 6}
// 差集 (A - B)
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log(difference); // Set {1, 2}
// 对称差集
const symDifference = new Set([
...[...setA].filter(x => !setB.has(x)),
...[...setB].filter(x => !setA.has(x))
]);
console.log(symDifference); // Set {1, 2, 5, 6}
5. Set的实际应用
// 应用1:标签管理
class TabManager {
constructor() {
this.tabs = new Set();
this.activeTab = null;
}
addTab(tabId) {
this.tabs.add(tabId);
if (this.activeTab === null) {
this.activeTab = tabId;
}
console.log(`Tab ${tabId} added. Active: ${this.activeTab}`);
}
removeTab(tabId) {
this.tabs.delete(tabId);
if (this.activeTab === tabId) {
this.activeTab = this.tabs.size > 0 ? this.tabs.values().next().value : null;
}
console.log(`Tab ${tabId} removed. Active: ${this.activeTab}`);
}
setActiveTab(tabId) {
if (this.tabs.has(tabId)) {
this.activeTab = tabId;
console.log(`Active tab changed to ${tabId}`);
}
}
}
// 应用2:权限管理
class PermissionManager {
constructor() {
this.permissions = new Set();
}
grantPermission(permission) {
this.permissions.add(permission);
console.log(`Granted permission: ${permission}`);
}
revokePermission(permission) {
this.permissions.delete(permission);
console.log(`Revoked permission: ${permission}`);
}
hasPermission(permission) {
return this.permissions.has(permission);
}
hasAllPermissions(permissions) {
return permissions.every(p => this.permissions.has(p));
}
}
// 应用3:缓存去重
class Cache {
constructor(maxSize = 100) {
this.cache = new Map();
this.maxSize = maxSize;
}
get(key) {
if (this.cache.has(key)) {
const value = this.cache.get(key);
// LRU: 移到最后
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
return null;
}
set(key, value) {
if (this.cache.size >= this.maxSize) {
// 删除最旧的
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}
🗺️ Map映射
1. Map基础操作
// 创建Map
const map1 = new Map();
const map2 = new Map([
['name', 'Alice'],
['age', 25],
['city', 'New York']
]);
// 基本操作
map1.set('key1', 'value1');
map1.set('key2', 'value2');
map1.set(1, 'number key');
map1.set({}, 'object key');
console.log(map1.get('key1')); // 'value1'
console.log(map1.get('key2')); // 'value2'
console.log(map1.has('key1')); // true
console.log(map1.has('key3')); // false
map1.delete('key1');
console.log(map1.has('key1')); // false
console.log(map1.size); // 3
map1.clear();
console.log(map1.size); // 0
2. Map的遍历
const userMap = new Map([
['name', 'Alice'],
['age', 25],
['city', 'New York'],
['hobbies', ['reading', 'coding', 'music']]
]);
// 遍历键
for (const key of userMap.keys()) {
console.log(key); // name, age, city, hobbies
}
// 遍历值
for (const value of userMap.values()) {
console.log(value); // Alice, 25, New York, ['reading', 'coding', 'music']
}
// 遍历键值对
for (const [key, value] of userMap.entries()) {
console.log(`${key}: ${value}`);
}
// forEach
userMap.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
3. Map与Object的对比
// Map vs Object的特点对比
const map = new Map();
const obj = {};
// 1. 键的类型
map.set(1, 'number key');
map.set('1', 'string key');
map.set({}, 'object key');
map.set([], 'array key');
obj[1] = 'number key';
obj['1'] = 'string key';
// obj[{}] 和 obj[[]] 都会被转换为字符串
// 2. 键的数量
console.log(map.size); // 4
console.log(Object.keys(obj).length); // 2
// 3. 顺序
map.set('a', 1);
map.set('b', 2);
map.set('c', 3);
for (const [key, value] of map) {
console.log(key, value); // 'a' 1, 'b' 2, 'c' 3 (插入顺序)
}
// 4. 性能
// Map在频繁增删操作时性能更好
const performanceTest = () => {
const iterations = 100000;
// Map性能测试
const testMap = new Map();
console.time('Map');
for (let i = 0; i < iterations; i++) {
testMap.set(i, i);
}
for (let i = 0; i < iterations; i++) {
testMap.get(i);
}
console.timeEnd('Map');
// Object性能测试
const testObj = {};
console.time('Object');
for (let i = 0; i < iterations; i++) {
testObj[i] = i;
}
for (let i = 0; i < iterations; i++) {
testObj[i];
}
console.timeEnd('Object');
};
performanceTest();
4. Map的实际应用
// 应用1:缓存系统
class CacheMap {
constructor() {
this.cache = new Map();
}
set(key, value) {
this.cache.set(key, {
value,
timestamp: Date.now(),
hits: 0
});
}
get(key) {
if (this.cache.has(key)) {
const item = this.cache.get(key);
item.hits++;
return item.value;
}
return null;
}
getStats(key) {
return this.cache.get(key) || null;
}
cleanup(maxAge = 3600000) { // 1小时
const now = Date.now();
for (const [key, item] of this.cache) {
if (now - item.timestamp > maxAge) {
this.cache.delete(key);
}
}
}
}
// 应用2:状态管理
class StateManager {
constructor(initialState = {}) {
this.state = new Map(Object.entries(initialState));
this.listeners = new Map();
}
setState(key, value) {
const oldValue = this.state.get(key);
this.state.set(key, value);
// 通知监听器
if (this.listeners.has(key)) {
this.listeners.get(key).forEach(listener => {
listener(value, oldValue);
});
}
}
getState(key) {
return this.state.get(key);
}
subscribe(key, listener) {
if (!this.listeners.has(key)) {
this.listeners.set(key, []);
}
this.listeners.get(key).push(listener);
// 返回取消订阅的函数
return () => {
const listeners = this.listeners.get(key);
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
};
}
}
// 应用3:路由管理
class Router {
constructor() {
this.routes = new Map();
}
addRoute(path, handler) {
this.routes.set(path, handler);
}
navigate(path) {
if (this.routes.has(path)) {
const handler = this.routes.get(path);
handler();
} else {
console.log(`Route not found: ${path}`);
}
}
hasRoute(path) {
return this.routes.has(path);
}
}
🔗 WeakSet和WeakMap
1. WeakSet
// WeakSet只能存储对象,不能存储原始值
const weakSet = new WeakSet();
const obj1 = { id: 1 };
const obj2 = { id: 2 };
weakSet.add(obj1);
weakSet.add(obj2);
console.log(weakSet.has(obj1)); // true
console.log(weakSet.has(obj2)); // true
weakSet.delete(obj1);
console.log(weakSet.has(obj1)); // false
// ❌ 不能存储原始值
// weakSet.add(1); // TypeError: Invalid value used in weak set
// WeakSet不能遍历
// for (const item of weakSet) {} // TypeError: weakSet is not iterable
// WeakSet的应用:跟踪活动对象
class ObjectTracker {
constructor() {
this.tracked = new WeakSet();
this.count = 0;
}
track(obj) {
if (!this.tracked.has(obj)) {
this.tracked.add(obj);
this.count++;
console.log(`Tracking object ${this.count}`);
}
}
untrack(obj) {
if (this.tracked.has(obj)) {
this.tracked.delete(obj);
this.count--;
console.log(`Untracking object. Count: ${this.count}`);
}
}
getCount() {
return this.count;
}
}
2. WeakMap
// WeakMap的键必须是对象,值可以是任意类型
const weakMap = new WeakMap();
const key1 = { id: 1 };
const key2 = { id: 2 };
weakMap.set(key1, 'value1');
weakMap.set(key2, 'value2');
console.log(weakMap.get(key1)); // 'value1'
console.log(weakMap.get(key2)); // 'value2'
// WeakMap不能遍历
console.log(weakMap.size); // undefined
// WeakMap的应用:私有数据存储
class PrivateData {
constructor() {
this.data = new WeakMap();
}
set(obj, data) {
this.data.set(obj, data);
}
get(obj) {
return this.data.get(obj);
}
has(obj) {
return this.data.has(obj);
}
delete(obj) {
return this.data.delete(obj);
}
}
class User {
constructor(name) {
this.name = name;
this._privateData = new PrivateData();
}
setSecretData(data) {
this._privateData.set(this, data);
}
getSecretData() {
return this._privateData.get(this);
}
}
// WeakMap的应用:避免内存泄漏
class EventHandler {
constructor() {
this.handlers = new WeakMap();
}
addHandler(element, handler) {
if (!this.handlers.has(element)) {
this.handlers.set(element, []);
}
this.handlers.get(element).push(handler);
}
removeHandler(element, handler) {
if (this.handlers.has(element)) {
const handlers = this.handlers.get(element);
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
}
trigger(element, event) {
if (this.handlers.has(element)) {
this.handlers.get(element).forEach(handler => {
handler(event);
});
}
}
}
3. 弱引用的实际意义
// 演示垃圾回收
function demonstrateGC() {
const weakMap = new WeakMap();
const objects = [];
for (let i = 0; i < 10; i++) {
const obj = { id: i };
objects.push(obj);
weakMap.set(obj, `Object ${i}`);
}
console.log('Before cleanup:', weakMap.size); // 10
// 删除所有对象的引用
objects.length = 0;
// 强制垃圾回收(如果可用)
if (global.gc) {
global.gc();
}
// 稍等一会儿让垃圾回收生效
setTimeout(() => {
console.log('After cleanup:', weakMap.size); // 0 (对象被回收了)
}, 100);
}
demonstrateGC();
// 内存管理最佳实践
class ResourceManager {
constructor() {
this.resources = new Map();
this.weakResources = new WeakMap();
}
addResource(id, resource) {
// 强引用:不会被垃圾回收
this.resources.set(id, resource);
}
addWeakResource(obj, resource) {
// 弱引用:当对象被回收时,资源也会被清理
this.weakResources.set(obj, resource);
}
getResource(id) {
return this.resources.get(id);
}
getWeakResource(obj) {
return this.weakResources.get(obj);
}
cleanup() {
this.resources.clear();
// WeakMap会自动清理
}
}
🎯 高级应用技巧
1. 使用Symbol实现枚举
// Symbol枚举实现
const Directions = {
NORTH: Symbol('north'),
SOUTH: Symbol('south'),
EAST: Symbol('east'),
WEST: Symbol('west')
};
function getDirectionName(direction) {
switch (direction) {
case Directions.NORTH:
return 'North';
case Directions.SOUTH:
return 'South';
case Directions.EAST:
return 'East';
case Directions.WEST:
return 'West';
default:
return 'Unknown';
}
}
// 使用常量对象作为对比
const Status = Object.freeze({
PENDING: 'pending',
SUCCESS: 'success',
ERROR: 'error'
});
2. 使用Symbol实现私有属性
class PrivateProperty {
[#private](/tags/private) = Symbol('private');
constructor(value) {
this[this.#private] = value;
}
getValue() {
return this[this.#private];
}
setValue(value) {
this[this.#private] = value;
}
}
const instance = new PrivateProperty('secret');
console.log(instance.getValue()); // 'secret'
instance.setValue('new secret');
console.log(instance.getValue()); // 'new secret'
3. 使用Set实现高效的去重函数
// 多维数组去重
function deepUnique(arr) {
const seen = new Set();
const result = [];
function process(item) {
if (item =<mark> null || typeof item !</mark> 'object') {
if (!seen.has(item)) {
seen.add(item);
result.push(item);
}
} else {
const key = JSON.stringify(item);
if (!seen.has(key)) {
seen.add(key);
result.push(item);
} else {
// 检查是否是数组或对象
if (Array.isArray(item)) {
const processedArray = [];
let allUnique = true;
for (const subItem of item) {
const subProcessed = process(subItem);
if (subProcessed !== false) {
processedArray.push(subProcessed);
} else {
allUnique = false;
}
}
if (allUnique) {
result.push(processedArray);
}
} else {
// 对象去重更复杂,这里简化处理
result.push(item);
}
}
}
return true;
}
arr.forEach(process);
return result;
}
const nestedArray = [
1,
[2, 3],
[2, 3], // 重复
{ a: 1 },
{ a: 1 }, // 重复
null,
null, // 重复
'string',
'string' // 重复
];
console.log(deepUnique(nestedArray));
⚠️ 常见陷阱
1. Symbol的隐式转换
// ❌ 错误:Symbol会隐式转换为字符串
const sym = Symbol('test');
const obj = {};
obj[sym] = 'value';
console.log(obj[sym] =<mark> obj[sym]); // true
console.log(obj[sym] </mark>= 'value'); // false
console.log(typeof sym); // 'symbol'
console.log(typeof sym.toString()); // 'string'
// ✅ 注意Symbol的唯一性
const sym1 = Symbol('same');
const sym2 = Symbol('same');
console.log(sym1 === sym2); // false
2. Set的类型判断
// ❌ 错误:Set和Array不同
const set = new Set([1, 2, 3]);
const arr = [1, 2, 3];
console.log(set instanceof Array); // false
console.log(arr instanceof Set); // false
// ✅ 正确的类型检查
console.log(set instanceof Set); // true
console.log(Array.isArray(set)); // false
console.log(Array.isArray(arr)); // true
3. Map的键类型陷阱
// ❌ 错误:对象键会被转换为字符串
const obj = {};
const map = new Map();
obj[{a: 1}] = 'object key';
map.set({a: 1}, 'map value');
console.log(obj[{'a': 1}]); // 'object key'
console.log(map.get({a: 1})); // 'map value'
console.log({a: 1} === {'a': 1}); // false (不同对象引用)
📝 最佳实践
1. Symbol使用原则
// ✅ 1. 使用Symbol定义常量
const EVENTS = {
CLICK: Symbol('click'),
HOVER: Symbol('hover'),
FOCUS: Symbol('focus')
};
// ✅ 2. 使用Symbol避免命名冲突
const PRIVATE_KEYS = {
DATA: Symbol('data'),
CACHE: Symbol('cache'),
CONFIG: Symbol('config')
};
// ✅ 3. 使用Symbol.for()创建全局共享Symbol
const API_ENDPOINTS = {
USER: Symbol.for('api.user'),
PRODUCT: Symbol.for('api.product')
};
2. Set使用原则
// ✅ 1. 使用Set进行快速查找
const activeUsers = new Set();
function addUser(userId) {
activeUsers.add(userId);
}
function removeUser(userId) {
activeUsers.delete(userId);
}
function isActive(userId) {
return activeUsers.has(userId);
}
// ✅ 2. 使用Set进行去重
function getUniqueItems(items) {
return [...new Set(items)];
}
// ✅ 3. 使用Set进行集合操作
function intersection(setA, setB) {
return new Set([...setA].filter(x => setB.has(x)));
}
3. Map使用原则
// ✅ 1. 当键的类型多样时使用Map
const cache = new Map();
cache.set(1, 'number key');
cache.set('1', 'string key');
cache.set({}, 'object key');
// ✅ 2. 使用Map维护键值对关系
const userRoles = new Map();
userRoles.set('user1', 'admin');
userRoles.set('user2', 'editor');
// ✅ 3. 使用Map实现LRU缓存
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (this.cache.has(key)) {
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
return null;
}
set(key, value) {
if (this.cache.size >= this.capacity) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}
🎯 小结
- 掌握了Symbol的基本概念和创建方式
- 理解了Symbol的唯一性和不可变性
- 学会了使用Set集合进行唯一值管理
- 掌握了Map映射的键值对操作
- 了解了WeakSet和WeakMap的弱引用特性
- 学会了这些数据结构的实际应用场景
Symbol、Set、Map是ES6引入的重要数据结构,它们为JavaScript提供了更强大的数据处理能力,特别适合需要唯一性、快速查找和键值对管理的场景。
下一步学习:设计模式