基本
ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是 JavaScript 的第七种数据类型。
Symbol 值通过 Symbol 函数生成。Symbol 函数前面不能使用 new 命令,否则会报错。
这是因为生成的 Symbol 是一种原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。
但是为什么不能 new 呢?百度找到一种解释:这样做是为了避免创建 Symbol 原始值包装类型。
在引用类型中,有三种原始值包装类型:String、Number、Boolean。原始值类型 "abc"、123 不是对象,原始值包装类型是用来把原始值包装成对象的引用类型。
除了 JS 自身内部处理可能会使用外,我们在开发的实际场景中,一般都不会使用。一般使用字面量的形式,不会创建对应的对象。对开发体验来讲是比较鸡肋的功能。新增的 Symbol、BigInt 没有必要向开发者提供创建对应原始值对象的 API。一般也不会使用,多此一举。
Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,在这里参数仅仅作为描述。利于区分。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
Symbol('foo') == Symbol('foo') // false
如果 Symbol 的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后再生成一个 Symbol 值。
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
sym // Symbol(abc)
Symbol({}) // Symbol([object Object])
Symbol 值不能和其他类型的值进行运算,会报错,但是可以显示的转为字符串。
let sym = Symbol('My symbol');
"your symbol is " + sym
// Uncaught TypeError: Cannot convert a Symbol value to a string
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
Symbol 可以转为布尔值,但是不能转为数值。
Boolean(Symbol())
// true
Number(Symbol())
// Uncaught TypeError: Cannot convert a Symbol value to a number
Symbol.prototype.description
返回 Symbol 的描述。
const sym = Symbol('foo');
sym.description // "foo"
作为属性名的 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 mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
因为点运算符后面总是字符串,所以不会读取 mySymbol 作为标识名所指代的那个值,导致 a 的属性名实际上是一个字符串 'mySymbol',而不是一个 Symbol 值。
同理,在对象内部使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。(其实不止是 Symbol 值,其他任何变量名都应该放在 [])
let s = Symbol();
let p = 'parma';
let obj = {
[s]: 1,
p: 2,
[p]: 3
};
console.log(obj);
// {
// p: 2,
// parma: 3,
// [Symbol()]: 1
// }
实例:消除魔术字符串
魔术字符串指的是,在代码中多次出现的,与代码形成强耦合的某一个具体的字符串或者数值。示例
属性名的遍历
Symbol 作为属性名,遍历对象的时候,该属性不会被 for...in...、for...of...循环出来,也不会被 Object.key()、Object.getOwnPropertyNames()、JSON.stringify()返回。
var a = {
[Symbol()]: 1,
b: 2
};
console.log(Object.keys(a));
console.log(Object.getOwnPropertyNames(a));
console.log(JSON.stringify(a));
// ["b"]
// ["b"]
// {"b":2}
获取 Symbol 属性名的方法:Object.getOwnPropertySymbol() 方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象所有用作属性名的 Symbol 值。
const obj = {};
const foo = Symbol('foo');
obj[foo] = 'bar';
for (let i in obj) {
console.log(i); // 无输出
}
Object.getOwnPropertyNames(obj) // []
Object.getOwnPropertySymbols(obj) // [Symbol(foo)]
obj[Object.getOwnPropertySymbols(obj)[0]] // bar
Reflect.ownKeys() 方法可以返回所以类型的键名,包括常规键名和 Symbol 键名。
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};
Reflect.ownKeys(obj)
// ["enum", "nonEnum", Symbol(my_key)]
Symbol.for()
接受一个字符串作为参数,然后搜索有没有以该参数作为名称(描述)的 Symbol 值。如果有就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true
Symbol.for() 不会每次调用就返回一个新的 Symbol 值,而是会先检查给定的 key(描述) 是否已经存在,如果不存在才会新建一个值。
Symbol.for() 有登记机制,Symbol() 没有登记机制。
注意:Symbol.for() 为 Symbol 值登记的名字,是全局的,不管有没有在全局环境中运行。
Symbol.keyFor()
返回一个已经登记的 Symbol 类型值的 key(描述)。
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
内置的 Symbol 值
Symbol.hasInstance
对象的 Symbol.hasInstance 属性,指向一个内部方法。当其他对象使用 instanceof 运算符时,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo 在语言内部时,实际调用的是 Foo[Symbol.hasInstance](foo)。
class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}
}
[1, 2, 3] instanceof new MyClass() // true
MyClass 是一个类,new MyClass() 会返回一个实例。该实例的 Symbol.hasInstance 方法,会在 instanceof 运算时自动调用,判断左边的运算实例是否为 Array 的实例。
class Even {
static [Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
}
// 等同于
const Even = {
[Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
};
1 instanceof Even // false
2 instanceof Even // true
1 instanceof Even ==> Even[Symbol.hasInstance](1) ==> Number(1) % 2 === 0 ===> false
Symbol.isConcatSpreadable
Symbol.isConcatSpreadable 属性等于一个布尔值,表示该对象用于 Array.prototype.concat() 时,是否可以展开。
Symbol.isConcatSpreadable 的默认值为 undefined,但是对于数组来说,默认是展开的。当然该属性为 true 也是可以展开的。
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined
let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
对于对象来说,默认则是不可展开。
let obj = {length: 2, 0: 'c', 1: 'd'};
['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e']
obj[Symbol.isConcatSpreadable] = true;
['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']