ES6特性:Symbol獨一無二的類型

ES6: Symbol

簡介

JS 的對象屬性名都是字符串,常常容易造成命名衝突,當我們寫好別人創造的對象時,可能會覆蓋掉其他人定義過的屬性。這時候 Symbol 登場了,作為 JS 第七種原生類型,每一個都擁有全場獨一無二的值,每次構造函數創建出來的值都是獨一無二了,同時能夠作為對象的屬性名,如此便能成功將對象的屬性區隔開來。

參考

ES6 symbolhttp://caibaojian.com/es6/symbol.html
ES6 中的元编程https://juejin.im/post/5a0e65c1f265da430702d6b9

正文

Symbol 創建

Symbol 可以透過兩種方式創建:

  1. Symbol(string):構造函數創建,為全局獨一無二的值
  2. Symbol.for(string):創建 Symbol 的同時,向全局註冊一個字符串,字符串已經被註冊的時候就會引用同一個對象。使用 Symbol.keyFor(symbol) 查看構造 Symbol 對象時向全局註冊的值
const s1 = Symbol()
s1 // Symbol()
typeof s1 // "symbol"
s1.toString() // "Symbol()"
s1 + '' // Cannot convert a Symbol value to a string

const s2 = Symbol()
s1 === s2 // false

const s3 = Symbol.for('1')
const s4 = Symbol.for('1')
s3 === s4 // true
Symbol.keyFor(s3) // "1"

const s5 = Symbol.for()
Symbol.keyFor(s5) // "undefined"
Symbol.keyFor(s1) // undefined
  • 說明 1:s1、s2 為不同的對象,所以即使構造函數傳入的參數相同在 === 判斷下還是不同
  • 說明 2:s3、s4 都使用了 Symbol.for,所以 s3 向全局註冊"1"之後,s4 創建時引用的是與 s3 相同的對象,所以比較相同
  • 說明 3:s5 無參數的 Symbol.for() 向全局註冊的字符串為 “undefined”,而 s1 並未向全局註冊所以返回 undefined,兩者有區別。

Usage 使用

作為屬性名

透過 ES6 擴展,對象屬性名可以使用方括號組合出屬性名,除了 string 之外 Symbol 也可以作為屬性名:

const key = Symbol('key')
const func = Symbol('func')
const obj = {
  key: 'string key value',
  [key]: 'Symbol key value',
  [func]() {
    console.log('invoke function which name is a Symbol')
  }
}
obj // {key: "string key value", Symbol(key): "Symbol key value", Symbol(func): ƒ}
obj.key // string key value
obj[key] // Symbol key value
obj[func]() // invoke function which name is a Symbol

消除魔術字符串(Magic string)

我們模仿枚舉,檢查屬性名的時候可能會出現意想不到的重名,這時候就可以透過 Symbol 來避免這個問題:

// bad
const shape = {
  triangle: 'triangle',
  square: 'square',
  rectangle: 'rectangle'
}
// good
const shape = {
  triangle: Symbol(),
  square: Symbol(),
  rectangle: Symbol()
}

function area(shape, options) {
  switch (shape) {
    case shape.triangle:
      return 0.5 * options.width * options.height
    case shape.square:
      return options.length * options.length
    case shape.rectangle:
      return options.width * options.height
  }
  return 0
}

area(shape.triangle, { width: 50, height: 100 })
// 2500
area(shape.square, { length: 50 })
// 2500
area(shape.rectangle, { width: 50, height: 100 })
// 5000

非私有內部方法

當然你也可以夠過 get/set 定義一個完全私有的方法,不過這邊的重點在於透過簡單的 Symbol 作為屬性可以對一些遍歷和獲取屬性名屏蔽:

const top = Symbol()

class Stack {
  constructor() {
    this[top] = -1
  }
  pop() {
    const val = this[this[top]]
    delete this[this[top]--]
    return val
  }
  push(val) {
    this[++this[top]] = val
  }
}
stack.push(1) // Stack { '0': 1, [Symbol()]: 0 }
stack.push(2) // Stack { '0': 1, '1': 2, [Symbol()]: 1 }
stack.push(3) // Stack { '0': 1, '1': 2, '2': 3, [Symbol()]: 2 }

