likes
comments
collection
share

关于Symbol,这一篇你一定不能错过

作者站长头像
站长
· 阅读数 71

对于Symbol这个新语法在网上也看了很多的帖子和文章,主要内容80%都是原封不动的摘抄高程4或者阮老师的ES6。类似于听君一席话,如听一席话。所以想在前人的基础上,以另一种更容易被理解的方式来谈谈Symbol。有不对的地方希望各位看官可以在评论区指正。

为什么会有Symbol?

符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

多数文章都原封不动的引用了这句话,却没对它进行具体解释。让我们看看下面的例子:

    let objKey = {num: 0};
    let objKey1 = {num: 1};
    let obj = {
        // 基本数据类型
        undefined: undefined,
        null: null,
        true: true,
        1: 1,
        'name': "余江川",
        // 复杂数据类型
        //   {}: 'obj', // 报错,不被允许
        [objKey]: 'obj',  
    };

    obj.undefined = 'un';
    obj.null = 'nu';
    obj.true = 'tt';
    //  obj.1  // 报错,不被允许
    obj[1] = '2';
    obj[objKey1] = 'obj changed';
    
    console.log(obj);
    
    {
      '1': '2',
      undefined: 'un',
      null: 'nu',
      true: 'tt',
      name: '余江川',
      '[object Object]': 'obj changed'
    }

通过上面的例子可以清晰的发现无论是使用基本数据类型作为对象的key值,还是使用对象作为key值,都可以通过obj.或者是obj[]的方式进行修改和覆盖。

当用一个对象或复杂数据类型作为对象的key值时这种现象更为离谱,objKey和objKey1明显是两个不同的对象,但是最后的结果中却只有一个[object Object],而且他的值被修改覆盖了。

之所以会发生这种情况,原因就在于javaScript会将作为对象属性的key值隐式转换为字符串类型。而每一个对象经过转换后的结果都是[object Object],所以造成了用对象作为属性时会被修改和覆盖。

当实际项目中,需要用到对象作为key键时就会出现很尴尬的情况。同时如果我们的代码中如果有一些重要的属性不希望被覆盖和修改,需要一个唯一的标识符来标识这个值。这时通过普通的基本数据类型和复杂数据类型是无法做到这一点的。

为了解决上面的问题,JavaScript推出了Map来处理object、array等复杂数据类型作为枚举键的问题。针对第二个痛点,推出了Symbol

Symbol

Symbol如何解决上述的问题?

用Symbol作为key键时:

let obj = {
    [Symbol()]: 'symbol'
}
obj[Symbol()] = 'symbol changed';
console.log(obj);

{ 
    [Symbol()]: 'symbol', 
    [Symbol()]: 'symbol changed' 
}

可以看到用了同样的Symbol()但是并没有被覆盖属性而是又新建了一个键,出现这种情况的原因是任两个创建的Symbol值都是不同的。因此说他是唯一标识符

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

Symbol是什么类型?

废话,Symbol当然是symbol类型了。让我们看下面的例子:

console.log(typeof Symbol)   // function
console.log(typeof Symbol())  // symbol

所以当面试官问Symbol是什么时你可以回答的更为细致一点。

Symbol是函数类型,他的返回值是symbol类型。

我们经常说Symbol,但实际用到的是他被执行后的返回值Symbol()。 这一点要注意,在下面的使用中很关键。

Symbol是一个特殊函数

从上面Symbol的类型来看他是一个函数,但是他无法通过new关键字来创建实例。

console.log(Symbol.constructor);  // [Function: Function]
// let a = new Symbol();  // Symbol is not a constructor

Symbol()的使用方法

Symbol函数的返回值,所以不能通过.的方式调用

let obj = {
    [Symbol()]: 'symbol'  //  直接使用
};
let ss = Symbol();  //  先声明在使用
obj[ss] = 'symbol ss';
obj.Symbol = 'str';  // 一定注意symbol是Symbol函数的返回值
// obj.Symbol() = 'error'; // obj.Symbol is not a function 不能通过.的方式调用
console.log(obj);

