重学 Symbol
之前在写基础类型的笔记时暂时性的先跳过了 symbol,现在也有了一些项目的使用经验后,觉得还是需要重新回滚并且学习一下,温故而知新。
首先依旧回顾一下 symbol 的特点:
-
是原始值
-
唯一
-
不可变
-
可以提供私有属性,模仿其他语言中的 private
-
不可以使用
new
关键词这是因为 JS 在实现的时候刻意避免了使用
new
关键词去调用原始值包装类型 -
Symbol 作为对象的 key 时不可被枚举
Symbol 的基本用法
虽然 Symbol 可以模拟私有类型,不过其刚开始被创造出来的意义是为了创建唯一属性,而非私有属性。
以下面代码为例:
const uid = Symbol('uid');
export const item = {
[uid]: 'p1',
};
import { item } from './obj.js';
const uid = Symbol('uid');
item[uid] = 'p2';
console.log(item);
上面两段代码分别属于不同的 module,第一段中的 symbol 也没有被 expor。换言之,在其他地方使用的 item
这个对象的代码是无法获取到生成时创建的 symbol。
第二段代码用同样的字符串新创建了一个 Symbol,如果是其他属性的话,那么这时候 item
中对应的值可能就被重写了,不过 Symbol 不是:
可以看到,尽管两个 Symbol 中的字符串是一样的,但是 JS 会创建一个完全不同的 Symbol:
const uid2 = Symbol('uid');
console.log(uid === uid2);
搭配上 Symbol 不可被枚举的特性,就可以模拟成一个私有属性:
const uid = Symbol('uid');
item[uid] = 'p2';
console.log(item);
item.key = 'value';
console.log(Object.keys(item)); // 只有 key
项目中的使用案例
也因此,在我们的项目中,也会使用过 Symbol 去创建一些 ref 值(不需要被传到 API 中去的值),如:
class Example {
constructor() {
const privateKey = Symbol('private');
this[privateKey] = 'private id';
this.id = 0;
this.value = 'value';
}
messageObj = () => {
console.log(Object.keys(this));
const messageObj = {};
for (const key of Object.keys(this)) {
if (!(this[key] instanceof Object)) {
messageObj[key] = this[key];
}
}
return messageObj;
};
}
const example = new Example();
console.log(example.messageObj());
Symbol 的常用方法和属性
方法
我们的项目中用的是 export 的方法,不过怎么说呢……之前的开发做 ios 的,对 JS 不是很熟,正确的全局方法应该使用 Symbol.for()
去实现:
const sym1 = Symbol.for('shared symbol');
const sym2 = Symbol.for('shared symbol');
console.log(sym1 === sym2);
// 对比
const sym3 = Symbol('shared symbol');
console.log(sym3 === sym1);
如果要查找 symbol 是否被挂在到了全局分享上,就可以使用 keyFor()
进行查找:
console.log(Symbol.keyFor(sym3));
console.log(Symbol.keyFor(sym1));
鉴于每次调用 Symbol.for
,JS 都会进行查找,如果没有注册当前输入的值,就会自动创建一个新的值这一操作,这个方法的使用范围还是比较有限的。
另外一个可能用得到的方法是 toString()
属性
属性又名 常用内置符号(Well-known Symbols),是 JS 内部暴露出来的,一些属于对象的内置属性。一些非常常用的功能(尤其是数组、set、map)都是 JS 内部实现的,不过也有一些情况下需要我们手动重写。
如 instanceof
就会隐式调用 [Symbol.hasInstance]
,而 [Symbol.isConcatSpreadable]
则是一个 boolean,这个值决定当 Array.prototype.concat()
中存在当前值是,它是会将当前值扁平化,还是保存为一个 array-like object 推进去。
目前实现的属性有下面这些:
对于日常开发来说,最重要的的是 iterator
,其他的属性相对而言用的比较少。
另一个比较好用的是 toStringTag
,对于开发和 debug 比较有帮助。这里不会列举所有的属性,毕竟这部分如果真的要用,参考 MDN 文档会更 up-to-date。
Symbol.toStringTag
主要使用方法如下:
class Example {}
const ex1 = new Example();
console.log(ex1.toString());
class Example2 {
get [Symbol.toStringTag]() {
return 'Example2';
}
}
const ex2 = new Example2();
console.log(ex2.toString());
function Example3() {}
Object.defineProperty(Example3.prototype, Symbol.toStringTag, {
get: function () {
return 'Example3';
},
});
const ex3 = new Example3();
console.log(ex3.toString());
function Example4() {}
Example4.prototype[Symbol.toStringTag] = 'Example4';
const ex4 = new Example4();
console.log(ex4.toString());
这个相对而言用于开发、debug 还是有一定效果的:
Symbol.iterator
Symbol.iterator
实现的方法是对象中的迭代器,所有可迭代对象——在 JS 中,这代表可以使用 for...of
, [...arr]
或是 const [a, b, c] = arr
这种语法——在底层都会查找实现的迭代器。
这里继续举一个例子,如:
class Obj {
constructor() {
this.idx = 0;
this.arr = [1, 2, 3];
}
*[Symbol.iterator]() {
while (this.idx < this.arr.length) {
yield this.arr[this.idx++];
}
}
}
const obj = new Obj();
for (const val of obj) {
console.log(val);
}
const [a, b, c] = new Obj();
a;
b;
c;
Symbol.asyncIterator
本来写了个使用案例,后来发现那个更加偏向于 generator 的使用方式,没设么必要迭代实现,这里就丢一个 MDN 的案例:
const delayedResponses = {
delays: [500, 1300, 3500],
wait(delay) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
},
async *[Symbol.asyncIterator]() {
for (const delay of this.delays) {
await this.wait(delay);
yield `Delayed response for ${delay} milliseconds`;
}
},
};
(async () => {
for await (const response of delayedResponses) {
console.log(response);
}
})();
// Expected output: "Delayed response for 500 milliseconds"
// Expected output: "Delayed response for 1300 milliseconds"
// Expected output: "Delayed response for 3500 milliseconds"
这里主要还是搭配了一个异步的实现,可以在 for...of
循环中使用 await
去等结果。