第十章 Symbol
在本章节阮一峰老师就 Symbol
的定义和作用展开描述,其中重点讲解了概述、属性名遍历、for()
和 keyFor()
方法,最后来两个实例。
概述
在 ES5 时,如果使用了他人的数据对象,但又想添加自己的数据,很容易会因为属性名冲突而导致数据覆盖。因为 ES5 对象的属性名都是字符串型。
为了解决这个问题,ES6 推出了 Symbol,他是第7种数据类型。
🖇 拓展
前6种数据类型分别是 undefined、null、布尔值(boolean)、字符串(string)、数值(number)、对象(object)。
通过 Symbol 函数生成,是第一无二的,不会与其他属性名造成冲突。可以接收一个字符串,主要用于控制台打印区分。
let s = Symbol()
let b = Symbol('bar')
console.log(typeof s) // symbol
console.log(typeof b) // symbol(bar)
⚠️ 注意
Symbol 函数前不能使用 new 命令,否则会报错 这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是 一种类似于字符串的数据类型。
若 Symbol 的参数是一个对象,则会调用对象的 toString 方法转为字符串后再生成 Symbol 值。
const obj = {
toString() {
return 'abc'
}
}
const sym = Symbol(obj)
console.log(sym) // Symbol(abc)
Symbol 函数的参数只表示对当前 Symbol 值的描述,因此相同参数的 Symbol 函数的返回值是不相等的。
const s1 = Symbol()
const s2 = Symbol()
s1 === s2 // false
const b1 = Symbol('bar')
const b2 = Symbol('bar')
b1 === b2 // false
最后,Symbol 不可以参与运算,可以转为布尔值和字符串,不可转为数值。
const sym = Symbol('abc')
console.log('symbol is:' + sym) // Uncaught TypeError: Cannot convert a Symbol value to a string
console.log(`symbol is: ${sym}`) // Uncaught TypeError: Cannot convert a Symbol value to a string
console.log(String(sym)) // abc
console.log(sym.toString()) // abc
console.log(Boolean(sym)) // true
console.log(!sym) // false
Number(sym) // Uncaught TypeError: Cannot convert a Symbol value to a number
作为属性名的Symbol
在ES6中,Symbol(符号)是一种新的基本数据类型,它的主要特点是其值是唯一且不可变的。这使得Symbol特别适合作为对象属性名,用于解决属性名冲突的问题,保证属性不会被意外修改或覆盖。
使用Symbol作为对象属性名的几种写法:
方括号结构
jsvar mySymbol = Symbol(); var a = {}; a[mySymbol] = 'Hello!'; console.log(a[mySymbol]); // 输出: "Hello!"
方括号定义属性
jsvar mySymbol = Symbol(); var a = { [mySymbol]: 'Hello!' }; console.log(a[mySymbol]); // 输出: "Hello!"
使用Object.defineProperty
jsvar mySymbol = Symbol(); var a = {}; Object.defineProperty(a, mySymbol, { value: 'Hello!' }); console.log(a[mySymbol]); // 输出: "Hello!"
注意事项:
点运算符无法使用Symbol作为属性名
因为点运算符后面总是字符串,不会将Symbol作为标识符,而是将其转换为字符串。同理,在对象的 使用 Symbol 值定义属性时, mbol 值必须放在方括号中。
jsvar mySymbol = Symbol(); var a = {}; a.mySymbol = 'Hello!'; // 错误的写法,属性名实际是字符串 "mySymbol" console.log(a[mySymbol]); // undefined console.log(a['mySymbol']); // 输出: "Hello!" a = { [mySymbol]: function() {} }
Symbol作为属性名是公开属性
使用Symbol作为属性名时,并不会使属性成为私有属性,仍然可以通过其他方式访问到这些属性。
Symbol还可以用于定义一组唯一且不可变的常量,确保不同常量之间不会发生值相等的情况,例如日志级别或颜色常量。
const LOG_LEVELS = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
};
function log(level, message) {
// 省略具体实现
}
log(LOG_LEVELS.DEBUG, 'Debug message');
log(LOG_LEVELS.INFO, 'Info message');
这种方式保证了每个常量都是独一无二的Symbol,可以用于标识不同的日志级别或其他场景下的常量。
总而言之,Symbol
作为 ES6 引入的一种新的基本数据类型,用于创建唯一的标识符,特别适合作为对象属性名或常量的定义。它的唯一性可以有效地防止属性名冲突,并且保证了常量值的独特性,是 JavaScript 语言中更加安全和清晰的编程实践之一。
实例:消除魔术字符串
魔术字符串是指在代码中多次出现、与代码强耦合的具体字符串或值。在编写风格良好的代码时,应尽量消除魔术字符串,用含义清晰的变量或常量代替,以提高代码的可读性、可维护性和安全性。
使用命名的常量或变量
最简单的方法是将魔术字符串定义为命名的常量或变量,例如:
js// 使用命名的常量 const SHAPE_TRIANGLE = 'Triangle'; function getArea(shape, options) { let area = 0; switch (shape) { case SHAPE_TRIANGLE: area = 0.5 * options.width * options.height; break; // 其他形状的处理 } return area; } getArea(SHAPE_TRIANGLE, { width: 100, height: 100 });
将字符串
'Triangle'
定义为常量SHAPE_TRIANGLE
,用来代替原来的魔术字符串。这样做可以减少代码中字符串的重复出现,提高代码的可维护性。使用对象属性
另一种方法是将魔术字符串作为对象的属性,这种方式也可以避免字符串的直接出现:
js// 使用对象属性 const shapeType = { triangle: 'Triangle' }; function getArea(shape, options) { let area = 0; switch (shape) { case shapeType.triangle: area = 0.5 * options.width * options.height; break; // 其他形状的处理 } return area; } getArea(shapeType.triangle, { width: 100, height: 100 });
shapeType
对象中的triangle
属性存储了字符串'Triangle'
,在getArea
函数中使用shapeType.triangle
来替代原始的魔术字符串。使用Symbol作为属性值
如果确保每个属性的值唯一且不变是关键,可以考虑使用Symbol作为属性值:
js// 使用Symbol作为属性值 const shapeType = { triangle: Symbol() }; function getArea(shape, options) { let area = 0; switch (shape) { case shapeType.triangle: area = 0.5 * options.width * options.height; break; // 其他形状的处理 } return area; } getArea(shapeType.triangle, { width: 100, height: 100 });
由于 shapeType.triangle` 的值是一个Symbol,保证了它的唯一性。这种做法适用于需要确保属性值不与其他任何值冲突的情况,同时能有效地消除魔术字符串。
总而言之,消除魔术字符串可以通过使用命名的常量、对象属性或Symbol来实现。这些方法可以提高代码的可读性和可维护性,减少由于字符串拼写错误或修改时遗漏更新导致的bug。
属性名的遍历
前面有介绍到,Symbol
并不是私有属性,而是公有属性,但是不会被 for...of
、for...in
、Object.keys()
、Object.getOwnPropertyNames()
等方法遍历返回。
若需要获取某个对象的 Symbol
属性名,则需要通过 Object.getOwnPropertySymbols
方法获取。该方法返回的是一个数组,每一项是当前对象所有用作属性名的 Symbol
值。
let a = Symbol('a')
let b = Symbol('b')
let f = Symbol('f')
let obj = {
[a]: 'hello',
[b]: 'world',
}
Object.defineProperty(obj, f, {
value: 'foo'
})
for (let i in obj) {
console.log(i) // 无输出
}
Object.getOwnPropertyNaames(obj) // []
let objSym = Object.getOwnPropertySymbols(obj)
console.log(objSym) // [Symbol(a), Symbol(b), Symbol(f)]
另外,Reflect.ownKwys
方法可以返回所有类型的键名,代码如下:
let obj = {
[Symbol('key')]: 1,
value: 'daodao'
}
Reflect.ownKwys(obj) // ['value', Symbol('key')]
由于用 Symbol
定义的属性不会被常规方法遍历,因此可以利用其这一特性来定义一些非私有但只希望内部使用的方法。
Symbol.for()、Symbol.keyFor()
Symbol
与 Symbol.for()
都会生成新的 Symbol
,二者的区别是 Symbol
每次生成都是返回新的 Symbol
值;而 Symbol.for()
会在全局中提供搜索,如果全局中存在该 key
值的 Symbol
值,则会返回同一个 Symbol
值。
Symmbol.for('bar') === Symmbol.for('bar') // true
Symbol('foo') === Symbol('foo') // false
Symbol.keyFor()
则会返回已登记的 Symbol
类型的 key
。
let s1 = Symbol.for('foo')
Symbol.keyFor(s1) // foo
let s2 = Symbol('bar')
Symbol.keyFor(s2) // undefined
🖇 拓展
Symbol.for()
为 Symbol
值登记的名字是全局环境的,可以在不同的 iframe 或 service worker 中取到同一个值。
实例:模块的Singleton模式
Singleton 模式确保一个类只有一个实例,并提供一个全局访问点。在 Node.js 中,模块文件可以视作一个类,如何保证每次加载模块时都返回同一个实例呢?
方法一:使用全局变量
_foo
。js// mod.js function A() { this.foo = 'hello'; if (!global._foo) { global._foo = new A(); module.exports = global._foo; } } // 使用方式 const a = require('./mod.js'); console.log(a.foo); // 输出 'hello'
问题: 全局变量
_foo
可被其他模块修改,可能导致实例失真。方法二:使用 Symbol 作为键名
js// mod.js const FOO_KEY = Symbol.for('foo'); function A() { this.foo = 'hello'; if (!global[FOO_KEY]) { global[FOO_KEY] = new A(); module.exports = global[FOO_KEY]; } } // 使用方式 const a = require('./mod.js'); console.log(a.foo); // 输出 'hello'
优点: 使用
Symbol.for('foo')
作为键名,外部无法直接引用到该 Symbol,可以避免意外覆盖。问题: 全局变量依然可以被改写,例如
global[Symbol.for('foo')] = 123;
。方法三:使用局部 Symbol
js// mod.js const FOO_KEY = Symbol('foo'); function A() { this.foo = 'hello'; if (!global[FOO_KEY]) { global[FOO_KEY] = new A(); module.exports = global[FOO_KEY]; } } // 使用方式 const a = require('./mod.js'); console.log(a.foo); // 输出 'hello'
优点: 使用局部 Symbol,其他脚本无法引用到
FOO_KEY
,避免外部修改。问题: 多次执行模块时,每次得到的
FOO_KEY
都不同,不过一般情况下 Node.js 会缓存模块的执行结果,不会多次执行同一个脚本。
💡 总结
- 全局变量方法容易被意外覆盖,不安全。
- Symbol 作为键名可以保证键名不被外部轻易修改,但仍可能被改写。
- 局部 Symbol可以防止外部修改,但多次执行脚本时每次得到的 Symbol 不同,不过 Node.js 通常会缓存模块执行结果,一般不会影响单例实例的创建。
内置的Symbol值
ES6 还提供了 11个内置的 Symbol
值,指向语言内部使用的方法。
Symbol.hasInstance
指向一个内部方法,对象使用
instanceof
运算符时会调用该方法。如foo instance Foo
内部实际上是Foo[Symbol.hasInstance](foo)
。jsclass Event { [Symbol.hasInstance] (foo) { return foo instanceof Array } } [1, 2, 3] instanceof new Event() // true {a: 1} instanceof new Event() // false class OwnNum { static [Symbol.hasInstance] (foo) { return foo % 2 === 0 } } 1 instanceof new OwnNum() // false 2 instanceof new OwnNum() // true
Symbol.isConcatSpreadable
该属性是一个布尔值,表示对象或数组在使用
concat
方法时是否可以展开。数组默认是
true
或undefined
,表示可以展开;若手动修改为false
,则表示不可展开。jslet arr1 = [1, 2] ['a', 'b'].concat(arr1, '开') // ['a', 'b', 1, 2, '开'] let arr2 = [3, 4] arr2[Symbol.isConcatSpreadable] = false ['c', 'd'].concat(arr2, '不开') // ['c', 'd', [3, 4], '不开']
对象默认是
false
不可展开,想要展开需要手动设置为true
。jslet obj1 = {length: 1, 0: 'a'} [1, 2].concat(obj, 'open') // [1, 2, obj, 'open'] let obj2 = {length: 1, 0: 'b'} obj2[Symbol.isConcatSpreadable] = true [3, 4].concat(obj, 'open') // [3, 4, 'b', 'open']
对于类而言,
Symbol.isConcatSpreadable
属性必须写为类的实例。jsclass A extends Array { constructor(args) { super(args) this[Symbol.isConcatSpreadable] = true } } class B extends Array { constructor(args) { super(args) this[Symbol.isConcatSpreadable] = false } } let a1 = new A() a1[0] = 1 a1[1] = 2 let b1 = new B() b1[0] = 3 b1[1] = 4 ['a', 'b'].concat(a1).concat(b1) // ['a', 'b', 1, 2, [3, 4]]
Symbol.species
Symbol.species
是一个内置的 Symbol 属性,用于定义在执行数组方法时,如何创建新的派生类实例。这个属性允许子类覆盖父类的默认行为,控制返回的实例类型。默认情况下,若未定义
Symbol.species
,这些方法将返回父类(即调用方法的类)的实例。如果定义了,则返回由Symbol.species
指定的构造函数的实例。jsclass MyArray extends Array { // 使用 get 读取器定义 Symbol.species 属性 static get [Symbol.species]() { return Array; } } let a = new MyArray(1, 2, 3); let mapped = a.map(x => x * x); console.log(mapped instanceof MyArray); // false console.log(mapped instanceof Array); // true
Symbol.match
对象的 Symbol match 属性指向 个函数 str match myObject )时 ,如果该 属性存在 会调用它返回该方法的返回值。
jsString.prototype.match(regexp) //等同于 regexp[Symbol.match](this) class MyMatcher { [Symbol.match] (string) { return 'hello world'.indexOf(string) } } 'e'.match(new MyMatcher()) // 1 class BooleanMatch { [Symbol.match] (string) { return Boolean(string) } } 1.match(new BooleanMatch()) // true
Symbol.replace
使用
Symbol.replace
属性可以自定义对象在字符串的replace
方法中的行为。该属性定义的方法会在字符串
replace
方法调用时被调用,接收两个参数:第一个是当前作用的字符串对象,第二个是替换值。js// 定义一个空对象 x const x = {}; // 使用 Symbol.replace 属性定义一个方法 x[Symbol.replace] = (...s) => console.log(s); // 调用字符串的 replace 方法,并传入 x 对象作为搜索值 'Hello'.replace(x, 'World'); // 控制台输出为:[ 'Hello', 'World' ]
Symbol.search
使用
Symbol.search
属性可以自定义对象在字符串的search
方法中的行为。该属性定义的方法会在字符串
search
方法调用时被调用,接收一个字符串参数,表示当前进行搜索的字符串。js// 定义一个自定义的搜索类 MySearch class MySearch { constructor(value) { this.value = value; } // 使用 Symbol.search 属性定义一个方法 [Symbol.search](string) { return string.indexOf(this.value); } } // 调用字符串的 search 方法,并传入 MySearch 类的实例作为正则表达式参数 console.log('foobar'.search(new MySearch('foo'))); // 输出为 0
Symbol.split
当对象被
String.propertype.split
方法调用时会返回该方法的返回值。Symbol.iterator
Symbol.toPrimitive
Symbol.toStringTag
Symbol.unscopables
总结
Symbol
是ES6引入的第七种数据类型,通过 Symbol
函数生成,具有唯一性,不会与其他属性名冲突,适合用作对象属性名以避免冲突。Symbol
可以接收一个字符串作为描述,但这个描述并不影响 Symbol
的相等性比较。Symbol
值不能参与运算,可以转为布尔值和字符串,但不能转为数值。
Symbol
作为属性名时,不会被常规的属性遍历方法发现,只能通过 Object.getOwnPropertySymbols()
获取。此外,Symbol.for()
和 Symbol.keyFor()
用于全局 Symbol
的创建和查找。Symbol
的这些特性使其在消除魔术字符串、实现Singleton
模式等方面非常有用。
ES6还提供了11个内置的 Symbol
值,用于语言内部的方法,如 Symbol.iterator
、Symbol.toStringTag
等,这些内置 Symbol
值影响对象默认行为和内置方法的实现。