2021.04.28(属性名的遍历、Symbol.for(),Symbol.keyFor()、实例:模块的 Singleton 模式)
属性名的遍历
Symbol 作为属性名,遍历对象的时候,该属性不会出现在 for...in 、 for...of 循环中,也不
会被 Object.keys() 、 Object.getOwnPropertyNames() 、 JSON.stringify() 返回。
但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols() 方法,可以获取指定对象的
所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
1. const obj = {}; 2. let a = Symbol('a'); 3. let b = Symbol('b'); 4. 5. obj[a] = 'Hello'; 6. obj[b] = 'World'; 7. 8. const objectSymbols = Object.getOwnPropertySymbols(obj); 9. 10. objectSymbols 11. // [Symbol(a), Symbol(b)]
上面代码是 Object.getOwnPropertySymbols() 方法的示例,可以获取所有 Symbol 属性名。
下面是另一个例子, Object.getOwnPropertySymbols() 方法与 for...in 循
环、 Object.getOwnPropertyNames 方法进行对比的例子。
1. const obj = {}; 2. const foo = Symbol('foo'); 3. 4. obj[foo] = 'bar'; 5. 6. for (let i in obj) { 7. console.log(i); // 无输出 8. } 9. 10. Object.getOwnPropertyNames(obj) // [] 11. Object.getOwnPropertySymbols(obj) // [Symbol(foo)]
上面代码中,使用 for...in 循环和 Object.getOwnPropertyNames() 方法都得不到 Symbol 键
名,需要使用 Object.getOwnPropertySymbols() 方法。
另一个新的 API, Reflect.ownKeys() 方法可以返回所有类型的键名,包括常规键名和 Symbol键名。
1. let obj = { 2. [Symbol('my_key')]: 1, 3. enum: 2, 4. nonEnum: 3 5. }; 6. 7. Reflect.ownKeys(obj) 8. // ["enum", "nonEnum", Symbol(my_key)]
由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非
私有的、但又希望只用于内部的方法。
Symbol.for(),Symbol.keyFor()
有时,我们希望重新使用同一个 Symbol 值, Symbol.for() 方法可以做到这一点。它接受一个字
符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol
值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。
1. let s1 = Symbol.for('foo'); 2. let s2 = Symbol.for('foo'); 3. 4. s1 === s2 // true
上面代码中, s1 和 s2 都是 Symbol 值,但是它们都是由同样参数的 Symbol.for 方法生成
的,所以实际上是同一个值。
Symbol.for() 与 Symbol() 这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记
在全局环境中供搜索,后者不会。 Symbol.for() 不会每次调用就返回一个新的 Symbol 类型的
值,而是会先检查给定的 key 是否已经存在,如果不存在才会新建一个值。比如,如果你调
用 Symbol.for("cat") 30 次,每次都会返回同一个 Symbol 值,但是调用 Symbol("cat") 30
次,会返回 30 个不同的 Symbol 值。
1. Symbol.for("bar") === Symbol.for("bar") 2. // true 3. 4. Symbol("bar") === Symbol("bar") 5. // false
上面代码中,由于 Symbol() 写法没有登记机制,所以每次调用都会返回一个不同的值。
Symbol.keyFor() 方法返回一个已登记的 Symbol 类型值的 key 。
1. let s1 = Symbol.for("foo"); 2. Symbol.keyFor(s1) // "foo" 3. 4. let s2 = Symbol("foo"); 5. Symbol.keyFor(s2) // undefined
上面代码中,变量 s2 属于未登记的 Symbol 值,所以返回 undefined 。
注意, Symbol.for() 为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。
1. function foo() { 2. return Symbol.for('bar'); 3. } 4. 5. const x = foo(); 6. const y = Symbol.for('bar'); 7. console.log(x === y); // true
上面代码中, Symbol.for('bar') 是函数内部运行的,但是生成的 Symbol 值是登记在全局环境
的。所以,第二次运行 Symbol.for('bar') 可以取到这个 Symbol 值。
Symbol.for() 的这个全局登记特性,可以用在不同的 iframe 或 service worker 中取到同
一个值。
1. iframe = document.createElement('iframe'); 2. iframe.src = String(window.location); 3. document.body.appendChild(iframe); 4. 5. iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo') 6. // true
上面代码中,iframe 窗口生成的 Symbol 值,可以在主页面得到。
实例:模块的 Singleton 模式
Singleton 模式指的是调用一个类,任何时候返回的都是同一个实例。
很容易想到,可以把实例放到顶层对象 global 。
1. // mod.js 2. function A() { 3. this.foo = 'hello'; 4. } 5. 6. if (!global._foo) { 7. global._foo = new A(); 8. } 9. 10. module.exports = global._foo;
然后,加载上面的 mod.js 。
1. const a = require('./mod.js');
2. console.log(a.foo);
上面代码中,变量 a 任何时候加载的都是 A 的同一个实例。
但是,这里有一个问题,全局变量 global._foo 是可写的,任何文件都可以修改。
1. global._foo = { foo: 'world' }; 2. 3. const a = require('./mod.js'); 4. console.log(a.foo);
上面的代码,会使得加载 mod.js 的脚本都失真。
为了防止这种情况出现,我们就可以使用 Symbol。
1. // mod.js 2. const FOO_KEY = Symbol.for('foo'); 3. 4. function A() { 5. this.foo = 'hello'; 6. } 7. 8. if (!global[FOO_KEY]) { 9. global[FOO_KEY] = new A(); 10. } 11. 12. module.exports = global[FOO_KEY];
上面代码中,可以保证 global[FOO_KEY] 不会被无意间覆盖,但还是可以被改写。
1. global[Symbol.for('foo')] = { foo: 'world' }; 2. 3. const a = require('./mod.js');
如果键名使用 Symbol 方法生成,那么外部将无法引用这个值,当然也就无法改写。
1. // mod.js 2. const FOO_KEY = Symbol('foo'); 3. 4. // 后面代码相同 ……
上面代码将导致其他脚本都无法引用 FOO_KEY 。但这样也有一个问题,就是如果多次执行这个脚本,
每次得到的 FOO_KEY 都是不一样的。虽然 Node 会将脚本的执行结果缓存,一般情况下,不会多次
执行同一个脚本,但是用户可以手动清除缓存,所以也不是绝对可靠。
2021-04-29 17:26:00