技术分享01 - Symbol 有什么用?

何为 Symbol?

Symbol 是ES6 引入的一种新的原始数据类型,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

为什么引入?

因为 ES5 的对象属性名都是字符串,所以这就容易造成属性名的冲突。比如,当我们使用一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式)时,新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入 Symbol 的原因。

语法

Symbol([description])

构造方法 和 一些特性

const symbol1 = Symbol();

/* Symbol函数可以接受一个字符串作为参数,
 * 表示对 Symbol 实例的描述,
 * 主要是为了在控制台显示,或者转为字符串时,比较容易区分。*/
const symbol2 = Symbol('foo');
const symbol3 = Symbol(42);
console.log(typeof symbol3.description) // "string"

console.log(typeof symbol1); // "symbol"

/* Symbol 值可以显式转为字符串。 */
console.log(symbol2.toString()); // "Symbol(foo)"
/* ES2019 提供了一个实例属性 description ,直接返回 Symbol 的描述。 */
symbol2.description; // "foo"

console.log(symbol3 === 42); // false

/* 注意,
 * Symbol函数的参数只是表示对当前 Symbol 值的描述,
 * 因此相同参数的Symbol函数的返回值是不相等的。 */
console.log(Symbol('foo') === Symbol('foo')); // false

 注意!

Symbol 的构造函数并不完整,因为它 不支持 语法:new Symbol()

var sym = new Symbol(); // TypeError

Symbol 的特点

唯一性

即使是同一个变量,生成的值也不相等:

let id1 = Symbol("id");
let id2 = Symbol("id");
console.log(id1 == id2); // false

全局共享的 Symbol

虽然 Symbol 保证了唯一性,但当我们想要多次使用同一个 Symbol 时,可以使用官方提供的全局注册并登记的方法 Symbol.for()

let name1 = Symbol.for('name'); // 检测到未创建后,新建
let name2 = Symbol.for('name'); // 检测到已创建后,返回
console.log(name1 === name2);   // true

可以使用 Symbol.keyFor() 来获取 Symbol 对象的参数值:

let name1 = Symbol.for('name');
let name2 = Symbol.for('name');
console.log(Symbol.keyFor(name1)); // 'name'
console.log(Symbol.keyFor(name2)); // 'name'

隐藏性

无法用 for...inobject.keys() 访问:

let id = Symbol("id");
let obj = {
	[id]: 'symbol'
};
for(let option in obj){
	console.log(obj[option]); // 空
}

在对象中查找用 Symbol 标识的属性名

可以用 Object.getOwnPropertySymbols 方法访问,该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值:

let id = Symbol("id");
let obj = {
	[id]: 'symbol'
};
let array = Object.getOwnPropertySymbols(obj);
console.log(array);          // [Symbol(id)]
console.log(obj[array[0]]);  // 'symbol'

Symbol 有什么用?

MDN 上是这样介绍的:

每个从 Symbol() 返回的 symbol 值都是唯一的。一个 symbol 值能作为对象属性的标识符;这是该数据类型仅有的目的。

它本质上是一种 唯一标识符,可用作对象的唯一属性名,这样其他人就不会改写或覆盖你设置的属性值。

防止属性名冲突

作为属性名的 Symbol

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

当一个 symbol 值作为对象属性的标识符,也就是说,将对象属性名指定为一个 Symbol 值时,有以下几种写法:

let mySymbol = Symbol();

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

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

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

// 以上写法都得到同样结果
console.log(a[mySymbol]); // "Hello!"

⚠️ 注意!

Symbol 值作为对象属性名时,不能用点运算符。

const mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!';

console.log(a[mySymbol]);   // undefined
console.log(a['mySymbol']); // "Hello!"

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


所以,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。如果不放在方括号中,那么该属性的键名就是一个字符串,而不是那个 Symbol 值。

实例:消除魔术字符串

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

风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。

function getArea(shape, options) {
  let area = 0;
  switch (shape) {
    case 'Triangle': // 魔术字符串
      area = .5 * options.width * options.height; // S = 1/2 * d * h
      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 具有隐藏性,所以我们可以很方便地用来模拟私有属性。

因为 symbol 不会出现在 Object.keys() 的结果中,因此除非你明确地 export 一个 symbol ,或者用 Object.getOwnPropertySymbols() 函数获取,否则其他代码是无法访问这个属性的。另外,symbol 也不会出现在 JSON.stringify() 的结果里,确切地说是 JSON.stringify() 会忽略 symbol 属性名和属性值。

const symbol = Symbol('test');
const obj = { [symbol]: 'test', test: symbol };

JSON.stringify(obj); // "{}"

参考资料