首頁 > 軟體

vscode工具函數Symbol使用深入解析

2023-03-29 06:01:34

什麼是Symbol?

符號(Symbol)是JavaScript中的一個原始資料型別,是ECMAScript 6標準引入的新特性。符號是一種類似於字串的資料型別,但與字串不同的是,符號是唯一的並且不可變的。

Symbol的定義方法如下:

const mySymbol = Symbol('my symbol');

每次呼叫Symbol建立的值都是唯一的,即使對同一個引數呼叫兩遍Symbol它們的值還是不一樣的:

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

js的第六種基本資料型別

Symbol出現之前,Javascript已經有五種內建的基本資料型別:

  • 布林值(Boolean):表示真或假,只有兩個取值:true和false。
  • 數位(Number):表示整數或浮點數,可以使用十進位制、十六進位制、八進位制、科學計數法等多種表示方式。
  • 字串(String):表示文字字串,可以使用單引號、雙引號、反引號等方式表示。
  • 空值(Null):表示一個空值或不存在的物件。
  • 未定義(Undefined):表示一個未定義的值或未宣告的變數。

Symbol則作為第六種基本資料型別加入到語言中:

  • 符號(Symbol):表示唯一的、不可變的值,用於保護屬性名、實現私有屬性或方法等場景。

Symbol的起源

在JavaScript誕生之初,物件屬性只能使用字串作為鍵,這導致了一些問題。例如,當兩個不同的物件試圖使用相同的字串作為屬性名時,可能會導致屬性名衝突。此外,JavaScript中沒有一種簡單的方法來實現私有屬性或方法。

其實對於Symbol的追溯早在Lisp語言中就有體現:

(setq x (intern "my-symbol"))

這裡其實就是建立了一個名為my-symbol的符號物件,並將其賦值給變數x

另外,ES6引入Symbol其實離不開Ruby的身影,在Ruby中,可以使用冒號(:)來建立符號。冒號後面跟著符號的名稱,如:

:my_symbol

可以看到其實Ruby的語法更加簡潔,定義和使用都是用冒號區分:

person = {
  'name' => 'John',
  'age' => 30,
  :gender => 'Male'
}
puts person[:gender]  # 輸出:'Male'

所以,在這樣的需求背景下,ES6在首批特性中包含了Symbol也不足為奇了。

Symbol的基本知識

定義與使用

JavaScript中,可以使用Symbol()函數來建立一個符號,如下所示:

const mySymbol = Symbol();

Symbol函數可以接受一個描述性字串作為引數,用於識別符號號的含義,如下所示:

const mySymbol = Symbol('my symbol');

需要注意的是,每個Symbol()函數呼叫都會返回一個唯一的符號,即使描述性字串相同,它們也是不同的符號。

Symbol型別的值可以用作物件的屬性名,如下所示:

const mySymbol = Symbol('my symbol');
const myObject = {
  [mySymbol]: 'hello'
};
console.log(myObject[mySymbol]);  // 輸出:'hello'

在上面的程式碼中,我們使用符號mySymbol作為物件myObject的屬性名,並將其值設定為'hello'。使用符號作為屬性名的好處是它們不會與其他屬性名衝突,並且對外不可見,因此可以用於實現私有屬性或方法等場景。

另外,JavaScript中的Symbol型別有兩個特殊的方法Symbol.for()Symbol.keyFor(),用於建立全域性符號和獲取已經存在的全域性符號。

  • Symbol.for(): 用於建立或獲取一個全域性符號,如果全域性符號已經存在,則返回已經存在的符號,否則建立一個新的全域性符號。例如:
const mySymbol = Symbol.for('my symbol');
const sameSymbol = Symbol.for('my symbol');
console.log(mySymbol === sameSymbol);  // 輸出:true

在上面的程式碼中,我們使用Symbol.for()方法來建立一個全域性符號'my symbol',並將其賦值給mySymbol變數。然後,我們再次使用Symbol.for()方法來獲取同一個全域性符號,賦值給sameSymbol變數。由於全域性符號已經存在,因此sameSymbol變數的值等於mySymbol變數的值,輸出true

