About Symbol

330 阅读8分钟
  • Symbol基础知识和实际运用场景
  • Symbol注册表与Symbol.for/keyFor
  • for of循环的底层机制Symbol.iterator
  • for await...of循环和Symbol.asyncIterator
  • Symbol.hasInstance/toPrimitive/toStringTag的运用

Symbol基础知识和实际运用场景

概述

ES6 引入了一种新的原始数据类型 Symbol ,表示独一无二的值,最大的用法是用来定义对象的唯一属性名。

符号(symbol)是一种特殊的、不可变的数据类型,可以作为对象属性的标识符使用。生成 Symbol 值的最简单的方式就是直接通过 Symbol 函数生成。

let sym = Symbol();
console.log(sym);  // Symbol()

基本用法

  • Symbol 函数栈不能用 new 命令,因为 Symbol 是原始数据类型,不是对象(普通函数非构造函数)。
  • 可以接受一个字符串作为参数,为新创建的Symbol 提供描述,用来显示在控制台或者作为字符串的时候使用,便于区分。
let sym1 = Symbol("test");
let sym2 = Symbol("test");
console.log(sym1);          // Symbol(test)
console.log(sym2);          // Symbol(test)
console.log(sym1 == sym2);  // false
console.log(sym1 === sym2); // false
  • Symbol值不能与其他类型的值进行运算,会报错;
let sym = Symbol("test");
let str = "hello";
console.log(str + sym); // Uncaught TypeError: Cannot convert a Symbol value to a string(…)
  • Symbol不能被隐式自动转换为字符串,这和语言中的其它类型不同,但是它可以显式转换为字符串。
var sym = Symbol('My symbol');
String(sym)    // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'

