ES6入门 ___ Symbol

一、概述

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。Symbol值通过 Symbol 函数生成。也就是说,对象的属性名可以有两种类型:一种是原来就有的字符串,一种就是新增的 Symbol 类型(可以保证不会与其他属性名产生冲突)。

let s = Symbol()

typeof s
// "symbol"

注意

Symbol 函数前不能使用 new 命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似与字符串的数据类型。

var s1 = Symbol('foo')
var s2 = Symbol('bar')

s1 // Symbol(foo)
s2 // Symbol(bar)

s1.toString() // "Symbol(foo)"
s1.toString() // "Symbol(bar)"

如果 Symbol 的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后才生成一个 Symbol 值。


Symbol 函数的参数只表示对当前 Symbol 值的描述,因此相同参数的 Symbol 函数的返回值是不相等的。

// 没有参数的情况
var s1 = Symbol()
var s2 = Symbol()

s1 === s2 // false

// 有参数的情况
var s1 = Symbol('foo')
var s2 = Symbol('foo')

s1 === s2 // false

Symbol 值不能与其他类型的值进行运算,否则会报错。(TypeError:can't convert symbol to string)

Symbol 值可以显示转为字符串、布尔值,但是不能转为数值。

var sym = Symbol('My symbol')

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

Boolean(sym) // true

Number(sym) // 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"

注意,Symbol 值作为对象属性名时不能使用 点 运算符。(因为点运算符后面总是字符串,所以不会读取 mySymbol 作为标识名所指代的值,导致 a 的属性名实际上是一个字符串,而不是一个 Symbol 值。)同理,在对象内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号中。

Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的,

let log = {}
log.levels = {
  DEBUG: Symbol('debug'),
  INFO: Symbol('info'),
  WARN: Symbol('warn')
}

console.log(log.levels.DEBUG, 'debug message')
console.log(log.levels.INFO, 'info message')

常量使用 Symbol 值的最大好处就是,其他任何值都不可能有相同的值了

当 Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。

三、实例:消除魔术字符串

魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或数值。风格良好的代码,应该尽量消除魔术字符串,而由含义清晰的变量代替。

function getArea(shape, options) {
  var area = 0

  switch(shape) {
    case: 'Triangle' // 魔术字符串
      area = .5 * options.width * options.height
      break;
  /* ... */
  }

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

字符串“Triangle” 就是一个魔术字符串。

常用的消除魔术字符串的方法,就是把它写成一个常量。

var shapeType = {
  triangle: 'Triangle'
}

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

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

把 'Triangle' 写成 shapeType 对象的 triangle 属性,这样就消除了强耦合。

可以发现 shapeType.triangle 等于哪个值并不重要,只要确保不会和其他 shapeType 属性的值冲突即可。因此,这里就很适合改用 Symbol 值

const shapeType = {
  triangle: Symbol()
}

四、属性名的遍历

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

Object.getOwnPropertySymbols 方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

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

obj[a] = 'Hello'
obj[b] = 'World'

var objectSymbols = Object.getOwnPropertySymbols(obj)

objectSymbols
// [Symbol(a), Symbol(b)]

另一个新的 API —— Reflect.ownKeys 方法可以返回所有类型的键名,包括常规键名和Symbol 键名。

let obj = {
  [Symbol('my_key')]: 1,
  enum: 2,
  nonEnum: 3
}

Reflect.ownKeys(obj)
// ["enum", "nonEnum", Symbol(my_key)]

以 Symbol 值作为名称的属性不会被常规方法遍历得到。我们可以利用这个特性为对象定义一些非私有但又希望只用于内部的方法。

var size = Symbol('size')

class Collection {
  constructor() {
    this[size] = 0
  }

  add(item) {
    this[this[size]] = item
    this[size]++
  }

  static sizeOf(instance) {
    return instance[size]
  }
}

var x = new Collection()
Collection.sizeOf(x) // 0

x.add('foo')
Collection.sizeOf(x) // 1

Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]

上面的代码中,对象 x 的size 属性是一个 Symbol 值,所以 Object.keys(x)、Object.getOwnPropertyNames(x) 都无法获取它。这就造成了一种非私有的内部方法的效果。

五、Symbol.for()、Symbol.keyFor()

有时,我们希望重新使用 同一个 Symbol 值,Symbol.for 方法可以做到这一点。它接受 一个字符串 作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称 的Symbol 值。

var s1 = Symbol.for('foo')
var s2 = Symbol.for('foo')

s1 === s2 // true

Symbol.for() 与 Symbol() 这两种写法都会产生新的 Symbol。它们的区别是,前者会被登记在全局环境中共搜索(登记机制),而后者不会

Symbol.keyFor 方法 返回一个已登记的 Symbol 类型值的 Key

var s1 = Symbol.for('foo')
Symbol.keyFor(s1) // "foo"

var s2 = Symbol("foo")
Symbol.keyFor(s2) // undefined

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

六、实例:模块的 Singleton 模式

Singleton 模式指调用一个类并且在任何时候都返回同一个实例。对于 Node 来说,模块文件可以看出是一个类,为了每次执行这个模块文件都返回同一个类,可以把实例放到顶层对象 global 中。

// mod.js
function A() {
  this.foo = 'hello'
}

if (!global._foo) {
  global._foo = new A()
}

module.exports = global._foo

加载上面的 mod.js

var a = require('./mod.js')
console.log(a.foo)
// A { foo: 'hello' }

这里有一个问题,全局变量 global._foo 是可写的,任何文件都可以修改(global._foo = 123)。这样会是别的脚本在加载 mod.js 时都产生失真。为了防止这种情况可以使用 Symbol

// mod.js
const FOO_KEY = Symbol.for('foo')

function A() {
  this.foo = 'hello'
}

if (!global._foo) {
  global[FOO_KEY] = new A()
}

module.exports = global[FOO_KEY]

上面代码中,可以保证 global[FOO_KEY]不会被无意间覆盖,但是可以被改写。

var a = require('./mod.js')
global[Symbol.for('foo')] = 123

如果键名使用 Symbol 方法生成,那么外部将无法引用这个值,当然也无法改写。

const FOO_KEY = Symbol('foo')

但是这样也会导致一个问题,多次执行这个脚本时,每次得到的FOO_KEY 都是不一样的。虽然 Node 会将脚本的执行结果缓存,一般情况下不会多次执行同一个脚本,但是用户可以手动清除缓存,所以也不是完全可靠。

七、内置的 Symbol 值

除了定义自己使用的 Symbol 值,ES6 还提供了11个内置的 Symbol 值,指向语言内部使用的方法。

7.1、Symbol.hasInstance

对象的 Symbol.hasInstance 属性指向一个内部方法,对象使用 instanceof 运算符 时会调用这个方法,判断该对象是否为某个构造函数的实例。比如,foo instanceof FOO 在语言内部实际调用的是 FOO[Symbol.hasInstance](foo)

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

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

new MyClass() 会返回一个实例。该实例的 Symbol.hasInstance 方法会在 进行 instanceof 运算时自动调用,判断左侧的运算子是否为 Array 的实例。

7.2、Symbol.isConcatSpreadable

Symbol.isConcatSpreadable 属性等于一个布尔值,表示该对象使用 Array.prototype.concat() 时是否可以展开

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", Array(2), "e"]

上面说明,数组的默认行为是可以展开的。Symbol.isConcatSpreadable 属性等于 true 或 undefined,都有这个效果

类是数组的对象也可以展开,但它的 Symbol._isConcatSpreadable 属性默认为 false,必须手动打开。

let obj = {
  length: 2,
  0: 'c',
  1: 'a'
};
['a', 'b'].concat(obj, 'e') // ["a", "b", {…}, "e"]

obj[Symbol.isConcatSpreadable] = true
['a', 'b'].concat(obj, 'e') // ["a", "b", "c", "a", "e"]

对于一个类而言,Symbol.isConcatSpreadable 属性必须写成实例的属性

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

class A2 extends Array {
  constructor(args) {
    super(args)
    this[Symbol.isConcatSpreadable] = 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, A2(2)]

A1 是可扩展的,A2 是不可扩展的,所以使用 concat 时有不一样的结果。

7.3、Symbol.species

对象的 Symbol.species 属性指向当前对象的构造函数。创造实例时默认调用这个方法,即使用这个属性返回的函数当作构造函数来创造新的实例对象。

class MyArray extends Array {
  // 覆盖父类 Array 的构造函数
  static get [Symbol.species]() {
    return Array
  }
}

var arr = new MyArray
arr instanceof Array // true

定义 Symbol.species 属性要采用 get 读取器。默认的 Symbol.species 属性等同于下面的写法。

static get [Symbol.species]() {
  return this
}

7.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)
  }
}

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

