一次性搞明白面试常问ES6的新特性Symbol

286 阅读6分钟

前言

在JavaScript的发展历程中,ES6引入了许多新的特性,其中的Symbol让我很是着迷,因为Symbol提供了一个新的原始数据(基本数据)类型,用于创建唯一的标识符,谁不喜欢专一的男人呢,一生只有一个唯一。这不仅让他特别适合用在对象属性键的应用上,避免了属性名冲突的问题。

简单介绍一下Symbol

前面我们已经说说了它是用来创建唯一的标识符,这是什么意思?意味着独一无二,绝无二家。我们来创建试一下.

创建Symbol

创建Symbol是通过调用 Symbol() 函数来实现的。Symbol() 是一个全局函数,用于生成一个新的、独一无二的 Symbol 值。它不是构造函数,因此不应该使用 new 操作符来调用它;如果这样做,会抛出一个错误。

const sym = Symbol();
console.log(sym)  // Symbol()

给Symbol添加描述

我们还可以给Symbol添加描述(Description),虽然 Symbol 的值是唯一的,但你可以通过传递一个字符串作为参数来为 Symbol 添加一个描述性的标签(label)。这个标签不会影响 Symbol 的唯一性,主要用于调试和日志输出。

const sym = Symbol('我是唯一的');
console.log(sym); // Symbol(我是唯一的)

最主要的是Symbol的唯一性

每次调用 Symbol() 都会生成一个全新的、唯一的符号,即使传递相同的描述,生成的 Symbol 也是不同的。

const sym1 = Symbol('key');
const sym2 = Symbol('key');
console.log(sym1 === sym2); // false

为什么我们需要Symbol

在ES6之前,对象的属性名都是字符串,这非常容易造成属性名的冲突,举个例子,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式,即混入,区别于继承,js特有的),新方法的名字就有可能与现有方法产生冲突。如果有一种新的机制,可以保证每个属性的名字都是独一无二的,唯一的就可以从根本上去解决属性名冲突问题,这就是为什么需要Symbol的原因

来个场景带大家理解一下,你有一个来自第三方库的对象 thirdPartyObj,并且你想给这个对象添加一个新的功能 printInfo。然而,你不希望破坏现有的功能,也不确定第三方库是否已经有了同名的方法。

不使用 Symbol 的情况

// 第三方提供的对象
let thirdPartyObj = {
    printInfo: function() {
        console.log('Third party info');
    }
};

// 我们想混入自己的功能
thirdPartyObj.printInfo = function() {
    console.log('Our own info');
};

// 调用 printInfo 方法
thirdPartyObj.printInfo(); // 输出 "Our own info"

在这个例子中,我们新添加的 printInfo 方法覆盖了原有的方法,这可能会破坏第三方库的预期行为。

使用 Symbol 的情况
// 第三方提供的对象
let thirdPartyObj = {
    printInfo: function() {
        console.log('Third party info');
    }
};

// 创建一个独一无二的 Symbol
const printInfoSymbol = Symbol('printInfo');

// 混入自己的功能,使用 Symbol 作为属性名
thirdPartyObj[printInfoSymbol] = function() {
    console.log('Our own info');
};

// 调用原有方法
thirdPartyObj.printInfo(); // 输出 "Third party info"

// 调用我们混入的方法
thirdPartyObj[printInfoSymbol](); // 输出 "Our own info"

在这个例子中,通过使用 Symbol 作为属性名,我们成功避免了与现有方法 printInfo 的冲突。原有的方法和新添加的方法都可以共存,不会互相干扰。

此外,由于 Symbol 是不可枚举的,除非你明确知道 Symbol 的引用,否则无法访问到这些属性,这也提供了一种形式上的私有化,有助于保护对象内部的状态不被外部代码轻易修改。

获取对象上的Symbol属性的门道

