# Symbol用法

# 前言

  1. 前端做了这么多年,当面试官问到symbol这个类型可以简单说下吗?尴尬的答复到是一种新型的数据类型,主要保证数据的唯一性
  2. 基于上面的说法太过于单薄,于是乎写这篇文章也是一个总结报告,深入学习symbol到底是干嘛的

# symbol的由来

Symbol 的设计初衷是为了创建一种独一无二的标识符,以防止属性名冲突。在 JavaScript 中,对象的属性名是字符串,如果两个对象使用相同的字符串作为属性名,就会导致冲突。为了避免这种冲突,Symbol 提供了一种生成唯一标识符的机制。 通过这个由来我们带大家建立Symbol的第一印象,那就是symbol是用来解决对象属性名命名冲突的

# symbol的特点

这里先说一下Symbol的特点:

  1. 唯一性:每个 Symbol 值都是唯一的,没有两个 Symbol 值是相等的。
    • 注意Symbol不支持new Symbol(),它他并不是完整的构造函数
    • 每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的
  2. 不可变性Symbol 值是不可变的,一旦创建就不能修改。
  3. 私有性Symbol 值在全局范围内是唯一可见的,无法通过遍历对象的方式获取到 Symbol 类型的属性。因为这个特点,我们还可以建立对象的私有属性,这样不能通过对象遍历出来,有助于保护对象的实现细节

# symbol的属性和常用方法

定义与使用:

JavaScript中,可以使用Symbol()函数来创建一个符号,如下所示:

const mySymbol = Symbol();
1

Symbol函数可以接受一个描述性字符串作为参数,用于标识符号的含义,如下所示:

const mySymbol = Symbol('my symbol');
1

需要注意的是,每个Symbol()函数调用都会返回一个唯一的符号,即使描述性字符串相同,它们也是不同的符号。

Symbol类型的值可以用作对象的属性名,如下所示:

const mySymbol = Symbol('my symbol');
const myObject = {
  [mySymbol]: 'hello'
};
console.log(myObject[mySymbol]);  // 输出:'hello'
1
2
3
4
5

在上面的代码中,我们使用符号mySymbol作为对象myObject的属性名,并将其值设置为'hello'。使用符号作为属性名的好处是它们不会与其他属性名冲突,并且对外不可见,因此可以用于实现私有属性或方法等场景。

  • Symbol.length Symbol构造函数的length属性值为0。
console.log(Symbol.length); // 0
1

另外,JavaScript中的Symbol类型有两个特殊的方法Symbol.for()Symbol.keyFor(),用于创建全局符号和获取已经存在的全局符号。

  • Symbol.for(): 用于创建或获取一个全局符号,如果全局符号已经存在,则返回已经存在的符号,否则创建一个新的全局符号。例如:
const mySymbol = Symbol.for('my symbol');
const sameSymbol = Symbol.for('my symbol');

console.log(mySymbol === sameSymbol);  // 输出:true
1
2
3
4

在上面的代码中,我们使用Symbol.for()方法来创建一个全局符号'my symbol',并将其赋值给mySymbol变量。然后,我们再次使用Symbol.for()方法来获取同一个全局符号,赋值给sameSymbol变量。由于全局符号已经存在,因此sameSymbol变量的值等于mySymbol变量的值,输出true

  • Symbol.keyFor() 方法会返回一个已经存在的Symbol值的key。如果给定的Symbol值不存在于全局Symbol注册表中,则返回undefined。
const symbol1 = Symbol.for('foo');
const key1 = Symbol.keyFor(symbol1);

const symbol2 = Symbol('bar');
const key2 = Symbol.keyFor(symbol2);

console.log(key1); // 'foo'
console.log(key2); // undefined
1
2
3
4
5
6
7
8

使用场景: 当我们需要获取一个全局唯一的Symbol值的key时,可以使用Symbol.keyFor()方法。但需要注意的是,只有在该Symbol值被注册到全局Symbol注册表中时,才能使用Symbol.keyFor()方法获取到其key。

  • Symbol.iterator Symbol.iterator是一个预定义好的Symbol值,表示对象的默认迭代器方法。该方法返回一个迭代器对象,可以用于遍历该对象的所有可遍历属性。
const obj = { a: 1, b: 2 };

for (const key of Object.keys(obj)) {
  console.log(key);
}
// Output:
// 'a'
// 'b'

