symbol 是原始值,且符号实例是唯一、不可变的。symbol 用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
所以常用的场景:创建唯一记号

基本用法

初始化

Symbol函数前不能使用new命令,否则会报错。

  • 无参: let s = Symbol(); typeof s // symbol
  • 有参: Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
1
2
3
4
5
6
7
8
let s1 = Symbol('foo');
let s2 = Symbol('bar');

s1 // Symbol(foo)
s2 // Symbol(bar)

s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
  • 若参数的值是对象,会调用对象的toString方法
1
2
3
4
5
6
7
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
sym // Symbol(abc)
  • 使用 Symbol 直接传入一个函数,会调用 toString 函数,将函数内容转换为字符串
1
2
3
4
5
let a = function(){
console.log('哈哈哈');
}
console.log(Symbol(a)); // Symbol(function(){ console.log('哈哈哈');})
console.log(typeof Symbol(a)); // symbol
  • 参数只是对当前 Symbol 值的描述,相同参数 的Symbol函数的 返回值不相等(唯一性)
1
2
3
4
5
6
7
8
9
10
11
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();

s1 === s2 // false

// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');

s1 === s2 // false
  • Symbol 值不能与其他类型的值进行运算,会报错。
1
2
3
4
5
6
7
let sym = Symbol('My symbol');

"your symbol is " + sym
// TypeError: can't convert symbol to string

`your symbol is ${sym}`
// TypeError: can't convert symbol to string
  • Symbol可以显示转化为字符串和布尔类型,但是不可转化为Number类型
1
2
3
4
5
6
7
8
let sym = Symbol('My symbol');

String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'

Boolean(sym) // true

Number(sym) // TypeError

全局注册

使用同一个 Symbol 值,全局注册Symbol.for方法可以做到这一点。

原理: Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。

1
2
3
4
5
6
7
8
9
10
11
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true


Symbol.for("bar") === Symbol.for("bar")
// true

Symbol("bar") === Symbol("bar")
// false 由于Symbol()写法没有登记机制

检查某个属性是否已经登记,Symbol.keyFor()方法返回已登记的 Symbol 类型值的参数值。

  • 使用Symbol.keyFor()来查询全局符号注册表。通过接收参数为 symbol 类型,返回该 symbol 对应的字符串键,如果查询不到则返回 undefined
  • Symbol.keyFor 仅对 Symbol.for() 方法创建的 symbol 实例才可以查找到,如果是 Symbol() 方法创建将返回 undefined。
1
2
3
4
5
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

Symbol应用场景

作为对象属性名

需要使用 Symbol 对象属性的时候,用 obj[]必须使用 [] 方式获取属性,不能用点运算符。 同理,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';

// 第二种写法(最常见)
let a = {
[mySymbol]: 'Hello!'
};

// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

Symbol 类型用于定义一组常量,保证这组常量的值都是不相等的。(高级用法)

1
2
3
4
5
6
7
8
9
const log = {};

log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
};
console.log(log.levels.DEBUG, 'debug message');
console.log(log.levels.INFO, 'info message');

获取Symbol类型属性

Symbol 创建的值是不可枚举的。

可获取symbol

  1. Object.assign 将属性从源对象复制到目标对象,会包含 Symbol 类型作为 key 的属性(不可枚举属性不会复制
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
const symbolKey = Symbol('key');
const source = {
[symbolKey]: 'Symbol Property',
regularProperty: 'Regular Property'
};
Object.defineProperty(source,"w",{
value:456,
enumerable:true,
configurable:true,
writable:true
})
Object.defineProperty(source,"r",{
value:123,
enumerable:false,
configurable:false,
writable:false
})

const target = {};
Object.assign(target, source);
console.log(target);
// 输出
// {
// regularProperty: 'Regular Property',
// w: 456,
// [Symbol(key)]: 'Symbol Property'
// }
// [Symbol(key)] 类型也会被打印,但是不可枚举属性不会打印
  1. Object.getOwnPropertySymbols 方法可以获取指定对象的所有 Symbol 属性名
  2. Reflect.ownKeys 方法可以获取指定对象的所有 Symbol 属性名
1
2
3
4
5
6
7
8
9
let symbol = Symbol('test');
let obj = {[symbol]:123};
for(const key in obj){
console.log(key); // 无打印信息
}
console.log(obj[symbol]); // 123
console.log(Object.keys(obj)); // []
console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(test) ]
console.log(Reflect.ownKeys(obj)); // [ Symbol(test) ]

