2025 年是时候重新认识 Symbol 的八大特性了?
myzbx 2025-07-17 22:54 2 浏览
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
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
相关推荐
- 资深架构师亲授,从堆栈到GC,一篇文章打通任督二脉!
-
“又双叒OOM了?”“服务半夜崩了,日志全是`java.lang.OutOfMemoryError`...”“GC停顿太长,用户投诉卡顿!”如果你也常被这些问题折磨,根本症结往往在于:你对Java...
- Java JAR 启动内存参数配置指南:从基础设置到性能优化
-
在启动Java可执行JAR文件时,合理配置JVM内存参数是保障应用稳定性和性能的关键。本文将系统讲解如何通过命令行参数、环境变量等方式指定内存配置,并结合实际场景提供优化建议。一、核心内存...
- 浏览器存储"四大家族":谁才是你的数据管家?
-
当你关闭浏览器再重新打开,登录状态为何还在?购物车商品为何不会消失?这些"记忆"背后,藏着浏览器存储的"四大家族"——Cookie、localStorage、sessi...
- SOP与SIP深度解析(sop与soic)
-
SOP(标准作业程序)与SIP(标准检验程序)是确保产品质量和生产效率的两大支柱,分别聚焦于生产执行和质量验证。一、核心区别:目标与作用域维度SOP(标准作业程序)SIP(标准检验程序)定位指导“如何...
- Java 技术岗面试全景备战!从基础到架构的系统性通关攻略分享
-
Java技术岗的面试往往是一项多维度的能力检验。本文将会从核心知识点、项目经验到面试策略,为你梳理一份系统性的备战攻略!需要的同学可以私信小编【学习】一、技术基础:面试的“硬性指标”1.最重要的还是...
- C++11 新特性(c++11新特性 pdf)
-
一、核心语言革新移动语义与右值引用通过&&标识临时对象(右值),实现资源转移而非复制。例如移动构造函数将原对象资源指针转移后置空,避免深拷贝,极大优化容器操作性能。12类型推导auto:自动推导变量类...
- 2026年前每个开发者都应该学习的技能
-
优秀开发者和伟大开发者之间的差距正在快速扩大。随着AI工具的爆炸式增长、自动化工作流程和日益复杂的技术栈,开发者不能再仅仅"知道如何编码"了。在2026年及以后,您的优势不仅仅是编写代...
- 看一看,Python这四种作用域你都知道吗?
-
点赞、收藏、加关注,下次找我不迷路一、啥是作用域?先打个比方比如说,你在自己的卧室(相当于一个小空间)里放了一本书,这本书在卧室里随便你怎么看,这就是这本书在卧室这个"作用域"内...
- 抛弃立即执行函数 (IIFE),拥抱现代 JavaScript 块级作用域
-
IIFE(ImmediatelyInvokedFunctionExpression)曾是JavaScript开发中的重要工具,但随着ES6+的块级作用域特性,我们现在有了更优雅的替代...
- 2025 年是时候重新认识 Symbol 的八大特性了?
-
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!1.什么是Symbol原始类型在J...
- 函数、表达式与控制流:Rust 的核心语法构建块
-
在上一篇中我们了解了变量与类型,本篇将深入函数、表达式与控制流的使用,让你的代码更具逻辑性。一、函数定义与调用函数是组织和复用代码的基本单元。在Rust中,使用fn关键字定义函数:///计算...
- 所有权、借用与生命周期:理解 Rust 的核心机制
-
上一篇我们学习了函数、表达式和控制流,这一篇将正式进入Rust最核心、最独特的语言机制:所有权系统。一、为什么需要所有权机制?在C/C++中,内存管理依赖开发者手动操作,容易出现野指针、重复...
- Rust 语言的借用规则:构筑安全内存管理体系的核心保障机制
-
前言在系统级编程范畴内,内存安全始终是一项极具挑战性的关键议题。Rust语言凭借其独树一帜的「借用规则」(BorrowingRules),于编译阶段便有效规避了诸如空指针、野指针以及数据竞争等一系...
- 函数编写指南:参数、返回值与作用域实战详解
-
你是否在编写函数时遇到过参数传递混乱、返回值逻辑不清晰,或者变量作用域导致的奇怪bug?别担心,这篇文章将用最通俗的语言和实战案例,带你彻底搞懂函数的核心三要素:参数、返回值与作用域。一、参数:灵活...
- 服务器频繁报错?5 步教你快速排查修复!运维必看!
-
服务器突然报错、网站打不开、数据库连不上……这些问题是不是让你头大?别慌!今天教你一套「望闻问切」的排查法,90%的服务器故障都能轻松解决!一、定位错误类型:先看日志再动手1.日志是关键系统日志...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)
- JS Loop For (32)