重学ES6(三)Symbol

1,554 阅读8分钟

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,也是没办法获取到这个属性的,所以达到一个私有化的效果。