- 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 值作为对象属性名:
- 对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的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!"
- Symbol 作为对象属性名时不能用.运算符,要用方括号。因为.运算符后面是字符串,所以取到的是字符串 sy 属性,而不是 Symbol 值 sy 属性。
let sy = Symbol("key1");
let syObject = {};
syObject[sy] = "kk";
syObject[sy]; // "kk"
syObject.sy; // undefined
- 属性名的遍历: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]);
}
- 缺点:
- 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
- for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
- 某些情况下,for...in循环会以任意顺序遍历键名。 for...in循环主要是为遍历对象而设计的,不适用于遍历数组。
完胜:
- 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
- 不同于forEach方法,它可以与break、continue和return配合使用。
- 提供了遍历所有数据结构的统一操作接口。
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]'