for (let key in stack) {
  console.log(`stack[${key}] = ${stack[key]}`)
}
// stack[0] = 1
// stack[1] = 2
// stack[2] = 3

stack.top = -1 // Stack { '0': 1, '1': 2, '2': 3, [Symbol()]: 2, 'top': -1 }
  • 說明:this[top] 將作為棧頂指針,對 for...inObject.getOwnPropertyNames() 都是透明的,模塊外只能透過 Object.getOwnPropertySymbols() 獲得 top 對象。並且再定義字符串作屬性名也沒有問題。

內置 Symbol 值

老實說前面的應用我個人認為在真正開發的時候其實都是可以避免的,或是最多就是多一層保險而已。然而接下來這部分我想才是 Symbol 最重要的應用:內置屬性

我們都知道 JS 對象的接口方法、繼承方法等都是添加在原型上,然而如果原生添加了過多的屬性方法,都是透過字符串作為屬性名的話,似乎有點佔用空間並且擠壓到開發者的命名空間,Symbol 正是一個好的解決辦法!

Overview

ES6 總共添加了 11 種內置 Symbol 值,有點像是 Symbol 類的靜態類型,取代字符串作為方法名,便能將這些特殊的方法與一般方法區隔開來,同時許多 JS 內置方法也將調用以 Symbol 值為名的方法。透過這些 Symbol 內置值,程序員開始能夠介入一些內置方法的操作而不用再透過多餘的方法調用。

ValuesAbout
Symbol.hasInstanceinstanceof 運算符
Symbol.isConcatSpreadableArray.prototype.concat()
Symbol.species
Symbol.matchString.prototype.match()
Symbol.replaceString.prototype.replace()
Symbol.searchString.prototype.search()
Symbol.splitString.prototype.split()
Symbol.iteratorfor…of
Symbol.toPrimitiveNumber(), String()
Symbol.toStringTagObject.prototype.toString()
Symbol.unscopableswith(obj)

Symbol.hasInstance

相當於置換 instanceof 運算符的行為,下列兩種調用等價:

A instanceof B
// 等價於
B[Symbol.hasInstance](A)

因此我們可以這樣為我們的類定義:

class B {
  [Symbol.hasInstance](other) {
    return typeof other === "object"
  }
}
'object' instanceof new B() // false
{} instanceof new B() // true

Symbol.isConcatSpreadable

這個屬性對應的值是一個布林值,將決定對象被 Array.prototype.concat() 調用的時候是否被展開,默認為 true

let arr1 = [3, 4]
;[1, 2].concat(arr1, 5)
// [ 1, 2, 3, 4, 5 ]

arr1[Symbol.isConcatSpreadable] = false
;[1, 2].concat(arr1, 5)
// [ 1, 2, [ 3, 4 ], 5 ]

Symbol.species

Symbol.species 提供訂製構造函數的入口,當我們利用 Array.prototype.map 或其他內置方法從實例創建新對象的時候,內置方法就會調用 this[Symbol.species] 來構造新對象的類

應用場景通常是我們需要指定一個將會返回以新的自己的類的對象,例如 Promise 調用 then 方法之後會返回一個新的 Promise 等:

class DelayPromise extends Promise {
  static get [Symbol.species]() {
    return Promise
  }
}
const p = new DelayPromise(() => {})
p instanceof DelayPromise // true

const p2 = p.then(() => {})
p2 instanceof DelayPromise // false
p2 instanceof Promise // true

Symbol.match

當調用 String.prototype.match() 的時候,傳入參數存在 Symbol.match 的話就會以這個方法替代,即可以客製化 match 的行為

即以下兩個表達式等價:

String.prototype.match(regexp)
// 等價於
regexp[Symbol.match](this)
class Matcher {
  [Symbol.match](string) {
    return string.indexOf('e')
  }
}
'Hello world'.match(new Matcher()) // 1