for (const key of Object.getOwnPropertyNames(obj)) {
  console.log(key);
}
// Output:
// 'a'
// 'b'

for (const key of Object.getOwnPropertySymbols(obj)) {
  console.log(key);
}
// Output: 
// No output

obj[Symbol.iterator] = function* () {
  for (const key of Object.keys(this)) {
    yield key;
  }
}

for (const key of obj) {
  console.log(key);
}
// Output:
// 'a'
// 'b'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

使用场景: 当我们需要自定义一个对象的迭代行为时,可以通过定义Symbol.iterator属性来实现。例如,对于自定义的数据结构,我们可以定义它的Symbol.iterator方法以便能够使用for...of语句进行遍历。

  • Symbol.isConcatSpreadable Symbol.isConcatSpreadable是一个预定义好的Symbol值,用于定义对象在使用concat()方法时的展开行为。如果一个对象的Symbol.isConcatSpreadable属性为false,则在调用concat()方法时,该对象不会被展开。
const arr1 = [1, 2];
const arr2 = [3, 4];
const obj = { length: 2, 0: 5, 1: 6, [Symbol.isConcatSpreadable]: false };

console.log(arr1.concat(arr2)); // [1, 2, 3, 4]
console.log(arr1.concat(obj)); // [1, 2, { length: 2, 0: 5, 1: 6, [Symbol(Symbol.isConcatSpreadable)]: false }]
1
2
3
4
5
6
  • Symbol.toPrimitive

Symbol.toPrimitive是一个预定义好的Symbol值,用于定义对象在被强制类型转换时的行为。如果一个对象定义了Symbol.toPrimitive方法,则在将该对象转换为原始值时,会调用该方法。

const obj = {
  valueOf() {
    return 1;
  },
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') {
      return 2;
    } else if (hint === 'string') {
      return 'foo';
    } else {
      return 'default';
    }
  }
};

console.log(+obj); // 2
console.log(`${obj}`); // 'foo'
console.log(obj + ''); // 'default'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • Symbol.prototype.valueOf() Symbol.prototype.valueOf()方法会返回Symbol值本身。
const symbol = Symbol('foo');

console.log(symbol.valueOf()); // Symbol(foo)

使用场景: 当我们需要获取一个Symbol值本身时,可以使用Symbol.prototype.valueOf()方法。
1
2
3
4
5
  • Symbol.replace Symbol.replace是一个预定义好的Symbol值,用于定义对象在调用String.prototype.replace()方法时的行为。如果一个对象定义了Symbol.replace方法,则在调用该对象的replace()方法时,会调用该方法进行替换。
class Foo {
  [Symbol.replace](str, replacement) {
    return str.replace('foo', replacement);
  }
}

console.log('foobar'.replace(new Foo(), 'baz')); // 'bazbar'
console.log('barbaz'.replace(new Foo(), 'baz')); // 'barbaz'

使用场景: 当我们需要自定义一个对象在调用String.prototype.replace()方法时的行为时,可以通过定义Symbol.replace方法来实现。
1
2
3
4
5
6
7
8
9
10
  • Symbol.search Symbol.search是一个预定义好的Symbol值,用于定义对象在调用String.prototype.search()方法时的行为。如果一个对象定义了Symbol.search
class Foo {
  [Symbol.search](str) {
    return str.indexOf('foo');
  }
}

console.log('foobar'.search(new Foo())); // 0
console.log('barbaz'.search(new Foo())); // -1

使用场景: 当我们需要自定义一个对象在调用String.prototype.search()方法时的行为时,可以通过定义Symbol.search方法来实现。
1
2
3
4
5
6
7
8
9
10
  • Symbol.split Symbol.split是一个预定义好的Symbol值,用于定义对象在调用String.prototype.split()方法时的行为。如果一个对象定义了Symbol.split方法,则在调用该对象的split()方法时,会调用该方法进行分割。
class Foo {
  [Symbol.split](str) {
    return str.split(' ');
  }
}

console.log('foo bar baz'.split(new Foo())); // ['foo', 'bar', 'baz']
console.log('foobarbaz'.split(new Foo())); // ['foobarbaz']

使用场景: 当我们需要自定义一个对象在调用String.prototype.split()方法时的行为时,可以通过定义Symbol.split方法来实现。
1
2
3
4
5
6
7
8
9
10
  • Symbol.prototype.toString() Symbol.prototype.toString()方法会返回Symbol值的字符串表示形式,该表示形式包含Symbol()函数创建时指定的描述信息。
