Symbol 的作用

Symbols 的涌现是为了什么呢?

  • 翻译自 medium
  • Symbols 是 JavaScript 最新推出的一种基础范例,它被当作对象属性时迥殊有效,然则有什么是它能做而 String 不能做的呢?
  • 在我们最先探究 Symbols 功用之前,我们先来看一下被很多开发者疏忽 JavaScript 的特征。

背景:

  • JavaScript 有两种值范例,一种是 基础范例 (primitives),一种是 对象范例 (objects,包含 function 范例),基础范例包含数字 number (包含 integer,float,Infinity,NaN),布尔值 boolean,字符串 string,undefined,null,只管 typeof null === 'object',null 依然是一个基础范例。
  • 基础范例的值是不可变的,固然了,寄存基础范例值得变量是能够被重新分配的,比方当你写 let x = 1; x++,变量 x 就被重新分配值了,然则你并没有转变本来的1.
  • 一些言语,比方 c 言语有援用通报和值通报的观点,JavaScript 也有相似的观点,只管它通报的数据范例须要揣摸。当你给一个 function 传值的时刻,重新分配值并不会修正该要领挪用时的参数值。但是,假如你修正一个非基础范例的值,修正值也会影响本来的值。
  • 斟酌下下面的例子:
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
// Though, their .name properties ARE primitives:
console.log(obj1.name === obj2.name); // true
  • 对象扮演了一个 JavaScript 言语的基础角色,它们被随处运用,它们常被用在键值对的存储。但是如许运用有一个很大的限定:在 symbols 降生之前,对象的键只能是字符串。假如我们试着运用一个非字符串当作对象的键,就会被转换为字符串,以下所示:
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 数据结构被建立的目标就是为了应对存储键值对中,键不是字符串的状况。

symbols 是什么?

  • 如今我们知道了什么是基础范例,终究准备好怎样定义什么是 symbols 了。symbols 是一种没法被重修的基础范例。这时候 symbols 有点相似与对象建立的实例相互不相称的状况,但同时 symbols 又是一种没法被转变的基础范例数据。这里有一个例子:
const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2); // false
  • 当你初始化一个带有一个吸收可选字符串参数的 symbols 时,我们能够来 debug 看下,除此之外看看它会否影响本身。
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)

symbols 作为对象的属性

  • symbols 有另一个很主要的用处,就是用作对象的 key。这儿有一个 symbols 作为对象 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() 并没有返回 symbols,这是为了向后兼容性的斟酌。老代码不兼容 symbols,因而陈旧的 Object.keys() 不应该返回 symbols。
  • 看第一眼,我们能够会以为 symbols 这个特征很合适作为对象的私有属性,很多其他言语都要相似的类的隐蔽属性,这一向被认为是 JavaScript 的一大短板。不幸的是,照样有能够经由过程 symbols 来取到对象的值,甚至都不必试着猎取对象属性就能够取得对象 key,比方,经由过程 Reflect.ownKeys() 要领就能够猎取一切的 key,包含 字符串和 symbols,以下所示:
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,只管这并不会使一切的对象受益,它依然对对象的实例有效,Private Fields 在 Chrome 74版本可用。

阻挠对象属性名争执

  • symbols 能够对对象的私有属性没有直接长处,然则它有别的一个用处,它在不知道对象原有属性名的状况下,扩大对象属性很有效。
  • 斟酌一下当两个差别的库要读取对象的一些原始属性时,也许它们都想要相似的标识符。假如只是简朴的运用字符串 id 作为 key,这将会有很大的风险,由于它们的 key 完全有能够雷同。
function lib1tag(obj) {
  obj.id = 42;
}
function lib2tag(obj) {
  obj.id = 369;
}
  • 经由过程运用 symbols,差别的库在初始化的时刻天生其所需的 symbols,然后就能够在对象上恣意赋值。
