1. 程式人生 > >ES6中的Symbol類型

ES6中的Symbol類型

struct 類的構造函數 引入 ++ ring 指向 自己 混合 繼承

前面的話

  ES5中包含5種原始類型:字符串、數字、布爾值、null和undefined。ES6引入了第6種原始類型——Symbol

  ES5的對象屬性名都是字符串,很容易造成屬性名沖突。比如,使用了一個他人提供的對象,想為這個對象添加新的方法,新方法的名字就有可能與現有方法產生沖突。如果有一種機制,保證每個屬性的名字都是獨一無二的,這樣就從根本上防止了屬性名沖突。這就是ES6引入Symbol的原因,本文將詳細介紹ES6中的Symbol類型

創建

  Symbol 值通過Symbol函數生成。這就是說,對象的屬性名可以有兩種類型:一種是字符串,另一種是Symbol類型。凡是屬性名屬於 Symbol 類型,就都是獨一無二的,可以保證不會與其他屬性名產生沖突

let firstName = Symbol();
let person = {};
person[firstName] = "huochai";
console.log(person[firstName]); // "huochai"

  [註意]Symbol函數前不能使用new命令,否則會報錯。因為生成的 Symbol 是一個原始類型的值,不是對象

//Uncaught TypeError: Symbol is not a constructor
let firstName = new Symbol();

  Symbol函數接受一個可選參數,可以添加一段文本來描述即將創建的Symbol,這段描述不可用於屬性訪問,但是建議在每次創建Symbol時都添加這樣一段描述,以便於閱讀代碼和調試Symbol程序

技術分享
let firstName = Symbol("first name");
let person = {};
person[firstName] = "huochai";
console.log("first name" in person); // false
console.log(person[firstName]); // "huochai"
console.log(firstName); // "Symbol(first name)"
技術分享

  Symbol的描述被存儲在內部[[Description]]屬性中,只有當調用Symbol的toString()方法時才可以讀取這個屬性。在執行console.log()時隱式調用了firstName的toString()方法,所以它的描述會被打印到日誌中,但不能直接在代碼裏訪問[[Description]]

【類型檢測】

  Symbol是原始值,ES6擴展了typeof操作符,返回"symbol"。所以可以用typeof來檢測變量是否為symbol類型

let symbol = Symbol("test symbol");
console.log(typeof symbol); // "symbol"

使用

  由於每一個Symbol值都是不相等的,這意味著Symbol值可以作為標識符,用於對象的屬性名,就能保證不會出現同名的屬性。這對於一個對象由多個模塊構成的情況非常有用,能防止某一個鍵被不小心改寫或覆蓋

  所有使用可計算屬性名的地方,都可以使用Symbol

技術分享
let firstName = Symbol("first name");
// 使用一個需計算字面量屬性
let person = {
    [firstName]: "huochai"
};
// 讓該屬性變為只讀
Object.defineProperty(person, firstName, { writable: false });
let lastName = Symbol("last name");
Object.defineProperties(person, {
    [lastName]: {
        value: "match",
        writable: false
    }
});
console.log(person[firstName]); // "huochai"
console.log(person[lastName]); // "match"
技術分享

  在此示例中,首先通過可計算對象字面量屬性語法為person對象創建了個Symbol屬性firstName。後面一行代碼將這個屬性設置為只讀。隨後,通過Object.defineProperties()方法創建一個只讀的Symbol屬性lastName,此處再次使用了對象字面量屬性,但卻是作為object.defineProperties()方法的第二個參數使用  

  [註意]Symbol 值作為對象屬性名時,不能用點運算符

技術分享
var mySymbol = Symbol();
var a = {};

a.mySymbol = ‘Hello!‘;
a[mySymbol] // undefined
a[‘mySymbol‘] // "Hello!"
技術分享

  由上面結果看出,a.mySymbol和a[‘mySymbol‘]裏的mySymbol是字符串類型的屬性名,a[mySymbol]裏的mySymbol才是Symbol類型的屬性名。雖然都叫mySymbol,但值不相同

  盡管在所有使用可計算屬性名的地方,都可以使用Symbol來代替,但是為了在不同代碼片段間有效地共享這些Symbol,需要建立一個體系

