javascript Symbols

Symbols 是最新的js原始类型,它带来了一些好处,尤其上司当做对象属性时特别有用。但是它提供的什么是String做不到的呢?

在我们开始深入了解Symbols之前让我们先来看看有哪些细节是开发者们不太容易注意到的。

背景

在js中有两种数据类型,原始类型和引用数据类型(原文这里用的objects); 原始类型包括numbers(包括整数,浮点数,Infinity,NaN),布尔值,字符串,undefined,还有null(虽然 typeof null === 'object'null仍然是一个原始类型)

解百纳类型的值是不可变的。他们不能被改变。一个变量被赋值了一个原始类型仍然可以被重新赋值。举例: let x = 1; x++;.这时候就是把变量x重新赋值了,但是并没有改变1的原始数值。

有些其它语言,比如C,有按引用传递和按值传递的概念。js也有这个概念,虽然它根据要传递的数据类型来推断。当你给一个方法传递原始类型的值时,重新给参数赋值并不会改变传递的值。但当你修改一个不是原始类型的值,传递的值也会被修改.

function primitiveMutator(val){
  val = val + 1;
}

let x = 1;
primitiveMutator(x):
console.log(x); //1

function objectMutator(val){
  val.prop = val.prop+1;
}
let obj = { prop: 1 };
objectMutator(obj);
console.log(obj.prop); //2

原始类型(除了神秘的 NaN )总是和另一个内容一样的原始类型的值完全相等。

const first = "abc" + "def";
const second = "ab" + "cd" + "ef";
console.log(first === second); //true

但是构造相同的非原始类型的值并不相等:

const obj1 = { name: "Intrinsic" };
const obj2 = { name: "Intrinsic" };

console.log(obj1 === obj2); //false
console.log(obj1.name === obj2.name) //true

对象再js中的用途很广泛,它们无处不在。经常作为键值对组合。但是当以这种方式使用的时候会有很大的限制:直到Symbols出现,object的key只能是string字符串。如果我们试图用一个不是字符串的值去当key的话,这个值就会强制转换成string;

const obj = {};
obj.foo = 'foo';
obj['bar'] = 'bar';
obj[2] = 2;
obj[{}] = 'someobj';

console.log(obj);
//{'2':2,foo:'foo',bar:'bar','[object Object]':'someobj'}

注意:主题略有偏离,但创建Map数据结构的一部分目的是为了在键不是字符串的情况下存储键/值

什么是Symbol

限制我们了解了什么是原始类型,我们终于准备好定义Symbol了。Symbol 是不能被重复创建的原始类型.在这种情况下symbol就像是被许多实例化的对象被赋予了不同的值.

const s1 = Symbol();
const s2 = Symbol();

console.log(s1 === s2); //false

当初始化一个symbol的时候,可以传递一个字符串当做可选的参数。这个值将会用做debug的标识,并不是真实的影响symbol本身。

const s1 = Symbol('debug')
const str = 'debug'
const s2 = Symbol('xxyy');

console.log(s1 === str);//false
console.log(s1 === s2); //false
console.log(s1); //Symbol(debug)

Symbol 当做对象属性

Symbol有另一个重要的用途.它能用来当做object的key,下面是一个例子:

const obj = {};
const sym = Symbol();
obj[sym] = 'foo';
obj.bar = 'bar';

console.log(obj); //{bar:'bar'}
console.log(sym in obj); //true
console.log(obj[sym]); //foo
console.log(Object.keys[obj)); //['bar']

注意Object.keys()并不返回Symbol,这也是处于向后兼容的目的.旧代码并不认识symbol,所以Objects.keys()方法也不应该返回他们;

一眼看上去,symbol可以用来创建object的一个私有属性。许多其它编程语言在类中都有隐藏属性的概念,长期以来这也被视为js的一个缺点.

不幸的是,与对象交互的方法仍有可能访问其key为symbol的值。甚至在调用代码尚未访问符号本身的情况下也有可能。Reflect.ownKeys() 就是一个例子,这个方法可以列出object的所有属性,string的和symbol的。

function tryToAddPrivate(o){
  o[Symbol('Pseudo Private')] = 42;
}

const obj = {prop:'hello'};
tryToAddPrivate(obj);

console.log(Reflect.ownKeys(obj)); //['prop',Symbol(Pseudo Private)]
console.log(obj[Reflect.ownKeys(obj)[1]]); //42

注意: 当前正在完成解决向JavaScript中的类添加私有属性的问题。此功能的名称称为 “Private Fields” ,尽管这不会使所有对象受益,但它将使作为类实例的对象受益。专用字段自Chrome 74起可用

防止命名冲突

Symbol可能无法为JavaScript提供对象私有属性的好处,但是它还有另一个好处,在不同的库希望向同一个object添加属性而又不希望引起命名冲突的问题的情况下很有用。

想象一下这样一个场景,两个不同的库希望将要某种元数据附加到同一个对象的情况。比如他们都想在对象上设置一个标识的key,有很大风险许多库都会用id作为标识;

function lib1tag(obj){ obj.id = 42;}
function lib2tag(obj){ obj.id = 24;}

用symbol的话就解决了这个问题,每个库产生自己的symbol然后再添加到object上。

const lib1property = Symbol('lib1');
function lib1tag(obj){ obj[lib1property] = 42; }

const lib2property = Symbol('lib2');
function lib2tag(obj){ obj[lib2property] = 24; }

是不是感觉好多了。

你也许会想,为什么每个库不能生成一个随机的字符串或者使用命名空间呢?

const lib1property = uuid();
function lib1tag(obj){ obj[lib1property] = 42; }

const lib2property = 'LIB2-NAMESPACE-id';
function lib2tag(obj){ obj[lib2property] = 24; }

