ECMAScript 6:一、Symbol

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将指向外层作用域的变量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值