Symbol的重要屬性

1. Symbol.iterator: 用於指定物件的預設迭代器,例如:

const myObject = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
};
for (const value of myObject) {
  console.log(value);
}
// 輸出:1 2 3

在上面的程式碼中,我們為myObject物件設定了Symbol.iterator符號,並指定了一個生成器函數作為迭代器的實現。然後,我們可以使用for...of迴圈迭代myObject物件,並輸出其中的值。

2. Symbol.hasInstance: 用於定義一個物件是否為某個建構函式的範例。

Symbol.hasInstance方法接受一個引數,表示要檢查的物件。該方法需要返回一個布林值,表示該物件是否為該建構函式的範例。例如:

class MyClass {
  static [Symbol.hasInstance](obj) {
    return obj instanceof Array;
  }
}
console.log([] instanceof MyClass);  // 輸出:true
console.log({} instanceof MyClass);  // 輸出:false

在上面的程式碼中,我們定義了一個MyClass類,並使用Symbol.hasInstance方法自定義了instanceof運運算元的行為,使其檢查物件是否為陣列。當檢查[]物件時,instanceof運運算元返回true,因為[]是Array的範例;當檢查{}物件時,instanceof運運算元返回false,因為{}不是Array的範例。

需要注意的是,Symbol.hasInstance方法是一個靜態方法,需要定義在建構函式的靜態屬性中。另外,Symbol.hasInstance方法不能被繼承,因此子類需要重新定義該方法。

3. Symbol.toStringTag: 用於自定義物件的預設字串描述。

當呼叫Object.prototype.toString()方法時,會使用該物件的Symbol.toStringTag屬性作為預設的字串描述,例如:

class MyObject {
  get [Symbol.toStringTag]() {
    return 'MyObject';
  }
}
const obj = new MyObject();
console.log(Object.prototype.toString.call(obj));  // 輸出:'[object MyObject]'

在上面的程式碼中,我們定義了一個MyObject類,並使用Symbol.toStringTag屬性自定義了該類的預設字串描述。然後,我們建立了一個obj物件,並使用Object.prototype.toString()方法獲取其字串描述,輸出'[object MyObject]'

需要注意的是,Symbol.toStringTag屬性只有在呼叫Object.prototype.toString()方法時才會生效,對其他方法沒有影響。另外,如果沒有定義Symbol.toStringTag屬性,則預設使用建構函式的名稱作為字串描述。

4. Symbol.asyncIterator: 用於指定物件的預設非同步迭代器。

當使用for await...of迴圈迭代一個物件時,會呼叫該物件的Symbol.asyncIterator方法獲取非同步迭代器。

Symbol.asyncIterator方法需要返回一個非同步迭代器物件,該物件實現了next()方法,並返回一個Promise物件。當迭代器迭代到結束時,next()方法應該返回一個Promise物件,該Promise物件的value屬性為undefineddone屬性為true

例如,下面的程式碼演示瞭如何使用Symbol.asyncIterator屬性定義一個非同步迭代器:

const myObject = {
  async *[Symbol.asyncIterator]() {
    yield Promise.resolve(1);
    yield Promise.resolve(2);
    yield Promise.resolve(3);
  }
};
(async function() {
  for await (const value of myObject) {
    console.log(value);
  }
})();
// 輸出:1 2 3

在上面的程式碼中,我們為myObject物件設定了Symbol.asyncIterator符號,並指定了一個非同步生成器函數作為非同步迭代器的實現。然後,我們使用for await...of迴圈迭代myObject物件,並輸出其中的值。

需要注意的是,使用Symbol.asyncIterator屬性定義的非同步迭代器只能使用for await...of迴圈進行迭代,不能使用普通的for...of迴圈。此外,Symbol.asyncIterator屬性只有在支援非同步迭代器的環境中才能使用,例如Node.js的版本必須在10.0.0以上才支援非同步迭代器。

Symbol的實現原理

symbol作為基本資料型別實現比較簡單,在最新的v8程式碼實現如下:

