# 理解 Es6 中的 Symbol 类型
# 快速导航
# 前言
在 Es6 中引入了一个新的基础数据类型:Symbol
,对于其他基本数据类型(数字number
,布尔boolean
,null
,undefined
,字符串string
)想必都比较熟悉,但是这个Symbol
平时用得很少,甚至在实际开发中觉得没有什么卵用,能够涉及到的应用场景屈指可数.
往往在面试的时候,屡面不爽.下面一起来看看的这个数据类型的
# 具体解决的问题
在 Es5 的对象属性名中都是字符串,当一对象的属性名出现重复时,后者往往会覆盖前者.
若使用Symbol
就能够保证每个属性的名字都是独一无二的,相当于生成一个唯一的标识 ID,这样就从根本上防止属性名的冲突
# Symbol 类型
symbol
是Es6
规范引入的一项新的特性,表示独一无二的值,归纳为JS
语言的第 7 种数据类型,它是通过Symbol
函数生成
通过Symbol()
函数来创建生成一个Symbol实例
let s1 = Symbol();
console.log(typeof s1); //symbol
console.log(Object.prototype.toString.call(s1)); // [object Symbol]
2
3
在上面示例代码中,用typeof
进行了类型的检测,它返回的是Symbol
类型,而不是什么string
,object
之类的
在 Es5
中原有的对象的属性名是字符串类型中拓展了一个Symbol
类型,也就是说,现在对象的属性名有两种类型
- 字符串类型
Symbol
类型
注意
Symbol
函数前不能使用new
关键字,否则就会报错,这是因为生成的Symbol
是一个原始类型的值,它不是对象
因为不是对象,所以也不能添加属性,它是一种类似于字符串的数据类型,可以理解为是在字符串类型的一种额外的拓展
Symbol
函数可以接收一个字符串做为参数,它是对该Symbol
实例的一种描述,主要是为了在控制台显示
Symbol 的描述是可选的,仅用于调试目的或转为字符串时,进行区分,不是访问 symbol 本身
可以使用Symbol().description
会返回Symbol()
的实例描述的具体内容,如果有值,则会返回该描述,若无则会返回undefined
description
是Symbol
的一个静态属性
当使用字符串定义对象的属性名时,若出现同名属性,则会出现属性覆盖问题,而使用Symbol
类型定义的对象属性名,则不会,它是独一无二的,每调用一次Symbol()
都会生成一个唯一的标识,即使是使用Symbol()
生成的实例描述相同,但它们依旧不相等,总会返回false
如下代码所示
let s1 = Symbol('itclanCoder'); // 定义了一s1变量,它是Symbol()类型,并接收了一个itclanCoder字符串,作为该Symbol的实例
let s2 = Symbol('itclanCoder'); // 实例化了一个s2,Symbol()类型
console.log(s1.description); // itclanCoder
console.log(s1.description); // itclanCoder
console.log(s1 === s2); // false
2
3
4
5
从第 5 行代码比较结果看出,s1
与s2
是两个不同的Symbol
值,这里让Symbol
接受一个参数,如果不加参数,它们在控制台输出的都是Symbol
,即使参数相同,但是它们依旧是两个不同的Symbol
如果您希望使用拥有同一个Symbol
值,那该怎么办?在 Es6 中,提供了一个Symbol.for()
方法可以实现,它接受一个字符串作为参数
然后搜索有没有以该参数作为名称的Symbol值
如果有,就返回这个Symbol值
,否则就新建一个以该字符串为名称的Symbol值
,并会将它注册到全局坏境中
let s1 = Symbol.for('itclanCoder');
let s2 = Symbol.for('itclanCoder');
console.log(s1 === s2); // true
2
3
在上面的示例代码中,s1
和 s2
都是Symbol
实例化出来的值,但是它们都是由Symbol.for
方法生成的,指向的是同一个值,地止
#
Symbol
与Symbol.for
的区别
比较
共同点: 都会生成新的Symbol
不同点: Symbol.for()
会被登记在全局坏境中供搜索,而Symbol()
不会,Symbol.for()
不会每次调用就返回一个新的Symbol
类型的值,而是会先检查给定的key
是否已经存在,如果不存在才会新建一个Symbol
值
如:调用Symbol.for('itclanCoder')
100 次,每次都会返回同一个Symbol
值,但是调用Symbol('itclanCoder')
100 次,会返回 100 个不同的Symbol
值
Symbol.for("itclanCoder") === Symbol.for("itclanCoder") // true
Symbol("itclanCoder") === Symbol("itclanCoder") // false
2
3
在上面代码中,由于Symbol()
写法没有登记机制,所以每次调用都会返回一个不同的值,也就是每次都会在栈内存中重新开辟一块空间
也可以通过Symbol.keyFor()
方法返回一个已登记的Symbol
类型值的key
,通过该方法检测是否有没有全局注册
let s1 = Symbol.for("itclan");
console.log(Symbol.keyFor(s1)) // "itclan"
let s2 = Symbol("itclan");
console.log(Symbol.keyFor(s2)) // undefined
2
3
4
5
6
在上面的代码中,变量s2
属于未被登记的Symbol
值,所以就返回undefined
注意
Symbol.for()
是为Symbol
值登记的名字,在整个全局作用域范围内都起作用
function foo() {
return Symbol.for('itclan');
}
const x = foo();
const y = Symbol.for('itclan');
console.log(x === y); // true
2
3
4
5
6
7
在上面代码中,Symbol.for('itclan')
是在函数内部运行的,但是生成的 Symbol 值
是登记在全局环境的。所以,第二次运行Symbol.for('itclan')
可以取到这个 Symbol 值
# 应用场景:Symbol.for()
这个全局记录特性,可以用在不同的iframe
火service worker
中取到同一个值
在前端开发中,有时候会用到iframe
,但是iframe
之间相互隔离的,有时候想要取到不同的iframe
中同一份数据,那么这个Symbol.for()
就派上用场了的
如下示例代码所示
点击即可查看
let iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);
iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo') // true
2
3
4
5
在上面代码中,iframe
窗口生成的Symbol 值
,可以在主页面拿得到,在整个全局作用域内都可以取到
# Symbol 应用场景
# 应用场景 1-使用
Symbol
来作为对象属性名(key)
在 Es6 之前,通常定义或访问对象的属性都是使用字符串,如下代码所示
let web = {
site: "http://itclan.cn",
name: "itclanCoder"
}
console.log(web['site']); // http://itclan.cn
console.log(web['name']); // itclanCoder
2
3
4
5
6
访问变量对象的属性,除了可以通过对象.属性名
的方式外,可以通过对象['属性名']
的方式进行访问,如果一个对象中出现了同名属性那么后者会覆盖前者
由于每调用一次Symbol
函数,生成的值都是不相等的,这意味着Symbol
值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性
针对一个对象由多个模块构成的情况就变得非常有用了的,使用Symbol
能放置某一个键被不小心改写或覆盖
Symbol
可以用于对象属性的定义和访问
如下示例代码所示
点击即可查看
const PERSON_NAME = Symbol();
const PERSON_AGE = Symbol();
let person = {
[PERSON_NAME]: "随笔川迹"
}
person[PERSON_AGE] = 20;
console.log(person[PERSON_NAME]) // 随笔川迹
console.log(person[PERSON_AGE]) // 20
2
3
4
5
6
7
8
9
10
11
在上面的示例代码中,使用Symbol
创建了PERSON_NAME
,PERSON_AGE
两个Symbol
类型,但是在实际开发中却带来了一些问题
当您使用了Symbol
作为对象的属性key
后,你若想对该对象进行遍历,于是用到了Object.keys()
,for..in
,for..of
,Object.getOwnPropertyNames()、JSON.stringify()
进行枚举对象的属性名
你会发现使用Symbol
后会带来一个非常令人难以接受的现实,如下示例代码所示
let person = {
[Symbol('name')]: '随笔川迹',
age: 20,
job: 'Engineer'
}
console.log(Object.keys(person)) // ["age", "job"]
for(var i in person) {
console.log(i); // age job
}
Object.getOwnPropertyNames(person) // ["age", "job"]
JSON.stringify(person); // "{"age":20,"job":"Engineer"}"
2
3
4
5
6
7
8
9
10
11
12
通过上面的示例代码结果可知,Symbol
类型实例化出的key
是不能通过Object.keys()
,for..in
,for..of
,来枚举的
它也没有包含子自身属性集合Object.getOwnPropertyName()
当中,该方法无法获取到
利用该特性,我们可以把一些不需要对外操作和访问的属性使用Symbol
来定义
这样,我们在定义接口的数据对象时,可以决定对象的哪些属性,对内私有操作与对外公有操作变得可控,更加的方便
使用常规的方法,无法获取到以Symbol
方式定义对象的属性,在 Es6 中,提供了一个专门针对Symbol
的 API
用Object.getOwnPropertySymbols()
方法,可以获取指定对象的所有Symbol
属性名,该方法会返回一个数组
它的成员是当前对象的所有用作属性名的 Symbol 值
点击即可查看
let person = {
[Symbol('name')]: '随笔川迹',
age: 20,
job: 'Engineer'
}
// 使用Object的API
Object.getOwnPropertySymbols(person) // [Symbol(name)]
2
3
4
5
6
7
8
9
如下是Object.getOwnPropertySymbols()
方法与for..in
循环,Object.getOwnPropertyNames
方法进行对比的例子
点击即可查看
const person = {};
const name = Symbol('name');
person[name] = "随笔川迹"
for(let i in person) {
console.log(i); // 无任何输出
}
Object.getOwnPropertyNames(person); // []
Object.getOwnPropertySymbols(person); // [Symbol('name')]
2
3
4
5
6
7
8
9
10
11
在上面代码中,使用for...in
循环和Object.getOwnPropertyNames()
方法都得不到 Symbol 键名
,需要使用Object.getOwnPropertySymbols()
方法。
如果想要获取全部的属性,可以使用一个新的 API,Reflect.ownKeys()
方法可以返回所有类型的键名,包括常规键名和 Symbol
键名
点击即可查看
let person = {
[Symbol('name')]: "川川",
enum: 2,
nonEnum: 3
};
Reflect.ownKeys(person) // ["enum", "nonEnum", Symbol(name)]
2
3
4
5
6
7
正由于以Symbol 值
作为键名,不会被常规方法(for..in
,for..of
)遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法,达到保护私有属性的目的
# 应用场景 2:使用 Symbol 定义类的私有属性/方法
JavaScript 是一弱类型语言,弱并不是指这个语言功能弱,而所指的是,它的类型没有强制性,是没有如java
等面向对象语言的访问控制关键字private
的,类上所有定义的属性和方法都是公开访问的,当然在TypeScript
中新增了一些关键字,解决了此问题的
有时候,类上定义的属性和方法都能公开访问,会造成一些困扰
而有了Symbol
类的私有属性和方法成为了实现
如下示例代码
点击即可查看
let size = Symbol('size'); // 声明定义了一个size变量,类型是Symbol(),类型描述内容是size
class Collection { // class关键字定义了一个Collection类
constructor() { // 构造器`constructor`函数
this[size] = 0; // 在当前类上私有化了一个size属性
}
add(item) { // Collection类下的一个方法
this[this[size]] = item;
this[size]++;
}
static sizeOf(instance) { // 静态属性
return instance[size];
}
}
let x = new Collection(); // 实例化x对象
Collection.sizeOf(x) // 0
x.add('foo'); // 调用方法
Collection.sizeOf(x) // 1
Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
上面代码中,对象 x
的 size
属性是一个 Symbol
值,所以 Object.keys(x)
、Object.getOwnPropertyNames(x)
都无法获取它。这就造成了一种非私有的内部方法的效果
# 应用场景 3-模块化机制
结合Symbol
与模块化机制,类的私有属性和方法完美实现,如下代码所示
在文件a.js
中
点击即可查看
const PASSWORD = Symbol(); // 定义一个PASSWORD变量,类型是Symbol
class Login() { // class关键字声明一个Login类
constructor(username, password) { // 构造器函数内初始化属性
this.username = username;
this[PASSWORD] = password;
}
checkPassword(pwd) {
return this[PASSWORD] === pwd;
}
}
export default Login;
2
3
4
5
6
7
8
9
10
11
12
13
14
在文件b.js
中
点击即可查看
import Login from './a'
const login = new Login('itclanCoder', '123456'); // 实例化一个login对象
login.checkPassword('123456'); // true
login.PASSWORD; // 访问不到
login[PASSWORD]; // 访问不到
login['PASSWORD'] // 访问不到
2
3
4
5
6
7
8
9
因为通过Symbol
定义的PASSWORD
常量定义在a.js
模块中,外面的模块是获取不到这个Symbol
的,在外部无法引用这个值,也无法改写,也不可能在在创建一个一模一样的Symbol
出来
因为Symbol
是唯一的
在a.js
模块中,这个PASSWORD
的Symbol
类型只能在当前模块文件(a.js
)中内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的
这样就达到了一个私有化的效果
# 应用场景 4-使用
Symbol
来替代常量
在使用React
中,结合Redux
做公共数据状态管理时,当想要改变组件中的某个状态时,reducer
是一个纯函数,它会返回一个最新的状态给store
,返回的结果是由action
和state
共同决定的
action
是一个对象,有具体的类型type
值,如果你写过几行Redux
的代码,就会常常看到,进行action
的拆分,将事件动作的类型定义成常量
点击即可查看
const CHANGE_INPUT_VALUE = 'CHANGE_INPUT_VALUE'; // 监听input框输入值的常量
const ADD_INPUT_CONTENT = 'ADD_INPUT_CONTENT'; // 添加列表
const DELETE_LIST = 'DELETE_LIST'; // 删除列表
function reducer(state, action) {
const newState = JSON.parse(JSON.stringify(state));
switch(action.type) {
case CHANGE_INPUT_VALUE:
// ...
case ADD_INPUT_CONTENT:
// ...
case DELETE_LIST;
// ...
default:
return state;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
以上代码在Redux
中很常见,将action
对象中的type
值,给抽离出来,定义一个常量存储,来代表一种业务逻辑,通常希望这些常量是唯一的,在Redux
中定义成常量,是为了便于调试查错
常常因为取type
值时,非常苦恼.
现在有了Symbol
,改写一下,就可以这样
const CHANGE_INPUT_VALUE = Symbol()
const ADD_INPUT_CONTENT = Symbol();
const DELETE_LIST = Symbol()
function reducer(state, action) {
const newState = JSON.parse(JSON.stringify(state));
switch(action.type) {
case CHANGE_INPUT_VALUE:
// ...
case ADD_INPUT_CONTENT:
// ...
case DELETE_LIST;
// ...
default:
return state;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
通过Symbol
定义字符串常量,就保证了三个常量的值唯一性
划重点
- 常量使用
Symbol
值最大的好处,就是其他任何值都不可能有相同的值了,可以保证常量的唯一性,因此,可以保证上面的switch
语句按照你设计的方式条件去工作 - 当
Symbol
值作为属性名时,该属性是公开属性,不是私有属性
# 应用场景 5-注册和获取全局的
Symbol
在浏览器窗口(window
)中,使用Symbol()
函数来定义生成的Symbol
实例是唯一的
但是若应用涉及到多个window
,最常见的就是在各个页面窗口中嵌入iframe
了,并在各个iframe
页面中取到来自同一份公共的数据源
也就是在各个window
中使用的某些Symbol
希望是同一个,那么这个时候,使用Symbol()
就不行不通了
因为用它在不同window
中创建的Symbol实例
总是唯一的,而我们需要的是在所有这些window环境下
保持一个共享的Symbol
值。
在这种情况下,我们就需要使用另一个 API 来创建或获取Symbol
,那就是Symbol.for()
,它可以注册或获取一个window
间全局的Symbol实例
,它是Symbol
的一个静态方法
这个在前面已经提到过一次,这个还是有那么一点点用处,所以在提一嘴的
如下示例代码所示
let gs1 = Symbol.for('global_symbol_1') //注册一个全局Symbol
let gs2 = Symbol.for('global_symbol_1') //获取全局Symbol
console.log(gs1 === gs2 ) // true
2
3
4
经过Symbol.for()
实例化出来的Symbol
字符串类型,只要描述的内容相同,那么不光是在当前window
中是唯一的,在其他全局范围内的window
也是唯一的,并且相同
该特性,若是创建跨文件可用的symbol
,甚至跨域(每个window
都有它自己的全局作用域) , 可以使用 Symbol.for()
取到相同的值
也就是说,使用了Symbol.for()
在全局范围内,Symbol
类型值可以共享
# 注意事项
#
Symbol
值不能与其他类型的值进行运算-会报错
let symItclan = Symbol('itclan');
console.log("主站" + symItclan)
console.log(`主站 ${symItclan}`) // Uncaught TypeError: Cannot convert a Symbol value to a string
2
3
4
#
Symbol
可以显示转为字符串
let SyItclanCoder = Symbol('https://coder.itclan.cn');
console.log(String(SyItclanCoder)) // Symbol(https://coder.itclan.cn)
console.log(SyItclanCoder.toString()) // Symbol(https://coder.itclan.cn)
2
3
4
#
Symbol
值可以转为布尔值,但是不能转为数值
let sym = Symbol();
console.log(Boolean(sym)) // true
console.log(!sym) // false
if (sym) {
// ...
}
Number(sym) // TypeError Cannot convert a Symbol value to a number
sym + 2 // TypeError
2
3
4
5
6
7
8
9
10
由上面的错误提示可以看出,Symbol
不能转换为数字,无法做相应的运算
#
Symbol
函数不能使用new
命令
Symbol
函数前不能使用new
命令,否则就会报错,Symbol
是一个原始类型的值,不是对象,它是类似字符串的数据类型
#
Symbol值
作为对象属性名时,不能用点运算符
当Symbol
值作为对象的属性名时,访问它时,不能用点运算符
const username = Symbol();
const person = {};
person.username = '随笔川迹';
person[username]; // undefined
person['username']; // 随笔川迹
2
3
4
5
第 4 行代码值为undefined
,因为点运算符后面总是字符串,所以不会读取username
作为标识符名所指代的那个值
导致person
对象的属性名实际上是一个字符串,而不是一个Symbol
值
由此可见:在对象内部,使用Symbol
类型定义属性名时,Symbol
值必须放在中括号之中
let s = Symbol();
let obj = {
[s]: function(arg) {
return arg;
}
}
obj[s]("itclanCoder")
2
3
4
5
6
7
8
在上面的代码中,如果变量s
不放在中括号中,该属性的键名就是字符串s
,而不是定义Symbol
类型值
# 总结
本文主要介绍了Es6
中Symbol
的常见使用,Symbol
是一种新的基础类型,它形式字符串的数据类型,是字符串类型的一种额外拓展
常用于作为对象属性的键名,每个从Symbol()
返回的symbol值
都是唯一的,可保证对象的每个属性名的唯一性,可解决属性名的冲突问题
Symbol()
函数会返回symbol
类型的值,该类型具有静态属性(如Symbol().description
,)和静态方法(Symbol.for()
,Symbol.keyFor()
)
当然也介绍了Symbol
的一些常见应用场景,作为对象的属性名(key),定义类的私有属性和方法,替代常量,以及注册全局Symbol
等,以及一些注意事项
关于Symbol
暂且就这么多,还是得多多使用
# 相关参考文档
← 深度理解闭包 用户名-手机号加密特殊处理 →