{ Symbol: 'str', [Symbol()]: 'symbol', [Symbol()]: 'symbol ss' }

传入一个字符串对Symbol()做解释

直接使用Symbol的话虽然做到了唯一性但是也失去了字符串的可理解性,可以通过传入一个字符串的方式来表示这个Symbol()的含义。

let obj = {
    [Symbol()]: '张三',
    [Symbol()]: 20,
};
let obj1 = {
    [Symbol('name')]: '张三',
    [Symbol('age')]: 20,
};

这个字符串只是用作对这个Symbol()的描述,用来理解这个Symbol()的作用.除此之外他没有任何别的作用,两个Symbol()即使描述相同,也不是同一个Symbol()。

let s1 = Symbol('name');
let s2 = Symbol('name');
console.log(s1 == s2);  //  false

可以通过Symbol身上的description属性来获取到这个描述值。

let s1 = Symbol('name');
console.log(s1.description);  //  name

Symbol.for()

这个方法用来创建一个全局共享和可供重用的符号实例。

什么是全局共享?

在我第一次接触到这个属性时很不理解这个方法的意义,因为JavaScript分为全局作用域和局部作用域。如果我要在某些不同部分使用它,为何不在全局定义这个Symbol()?这个属性岂不是很鸡肋???

我之所以会有这样的理解,是因为各种文章帖子没有说的很清楚,又或者是自己对注册表的概念理解不深而造成的。事实上Symbol.for()是一个很强大的方法。

下面是MDN关于global Symbol registry的解释:

The above syntax using the Symbol() function will create a Symbol whose value remains unique throughout the lifetime of the program. To create Symbols available across files and even across realms (each of which has its own global scope), use the methods Symbol.for() and Symbol.keyFor() to set and retrieve Symbols from the global Symbol registry.

Note that the "global Symbol registry" is only a fictitious concept and may not correspond to any internal data structure in the JavaScript engine — and even if such a registry exists, its content is not available to the JavaScript code, except through the for() and keyFor() methods.

其中重要语句翻译过来大概意思是:

使用Symbol()函数的上述语法将创建一个Symbol,该Symbol的值在程序的整个生命周期中保持唯一。创建了一个跨文件甚至跨领域(每个领域都有自己的全局范围)的Symbol()。

请注意,“全局符号注册中心”只是一个虚构的概念,可能与JavaScript引擎中的任何内部数据结构都不对应-即使存在这样的注册中心,其内容对JavaScript代码也不可用,除非通过for()和keyFor()方法。

因此这里所指的全局共享指的并不是真实存在的全局作用域,而是在整个程序运行期间存在于虚拟的一个全局Symbol注册中心。跨文件甚至跨领域意味着他可以在iframe 和 workers 等环境中被获取到

什么是重用?

重用意思就是如果这个值已经存在就会复用这个值,如果不存在就创建这个值。这一点与Symbol有很大的区别,看下面的例子:

    let obj = {};
    let sf = Symbol.for('sf');
    let otherSf = Symbol.for('sf');

    let s = Symbol('s');
    let otherS = Symbol('s');

    obj[sf] = 'sf';
    obj[otherSf] = 'otherSf';
    obj[s] = 's';
    obj[otherS] = 'otherS';

    console.log(sf === otherSf);
    console.log(s === otherS);
    console.log(obj);
    
    
    //   true   使用Symbol.for()创建的两个变量是相等的
    //   false  使用Symbol()创建的两个变量是不想等的
    //   { 
    //        [Symbol(sf)]: 'otherSf', 
    //        [Symbol(s)]: 's', 
    //        [Symbol(s)]: 'otherS' 
    //   }
    

Symbol.keyFor()

这个方法翻译过来就是 什么Symbol的key。因此需要传入一个Symbol,返回这个Symbol的key值。但是需要注意以下几点:

  1. 这方法针对的是Symbol.for()创建的Symbol,而不是Symbol()。
