JavaScript中的Symbol类型

175 阅读7分钟

深入探讨 JavaScript 的 Symbol 类型及其应用,包括基本用法、私有属性、内置属性及相应使用,提升代码可读性与安全性。掌握 Symbol 的强大功能,优化编程效率。

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

基本用法

初始化

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

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

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

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

s1 === s2 // false

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

s1 === s2 // false
  • Symbol 值不能与其他类型的值进行运算,会报错。
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类型
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是否已经存在,如果不存在才会新建一个值。

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。
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

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

Symbol应用场景

作为对象属性名

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

let mySymbol = Symbol();

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

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

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

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

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

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 的属性(不可枚举属性不会复制
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 属性名
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来替代常量(消除魔术字符串)

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

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 写成一个变量

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其他属性值冲突就好

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

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

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

// 在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

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

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

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对象实现支持迭代器

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() 方法 内部访问

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

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 是对象的类型

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 键对应的值。比如:

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() 方法识别并返回特定的类型标签。如下:

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 属性,自定义的类也就拥有了自定义的类型标签

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

Symbol.toPrimitive

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

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

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

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操作符的行为

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方法的行为

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用来指定创建派生对象时要使用的构造函数。它可以用来自定义创建新对象的内置方法的行为

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方法的行为

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() 方法调用。

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方法的行为

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

Symbol.search

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

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方法的行为

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语句的行为

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 一起使用。

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

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

const getResource = () => ({
  [Symbol.asyncDispose]: async () => {
    await someAsyncFunc();
  },
});
{
  await using resource = getResource();
}

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

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


下面是一些例子:

文件处理

没有 using
import { open } from "node:fs/promises";
let filehandle;
try {
  filehandle = await open("thefile.txt", "r");
} finally {
  await filehandle?.close();
}
使用 using:
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
const connection = await getDb();
try {
  // Do stuff with connection
} finally {
  await connection.close();
}
使用 using
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!