ES6-Symbol

ES6 中的 Symbol: 独特的数据类型与强大应用

引言

在 JavaScript 的发展长河中,ES6(ECMAScript 2015)无疑是一座重要的里程碑,带来了诸多令人瞩目的新特性。其中,Symbol 类型的引入,为 JavaScript 开发者们开启了一扇全新的大门,它为解决传统开发中遇到的一系列棘手问题提供了创新的方案。Symbol 作为一种独一无二的基本数据类型,以其独特的性质和广泛的应用场景,极大地提升了代码的健壮性、可维护性以及安全性。在这篇博客中,我们将全方位、深层次地探索 ES6 中的 Symbol,从其基础概念到复杂的高级应用,力求为大家呈现一个完整而清晰的 Symbol 知识体系。

Symbol 的作用

解决属性名冲突难题

在传统的 JavaScript 开发过程中,尤其是在多人协作或者使用第三方库的复杂项目里,对象属性名冲突是一个令人头疼的常见问题。不同的模块或者代码片段可能会不经意地使用相同的属性名,这往往会导致意想不到的错误和难以调试的问题。Symbol 的出现,为这个难题提供了完美的解决方案。由于每个 Symbol 都是独一无二的,无论在何处创建,它都不会与其他 Symbol 重复。这就意味着,当我们使用 Symbol 作为对象的属性名时,能够确保属性名的唯一性,从而有效避免属性名冲突。例如,在一个大型项目中,多个模块可能都需要向某个公共对象添加特定的属性,使用 Symbol 作为属性名,各个模块之间就不会因为属性名相同而产生冲突,极大地提高了代码的稳定性。

实现对象的有效封装

封装是面向对象编程中的重要概念,它能够将对象的内部状态和实现细节隐藏起来,只对外提供必要的接口。在 JavaScript 中,利用 Symbol 可以实现对象的内部属性封装。由于 Symbol 属性不能通过常规的对象遍历方法(如for...in、Object.keys())获取,我们可以创建一些不希望被外部随意访问或修改的对象内部属性。这些内部属性只有在对象内部的方法中才能被访问和操作,从而增强了对象的封装性和安全性。比如,我们可以创建一个包含敏感信息的对象,将这些敏感信息的属性名设置为 Symbol 类型,这样外部代码就无法轻易地获取或修改这些敏感信息,保护了对象的内部状态。

拓展对象功能与增强代码可读性

Symbol 还为拓展对象的功能提供了一种优雅的方式。在不修改现有对象原型的前提下,我们能够使用 Symbol 为对象添加额外的功能。这在处理第三方库的对象时尤为有用,我们可以在不影响其原有结构和其他代码使用的情况下,为对象增添新的行为。同时,使用 Symbol 作为事件类型或者模块间的唯一标识等,可以使代码更加清晰、易读。例如,在事件驱动编程中,使用 Symbol 作为事件类型,能够避免与其他可能定义的事件类型发生冲突,并且从代码中可以直观地看出事件的唯一性,增强了代码的可读性和可维护性。

Symbol () 方法

创建唯一的 Symbol 值

创建一个 Symbol 非常简单,通过调用Symbol()函数即可。如下所示:

javascript 复制代码
let sym = Symbol();

console.log(typeof sym); // "symbol"

这里,Symbol()函数返回一个全新的、唯一的 Symbol 值。typeof 操作符用于检测数据类型,当应用于通过Symbol()创建的变量时,会返回"symbol",表明它是一种新的数据类型

注意:Symbol是一种原始数据类型,并不是一种构造函数,不能new

使用描述参数增加可读性

Symbol()函数可以接受一个可选的字符串参数,这个参数作为对该 Symbol 的描述,主要用于在调试或打印输出时进行区分,方便开发者理解该 Symbol 的用途。例如:

javascript 复制代码
let sym1 = Symbol('description');

let sym2 = Symbol('description');

console.log(sym1 === sym2); // false

尽管sym1和sym2的描述相同,但它们仍然是两个完全不同的 Symbol 值。这个描述参数并不会影响 Symbol 的唯一性,只是为了在开发过程中提供更多的上下文信息,帮助开发者更好地理解代码的意图。

注意:Symbol的描述只是对于这个Symbol值的描述,并不是它的值,真正的值存在内存里面,无法被看到

Symbol 与其它数据类型的转换关系和运算关系

与字符串的转换

Symbol 不能直接转换为字符串,这是为了保持其唯一性和特殊性。如果尝试将 Symbol 与字符串进行连接操作,会导致类型错误。例如:

javascript 复制代码
let sym = Symbol('test');

let result = sym + 'string'; // TypeError: can't convert symbol to string

