vscode工具函数Symbol使用深入解析
作者:孟健
什么是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
属性为undefined
,done
属性为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的使用场景
Symbol
在VSCode
的应用其实不多,最新的代码只有:
/** * 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...in
,Object.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
作为一种新的数据类型,其功能和用途都比较有限,因此tc39
在Symbol
的基础上提出了一些新的提案,以扩展其功能和用途。其中一个比较重要的提案是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 keys
和Symbol Predicates Proposal
。这些提案旨在扩展Symbol
的功能和用途,并为JavaScript
开发人员提供更多的选项。
总之,Symbol
为JavaScript
添加了一个新的基本数据类型,为开发人员提供了一种新的表示唯一标识符的方式,可以用于创建唯一属性键、定义私有属性、内置Symbol
和注册全局Symbol
等用途。
在实际项目中,如果遇到定义一个唯一的key
的场景,就可以考虑使用 Symbol
来完成,可以避免冲突。
以上就是vscode工具函数Symbol使用深入解析的详细内容,更多关于vscode工具函数Symbol的资料请关注脚本之家其它相关文章!