资讯专栏INFORMATION COLUMN

Symbol 的作用

xingqiba / 1086人阅读

摘要:然而这样使用有一个很大的限制在诞生之前,对象的键只能是字符串。如果只是简单的使用字符串作为,这将会有很大的风险,因为它们的完全有可能相同。只允许字符串作为,并没有试图让输出。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103266.html

相关文章

  • ES6学习笔记1--let和const命令、解构赋值和Symbol

    摘要:和命令命令在声明所在的块级作用域内有效。解构赋值从数组和对象中提取值,对变量进行赋值,这被称为解构。数值和布尔值的解构解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。默认值解构赋值允许指定默认值。 let和const命令 let命令 在声明所在的块级作用域内有效。 只要块级作用域内存在let命令,它所声明的变量就绑定(binding)这个区域,不再受外部的影响。 在同一个作用...

    liaosilzu2007 评论0 收藏0
  • ES6基本语法

    摘要:注意有些数据结构是在现有数据结构的基础上计算生成的,比如的数组都部署了一下三个方法,调用后都返回遍历器对象返回一个遍历器对象,用于遍历键名,键值组成的数组。 ES6是什么? JavaScript的第六版,在ES5的基础上增加了许多特性:箭头函数、字符串插值、代理、生成器、结构赋值、块级作用域等等。 一、let和const 1.作用:声明变量 ES6中明确规定,如果区块中存在let和co...

    Jeffrrey 评论0 收藏0
  • javascript---Symbol类型, 引用类型, 作用

    摘要:指针指针指针重要的时期说三遍由于对象类型为指针引用在变量复制方面,基本类型和引用类型也有所不同。在浏览器中,全局执行环境被认为是对象。 javascript---Symbol类型, 引用类型, 作用域 javascript的引用类型, 即对象类型是我们最常用的的类型, 其中有许多让我们需要注意的地方, 最新的 , ES6 的推出, 使得对象类型的属性名不仅仅可以是字符串类型,还可是Si...

    leejan97 评论0 收藏0
  • ECMAScript6

    摘要:返回布尔值标签模板可以紧跟一个函数名后边,该函数将被调用来处理这个模板字符串。其它情况下返回值为在内部,整数和浮点数使用同样的存储方法,所以和被视为同一个值。 简介 ES6目标,让JavaScript变成一个企业级的开发语言,不仅仅限制与前端页面的脚本语言。 标准(Standard): 用于定义与其他事物区别的一套规则 实现(Implementation): 某个标准的具体实施/真实实...

    MSchumi 评论0 收藏0
  • ECMAScript6标准入门(一)新增变量与数据结构

    摘要:一简介与的关系是的规格,是的一种实现另外的方言还有和转码器命令行环境安装直接运行代码命令将转换成命令浏览器环境加入,代码用环境安装,,根目录建立文件加载为的一个钩子设置完文件后,在应用入口加入若有使用,等全局对象及上方法安装 一、ECMAScript6 简介 (1) 与JavaScript的关系 ES是JS的规格,JS是ES的一种实现(另外的ECMAScript方言还有Jscript和...

    Tangpj 评论0 收藏0
  • ECMAScript6学习笔记

    摘要:笔记和和是块作用域的,是声明常量用的。一个对象如果要有可被循环调用的接口,就必须在的属性上部署遍历器生成方法原型链上的对象具有该方法也可。这种方式会访问注册表,其中存储了已经存在的一系列。这种方式与通过定义的独立不同,注册表中的是共享的。 ECMAScript6 笔记 let 和 const let和const是块作用域的 ,const是声明常量用的。 {let a = 10;} a ...

    CODING 评论0 收藏0

发表评论

0条评论

xingqiba

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<