不可获取symbol

因为Symbol 创建的值是不可枚举的,所以一般遍历对象的结果都不会包含 symbol 内容

  • for in 循环:循环会遍历对象的可枚举属性,但会忽略不可枚举的属性

  • Object.keys() :方法返回一个数组,其中包含对象的所有可枚举属性的名称。

  • JSON.stringify() 只会序列化对象的可枚举属性,而不会包含不可枚举属性

    • JSON.stringify 的时候,如果对象中 key 或者 value 都是 Symbol类型时候。转换过程会把它忽略掉
    • JSON.stringify 直接转换 symbol类型数据,转换后的结果为 undefined
    • JSON.stringify 转换的是一个对象,无论 key 还是 value 中有 symbol类型,都会忽略掉
  • Object.getOwnPropertyNames() :返回一个数组,其中包含对象的所有属性( 包括不可枚举属性 )的名称,但是 不包括使用symbol值作为名称的属性

使用Symbol来替代常量(消除魔术字符串)

魔术字符串 指的是,在代码中 多次出现、与代码形成强耦合 的某一个具体的字符串或者数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getArea(shape, options) {
let area = 0;

switch (shape) {
case 'Triangle': // 魔术字符串
area = .5 * options.width * options.height;
break;
/* ... more code ... */
}

return area;
}

getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串'Triangle'

常用的消除魔术字符串的方法,就是 Triangle 写成一个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const shapeType = {
triangle: 'Triangle'
}
function getArea(shape, options) {
let area = 0;

switch (shape) {
case shapeType.triangle: // 魔术字符串
area = .5 * options.width * options.height;
break;
/* ... more code ... */
}

return area;
}

getArea(shapeType.triangle, { width: 100, height: 100 }); // 魔术字符串'Triangle'

这样就消除了代码的强耦合性。分析发现shapeType.triangle的值是什么无关紧要,只要保证不与shapeType其他属性值冲突就好

1
2
3
4
const shapeType = {
triangle: Symbol()
}
// 利用Symbol的唯一性就可以做到与其他属性值不冲突

使用Symbol定义类的私有属性/方法

由于Symbol常量PASSWORD被定义在a.js所在的模块中,外面的模块获取不到这个Symbol,也 不可能再创建一个一模一样的Symbol出来(因为Symbol是唯一的) ,因此这个PASSWORD的Symbol只能被限制在a.js内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的,达到了一个私有化的效果。

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
// 在a.js中:
const PASSWORD = Symbol()

class Login {
constructor(username, password) {
this.username = username
this[PASSWORD] = password
}

checkPassword(pwd) {
return this[PASSWORD] === pwd
}
}
export default Login
// 只导出了Login,没有导出PASSWORD,所以在b.js中无法访问PASSWORD


// 在b.js中:
import Login from './a'

const login = new Login('admin', '123456')

login.checkPassword('123456') // true

login.PASSWORD // oh!no!报错
login[PASSWORD] // oh!no!报错
login["PASSWORD"] // oh!no!报错

Symbol内置属性

Symbol.iterator

Symbol.iterator用来为对象定义默认的迭代器。它被用来在for-of循环中实现对对象的迭代,或用于扩展操作符。**Array,Map,Set,String 都有内置的迭代器**。但是普通对象是不支持迭代器功能的,也就不能使用 for of 循环遍历。

接下来使用 Symbol.iterator 实现一个可迭代对象:

Symbol.iterator属性中使用next

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let symbolObjTest1 = {
0:"a",
1:"b",
2:"c",
length:3,
[Symbol.iterator]:function(){
let index = 0;
return {
next(){ // 迭代器返回的对象需要有next函数
return {
value:symbolObjTest1[index++], // value为迭代器生成的值
done:index>symbolObjTest1.length // 迭代器的终止条件,done为true时终止遍历
}
}
}
}
}
for(const iterator1 of symbolObjTest1){
console.log(iterator1); // 打印 a b c
}

