JavaScript Symbol 类型详解与核心用法

作者:小括号

时间:2026-10-31

浏览:488

本文深入解析 JavaScript 中的 Symbol 原始类型,阐述其解决属性名冲突的背景。详细介绍 Symbol() 函数的创建规则、与 new 关键字的交互限制,以及通过 Symbol.for() 和 Symbol.keyFor() 实现全局符号注册表共享。同时涵盖将 Symbol 用作对象属性键的具体语法,并列举 Symbol.asyncIterator 等内置通用符号的实际应用场景。

文章目录

1、 引入Symbol类型的背景

在 ES5 标准中,对象属性名仅支持字符串类型,这导致在扩展已有对象时极易发生属性名冲突。

例如,当开发者引入第三方模块或对象并希望为其添加新属性时,新定义的属性名可能与该对象原有的属性名重复,从而覆盖原有数据。

2、Symbol类型简介

Symbol 是 ES6 引入的一种原始数据类型,用于表示独一无二的值。

  • JavaScript 中其他原始类型包括:未定义 (undefined)、空值 (null)、布尔值 (boolean)、字符串 (string) 和数值 (number)。注意,对象 (object) 属于引用类型,不属于原始类型。
  • Symbol 的核心特性是其值的唯一性,确保每次创建的 Symbol 实例都不相同。
  • Symbol 类型的“真实值”无法直接获取,因为它没有对应的字面量语法,也不能像字符串那样通过字面量直接定义。
  • Symbol 的设计意义在于提供唯一标识符以实现区分和避免重复,而非关注其内部的具体数值。

Symbol() 是 JavaScript 的原生构造函数之一。

  • 常见的原生构造函数包括:String()、Number()、Boolean()、Array()、Object()、Function()、RegExp()、Date()、Error() 以及 Symbol()。

3、基本用法

创建 Symbol 必须使用 Symbol() 函数进行初始化。

let sym = Symbol();
// 因为符号本身是原始类型,所以typeof操作符对符号返回symbol
console.log(typeof sym); // symbol

调用 Symbol() 时,可以传入一个可选的字符串参数作为该符号的描述(description),主要用于代码调试和日志记录。需要注意的是,这个描述字符串与 Symbol 本身的定义或标识完全无关,不影响其唯一性。

// Symbol的值是唯一的,不会出现相同值的常量
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
console.log(genericSymbol == otherGenericSymbol); // false

// 可以传入一个字符串参数作为对符号的描述
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(fooSymbol == otherFooSymbol); // false

Symbol 没有字面量语法。根据规范,只要创建 Symbol() 实例并将其用作对象的新属性,即可保证它不会覆盖对象中已有的属性,无论是其他 Symbol 属性还是字符串属性。

let genericSymbol = Symbol();
console.log(genericSymbol); // Symbol()

let fooSymbol = Symbol('foo');
console.log(fooSymbol); // Symbol(foo)

Symbol() 函数不能与 new 关键字结合使用作为构造函数。

这一限制旨在防止创建符号包装对象。与其他支持构造函数且可用于初始化包含原始值的包装对象(如 Boolean、String 或 Number)不同,Symbol 直接返回原始值。

let myBoolean = new Boolean();
console.log(typeof myBoolean); // "object"

let myString = new String();
console.log(typeof myString); // "object"

let myNumber = new Number();
console.log(typeof myNumber); // "object"

let mySymbol = new Symbol(); // 报错,TypeError
console.log(mySymbol);

如果确实需要创建符号的包装对象,可以借用 Object() 函数进行转换。

let mySymbol = Symbol();
let myWarppedSymbol = Object(mySymbol);
console.log(typeof myWarppedSymbol); // "object"

4、 使用全局符号注册表

当运行时的不同部分需要共享和重用相同的符号实例时,可以使用全局符号注册表。通过字符串作为键,可以在注册表中创建并重用符号。

let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // symbol

Symbol.for() 方法对每个字符串键执行幂等操作,确保相同键返回相同实例。

第一次使用某个字符串调用 Symbol.for() 时,它会检查全局运行时注册表。若发现不存在对应的符号,则生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用会检查注册表,发现已存在与该字符串对应的符号,直接返回该符号实例。

// 创建新符号
let fooGlobalSymbol = Symbol.for('foo');
// 重用已有符号
let otherFooGlobalSymbol = Symbol.for('foo');

console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true

采用相同描述字符串,通过全局注册表定义的符号与直接使用 Symbol() 定义的符号并不等同。

// 使用Symbol()定义
let localSymbol = Symbol('foo');
// 使用全局注册表定义
let globalSymbol = Symbol.for('foo');

console.log(localSymbol === globalSymbol); // false

全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for() 的任何非字符串值都会被隐式转换为字符串。

注册表中使用的键同时也会被用作符号的描述(description)。

let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol); // Symbol(undefined)

可以使用 Symbol.keyFor() 来查询全局注册表。该方法接收一个符号,返回该全局符号对应的字符串键。如果查询的不是全局符号(即通过 Symbol() 创建的局部符号),则返回 undefined。

// 创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo

// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined

如果传给 Symbol.keyFor() 的参数不是符号类型,该方法将抛出 TypeError 错误。

Symbol.keyFor(123); // TypeError: 123 is not a symbol

5、 使用符号作为属性

凡是可以使用字符串或数值作为属性名的地方,都可以使用符号作为属性名。

这包括对象字面量属性,以及通过 Object.defineProperty(obj, prop, descriptor) 或 Object.defineProperties() 定义的属性。

在对象字面量中,符号只能在使用计算属性语法(即方括号 [])时作为属性名。

let s1 = Symbol('foo'),
    s2 = Symbol('bar'),
    s3 = Symbol('baz'),
    s4 = Symbol('qux');