Symbol.replace

當調用 String.prototype.replace() 的時候,以 Symbol.replace 的方法來置換,即可以透過此方法客製化 replace 的行為,即下列表達式等價

String.prototype.replace(searchVal, replaceVal)
// 等價於
searchVal[Symbol.replace](this, replaceVal)
const replacement = {}
replacement[Symbol.replace] = (searchVal, replaceVal) => {
  let result = ''
  for (let c of searchVal) {
    if (c === 'e') {
      result += '-'
    } else {
      result += c
    }
  }
  return result
}
'Hello'.replace(replacement, '-') // H-llo

Symbol.search

當調用 String.prototype.search() 的時候,以 Symbol.search 的方法來置換,即可以透過此方法客製化 search 的行為,即下列表達式等價

String.prototype.search(regexp)
// 等價於
regexp[Symbol.search](this)
class SearchEngine {
  constructor(target) {
    this.target = target
  }
  [Symbol.search](s) {
    return s.indexOf(this.target)
  }
}
'Hello World'.search(new SearchEngine('llo')) // 2

Symbol.split

與 match, replace, search,就是用 Symbol.split 替代原本的 split(ES6 的團隊真的很喜歡字符串操作。。。

String.prototype.split(separator, limit)
// 等價於
separator[Symbol.split](this, limit)

Symbol.iterator

Symbol.iterator 為一個 Generator 函數,在使用 for...of 的時候依次調用這個函數,與 Generator 相關的細節請看另一篇:

const obj = {}
obj[Symbol.iterator] = function* () {
  for (let i = 0; i < 11; i += 5) {
    yield i
  }
}
for (let val of obj) {
  console.log(val)
}
// output:
// 0
// 5
// 10

Symbol.toPrimitive

當對象觸發隱式的類型轉換,如 obj + 1obj + '1' 之類的動作,就會調用 obj[Symbol.toPrimitive] 方法,這個方法傳入一個狀態,狀態存在三種模式:

  1. ‘number’:被轉成數字的時候
  2. ‘string’:被轉成字符串的時候
  3. ‘default’:可以被轉成數字也可以被轉成字符串的時候
const obj = {
  [Symbol.toPrimitive](mode) {
    switch (mode) {
      case 'number':
        return 100
      case 'string':
        return '***'
      case 'default':
        return '???'
      default:
        throw new Error('unexpected mode')
    }
  }
}
3 + obj // 3???
3 - obj // -97
3 * obj // 300
3 / obj // 0.03
'???' == obj // true
Number(obj) // 100
String(obj) // ***

Symbol.toStringTag

一般來說如果直接調用 Object.prototype.toStrign() 的話應該會返回 [object Object][object Array],而 Symbol.toStringTag 可以客製化這個方法(聽起來頗沒用):

const obj = {
  get [Symbol.toStringTag]() {
    return '???'
  }
}
obj.toString() // [object ???]

Symbol.unscopables

Symbol.unscopables 指向一個對象,指定使用 with 關鍵字的時候會被忽略的屬性:

class Obj {
  foo() {
    console.log('invoke foo in obj')
  }
  bar() {
    console.log('invoke bar in obj')
  }
  get [Symbol.unscopables]() {
    return {
      foo: true
    }
  }
}
function foo() {
  console.log('invoke foo out of obj')
}
function bar() {
  console.log('invoke bar out of obj')
}
with (Obj.prototype) {
  foo()
  bar()
}

with 的詳細說明詳見其他篇

結語

Symbol 看似不顯眼卻是相當前瞻的想法,考慮到未來將有更多的框架和庫都將提供一些定製化的對象,Symbol 能夠很好的避免使用者更動到原本提供的內部屬性。縱然外部的程序員依舊能夠透過 Symbol.for 或是 Object.getOwnPropertySymbols,但是只要使用者不作死,Symbol 已經能很大程度的屏蔽幾乎所有遍歷方法和捕捉對象屬性的方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值