let s = Symbol('s');
console.log(Symbol.keyFor(s) === undefined); // true
  1. 如果没有输入默认的key值,系统默认返回的是字符串类型的undefined,而不是undefined类型。
let sf = Symbol.for();
console.log(sf, typeof Symbol.keyFor(sf));  // Symbol(undefined) string
console.log(Symbol.keyFor(sf) === undefined);  // false

Symbol的特性

不能被常规的循环方法遍历到

常用遍历对象的方法 for...in 和 Object.getOwnPropertyNames()是不能获取到Symbol()值的,要用以下三种方法。

利用这一特性可以将私有属性命名为Symbol类型,虽然还是可以被获取到,但是一般情况下没有多少人会使用下面三种获取对象键的方法。

let s = Symbol(),
   sf = Symbol.for('sf'),
   obj = {};
obj[s] = 's';
obj[sf] = 'sf';
obj[Symbol()] = 'symbol';

for(let key in obj) {
    console.log(key, obj[key]); // 无输出
}

console.log(Object.getOwnPropertyNames(obj));  // []
console.log(Object.getOwnPropertySymbols(obj));  // [ Symbol(), Symbol(sf), Symbol() ]
console.log(Reflect.ownKeys(obj)); // [ Symbol(), Symbol(sf), Symbol() ]
console.log(Object.getOwnPropertyDescriptors(obj));
/**
 * {
  [Symbol()]: { value: 's', writable: true, enumerable: true, configurable: true },
  [Symbol(sf)]: { value: 'sf', writable: true, enumerable: true, configurable: true },
  [Symbol()]: {
    value: 'symbol',
    writable: true,
    enumerable: true,
    configurable: true
  }
}
 */

无法被转换为JSON

当有一些重要的属性要经过JSON转换传输时,例如前后端传值。就可以将自己的私有属性设置为Symbol类型,这样转换时就不会被传输。

let s = Symbol(),
   sf = Symbol.for('sf'),
   obj = {name: '李四'};
obj[s] = 's';
obj[sf] = 'sf';


console.log(JSON.stringify(obj)); //  {"name":"李四"}

常用内置符号

常用内置符号(well-known sumbol)是用来暴露语言内部的行为和属性。我们可以通过改写或重新定义这些属性来改变原生结构的行为。

常见的有以下:

Symbol.asyncIterator
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.match
Symbol.matchAll
Symbol.replace
Symbol.search
Symbol.species
Symbol.split
Symbol.toPrimitive
Symbol.toStringTag
Symbol.unscopables

这里比较常用的可能就是重写迭代器属性Symbol.iterator来实现自己的需求,其他的用到的比较少。这部分不做过多介绍,别的文章介绍的很全面。

具体释义可查看MDN

Symbol的应用场景

用于"私有属性”

前面提到过,虽然Symbol作为键同样可以被一些方法获取到,但是比起字符串类型的属性还是具有一定的隐蔽性。因此在不希望别人篡改自己的属性时,还是可以通过这种方法来做一个简单的"私有属性".

用于全局注册的变量

前面提到过Symbol.for()创建的变量可以跨域全局共享,因此当我们在iframe,workers等非windows环境下可以通过这种方式来获取到全局变量。

react中关于Symbol的应用

react中通过使用Symbol.for()属性来为每一种react元素设置独一无二的标记,并将其存放于ReactSymbol.js中。


