网站首页 > 技术文章 正文
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
1. 什么是 Symbol 原始类型
在 JavaScript 中,对象的属性键只能是字符串或 Symbol,那么什么是 Symbol 呢?
Symbol 是一种原始数据类型,类似于字符串或数字。其不能通过字面量创建,只能通过使用 Symbol 包装器对象构造函数来创建,任何使用 Symbol 作为构造函数(new)来创建显式 Symbol 包装器对象都会引发 TypeError。
const myNumber = Number(2);
console.log(myNumber);
// 打印: 2
const mySymbol = Symbol();
console.log(typeof mySymbol);
// 打印: "symbol"
console.log(mySymbol);
// 打印: Symbol()
在上面的例子中,如果参数本身不是数字,Number(argument) 会自动转换为数字。但 Symbol() 并没有参数,其不会涉及任何转换。
Symbol() 会创建隐藏的、唯一的原始值,尤其适用于对象属性的键。不过,开发者也可以向 Symbol() 添加字符串参数,其表示 Symbol 的描述。但实际上没有特殊意义,只是使 Symbol() 更具描述性或可识别性,而 Symbol.description 属性则原样返回 Symbol 的该只读描述。
// Symbol 的值是唯一的
console.log(Symbol("foo") === Symbol("foo"));
// 打印: false
const myObj = {};
myObj[Symbol("foo")] = 1;
myObj[Symbol("foo")] = 2;
console.log(myObj);
// 打印: {Symbol("foo"): 1, Symbol("foo"): 2 }
// 通过. description 属性获取 Symbol 描述
const mySymbol = Symbol("some description");
console.log(mySymbol.description);
// 打印: "some description"
2.Symbol 的类型转换
Symbol 永远无法强制转换为数字,否则抛出 TypeError 错误。
// 抛出 TypeError 错误
console.log(+Symbol());
console.log(Number(Symbol()));
Symbol 也不会强制转换为字符串(否则抛出 TypeError),但可以通过使用 String(symbol) 或 symbol.toString(),但不能通过 new String(symbol) 转换为字符串。
console.log(String(Symbol("foo")));
// 打印: "Symbol(foo)"
console.log(Symbol("foo") + "bar");
// 抛出错误 TypeError: can't convert symbol to string
最后值得一提的是,Symbol 转换为布尔值总是为 true:
console.log(Boolean(Symbol()));
// 打印: true
if (Symbol()) {
console.log("This will be logged.");
}
// 打印: "This will be logged."
3.Symbol 作为属性键
前文讲过,Symbol 特别适合创建唯一的对象属性键且不会与其他键冲突,并且能够尽量隐藏该属性。
const symbolKey = Symbol();
// 创建一个 Symbol 的属性
const someObj = {
[symbolKey]: "Some property value",
};
console.log(someObj[symbolKey]);
// 打印: "Some property value"
在上面的例子中,对象的属性不会被其他代码覆盖。同样,保存 Symbol 值的 symbolKey 也不能被覆盖,因为其是一个常量。如果这个常量的值不是 Symbol,例如是一个字符串,则可以被覆盖:
const stringKey = "keyName";
const someObj = {
[stringKey]: "Some property value",
};
console.log(someObj.keyName === someObj[stringKey]);
// 打印: true
console.log(someObj[stringKey]);
// 打印: "Some property value"
someObj.keyName = "Overwritten ";
console.log(someObj[stringKey]);
// 打印: "Overwritten "
4.Symbol 属性的可枚举性
与字符串键一样,带有 Symbol 键的属性默认设置为 可写、可枚举和可配置,除非是使用 Object.defineProperty() 创建的(默认将属性设置为 false)。并且,与带有字符串键的属性一样,带有 Symbol 键的属性也可以使用 Object.defineProperty() 进行更改。
const symbolKey = Symbol();
const someObj = {
[symbolKey]: "Some property value",
};
const descriptor = Object.getOwnPropertyDescriptor(someObj, symbolKey);
console.log(descriptor);
// 打印结果为:
// {value: "Some property value", writable: true, enumerable: true, configurable: true}
带有字符串键的可枚举属性在 for...in 和 Object.keys 枚举中会被访问。然而,Symbol 键属性即使可枚举也会被跳过,正如在 MDN 文章 “属性的可枚举性和所有权” 的表格中所见,只有
Object.getOwnPropertySymbols 可以遍历 Symbol 键属性。
同时少数方法,例如:
Object.getOwnPropertyDescriptors 和 Object.assign ,也可以同时遍历字符串键和 Symbol 键属性。
const someObj = {
prop1: 3,
prop2: "Hello",
[Symbol("symbol1")]: "symbol one",
[Symbol("symbol2")]: "symbol two",
};
const objectStrings = Object.keys(someObj);
console.log(objectStrings);
// 打印结果: ["prop1", "prop2"]
const objectSymbols = Object.getOwnPropertySymbols(someObj);
console.log(objectSymbols);
// 打印结果: [Symbol("symbol1"), Symbol("symbol2") ]
objectSymbols.forEach((symbolKey) => console.log(someObj[symbolKey]));
// 打印结果:"symbol one", "symbol two"
值得注意的是,Object.assign 以及扩展语法 (...) 可用于克隆或合并对象,且包括 Symbol 键属性。
5. 聊聊全局 Symbol
Symbol.for(key) 会创建一个 全局 Symbol,可以通过使用相同的键重复执行来检索该 Symbol。
Symbol.for(key) 方法使用给定的键在 “全局 Symbol 注册表” 中搜索现有 Symbol,如果找到则返回该 Symbol,否则使用此键在全局 Symbol 注册表中创建一个新 Symbol。只有使用 Symbol.for() 创建的 Symbol 才会保留在全局 Symbol 注册表中,其被称为全局 Symbol 或共享 Symbol。
console.log(Symbol.for("foo") === Symbol.for("foo"));
// 打印: true
console.log(String(Symbol.for("foo")));
// 打印: "Symbol(foo)"
需要注意的是,全局 Symbol 也是唯一的值,键也是唯一的。全局 Symbol 不能从全局 Symbol 注册表中删除,也不能被覆盖。
与局部 Symbol 不同,全局 Symbol 可以跨文件和跨域使用。全局 Symbol 可以用作属性名称,以便将其在常见的遍历方法中隐藏。但是,使用全局 Symbol 作为属性键可以覆盖当前属性值。
const myObj = {
[Symbol.for("foo")]: "some property value",
};
console.log(myObj[Symbol.for("foo")]);
// 打印: "some property value"
myObj[Symbol.for("foo")] = "Overwritten ";
console.log(myObj[Symbol.for("foo")]);
// 打印: "Overwritten ",且已经被覆盖
6. 全局 Symbol 注册
“全局 Symbol 注册表” 只是一个概念,用于描述仅可通过 Symbol.for() 和 Symbol.keyFor() 方法获取的全局 Symbol 的记录。
Symbol.keyFor() 方法从全局 Symbol 注册表中检索给定全局 Symbol 的全局 Symbol 键。
const globalSym = Symbol.for("someKey");
// 设置一个全局 Symbol
console.log(Symbol.keyFor(globalSym));
// 打印: "someKey"
全局 Symbol 注册表与全局对象不同,或者说其不是全局对象的一部分,全局 Symbol 注册表对所有关联的域 (realms) 都是全局的。本质上,全局 Symbol 是跨文件和跨域可用的,但每个文件和域都有各自的全局作用域。
关于域的概念下面在做一个深入的剖析:
一个应用程序可能由多个 JavaScript 环境组成,每个环境都有各自的全局作用域和全局对象,而环境被称为一个域 (Realm)。从代码内部打开的窗口、网页中的 <iframe> 以及 Web Worker 都是域。一个域中的代码可以访问其他关联域中的代码,但并不共享同一个全局作用域。
<iframe srcdoc="<script>window.parent.someFunction(['foo','bar'])</script>"></iframe>
<script>
function someFunction(arg) {
console.log(arg instanceof Array); // logs: false
}
</script>
在上面的例子中,参数数组 ['foo', 'bar'] 是在 <iframe> 窗口中通过 <iframe> 域中的 Array 构造函数构造的,并且属于该全局作用域的一部分。因此,该数组不是父窗口(父窗口位于另一个域)中 Array 的实例。
7. 有那些常见的 JS 内置 Symbol
Symbol 构造函数提供了许多 “内置” 属性,这些属性本身都是 Symbol,被称为内置 Symbol。这些内置 Symbol 用作属性名或方法名,JavaScript 可以 “识别” 这些属性名并在内部使用属性名来标识某些操作的 “协议”。
开发者可以通过属性值自定义这些操作,从而自定义对象的行为:
const iterable1 = {};
iterable1[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
console.log([...iterable1]);
// 打印 Array [1, 2, 3]
console.log(Symbol.keyFor(Symbol.iterator));
// 打印 undefined
那么第二个日志为什么输出 undefined?这是因为 Symbol.iterator 是 JavaScript 内置的一个 Symbol 值,用于定义对象的迭代器协议。其是一个静态的、不可变的 Symbol ,且是通过 Symbol(description) 创建的,而不是通过 Symbol.for(key) 创建的。
注意:通过 Symbol() 创建能保证属性不冲突。
当然常见的 Symbol 还包括 Symbol.toPrimitive,所有类型强制转换算法都会在对象上查找此 Symbol,查找接受首选类型并返回对象原始表示的方法,最后默认使用对象的 valueOf() 和 toString() 。
const object1 = {
[Symbol.toPrimitive](hint) {
if (hint === "number") {
return 42;
}
return null;
},
};
console.log(+object1);
// 预期输出: 42
8. 非全局 Symbol 可以正常垃圾回收
前端开发者都知道 WeakMap 和 WeakSet 只能存储对象或 Symbol,这是因为只有对象会被垃圾回收,原始值可以被伪造 (forged),即: 1 === 1 但 {} !== {},从而会使得原始值永远存在于集合中。
注意:forged 意味着可以被模仿、复制或重新创建。换句话说,原始值由于其简单性和不可变性,很容易被重新生成或伪造出相同的值。
例如下面是原始值伪造的示例:
const original = 42;
const forged = 42;
console.log(original === forged);
// true
同时,通过 Symbol.for("key") 创建的全局 Symbol 也可以被伪造,因此无法被垃圾回收。
const sym1 = Symbol.for("myKey");
let sym1Ref = sym1;
sym1Ref = null;
// 注意:原始值是直接拷贝,而非引用赋值
// 再次通过相同的 key 获取这个 Symbol
const sym2 = Symbol.for("myKey");
console.log(sym1 === sym2);
// true,证明 sym1 仍然存在,未被垃圾回收
但使用 Symbol("key") 创建的 Symbol 是可以被垃圾回收的,因为 Symbol 是唯一具有引用标识的原始数据类型 ,即开发者不能两次创建相同的 Symbol,因此其在某种程度上表现得非常像对象。
也正是因为此,非全局 Symbol 可以存储在 WeakMap、WeakSet、WeakRef 和 FinalizationRegistry 对象中。
参考资料
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Memory_management
https://library.fridoverweij.com/docs/jstutorial/symbol.html
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
https://www.youtube.com/watch?v=6R82DEqrelw
猜你喜欢
- 2025-05-15 宇宙厂:深入聊聊 CJS 和 ESM 模块化三点核心差异?
- 2025-05-15 #前端高手进阶#一起薅羊毛~
- 2025-05-15 前端基础进阶(十):深入详解函数的柯里化
- 2025-05-15 2025 年 Object 和 Map 如何选择?
- 2025-05-15 为何说 postMessage 才是真正的 setTimeout(0)?
- 2025-05-15 为什么高手写 JS 总是又快又好?这10个技巧你要知道
- 2025-05-15 2025 年 Deno 终于官宣 pnpm 和 Yarn 可使用 JSR?
- 2025-05-15 宇宙厂:为什么前端要了解 Interaction to Next Paint (INP)
- 2025-05-15 Node.js 原生支持 TypeScript?开发者需要了解的一切
- 2025-05-15 请务必用 postTask/isInputPending 释放JS主线程!
你 发表评论:
欢迎- 05-23浅谈3种css技巧——两端对齐
- 05-23JSONP安全攻防技术
- 05-23html5学得好不好,看掌握多少标签
- 05-23Chrome 调试时行号错乱
- 05-23本文帮你在Unix上玩转C语言
- 05-23Go 中的安全编码 - 输入验证
- 05-2331个必备的python字符串方法,建议收藏
- 05-23Dynamics.js – 创建逼真的物理动画的 JS 库
- 最近发表
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端react (48)
- 前端md5加密 (49)
- 前端路由 (55)
- 前端数组 (65)
- 前端定时器 (47)
- 前端接口 (46)
- Oracle RAC (73)
- oracle恢复 (76)
- oracle 删除表 (48)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle约束 (46)
- oracle 中文 (51)
- oracle链接 (47)
- oracle的函数 (57)
- mac oracle (47)
- 前端调试 (52)
- 前端登录页面 (48)
本文暂时没有评论,来添加一个吧(●'◡'●)