const library1property = Symbol('lib1');
function lib1tag(obj) {
  obj[library1property] = 42;
}
const library2property = Symbol('lib2');
function lib2tag(obj) {
  obj[library2property] = 369;
}
  • 这方面 symbols 确切对 JavaScript 有效。然后你也许会新鲜,差别的库举行初始化的时刻为何不运用随机字符串,或许运用定名空间呢?
const library1property = uuid(); // random approach
function lib1tag(obj) {
  obj[library1property] = 42;
}
const library2property = 'LIB2-NAMESPACE-id'; // namespaced approach
function lib2tag(obj) {
  obj[library2property] = 369;
}
  • 你是对的,这类要领确切相似于 symbols 的这一作用,除非两个库运用雷同的属性名,那就会有被覆写的风险。
  • 机灵的读者已发明这两种计划的结果并不完全雷同。我们独占的属性名依然有一个瑕玷:它们的 key 很轻易被找到,尤其是当代码举行递归或许系列化对象,斟酌以下的例子:
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}'
  • 假如我们运用 symbols 作为属性名,json 的输出将不会包含 symbols,这是为何呢?由于 JavaScript 支撑 symbols,并不意味着 json 范例也会随着修正。json 只允许字符串作为 key,JavaScript 并没有试图让 json 输出 symbols。
  • 我们能够简朴的经由过程 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,"f468c902-26ed-4b2e-81d6-5775ae7eec5d":369}'
console.log(JSON.stringify(user));
console.log(user[library2property]); // 369
  • 相似于 symbols,对象经由过程设置 enumerable 标识符来隐蔽字符串 key,它们都邑被 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)); // {}
  • 在这一点上,我们相当于重修了 symbols,我们的隐蔽字符串和 symbols 都被序列化器隐蔽了,属性也都能够经由过程 Reflect.ownKeys() 来猎取,因而他们并不算私有属性。假定我们运用定名空间、随机字符串等字符串作为对象的属性名,我们就能够防止多个库重名的风险。
  • 然则依然有一点纤细的差别,字符串是不可变的,而 symbols 能够保证永久唯一,因而依然有能够会有人天生重名的字符串。从数学意义上 symbols 供应了一个字符串没有的长处。
  • 在 Node.js 内里,当检测一个对象(比方运用 console.log()),假如对象上的一个要领叫做 inspect,当纪录对象时,该要领会被挪用并输出。你能够设想,这类行动并非每个人都邑如许做,被用户建立的 inspect 要领经常会致使定名争执,如今 require(‘util’).inspect.custom 供应的 symbol 能够被用在函数上。inspect 要领在 Node.js v10 被摒弃,在 v11 版直接被疏忽。如今没人能够遽然就转变 inspect 要领的行动了。

模仿私有属性

  • 这里有一个在对象上模仿私有属性的风趣的尝试。运用了另一个 JavaScript 的新特征:proxy。proxy 会包住一个对象,然后我们就能够跟这个对象举行林林总总的交互。
  • proxy 供应了很多种阻拦对象行动的体式格局。这里我们感兴致的是读取对象属性的行动。我并不会完全的诠释 proxy 是怎样事情的,所以假如你想要相识的更多,能够检察我们的另一篇文章:JavaScript Object Property Descriptors, Proxies, and Preventing Extension
  • 我们能够运用代办来展现对象上可用的属性。这里我们先建立一个 proxy 来隐蔽两个属性,一个是字符串 _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 属性很简朴,只须要浏览源码即可,别的,动态的 key 能够经由过程暴力破解体式格局取得(比方前面的 uuid 例子)。然则对 symbol 属性,假如你没有直接的援用,是没法接见到 Metro 2033 这个值的。
  • Node.js 备注:有一个特征能够破解私有属性,这个特征不是 JavaScript 的言语特征,也不存在与其他场景,比方 web 浏览器。当运用 proxy 时,你能够猎取到对象隐蔽的属性。这里有一个破解上面私有属性的例子:
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
    原文作者:shauvet
    原文地址: https://segmentfault.com/a/1190000018773865
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