共享體系

  有時希望在不同的代碼中共享同一個Symbol,例如,在應用中有兩種不同的對象類型,但是希望它們使用同一個Symbol屬性來表示一個獨特的標識符。一般而言,在很大的代碼庫中或跨文件追蹤Symbol非常困難而且容易出錯,出於這些原因,ES6提供了一個可以隨時訪問的全局Symbol註冊表

【Symbol.for()】

  如果想創建一個可共享的Symbol,要使用Symbol.for()方法。它只接受一個參數,也就是即將創建的Symbol的字符串標識符,這個參數同樣也被用作Symbol的描述

let uid = Symbol.for("uid");
let object = {};
object[uid] = "12345";
console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"

  Symbol.for()方法首先在全局Symbol註冊表中搜索鍵為"uid"的Symbol是否存在。如果存在,直接返回已有的Symbol,否則,創建一個新的Symbol,並使用這個鍵在Symbol全局註冊表中註冊,隨即返回新創建的Symbol

  後續如果再傳入同樣的鍵調用Symbol.for()會返回相同的Symbol

技術分享
let uid = Symbol.for("uid");
let object = {
    [uid]: "12345"
};
console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"
let uid2 = Symbol.for("uid");
console.log(uid === uid2); // true
console.log(object[uid2]); // "12345"
console.log(uid2); // "Symbol(uid)
技術分享

  在這個示例中,uid和uid2包含相同的Symbol並且可以互換使用。第一次調用Symbol.for()方法創建這個Symbol,第二次調用可以直接從Symbol的全局註冊表中檢索到這個Symbol

【Symbol.keyFor()】

  還有一個與Symbol共享有關的特性:可以使用Symbol.keyFor()方法在Symbol全局註冊表中檢索與Symbol有關的鍵

技術分享
let uid = Symbol.for("uid");
console.log(Symbol.keyFor(uid)); // "uid"
let uid2 = Symbol.for("uid");
console.log(Symbol.keyFor(uid2)); // "uid"
let uid3 = Symbol("uid");
console.log(Symbol.keyFor(uid3)); // undefined
技術分享

  uid和uid2都返回了"uid"這個鍵,而在Symbol全局註冊表中不存在uid3這個Symbol,也就是不存在與之有關的鍵,所以最終返回undefined

  [註意]Symbol.for為Symbol值登記的名字,是全局環境的,可以在不同的 iframe 或 service worker 中取到同一個值

let iframe = document.createElement(‘iframe‘);
iframe.src = String(window.location);
document.body.appendChild(iframe);

console.log(iframe.contentWindow.Symbol.for(‘foo‘) === Symbol.for(‘foo‘));// true

  上面代碼中,iframe 窗口生成的 Symbol 值,可以在主頁面得到

  Symbol全局註冊表是一個類似全局作用域的共享環境,也就是說不能假設目前環境中存在哪些鍵。當使用第三方組件時,盡量使用Symbol鍵的命名空間以減少命名沖突。例如,jQuery的代碼可以為所有鍵添加"jquery"前綴,就像"jquery.element"或其他類似的鍵

類型轉換

  類型轉換是JS中的一個重要語言特性,然而其他類型沒有與Symbol邏輯等價的值,因而Symbol使用起來不是很靈活

  使用console.log()方法來輸出Symbol的內容,它會調用Symbol的String()方法並輸出有用的信息。也可以像這樣直接調用string()方法來獲得相同的內容

let uid = Symbol.for("uid"),
    desc = String(uid);
console.log(desc); // "Symbol(uid)"

  String()函數調用了uid.toString()方法,返回字符串類型的Symbol描述裏的內容。但是,如果嘗試將Symbol與一個字符串拼接,會導致程序拋出錯誤

let uid = Symbol.for("uid"),
desc = uid + ""; // 引發錯誤!

  將uid與空字符串拼接,首先要將uid強制轉換為一個字符串,而Symbol不可以被轉換為字符串,故程序直接拋出錯誤

  同樣,也不能將Symbol強制轉換為數字類型。將Symbol與每一個數學運算符混合使用都會導致程序拋出錯誤

let uid = Symbol.for("uid"),
sum = uid / 1; // 引發錯誤!

  嘗試將Symbol除1,程序直接拋出錯誤。而且無論使用哪一個數學操作符,都無法正常運行

  [註意]布爾值除外,因為Symbol與JS中的非空值類似,其等價布爾值為true

