前言
2015年6月ES6
正式发布,距今已经有八年多快九年的时间了。
众所周知,在ES6
发布的时候伴随着新出了一个新的数据类型Symbol
。我们知道Symbol
代表的值是独一无二的,任何情况下两个Symbol
类型的值都是不同,Symbol()!==Symbol()
。因此Symbol
类型的值非常适合作为封装的对象里的key
来使用,因为这样即使别人使用你封装的这个对象,再添加新的key到这个封装对象里,无论如何也不会覆盖原有的key
。除此之外非要查找对象里以Symbol
作为key
的可以使用Object.getOwnPropertySymbols(obj)
或者使用Reflect.ownKeys(obj)
来查找Symbol
类型的key
。
好了今天的文章就到此结束了 (。・ω・。)。
当然Symbol
的用途不止这些,这些只是它的基本使用,那还有其他用的用途是什么呢?
正文
Symbol
本身有着数个属性和方法,方法也许不是很重要,但是那些属性才是除了Symbol
的特点之外核心所在。这里Symbol属性基本是作为动态的方法或属性名来使用。
Symbol属性
Symbol.iterator
iterator
译为迭代器。迭代器是什么,它允许你遍历一个集合的元素(例如数组、字符串、Map、Set等)而不需要知道集合的底层表示方式。迭代器对象提供了一种方法来访问集合的下一个元素,直到所有元素都被访问完。也许你没有在意过,但是能被遍历集合的原型上都存在Symbol.iterator
属性。
在ES6中同样新引入了一个遍历方法for..of
,当使用for..of
对元素进行遍历的时候,会先看或者元素是否是迭代器元素(也可以理解为这个元素上是否存在Symbol.iterator ),所以当使用for..of
遍历Object
类型的元素时,会报错,因为Object
类型的元素上不存在Symbol.iterator
。
js
const obj = {
name: "iceCode",
age: 18,
site: "成都",
};
for (let key of obj) {
console.log(key);
}
这也可以引出另外一个问题,如何使用for..of
遍历Object
的元素?
js
//这里仅需简单的改造一下
const obj = {
name: "iceCode",
age: 18,
site: "成都",
//当使用for..of遍历元素的时候,会自动调用Symbol.iterator这个方法,如果没有则报错
[Symbol.iterator]: function* () {
for (let key of Object.keys(this)) {
yield key;
}
},
};
for (let key of obj) {
console.log(`使用for..of遍历出的${key}`);
}
生成器函数
这里Symbol.iterator
上的方法是生成器函数,简单的介绍一下生成器函数,使用function
定义函数时在function
和函数名之间加个*号 ,函数内要返回值不再是return
关键字,而是使用yield
关键字返回值。可以放回多个值,访问时则是使用 fn().next().value
来访问,每次仅可访问一个数据,可以使用for..of
批量访问生成器函数内的数据。
仅需知道生成器函数最初是为了解决在处理大量数据时,一次性将所有数据加载到内存中可能会导致内存溢出和回调地狱而的问题。但随后有了Promise、async/await
之后,此函数便几乎不再使用。
js
function* fn() {
const a1 = yield 111;
console.log(a1); // 222
//这里的a1简单理解是下面调用next()方法传入的参数
const a2 = yield a1 + 1;
//这里a2同理
console.log(a2); // 333
const a3 = yield a2 + 1;
//a3为undefined是应为没有第四次调用next()方法,也不存在传入有的参数
console.log(a3); // undefined
}
const a1 = fn();
//这里所有的值都是yield返回的值,而不是传入的值
console.log(a1.next().value); //111
console.log(a1.next(222).value); //223
console.log(a1.next(333).value); //334
// 使用for...of循环 因为直接可以显示出来,并没有next()参数,这里从第二个开始就是undefined+1 自然是NaN
for (const i of a1) {
console.log(i); //111 NaN NaN
}
回到正题,除了在使用for...of
时会自动调用Symbol.iterator
的方法,还有一种情况下也会自动调用Symbol.iterator
方法,就是ES6中的解构赋值,当重新解构为一个数组时,就会自动调用Symbol.iterator
方法
这里又可以引出另外一个问题,怎么才能让这个公式成立 const [a,b]={a:1,b:2}(可能平时没人会傻到这样解构赋值),这种情况就可以根据以上的方法举一反三来解决。
js
const obj = {
a: 1,
b: 2,
[Symbol.iterator]: function* () {
yield this.a;
yield this.b;
},
};
//这里解构赋值是按照Symbol.iterator方法返回的迭代器来解构的 改变yield的顺序就可以改变解构的顺序
const [a, b] = obj; // 输出:1 2
另外当使用扩展运算符时,也会自动调用Symbol.iterator
,假如是Object
元素,只有在{...obj}
的情况下不会报错(自动调用Symbol.iterator
),既然会自动调用Symbol.iterator
,那就添加上即可
Symbol.asyncIterator
asyncIterator
异步迭代器,顾名思义,这个同迭代器基本类似,但区别不同是的这个主要处理异步方法。在for await ...of
时会自动调用。
js
//此方法同iterator类似就不多做介绍了,主要讲一些独占的差别
const p = (v) =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(v);
}, v);
});
const obj = {
//for...of 只会调用这个
*[Symbol.iterator]() {
yield p(222);
yield p(111);
yield p(333);
},
//for await ...of 只会调用这个
async *[Symbol.asyncIterator]() {
yield p(2222);
yield p(1111);
yield p(3333);
},
};
//最终调用的顺序也是从上到下,但是两个调用的方法不同
async function an() {
for await (const item of obj) {
console.log(item);
}
for (const item of obj) {
const i = await item;
console.log(i);
}
}
an();
Symbol.hasInstance
简单来说,此属性会在使用 instanceof 时自动调用。传入一个参数,为instanceof前的元素。
官方来说,instanceof
运算符 用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上,Symbol.hasInstance
用于判断某对象是否为某构造器的实例。因此你可以用它自定义 instanceof
操作符在某个类上的行为。
js
class Obj {
//在class中的这个方法必须加 static 关键字,否则无法被instanceof访问到
static [ Symbol.hasInstance ]( instance ) {
//这里接收的参数就是instanceof运算符右边的实例
console.log(instance);//[]
return instance;
}
}
//对象中同上,但一般instanceof基本是用在构造函数上
const obj = {
[Symbol.hasInstance](instance) {
console.log(instance);
return instance;
},
};
//因为Symbol.hasInstance方法中的返回值问题,无论什么时候都是true
console.log([] instanceof obj); // 输出 true
console.log([] instanceof Obj); // 输出 true
Symbol.isConcatSpreadable
该属性用于在数组类型元素使用.concat()
合并时是否展开数组元素内的数据
js
//此属性很好理解,看例子瞬间秒懂,就是数组类型元素将Symbol.isConcatSpreadable的值设置为false时
//将不会展开数组内的元素进行合并,将直接把第数组push到元素内
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = arr1.concat(arr2); // [1, 2, 3, 4, 5, 6]
arr2[Symbol.isConcatSpreadable] = false; // 禁止展开数组元素,即不使用concat方法合并数组元素。
const arr4 = arr1.concat(arr2); // [1, 2, 3, [4, 5, 6]]
console.log(arr3, arr4);
Symbol.search
在作为字符串方法search
的参数时,会自动调用Symbol.search
方法,传入一个参数,当前调用search
方法的字符串。字面意思可能不太很好理解,看例子。
js
//定义一个简单的对象
const obj = {
name: "iceCode",
site: "chegndu",
hobby: "code、music、game",
//obj作为search的参数时,此方法会被自动调用
[Symbol.search](str) {
//这里 str==='code' 使用toLowerCase是为了不区分大小写
str = str.toLowerCase();
const arr = [];
for (const key in this) {
//如果此对象中哪个属性值包含'code',则会将key添加到数组中并返回
this[key].toLowerCase().includes(str) && arr.push(key);
}
return arr;
},
};
//所以在这里得到的结果为 属性值里包含'code'的key的数组
console.log("code".search(obj));//['name', 'hobby']
//当然也可以是class类
class Cla {
constructor(value) {
this.value = value;
}
//不论是对象还是类,只要是作为search的参数时,这个方法就会被调用
[Symbol.search](str) {
//这里str依旧是调用search的字符串 不区分大小写返回索引
return str.toLowerCase().indexOf(this.value.toString().toLowerCase());
}
}
//所以这里得到的结果为 1
console.log("name".search(new Cla("a")));//1
Symbol.match
此属性用于设置是否为正则表达,通常用在正则表达上。
js
//如果字符串使用startsWith方法来判断开头的字符串是否为参数内的元素时,传入一个正常的正则表达则会报错
'/name/'.startsWith(/name/)
js
const exp = /de/;
//改变正则的Symbol.match方法为false时,此正则表达式不在作为正则表达式,而是字符串
exp[Symbol.match] = false;
//虽然这里类型判断依旧是正则类型
console.log(Object.prototype.toString.call(exp)); //[object RegExp]
//这里就可以正常判断了
console.log("/de/".startsWith(exp));//true
另外它也可以是一个函数,既然是Symbol.match
了,那么也肯定会在字符串的match方法的参数时自动被调用
js
const exp = /de/;
console.log("/desdfasdf/".match(exp));
正常情况下返回一个数组,里面是对匹配的一些详细描述,那么自动调用可以稍微改造一下
js
const exp = /de/;
//在作为match参数时自动调用
exp[Symbol.match] = function (v) {
//参数v就是字符出,this为正则表达式本身
console.log(v, this);
return {
isMatch: this.test(v), // 返回一个布尔值,表示是否匹配成功
index: v.indexOf(this.toString().replaceAll("/", "")),//判断索引位置
describe: `匹配${this.test(v) ? "成功" : "不成功"},匹配的位置在索引${v.indexOf(this.toString().replaceAll("/", ""))}`,
};
};
console.log("/desdfasdf/".match(exp));
Symbol.matchAll
此属性作为正则表达式的方法名,可接受一个字符串参数,返回一个该正则表达式匹配的字符串参数的迭代器。
介绍看着有点抽象,直接上代码
js
//创建一个只匹配数字的正则表达式
const exp = /[0-9]+/g;
//这里是要匹配的字符串
const str = "2023-12-31|2024-01-01";
//当我们调用它的Symbol.matchAll方法并传入字符串时,则会返回一个迭代器,该迭代器可以遍历所有匹配结果
const result = exp[Symbol.matchAll](str);
console.log(result); //直接打印这个结果只能看到正则表达式字符串迭代器,什么内容都无法看到
//前面说了迭代器,那么既然是迭代器,我们就可以用for...of循环来遍历它
for (const match of result) {
console.log(match);
}
可以看到最终的结果,是match
方法的一个集合
js
//如果是需要匹配到的结果,可以是用Array.form()方法,同样是找到迭代器然后返回一个浅拷贝的数组
console.log(Array.from(result, (x) => x[0]));//['2023', '12', '31', '2024', '01', '01']
同样,如果是作为字符串的matchAll的参数时,这个方法也会被自动调用,如果需要也可以同match一样将此方法重写,因为相同这里就不做演示了。
Symbol.species
官方描述:知名的 Symbol.species
是个函数值属性,其被构造函数用以创建派生对象。另外也可以简单的理解为在调用方法重新指向时会自动调用这方法,并可以在此方法内重新指向。
js
//这里继承一下Array,创建一个自己的类
class MyArr extends Array {
//这里必须要有static get 两个关键字,少一不可
static get [Symbol.species]() {
//在调用Array方法,重新返回当前Array时会自动调用。这种方式可以使Array类型的数据进行链式调用
console.log("gaibian");
//这里不再返回原有的MyArr类,直接返回Array
return Array;
}
}
//创建arr实例
const arr = new MyArr(1, 2, 3, 4);
//当使用arr方法需要返回一个数组
const mapArr = arr.map((item) => item);
//可以发现 当使用instanceof时,他的构造类不再是MyArr,而是Array
console.log(mapArr instanceof MyArr); // false
console.log(mapArr instanceof Array); // true
Symbol.replace
根据前面的规律,顾名思义,此属性方法将会元素作为字符串的replace
方法的参数时调用,传入一个参数,为当前调用repalce
的字符串。
js
const obj = {
name: "icecode",
age: 18,
//当obj作为replace的参数时,此方法会自动调用,返回的结果则是replace之后的结果
[Symbol.replace](str) {
//这里随便写个判断 在obj中第一个匹配到str的则会返回这个值
for (const key in this) {
if (typeof this[key] === "string" && this[key].includes(str)) {
return this[key];
}
}
},
};
//这里的结果就是匹配到的值
console.log("code".replace(obj));//icecode
</script>
Symbol.split
同上,当作为split
的参数时,此属性方法将会自动被调用,传入一个参数,为当前调用split
方法的字符串
js
//这里应该很容易就能看懂,obj作为参数时,调用Symbol.split方法,返回了一'-'拼接的字符串
const obj = {
[Symbol.split](str) {
let strs = "";
for (const s of str) {
strs += s + "-";
}
return strs.substring(0, strs.length - 1);
},
};
console.log("code".split(obj));//c-o-d-e
Symbol.toPrimitive
当进行类型转换时则会自动调用此方法,方法传入一个参数,参数类型只有三种'number'、'string'、'default'
。方法的返回值
- 当使用算数运算符时,类型为
number
- 在模板字符串中时,类型为
string
- 其余类型转换情况下,类型为
default
js
//对象或者类都行
const obj = {
[Symbol.toPrimitive](hint) {
console.log(hint);
return 1;
},
};
//这中算数运算符或者强制性数字转换的情况下,hint的值为number
console.log(obj * 1);
console.log(obj / 1);
console.log(obj - 0);
console.log(+obj);
console.log(Number(obj));
//当在模板字符串中或者强制性字符串转换,hint的值为string
console.log(`${obj}`);
console.log(String(obj));
//只有在这种情况,hint的值才为default
console.log(obj + "");
//这种情况下是不会触发Symbol.toPrimitive方法的
console.log(Array(obj));
console.log(Object(obj));
另外此属性还可以作为Symbol
类型实例的方法调用,将Symbol
类型转换为原始值
js
const name = Symbol("iceCode");
//这里的结果是全等的
console.log(name === name[Symbol.toPrimitive]());//true
Symbol.toStringTag
此方法用于创建自定义字符串描述,在js类型判断中,我们知道有多种类型判断方法,比如typeof、instanceof、constructor
等来判断,但是这些判断方法都不是很全面。有一个判断方法则是非常全面Object.prototype.toString.call()
方法,返回一个json类型的类数组数据,第二个参数则是该元素的类型。
Symbol.toStringTag
方法则是在作为Object.prototype.toString.call()
的参数时会被调用,返回的值就是这个类数组的第二个参数。也就是说Object.prototype.toString.call()
的类型判断非常全面,但是类型是可以被改变的。
js
class Obj {
//当Obj类作为Object.prototype.toString.call参数时,此方法就会被调用,get 是必须要有的
get [Symbol.toStringTag]() {
return "string";
}
}
//正常情况下这里返回的[object Object],但是这里是[object string],改变了它本应该的类型
console.log(Object.prototype.toString.call(new Obj()));//[object string]
//继承了Promise后,返回一个number
class MyP extends Promise {
get [Symbol.toStringTag]() {
return "number";
}
}
//这里只是单独继承一下Promise
class P extends Promise {}
//可以看出两者的类型并不相同
console.log(Object.prototype.toString.call(MyP.resolve()));//[object number]
console.log(Object.prototype.toString.call(P.resolve()));//[object Promise]
另外,在我们判断DOM
类型的时候,也可使用Symbol.toStringTag
快捷显示类型。这种直接读取类型的方式仅只能在DOM
上生效。
js
//获取DOM
const div = document.querySelector("div");
//当我们想要知道这个DOM具体是什么类型,可以间的使用DOM.toString(),则返回json类型的类数组数据
console.log(div.toString());//[object HTMLDivElement]
//我们也可以使用Symbol.toStringTag,这里直接读取即可,直接返回DOM具体类型
console.log(div[Symbol.toStringTag]);//HTMLDivElement
Symbol.unscopables
Symbol.unscopables
用于指定一个对象,该对象的属性名称不会包含在环境绑定中。换句话说,当使用with
语句时,这些属性名称将从with
环境绑定中排除。
这个属性通常用于定义在with
语句中不应被搜索的属性。通过在对象的Symbol.unscopables
属性上设置属性为true
,可以使该属性变得不可作用域(即不会在词法环境变量中出现)。这在某些情况下可以防止属性被意外覆盖或修改。
需要注意的是,Symbol.unscopables
属性在非严格模式下可以使用with
语句。在严格模式下,with
语句将不可用,因此Symbol.unscopables
属性也不会有任何效果。
此外,Symbol.unscopables
属性通常用于自定义对象的行为,以便在with
语句中更好地控制属性的访问和修改。这对于编写库或框架的人来说可能是有用的,但对于大多数日常编程任务来说,可能并不需要直接使用这个属性。
js
const object1 = {
property1: 1,
property2: 2,
};
object1[Symbol.unscopables] = {
//这里设置为true的时候,with语句就不会访问object1.property1了,因为unscopables属性被设置为true了。
property1: true,
property2: false,
};
with (object1) {
console.log(property2);
//这里则会报错,因为with语句不会访问object1.property1
console.log(property1);
}
description
此属性为原型上的属性,它会返回 Symbol
对象的可选描述的字符串。
js
const name = Symbol("iceCode");
const age = Symbol(18);
//这里返回的是Symbol描述,描述的类型全为string类型,不会按原类型返回
console.log(name.description);//iceCode
console.log(age.description);//18
Symbol方法
Symbol.for()
我们知道,Symbol
类型的值永远都不会相等,即使Symbol
的参数一样。Symbol.for()
方法就是为了解决你想使用Symbol
类型数据,但在相同参数的情况下需要相等的问题。
Symbol.for(key)
方法会根据给定的键 key
,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol,并放入全局 symbol 注册表中。
js
//虽然Symbol参数相同,但他们不相等
const name = Symbol("iceCode");
const name1 = Symbol("iceCode");
console.log(name === name1); // false
//创建一个共享的symbol,如果已经存在,则返回该symbol,否则创建一个新的symbol。
const forName = Symbol.for("iceCode");
const forName1 = Symbol.for("iceCode");
//使用for方法后,如果是相等参数,则它们是同一个Symbol
console.log(forName === forName1); // true
//他们类型是相同的
console.log(typeof forName === typeof name);//true
Symbol.keyFor()
此方法是为了寻找在symbol
注册表中关联的键。使用for方法关联的键使用keyFor
则会被返回。
js
//创建一个共享的symbol,如果已经存在,则返回该symbol,否则创建一个新的symbol。
const forName = Symbol.for("iceCode");
//很简单理解 ,就是为了查找for注册的键
console.log(Symbol.keyFor(forName));//iceCode
toString()
toString()
方法返回当前 symbol 对象的字符串表示。
js
const forName = Symbol("iceCode");
//这里如果直接使用Symbol类型的数据加上字符串则会报错
console.log(forName + "to");
js
const forName = Symbol("iceCode");
//toString之后可以正常拼接字符串
console.log(forName.toString() + "to");//Symbol(iceCode)to
valueOf()
valueOf()
方法返回当前 symbol 对象所包含的 symbol 原始值。其他类型一般情况下会进行隐式调用,但是Symbol有些特殊,它不会隐式调用valueOf。所以这里valueOf方法几乎没有任何作用。
结尾
Symbol
类型从出来之后一般业务就很少能够使用到,即使最基本的Symbol(key)
也很少有作为对象键使用的。但对于很多工具库来说,Symbol
类型的出现可能解决了很多不必要的麻烦和减轻了一些被用户使用库时命名冲突问题。
其中的些方法和属性对于我们写业务的人来说,其实并不是很重要,这里以上的内容可以作为了解看一下,可以让你对Symbol有更多的了解(有可能迭代器iterator
会在面试时问到),谢谢。