Symbol Factory::NewSymbolInternal(AllocationType allocation) {
  DCHECK(allocation != AllocationType::kYoung);
  // Statically ensure that it is safe to allocate symbols in paged spaces.
  STATIC_ASSERT(Symbol::kSize <= kMaxRegularHeapObjectSize);
  Symbol symbol = Symbol::cast(AllocateRawWithImmortalMap(
      Symbol::kSize, allocation, read_only_roots().symbol_map()));
  DisallowGarbageCollection no_gc;
  // Generate a random hash value.
  int hash = isolate()->GenerateIdentityHash(Name::kHashBitMask);
  symbol.set_raw_hash_field(Name::kIsNotIntegerIndexMask |
                            (hash << Name::kHashShift));
  symbol.set_description(read_only_roots().undefined_value(),
                         SKIP_WRITE_BARRIER);
  symbol.set_flags(0);
  DCHECK(!symbol.is_private());
  return symbol;
}

該函數使用AllocateRawWithImmortalMap()方法為新的Symbol物件分配記憶體,並將其強制轉換為Symbol型別。接著,該函數使用DisallowGarbageCollection類禁用垃圾回收器,以確保不會在生成雜湊值的過程中觸發垃圾回收。接下來,該函數使用GenerateIdentityHash()方法生成一個隨機的雜湊值,並將其儲存在新的Symbol物件中。然後,該函數將Symbol物件的描述設定為undefined,並將其標誌設定為0。最後,該函數返回新建立的Symbol物件。

所以使用hash來唯一標識一個symbol,在v8內部還實現了symbol-table來實現Symbol.for的查詢,本質上也是一個雜湊表。

為了簡單起見,我們用js來模擬一下Symbol的實現:

const registry = {};
function createSymbol(description) {
  const symbol = Object.create(null);
  symbol.toString = () => `Symbol(${description || ''})`;
  Object.defineProperty(symbol, 'description', {
    value: description,
    writable: false,
    configurable: false,
    enumerable: false,
  });
  return symbol;
}
function Symbol(description) {
  if (typeof description !== 'undefined') {
    description = String(description);
  }
  if (registry[description]) {
    return registry[description];
  }
  const symbol = createSymbol(description);
  registry[description] = symbol;
  return symbol;
}
Symbol.for = function (key) {
  if (registry[key]) {
    return registry[key];
  }
  const symbol = createSymbol(key);
  registry[key] = symbol;
  return symbol;
};
Symbol.keyFor = function (symbol) {
  for (const key in registry) {
    if (registry.hasOwnProperty(key) && registry[key] === symbol) {
      return key;
    }
  }
};
export default Symbol;

我們使用一個全域性物件registry來儲存Symbol物件及其描述符資訊。createSymbol()函數用於建立新的Symbol物件,其中使用了Object.create()方法來建立一個沒有原型的物件,並通過定義toString()description屬性來實現Symbol物件的基本功能。Symbol()函數用於建立新的Symbol物件,它根據傳入的描述符資訊從registry中查詢Symbol物件,如果找到了則返回已有的Symbol物件,否則建立新的Symbol物件並新增到registry中。

Symbol的使用場景

SymbolVSCode的應用其實不多,最新的程式碼只有:

/**
 * Can be passed into the Delayed to defer using a microtask
 * */
export const MicrotaskDelay = Symbol('MicrotaskDelay');

在實際中,Symbol經常被用於:

1. 唯一屬性鍵:Symbol可以作為物件屬性的鍵,避免屬性名衝突。

這在建立第三方庫或外掛時非常有用,因為可以確保庫或外掛的屬性不會與其他程式碼意外衝突。

const uniqueKey = Symbol('uniqueKey');
const obj = {
  [uniqueKey]: 'This value is uniquely keyed'
};

2. 定義私有屬性(當然這一點現在ES規範已經有更好的方式了)

使用Symbol可以在物件上建立"私有"屬性,它們不會被常規的屬性列舉(如for...inObject.keys()JSON.stringify())包含在內。這有助於保護物件內部實現細節。

3. 內建Symbol

JavaScript內建了一些具有特定功能的Symbol。例如,Symbol.iterator可以定義物件的迭代行為,Symbol.toStringTag可以自定義Object.prototype.toString.call()方法的輸出。

