ES6 Symbol类型详解:创建、属性名与全局注册

作者:木棉

时间:2026-10-23

浏览:592

本文深入解析ES6引入的Symbol原始数据类型,阐述其作为第七种数据类型的核心特性:确保属性名唯一性,有效避免命名冲突。文章详细演示了Symbol的创建方法、作为对象属性名的正确语法(方括号与defineProperty),以及Symbol.for/keyFor的全局注册机制,并结合消除魔术字符串的实例,展示Symbol在实际开发中的应用价值。

文章目录

ES6 Symbol类型概述

背景

在ES5规范中,对象属性名仅支持字符串类型,这极易引发属性名冲突。例如,在使用他人提供的对象并试图通过mixin模式添加新方法时,新方法的名称可能与现有方法重名。为解决此问题,需要一种机制确保每个属性名具有唯一性,从而从根本上杜绝冲突。ES6引入Symbol正是为了解决这一痛点。 ES6新增了一种原始数据类型Symbol,用于表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种分别为:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)和对象(Object)。 Symbol值需通过Symbol函数生成。这意味着对象的属性名现在拥有两种类型:原有的字符串类型,以及新增的Symbol类型。只要属性名属于Symbol类型,即为独一无二的标识,可确保不会与其他属性名发生冲突。


let s = Symbol();

typeof s
// "symbol"

上面代码中,变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型,而不是字符串之类的其他类型。

需注意,Symbol函数前严禁使用new命令,否则将抛出错误。这是因为生成的Symbol是一个原始类型的值,而非对象。由于Symbol值不是对象,因此无法为其添加属性。本质上,它是一种类似于字符串的数据类型。 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)"

上面代码中,s1和s2是两个 Symbol 值。如果不加参数,它们在控制台的输出都是Symbol(),不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。

若Symbol的参数为对象,则会调用该对象的toString方法将其转换为字符串,随后再生成一个Symbol值。


const obj = {
 toString() {
  return 'abc';
 }
};
const sym = Symbol(obj);
sym // Symbol(abc)

需注意,Symbol函数的参数仅表示对当前Symbol值的描述,因此即使参数相同,Symbol函数的返回值也不相等。


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

s1 === s2 // false

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

s1 === s2 // false

上面代码中,s1和s2都是Symbol函数的返回值,而且参数相同,但是它们是不相等的。

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作为对象属性名的用法

作为属性名的 Symbol

由于每个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!"

上面代码通过方括号结构和Object.defineProperty,将对象的属性名指定为一个 Symbol 值。

需注意,当Symbol值作为对象属性名时,不能使用点运算符。


const mySymbol = Symbol();
const a = {};

a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"

上面代码中,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个 Symbol 值。

同理,在对象内部使用Symbol值定义属性时,Symbol值必须置于方括号之中。


let s = Symbol();

let obj = {
 [s]: function (arg) { ... }
};

obj[s](123);

上面代码中,如果s不放在方括号中,该属性的键名就是字符串s,而不是s所代表的那个 Symbol 值。

采用增强的对象写法,上面代码的obj对象可以写得更简洁一些。


let obj = {
 [s](arg) { ... }
};

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');

实例:消除魔术字符串

3实例:消除魔术字符串

魔术字符串指的是在代码中多次出现、与代码形成强耦合的特定字符串或数值。良好的代码风格应尽可能消除魔术字符串,改用含义清晰的变量替代。


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就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。 常用的消除魔术字符串的方法,就是把它写成一个变量。


const shapeType = {
 triangle: 'Triangle'
};

function getArea(shape, options) {
 let area = 0;
 switch (shape) {
  case shapeType.triangle:
   area = .5 * options.width * options.height;
   break;
 }
 return area;
}

getArea(shapeType.triangle, { width: 100, height: 100 });

上面代码中,我们把Triangle写成shapeType对象的triangle属性,这样就消除了强耦合。

仔细分析可知,shapeType.triangle等于何值并不重要,只要确保不与其他shapeType属性的值冲突即可。因此,此处非常适合改用Symbol值。


const shapeType = {
 triangle: Symbol()
};

Symbol属性名的遍历方法

4属性名的遍历

Symbol作为属性名时,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但它并非私有属性,可通过Object.getOwnPropertySymbols方法获取指定对象的所有Symbol属性名。

Object.getOwnPropertySymbols方法返回一个数组,成员为当前对象的所有用作属性名的Symbol值。


const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols
// [Symbol(a), Symbol(b)]

下面是另一个例子,Object.getOwnPropertySymbols方法与for...in循环、Object.getOwnPropertyNames方法进行对比的例子。


const obj = {};

let foo = Symbol("foo");

Object.defineProperty(obj, foo, {
 value: "foobar",
});

for (let i in obj) {
 console.log(i); // 无输出
}

Object.getOwnPropertyNames(obj)
// []

Object.getOwnPropertySymbols(obj)
// [Symbol(foo)]

上面代码中,使用Object.getOwnPropertyNames方法得不到Symbol属性名,需要使用Object.getOwnPropertySymbols方法。 另一个新的API,Reflect.ownKeys方法可返回所有类型的键名,包括常规键名和Symbol键名。


let obj = {
 [Symbol('my_key')]: 1,
 enum: 2,
 nonEnum: 3
};

Reflect.ownKeys(obj)
// ["enum", "nonEnum", Symbol(my_key)]

Symbol.for()与Symbol.keyFor()全局注册

5 Symbol.for(),Symbol.keyFor()

有时需重复使用同一个Symbol值,Symbol.for方法可实现此功能。它接受一个字符串作为参数,搜索是否存在以该参数为名称的Symbol值。若存在则返回该Symbol值,否则新建并返回一个以该字符串为名称的Symbol值。


let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true

上面代码中,s1和s2都是 Symbol 值,但是它们都是同样参数的Symbol.for方法生成的,所以实际上是同一个值。

Symbol.for()与Symbol()两种写法均会生成新的Symbol。区别在于,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用都返回新的Symbol值,而是先检查给定的key是否存在,若不存在才新建。例如,调用Symbol.for("cat")30次,每次返回同一个Symbol值;但调用Symbol("cat")30次,会返回30个不同的Symbol值。

Symbol.keyFor方法返回一个已登记的Symbol类型值的key。


let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

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

上面代码中,变量s2属于未登记的 Symbol 值,所以返回undefined。

需注意,Symbol.for为Symbol值登记的名字属于全局环境,可在不同的iframe或service worker中获取到同一个值。


iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);

iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')
// true

上面代码中,iframe 窗口生成的 Symbol 值,可以在主页面得到。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章