ECMAScript 6:一、Symbol
1 前言
ES6学习笔记1:Symbol。
学习地址:
https://es6.ruanyifeng.com/#docs/symbol
2 笔记
(1)概述
ES5对象属性名都是字符串,容易造成属性名的冲突。ES6引入原始数据类型Symbol,表示唯一性的值。
JavaScript的原始数据类型有:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt)、对象(Object)。
Symbol 值通过Symbol()函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。 – 总结:对象属性名,可以是字符串,也可以是Symbol。
Symbol作为对象的属性名(或者私有方法的名称):
let {log:l} = console;
let NAME = Symbol('name')
let AGE = Symbol('age')
let me = Symbol('person')
class Xiaoxu {
constructor(name, age){
// 在获取不到Symbol-NAME和AGE的前提下,这里可以视为类的私有属性
this[NAME] = name;
this[AGE] = age;
}
// 将方法名称命名为Symbol值,视为私有方法(在拿不到Symbol值的时候是私有的)
[me](name) {
l("just remember:" + name)
}
toString(){
return `XiaoXu[${this[NAME]}][${this[AGE]}]`;
}
}
let w = new Xiaoxu('jx', 20)
l(w.toString())
l(w)
w[me]("xiaoxu")
结果:
XiaoXu[jx][20]
Xiaoxu { [Symbol(name)]: 'jx', [Symbol(age)]: 20 }
just remember:xiaoxu
初始化一个Symbol:
let s = Symbol();
let {log:l} = console;
l(typeof s)
symbol
上面的变量s就是一个独一无二的值。
注意,Symbol()函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象,所以不能使用new命令来调用。另外,由于 Symbol 值不是对象,所以也不能添加属性。基本上,它是一种类似于字符串的数据类型。– 总结:Symbol()函数前不能使用new,直接使用Symbol()生成。
Symbol()函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述。这主要是为了在控制台显示,或者转为字符串时,比较容易区分。
let {log:l} = console;
let s1 = Symbol('xiaoxu')
let s2 = Symbol('xiaoli')
l(s1)
l(s2)
l(s1.toString())
l(s2.toString())
Symbol(xiaoxu)
Symbol(xiaoli)
Symbol(xiaoxu)
Symbol(xiaoli)
上面代码中,s1和s2是两个 Symbol 值。如果不加参数,它们在控制台的输出都是Symbol(),不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。
如果 Symbol 的参数是一个对象,就会调用该对象的toString()方法,将其转为字符串,然后才生成一个 Symbol 值。
let {log:l} = console;
class XiaoXu {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString(){
return `XiaoXu[${this.x},${this.y}]`;
}
}
let s0 = Symbol(new XiaoXu(5, 9));
l(s0)
l(s0.toString())
Symbol(XiaoXu[5,9])
Symbol(XiaoXu[5,9])
注意,Symbol()函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。– 总结:Symbol(‘xiaoxu’) === Symbol(‘xiaoxu’)将返回false。
let {log:l} = console;
let s1 = Symbol();
let s2 = Symbol();
l(s1 === s2) // false
// 有参数的情况
let s3 = Symbol('xiaoxu');
let s4 = Symbol('xiaoxu');
l(s3 === s4) // false
上面代码中,s1和s2都是Symbol()函数的返回值,而且参数相同,但是它们是不相等的。事实上,如果调用100次Symbol(),会得到100个互不相等的值。
Symbol 值不能与其他类型的值进行运算,会报错。
let sym = Symbol('I am xiaoxu');
"speak:" + sym
TypeError: Cannot convert a Symbol value to a string
– 总结:Symbol不能和其他类型的值一起运算。
但是,– 总结:Symbol 值可以显式转为字符串。
let sym = Symbol('xiaoxu');
l(String(sym))
l(sym.toString())
Symbol(xiaoxu)
Symbol(xiaoxu)
另外,– 总结:Symbol值也可以转为布尔值,但是不能转为数值。
let xx = Symbol();
l(Boolean(xx))
l(!xx)
if(xx) {
l("我是Symbol,可以作为布尔值")
}
可以转换为布尔值:
true
false
我是Symbol,可以作为布尔值
转换成数值将报错:
Number(xx) //TypeError: Cannot convert a Symbol value to a number
xx + 5 // TypeError: Cannot convert a Symbol value to a number
(2)Symbol.prototype.description
前面说过,Symbol()函数创建 Symbol 值时,可以用参数添加一个描述。
const xx = Symbol('xiaoxu')
xx这个值的描述就是字符串xiaoxu,读取这个描述如果将Symbol显式转为字符串,并不方便。
ES2019提供了一个Symbol值的实例属性description,直接返回Symbol值的描述。
const xx = Symbol('xiaoxu')
l(xx.description) //xiaoxu
(3)作为属性名的 Symbol
由于每一个Symbol值都是不相等的,这意味着只要Symbol值作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
let xiaoxuSymbol = Symbol();
// 第一种写法
let a = {};
a[xiaoxuSymbol] = 'hi';
l(a)
// 第二种写法
let b = {
[xiaoxuSymbol]: 'hi'
};
l(b)
// 第三种写法
let c = {};
Object.defineProperty(c, xiaoxuSymbol, { value: 'hi' });
l(c)
// 以上写法都得到同样结果
l("First:")
l(a[xiaoxuSymbol])
l(b[xiaoxuSymbol])
l(c[xiaoxuSymbol])
// 下面这种写法无法取到值
l("Second:")
l(a.xiaoxuSymbol)
l(b.xiaoxuSymbol)
l(c.xiaoxuSymbol)
{ [Symbol()]: 'hi' }
{ [Symbol()]: 'hi' }
{}
First:
hi
hi
hi
Second:
undefined
undefined
undefined
上面代码通过方括号结构和Object.defineProperty()方法,将对象的属性名指定为一个 Symbol 值。
注意,Symbol 值作为对象属性名时,不能用点运算符。
const sym = Symbol();
const a = {};
a.sym = "xiaoxu";
l(a[sym])
l(a['sym'])
l(a.sym)
结果:
undefined
xiaoxu
xiaoxu
上面代码中,因为点运算符后面总是字符串,所以不会读取sym作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个 Symbol 值。
同理,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
let {log:l} = console;
let s = Symbol();
let obj = {
[s]: function (...arg) {l(arg)}
};
obj[s](1,2,3);
结果:
[ 1, 2, 3 ]
上面代码中,如果s不放在方括号中,该属性的键名就是字符串s,而不是s所代表的那个 Symbol 值。
采用增强的对象写法,上面代码的obj对象可以写得更简洁一些。
let s = Symbol();
let obj = {
[s](...arg){l(arg)}
}
obj[s](1,2,3);
结果:
[ 1, 2, 3 ]
– 总结:Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。
const log = {}
log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
}
l(log.levels.DEBUG, "xiaoxu debug message")
l(log.levels.WARN, "xiaoxu warn message")
结果:
Symbol(debug) xiaoxu debug message
Symbol(warn) xiaoxu warn message
再举个栗子:
const red = Symbol('red');
const green = Symbol('green');
function changeColor(color){
switch(color) {
case red:
return green;
case green:
return red;
default:
throw new Error('undefined color found.');
}
}
l(changeColor(red)) //Symbol(green)
常量使用 Symbol 值最大的好处,就是其他任何值都不可能有相同的值了,因此可以保证上面的switch语句会按设计的方式工作。
还有一点需要注意,Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。
(4)消除魔术字符串
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。
function getMyArea(shapes, opts){
let area = 0;
switch(shapes) {
// 三角形
case 'Triangle':
area = .5 * opts.height * opts.width;
break;
default:
break;
}
return area;
}
l(getMyArea('Triangle', {height: 100, width: 20})) //1000
上面代码中,字符串Triangle就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
常用的消除魔术字符串的方法,就是把它写成一个变量。
const shapeType = {
triangle: 'Triangle'
};
function getMyArea(shapes, opts){
let area = 0;
switch(shapes) {
// 三角形
case shapeType.triangle:
area = .5 * opts.height * opts.width;
break;
default:
break;
}
return area;
}
l(getMyArea('Triangle', {height: 100, width: 20})) //1000
上面代码中,我们把Triangle写成shapeType对象的triangle属性,这样就消除了强耦合。
如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用 Symbol 值。
const shapeType = {
triangle: Symbol()
};
function getMyArea(shapes, opts){
let area = 0;
switch(shapes) {
// 三角形
case shapeType.triangle:
area = .5 * opts.height * opts.width;
break;
default:
break;
}
return area;
}
l(getMyArea(shapeType.triangle, {height: 100, width: 20})) //1000
(5)属性名的遍历
Symbol 值作为属性名,遍历对象的时候,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。
但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
let obj = {};
let names = Symbol('names');
let ages = Symbol('ages');
obj[names] = "xiaoxu"
obj[ages] = 99
obj['addr'] = '你家'
obj['fn'] = () => {l('你好')}
l(obj)
/* {
addr: '你家',
fn: [Function (anonymous)],
[Symbol(names)]: 'xiaoxu',
[Symbol(ages)]: 99
} */
// 序列化对象:将对象转换为字符串,不展示对象的Symbol属性
l(JSON.stringify(obj)) // {"addr":"你家"}
// Object.keys 不展示对象的Symbol属性
l(Object.keys(obj)) // [ 'addr', 'fn' ]
// Object.getOwnPropertyNames 不展示对象的Symbol属性
l(Object.getOwnPropertyNames(obj)) // [ 'addr', 'fn' ]
// Object.getOwnPropertyDescriptors 会展示对象的Symbol属性
l(Object.getOwnPropertyDescriptors(obj))
/* {
addr: {
value: '你家',
writable: true,
enumerable: true,
configurable: true
},
[Symbol(names)]: {
value: 'xiaoxu',
writable: true,
enumerable: true,
configurable: true
},
fn: {
value: [Function (anonymous)],
writable: true,
enumerable: true,
configurable: true
},
[Symbol(ages)]: {
value: 99,
writable: true,
enumerable: true,
configurable: true
}
} */
l(Object.getOwnPropertyDescriptors(obj)[names])
/* {
value: 'xiaoxu',
writable: true,
enumerable: true,
configurable: true
} */
使用for in:
for(let i in obj){
l(i)
}
// addr
// fn
上述除了Object.getOwnPropertyDescriptors(obj)方法均未返回Symbol类型的属性,改用Object.getOwnPropertySymbols()方法:
l(Object.getOwnPropertySymbols(obj))
//[ Symbol(names), Symbol(ages) ]
另一个新的 API,Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
let obj = {
[Symbol('own_keys')]: 99,
enum: 2,
nonEnum: 5,
writable: true
}
l(Reflect.ownKeys(obj))
//[ 'enum', 'nonEnum', 'writable', Symbol(own_keys) ]
– 总结:Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。
– 总结:通过js反射的API,Reflect.ownKeys()可以获取所有类型的键名,包括常规键名和 Symbol 键名。
由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。
let size = Symbol('size')
class Collection {
constructor(){
this[size] = 0;
}
add(item) {
this[this[size]] = item;
// js是单线程的,不使用多线程的场景++应该是线程安全的
this[size]++;
}
static sizeOf(instance) {
return instance[size];
}
}
let ArrayList = new Collection()
l(Collection.sizeOf(ArrayList))
ArrayList.add('xiaoxu')
ArrayList.add('xiaoli')
l(Collection.sizeOf(ArrayList))
l(Object.keys(ArrayList))
l(Object.getOwnPropertyNames(ArrayList))
l(Object.getOwnPropertySymbols(ArrayList))
l(Reflect.ownKeys(ArrayList))
// 0
// 2
// [ '0', '1' ]
// [ '0', '1' ]
// [ Symbol(size) ]
// [ '0', '1', Symbol(size) ]
(6)Symbol.for(),Symbol.keyFor()
有时,我们希望重新使用同一个 Symbol 值,Symbol.for()方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
l(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 值。
l(Symbol.for("xx") === Symbol.for("xx"))
// true
l(Symbol("xx") === Symbol("xx"))
// false
(7)实例模块的Singleton
Singleton 模式指的是调用一个类,任何时候返回的都是同一个实例。
对于 Node 来说,模块文件可以看成是一个类。怎么保证每次执行这个模块文件,返回的都是同一个实例呢?
很容易想到,可以把实例放到顶层对象global。
mod.js:
function A(){
this.name = "xiaoxu";
}
var {log:l} = console;
l(global)
l(module)
if(!global._goo) {
global._goo = new A();
}
module.exports = global._goo
l("第二次")
l(module)
module实际是每个js文件的Module对象,这里是为Module对象的exports属性赋值,值是:global._goo。
useMod.js:
let c = require('./mod.js')
let {log:l} = console;
l(c)
l(c.name)
useMod.js执行结果(结果打印关注c和c.name):
上面useMod的变量c,任何时候加载的都是A的同一个实例。但是这里存在问题,全局变量global._goo是可写的,任何文件均可以修改。
global._goo = {name:"xiaoli"}
let c = require('./mod.js')
let {log:l} = console;
l(c)
l(c.name)
重新执行,结果如下:
上面的代码,会使得加载mod.js的脚本都失真。
为了防止这种情况出现,我们就可以使用 Symbol。
对mod.js修改如下:
const NAME_KEY = Symbol.for('names')
function A(){
this.name = "xiaoxu";
}
var {log:l} = console;
l(global)
l(module)
if(!global[NAME_KEY]) {
global[NAME_KEY] = new A();
}
module.exports = global[NAME_KEY]
l("第二次")
l(module)
上面代码中,可以保证global[NAME_KEY]不会被无意间覆盖,但还是可以被改写:
修改useMod.js:
global[Symbol.for('names')] = {name :"xiaojia"}
let c = require('./mod.js')
let {log:l} = console;
l(c)
l(c.name)
如果键名使用Symbol方法生成,那么外部将无法引用这个值,当然也就无法改写。
// const NAME_KEY = Symbol.for('names')
const NAME_KEY = Symbol('names')
//...
上面代码将导致其他脚本都无法引用NAME_KEY。但这样也有一个问题,就是如果多次执行这个脚本,每次得到的NAME_KEY都是不一样的。虽然 Node 会将脚本的执行结果缓存,一般情况下,不会多次执行同一个脚本,但是用户可以手动清除缓存,所以也不是绝对可靠。
(8)内置的Symbol值
– 总结:Symbol元编程之11个内置Symbol值使用。
除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。
(8.1)Symbol.hasInstance
对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)。
class XxClass {
[Symbol.hasInstance](obj) {
return obj instanceof this;
}
}
class XClass {
[Symbol.hasInstance](obj) {
return obj instanceof Array;
}
}
l("数组:")
l([1,2] instanceof XClass)
l([1,2] instanceof Array)
l([1,2] instanceof new XClass())
l("XxClass:")
const x = new XxClass();
l(x instanceof XxClass)
l(x instanceof XClass)
执行结果:
数组:
false
true
true
XxClass:
true
false
但是注意上述的XxClass,因为写法是return obj instanceof this,如果使用如下的判断:
class XxClass {
[Symbol.hasInstance](obj) {
return obj instanceof this;
}
}
const x = new XxClass();
l(x instanceof new XxClass())
会报错提示:RangeError: Maximum call stack size exceeded。这里是因为instanceof this,而判断语句是:x instanceof new XxClass(),导致出现new XxClass()实例对象递归调用自身的[Symbol.hasInstance](obj)方法进行instanceof的判断而报错的。
而上述使用l(x instanceof XClass)则不会抛出递归报错。
我们可以修改上述的类,来说明递归的调用场景:
class XxClass {
count = 0;
[Symbol.hasInstance](obj) {
this.count++;
// this.count <= 6948时则会抛出:
// Maximum call stack size exceeded
if(this.count <= 6947) {
return obj instanceof this;
} else {
return obj instanceof XxClass;
}
}
}
l("XxClass:")
const x = new XxClass();
l(x instanceof new XxClass())
执行结果如下:
XxClass:
true
上述表明,当递归到6948次时,结束了instanceof的递归调用是不会抛出异常的,一旦执行到了6949次时,直接抛出Maximum call stack size exceeded异常。
故而我们应当避免使用this作为instanceof的判断对象,修改上述的XxClass:
class XxClass {
[Symbol.hasInstance](obj) {
return obj instanceof XxClass;
}
}
l("XxClass:")
const x = new XxClass();
l(x instanceof new XxClass())
l(x instanceof XxClass)
再次执行结果:
XxClass:
true
true
再举个栗子:
class XEven {
static [Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
}
// 等同于
const XEven2 = {
[Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
};
l("first:")
l(1 instanceof XEven) // false
l(2 instanceof XEven) // true
l(123 instanceof XEven) // false
l("second:")
l(1 instanceof XEven2) // false
l(2 instanceof XEven2) // true
l(123 instanceof XEven2) // false
执行结果:
first:
false
true
false
second:
false
true
false
(8.2)Symbol.isConcatSpreadable
对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
l(arr1[Symbol.isConcatSpreadable]) // undefined
let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
l(['a', 'b'].concat(arr2, 'e')) // ['a', 'b', ['c','d'], 'e']
let arr3 = ['c', 'd'];
arr3[Symbol.isConcatSpreadable] = true;
l(['a', 'b'].concat(arr3, 'e')) // [ 'a', 'b', 'c', 'd', 'e' ]
结果:
undefined
[
'a',
'b',
[ 'c', 'd', [Symbol(Symbol.isConcatSpreadable)]: false ],
'e'
]
[ 'a', 'b', 'c', 'd', 'e' ]
上面代码说明,数组的默认行为是可以展开,Symbol.isConcatSpreadable默认等于undefined。该属性等于true时,也有展开的效果。
类似数组的对象正好相反,默认不展开。它的Symbol.isConcatSpreadable属性设为true,才可以展开。
let obj = {length: 2, 0: 'c', 1: 'd'};
l(['a', 'b'].concat(obj, 'e')) // [ 'a', 'b', { '0': 'c', '1': 'd', length: 2 }, 'e' ]
obj[Symbol.isConcatSpreadable] = true;
l(['a', 'b'].concat(obj, 'e')) // ['a', 'b', 'c', 'd', 'e']
结果:
[ 'a', 'b', { '0': 'c', '1': 'd', length: 2 }, 'e' ]
[ 'a', 'b', 'c', 'd', 'e' ]
Symbol.isConcatSpreadable属性也可以定义在类里面。
class NewArray extends Array {
constructor(args){
super(args);
this[Symbol.isConcatSpreadable] = true;
}
}
class NewArray2 extends Array {
constructor(args) {
super(args);
}
get [Symbol.isConcatSpreadable] (){
return false;
}
}
let arr = new NewArray();
arr[0] = 2;
arr[1] = 4;
let arr2 = new NewArray2();
arr2[0] = 8;
arr2[1] = 9;
l([0,1].concat(arr).concat(arr2)) //[ 0, 1, 2, 4, NewArray2(2) [ 8, 9 ] ]
上面代码中,类NewArray是可展开的,类NewArray2是不可展开的,所以使用concat时有不一样的结果。
若修改:
class NewArray2 extends Array {
constructor(args) {
super(args);
}
get [Symbol.isConcatSpreadable] (){
return true;
}
}
执行结果:
[ 0, 1, 2, 4, 8, 9 ]
注意,Symbol.isConcatSpreadable的位置差异,NewArray是定义在实例上,NewArray2是定义在类本身,效果相同。
(8.3)Symbol.species
对象的Symbol.species属性,指向一个构造函数。创建衍生对象时,会使用该属性。
class NewArray extends Array {
}
const a = new NewArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
l(a instanceof NewArray) //true
l(b instanceof NewArray) //true
l(c instanceof NewArray) //true
上面代码中,子类NewArray继承了父类Array,a是NewArray的实例,b和c是a的衍生对象。你可能会认为,b和c都是调用数组方法生成的,所以应该是数组(Array的实例),但实际上它们也是NewArray的实例。
Symbol.species属性就是为了解决这个问题而提供的。现在,我们可以为NewArray设置Symbol.species属性。
class NewArray extends Array {
static get [Symbol.species]() { return Array; }
}
const a = new NewArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
l(a instanceof NewArray) //true
l(b instanceof NewArray) //false
l(c instanceof NewArray) //false
上面代码中,由于定义了Symbol.species属性,创建衍生对象时就会使用这个属性返回的函数,作为构造函数。这个例子也说明,定义Symbol.species属性要采用get取值器。默认的Symbol.species属性等同于下面的写法。
static get [Symbol.species]() {
return this;
}
现修改为上述再次执行如下:
class NewArray extends Array {
// 默认写法
static get [Symbol.species]() { return this; }
}
const a = new NewArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
l(a instanceof NewArray) //true
l(b instanceof NewArray) //true
l(c instanceof NewArray) //true
因为我们知道,在ES6的类中,static关键字中使用this,其实这个this指的是NewArray自身,所以上面的写法等同如下写法:
class NewArray extends Array {
// 默认写法
static get [Symbol.species]() { return NewArray; }
}
const a = new NewArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
l(a instanceof NewArray) //true
l(b instanceof NewArray) //true
l(c instanceof NewArray) //true
再来看下前面的栗子:
class NewArray extends Array {
static get [Symbol.species]() { return Array; }
}
const a = new NewArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
l(b instanceof Array) //true
l(b instanceof NewArray) //false
上面代码中,a.map(x => x)生成的衍生对象,就不是NewArray的实例,而直接就是Array的实例。
再看一个例子。
class T1 extends Promise {
}
class T2 extends Promise {
static get [Symbol.species]() {
return Promise;
}
}
l(new T1(r => r()).then(v => v) instanceof T1) // true
l(new T2(r => r()).then(v => v) instanceof T2) // false
上面代码中,T2定义了Symbol.species属性,T1没有。结果就导致了创建衍生对象时(then方法),T1调用的是自身的构造方法,而T2调用的是Promise的构造方法。
总之,Symbol.species的作用在于,实例对象在运行过程中,需要再次调用自身的构造函数时,会调用该属性指定的构造函数。它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。
(8.4)Symbol.match
对象的Symbol.match属性,指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值。
String.prototype.match(regexp)
// 等同于
regexp[Symbol.match](this)
class MyMatcher {
[Symbol.match](string) {
return 'hello world'.indexOf(string);
}
}
l('e'.match(new MyMatcher())) // 1
l('world'.match(new MyMatcher())) //6
(8.5)Symbol.replace
对象的Symbol.replace属性,指向一个方法,当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。
String.prototype.replace(searchValue, replaceValue)
// 等同于
searchValue[Symbol.replace](this, replaceValue)
下面是一个栗子:
const x = {};
x[Symbol.replace] = (...s) => {
console.log(`替换前的参数:${s}`)
return s[0] + "-by-xiaoxu-" + s[1];
};
l('Hello'.replace(x, 'World'))
执行结果:
替换前的参数:Hello,World
Hello-by-xiaoxu-World
Symbol.replace方法会收到两个参数,第一个参数是replace方法正在作用的对象,上面例子是Hello,第二个参数是替换后的值,上面例子是World。
(8.6)Symbol.search
对象的Symbol.search属性,指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值。
String.prototype.search(regexp)
// 等同于
regexp[Symbol.search](this)
Symbol.search栗子:
class MySearch {
constructor(value) {
this.value = value;
}
[Symbol.search](string) {
return string.indexOf(this.value);
}
}
l('xiaoxuxiaoli'.search(new MySearch('xiaoxu'))) // 0
l('xiaoxuxiaoli'.search(new MySearch('xiaoli'))) // 6
(8.7)Symbol.split
对象的Symbol.split属性,指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。
String.prototype.split(separator, limit)
// 等同于
separator[Symbol.split](this, limit)
举个栗子:
class XiaoxuSplitter {
constructor(value){
this.value = value;
}
[Symbol.split](string) {
if(string === undefined || string === null) return;
let index = string.indexOf(this.value);
if(index === -1) {
return string;
}
return [
string.substr(0, index),
string.substr(index + this.value.length)
];
}
}
l('xiaoxuxiaoli'.split(new XiaoxuSplitter('xiao'))) // [ '', 'xuxiaoli' ]
l('xiaoxuxiaoli'.split(new XiaoxuSplitter('xu'))) // [ 'xiao', 'xiaoli' ]
l('xiaoxuxiaoli'.split(new XiaoxuSplitter('li'))) // [ 'xiaoxuxiao', '' ]
l('xiaoxuxiaoli'.split(new XiaoxuSplitter(undefined))) // xiaoxuxiaoli
// TypeError: Cannot read properties of undefined (reading 'split')
// l(undefined.split(new XiaoxuSplitter('li')))
上面方法使用Symbol.split方法,重新定义了字符串对象的split方法的行为。
(8.8)Symbol.iterator
对象的Symbol.iterator属性,指向该对象的默认遍历器方法。
const myIterable = {};
// 创建一个生成器,类似python的yield, 为对象myIterable的Symbol.iterator赋值
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
l([...myIterable]) // [1, 2, 3]
for(let i of myIterable) {
l(i)
}
// 1
// 2
// 3
对象进行for…of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器。
class XCollection {
*[Symbol.iterator]() {
let i = 0;
while (this[i] !== undefined) {
yield this[i];
// 遍历器需要有索引下标遍历的处理
++i;
}
}
}
let xCol = new XCollection();
xCol[0] = 99;
xCol[1] = 'xiaoxu';
xCol[2] = 'xiaoli';
for(let val of xCol) {
l(val)
}
// 99
// xiaoxu
// xiaoli
(8.9)Symbol.toPrimitive
Primitive,即基础类型数据。java有9种基础类型数据(非包装类):byte、short、int、long、float、double、boolean、char、void。
在JavaScript中,对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。
Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。
Number:该场合需要转成数值
String:该场合需要转成字符串
Default:该场合可以转成数值,也可以转成字符串
let obj = {
[Symbol.toPrimitive](hint) {
switch(hint) {
case 'number':
return 9;
case 'string':
return 'xuli';
case 'default':
return '[default]';
default:
throw new Error('默认错误');
}
}
}
l(3 * obj) // 27
l(5 + obj) // 5[default]
l(obj == 'default') // false
l(obj == '[default]') // true
l(String(obj)) // xuli
(8.10)Symbol.toStringTag
对象的Symbol.toStringTag属性,用来设定一个字符串(设为其他类型的值无效,但不报错)。在目标对象上面调用Object.prototype.toString()方法时,如果Symbol.toStringTag属性存在,该属性设定的字符串会出现在toString()方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]或[object Array]中object后面的那个大写字符串。
// 原本的
const a = new Object();
l(a) // {}
l(a.toString()) // [object Object]
// 栗子1
l({[Symbol.toStringTag]: 'xiaoxu'}.toString()) // [object xiaoxu]
// 栗子2
class Collection {
// 注意:Symbol.toStringTag的get方法不要加static关键字,否则结果:[object Object]
get [Symbol.toStringTag]() {
return 'xiaoxuAndxiaoli';
}
}
let x = new Collection();
l(x.toString()) // [object xiaoxuAndxiaoli]
l(Object.prototype.toString.call(x)); // [object xiaoxuAndxiaoli]
l(Collection.prototype.toString.call(x)); // [object xiaoxuAndxiaoli]
java的toString()方法也是Object类中存在的方法,也是实例方法(public String toString()),供实例对象调用。上述的[Symbol.toStringTag]的get方法,加上static关键字也是不能正确打印的,故而是原型链上的方法。
取值函数(getter)和存值函数(setter)在原型链上,使用get和set关键字(如果get和set关键字加了static关键字,那么方法就不会存在原型链上了):
class MyGet {
get age(){
console.log("get age")
}
set age(value){
console.log("set age")
}
}
const a = new MyGet();
a.age; // get age
a.age = 11; // set age
ES6 新增内置对象的Symbol.toStringTag属性值如下。
JSON[Symbol.toStringTag]:'JSON'
Math[Symbol.toStringTag]:'Math'
Module 对象M[Symbol.toStringTag]:'Module'
ArrayBuffer.prototype[Symbol.toStringTag]:'ArrayBuffer'
DataView.prototype[Symbol.toStringTag]:'DataView'
Map.prototype[Symbol.toStringTag]:'Map'
Promise.prototype[Symbol.toStringTag]:'Promise'
Set.prototype[Symbol.toStringTag]:'Set'
%TypedArray%.prototype[Symbol.toStringTag]:'Uint8Array'等
WeakMap.prototype[Symbol.toStringTag]:'WeakMap'
WeakSet.prototype[Symbol.toStringTag]:'WeakSet'
%MapIteratorPrototype%[Symbol.toStringTag]:'Map Iterator'
%SetIteratorPrototype%[Symbol.toStringTag]:'Set Iterator'
%StringIteratorPrototype%[Symbol.toStringTag]:'String Iterator'
Symbol.prototype[Symbol.toStringTag]:'Symbol'
Generator.prototype[Symbol.toStringTag]:'Generator'
GeneratorFunction.prototype[Symbol.toStringTag]:'GeneratorFunction'
(8.11)Symbol.unscopables
对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。
l(Array.prototype[Symbol.unscopables])
// [Object: null prototype] {
// copyWithin: true,
// entries: true,
// fill: true,
// find: true,
// findIndex: true,
// flat: true,
// flatMap: true,
// includes: true,
// keys: true,
// values: true,
// at: true
// }
l(Object.keys(Array.prototype[Symbol.unscopables]))
// [
// 'copyWithin', 'entries',
// 'fill', 'find',
// 'findIndex', 'flat',
// 'flatMap', 'includes',
// 'keys', 'values',
// 'at'
// ]
上面代码说明,数组有11个属性,会被with命令排除。
// 没有 unscopables 时
class MyClass {
eat(){return 1;}
}
var eat = function() {return 2;}
with (MyClass.prototype) {
l(eat()); // 1
}
// // 有 unscopables 时
class MyClass2 {
ear(){return 3;}
get [Symbol.unscopables](){
return {ear : true};
}
}
// 不定义这个ear function, 报错:ReferenceError: ear is not defined
var ear = function() {return 4;}
with (MyClass2.prototype) {
l(ear()); //4
}
上面代码通过指定Symbol.unscopables属性,使得with语法块不会在当前作用域寻找ear属性,即ear将指向外层作用域的变量。