你是对的,这种方法实际上与用symbol的方法非常类似。除非两个库使用相同的属性名称,否则不会有冲突的风险。

在这一点上,精明的读者会指出,这两种方法并不完全相同。我们使用的唯一的属性名称仍然有个缺点:它很容易被发现,尤其是当代码在迭代keys或者序列号object的时候。例子:

const library2property = 'LIB2-NAMESPACE-id'; // namespaced
function lib2tag(obj) {
  obj[library2property] = 369;
}
const user = {
  name: 'Thomas Hunter II',
  age: 32
};
lib2tag(user);
JSON.stringify(user);
// '{"name":"Thomas Hunter II","age":32,"LIB2-NAMESPACE-id":369}'

如果我们用symbol当做object的key,序列化的结果就不会出现我们设置的名称了。为什么呢?因为js支持symbol并不意味着JSON规范也改变了。JSON只允许用字符串当做key,JavaScript并不会尝试用symbol来表示最终的结果。

我们可以用 Object.defineProperty() 来修正自定义的属性污染JSON输出的问题.

const library2property = uuid(); // namespaced approach
function lib2tag(obj) {
  Object.defineProperty(obj, library2property, {
    enumerable: false,
    value: 369
  });
}
const user = {
  name: 'Thomas Hunter II',
  age: 32
};
lib2tag(user);
// '{"name":"Thomas Hunter II","age":32}'
console.log(JSON.stringify(user));
console.log(user[library2property]); // 369

通过设置 enumerable 属性,字符串key就被“隐藏”了,这样就和用symbol很像了。Object.keys() 都不会显示,并且 Reflect.ownKeys() 都会显示。

const obj = {};
obj[Symbol()] = 1;
Object.defineProperty(obj, 'foo', {
  enumberable: false,
  value: 2
});
console.log(Object.keys(obj)); // []
console.log(Reflect.ownKeys(obj)); // [ 'foo', Symbol() ]
console.log(JSON.stringify(obj)); // {}

至此,我们几乎重新创建了Symbol。隐藏的字符串属性和符合都从序列化程序中隐藏。并且他们都可以用 Reflect.ownKeys() 方法来提取,这两个属性也不是私有的。假设我们用命名空间/随机的字符串当做属性名称的话,我们就避免了命名冲突的风险.

但还是有点小区别。由于字符串是不可变的,并且Symbol是唯一的,因此仍有可能有人生成相同的组合并产生冲突.从数学上讲,Symbol确实提供了字符串无法提供的好处。

在Nodejs中,当检测对象时(比如 console.log()),如果遇到了对象上有个方法叫 inspect 的话,这个方法就会被调用,并且输出结果会表示为对象的结果.正如你所想的那样,这个特性不是每个人都期望的,并且这个通用的名称 inspect 经常会与开发人员的名称冲突。现在有一个可用的Symbol实现的方法。 require('util').inspect.custom. inspect 方法以及在Nodejs V10中设为不推荐,并且在V11中被弃用了。现在也不会有这种情况产生了。

模拟私有属性

这有一种有趣的方法,我们可以在object上模拟一个私有的属性。这个方法会用到js的另一个特性: Proxy; Proxy实质上就是包了一层对象,并允许我们与这个对象进行交互。

proxy 提供了多种方法来拦截对object执行的操作。现在我们想用的就是在读取object的key的时候会产生效果。我并不打算介绍proxy是如何工作的,如果有兴趣的话可以去看我们的另一篇文章:JavaScript Object Property Descriptors, Proxies, and Preventing Extension

我们用代理来确定对象上哪些属性可用。在这个例子中我们将设计一个代理来隐藏我们的两个已知隐藏属性,一个是字符串 _favColor, 另一个是symbol favBook

let proxy;

{
  const favBook = Symbol('fav book');

  const obj = {
    name: 'Thomas Hunter II',
    age: 32,
    _favColor: 'blue',
    [favBook]: 'Metro 2033',
    [Symbol('visible')]: 'foo'
  };

  const handler = {
    ownKeys: (target) => {
      const reportedKeys = [];
      const actualKeys = Reflect.ownKeys(target);

      for (const key of actualKeys) {
        if (key === favBook || key === '_favColor') {
          continue;
        }
        reportedKeys.push(key);
      }

      return reportedKeys;
    }
  };

  proxy = new Proxy(obj, handler);
}

console.log(Object.keys(proxy)); // [ 'name', 'age' ]
console.log(Reflect.ownKeys(proxy)); // [ 'name', 'age', Symbol(visible) ]
console.log(Object.getOwnPropertyNames(proxy)); // [ 'name', 'age' ]
console.log(Object.getOwnPropertySymbols(proxy)); // [Symbol(visible)]
console.log(proxy._favColor); // 'blue'

_favColor 字符串很简单:只需阅读库的源代码即可。另外,可以通过蛮力找到动态键(例如,之前的uuid示例)。但是没有直接引用该符号,没有人可以从proxy object访问“ Metro 2033”值

Nodejs 提示:Node.js中有一个功能可以打破代理的隐私,该功能在JavaScript语言中并不存在,并且在其它情况下(比如在浏览器中)也不适用。给定代理后,它就可以访问基础对象。

const [originalObject] = process
  .binding('util')
  .getProxyDetails(proxy);
const allKeys = Reflect.ownKeys(originalObject);
console.log(allKeys[3]); // Symbol(fav book)

我们现在需要修改全局Reflect对象或修改util进程绑定,以防止它们在特定的Node.js实例中使用.但这是一个坑,有兴趣的话翻阅另一篇文章 Protecting your JavaScript APIs

原文地址: https://medium.com/intrinsic/javascript-symbols-but-why-6b02768f4a5c