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。


参考自阮一峰的《ECMAScript6入门》


ES6学习笔记目录

 

ES6学习笔记(一)let和const

ES6学习笔记(二)参数的默认值和rest参数

ES6学习笔记(三)箭头函数简化数组函数的使用

ES6学习笔记(四)解构赋值

ES6学习笔记(五)Set结构和Map结构

ES6学习笔记(六)Iterator接口

ES6学习笔记(七)数组的扩展

ES6学习笔记(八)字符串的扩展

ES6学习笔记(九)数值的扩展

ES6学习笔记(十)对象的扩展

ES6学习笔记(十一)Symbol

ES6学习笔记(十二)Proxy代理

ES6学习笔记(十三)Reflect对象

ES6学习笔记(十四)Promise对象

ES6学习笔记(十五)Generator函数

ES6学习笔记(十六)asnyc函数

ES6学习笔记(十七)类class

ES6学习笔记(十八)尾调用优化

ES6学习笔记(十九)正则的扩展

ES6学习笔记(二十)ES Module的语法

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值