然而,如果确实需要将 Symbol 转换为字符串形式进行显示,可以使用Symbol.prototype.toString()方法。该方法会返回一个包含 Symbol 描述的字符串。例如:

javascript 复制代码
let sym = Symbol('test');

let str = sym.toString();

console.log(str); // "Symbol(test)"

与数字的转换

Symbol 与数字之间也不存在直接的转换关系。不能将 Symbol 当作数字进行数学运算,如加法、减法等。例如:

javascript 复制代码
let sym = Symbol('test');

let num = 5 + sym; // TypeError: can't convert symbol to number

这是因为 Symbol 表示的是独一无二的值,其语义与数字完全不同,不适合进行常规的数学运算。

与布尔值的关系

在 JavaScript 中,Symbol 类型的值在布尔运算中被视为真值。也就是说,当 Symbol 类型的值出现在需要布尔值的上下文中(如if语句、while循环条件等)时,会被当作true处理。例如:

javascript 复制代码
let sym = Symbol('test');

if (sym) {

console.log('Symbol is truthy');

}

上述代码会输出Symbol is truthy,表明 Symbol 在布尔运算中表现为真值。

如何在对象里面使用 Symbol 类型的变量作为属性名

避免属性名冲突的应用

在对象中使用 Symbol 作为属性名是其最常见的应用之一,主要用于避免属性名冲突。例如:

javascript 复制代码
let obj = {};

let sym1 = Symbol('prop1');

let sym2 = Symbol('prop2');

obj[sym1] = 'value1';

obj[sym2] = 'value2';

console.log(obj[sym1]); // "value1"

console.log(obj[sym2]); // "value2"

在这个例子中,我们创建了一个空对象obj,然后使用两个不同的 Symbol 作为属性名,分别为其赋值。由于 Symbol 的唯 一性,即使有其他代码也尝试向obj对象添加名为prop1或prop2的属性(假设使用的也是 Symbol 类型),也不会与现有的属性产生冲突。这样,在复杂的项目中,不同模块对同一对象进行属性添加时,使用 Symbol 可以确保各个属性的独立性和唯一性。

实现对象内部属性的封装

通过将 Symbol 类型的变量作为对象的属性名,还可以实现对象内部属性的封装。例如:

javascript 复制代码
let myObject = (function () {

let internalProp = Symbol('internal');

let obj = {

setValue(value) {

this[internalProp] = value;

},

getValue() {

return this[internalProp];

}

};

return obj;

})();

myObject.setValue(42);

console.log(myObject.getValue()); // 42

// 无法直接访问内部属性

console.log(myObject[Symbol('internal')]); // undefined

在上述代码中,我们在一个立即执行函数表达式内部创建了一个 Symbol 类型的变量internalProp,并将其作为obj对象的内部属性名。obj对象提供了setValue和getValue方法来间接操作这个内部属性。由于外部代码无法直接获取到internalProp这个 Symbol,所以不能直接访问或修改该内部属性,从而实现了对象内部属性的封装,提高了对象的安全性和可维护性。

Symbol 的几个常用属性和方法

description 属性

每个 Symbol 实例都有一个只读的description属性,它返回创建 Symbol 时传入的描述字符串。如果创建 Symbol 时没有提供描述字符串,description属性的值为undefined。例如:

javascript 复制代码
let sym1 = Symbol('test description');

console.log(sym1.description); // "test description"

let sym2 = Symbol();

console.log(sym2.description); // undefined

description属性主要用于在调试和日志记录中,帮助开发者快速了解 Symbol 的用途和含义。通过查看description属性的值,开发者可以更清晰地理解代码中各个 Symbol 的作用,尤其是在复杂的项目中,众多的 Symbol 可能会让代码阅读变得困难,description属性能够提供关键的上下文信息。(描述只是描述!!)

Object.getOwnPropertySymbols () 方法

由于正常的遍历方法无法获取Symbol数据类型的属性,故有如下方法获取

Object.getOwnPropertySymbols()方法用于获取一个对象的所有 Symbol 类型的自有属性(即直接在该对象上定义的属性,而不是从原型链继承的属性)。该方法返回一个包含所有 Symbol 属性的数组。例如:

javascript 复制代码
let obj = {};

let sym1 = Symbol('prop1');

let sym2 = Symbol('prop2');

obj[sym1] = 'value1';

obj[sym2] = 'value2';

let symbols = Object.getOwnPropertySymbols(obj);

console.log(symbols); // [Symbol(prop1), Symbol(prop2)]

在这个例子中,我们首先创建了一个对象obj,并使用两个 Symbol 作为属性名向其添加了属性。然后,通过Object.getOwnPropertySymbols()方法获取obj对象的所有 Symbol 类型的自有属性,返回的数组包含了我们之前定义的sym1和sym2。这个方法在需要遍历对象的所有 Symbol 属性时非常有用,比如在进行对象的深度克隆或者对对象的所有属性(包括 Symbol 属性)进行统一处理时。