Symbol 作为对象属性键时,具有以下特点:

  • Object.keys()Object.values()Object.entries() 不包括 Symbol 类型的键:这些方法只返回对象的可枚举属性,而 Symbol 类型的键默认是不可枚举的,因此不会出现在这些方法的结果中。

    const obj = {
      [Symbol('id')]: 123,
      name: 'Alice'
    };
    
    console.log(Object.keys(obj)); // ['name']
    console.log(Object.values(obj)); // ['Alice']
    console.log(Object.entries(obj)); // [['name', 'Alice']]
    
  • Object.getOwnPropertySymbols() 可以获取对象上的所有 Symbol:如果你想访问对象上的 Symbol 属性,可以使用 Object.getOwnPropertySymbols() 方法。

    const obj = {
      [Symbol('id')]: 123,
      name: 'Alice'
    };
    
    console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(id)]
    
  • Reflect.ownKeys() 可以获取对象的所有键(包括 Symbol 键) :如果你需要获取对象的所有键,包括 Symbol 键和普通字符串键,可以使用 Reflect.ownKeys() 方法。

    const obj = {
      [Symbol('id')]: 123,
      name: 'Alice'
    };
    
    console.log(Reflect.ownKeys(obj)); // ['name', Symbol(id)]
    

Object.getOwnPropertyDescriptors() 可以查看对象的所有属性及其描述符: 虽然 symboles emeumberable 属性为true, 但是不可枚举.

const obj = {
  [Symbol('id')]: 123,
  name: 'Alice'
};

console.log(Object.getOwnPropertyDescriptors(obj));

image.png

Symbol 的动态属性名

在 JavaScript 中,属性名可以是静态的(如 obj.name)或动态的(如 obj[variable])。Symbol 作为属性名时,必须使用方括号 [] 语法来定义动态属性名。


const key = Symbol('id');
const obj = {
  [key]: 123
};

console.log(obj[key]); // 123

为什么 [Symbol("olivia")]: { grade: 50, gender: "female" } 里面不能没有 []?这是因为 Symbol 作为一种特殊的属性名,不能直接作为静态属性名使用。只有通过方括号 [] 语法,才能将 Symbol 作为动态属性名赋值给对象。

  • 静态属性名:指那些在编写代码时就已经明确指定的属性名
  • 动态属性名:动态属性名 是指在运行时通过变量、表达式或其他计算结果来确定的对象属性名

Symbol.for()

创建Symbo除了调用函数Symbol() 可以创建,还可以使用Symbol.for() ,我们需要弄明白他们之间的区别 Symbol()Symbol.for(key) 都是用于创建符号的工具,但它们的行为和适用场景有所不同。

  • Symbol() :每次调用 Symbol() 都会生成一个全新的、唯一的符号,即使传递相同的描述,生成的 Symbol 也是不同的。这种符号是局部的,仅在当前作用域内有效。

    
    const sym1 = Symbol('key');
    const sym2 = Symbol('key');
    console.log(sym1 === sym2); // false
    
  • Symbol.for(key)Symbol.for(key) 会从全局符号注册表中查找是否存在与给定 key 相关联的符号。如果存在,则返回该符号;如果不存在,则创建一个新的符号并将其存储在全局注册表中。因此,Symbol.for(key) 返回的符号是全局共享的,多次调用 Symbol.for('key') 会返回同一个符号。

    
    const sym1 = Symbol.for('key');
    const sym2 = Symbol.for('key');
    console.log(sym1 === sym2); // true
    
  • 适用场景

    • 使用 Symbol() 时,符号是局部的、唯一的,适合用于防止属性名冲突的场景。
    • 使用 Symbol.for(key) 时,符号是全局共享的,适合用于跨模块或跨文件共享符号的场景。

END

Symbol 是 ES6 引入的一个非常有用的新特性,它不仅提供了唯一的标识符,还解决了对象属性名冲突的问题。通过理解它,我们能在后面的项目开发中,更灵活的使用他,此外,Symbol 的不可枚举特性也使得它非常适合用于隐藏某些内部属性,避免不必要的暴露。