一、简介
symbol
类型是ES6新引入的一种基本数据类型,该类型具有静态属性和静态方法。其中静态属性暴露了几个内建的成员对象,静态方法暴露了全局的symbol
注册。
symbol
类型具有以下特点:① 唯一性:每个symbol
值都是唯一的;② 不可变性:symbol
值是不可被修改的;③ 属性标识符:symbol
类型的值可以作为对象属性的标识符key
;④Symbol可以与其他数据类型进行运算,但不能被强制转换为其他数据类型。利用前两个特性,可以避免属性名的冲突和保证属性不会被意外覆盖。
symbol
类型在许多库和框架中被广泛应用,常见的应用场景有创建私有属性、定义常量、定义事件名称以及实现各种标识符相关的功能。
二、创建
1、Symbol([description])
通过Symbol([description])
函数创建symbol
类型的值,但并不会被添加到全局symbol表中,可选参数description
,是字符串类型的数据,表示对当前symbol
的描述,该参数只用来作为标识,并不会影响symbol
的唯一性。
但该函数并不是一个构造函数,因为该函数不支持new Symbol()
的语法,如果使用该语法将会抛出TypeError
错误。每个通过Symbol()
函数获取的symbol
类型的值都是唯一的、独一无二的,即使两个symbol
拥有相同的description
,它们也属于两个不同的值。
// 创建symbol数据
let s = Symbol();
console.log(s); // Symbol()
console.log(typeof s); // symbol
// 创建symbol数据并添加描述
let s1 = Symbol('one');
console.log(s1); // Symbol(one)
console.log(typeof s1); // symbol
// 创建symbol数据并添加相同的描述
let s2 = Symbol('one');
console.log(s2); // Symbol(one)
console.log(typeof s2); // symbol
// 判断symbol数据是否相等
console.log(s1 === s2); // false
console.log(s1 == s2); // false
// 使用new关键字创建symbol数据
let s3 = new Symbol(); // Uncaught TypeError: Symbol is not a constructor
2、Symbol.for(key)
第四部分,常用方法章节中讲解。
三、常用属性
1、description(只读)
该属性是一个只读属性,用于获取当前symbol
值的描述字符串。
案例代码:
// 创建symbol数据 不添加描述
let s = Symbol();
// 创建symbol数据并添加描述
let s1 = Symbol('one');
// 使用description输出symbol数据的描述
console.log(s.description); // undefined
console.log(s1.description); // one
2、hasInstance
该属性是 Symbol
类型的一个内置静态属性,属性值是一个 Symbol 值,它是不可变的,用于定义对象的 @@hasInstance
方法的键,@@hasInstance
方法用于判断一个对象是否为某个构造函数的实例,我们可以利用该属性自定义instanceof
操作符在某个类上的行为。
当一个对象被使用 instanceof
运算符检查其原型链时,会调用该对象的 @@hasInstance
方法。换句话说,obj instanceof Constructor
实际上是调用了 Constructor[Symbol.hasInstance](obj)
。因此我们可以通过自定义的 @@hasInstance
方法,来自定义判断对象是否为某个构造函数的实例。
案例代码:
class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
console.log([] instanceof MyArray); // true
console.log({} instanceof MyArray); // fales
3、isConcatSpreadable
该属性是 Symbol
类型的一个内置静态属性,属性值是一个 Symbol 值,它是不可变的,用于定义对象的 @@isConcatSpreadable
的键。@@isConcatSpreadable
方法是一个内部Symbol,用于确定对象在调用数组的 concat()
方法时是否展开其元素。
如果@@isConcatSpreadable
返回true
(默认值),则对象元素被展开合并;如果方法返回false
,则将对象作为单个元素添加到数组中,构成多维数组的形式。我们可以通过设置对象的 @@isConcatSpreadable
来自定义对象在 concat()
方法中的展开行为
案例代码:
let arr1 = [1, 2, 3]
let arr2 = ['a', 'b', 'c']
// 默认为true 进行展开合并
console.log(arr1.concat(arr2)); // [1, 2, 3, 'a', 'b', 'c']
// 设置arr2 的值为false
arr2[Symbol.isConcatSpreadable] = false
// 再次合并 此时不会展开
console.log(arr1.concat(arr2)); // [1, 2, 3, ['a', 'b', 'c']]
4、iterator
Symbol.iterator
是 Symbol 类型的一个内置静态属性,属性值是一个 Symbol 值,用于定义对象的默认的迭代器@@iterator
。 @@iterator
方法是一个特殊的内部方法,用于返回一个迭代器对象。
当一个对象使用 for...of
循环或扩展运算符 (...
)进行遍历时,会调用该对象的 @@iterator
方法来获取一个迭代器对象。迭代器对象可以通过调用其 next()
方法来依次访问对象的每个元素。我们可以通过自定义 @@iterator
方法来实现自定义的迭代器行为。自定义的 @@iterator
方法应该返回一个具有 next()
方法的迭代器对象。每次调用 next()
方法时,迭代器对象应该返回一个包含 value
和 done
属性的对象,value
表示当前迭代的值,done
表示迭代是否结束。
案例代码:
let arr3 = [1, 2, 3]
// for..of..形式遍历
console.log('for..of..形式遍历');
for (const item of arr3) {
console.log(item);
}
// ... 扩展运算符形式遍历
console.log('扩展运算符形式遍历');
console.log([...arr3]);
// 自定义默认迭代器
arr3[Symbol.iterator] = function () {
let index = 0;
return {
next: function () {
if (index < arr3.length) {
return { value: arr3[index++] * 3, done: false };
} else {
return { value: undefined, done: true };
}
}
};
};
// 输出数组本身
console.log('输出数组本身');
console.log(arr3);
// for..of..形式遍历
console.log('for..of..形式遍历');
for (const item of arr3) {
console.log(item);
}
// ... 扩展运算符形式遍历
console.log('扩展运算符形式遍历');
console.log([...arr3]);
// forEach 形式遍历
console.log(' forEach 形式遍历');
arr3.forEach(item => {
console.log(item);
});
// for 形式遍历
console.log('for 形式遍历');
for (let i = 0; i < arr3.length; i++) {
console.log(arr3[i]);
}
执行结果:
5、toPrimitive
Symbol.toPrimitive
是 Symbol 类型的一个内置静态属性,用于定义对象的 @@toPrimitive
转换方法的键。 @@toPrimitive
转换方法是一个内部方法,用于将对象转换为一个原始值,它接受一个参数 hint
,表示预期的转换类型。hint
参数可以是以下三个值之一:
"default"
:表示该对象可以被转换为任意类型的原始值,例如:"" + x
(强制转为原始值,而不是字符串)。"number"
:表示该对象被期望转换为数字类型的原始值,例如:算术运算(+-*/
)。"string"
:表示该对象被期望转换为字符串类型的原始值,例如模板字符串(${}
)、String()
等。
参数 hint
的值根据上下文和操作的需要等诸多复杂因素来决定,如:调用对象的运算符、隐式类型转换、valueOf
和 toString
方法等等。
当一个对象被用于需要原始值的上下文中,例如进行算术运算或字符串拼接时,JavaScript 引擎会首先查找对象的 Symbol.toPrimitive
属性。如果该属性存在并且是一个函数,引擎将调用该函数,并传入相应的 hint
参数,转换获取对象的原始值,即字符串、数字或布尔值。我们可以通过自定义 @@toPrimitive
方法来自定义对象的转换行为,完全控制原始转换过程,并返回对象的原始值。
案例代码:
// 一个没有提供 Symbol.toPrimitive 属性的对象
const obj1 = {};
console.log(+obj1); // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ""); // "[object Object]"
// 接下面声明一个对象,手动赋予了 Symbol.toPrimitive 属性
const obj2 = {};
obj2[Symbol.toPrimitive] = function (hint) {
// hint 参数值是 "number"
if (hint === "number") {
// 返回对象有多少条属性
return Object.keys(obj2).length;
}
// hint 参数值是 "string"
if (hint === "string") {
// 返回对象转换成的JSON字符串
return JSON.stringify(obj2);
}
// hint 参数值是 "default"
return true;
};
console.log(+obj2); // 0
console.log(`${obj2}`); // "{}"
console.log(obj2 + ""); // "true"
6、match、replace、search、toStringTag等其他属性
请自行了解。。。
四、常用方法
1、for()
Symbol.for(key)
方法会根据参数key
,从所有声明的全局symbol数据中寻找对应的值(不包括通过Symbol()
创建的数据),如果这个值存在,则返回它;如果不存在,则新建一个以这个key
为description
的全局symbol数据,并将创建的数据返回。
如果先使用Symbol()
创建了以这个key
为description
的数据,然后再使用该方法进行查找,则也不会被查找到,因为该方法只在全局symbol
数据中进行查找,而Symbol()
创建的是非全局的数据。
// 第一次使用for()方法 由于之前不存在以foo为key的symbol 所以创建一个 symbol 并放入 symbol 注册表中,键为 "foo"
const a = Symbol.for("aaa");
// 第二次使用for()方法 由于之前注册过 所以直接从 symbol 注册表中读取键为"foo"的 symbol
const b = Symbol.for("aaa");
// 验证两者是否为同一个symbol
console.log(a === b); // true
// 如果是通过Symbol()方法创建 则会创建两个key相同的symbol 但并不是同一个symbol
const c = Symbol("bbb");
const d = Symbol("bbb");
// 验证两者是否为同一个symbol
console.log(c === d); // false
2、keyFor()
Symbol.keyFor(symbol)
方法会根据参数symbol
,从所有声明的全局symbol
数据中寻找该数据,并返回该数据的key
。如果从全局symbol
数据中查找到该 symbol
,则返回该 symbol
的 key
值,返回值为字符串类型;如果不存在该symbol
或者该symbol
对应的key
为空,则返回 undefined
。
如果参数symbol
是通过Symbol()
创建的数据,则也不会被查找到,因为该方法创建symbol
数据并非全局的,返回值为undefined
。
// 创建一个全局 Symbol 且有key
const a = Symbol.for("aaa");
console.log(Symbol.keyFor(a)); // "aaa"
// 创建一个全局 Symbol 但没有key
const b = Symbol.for();
console.log(Symbol.keyFor(b)); // undefined
// 创建一个非全局 Symbol 且有key
const c = Symbol("ccc");
// 创建一个全局的 Symbol 且有相同的key
const c2 = Symbol.for("ccc");
console.log(Symbol.keyFor(c)); // undefined
console.log(Symbol.keyFor(c2)); // "ccc"
// 验证两者是否为同一个symbol
console.log(c === c2); // false
// 下面的 原生Symbol 没有保存在全局 Symbol 注册表中
console.log(Symbol.keyFor(Symbol.iterator)); // undefined
3、toString()
Symbol
对象拥有自己的toString()
方法,覆盖了原型链上的Object.prototype.toString()
方法。
symbol数据不能隐式转换为字符串,因此需要toString()
方法,将数据转换成字符串。
// 创建一个symbol
const a = Symbol("aaa");
console.log(a.toString()); // Symbol(aaa)
// 创建一个全局symbol
const b = Symbol.for("bbb");
console.log(b.toString()); // Symbol(bbb)
4、valueOf()等其他方法
请自行了解。。。
五、相关应用
1、作为对象唯一的属性键
Symbol
可以用作对象属性的键,确保属性key
的唯一性,避免属性被意外覆盖或冲突。
// 声明一个 symbol 数据
const a = Symbol('aaa');
// 声明一个全局 symbol 数据
const b = Symbol.for('bbb')
// 声明对象 利用 symbol 数据作为 key
const obj = {
[a]: '11111111',
a: '22222222',
[b]: '33333333'
};
// 再次声明一个 symbol 数据 与 a 有相同的description
// 以该symbol作为key 修改数据 虽然声明时的 description 相同
// 但是是两个不同的 symbol 数据 所以obj[Symbol('aaa')] 与 obj[a] 是两个不同的属性
// 修改其中一个不会影响另一个
obj[Symbol('aaa')] = '44444444';
// 输出属性数据
console.log(obj[a]); // 11111111
// 此处相当于又声明了一个symbol
console.log(obj[Symbol('aaa')]); // undefined
// 再次输出对象的数据
console.log(obj);
// 可通过 symbol 数据作为 key 修改数据
// obj[a] = '44444444';
执行结果:
2、声明唯一的常量
借助Symbol
数据的唯一性,我们可以声明常量,并确保常量值的唯一性,而且不会被意外修改或覆盖。
// 声明一个常量 且常量值为 symbol 数据
const a = Symbol("aaa");
// 在函数中匹配常量值
function myFunction(value) {
// 判断传入的值是否与常量值相等
if (value === a) {
console.log("常量值被匹配");
} else {
console.log("常量值未匹配");
}
}
// 传入定义的变量
myFunction(a); // 输出 "常量值被匹配"
// 传入一个新的 symbol 数据 且 description 与常量值相同
myFunction(Symbol("aaa")); // 输出 "常量值未匹配"
3、改写对象的内置方法
通过利用Symbol和内置属性,我们可以改写对象的内置方法,以适应特定的业务场景,并自定义对象的行为。但在改写内置方法时不能破坏原有的语言规范和其他代码的预期行为。
// 声明一个对象 并改写其内置toString方法的返回值
const myObj = {
// 该Symbol的内置属性 决定了toString方法的返回值
[Symbol.toStringTag]: "MyObject",
};
// 输出对象的toString方法的返回值
console.log(Object.prototype.toString.call(myObj)); // 输出 "[object MyObject]"
4、定义类的私有成员
Symbol
可以在类中作为私有成员的标识符。
// 定义一个symbol数据
const _privateMember = Symbol("private");
// 定义一个类
class MyClass {
constructor() {
// 定义类的私有成员 以symbol数据作为标识符
this[_privateMember] = "私有成员";
}
// 私有成员的get方法
getPrivateMember() {
return this[_privateMember];
}
}
// new 一个实体对象
const instance = new MyClass();
// 通过get方法正常访问
console.log(instance.getPrivateMember()); // 输出 "私有成员"
// 直接访问 无法访问
console.log(instance[_privateMember]); // 输出 undefined
5、其他用法。。。
六、参考资料
Symbol