【ES6系列】Symbol

最近在學習一些第三方代碼,發現裏面出現了Symbol字段,因爲以前ES6系列梳理有個小暫停,因此本篇開始針對Symbol進行一下學習。

JavaScript 數據類型

在ES6以前,咱們所知道的JavaScript 數據類型有:es6

  • Null
  • Undefined
  • 布爾值(Boolean)
  • 字符串(String)
  • 數值(Number)
  • 對象(Object)
  • 數組(Array)

Symbol引入

在咱們常規的開發過程當中,特別是在多人協做過程當中,老是不可避免的會出現互相沖突覆蓋的狀況。好比,你使用了一個他人提供的對象,但又想爲這個對象添加新的方法(mixin 模式),新方法的名字就有可能與現有方法產生衝突。此時咱們可能會想,若是能有一個不被覆蓋,本身獨一無二的屬性字段該多好啊。從而ES6中引入了Symbol這一個數據類型,來解決這種衝突問題。跨域

Symbol()函數會返回symbol類型的值,該類型具備靜態屬性和靜態方法。它的靜態屬性會暴露幾個內建的成員對象;它的靜態方法會暴露全局的symbol註冊,且相似於內建對象類,但做爲構造函數來講它並不完整,由於它不支持語法:"new Symbol()"。數組

每一個從Symbol()返回的symbol值都是惟一的。一個symbol值能做爲對象屬性的標識符;這是該數據類型僅有的目的。app

語法結構

圖片描述

基本使用

直接使用Symbol()建立新的symbol類型,並用一個字符串(可省略)做爲其描述。函數

let sym1 = Symbol();
let sym2 = Symbol('foo');
let sym3 = Symbol('foo');

上面的代碼建立了三個新的symbol類型。Symbol函數能夠接受一個字符串做爲參數,表示對 Symbol 實例的描述,主要是爲了在控制檯顯示,或者轉爲字符串時,比較容易區分。注意,Symbol("foo") 不會強制字符串 「foo」 成爲一個 symbol類型。它每次都會建立一個新的 symbol類型:學習

Symbol("foo") === Symbol("foo"); // false

當使用new運算符的語法時,會拋出TypeError錯誤,這是由於生成的 Symbol 是一個原始類型的值,不是對象。也就是說,因爲 Symbol 值不是對象,因此不能添加屬性。基本上,它是一種相似於字符串的數據類型。spa

let sym = new Symbol(); // TypeError

若是 Symbol 的參數是一個對象,就會調用該對象的toString方法,將其轉爲字符串,而後才生成一個 Symbol 值。debug

const obj = {
  toString() {
    return 'abc';
  }
};
const sym = Symbol(obj);
sym // Symbol(abc)
注意,Symbol函數的參數只是表示對當前 Symbol 值的描述,所以相同參數的Symbol函數的返回值是不相等的。
// 沒有參數的狀況
let s1 = Symbol();
let s2 = Symbol();

s1 === s2 // false

// 有參數的狀況
let s1 = Symbol('foo');
let s2 = Symbol('foo');

s1 === s2 // false

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值能夠顯式轉爲字符串,或是布爾值,可是不能是數值。code

let sym = Symbol('My symbol');

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

Boolean(sym) // true
!sym  // false

if (sym) {
  // ...
}

Number(sym) // TypeError
sym + 2 // TypeError

做爲屬性名的Symbol

在前面咱們也說到了,Symbol的出現就是解決覆蓋衝突問題而產生的,它的主要做用也是被用做對象的屬性名來使用,由於它是獨一無二的,這樣就不會出現同名的屬性,也就不會出現覆蓋的狀況了。這對於一個對象由多個模塊構成的狀況很是有用,能防止某一個鍵被不當心改寫或覆蓋。

let mySymbol = Symbol();

// 第一種寫法
let a = {};
a[mySymbol] = 'Hello!';

// 第二種寫法
let a = {
  [mySymbol]: 'Hello!'
};

// 第三種寫法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上寫法都獲得一樣結果
a[mySymbol] // "Hello!"
注意,Symbol 值做爲對象屬性名時,不能用點運算符。
const mySymbol = Symbol();
const a = {};

a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"

上面代碼中,由於點運算符後面老是字符串,因此不會讀取mySymbol做爲標識名所指代的那個值,致使a的屬性名其實是一個字符串,而不是一個 Symbol 值。同理,在對象的內部,使用 Symbol 值定義屬性時,Symbol 值必須放在方括號之中。

let s = Symbol();

let obj = {
  [s]: function (arg) { ... }
};

obj[s](123);

Symbol 類型還能夠用於定義一組常量,保證這組常量的值都是不相等的。

const 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');

// 另外一個例子
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 值最大的好處,就是其餘任何值都不可能有相同的值了,所以能夠保證上面的switch語句會按設計的方式工做。

實例:消除魔術字符串

魔術字符串指的是,在代碼之中屢次出現、與代碼造成強耦合的某一個具體的字符串或者數值。
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: 'Triangle'
};

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

這時咱們能夠發現shapeType.triangle的具體值咱們並不關心,但它偏偏又能知足咱們的需求。

還有一點須要注意,Symbol 值做爲屬性名時,該屬性仍是公開屬性,不是私有屬性。

getOwnPropertySymbols遍歷

Symbol 做爲屬性名,該屬性不會出如今for...in、for...of循環中,也不會被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。可是,它也不是私有屬性,有一個Object.getOwnPropertySymbols方法,能夠獲取指定對象的全部 Symbol 屬性名。

const obj = {};
let a = Symbol('a');
let b = Symbol('b');

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

const objectSymbols = Object.getOwnPropertySymbols(obj);

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

Reflect.ownKeys

Reflect.ownKeys方法能夠返回全部類型的鍵名,包括常規鍵名和 Symbol 鍵名。

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

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

Symbol.for(),Symbol.keyFor()

在上面使用Symbol()函數的語法,不會在咱們整個代碼庫中建立出一個可用的全局的Symbol類型的變量,可是有時候咱們但願可以從新使用同一個Symbol值。要建立跨文件可用的symbol,甚至跨域(每一個都有它本身的全局做用域),咱們可使用Symbol.for() 方法和 Symbol.keyFor()方法從全局的Symbol註冊表中建立和獲取Symbol。

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

s1 === s2 // true

Symbol()和Symbol.for()的區別是:前者會被登記在全局環境中供搜索,後者不會。Symbol.for()不會每次調用就返回一個新的 Symbol 類型的值,而是會先檢查給定的key是否已經存在,若是不存在纔會新建一個值。好比,若是你調用Symbol.for("cat")30 次,每次都會返回同一個 Symbol 值,可是調用Symbol("cat")30 次,會返回 30 個不一樣的 Symbol 值

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

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

Symbol.keyFor方法返回一個已登記的 Symbol 類型值的key。

let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo", 已登記的Symbol值

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined, 未登記的Symbol值
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

小結

本次主要針對ES6中新增的數據類型Symbol進行了梳理,關於內置的Symbol值的相關內容,在後續使用到的過程當中再進行分享,基本的一些使用你們能夠參照ECMAScript 6入門中Symbol中的講解

相關文章
相關標籤/搜索