JavaScript为什么要有Symbol 类型

Symbols 是ES6引入了一个新的数据类型 ,它为JS带来了一些好处,尤其是对象属性时。 但是,它们能为我们做些字符串不能做的事情呢?

在深入探讨Symbol之前,让我们先看看一些 JavaScript 特性,许多开发人员可能不知道这些特性。

背景

js中的数据类型总体来说分为两种,他们分别是:值类型和 引用类型

值类型(基本类型):数值型(Number),字符类型(String),布尔值型(Boolean),null 和 underfined

引用类型(类):函数,对象,数组等

值类型理解:变量之间的互相赋值,是指开辟一块新的内存空间,将变量值赋给新变量保存到新开辟的内存里面;之后两个变量的值变动互不影响,例如:

var a=10; //开辟一块内存空间保存变量a的值“10”;

var b=a; //给变量 b 开辟一块新的内存空间,将 a 的值 “10” 赋值一份保存到新的内存里;

//a 和 b 的值以后无论如何变化,都不会影响到对方的值;

一些语言,比如 C,有引用传递和值传递的概念。JavaScript 也有类似的概念,它是根据传递的数据类型推断的。如果将值传递给函数,则重新分配该值不会修改调用位置中的值。但是,如果你修改的是引用类型,那么修改后的值也将在调用它的地方被修改。

顺便给大家推荐一个裙,它的前面是 537,中间是631,最后就是 707。想要学习前端的小伙伴可以加入我们一起学习,互相帮助。群里每天晚上都有大神免费直播上课,如果不是想学习的小伙伴就不要加啦。

引用类型理解:变量之间的互相赋值,只是指针的交换,而并非将对象(普通对象,函数对象,数组对象)复制一份给新的变量,对象依然还是只有一个,只是多了一个指引~~;例如:

var a={x:1,y:2} //需要开辟内存空间保存对象,变量 a 的值是一个地址,这个地址指向保存对象的空间;

var b=a; // 将a 的指引地址赋值给 b,而并非复制一给对象且新开一块内存空间来保存;

// 这个时候通过 a 来修改对象的属性,则通过 b 来查看属性时对象属性已经发生改变;

值类型(神秘的 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

// 但是,它们的 .name 属性是基本类型:

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

对象在 JavaScript 语言中扮演重要角色,它们的使用无处不在。对象通常用作键/值对的集合,然而,以这种方式使用它们有一个很大的限制: 在symbol出现之前,对象键只能是字符串,如果试图使用非字符串值作为对象的键,那么该值将被强制转换为字符串,如下:

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' }

Symbol 是什么

Symbol()函数会返回 symbol 类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的 symbol 注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。所以使用 Symbol 生成的值是不相等:

const s1 = Symbol();

const s2 = Symbol();

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

实例化symbol时,有一个可选的第一个参数,你可以选择为其提供字符串。 此值旨在用于调试代码,否则它不会真正影响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 还有另一个重要的用途,它们可以用作对象中的键,如下:

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']

乍一看,这看起来就像可以使用symbol在对象上创建私有属性,许多其他编程语言在其类中有自己的私有属性,私有属性遗漏一直被视为 JavaScript 的缺点。

不幸的是,与该对象交互的代码仍然可以访问其键为 symbol 的属性。 在调用代码尚不能访问 symbol 本身的情况下,这甚至是可能的。 例如,Reflect.ownKeys()方法能够获取对象上所有键的列表,包括字符串和 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中向类添加私有属性的问题。这个特性的名称被称为私有字段,虽然这不会使所有对象受益,但会使类实例的对象受益。私有字段从 Chrome 74开始可用。

防止属性名称冲突

符号可能不会直接受益于JavaScript为对象提供私有属性。然而,他们是有益的另一个原因。当不同的库希望向对象添加属性而不存在名称冲突的风险时,它们非常有用。

Symbol 为 JavaScrit 对象提供私有属性还有点困难,但 Symbol 还有别外一个好处,就是避免当不同的库向对象添加属性存在命名冲突的风险。

考虑这样一种情况:两个不同的库想要向一个对象添加基本数据,可能它们都想在对象上设置某种标识符。通过简单地使用id作为键,这样存在一个巨大的风险,就是多个库将使用相同的键。

function lib1tag(obj) {

  obj.id = 42;

}

function lib2tag(obj) {

  obj.id = 369;

}

通过使用 Symbol,每个库可以在实例化时生成所需的 Symbol。然后用生成 Symbol 的值做为对象的属性:

const library1property = Symbol('lib1');

function lib1tag(obj) {

  obj[library1property] = 42;

}

const library2property = Symbol('lib2');

function lib2tag(obj) {

  obj[library2property] = 369;

}

出于这个原因,Symbol 似乎确实有利于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;

}