Symbol.iterator属性中使用Generator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let symbolObjTest2 = {
0:"d",
1:"e",
2:"f",
length:3,
[Symbol.iterator]:function*(){ // 注意Generator函数格式
let index = 0;
while(index<symbolObjTest2.length){
yield symbolObjTets2[index++]
}
}
}
for(const iterator2 of symbolObjTest2){
console.log(iterator2);//打印 d e f
}

不影响原始对象遍历,遍历正常返回key value

1
2
3
4
5
6
7
8
9
const obj = {a:1,b:2,c:3};
obj[Symbol.iterator] = function*(){
for(const key of Object.keys(this)){
yield [key,this[key]]
}
}
for(const [key,value] of obj){
console.log(`${key}:${value}`); // 打印
}

将一个class对象实现支持迭代器

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
class Animal{
constructor(name,sex,isMammal){
this.name = name;
this.sex = sex;
this.isMammal = isMammal;
}
}

class Zoo{
constructor(){
this.animals = [];
}
addAnimals(animal){
this.animals.push(animal);
}
[Symbol.iterator](){
let index = 0;
const animals = this.animals;
return {
next(){
return {
value:animals[index++],
done:index>animals.length
}
}
}
}
}

const zoo = new Zoo();
zoo.addAnimals(new Animal('dog','victory',true));
zoo.addAnimals(new Animal('pig','defeat',false));
zoo.addAnimals(new Animal('cat','defeat',false));
for (const animal of zoo) {
console.log(`${animal.name};${animal.sex};${animal.isMammal}`)
}
// 打印 dog;victory;true pig;defeat;false cat;defeat;false

Symbol.toStringTag

Symbol.toStringTag 官方描述是一个字符串值属性,用于创建对象的默认字符串描述。Object.property.toString() 方法 内部访问

最常见的场景是判断类型:

1
2
3
4
5
6
const toStringCallFun = Object.prototype.toString.call;
toStringCallFun(new Date); // [object Date]
toStringCallFun(new String); // [object String]
toStringCallFun(Math); // [object Math]
toStringCallFun(undefined); // [object Undefined]
toStringCallFun(null); // [object Null]

默认情况下,toString() 方法被每个 Object 对象继承,如果此方法在自定义对象中未被覆盖, toString() 返回“ [object type] ”,其中 type 是对象的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
function Student(score,age){
this.score = score;
this.age = age;
}
let student = new Student('100',13);
// 直接调用toString函数
console.log(student.toString()); // '[object Object]'

// 覆盖默认的toString函数
Student.prototype.toString = function(){
return `年龄${this.age};成绩${this.score}`
}
console.log(student.toString()); // 年龄13;成绩100

在ES6 之后大多数内置的对象提供了它们自己的 Symbol.toStringTag 标签,toString 时回默认返回 Symbol.toStringTag 键对应的值。比如:

1
2
3
4
Object.prototype.toString.call(new Map());       // "[object Map]"
Object.prototype.toString.call(function* () {}); // "[object GeneratorFunction]"
Object.prototype.toString.call(Promise.resolve()); // "[object Promise]"
// ... and more

但是在早期不是所有对象都有 toStringTag 属性,没有 toStringTag 属性的对象也会被toString() 方法识别并返回特定的类型标签。如下:

1
2
3
4
5
6
7
let toStringFunc = Object.prototype.toString
toStringFunc.call('foo') // '[object String]'
toStringFunc.call([1, 2]) // '[object Array]'
toStringFunc.call(3) // '[object Number]'
toStringFunc.call(true) // '[object Boolean]'
toStringFunc.call(undefined) // '[object Undefined]'
toStringFunc.call(null) // '[object Null]'

自己创建的类,toString() 找不到 toStringTag 属性!只会默认返回 Object 标签。

类增加一个 toStringTag 属性,自定义的类也就拥有了自定义的类型标签

1
2
3
4
5
6
class TestClass{
get [Symbol.toStringTag](){
return "TestToStringTag"
}
}
Object.prototype.toString.call(new TestClass());// '[object TestToStringTag]'