4. 註冊全域性Symbol

Symbol.for()方法允許在全域性Symbol登入檔中建立或獲取Symbol。這對於跨多個地方或模組使用相同的Symbol時非常有用。

const globalSymbol = Symbol.for('globalSymbol');
const sameGlobalSymbol = Symbol.for('globalSymbol');
console.log(globalSymbol === sameGlobalSymbol); // true

Symbol的發展

tc39上已經有兩個關於Symbol的提案:

Symbols as WeakMap keys(Stage3)

Symbol作為一種新的資料型別,其功能和用途都比較有限,因此tc39Symbol的基礎上提出了一些新的提案,以擴充套件其功能和用途。其中一個比較重要的提案是Symbols as WeakMap keys,該提案已經進入到Stage3階段。

WeakMap是一種新的集合型別,可以用於儲存物件和關聯的後設資料。WeakMap的特點是鍵必須是物件,值可以是任意型別。WeakMap的另一個特點是,當鍵物件不再被參照時,WeakMap會自動刪除該鍵值對,以避免記憶體漏失。

Symbols as WeakMap keys提案的目的是將Symbol作為WeakMap的鍵。這樣,就可以在不影響WeakMap的自動垃圾回收機制的情況下,將Symbol作為物件的後設資料來使用。

const weak = new WeakMap();
// Pun not intended: being a symbol makes it become a more symbolic key
const key = Symbol('my ref');
const someObject = { /* data data data */ };
weak.set(key, someObject);

Symbol Predicates Proposal(Stage2)

這是另一個關於Symbol的提案,新增了以下判斷方法:Symbol.isRegistered(symbol)Symbol.isWellKnown(symbol)

其實對於庫作者而言,瞭解更多關於Symbol的資訊是很重要的。根據使用情況,瞭解一個Symbol是否真正唯一、可偽造(已註冊)或跨域共用(眾所周知)可能非常關鍵。例如,將Symbol用作WeakMap鍵需要確保Symbol未被註冊。該提案處於第二階段,正在受到JavaScript社群的廣泛關注。如果被採納,它將為Symbol的應用帶來更多的靈活性。

function isWeakMapKey(key) {
  switch (typeof key) {
    case "object":
      return key !== null;
    case "function":
      return true;
    case "symbol":
      return !Symbol.isRegistered(sym);
  }
  return false;
}
isWeakMapKey({}); // true
isWeakMapKey(Symbol()); // true
isWeakMapKey("foo"); // false
isWeakMapKey(Symbol.for("foo")); // false
isWeakMapKey(Symbol.asyncIterator); // true

您還可以檢查是否獲得了真正唯一的Symbol

const isUniqueSymbol = sym => typeof sym === "symbol" && !(Symbol.isRegistered(sym) || Symbol.isWellKnown(sym));
isUniqueSymbol(Symbol()); // true
isUniqueSymbol(Symbol.for("foo")); // false
isUniqueSymbol(Symbol.asyncIterator); // false
isUniqueSymbol({}); // false

小結

本文介紹了JavaScript中的Symbol型別,包括Symbol的建立、使用場景以及實現原理。Symbol是一種新的基本資料型別,用於表示唯一識別符號。與字串和數位不同,Symbol值是唯一的,不可修改和可列舉的。Symbol的主要用途包括:定義唯一屬性鍵、定義私有屬性、內建Symbol和註冊全域性Symbol

此外,文章還介紹了兩個關於Symbol的提案:Symbols as WeakMap keysSymbol Predicates Proposal。這些提案旨在擴充套件Symbol的功能和用途,併為JavaScript開發人員提供更多的選項。

總之,SymbolJavaScript新增了一個新的基本資料型別,為開發人員提供了一種新的表示唯一識別符號的方式,可以用於建立唯一屬性鍵、定義私有屬性、內建Symbol和註冊全域性Symbol等用途。

在實際專案中,如果遇到定義一個唯一的key的場景,就可以考慮使用 Symbol 來完成,可以避免衝突。

以上就是vscode工具函數Symbol使用深入解析的詳細內容,更多關於vscode工具函數Symbol的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com