ES6学习笔记(十一)Symbol

在JavaScript中,原本是有六种数据类型:undefined,null,数值,字符串,布尔值,对象。在ES6种,新增了Symbol这种数据类型

Symbol的构建


Symbol值通过Symbol函数来生成,表示独一无二的值,其方法可以传入一个字符串作为参数,表示对Symbol值的描述。但不管是否传入参数,参数是否相同,Symbol值都是独一无二的,没有两个Symbol值会相等。

var s1=Symbol()

var s2=Symbol()

s1===s2

//false



var s1=Symbol('s')

var s2=Symbol('s')

s1===s2

//false

若传入的参数不为字符串,那么会将其转为字符串,如果参数是对象,那么会调用参数的toString方法

var s=Symbol(1,2,3)
s
//Symbol(1)


var s=Symbol(true)
s
//Symbol(true)


var obj={}
var s=Symbol(obj)
s
//Symbol([object Object])



var obj = {
  toString() {
    return 'abc';
  }
};

var sym = Symbol(obj);
sym // Symbol(abc)

Symbol不能参与其他类型的运算

let sym = Symbol('My symbol');

"your symbol is " + sym

// TypeError: can't convert symbol to string

`your symbol is ${sym}`

// TypeError: can't convert symbol to string

Symbol可以使用String方法和toString方法显示地转为字符串,也可以使用Boolean方法转为布尔值(都为true),但不能转为数值

let sym = Symbol('My symbol');

String(sym) // 'Symbol(My symbol)'

sym.toString() // 'Symbol(My symbol)'

let sym = Symbol();

Boolean(sym) // true

!sym  // false

if (sym) {

  // ...

}

Number(sym) // TypeError

sym + 2 // TypeError

作为属性名的Symbol


Symbol值由于其独一无二的特性,用来做为属性的键名很合适,不会被其它模块的属性名覆盖。

Symbol值作为属性名要使用方括号定义,不能使用点运算符

var mySymbol = Symbol();

// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
var a = {
  [mySymbol]: 'Hello!'};

// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果a[mySymbol] // "Hello!"

var mySymbol = Symbol();
var a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"

如上面代码中使用了点运算符,被理解为是用字符串作为属性名

使用Symbol值来定义常量也十分合适,可以保证常量的值都是不相等的

const COLOR_RED    = Symbol();
const COLOR_GREEN  = Symbol();
function getComplement(color) {
  switch (color) {
    case COLOR_RED:
      return COLOR_GREEN;
    case COLOR_GREEN:
      return COLOR_RED;
    default:
      throw new Error('Undefined color');
    }
}

这样子只有使用唯一的常量值才能触发对应得代码块。

还有一点需要注意,Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。

消除魔术字符串


在阮一峰的ES6入门中提到魔术字符串是“在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值”。如下:

function getArea(shape, options) {
  let area = 0;
  switch (shape) {
    case 'Triangle': // 魔术字符串      area = .5 * options.width * options.height;
      break;
    /* ... more code ... */
  }
  return area;
}

getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串

Triangle就是一个魔术字符串,它与代码强耦合,但是修改一处又得修改多处,这样就会导致代码的维护出现困难。消除魔术字符串可以通过使用变量来实现,只要变量最终的值不与其他值冲突就可以,这种情况很适合使用Symbol值。

const shapeType = {
  triangle: Symbol()
};

function getArea(shape, options) {
  let area = 0;
  switch (shape) {
    case shapeType.triangle:
      area = .5 * options.width * options.height;
      break;
  }
  return area;
}

getArea(shapeType.triangle, { width: 100, height: 100 });

Symbol值作为属性时的遍历


Symbol 作为属性名,不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,该方法返回一个数组,数组成员为指定对象的所有 Symbol 属性名。

var obj={}
var a=Symbol('a')
var b=Symbol('b')
obj[a]='a'
obj[b]='b'

for (let i in obj)
    console.log(i)
//undefined

Object.keys(obj)
//[]

Object.getOwnPropertyNames(obj)
//[]

