symbol

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。Symbol 值通过Symbol函数生成。

let s = Symbol();

typeof s
// "symbol"

在symbol类型出来后,对象拥有了两种数据类型,一个是字符串,另一个就是symbol数据类型。属性名是symbol类型的可以保证不会与其他属性造成冲突

symbol函数可以接受一个字符串作为变量。表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

let s1 = Symbol('foo');
let s2 = Symbol('bar');

s1 // Symbol(foo)
s2 // Symbol(bar)

s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"

这个参数仅仅只是作为我们对symbol这数据的描述,用来区分不同的symbol数据,所以如果我们对Symbol函数传入了相同的参数,即使他们的描述相等,但是Symbol的返回值是不同的。

let s1 = Symbol('foo');
let s2 = Symbol('foo');

s1 === s2 // false

Symbol 值不能与其他类型的值进行运算,会报错。但是,Symbol 值可以显式转为字符串,也可以转为布尔值,但是不能转为数值。即Symbol是可以调用toString方法和作为参数传入Boolean方法的

但是无法作为参数传入Number函数转为数值的。

我们在创建Symbol的时候会给他添加一个描述,ES2019提供了一个实例方法,会直接返回Symbol的描述

const sym = Symbol('foo');

sym.description // "foo"

因为Symbol作为独一无二的值,他往往会被用在对象的属性中,防止对象的属性被覆盖或者改写。值得注意的是,在Symbol出现之前,我们在对象中所指定的属性名都是以字符串的形式存在,也仅仅只能以字符串形式存在。ES6出现了新的数据结构map,它与对象相似,但是却可以以其他的数据类型作为属性名存在。同样的ES6出现Symbol类型之后,传统的对象属性名称不仅可以用字符串表示,也可以用Symbol表示。

但是请注意,Symbol 值作为对象属性名时,不能用点运算符。对象读取属性常见的方法有两种

obj.key
obj[key]

使用Symbol类型作为对象属性的读取

let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法 let a = { [mySymbol]: 'Hello!' };
// 第三种写法 let a = {}; Object.defineProperty(a, mySymbol, { value: 'Hello!' }); // 以上写法都得到同样结果 a[mySymbol] // "Hello!"

点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个 Symbol 值。代码如下

const mySymbol = Symbol();
const a = {};

a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"

属性名的遍历

Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用Symbol 作属性名的 。

const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols
// [Symbol(a), Symbol(b)]

另一个新的 API,Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

let obj = {
  [Symbol('my_key')]: 1,
  enum: 2,
  nonEnum: 3
};

Reflect.ownKeys(obj)
//  ["enum", "nonEnum", Symbol(my_key)]

因为symbol这一个不能被轻易遍历到的特性,我们可以将一些非私有但是仅仅用在对象内部的属性用symbol保存在对象中

使用同一个symbol

有时,我们希望重新使用同一个 Symbol 值,Symbol.for()方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true

s1s2都是 Symbol 值,但是它们都是由同样参数的Symbol.for方法生成的,所以实际上是同一个值。

Symbol.for()Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境*搜索,后者不会。就像diff算法一样,Symbol.for()调用返回新的symbol类型的时候会去寻找是否有相同key(也就是symbol的描述)的symbol存在,若不存在则创建新的,若存在则不创建新的symbol。而Symbo()则是调用即创建新的数据类型。我们将symbol.for()这一特性称为symbol的登记机制。

Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key

let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

注意,Symbol.for()为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。

实例:模块的 Singleton 模式(参考阮一峰教程)

Singleton 模式指的是调用一个类,任何时候返回的都是同一个实例。

对于 Node 来说,模块文件可以看成是一个类。怎么保证每次执行这个模块文件,返回的都是同一个实例呢?

很容易想到,可以把实例放到顶层对象global

以下是mod.js的代码,以供暴露实例以供其他模块使用

// mod.js
function A() {
  this.foo = 'hello';
}

if (!global._foo) {
  global._foo = new A();
}

module.exports = global._foo;

加载上面的mod.js

const a = require('./mod.js');
console.log(a.foo);//hello

上面代码中,变量a任何时候加载的都是A的同一个实例。

但是,这里有一个问题,全局变量global._foo是可写的,任何文件都可以修改。

global._foo = { foo: 'world' };

const a = require('./mod.js');
console.log(a.foo);

上面的代码,会使得加载mod.js的脚本都失真。

为了防止这种情况出现,我们就可以使用 Symbol.for。

// mod.js
const FOO_KEY = Symbol.for('foo');

function A() {
  this.foo = 'hello';
}

if (!global[FOO_KEY]) {
  global[FOO_KEY] = new A();
}

module.exports = global[FOO_KEY];

上面代码中,可以保证global[FOO_KEY]不会被无意间覆盖,但还是可以被改写。

// mod.js
const FOO_KEY = Symbol('foo');

// 后面代码相同 ……

 

global[Symbol.for('foo')] = { foo: 'world' };

const a = require('./mod.js');

如果键名使用Symbol方法生成,那么外部将无法引用这个值,当然也就无法改写。

// mod.js
const FOO_KEY = Symbol('foo');

// 后面代码相同 ……

上面代码将导致其他脚本都无法引用FOO_KEY。但这样也有一个问题,就是如果多次执行这个脚本,每次得到的FOO_KEY都是不一样的。虽然 Node 会将脚本的执行结果缓存,一般情况下,不会多次执行同一个脚本,但是用户可以手动清除缓存,所以也不是绝对可靠。

第一次认真的看一篇文档,便将重要的东西挑出来简述,symbol是以前都没接触过的类型,也是没使用过的。我感觉symbol的出现,让JS好像变得越来越严谨希望有朝一日JS可以变得越来越强大。

宣传下自己的网站mishi-blog.com

尚在襁褓中的一个网站。

上一篇:原生js之Symbol——读Javascript高级程序设计①


下一篇:ES6学习---初识symbol