众所周知,ES5提供了6类基础数据类型: Number 、String、 Boolean、 Null、 undefined、 Object ,在后来的ES6中新增了一种数据类型symbol,但这个新的数据类型我们是不是会感觉到在实际的开发工作中没有场景会用到它?那今天我们就一起来认识一下这个新的数据类型吧。
1.基础概念:
symbol 是一种基本数据类型 (primitive data type),表示独一无二的值。
2.Symbol的唯一性和特殊性:
- symbol 是通过 Symbol 函数生成的:
let a = Symbol() typeof a // 'symbol'
这里代码就是通过Symbol函数,生成了一个独一无二的值a,通过typeof可以判断a的数据类型是Symbol。
- Symbol.prototype.description: 创建 Symbol 的时候,可以添加一个描述,表示对Symbol实例的描述,主要是为了在控制台打印或者转字符串的时候,更容易区分。
let s1 = Symbol('s1') let s2 = Symbol('s2') s1 // Symbol(s1) s2 // Symbol(s2) s1.toString() // "Symbol(s1)" s2.toString() // "Symbol(s2)"
通过传入参数s1和s2,对两个symbol进行描述,若不添加描述,在控制台打印s1 和 s2 的时候会打印出两个symbol,不方便进行区分。
同时也可以直接通过实例属性description,直接返回Symbol的描述。例如: s1.description //'s1'。
- 当Symbol函数的参数是一个对象是,会调用该对象的toString方法,将其转为字符串,然后再生产一个symbol值。
let obj = { toString () { return 'abc' } } let sym = Symbol(obj) sym // Symbol('abc')
- Symbol 函数的参数只是作为 symbol 值的描述,因此相同参数的 symbol 函数返回值也是不相等的(展示了它的独立唯一性)。
let s1 = Symbol()
let s2 = Symbol()
s1 === s2 // false
let s1 = Symbol('a')
let s2 = Symbol('a')
s1 === s2 // false
- 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值可以显示的转为字符串,也可以转为 Boolean,但是不可以转为数值。
let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
Boolean(sym) // true
!sym // false
Number(sym) // Cannot convert a Symbol value to a number
- 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作为属性名时,需要注意:不能用点运算符。
不能通过a.mySymbol获取到该属性的值,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的值,导致a的属性名是一个字符串,而不是一个Symbol值。
- Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。
let obj = {
a: 1,
[Symbol]: 3
}
obj // {a: 1, Symbol(): 3}
// Object.keys 无法获取到
Object.keys(obj) // ["a"]
// for of 也无法打印出来
for(item in obj) {
console.log(item)
}
// a
- 通过Object.getOwnPropertySymbols()方法可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 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)]
- Symbol.for(),Symbol.keyFor()
在某些特殊情况下,开发过程中可能需要使用同一个Symbol值,通过Symbol.for可以做到这点。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.for("bar") === Symbol.for("bar")
// true
Symbol("bar") === Symbol("bar")
// false
由于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 值登记的名字,是全局环境的,不管有没有在全局环境运行。
function foo() {
return Symbol.for('bar');
}
const x = foo();
const y = Symbol.for('bar');
console.log(x === y); // true
代码中,Symbol.for('bar')是函数内部运行的,但是生成的 Symbol 值是登记在全局环境的。所以,第二次运行Symbol.for('bar')可以取到这个 Symbol 值。
所以可以通过Symbol.for()的这个全局登记特性,可以用在不同的 iframe 或 service worker 中取到同一个值。
3.symbol的实际应用
- 使用Symbol来作为对象的唯一属性名(key):由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
let obj = {
[Symbol('name')]: '一斤代码',
age: 18,
title: 'Engineer'
}
Object.keys(obj) // ['age', 'title']
for (let p in obj) {
console.log(p) // 分别会输出:'age' 和 'title'
}
Object.getOwnPropertyNames(obj) // ['age', 'title']
根据我上面的介绍和这段代码可知,Symbol类型的key是不能通过Object.keys()或者for...in来枚举的,它未被包含在对象自身的属性名集合(property names)之中。所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用Symbol来定义。
也正因为这样一个特性,当使用JSON.stringify()将对象转换成JSON字符串的时候,Symbol属性也会被排除在输出内容之外:
JSON.stringify(obj) // {"age":18,"title":"Engineer"}
开发过程中就可以利用这一特点来更好的设计我们的数据对象,让“对内操作”和“对外选择性输出”变得更加优雅。
当然,不排除我们想要获取到对象所有的 key 值的需求,所以也会有一些专门针对Symbol的API,比如: Reflect.ownKeys
// 使用Object的API
Object.getOwnPropertySymbols(obj) // [Symbol(name)]
// 使用新增的反射API
Reflect.ownKeys(obj) // [Symbol(name), 'age', 'title']
- 使用symbol定义常量,保证常量的唯一性。
const obj = {}
obj.alw = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
} //通过symbol定义一组常量,保证这组常量的值都是不相等的
通过这个性质,可以消除魔术字符串。
魔术字符串:在代码中多次出现,且与代码形成强耦合的某一个具体的字符串或者数值,这种关联性太多的字符串就会导致变量含义不明确,所以应当尽量消除魔术字符串,改由含义清晰的变量代替;
耦合性:常用来表示块之间的联系,耦合性越强,其代码块或者模块之间的联系就越强,设计代码块或者模块就是为了其功能的独立性,所以往往更希望低耦合;
内聚性:常用来表示块内在的联系,内聚性越强,模块功能强度越强,即一个模块内各元素(语名之间、程序段之间)联系的就越紧密,所以一般就更希望高内聚;
简单粗暴的讲就是魔术字符串是出现多次的某个具体的字符串或者数值,就和代码形成了强大的联系,不利于以后的修改和维护。
实例:
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()
};
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 });
- 在 Javascript中使用 Symbol 定义类的私有属性/方法:因为在 Javascript 中,是没有 private 的,类上所有定义的属性和方法都是可公开访问的,但是现在可以通过Symbol 和 模块化,就可以实现类的私有属性和方法。
实例:
a.js
const PASSWORD = Symbol(); // 这个 PASSWORD 只能在a.js中使用了
class Login {
constructor (username, password) {
this.username = username
this[PASSWORD] = password
}
checkPassword (pwd) {
return this[PASSWORD] === pwd
}
}
export default Login
b.js
import Login from './a'
const login = new Login('chencc', '123')
login.checkPassword('123') // true
login[PASSWORD] // ReferenceError: PASSWORD is not defined
因为 PASSWORD 被定义在a.js所在的模块中,外面的模块获取不到这个 Symbol,也不可能在创建一个一摸一样的Symbol出来,因此这个 PASSWORD 只能在a.js内部中使用,所以使用它来定义的类属性是没有办法被模块外访问到的,达到了一个私有化的效果。