JSON.stringify(obj)
//"{}"

Object.getOwnPropertySymbols(obj)
//[Symbol(a), Symbol(b)]

使用Reflect.ownKeys也可以遍历到Symbol键名,除此外,常规的键名也能遍历到。

var a=Symbol('a');

var b=Symbol('b');

var obj={
    foo:1
}

obj[a]=1;

obj[b]=2;

Reflect.ownKeys(obj);
//["foo", Symbol(a), Symbol(b)]

重用Symbol值


Symbol值有时也有要重用的时候,但是Symbol值是独一无二的,无法重用。这是可以使用Symbol.for方法来登记Symbol值,以达到重用的效果。Symbol.for接受一个参数,搜索全局中是否有该参数作名称的Symbol值,若有,则返回该Symbol值,达到重用的效果,若没有,则新建并返回一个以该参数作名称的Symbol值。

let s1 = Symbol.for('foo');

let s2 = Symbol.for('foo');

s1 === s2 // true

Symbol.for和Symbol都可以新建一个Symbol值,不同的是,前者新建的Symbol值会登记,多次创建若传入参数相同则返回相同的Symbol值,而后者的不会,其每次新建的Symbol值都是独一无二的。

Symbol.for("bar") === Symbol.for("bar")
// true

Symbol("bar") === Symbol("bar")
// false

Symbol.keyFor返回一个已登记的Symbol值的key,即被Symbol.for创建的Symbol值得key。

let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

上面代码中,s1对应的Symbol值是被登记的,而s2对应的Symbol值是没被登记的,所以Symbol.keyFor(s1)返回”foo”,而Symbol.keyFor(s1)返回undefined

要注意的是,Symbol.for为 Symbol 值登记的名字,是全局环境的,可以在不同的 iframe 或 service worker 中取到同一个值。

iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);


iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')
// true

内置的Symbol值


ES6提供了11个内置的Symbol值,在调用某些对象的方法和操作时会调用这些值指向的方法

Symbol.hasInstance 

对象的Symbol.hasInstance属性指向一个内部方法,当其他对象在使用instanceof运算符判断是否为该对象实例时,会调用该方法

class MyClass {
  [Symbol.hasInstance](foo) {
    return foo instanceof Array;
  }
}

[1, 2, 3] instanceof new MyClass() // true

上面代码中,[1,2,3]使用instanceof运算符判断是否为MyClass的实例时调用了MyClass的[Symbol.hasInstance](foo)。

Symbol.isConcatSpreadable

Symbol.isConcatSpreadable属性用于表示对象用于Array.prototype.concat()时是否可以展开。对于数组,默认是可以展开的,所以在Symbol.isConcatSpreadable属性为true和undefined时可以展开,为false时不可展开。(Symbol.isConcatSpreadable属性默认为undefined)

let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined
let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
arr2[Symbol.isConcatSpreadable] = true;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', 'c', 'd', 'e']

上面代码中可以看到,没有设置Symbol.isConcatSpreadable属性时,默认可以展开,其值为undefined,将其设置为false后,即不能展开,将其设置回true后,又可以展开了。

类似数组正好相反,默认不能展开,即Symbol.isConcatSpreadable属性为true时可展开,为undefined和false时不可展开

