ES6學習筆記3---Symbol

weixin_33806914發表於2015-10-14

ES6引入Symbol的原因:防止屬性名的衝突(ES5的物件屬性名都是字串,容易造成屬性名的衝突)。

ES6引入一種新的原始資料型別:Symbol,表示獨一無二的值。它是javascript語言的第七種資料型別,前六種是:UndefinedNullBooleanStringNumberObject

Symbol值是通過Symbol函式生成的。

let s = Symbol();
typeof s  //"symbol"

注:Symbol函式前不能使用new命令,否則會報錯。報錯的原因就是:生成的Symbol是一個原始型別的值,而不是物件,所以不能新增屬性。它是一種類似於字串的資料型別。

Symbol函式可以接受一個字串作為引數,表示對Symbol例項的描述,主要是為了在控制檯顯示,或者轉為字串時,比較容易區分。

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

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

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

注:Symbol函式的引數只是表示對當前Symbol值的描述,因此相同引數的Symbol函式的返回值是不相等的。

// 沒有引數的情況
var s1 = Symbol();
var s2 = Symbol();

s1 === s2 // false

// 有引數的情況
var s1 = Symbol("foo");
var s2 = Symbol("foo");

s1 === s2 // false

Symbol值不能與其他型別的值進行運算。

var sym = Symbol('My symbol');
"your symbol is " + sym;
// Error: Cannot convert a Symbol value to a string

但是Symbol值可以轉為字串。

var sym = Symbol('My symbol');

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

2、作為屬性名的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!"

Object.defineProperty用來將物件的屬性名指定為一個Symbol值。

注: Symbol值作為物件屬性名時,不能用點運算子。

var mySymbol = Symbol();
var a = {};

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

上面程式碼中,因為點運算子後面總是字串,所以不會讀取mySymbol作為標識名所指代的那個值,導致a的屬性名實際上是一個字串,而不是一個Symbol值。

同理,在物件的內部,使用Symbol值定義屬性時,Symbol值必須放在方括號之中。

let s = Symbol();

let obj = {
  [s]: function (arg) { ... }
};
//等同於
let obj = {
  [s](arg) { ... }
};
obj[s](123);

上面程式碼中,如果s不放在方括號中,該屬性的鍵名就是字串s,而不是s所代表的那個Symbol值。

Symbol型別還可以定義一組常量,使這組常量的值不想等。

var log = {};
log.levels = {
  DEBUG: Symbol('debug'),
  INFO: Symbol('info'),
  WARN: Symbol('warn'),
};
console.log(log.levels.DEBUG, 'debug message');//Symbol(debug) 'debug message'
console.log(log.levels.INFO, 'info message');//Symbol(info) 'info message'

需要注意的是:Symbol值作為屬性名時,該屬性還是公開屬性,不是私有屬性。


3、屬性名的遍歷
Symbol作為屬性名,該屬性不會出現在for...infor...of迴圈中,也不會被Object.keys()Object.getOwnPropertyNames()返回。但是,它也不是私有屬性,有一個Object.getOwnPropertySymbols方法,可以獲取指定物件的所有Symbol屬性名。

注:這裡涉及到兩個物件方法,它們的作用是:

  • Object.getOwnPropertyNames:獲取所有的屬性名,不包括prototy中的屬性,返回一個陣列。

var o = Object.create({
    "say": function () {
        alert(this.name);
    },
    "name":"Byron"
});
Object.defineProperties(o, {
    'age': {
        value: 24,
        writable: true,
        enumerable: true,
        configurable: true
    },
    'sex': {
        value: 'male',
        writable: false,
        enumerable: false,
        configurable: false
    }
});
console.log(o); //結果如下圖
console.log(Object.getOwnPropertyNames(o));//["age", "sex"]

上面程式碼console.log(o)的結果

從上面的例子中可以看到prototype中的name屬性沒有獲取到。

  • Object.keys()
    getOwnPropertyNames方法類似,但是獲取所有的可列舉的屬性,返回一個陣列。

console.log(Object.keys(o)); //["age"]

Object.getOwnPropertySymbols方法返回一個陣列,成員是當前物件的所有用作屬性名的Symbol值。

var obj = {};

var foo = Symbol("foo");

Object.defineProperty(obj, foo, {
  value: "foobar",
});

for (var i in obj) {
  console.log(i); // 無輸出
  //但是直接在最新版本的chrome裡卻是輸出了{Symbol(foo): "foobar"}
}

Object.getOwnPropertyNames(obj)
// []

Object.getOwnPropertySymbols(obj)
// [Symbol(foo)]

有一個新的APIReflect.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)都無法獲取它。這就造成了一種非私有的內部方法的效果。


4、Symbol.for(),Symbol.keyFor()
Symbol.for方法:接受一個字串作為引數,並且建立的Symbol值是全域性的,如果我們已經有一個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

Symbol.for的工作原理就是:搜尋有沒有以該引數作為名稱的Symbol值,如果有,就返回這個Symbol值,否則就新建並返回一個以該字串為名稱的Symbol值。

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

s1 === s2   // true

下面我們來對比一下:Symbol.for()Symbol()
相同點:都會生成新的Symbol。
區別:

Item 全域性搜尋 每次呼叫返回一個新的Symbol值
Symbol.for() 會先檢測給定的key是否存在,如果不存在則建立新的Symbol值
Symbol()
Symbol.for("bar") === Symbol.for("bar")
// true

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

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

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

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

