Day371:使用 Symbol 函数都有哪些要注意的点?
Genzhen opened this issue · 0 comments
每日一题会在下午四点在交流群集中讨论,五点小程序中更新答案
欢迎大家在下方发表自己的优质见解二维码加载失败可点击 小程序二维码
扫描下方二维码,收藏关注,及时获取答案以及详细解析,同时可解锁800+道前端面试题。
Symbol
ES6 中引入了一种新的基础数据类型:Symbol,这是一种新的基础数据类型(primitive type)。
它的功能类似于一种标识唯一性的 ID。通常情况下,我们可以通过调用 Symbol()函数来创建一个 Symbol 实例:
let s1 = Symbol();
或者,你也可以在调用 Symbol()函数时传入一个可选的字符串参数,相当于给你创建的 Symbol 实例一个描述信息:
let s2 = Symbol("another symbol");
如果用当下比较流行的 TypeScript 的方式来描述这个 Symbol()函数的话,可以表示成:
/**
* @param {any} description 描述信息。可以是任何可以被转型成字符串的值,如:字符串、数字、对象、数组等
*/
function Symbol(description:any):symbol;
由于 Symbol 是一种基础数据类型,所以当我们使用 typeof 去检查它的类型的时候,它会返回一个属于自己的类型 symbol,而不是什么 string、object 之类的:
typeof s1; // 'symbol'
另外,我们需要重点记住的一点是:每个 Symbol 实例都是唯一的。因此,当你比较两个 Symbol 实例的时候,将总会返回 false:
let s1 = Symbol();
let s2 = Symbol("another symbol");
let s3 = Symbol("another symbol");
s1 === s2; //false
s2 === s3; //false
1.1 一些应用场景
- 场景 1:使用 Symbol 来作为对象属性名(key)
在这之前,我们通常定义或访问对象的属性时都是使用字符串,比如下面的代码:
let obj = {
abc: 123,
hello: "world",
};
obj["abc"]; // 123
obj["hello"]; // 'world'
而现在,Symbol 可同样用于对象属性的定义和访问:
const PROP_NAME = Symbol();
const PROP_AGE = Symbol();
let obj = {
[PROP_NAME]: "yd",
};
obj[PROP_AGE] = 6;
obj[PROP_NAME]; // yd
obj[PROP_AGE]; // 6
随之而来的是另一个非常值得注意的问题:就是当使用了 Symbol 作为对象的属性 key 后,在对该对象进行 key 的枚举时,会有什么不同?在实际应用中,我们经常会需要使用Object.keys()
或者for...in
来枚举对象的属性名,那在这方面,Symbol 类型的 key 表现的会有什么不同之处呢?来看以下示例代码:
let obj = {
[Symbol("name")]: "yd",
age: 6,
title: "symbol",
};
Object.keys(obj); // ["age","title"]
for (let p in obj) {
console.log(p); // 分别输出 “age” 和 “title”
}
Object.getOwnPropertyNames(obj); // ["age","title"]
由上代码可知,Symbol 类型的 key 是不能通过Object.keys()
或者for...in
来枚举的,它未被包含在对象自身的属性名集合(property names)之中。所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用 Symbol 来定义。
也正因为这样一个特性,当使用 JSON.stringify()将对象转换成 JSON 字符串的时候,Symbol 属性也会被排除在输出内容之外:
JSON.stringify(obj); // {"age":6,"title":"symbol"}
我们可以利用这一特点来更好的设计我们的数据对象,让“对内操作”和“对外选择性输出”变得更加优雅。
然而,这样的话,我们就没办法获取以 Symbol 方式定义的对象属性了么?非也。还是会有一些专门针对 Symbol 的 API,比如:
// 使用Object的API
Object.getOwnPropertySymbol(obj); // [Symbol(name)]
// 使用新增的的反射API
Reflect.ownKeys(obj); // [Symbol(name),"age","title"]
- 场景 2:使用 Symbol 代替常量
看一下下面的代码是不是非常熟悉
const TYPE_AUDIO = "AUDIO";
const TYPE_VIDEO = "VIDEO";
const TYPE_IMAGE = "IMAGE";
function handleFileResource(resource) {
switch (resource.type) {
case TYPE_AUDIO:
playAudio(resource);
break;
case TYPE_VIDEO:
playVideo(resource);
break;
case TYPE_IMAGE:
previewImage(resource);
break;
default:
throw new Error("Unknown type of resource");
}
}
如上面的代码中那样,我们经常定义一组常量来代表一种业务逻辑下的几个不同类型,我们通常希望这几个常量之间是唯一的关系,为了保证这一点,我们需要为常量赋一个唯一的值(比如这里的'AUDIO'、'VIDEO'、 'IMAGE'),常量少的时候还算好,但是常量一多,你可能还得花点脑子好好为他们取个好点的名字。
现在有了 Symbol,我们大可不必这么麻烦了:
const TYPE_AUDIO = Symbol();
const TYPE_VIDEO = Symbol();
const TYPE_IMAGE = Symbol();
这样定义,直接就保证了三个常量的值是唯一的了!是不是挺方便的呢。
- 场景 3:使用 Symbol 定义类的私有属性/方法
我们知道在 JavaScript 中,是没有如 Java 等面向对象语言的访问控制关键字 private 的,类上所有定义的属性或方法都是可公开访问的。因此这对我们进行 API 的设计时造成了一些困扰。
而有了 Symbol 以及模块化机制,类的私有属性和方法才变成可能。例如:
在文件 a.js 中
const PASSWORD = Symbol();
class Login {
constructor(userName, password) {
this.userName = userName;
this[PASSWORD] = password;
}
checkPassword(pwd) {
return this[PASSWORD] === pwd;
}
}
export default Login;
在文件 b.js 中
import Login from "./a";
const login = new Login("admin", "123456");
login.checkPassword("admin"); // true
login.PASSWORD; // oh!no!
login[PASSWORD]; // oh!no!
login["PASSWORD"]; // oh!no!
由于 Symbol 常量 PASSWORD 被定义在 a.js 所在的模块中,外面的模块获取不到这个 Symbol,也不可能再创建一个一模一样的 Symbol 出来(因为 Symbol 是唯一的),因此这个 PASSWORD 的 Symbol 只能被限制在 a.js 内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的,达到了一个私有化的效果。
1.2 注册和获取全局 Symbol
通常情况下,我们在一个浏览器窗口中(window),使用 Symbol()函数来定义 Symbol 实例就足够了。
但是,如果你的应用涉及到多个 window(最典型的就是页面中使用了<iframe>
),并需要这些 window 中使用的某些 Symbol 是同一个,那就不能使用Symbol()
函数了,因为用它在不同 window 中创建的 Symbol 实例总是唯一的,而我们需要的是在所有这些 window 环境下保持一个共享的 Symbol。这种情况下,我们就需要使用另一个 API 来创建或获取 Symbol,那就是Symbol.for()
,它可以注册或获取一个 window 间全局的 Symbol 实例:
let gs1 = Symbol.for("global_symbol_1"); // 注册一个全局Symbol
let gs2 = Symbol.for("global_symbol_2"); // 获取全局Symbol
gs1 === gs2; // true
这样一个 Symbol 不光在单个 window 中是唯一的,在多个相关 window 间也是唯一的了。
1.3 一些需要特别注意的点
- Symbol 函数前不能使用 new 命令,否则会报错。
- Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
- Symbol 作为属性名,该属性不会出现在
for...in
、for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。 Object.getOwnPropertySymbols
方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。Symbol.for
接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。Symbol.keyFor
方法返回一个已登记的 Symbol 类型值的 key。