7.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)
'Hello'.replace(x, 'World') // ["Hello", "World"]

Symbol.replace 方法会接受到两个参数,第一个参数时 replace 方法正在作用的对象 在上面例子中是 Hello 第二个参数是替换后的值 在上面例子中 是 World.

7.6、Symbol.search

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

String.prototype.search(regexp)
// 等同于
regexp[Symbol.search](this)
class MySearch {
  constructor(value) {
    this.value = value
  }

  [Symbol.search] (string) {
    return string.indexOf(this.value)
  }
}

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

7.7、 Symbol.split

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

String.prototype.split(separator, limit)
// 等同于
separator[Symbol.split](this.limit)
class MySplitter {
  constructor(value) {
    this.value = value
  }
  [Symbol.split] (string) {
    var 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", ""]

7.8、Symbol.iterator

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

var myIterable = {}
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable]
// [1, 2, 3]

对象进行 for...of 循环时,会调用 Symbol.iterator 方法返回该对象的默认遍历器

7.9、Symbol.toPrimitive

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

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

  • 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

3 + obj
// "3default"
obj === 'default' // true
String(obj) // 'str'

7.10、Symbol.toStringTag

Symbol.toStringTag 属性指向一个方法,在对象上调用 Object.prototype.toString 方法时,如果这个属性存在,其返回值会出现在 toString 方法返回的字符串中,表示对象的类型。(这个属性可用于定制 [object Object] 类似的 后面的字符串)

({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"


class Collection {
  get [Symbol.toStringTag]() {
    return 'xxx'
  }
}
var x = new Collection()
Object.prototype.toString.call(x)
// "[object xxx]"

7.11、Symbol.unscopables

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • 1.概述 ES5的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象...
    赵然228阅读 798评论 2 10
  • 概述 ES5的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加...
    oWSQo阅读 508评论 1 3
  • [TOC] 参考阮一峰的ECMAScript 6 入门参考深入浅出ES6 let和const let和const都...
    郭子web阅读 1,768评论 0 1
  • 三,字符串扩展 3.1 Unicode表示法 ES6 做出了改进,只要将码点放入大括号,就能正确解读该字符。有了这...
    eastbaby阅读 1,506评论 0 8
  • 概要 64学时 3.5学分 章节安排 电子商务网站概况 HTML5+CSS3 JavaScript Node 电子...
    阿啊阿吖丁阅读 9,071评论 0 3