5、內建的Symbol值

除了自定義的Symbol值外,ES6還提供了11個內建的Symbol值,它們都是物件的屬性,指向語言內部使用的方法。

(1)、Symbol.hasInstance

該物件使用instanceof運算子時,會呼叫這個方法,判斷該物件是否為某個建構函式的例項。
比如:foo instanceof Foo在語言內部,實際呼叫的是Foo[Symbol.hasInstance](foo)

class MyClass {
  [Symbol.hasInstance](foo) {
    return foo instanceof Array;
  } //對這一段程式碼不是很理解
}
var o = new MyClass();
o instanceof Array // false
o instanceof MyClass // true

instanceof 運算子可以用來判斷某個建構函式的prototype屬性是否存在另外一個要檢測物件的原型鏈上。

用法:
object instanceof constructor
引數:
object:要檢測的物件
constructor:某個建構函式

function C(){}     //定義建構函式
function D(){}
var o = new C();
o instanceof C;    //true,因為:Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因為D.prototype不在o的原型鏈上
o instanceof Object; // true,因為Object.prototype.isPrototypeOf(o)返回true
C.prototype instanceof Object // true,同上
C.prototype = {};
var o2 = new C();
o2 instanceof C; // true
o instanceof C; // false,C.prototype指向了一個空物件,這個空物件不在o的原型鏈上.
D.prototype = new C();
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true

(2)、Symbol.isConcatSpreadable

物件的Symbol.isConcatSpreadable屬性等於一個布林值,表示該物件使用Array.prototype.concat()時,屬性值為true即為可以展開,false則不允許展開。

let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']

let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']   我執行的結果表示這個屬性沒有起作用

類似陣列的物件,它的Symbol.isConcatSpreadable屬性預設為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']   我執行的結果依舊錶示這個屬性沒有起作用,請大神賜教。

對於一個類來說,Symbol.isConcatSpreadable屬性必須寫成一個返回布林值的方法。

class A1 extends Array {
  [Symbol.isConcatSpreadable]() {
    return true;
  }
}
class A2 extends Array {
  [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]]
// 我的執行結果:[ 1, 2, A1 { '0': 3, '1': 4 }, A2 { '0': 5, '1': 6 } ]

(3)、Symbol.species

該物件作為建構函式創造例項時,會呼叫這個方法。即如果this.constructor[Symbol.species]存在,就會使用這個屬性作為建構函式,來創造新的例項物件。


(4)、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

上面程式碼,'e'.match(new MyMatcher())執行後,Symbol.match方法被呼叫,所以會返回字母e的位置時1。


(5)、Symbol.replace

當該物件被String.prototype.replace方法呼叫時,會返回該方法的返回值。

String.prototype.replace(searchValue, replaceValue)
// 等同於
searchValue[Symbol.replace](this, replaceValue)
class MyReplace{
    constructor(val1,val2){
        this.val1 = val1;
        this.val2 = val2;
    }
    [Symbol.replace](string){
        return string.replace(new RegExp(this.val1),this.val2);
    }
}

'hello world'.replace(new MyReplace('world','jumei'));  // hello jumei

(6)、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)、Symbol.split

當該物件被String.prototype.split方法呼叫時,會返回該方法的返回值。

String.prototype.split(separator, limit)
// 等同於
separator[Symbol.split](this, limit)
class MySplit{
    constructor(val1,val2){
        this.val1 = val1;
        this.val2 = val2;
    }
    [Symbol.split](string){
        return string.split(this.val1,this.val2);
    }
}

'How are you ?'.split(new MySplit(" ",3));
//[ 'How', 'are', 'you' ]

(8)、Symbol.iterator

物件的Symbol.iterator屬性,指向該物件的預設遍歷器方法,即該物件進行for...of迴圈時,會呼叫這個方法,返回該物件的預設遍歷器,詳細介紹參見《Iterator和for...of迴圈》一章。

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

(9)、Symbol.toPrimitive

該物件被轉為原始型別的值時,會呼叫這個方法,返回該物件對應的原始型別值。

JavaScript中的原始型別包括數字,字串和布林值。

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              我的執行結果:NaN
3 + obj // '3default'       我的執行結果:3[object Object]
obj === 'default' // true   我的執行結果:false
String(obj) // 'str'        我的執行結果:[object Object]

看來這個屬性現在或許還不支援吧,才造成我的執行結果是這樣的。


(10)、Symbol.toStringTag

在該物件上面呼叫Object.prototype.toString方法時,如果這個屬性存在,它的返回值會出現在toString方法返回的字串之中,表示物件的型別。

也就是說,這個屬性可以用來定製[object Object][object Array]中object後面的那個字串。

({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
//等同於
class Collection {
  get [Symbol.toStringTag]() {
    return 'Foo';
  }
}
var x = new Collection();
Object.prototype.toString.call(x) // "[object Foo]"

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'


(11)、Symbol.unscopables

該物件指定了使用with關鍵字時,哪些屬性會被with環境排除。

Array.prototype[Symbol.unscopables]
// {
//   copyWithin: true,
//   entries: true,
//   fill: true,
//   find: true,
//   findIndex: true,
//   keys: true
// }

Object.keys(Array.prototype[Symbol.unscopables])
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'keys']

上面程式碼說明,陣列有6個屬性,會被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
}

相關文章