这种方法是没错的,这种方法实际上与 Symbol 的方法非常相似,除非两个库选择使用相同的属性名,否则不会有冲突的风险。

在这一点上,聪明的读者会指出,这两种方法并不完全相同。我们使用唯一名称的属性名仍然有一个缺点:它们的键非常容易找到,特别是当运行代码来迭代键或序列化对象时。考虑下面的例子:

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,那么 JSON 输出将不包含它的值。这是为什么呢? 虽然 JavaScript 获得了对 Symbol 的支持,但这并不意味着 JSON 规范已经改变! JSON 只允许字符串作为键,JavaScript 不会尝试在最终 JSON 有效负载中表示 Symbol 属性。

const library2property = 'f468c902-26ed-4b2e-81d6-5775ae7eec5d'; // namespaced approach

function lib2tag(obj) {

  Object.defineProperty(obj, library2property, {

    enumerable: false,

    value: 369

  });

}

const user = {

  name: 'Thomas Hunter II',

  age: 32

};

lib2tag(user);

console.log(user); // {name: "Thomas Hunter II", age: 32, f468c902-26ed-4b2e-81d6-5775ae7eec5d: 369}

console.log(JSON.stringify(user)); // {"name":"Thomas Hunter II","age":32}

console.log(user[library2property]); // 369

通过将enumerable属性设置为false而“隐藏”的字符串键的行为非常类似于 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。隐藏的字符串属性和 Symbol 都对序列化器隐藏。这两个属性都可以使用Reflect.ownKeys()方法读取,因此它们实际上不是私有的。假设我们为属性名的字符串版本使用某种名称空间/随机值,那么我们就消除了多个库意外发生名称冲突的风险。

但是,仍然有一个微小的区别。由于字符串是不可变的,而且 Symbol 总是保证惟一的,所以仍然有可能生成字符串组合会产生冲突。从数学上讲,这意味着 Symbol 确实提供了我们无法从字符串中得到的好处。

在 Node.js 中,检查对象时(例如使用console.log()),如果遇到名为inspect的对象上的方法,将调用该函数,并将打印内容。可以想象,这种行为并不是每个人都期望的,通常命名为inspect的方法经常与用户创建的对象发生冲突。

现在 Symbol 可用来实现这个功能,并且可以在equire('util').inspect.custom中使用。inspect方法在Node.js v10 中被废弃,在 v1 1中完全被忽略, 现在没有人会偶然改变检查的行为。

模拟私有属性

这里有一个有趣的方法,我们可以用来模拟对象上的私有属性。这种方法将利用另一个 JavaScript 特性: proxy(代理)。代理本质上封装了一个对象,并允许我们对与该对象的各种操作进行干预。

代理提供了许多方法来拦截在对象上执行的操作。我们可以使用代理来说明我们的对象上可用的属性,在这种情况下,我们将制作一个隐藏我们两个已知隐藏属性的代理,一个是字符串_favColor,另一个是分配给favBook的 S ymbol :

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示例)。但是,如果没有对 Symbol 的直接引用,任何人都不能 从proxy对象访问'Metro 2033'值。

Node.js警告:Node.js中有一个功能会破坏代理的隐私。 JavaScript语 言本身不存在此功能,并且不适用于其他情况,例 如Web 浏览器。 它允许在给定代理时获得对底层对象的访问权。 以下是使用此功能打破上述私有属性示例的示例:

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。

原文:JavaScript 为什么要有 Symbol 类型

作者:前端小智

链接:https://segmentfault.com/a/1190000018522663

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容