带你重学ES6 | Symbol(不单单只是一个新的数据类型)

前言

这篇文章说实话,在写完的那一刻真的对Symbol这个类型肃然起敬,平时真的不用这个数据类型,也没有想过会用它,以前仍是停留在只是知道这个单词的阶段,在写完后才发觉它的强大。前端

Symbol,ES6 中新增的数据类型,为何要增长这么一个数据类型?当初一个面试官这么问的我,当时年少轻狂的我,心里的 os 是,我哪知道 🤣!其实仍是对 Symbol 这个数据类型不熟悉。git

在 ES6 以前,对象的键只能是字符串类型,可是这样有个问题,就是会形成键名命名冲突,后者覆盖前者,这个时候就须要一个惟一值来充当键名,Symbol 横空出世。es6

一、概念

symbol 是一种基本数据类型,Symbol()函数会返回 symbol 类型的值,该类型具备静态属性和静态方法。可是它不是构造函数,不能用 new Symbol()来建立。github

let symbol = Symbol();
typeof symbol; // "symbol" 复制代码

Symbol 做为对象属性时,当在对象内部时,必需要用方括号括起来,不用方括号括起来表明的是字符串。面试

let s = Symbol();
let obj = {  [s]: "Jack", }; obj[s]; // "Jack" obj.s; // undefined 复制代码

并且当要取该属性的值时,不能用点运算符,由于点运算符后面一样是字符串类型。正则表达式

建立 Symbol 数据类型时,都是 Symbol()这么建立的,当打印出来时,都为 Symbol(),这样很难区别各个 Symbol 类型的变量是什么意思。因此在 Symbol 函数内能够接收一个字符串的参数,表示该定义 Symbol 类型变量的描述。express

let s1 = Symbol("a");
console.log(s1); // Symbol(a) s1.toString(); // "Symbol(a)" 复制代码

若是 Symbol 类型接收的一个对象类型的话,那就会先调用其内部的 toString 方法,将其变为一个字符串,而后才生成一个 Symbol 值。编程

let arr = [1, 2, 3];
let s1 = Symbol(arr); console.log(s1); // Symbol(1,2,3) let obj = {  toString: () => "abc", }; let s2 = Symbol(obj); console.log(s2); // Symbol(abc) 复制代码

Symbol 类型的变量是不能和其余变量参与运算的,并且其只能转为 String 类型和 Boolean 类型。数组

let s = Symbol();
console.log("1" + s); // TypeError: Cannot convert a Symbol value to a string s.toString(); // "Symbol()" Boolean(s); // true Number(s); // TypeError: Cannot convert a Symbol value to a number 复制代码

二、Symbol.prototype.description

当给 Symbol 添加描述时,能够经过 Symbol.prototype.description 来获取该描述。markdown

let s = Symbol("Jack");
s.description; // 'Jack' 复制代码

三、Symbol.for(key)和 Symbol.keyFor(sym)

最开始看到这两个方法时,我觉得是两个遍历的方法 😅。

  1. Symbol.for(key):使用给定的 key 搜索现有的 symbol,若是找到则返回该 symbol。不然将使用给定的 key 在全局 symbol 注册表中建立一个新的 symbol。
  2. Symbol.keyFor(sym):从全局 symbol 注册表中,为给定的 symbol 检索一个 key。
let s1 = Symbol.for("foo");
let s2 = Symbol.for("foo"); s1 === s2; // true 复制代码

Symbol.for 会搜索有没有以该参数做为名称的 Symbol 值。若是有,就返回这个 Symbol 值,不然就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。因此由其建立的两个相同描述的值会相等。这种建立就和普通的 Symbol()有着大相径庭的结果了:

let s1 = Symbol("foo");
let s2 = Symbol("foo"); s1 === s2; // false 复制代码

由于无论怎样 Symbol()返回的都是一个全新的值,换句话说 Symbol()生成的值没有注册在全局中,因此返回的值都是全新的,而 Symbol.for()会在先在全局中查找,有就返回这个值,没有则建立新的值,但新的值也是挂载在全局中的。