let o = {
    // [属性],会对属性进行读取,并且转换成字符串。[s1]是读取了Symbol的字符串键'foo'
    [s1]: 'foo val'
};
// 或 o[s1] = 'foo val';
console.log(o); // { [Symbol(foo)]: 'foo val' }

Object.defineProperty(o, s2, { value: 'bar val' });
console.log(o); // {Symbol(foo): foo val, Symbol(bar): bar val}

Object.defineProperties(o, {
    [s3]: { value: 'baz val' },
    [s4]: { value: 'qux val' }
});
console.log(o); // {Symbol(foo): foo val, Symbol(bar): baz val,
                //  Symbol(foo): foo val, Symbol(bar): qux val}

let s1 = Symbol('foo'),
    s2 = Symbol('bar');

let o = {
    [s1]: 'foo val',
    [s2]: 'bar val',
    baz: 'baz val',
    qux: 'qux val'
};

// Object.getOwnPropertySymbols()返回对象实例的符号属性数组
console.log(Object.getOwnPropertySymbols(o)); // [ Symbol(foo), Symbol(bar) ]

// Object.getOwnPropertyNames()返回对象实例的常规属性数组
console.log(Object.getOwnPropertyNames(o)); // [ 'baz', 'qux' ]

// Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象
console.log(Object.getOwnPropertyDescriptors(o));
// {
//     baz: {
//       value: 'baz val',
//       writable: true,
//       enumerable: true,
//       configurable: true
//     },
//     qux: {
//       value: 'qux val',
//       writable: true,
//       enumerable: true,
//       configurable: true
//     },
//     [Symbol(foo)]: {
//       value: 'foo val',
//       writable: true,
//       enumerable: true,
//       configurable: true
//     },
//     [Symbol(bar)]: {
//       value: 'bar val',
//       writable: true,
//       enumerable: true,
//       configurable: true
//     }
//   }

// Reflect.ownKeys()会返回两种类型的键
console.log(Reflect.ownKeys(o)); // [ 'baz', 'qux', Symbol(foo), Symbol(bar) ]

注意:Object.getOwnPropertyNames() 和 Object.getOwnPropertySymbols() 两个方法的返回值彼此互斥,前者返回字符串键,后者返回符号键。

因为符号属性是对内存中符号对象的一个引用,所以直接创建并用作属性的符号不会丢失。

但是,如果没有显式地保存对这些属性的引用,开发者必须遍历对象的所有符号属性才能找到相应的属性键。

let o = {
    [Symbol('foo')]: 'foo val',
    [Symbol('bar')]: 'bar val'
};
console.log(o); // { [Symbol(foo)]: 'foo val', [Symbol(bar)]: 'bar val' }

let barSymbol = Object.getOwnPropertySymbols(o).find((Symbol) => 
				Symbol.toString().match(/bar/));
console.log(barSymbol); // Symbol(bar)

6、 所有属性

属性含义
Symbol.asyncIterator符号指定了一个对象的默认异步迭代器。如果一个对象设置了这个属性,它就是异步可迭代对象,可用于for await…of循环。
Symbol.prototype.descriptiondescription 是一个只读属性,它会返回 Symbol 对象的可选描述的字符串。
Symbol.hasInstance用于判断某对象是否为某构造器的实例。因此你可以用它自定义 instanceof 操作符在某个类上的行为。
Symbol.isConcatSpreadable用于配置某对象作为Array.prototype.concat()方法的参数时是否展开其数组元素。
Symbol.iterator为每一个对象定义了默认的迭代器。该迭代器可以被 for…of 循环使用。
Symbol.match指定了匹配的是正则表达式而不是字符串。String.prototype.match() 方法会调用此函数
Symbol.matchAll内置通用(well-known)符号指定方法返回一个迭代器,该迭代器根据字符串生成正则表达式的匹配项。此函数可以被 String.prototype.matchAll() 方法调用。
Symbol.replace这个属性指定了当一个字符串替换所匹配字符串时所调用的方法。
Symbol.search指定了一个搜索方法,这个方法接受用户输入的正则表达式,返回该正则表达式在字符串中匹配到的下标,这个方法由以下的方法来调用 String.prototype.search()。
Symbol.species知名的 Symbol.species 是个函数值属性,其被构造函数用以创建派生对象。
Symbol.split指向 一个正则表达式的索引处分割字符串的方法。这个方法通过 String.prototype.split() 调用。
Symbol.toPrimitive是内置的 symbol 属性,其指定了一种接受首选类型并返回对象原始值的表示的方法。它被所有的强类型转换制算法优先调用。
Symbol.toStringTag内置通用(well-known)symbol 是一个字符串值属性,用于创建对象的默认字符串描述。它由 Object.prototype.toString() 方法内部访问。
Symbol.unscopables指用于指定对象值,其对象自身和继承的从关联对象的 with 环境绑定中排除的属性名称。

附:Symbol的使用场景

1、作为对象属性:当一个复杂对象含有多个属性时,很容易将某个属性名覆盖掉。利用 Symbol 值作为属性名可以很好地避免这一现象,确保属性独立。

const name = Symbol('name');
const obj = {
    [name]: 'ClickPaas',
}

2、ES6 中的类是没有 private 关键字来声明类的私有方法和私有变量的,但是我们可以利用 Symbol 的唯一性来模拟私有成员。

const speak = Symbol();
class Person {
    [speak]() {
        console.log(123)
    }
}
let person = new Person()
console.log(person[speak]())

因为使用者无法在外部创建出一个相同的 Symbol,所以无法直接调用该方法或访问该变量,从而实现类似私有的效果。

总结

到此这篇关于JS中Symbol类型的介绍与基本用法的文章就介绍到这了,更多相关JS Symbol类型用法内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章