let uid = Symbol.for("uid");
console.log(uid);//‘Symbol(uid)‘
console.log(!uid);//false
console.log(Boolean(uid));//true

屬性檢索

  Symbol作為屬性名,該屬性不會出現在for...in、for...of循環中,也不會被Object.getOwnPropertyNames()、Object.keys()、JSON.stringify()返回。於是,在ES6中添加了一個Object.getOwnpropertySymbols()方法來檢索對象中的Symbol屬性

  Object.getOwnPropertySymbols()方法的返回值是一個包含所有Symbol自有屬性的數組

技術分享
let uid = Symbol.for("uid");
let object = {
    [uid]: "12345"
};
let symbols = Object.getOwnPropertySymbols(object);
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(uid)"
console.log(object[symbols[0]]); // "12345"
技術分享

  在這段代碼中,object對象有一個名為uid的Symbol屬性,object.getOwnPropertySymbols()方法返回了包含這個屬性的數組

  另一個新的API——Reflect.ownKeys()方法可以返回所有類型的鍵名,包括常規鍵名和 Symbol 鍵名

技術分享
let obj = {
  [Symbol(‘my_key‘)]: 1,
  enum: 2,
  nonEnum: 3
};
console.log(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

  除了定義自己使用的Symbol值以外,ES6還提供了11個內置的Symbol值,指向語言內部使用的方法

  1、Symbol.haslnstance

  一個在執行instanceof時調用的內部方法,用於檢測對象的繼承信息

  2、Symbol.isConcatSpreadable

  一個布爾值,用於表示當傳遞一個集合作為Array.prototype.concat()方法的參數時,是否應該將集合內的元素規整到同一層級

  3、Symbol.iterator

  一個返回叠代器的方法

  4、Symbol.match

  一個在調用String.prototype.match()方法時調用的方法,用於比較字符串

  5、Symbol.replace

  一個在調用String.prototype.replace()方法時調用的方法,用於替換字符串的子串

  6、Symbol.search

  一個在調用String.prototype.search()方法時調用的方法,用於在字符串中定位子串

  7、Symbol.species

  用於創建派生類的構造函數

  8、Symbol.split

  一個在調用String.prototype.split()方法時調用的方法,用於分割字符串

  9、Symbol.toprimitive

  一個返回對象原始值的方法

  10、Symbol.ToStringTag

  一個在調用Object.prototype.toString()方法時使用的字符串,用於創建對象描述

  11、Symbol.unscopables

  一個定義了一些不可被with語句引用的對象屬性名稱的對象集合

【Symbol.haslnstance】

  每個函數都有一個Symbol.haslnstance方法,用於確定對象是否為函數的實例。該方法在Function.prototype中定義,所有函數都繼承了instanceof屬性的默認行為。為了確保Symbol.haslnstance不會被意外重寫,該方法被定義為不可寫、不可配置並且不可枚舉

  Symbol.haslnstance方法只接受一個參數,即要檢查的值。如果傳入的值是函數的實例,則返回true

obj instanceof Array;

  以上這行代碼等價於下面這行

Array[Symbol.hasInstance](obj);

  本質上,ES6只是將instanceof操作符重新定義為此方法的簡寫語法。現在引入方法調用後,就可以隨意改變instanceof的運行方式了

技術分享
class MyClass {
  [Symbol.hasInstance](foo) {
    return foo instanceof Array;
  }
}
console.log([1, 2, 3] instanceof new MyClass()); // true
技術分享

  假設定義一個無實例的函數,就可以將Symbol.haslnstance的返回值硬編碼為false

技術分享
function MyObject() {
    // ...
}
Object.defineProperty(MyObject, Symbol.hasInstance, {
    value: function(v) {
        return false;
    }
});
let obj = new MyObject();
console.log(obj instanceof MyObject); // false
技術分享

  只有通過Object.defineProperty()方法才能夠改寫一個不可寫屬性,上面的示例調用這個方法來改寫symbol.haslnstance,為其定義一個總是返回false的新函數,即使obj實際上確實是Myobject類的實例,在調用過object.defineProperty()方法之後,instanceof運算符返回的也是false

  當然,也可以基於任意條件,通過值檢查來確定被檢測的是否為實例。例如,可以將1~100的數字定義為一個特殊數字類型的實例,具體實現的代碼如下

技術分享
function SpecialNumber() {
  // empty
}
Object.defineProperty(SpecialNumber, Symbol.hasInstance, {
    value: function(v) {
        return (v instanceof Number) && (v >=1 && v <= 100);
    }
});
let two = new Number(2),
zero = new Number(0);
console.log(two instanceof SpecialNumber); // true
console.log(zero instanceof SpecialNumber); // false
技術分享

  在這段代碼中定義了一個symbol.hasInstance方法,當值為Number的實例且其值在1~100之間時返回true。所以即使SpecialNumber函數和變量two之間沒有直接關系,變量two也被確認為specialNumber的實例

  如果要觸發Symbol.haslnstance調用,instanceof的左操作數必須是一個對象,如果左操作數為非對象會導致instanceof總是返回false  

  當然,可以重寫所有內建函數(如Date和Error函數)默認的symbol.haslnstance屬性。但是這樣做的後果是代碼的運行結果變得不可預期且有可能令人感到困惑,所以不推薦這樣做,最好的做法是,只在必要情況下改寫自己聲明的函數的Symbol.haslnstance屬性

【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‘, [‘c‘,‘d‘], ‘e‘]
技術分享

  上面代碼說明,數組的默認行為是可以展開。Symbol.isConcatSpreadable屬性等於undefined或true,都有這個效果

  類數組對象也可以展開,但它的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 {
  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, [5, 6]]