const symbol = Symbol('foo');

console.log(symbol.toString()); // 'Symbol(foo)'

使用场景: 当我们需要将一个Symbol值转换成字符串时,可以使用Symbol.prototype.toString()方法。
1
2
3
4
5

# Symbol的重要属性

1. Symbol.iterator: 用于指定对象的默认迭代器,例如:

const myObject = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
};

for (const value of myObject) {
  console.log(value);
}
// 输出:1 2 3
1
2
3
4
5
6
7
8
9
10
11
12

在上面的代码中,我们为myObject对象设置了Symbol.iterator符号,并指定了一个生成器函数作为迭代器的实现。然后,我们可以使用for...of循环迭代myObject对象,并输出其中的值。

2. Symbol.hasInstance: 用于定义一个对象是否为某个构造函数的实例。

class MyClass {
  static [Symbol.hasInstance](obj) {
    return obj instanceof Array;
  }
}

console.log([] instanceof MyClass);  // 输出:true
console.log({} instanceof MyClass);  // 输出:false
1
2
3
4
5
6
7
8
  • 在上面的代码中,我们定义了一个MyClass类,并使用Symbol.hasInstance方法自定义了instanceof运算符的行为,使其检查对象是否为数组。当检查[]对象时,instanceof运算符返回true,因为[]是Array的实例;当检查{}对象时,instanceof运算符返回false,因为{}不是Array的实例。
  • 需要注意的是,Symbol.hasInstance方法是一个静态方法,需要定义在构造函数的静态属性中。另外,Symbol.hasInstance方法不能被继承,因此子类需要重新定义该方法。

3. Symbol.toStringTag: 用于自定义对象的默认字符串描述。

当调用Object.prototype.toString()方法时,会使用该对象的Symbol.toStringTag属性作为默认的字符串描述,例如:

class MyObject {
  get [Symbol.toStringTag]() {
    return 'MyObject';
  }
}

const obj = new MyObject();
console.log(Object.prototype.toString.call(obj));  // 输出:'[object MyObject]'
1
2
3
4
5
6
7
8
  • 在上面的代码中,我们定义了一个MyObject类,并使用Symbol.toStringTag属性自定义了该类的默认字符串描述。然后,我们创建了一个obj对象,并使用Object.prototype.toString()方法获取其字符串描述,输出'[object MyObject]'
  • 需要注意的是,Symbol.toStringTag属性只有在调用Object.prototype.toString()方法时才会生效,对其他方法没有影响。另外,如果没有定义Symbol.toStringTag属性,则默认使用构造函数的名称作为字符串描述。

4. Symbol.asyncIterator: 用于指定对象的默认异步迭代器。

当使用for await...of循环迭代一个对象时,会调用该对象的Symbol.asyncIterator方法获取异步迭代器。

Symbol.asyncIterator方法需要返回一个异步迭代器对象,该对象实现了next()方法,并返回一个Promise对象。当迭代器迭代到结束时,next()方法应该返回一个Promise对象,该Promise对象的value属性为undefineddone属性为true

例如,下面的代码演示了如何使用Symbol.asyncIterator属性定义一个异步迭代器:

const myObject = {
  async *[Symbol.asyncIterator]() {
    yield Promise.resolve(1);
    yield Promise.resolve(2);
    yield Promise.resolve(3);
  }
};

(async function() {
  for await (const value of myObject) {
    console.log(value);
  }
})();
// 输出:1 2 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 在上面的代码中,我们为myObject对象设置了Symbol.asyncIterator符号,并指定了一个异步生成器函数作为异步迭代器的实现。然后,我们使用for await...of循环迭代myObject对象,并输出其中的值。
  • 需要注意的是,使用Symbol.asyncIterator属性定义的异步迭代器只能使用for await...of循环进行迭代,不能使用普通的for...of循环。此外,Symbol.asyncIterator属性只有在支持异步迭代器的环境中才能使用,例如Node.js的版本必须在10.0.0以上才支持异步迭代器。

# symbol的应用场景

# 使用场景

  • 解决属性名称冲突:

当你在开发一个库或框架时,为了避免属性名冲突,可以使用 Symbol 作为对象的属性名。这样可以确保属性名的唯一性,例如:

const PRIVATE_KEY = Symbol('private');