Symbol.keyFor(sym)是在全局中查找是否有该 Symbol 值,有则返回该描述。

let s1 = Symbol.for("Jack");
Symbol.keyFor(s1); // 'Jack' let s2 = Symbol("Rose"); Symbol.keyFor(s2); // undefined 复制代码

由于 s2 没有挂载在全局中,因此 Symbol.keyFor()找不到它,故返回 undefined。

四、内置的 Symbol 属性

除了定义本身使用的 Symbol 值之外,ES6 还提供了 13(有可能从此会更多 😛) 个内置的 Symbol 值,指向语言内部使用的方法。

4.1 Symbol.asyncIterator

Symbol.asyncIterator 符号指定了一个对象的默认异步迭代器。若是一个对象设置了这个属性,它就是异步可迭代对象,可用于 for await...of 循环。换句话说一个异步可迭代对象内部必须有 Symbol.asyncIterator 属性。

const myAsyncIterable = new Object();
myAsyncIterable[Symbol.asyncIterator] = async function* () {  yield "hello";  yield "async";  yield "iteration!"; };  (async () => {  for await (const x of myAsyncIterable) {  console.log(x);  // expected output:  // "hello"  // "async"  // "iteration!"  } })(); 复制代码

当执行 for await...of 时,就会执行该变量中 Symbol.asyncIterator 属性值。

4.二、Symbol.hasInstance

Symbol.hasInstance 用于判断某对象是否为某构造器的实例。所以你能够用它自定义 instanceof 操做符在某个类上的行为。换句话说当判断一个实例是否为一个类的实例时,其实就是执行该类里面的 Symbol.hasInstance 属性。

class Fu {
 [Symbol.hasInstance](num) {  return num === 1;  } } 1 instanceof new Fu(); // true 2 instanceof new Fu(); // false 复制代码

4.三、Symbol.isConcatSpreadable

内置的 Symbol.isConcatSpreadable 符号用于配置某对象做为 Array.prototype.concat()方法的参数时是否展开其数组元素。

// 默认状况下
let arr = [1, 2, 3]; let brr = [4, 5, 6]; arr.concat(brr); // [1, 2, 3, 4, 5, 6] // 设置了Symbol.isConcatSpreadable后 let arr = [1, 2, 3]; let brr = [4, 5, 6]; brr[Symbol.isConcatSpreadable] = false; arr.concat(brr); // [1, 2, 3, [4, 5, 6]] 复制代码

将数组的 Symbol.isConcatSpreadable 属性设置为 false 后,使用 concat 方法时该数据就不会展开。

对于类数组而言,默认数组使用 concat 方法该类数组是不展开的,咱们能够给类数组的 Symbol.isConcatSpreadable 设置为 true,这样就能够展开了,而且完成了类数组转换为数组,这样类数组转数组又多了一个方法。

// 默认状况下
function foo(x, y) {  let arr = [].concat(arguments);  console.log(arr); //[Arguments(2)] } foo(1, 2); // 设置了Symbol.isConcatSpreadable为true后 function foo(x, y) {  arguments[Symbol.isConcatSpreadable] = true;  let arr = [].concat(arguments);  console.log(arr); //[1, 2] } foo(1, 2); 复制代码

4.四、Symbol.iterator

Symbol.iterator 为每个对象定义了默认的迭代器。该迭代器能够被 for...of 循环使用。

const myIterable = {};
myIterable[Symbol.iterator] = function* () {  yield 1;  yield 2;  yield 3; };  [...myIterable]; // [1, 2, 3] 复制代码

对象进行 for...of 循环时,会调用 Symbol.iterator 方法,

4.五、Symbol.match

Symbol.match 指定了匹配的是正则表达式而不是字符串。String.prototype.match() 方法会调用此函数。换句话说就是当 str.match()执行时若是该属性存在,就会返回该方法的返回值。

class foo {
 [Symbol.match](string) {  return string;  } } "Jack".match(new foo()); // 'Jack' 复制代码

除上述以外,MDN 还提出了该属性另一个功能:此函数还用于标识对象是否具备正则表达式的行为。好比, String.prototype.startsWith(),String.prototype.endsWith() 和 String.prototype.includes() 这些方法会检查其第一个参数是不是正则表达式,是正则表达式就抛出一个 TypeError。如今,若是 match symbol 设置为 false(或者一个 假值),就表示该对象不打算用做正则表达式对象。

"/bar/".startsWith(/bar/); // TypeError: First argument to String.prototype.startsWith must not be a regular expression
// 当设置为false以后 var re = /foo/; re[Symbol.match] = false; "/foo/".startsWith(re); // true "/baz/".endsWith(re); // false 复制代码

4.六、Symbol.matchAll

Symbol.matchAll 返回一个迭代器,该迭代器根据字符串生成正则表达式的匹配项。此函数能够被 String.prototype.matchAll() 方法调用。

"abc".matchAll(/a/);
// 等价于 /a/[Symbol.matchAll]("abc"); 复制代码

4.七、Symbol.replace

Symbol.replace 这个属性指定了当一个字符串替换所匹配字符串时所调用的方法。String.prototype.replace() 方法会调用此方法。

String.prototype.replace(searchValue, replaceValue);
// 等同于 searchValue[Symbol.replace](this, replaceValue); // 例子 class Replace1 {  constructor(value) {  this.value = value;  }  [Symbol.replace](string) {  return `s/${string}/${this.value}/g`;  } }  console.log("foo".replace(new Replace1("bar"))); // "s/foo/bar/g" 复制代码

4.八、Symbol.search

Symbol.search 指定了一个搜索方法,这个方法接受用户输入的正则表达式,返回该正则表达式在字符串中匹配到的下标,这个方法由如下的方法来调用 String.prototype.search()。

String.prototype.search(regexp);
// 等价于 regexp[Symbol.search](this); // 例子 class Search1 {  [Symbol.search](str) {  return `${str} Word`;  } } "Hello".search(new Search1()); // Hello Word 复制代码

4.九、Symbol.species

Symbol.species 是个函数值属性,其被构造函数用以建立派生对象,换句话说 species 访问器属性容许子类覆盖对象的默认构造函数。

咱们举个例子:

// 默认状况下
class MyArray extends Array {} let arr = new MyArray(1, 2, 3); let brr = arr.map((item) => item); brr instanceof MyArray; // true brr instanceof Array; // true 复制代码

类 MyArray 继承于 Array,arr 为 MyArray 的实例,brr 为 arr 的衍生物,因此 brr 是 MyArray 的实例,而且因为原型链的缘故,brr 也是 Array 的实例。若是此时,咱们只想让 brr 为 Array 的实例,那 Symbol.species 属性值就派上用场了。

class MyArray extends Array {
 static get [Symbol.species]() {  return Array;  } } let arr = new MyArray(1, 2, 3); let brr = arr.map((item) => item); brr instanceof MyArray; // false brr instanceof Array; // true // 默认状况下 class MyArray extends Array {  static get [Symbol.species]() {  return this;  } } 复制代码

值得注意的是,定义 Symbol.species 属性时,前面必须声明是静态的 static 而且要运用 get 取值器。

4.十、Symbol.split

Symbol.split 指向 一个正则表达式的索引处分割字符串的方法。 这个方法经过 String.prototype.split() 调用。

String.prototype.split(separator, limit);
// 等价于 separator[Symbol.split](this, limit); // 例子 class Split1 {  [Symbol.split](str) {  return `${str} Word`;  } } "Hello".split(new Split1()); // Hello Word 复制代码

4.十一、Symbol.toPrimitive

Symbol.toPrimitive 是一个内置的 Symbol 值,它是做为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。该函数在调用时,会传递一个字符串参数 hint,表示要转换到的原始值的预期类型。字符串 hint 的类型有三种:'number', 'string', 'default'。

let obj =
 {  [Symbol.toPrimitive](hint) {  switch (hint) {  case "number":  return 123;  case "string":  return "123";  case "default":  return "default";  default:  throw new Error();  }  },  } + obj; // 123 `${obj}`; // '123' obj + ""; // "default" 复制代码

4.十二、Symbol.toStringTag

Symbol.toStringTag 是一个内置 symbol,它一般做为对象的属性键使用,对应的属性值应该为字符串类型,这个字符串用来表示该对象的自定义类型标签,一般只有内置的 Object.prototype.toString() 方法会去读取这个标签并把它包含在本身的返回值里。通俗点讲就是在 Object.prototype.toString()去判断自定义对象的数据类型时,返回的都是 object,能够经过这个属性来给自定义对象添加类型标签。 在我以前写的【重学 JS 之路】js 基础类型和引用类型写到最精确判断数据类型的方法就是 Object.prototype.toString(),至因而为何,在这就不过多阐述了,能够看这篇文章。

Object.prototype.toString.call('123'); // [object String]
...more 复制代码

另一些对象类型则否则,toString() 方法能识别它们是由于引擎为它们设置好了 toStringTag 标签:

Object.prototype.toString.call(new Map()); // "[object Map]"
Object.prototype.toString.call(function* () {}); // "[object GeneratorFunction]" Object.prototype.toString.call(Promise.resolve()); // "[object Promise]" ...more 复制代码

当咱们本身定义一个类时,调用 Object.prototype.toString()时,因为没有内部定义 toStringTag 标签,因此只能返回"[object Object]"

class Foo {}
Object.prototype.toString.call(new Foo()); // "[object Object]" // 设置Symbol.toStringTag class Foo {  get [Symbol.toStringTag]() {  return "Foo";  } } Object.prototype.toString.call(new Foo()); // "[object Foo]" 复制代码

4.1三、Symbol.unscopabless

Symbol.unscopables 指用于指定对象值,其对象自身和继承的从关联对象的 with 环境绑定中排除的属性名称。说白了其属性就是控制,在 with 词法环境中哪些属性会被 with 删除。

Array.prototype[Symbol.unscopabless];
// { // copyWithin: true, // entries: true, // fill: true, // find: true, // findIndex: true, // includes: true, // keys: true // } 复制代码

这里简单的讲解一下 with 函数,with 主要是用来对对象取值的,举个简单的例子:

let obj = {};
with (obj) {  let newa = a;  let newb = b;  console.log(newa + newb); } // 等价于 let newa = obj.a; let newb = obj.b; console.log(newa + newb); 复制代码

with 的 优势: 当 with 传入的值很是复杂时,即当 object 为很是复杂的嵌套结构时,with 就使得代码显得很是简洁。 with 的缺点: js 的编译器会检测 with 块中的变量是否属于 with 传入的对象, 上述例子为例,js 会检测 a 和 b 是否属于 obj 对象,这样就会的致使 with 语句的执行速度大大降低,性能比较差。

回归正题,咱们举个例子看一下 Symbol.unscopables 属性的做用。

let obj = {
 foo() {  return 1;  } } with(obj) {  foo(); // 1 } // 设置了Symbol.unscopables let obj = {  foo() {  return 1;  },  get [Symbol.unscopables]() {  return {  foo: true  }  } } with(obj) {  foo(); // Uncaught ReferenceError: foo is not defined } 复制代码

设置后报错的缘由是由于with已经将obj中的foo方法删除了。

这次也是对Symbol有了个从新的认识,也但愿对你有所帮助。 点个赞吧!💥🧡💖

参考

阮一峰《ECMAScript 6 入门——Symbol》

后语

以为还能够的,麻烦走的时候能给点个赞,你们一块儿学习和探讨!

还能够关注个人博客但愿能给个人github上点个Start,小伙伴们必定会发现一个问题,个人全部用户名几乎都与番茄有关,由于我真的很喜欢吃番茄❤️!!!

想跟车不迷路的小伙还但愿能够关注公众号 前端老番茄 或者扫一扫下面的二维码👇👇👇。

我是一个编程界的小学生,您的鼓励是我不断前进的动力,😄但愿能一块儿加油前进。

本文使用 mdnice 排版

相关文章
相关标签/搜索