Symbol.toPrimitive

Symbol.toPrimitive用来指定对象在隐式调用valueOftoString方法时的行为。可以用它来为对象提供自定义的字符串和数字表示形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Life {
valueOf() {
return 42;
}

[Symbol.toPrimitive](hint) {
switch (hint) {
case "number":
return this.valueOf();
case "string":
return "Forty Two";
case "default":
return true;
}
}
}

const myLife = new Life();
console.log(+myLife); // 42
console.log(`${myLife}`); // "Forty Two"
console.log(myLife + 0); // 1 调用的是'default'
console.log(myLife - 0); // 42

Symbol.asyncIterator

Symbol.asyncIterator用来为对象定义一个异步的迭代器。可以用它来为对象启用异步迭代。它可以用于遍历异步数据流,比如异步生成器函数、异步可迭代对象等。这个特性在需要处理异步数据流时非常有用。

举一个实际的应用场景:假设正在开发一个异步数据源处理器,其中包含了大量的异步数据,比如网络请求、数据库查询等。这些数据需要被逐个获取并处理,同时由于数据量非常大,一次性获取全部数据会导致内存占用过大,因此需要使用异步迭代器来逐个获取数据并进行处理

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
class AsyncDataSource {
constructor(data) {
this._data = data;
}

async *[Symbol.asyncIterator]() {
for (const item of this._data) {
const result = await this._processAsyncData(item);
yield result;
}
}

async _processAsyncData(item) {
// 模拟异步处理数据的过程
return new Promise((resolve) => {
setTimeout(() => {
resolve(item.toUpperCase());
}, 1000);
});
}
}

async function processData() {
const dataSource = new AsyncDataSource(['a', 'b', 'c', 'd', 'e']);
for await (const data of dataSource) {
console.log(data);
}
}

processData();

// 输出结果:
// (1s后)A
// (1s后)B
// (1s后)C
// (1s后)D
// (1s后)E

Symbol.hasInstance

Symbol.hasInstance用来确认一个对象是否是构造函数的实例。它可以用来更改instanceof操作符的行为

1
2
3
4
5
6
7
8
class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}

const arr = [1, 2, 3];
console.log(arr instanceof MyArray); // true

Symbol.isConcatSpreadable

Symbol.isConcatSpreadable用来确定对象在与其他对象连接时是否应该被展开。它可以用来更改Array.prototype.concat方法的行为

1
2
3
4
const arr1 = [1, 2, 3];
const spreadable = { [Symbol.isConcatSpreadable]: true, 0: 4, 1: 5, 2: 6, length: 3 };

console.log([].concat(arr1, spreadable)); // [1, 2, 3, 4, 5, 6]

改成false后:

Symbol.species

Symbol.species用来指定创建派生对象时要使用的构造函数。它可以用来自定义创建新对象的内置方法的行为

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
class MyArray extends Array {
static get [Symbol.species]( "Symbol.species") {
return Array;
}
}

const myArray = new MyArray(1, 2, 3);
const mappedArray = myArray.map(x => x * 2);

console.log(mappedArray instanceof MyArray); // false
console.log(mappedArray instanceof Array); // true


// 如果改成MyArray:
class MyArray extends Array {
static get [Symbol.species]( "Symbol.species") {
return MyArray;
}
}

const myArray = new MyArray(1, 2, 3);
const mappedArray = myArray.map(x => x * 2);

console.log(mappedArray instanceof MyArray); // ftrue
console.log(mappedArray instanceof Array); // true

Symbol.match

Symbol.match用来在使用String.prototype.match方法时确定要搜索的值。 它可以用来更改类似于RegExp对象的match方法的行为

1
2
3
4
5
6
7
8
9
const myRegex = /test/;
'/test/'.startsWith(myRegex); // Throws TypeError
// 默认正则对象是不能被string方法搜索到的

const re = /foo/;
re[Symbol.match] = false;
// 加了之后就可以了
"/foo/".startsWith(re); // true
"/bar/".endsWith(re); // false

Symbol.matchall

Symbol.matchAll 内置通用(well-known)符号指定方法返回一个迭代器,该迭代器根据字符串生成正则表达式的匹配项。

