返回首页

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提供了更强大的数据处理能力,特别适合需要唯一性、快速查找和键值对管理的场景。


下一步学习设计模式