let obj = {length: 2, 0: 'c', 1: 'd'};
['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e']
obj[Symbol.isConcatSpreadable] = true;
['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
obj[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e']

Symbol.isConcatSpreadable属性也可以定义在类里面。定义的位置可以是在实例上,也可以在类本身,效果相同

class A1 extends Array {
  constructor(args) {
    super(args);
    this[Symbol.isConcatSpreadable] = true;
  }
}

class A2 extends Array {
  constructor(args) {
    super(args);
  }
  get [Symbol.isConcatSpreadable] () {
    return false;
  }
}

let a1 = new A1();
a1[0] = 3;
a1[1] = 4;
let a2 = new A2();
a2[0] = 5;
a2[1] = 6;
[1, 2].concat(a1).concat(a2)
// [1, 2, 3, 4, [5, 6]]

Symbol.species

Symbol.species属性指向一个构造函数,创建衍生对象时,会使用该属性

class MyArray extends Array {
}
const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);

b instanceof MyArray // true
c instanceof MyArray // true

像上面的代码中,b和c是a的衍生对象,虽然b和c是使用数组方法生成的,但它们实际上是MyArray的实例。为了让其变为Array的实例,可以使用该属性返回Array

class MyArray extends Array {
  static get [Symbol.species]() { return Array; }
}

const a = new MyArray();

const b = a.map(x => x);

b instanceof MyArray // false

b instanceof Array // true

上面代码中,因为定义了该属性,所以在使用该实例来构建衍生对象时,衍生对象的实例会是Array实例。

总结来说,Symbol.species属性是用于当我们要创建一个继承其他类的实例的衍生对象时,如果想让其变为其继承的类的实例的话,就可以使用这个属性。

“它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。”---------阮一峰的ES6入门

Symbol.match

对象的Symbol.match属性,指向一个函数,当使用str.match(object)时,如果该属性存在,会调用该属性,返回对应的值。

class MyMatcher {
  [Symbol.match](string) {
    return 'hello world'.indexOf(string);
  }
}

'e'.match(new MyMatcher()) // 1

上面’e’这个字符串调用了match方法,参数为MyMatcher类的实例对象,所以调用了该对象的Symbol.match属性指向的函数,即返回了'hello world'.indexOf('e'),所以最后返回了1。

Symbol.replace

对象的Symbol.replace属性,指向一个方法,当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。

const x = {};

x[Symbol.replace] = (...s) => console.log(s);

'Hello'.replace(x, 'World') // ["Hello", "World"]

Symbol.replace方法会收到两个参数,第一个参数是replace方法正在作用的对象,上面例子是Hello,第二个参数是替换后的值,上面例子是World。上面代码用扩展运算符获取这两个参数,所以s为这两个参数作为成员的数组。

Symbol.search

对象的Symbol.search属性,指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值。

class MySearch {
  constructor(value) {
    this.value = value;
  }
  [Symbol.search](string) {
    return string.indexOf(this.value);
  }
}

'foobar'.search(new MySearch('foo')) // 0

上面代码中在调用search方法时调用了Mysearch类中Symbol.search属性指向的方法,即返回了查找的字符串在调用该方法的字符串中的位置。

Symbol.split

对象的Symbol.split属性,指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。

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']'

foobar'.split(new MySplitter('bar'))
// ['foo', '']

'foobar'.split(new MySplitter('baz'))
// 'foobar'

Symbol.iterator

对象的Symbol.iterator属性,指向该对象的默认遍历器方法。

class Collection {
  *[Symbol.iterator]() {
    let i = 0;
    while(this[i] !== undefined) {
      yield this[i];
      ++i;
    }
  }
}

let myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;

for(let value of myCollection) {
  console.log(value);
}

// 1
// 2



Symbol.toPrimitive

对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。

Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。

Number:该场合需要转成数值

String:该场合需要转成字符串

Default:该场合可以转成数值,也可以转成字符串

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  返回number 即变为123
3 + obj // '3default'  返回default 即变为'default'
obj == 'default' // true
String(obj) // 'str'

Symbol.toStringTag

对象的Symbol.toStringTag属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]或[object Array]中object后面的那个字符串。

class Collection {
  get [Symbol.toStringTag]() {
    return 'xxx';
  }
}

let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"

上面的代码在使用Collection实例调用toString方法时调用了Symbol.toStringTag属性指向的方法,返回了其定义好的内容。

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'

Symbol.unscopables

对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。

Object.keys(Array.prototype[Symbol.unscopables])

// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys']

上面的代码说名列出来的这7个属性会被with命令排除

// 没有 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
}

上面代码在MyClass中将Symbol.unscopables属性指向的对象中的foo设置为true,使得在使用with命令时忽略了其中的foo,返回了外部的foo。

猜你喜欢

转载自blog.csdn.net/zemprogram/article/details/86547439