const obj = {
  [PRIVATE_KEY]: 'private value',
};

console.log(obj[PRIVATE_KEY]); // 访问私有属性
1
2
3
4
5
6
7
  • 创建私有方法和属性:

Symbol 可以用于创建类中的私有方法和属性,以实现封装和隐藏内部实现细节,例如:

const PRIVATE_METHOD = Symbol('privateMethod');

class MyClass {
  constructor() {
    this[PRIVATE_METHOD] = function() {
      // 私有方法实现
    };
  }

  publicMethod() {
    // 公共方法调用私有方法
    this[PRIVATE_METHOD]();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

外面是访问不到PRIVATE_METHOD属性的,只能通过publicMethod方法才能访问到

  • 定义常量: 可能有这样的场景,我们开发的大型项目里面有很多常量,但是有可能常量名会冲突,这时候也可以使用Symbol来解决此类问题
const STATUS_SUCCESS = Symbol('success');
const STATUS_ERROR = Symbol('error');

function handleResponse(response) {
  if (response.status === STATUS_SUCCESS) {
    // 处理成功情况
  } else if (response.status === STATUS_ERROR) {
    // 处理错误情况
  }
}
1
2
3
4
5
6
7
8
9
10
  • 自定义迭代器

# 实现symbol

有的人觉得,为什么要实现symbol, 有什么好处?

这个主要是锻炼我们实现功能的能力,要知道我们的js无所不能的,只有多锻炼,才能早点达到无所不能的能力

因为使用原生js并不能完全实现Symbol,所以我们主要实现了一下功能

  1. symbol不能使用new命令
  2. instanceof的结果为false
  3. symbol接受一个字符串作为参数
  4. symbol值是唯一的,因为两个对象值不一样
  5. 调用toString方法,输出的值也是唯一的
(function () {
  var root = this;

  var generateName = (function () {
    var postfix = 0;
    return function (descString) {
      postfix++;
      return '@@' + descString + '_' + postfix;
    }
  })()

  var SymbolPolyfill = function Symbol(description) {
    if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

    var descString = description === undefined ? undefined : String(description);

    var symbol = Object.create({
      toString: function() {
        return this.__Name__;
      },
    });

    Object.defineProperties(symbol, {
      '__Description__': {
        value: descString,
        writable: false,
        enumrable: false,
        configurable: false,
      },
      '__Name__': {
        value: generateName(descString),
        writable: false,
        enumerable: false,
        configurable: false
      }
    })

    return symbol; 
  }

  root.SymbolPolyfill = SymbolPolyfill;
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

然后我们测试一下

const a = SymbolPolyfill('foo');
const b = SymbolPolyfill('foo');
console.log(a===b);

var o = {};
o[a] = 'hello';
o[b] = 'hi';

console.log(o); // Object { "@@foo_1": "hello", "@@foo_2": "hi" }
1
2
3
4
5
6
7
8
9

# 总结

上面学习完了symbol,接下来我们吐槽一下,难道大家没有感觉Symbol很鸡肋么?

  1. 可读性差:由于 Symbol 是一种唯一且不可变的值,它的值在代码中不易理解和追踪。相比起字符串或其他基本类型的属性名,Symbol 的用途和含义可能更加隐晦。
  2. 难以扩展:Symbol 的唯一性可能导致扩展困难。如果库或框架使用 Symbol 定义了一些内部属性或方法,但用户想要在其上进行自定义扩展时,可能会受限于无法直接访问和修改 Symbol 属性。
  3. 兼容性问题:旧版的 JavaScript 引擎可能不支持 Symbol 或只支持部分功能,这可能导致在一些特定环境中使用 Symbol 的代码无法正常运行。
  4. 额外的复杂性:引入 Symbol 可能会增加代码的复杂性。使用 Symbol 的场景通常是在一些高级、复杂的应用中,对于一般的开发需求来说,它可能会增加学习成本和开发复杂性,而并非每个项目都需要使用 Symbol

尽管 Symbol 有一些限制和问题,但它仍然具有一些有价值的应用场景,特别是在构建库、框架或需要确保属性名唯一性和隐藏性的高级应用中。它可以提供一种确保标识符的独特性和不可变性的机制。因此,对于特定的用途和需求,Symbol 仍然是一个有用的功能。

# 参考

一文理解Symbol应用场景---近万字总结 (opens new window)

最后更新时间: 6/24/2023, 7:42:46 PM