使用场景

  • Symbol 值作为对象属性名:
  1. 对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的Symbol类型。由于每一个 Symbol 的值都是不相等的,所以 Symbol 作为对象的属性名,可以保证属性不重名。
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
  [mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
  1. Symbol 作为对象属性名时不能用.运算符,要用方括号。因为.运算符后面是字符串,所以取到的是字符串 sy 属性,而不是 Symbol 值 sy 属性。
let sy = Symbol("key1");
let syObject = {};
syObject[sy] = "kk";
 
syObject[sy];  // "kk"
syObject.sy;   // undefined
  1. 属性名的遍历:Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols
// [Symbol(a), Symbol(b)]
  • Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。
const COLOR_RED = Symbol("red");
const COLOR_YELLOW = Symbol("yellow");
const COLOR_BLUE = Symbol("blue");
 
function ColorException(message) {
   this.message = message;
   this.name = "ColorException";
}
function getConstantName(color) {
    switch (color) {
        case COLOR_RED :
            return "COLOR_RED";
        case COLOR_YELLOW :
            return "COLOR_YELLOW ";
        case COLOR_BLUE:
            return "COLOR_BLUE";
        default:
            throw new ColorException("Can't find this color");
    }
}
 
try {
   
   var color = "green"; // green 引发异常
   var colorName = getConstantName(color);
} catch (e) {
   var colorName = "unknown";
   console.log(e.message, e.name); // 传递异常对象到错误处理
}

Symbol注册表与Symbol.for/keyFor方法

  • Symbol.for(key) 方法会根据给定的键 key,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol,并放入全局 symbol 注册表中。
    • Symbol.for()与Symbol()这两种写法,都会生成新的Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。
let yellow = Symbol("Yellow");
let yellow1 = Symbol.for("Yellow");
yellow === yellow1;      // false
 
let yellow2 = Symbol.for("Yellow");
yellow1 === yellow2;     // true
  • Symbol.keyFor(sym) 方法用来获取 symbol 注册表中与某个 symbol 关联的键。
let yellow1 = Symbol.for("Yellow");
Symbol.keyFor(yellow1);    // "Yellow"

for of循环的底层机制Symbol.iterator(es6)

for...of 是ES6引入用来遍历所有数据结构的统一方法。 这里的所有数据结构是指具有iterator接口的数据。通过一个键为Symbol.iterator 的方法来实现,就可以使用 for...of 循环遍历它的成员。

为什么引入 Iterator

因为 ES6添加了Map,Set,再加上原有的数组,对象,一共就是4种表示 “集合”的数据结构。没有 Map和 Set之前,我们都知道for...in一般是常用来遍历对象,for循环常用来遍历数据,现在引入的Map,Set,难道还要单独为他们引入适合用来遍历各自的方法么。聪明的你肯定能想到,我们能不能提供一个方法来遍历所有的数据结构呢,这个方法能遍历所有的数据结构,一定是这些数据结构要有一些通用的一些特征,然后这个公共的方法会根据这些通用的特征去进行遍历。

官方对Iterator的解释:遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。通俗点理解就是为了解决不同数据结构遍历的问题,引入了Iterator.

迭代过程

  • 通过 Symbol.iterator创建一个迭代器,指向当前数据结构的起始位置
  • 随后通过 next 方法进行向下迭代指向下一个位置, next 方法会返回当前位置的对象,对象包含了 value 和 done 两个属性, value 是当前属性的值, done 用于判断是否遍历结束
  • 当 done 为 true 时则遍历结束 下面通过一个简单的例子进行说明:
const items = ["zero", "one", "two"];
const it = items[Symbol.iterator]();
 
it.next();
>{value: "zero", done: false}
it.next();
>{value: "one", done: false}
it.next();
>{value: "two", done: false}
it.next();
>{value: undefined, done: true}

上面的例子,首先创建一个数组,然后通过 Symbol.iterator方法创建一个迭代器,之后不断的调用 next 方法对数组内部项进行访问,当属性 done 为 true 时访问结束。

可迭代的数据结构

Array

for (let item of ["zero", "one", "two"]) {
  console.log(item);
}
// output:
// zero
// one
// two

String(字符串是一个类似数组的对象,原生具有 Iterator 接口,会正确识别 32 位 UTF-16 字符。)

for (const c of 'z\uD83D\uDC0A') {
    console.log(c);
}
// output:
// z
// \uD83D\uDC0A

Map

const map = new Map();
map.set(0, "zero");
map.set(1, "one");
 
for (let item of map) {
  console.log(item);
}
// output:
// [0, "zero"]
// [1, "one"]

Set(Set 是对其元素进行迭代,迭代的顺序与其添加的顺序相同)

const set = new Set();
set.add("zero");
set.add("one");
 
for (let item of set) {
  console.log(item);
}
// output:
// zero
// one

函数的 arguments 对象

function args() {
  for (let item of arguments) {
    console.log(item);
  }
}
args("zero", "one");
// output:
// zero
// one

NodeList 对象(节点的集合)

let paras = document.querySelectorAll(“p”);

for (let p of paras) {
    console.log(p);
}

也可以叫做原生具备 Iterator 接口的数据结构。

普通对象不可迭代

普通对象是由 object 创建的,不可迭代,必须部署了 Iterator 接口后才能使用

let es6 = {
  edition: 6,
  committee: "TC39",
  standard: "ECMA-262"
};

for (let e in es6) {
  console.log(e);
}
// edition
// committee
// standard

for (let e of es6) {
  console.log(e);
}
// TypeError: es6[Symbol.iterator] is not a function

解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。

for (var key of Object.keys(someObject)) {
  console.log(key + ': ' + someObject[key]);
}

与其他遍历语法的比较

  • for循环-繁琐
for (var index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}
  • forEach-无法中途跳出forEach循环,break命令或return命令都不能奏效。
myArray.forEach(function (value) {
  console.log(value);
});
  • for...in(可以遍历数组的键名)
for (var index in myArray) {
  console.log(myArray[index]);
}
  • 缺点:
    1. 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
    2. for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
    3. 某些情况下,for...in循环会以任意顺序遍历键名。 for...in循环主要是为遍历对象而设计的,不适用于遍历数组。

完胜:

  1. 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
  2. 不同于forEach方法,它可以与break、continue和return配合使用。
  3. 提供了遍历所有数据结构的统一操作接口。

for await...of循环和Symbol.asyncIterator(es9)

同步遍历器的问题

function idMaker() {
  let index = 0;

  return {
    next: function() {
      return { value: index++, done: false };
    }
  };
}

const it = idMaker();

it.next().value // 0
it.next().value // 1
it.next().value // 2
// ...

上面代码中,变量it是一个遍历器(iterator)。每次调用it.next()方法,就返回一个对象,表示当前遍历位置的信息。

这里隐含着一个规定,it.next()方法必须是同步的,只要调用就必须立刻返回值。也就是说,一旦执行it.next()方法,就必须同步地得到value和done这两个属性。如果遍历指针正好指向同步操作,当然没有问题,但对于异步操作,就不太合适了。

异步遍历器的最大的语法特点,就是调用遍历器的next方法,返回的是一个 Promise 对象。

异步迭代器和同步迭代器相同,都是一个函数,并且含有一个next方法,区别在于同步迭代器的next方法返回一个含有value和done属性的对象,而异步迭代器的next方法返回一个Promise对象,并且Promise对象的值为含有value和done属性的对象。

asyncIterator
  .next()
  .then(
    ({ value, done }) => /* ... */
  );

for...await...of

  • for...of方法能够遍历具有Symbol.iterator接口的同步迭代器数据,但是不能遍历异步迭代器。
  • ES9新增的for...await...of可以用来遍历具有Symbol.asyncIterator方法的数据结构,也就是异步迭代器,且会等待前一个成员的状态改变后才会遍历到下一个成员,相当于async函数内部的await。
  • 如果next方法返回的 Promise 对象被reject,for await...of就会报错,要用try...catch捕捉。

Symbol.hasInstance/toPrimitive/toStringTag的运用

通过Symbol的属性来操作JavaScript内部的逻辑 (以前我们比较难去操作JavaScript本身语言内部的逻辑,最多也是在原型链上去定义或者修改某个方法的实现,Symbol的属性中提供了很多去处理程序内部执行的逻辑)

Symbol.hasInstance: instanceof

  • Symbol.hasInstance 是一个实现了 instanceof 行为的 Symbol。
  • 当一个兼容 ES6 的引擎在某个表达式中看到了 instanceof 运算符,它会调用 Symbol.hasInstance。
  • 基于ES6自己扩展,防止因为原型被重定向引发的instanceof检测不准确的问题

instance来判断是否为某个类或函数的实例,有一天我想给予它就算它是真实的实例,我也想让它返回false,即:不是一个实例。

function Pre(){

}
Object.defineProperty(Pre,Symbol.hasInstance,{
  value: function(){
     return false
  }
})

Symbol.toPrimitive

当 JavaScript 引擎需要将你对象转换为原始值时(如数字或者字符串),先执行"object [Symbol.toPrimitive] ([hint])"

例如,如果你执行 +object ,那么 JavaScript 会调用 object [Symbol.toPrimitive] ('number');

如果你执行 ''+object ,那么 JavaScript 会调用 object [Symbol.toPrimitive] ('string');

而如果你执行 if(object),JavaScript 则会调用 object [Symbol.toPrimitive] ('default').

Symbol.toPrimitive 的实现如下:

class AnswerToLifeAndUniverseAndEverything {
    [Symbol.toPrimitive](hint) {
        if (hint === 'string') {
            return 'Like, 42, man';
        } else if (hint === 'number') {
            return 42;
        } else {
            // 大多数类(除了 Date)都默认返回一个数值原始值
            return 42;
        }
    }
}

var answer = new AnswerToLifeAndUniverseAndEverything();
+answer === 42;
Number(answer) === 42;
''+answer === 'Like, 42, man';
String(answer) === 'Like, 42, man';

Symbol.toStringTag

Object.prototype.toString.call([value])检测数据类型的原理

尝试实现一个你自己的用于替代typeof运算符的类型判断, 改变Object.prototype.toString的返回值。

class Collection {

  get [Symbol.toStringTag]() {
    return 'Collection';
  }

}
var x = new Collection();
Object.prototype.toString.call(x) === '[object Collection]'