ES5 的提供了6类基础数据类型 Number String Boolean Null undefined Object,现在 ES6 中添加了一个新的数据类型 symbol。今天我们就一起来深入学习一下 symbol 这个新的数据类型。
基础知识
ES6 引入的 symbol,表示独一无二的值。symbol 是通过 Symbol 函数生成的。
Symbol 的唯一性和特殊性
eg1:
let s = Symbol()
typeof s // symbol
这里代码就是通过 symbol 函数 生成了一个一个独一无二的值 s。通过 typeof 可以判断出 s 的数据类型是 Symbol。
symbol 函数是可以接受一个字符串作为参数的,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转字符串的时候,容易区分。
eg2:
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 的描述,不然在控制台会打印出两个 symbol,无法区分。
当 symbol 函数的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后在生成一个 symbol 值。
eg3:
let obj = {
toString () {
return 'abc'
}
}
let a = Symbol(obj)
a // Symbol('abc')
symbol 函数的参数只是作为 symbol 值的描述,因此相同参数的 symbol 函数返回值也是不相等的。
eg4:
let s1 = Symbol()
let s2 = Symbol()
s1 === s2 // false
let s1 = Symbol('a')
let s2 = Symbol('a')
s1 === s2 // false
symbol 也不可以直接与其他数据类型进行计算
eg5:
let s1 = Symbol('chencc')
'my name is ' + s1
// Uncaught TypeError: Cannot convert a Symbol value to a string
symbol 虽然不可以直接和其他数据类型计算,但是可以通过调用的方式转为字符串,也可以转为 Boolean,但是不可以转为数值。
eg6:
let s1 = Symbol('chencc')
s1.toString() // Symbol(chencc)
!s1 // false
!!s1 // true
Number(s1)
// Cannot convert a Symbol value to a number
获取 symbol 的描述
eg7:
let s1 = Symbol('chencc')
s1.description // 'chencc'
symbol 在对象属性中,不会被 for...in for...of 获取到,也不会被 Object.keys() Object.getOwnPropertyNames() 和 JSON.stringify() 返回
eg8:
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
但是想获取到对象中所有 key 是通过 Symbol 设置的,可以使用 Object.getOwnPropertySymbols()方法来获取
eg9:
Object.getOwnPropertySymbols(obj)
// [Symbol()]
内置的 Symbol 属性
1、Symbol.hasInstance
这个属性是一个内部方法,当该对象使用instanceof时,会调用这个方法的。
eg1:
let Obj = {
[Symbol.hasInstance] (obj) {
return true
}
}
let obj = {}
console.log(obj instanceof Obj) // true
Symbol.hasInstance 方法,会在进行 instanceof运算时自动调用。
instanceof的原理
function instanceof (L, R) {
// 获取右侧对象的原型对象
let R_temp = R.prototype
// 获取左侧对象的原型对象
L = L.__proto__
while (true) {
// 若左侧对象的原型对象为null,则返回false
if (L == null) {
return false
}
// 若二者相对,则说明L是R实例化的
if (L == R_temp) {
return true
}
L = L.__proto__
}
}
2、Symbol.isConcatSpreadable
这个属性等于一个布尔值,表示该对象用于 Array.prototype.concat()时,是否可以展开
eg1:
let arr = [1, 2, 3]
[4, 5].concat(arr, 6)
// [4, 5, 1, 2, 3, 6]
arr[Symbol.isConcatSpreadable]
undefined
let arr1 = [1, 2, 3]
arr1[Symbol.isConcatSpreadable] = false
[4,5].concat(arr1, 6)
// [4, 5, Array(3), 6]
上面例子说明,数组 concat 时,默认是可以展开的,Symbol.isConcatSpreadable 默认是 undefined。该属性 true时,也有展开的效果。
类似数组的对象正好相反,默认是不展开的。当 Symbol.isConcatSpreadable 为true时,才可以展开。
eg2:
let obj = {length: 2, 0: 'c', 1: 'd'}
['a','b'].concat(obj)
// ["a", "b", {…}]
obj[Symbol.isConcatSpreadable]
// undefined
obj[Symbol.isConcatSpreadable] = true
['a','b'].concat(obj)
// (4) ["a", "b", "c", "d"]
3、Symbol.species
创建衍生对象时,会使用这个属性
eg1:
class CustomArray extends Array {}
const a = new CustomArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
b instanceof CustomArray // true
c instanceof CustomArray // true
这里 customArray 继承数组 Array,a是 customArray 的实例。b和c都是a的衍生对象,所以b 和 c 也都是 customArray 的实例。
Symbol.species就是在衍生对象这个时候出现的,这里在 CustomArray 中设置 Symbol.species 属性。
默认的Symbol.species属性是这么写的
eg2:
class CustomArray extends Array {
static get [Symbol.species]() {
return this;
}
}
当前这个对象继承对象生成衍生对象返回的是当前构造函数的this,所以是 CustomArray 的实例。
如果我们修改了返回的话,可以看下结果
eg3:
class CustomArray extends Array {
static get [Symbol.species]() { return Array; }
}
const a = new CustomArray();
const b = a.map(x => x);
b instanceof CustomArray // false
b instanceof Array // true
因为这里生成衍生对象时候,get时返回的是数组,所以就直接是数组的实例了。
4、Symbol.match
当执行 str.match(myObject) 时,当该属性存在,就会调用他。
eg1:
class Foo{
[Symbol.match](num){
return parseInt(num) > 100 // 自定义方法内的逻辑
}
}
console.log("123".match(new Foo())) // true
console.log("12".match(new Foo())) // false
5、Symbol.replace
这个属性指向一个方法,当执行str.replace(obj)时,会调用这个方法并返回其返回值
eg1:
class Person{
// 两个参数,第一个是对象,第二个是返回的结果
[Symbol.replace](str, result){
console.log(result) // chen
console.log(str) // chencc
return "hello" // 自定义返回值
}
}
console.log("chencc".replace(new Person(), "chen")) // hello
3、Symbol.search
当该对象被String.prototype.search方法调用时,会返回该方法的返回值。
eg1:
class MySearch {
constructor(value) {
this.value = value;
}
[Symbol.search](string) {
return string.indexOf(this.value);
}
}
'foobar'.search(new MySearch('foo')) // 0
6、Symbol.split
当该对象被String.prototype.split方法调用时,会返回该方法的返回值。
eg1:
class MySplitter {
constructor(value) {
this.value = value;
}
[Symbol.split](string) {
let index = string.indexOf(this.value);
if (index === -1) {
return string;
}
return [
string.substr(0, index),
string.substr(index + this.value.length)
];
}
}
'foobar'.split(new MySplitter('foo'))
// ['', 'bar']
上面自定义了 split 的方法,内部方法可以随便写一个,大家可以自己试试。
7、Symbol.iterator
对象的Symbol.iterator属性,指向该对象的默认遍历器方法。
eg1:
const myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable]
这里大概了解下,对于for of 或者 Generator 或者类似有 Iterator 遍历器的对象,都会调用这个方法。后面我们会在单独章节对于这部分单独分析,大家知道就行了。
8、Symbol.toPrimitive
对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。
eg1:
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
}
};
2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'
9、Symbol.toStringTag
当调用 toString 时, 用来自定义一个方法的toString()值。
eg1:
let person = {
[Symbol.toStringTag] : "Person"
}
console.log(person.toString()); // [object Person]
我们知道当我们在 console 一个对象的时候,会显示[object Object],这个属性指向的方法就是可以将Object换为自定义。
10、Symbol.unscopables
指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。
with语法大家都应该了解,下面一个简单的例子
eg1:
x = Math.cos(3 * Math.PI) + Math.sin(Math.LN10)
y = Math.tan(14 * Math.E)
with (Math) {
x = cos(3 * PI) + sin(LN10)
y = tan(14 * E)
}
with 范围内,可以直接使用 Math 内部的方法。
eg2:
// 没有 unscopables 时
class MyClass {
foo() { return 1; }
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 1
}
// 有 unscopables 时
class MyClass {
foo() { return 1; }
get [Symbol.unscopables]() {
return { foo: true };
}
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 2
}
应用场景
给对象作属性的唯一 key
因为每一个 symbol 值都是不会相等的,所以 symbol 值可以作为标识符,用于对象的属性名,就可以不同名。这个可以防止一个对象由多个模块合并组成的,能防止 key 被改写或者 覆盖了。
以下是给对象添加属性的三种方式,key值为 symbol
eg1:
let name = Symbol()
let a = {}
a[name] = 'chencc'
let a = {
[name]: 'chencc'
}
let a = {}
Object.defineProperty(a, name, {
value: 'chencc'
})
a[name] // 'chencc'
// 备注, symbol 为key时,不能用点运算符
同时我们想要获取到所有的 key 怎么办了,这里有一个新的 API Reflect.ownKeys
eg2:
let obj = {
a: 1,
[Symbol()]: 3,
[Symbol('name')]: 'chencc'
}
Reflect.ownKeys(obj)
// ["a", Symbol(), Symbol(name)]
对于生成的 Symbol 值,我们希望在别的地方使用这同一个 Symbol,可以通过 Symbol.for() 来实现。它接受一个字符串作为参数,然后搜索有没有该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并且将其注册到全局。
eg3:
let s1 = Symbol.for('name')
let s2 = Symbol.for('name')
s1 === s2
true
上面例子中 s1 和 s2 都是 Symbol 值,通过 Symbol.for方法生成,所以实际上是同一个值。
Symbol.for() 和 Symbol() 都会生成一个新的 Symbol 值。区别在于,前者会在被登记的全局环境中搜索,后者不会。Symbol.for() 不会每次调用就返回一个新的 Symbol 值,而是会坚持给定 key 的 Symbol是否已经存在,不存在才会生成。
Symbol.keyFor()返回一个已登记的 Symbol 类型的 key
eg4:
let s1 = Symbol.for('name')
Symbol.keyFor(s1) // "name"
let s2 = Symbol('name')
Symbol.keyFor(s2) // undefined
备注:Symbol.for() 生成的 Symbol 值是登记在全局环境的。
定义常量,保证常量的唯一性
eg5:
const obj = {
INFO: Symbol('info'),
WARN: Symbol('warn'),
SUCCESS: Symbol('success')
}
使用 Symbol 定义类的私有属性/方法
因为在 Javascript 中,是没有 private 的,类上所有定义的属性和方法都是可公开访问的。常规我们都是使用闭包的方式,搞了一个内部环境。现在我们有了Symbol 和 模块化,就可以实现类的私有属性和方法。
eg6:
a.js
// 这个 PASSWORD 只能在a.js中使用了
const PASSWORD = Symbol()
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
因为 PASSWORD 被定义在a.js所在的模块中,外面的模块获取不到这个 Symbol,也不可能在创建一个一摸一样的Symbol出来,因此这个 PASSWORD 只能在a.js中使用,并且这个是例话这个 login,也是没办法获取到这个属性的,所以达到一个私有化的效果。