Reflect.ownKeys () 方法

Reflect.ownKeys()方法返回一个包含对象自身所有属性键(包括字符串类型和 Symbol 类型)的数组。与Object.getOwnPropertyNames()方法不同,Object.getOwnPropertyNames()方法只能获取对象的字符串类型的自有属性键,而Reflect.ownKeys()方法可以获取所有类型的自有属性键。例如:

javascript 复制代码
let obj = {};

let sym1 = Symbol('prop1');

let sym2 = Symbol('prop2');

obj[sym1] = 'value1';

obj[sym2] = 'value2';

obj.stringProp = 'string value';

let keys = Reflect.ownKeys(obj);

console.log(keys); // [Symbol(prop1), Symbol(prop2), "stringProp"]

在上述代码中,我们创建了一个对象obj,包含两个 Symbol 类型的属性和一个字符串类型的属性。通过Reflect.ownKeys()方法,我们获取到了obj对象的所有自有属性键,包括 Symbol 类型和字符串类型

Symbol.for () 和 Symbol.keyFor () 方法

Symbol.for () 方法

Symbol.for() 方法用于在全局 Symbol 注册表 中搜索具有指定键的 Symbol。如果找到了匹配的 Symbol,则返回该 Symbol;如果没有找到,则在全局 Symbol 注册表中创建一个新的 Symbol,并返回它。与直接使用Symbol()函数创建 Symbol 不同,++Symbol.for()创建的 Symbol 是全局共享的,只要键相同,无论在何处调用Symbol.for(),返回的都是同一个 Symbol++。例如:

javascript 复制代码
let sym1 = Symbol.for('globalSymbol');

let sym2 = Symbol.for('globalSymbol');

console.log(sym1 === sym2); // true

在这个例子中,我们两次调用Symbol.for('globalSymbol'),尽管是在不同的代码位置,但由于使用了相同的键'globalSymbol',所以sym1和sym2指向的是同一个 Symbol。这种全局共享的特性使得 Symbol 在不同模块或者不同作用域之间能够实现统一的标识,在大型项目中,当需要在多个地方使用相同的唯一标识时,Symbol.for()非常有用。

此时描述就不再只是起描述的作用了,还起到值的作用

Symbol.keyFor () 方法

++Symbol.keyFor()方法用于返回一个已登记的 Symbol 在全局 Symbol 注册表中的键++。它接受一个 Symbol 作为参数,如果该 Symbol 是通过Symbol.for()方法在全局 Symbol 注册表中创建的,则返回其对应的键;如果该 Symbol 不是通过Symbol.for()方法创建的(例如直接使用Symbol()函数创建的),则返回undefined。例如:

javascript 复制代码
let sym1 = Symbol.for('globalSymbol');

let key = Symbol.keyFor(sym1);

console.log(key); // "globalSymbol"

let sym2 = Symbol('localSymbol');

let key2 = Symbol.keyFor(sym2);

console.log(key2); // undefined

在上述代码中,对于通过Symbol.for()创建的sym1,Symbol.keyFor()方法返回了其在全局 Symbol 注册表中的键'globalSymbol'。而对于直接使用Symbol()创建的sym2,由于它不在全局 Symbol 注册表中,所以Symbol.keyFor()方法返回undefined。Symbol.keyFor()方法在需要根据 Symbol 获取其对应的全局键时非常有用,比如在进行全局 Symbol 的管理或者在不同模块之间进行基于键的 Symbol 查找时。

相关推荐
曹牧15 分钟前
JSON 数组的正确使用方式
java·服务器·前端
石头猫灯19 分钟前
DNS 服务器配置实验
运维·服务器
认真的薛薛33 分钟前
Docker网络模式
linux·运维·数据库·面试·github
民乐团扒谱机1 小时前
【读论文】Frequency Comb Based Optical Time Transfer基于光频梳的光时间传递
运维·服务器
UP_Continue1 小时前
Linux--日志的模拟实现
linux·运维·服务器
xlp666hub1 小时前
深度剖析Linux Input子系统(1):宏观架构与核心原理
linux
东北甜妹1 小时前
playbook
linux·服务器·网络
我爱学习好爱好爱1 小时前
Ansible 入门:ad-hoc 临时命令与常用模块
linux·服务器·ansible
s09071361 小时前
【Zynq 进阶一】深度解析 PetaLinux 存储布局:NAND Flash 分区与 DDR 内存分配全攻略
linux·fpga开发·设备树·zynq·nand flash启动·flash分区
lwx9148521 小时前
Linux-sftp命令详解
linux·运维·服务器