技術分享

  上面代碼中,類A1是可展開的,類A2是不可展開的,所以使用concat時有不一樣的結果

【Symbol.species】

  對象的Symbol.species屬性,指向當前對象的構造函數。創造實例時,默認會調用這個方法,即使用這個屬性返回的函數當作構造函數,來創造新的實例對象

class MyArray extends Array {
  // 覆蓋父類 Array 的構造函數
  static get [Symbol.species]() { return Array; }
}

  上面代碼中,子類MyArray繼承了父類Array。創建MyArray的實例對象時,本來會調用它自己的構造函數,但是由於定義了Symbol.species屬性,所以會使用這個屬性返回的的函數,創建MyArray的實例

  這個例子也說明,定義Symbol.species屬性要采用get讀取器。默認的Symbol.species屬性等同於下面的寫法

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

  下面是一個例子

技術分享
class MyArray extends Array {
  static get [Symbol.species]() { return Array; }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);

mapped instanceof MyArray // false
mapped instanceof Array // true
技術分享

  上面代碼中,由於構造函數被替換成了Array。所以,mapped對象不是MyArray的實例,而是Array的實例

【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
技術分享

【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

【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
技術分享

【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‘, ‘‘]
‘foobar‘.split(new MySplitter(‘baz‘))// ‘foobar‘
技術分享

  上面方法使用Symbol.split方法,重新定義了字符串對象的split方法的行為

【Symbol.iterator】

  對象的Symbol.iterator屬性,指向該對象的默認遍歷器方法

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

[...myIterable] // [1, 2, 3]
技術分享

  對象進行for...of循環時,會調用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被調用時,會接受一個字符串參數,表示當前運算的模式,一共有三種模式

  1、Number:該場合需要轉成數值

  2、String:該場合需要轉成字符串

  3、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‘
技術分享

【String.toStringTag】

  對象的Symbol.toStringTag屬性,指向一個方法。在該對象上面調用Object.prototype.toString方法時,如果這個屬性存在,它的返回值會出現在toString方法返回的字符串之中,表示對象的類型。也就是說,這個屬性可以用來定制[object Object][object Array]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]"
技術分享

  ES6新增內置對象的Symbol.toStringTag屬性值如下、

技術分享
    JSON[Symbol.toStringTag]:‘JSON‘
    Math[Symbol.toStringTag]:‘Math‘
    Module[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環境排除。

技術分享
Array.prototype[Symbol.unscopables]
// {
//   copyWithin: true,
//   entries: true,
//   fill: true,
//   find: true,
//   findIndex: true,
//   includes: true,
//   keys: true
// }

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
}
技術分享

  上面代碼通過指定Symbol.unscopables屬性,使得with語法塊不會在當前作用域尋找foo屬性,即foo將指向外層作用域的變量

ES6中的Symbol類型