此函数可以被String.prototype.matchAll() 方法调用。

1
2
3
4
5
6
const myRegex = /foo/g;
const str = 'How many foos in the the foo foo bar?';

for (let result of myRegex[Symbol.matchAll](str)) {
console.log(result); // we will get the matches
}

Symbol.replace

Symbol.replace用来在使用String.prototype.replace方法时确定替换值。它可以用来更改类似于RegExp对象的replace方法的行为

1
2
3
4
5
6
7
let replaceHyphens = {
[Symbol.replace](string, replacer) {
return string.replace(/-/g, replacer);
}
};
console.log('123-45-678'.replace(replaceHyphens, ':')); // '123:45:678'
// 这个例子中,replaceHyphens 将字符串中的所有连字符替换为冒号。

Symbol.search 属性定义了当 String.prototype.search() 方法被调用时,如何返回字符串中匹配项的索引

1
2
3
4
5
6
7
let searchObject = {
[Symbol.search](string) {
return string.indexOf('JavaScript');
}
};
console.log('Hello JavaScript!'.search(searchObject)); // 6
// 在这个例子中,searchObject 实现了搜索 "JavaScript" 字符串并返回它在源字符串中的位置。

Symbol.split

Symbol.split用来在使用String.prototype.split方法时确定分隔值。它可以用来更改类似于RegExp对象的split方法的行为

1
2
3
4
5
6
7
8
9
const customSplit = str => str.split(/\d+/);

const customRegExp = {
[Symbol.split]: customSplit
};

const string = "foo123bar456baz";

string.split(customRegExp); // outputs [ 'foo', 'bar', 'baz' ]

Symbol.unscopables

Symbol.unscopables用于确定应该从with语句的作用域中排除哪些对象属性。它可以用来更改with语句的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
const person = {
age: 42
};

person[Symbol.unscopables] = {
age: true
};

with (person) {
console.log(age);
// Expected output: Error: age is not defined
}
// 在 with 语句中无法直接使用age。

Symbol.dispose

“显式资源管理”是指用户通过使用命令式方法(如Symbol.dispose )或声明式方法(如使用块作用域声明)显式地管理“资源”的生命周期的系统。

Symbol.dispose 是 JavaScript 中的一个新的全局符号。任何带有 Symbol.dispose 功能的都被视为“资源”—— “具有特定生命周期的对象” ——并且可以与关键字 using 一起使用。

1
2
3
4
5
const resource = {
[Symbol.dispose]: () => {
console.log("Hooray!");
},
};

还可以使用 Symbol.asyncDispose and await using 来处理需要异步处理的资源:

1
2
3
4
5
6
7
8
const getResource = () => ({
[Symbol.asyncDispose]: async () => {
await someAsyncFunc();
},
});
{
await using resource = getResource();
}

这将在继续之前等待 Symbol.asyncDispose 函数。

这对于数据库连接等资源很有用,例如您希望在这些资源中确保连接在程序继续运行之前关闭。


下面是一些例子:

文件处理

没有 using

1
2
3
4
5
6
7
import { open } from "node:fs/promises";
let filehandle;
try {
filehandle = await open("thefile.txt", "r");
} finally {
await filehandle?.close();
}

使用 using:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { open } from "node:fs/promises";
const getFileHandle = async (path: string) => {
const filehandle = await open(path, "r");
return {
filehandle,
[Symbol.asyncDispose]: async () => {
await filehandle.close();
},
};
};
{
await using file = getFileHandle("thefile.txt");
// Do stuff with file.filehandle
} // Automatically disposed!

数据库连接

使用 using 管理数据库连接是 C# 中的一个常见用例。

没有 using

1
2
3
4
5
6
const connection = await getDb();
try {
// Do stuff with connection
} finally {
await connection.close();
}

使用 using

1
2
3
4
5
6
7
8
9
10
11
12
13
const getConnection = async () => {
const connection = await getDb();
return {
connection,
[Symbol.asyncDispose]: async () => {
await connection.close();
},
};
};
{
await using { connection } = getConnection();
// Do stuff with connection
} // Automatically closed!