// The Symbol used to tag the ReactElement-like types.
export const REACT_ELEMENT_TYPE: symbol = Symbol.for('react.element');
export const REACT_PORTAL_TYPE: symbol = Symbol.for('react.portal');
export const REACT_FRAGMENT_TYPE: symbol = Symbol.for('react.fragment');
export const REACT_STRICT_MODE_TYPE: symbol = Symbol.for('react.strict_mode');
export const REACT_PROFILER_TYPE: symbol = Symbol.for('react.profiler');
export const REACT_PROVIDER_TYPE: symbol = Symbol.for('react.provider');
export const REACT_CONTEXT_TYPE: symbol = Symbol.for('react.context');
export const REACT_SERVER_CONTEXT_TYPE: symbol = Symbol.for(
  'react.server_context',
);
export const REACT_FORWARD_REF_TYPE: symbol = Symbol.for('react.forward_ref');
export const REACT_SUSPENSE_TYPE: symbol = Symbol.for('react.suspense');
export const REACT_SUSPENSE_LIST_TYPE: symbol = Symbol.for(
  'react.suspense_list',
);
export const REACT_MEMO_TYPE: symbol = Symbol.for('react.memo');
export const REACT_LAZY_TYPE: symbol = Symbol.for('react.lazy');
export const REACT_SCOPE_TYPE: symbol = Symbol.for('react.scope');
export const REACT_DEBUG_TRACING_MODE_TYPE: symbol = Symbol.for(
  'react.debug_trace_mode',
);
export const REACT_OFFSCREEN_TYPE: symbol = Symbol.for('react.offscreen');
export const REACT_LEGACY_HIDDEN_TYPE: symbol = Symbol.for(
  'react.legacy_hidden',
);
export const REACT_CACHE_TYPE: symbol = Symbol.for('react.cache');
export const REACT_TRACING_MARKER_TYPE: symbol = Symbol.for(
  'react.tracing_marker',
);
export const REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED: symbol = Symbol.for(
  'react.default_value',
);

export const REACT_MEMO_CACHE_SENTINEL: symbol = Symbol.for(
  'react.memo_cache_sentinel',
);

const MAYBE_ITERATOR_SYMBOL = Symbol.iterator;
const FAUX_ITERATOR_SYMBOL = '@@iterator';

export function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator<any> {
  if (maybeIterable === null || typeof maybeIterable !== 'object') {
    return null;
  }
  const maybeIterator =
    (MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL]) ||
    maybeIterable[FAUX_ITERATOR_SYMBOL];
  if (typeof maybeIterator === 'function') {
    return maybeIterator;
  }
  return null;
}

之所以采用Symbol.for()主要有以下几种原因:

  1. 保证react元素的唯一性,确保每一种react元素之间都不会发生重复;
  2. 保证在跨文件、跨域时也能获取到react元素的类型,对react元素进行渲染。
  3. 消除魔术字符串,实现对字符串的解耦。

魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。 ——阮一峰

function getArea(shape, options) {
  let area = 0;

  switch (shape) {
    case 'Triangle': // 魔术字符串
      area = .5 * options.width * options.height;
      break;
    /* ... more code ... */
  }

  return area;
}

getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串

使用魔术字符串的缺点就在于当代码中多处使用到这个值时,会将代码逻辑和这个字符串进行强绑定。在后面维护想要修改这个字符串时带来困难,因此设置一个全局变量是解决这种问题的最好方式。

  1. 服务端渲染时,防止XSS攻击。

当服务端渲染时,由于Symbol不能被转换为JSON,所以即使服务器存在用JSON作为文本返回安全漏洞,JSON 里也不包含 Symbol.for('react.element') 。React 会检测 element.$$typeof,如果元素丢失或者无效,会拒绝处理该元素。

Symbol()与Symbol.for()应用场景的区别

Symbol()与Symbol.for()最大的区别就在于一个即使重复定义也不会重复(Symbol()),另一个是如果定义过就不会重复定义(Symbol.for())。

所以:

如果你更期望定义一个唯一且不被修改不重复的变量,后续不是用它进行逻辑处理和运算(使用Symbol()很难做相等判断),且只在同一个全局环境中使用。使用Symbol()比较好。

如果你希望像使用字符串变量一样,期望可以参与逻辑运算(例如判断是否存在等),并且要在不同的全局环境(iframe、workers)中都要使用这个值。使用Symbol.for()更好。

转载自:https://juejin.cn/post/7160683748516708388
评论
请登录