一、简介

​ Symbol 类型是ES6新引入的一种基本数据类型,该类型具有静态属性和静态方法。其中静态属性暴露了几个内建的成员对象,静态方法暴露了全局的 Symbol 注册。Symbol 类型具有以下特性:

  • 唯一性:每个 Symbol 值都是唯一的。
  • 不可变性:Symbol 值是不可被修改的。
  • 无法强制转换:Symbol可以与其他数据类型进行运算,但不能被强制转换为其他数据类型。

​ Symbol 类型在许多库和框架中被广泛应用,常见的应用场景有创建私有属性、定义常量、定义事件名称以及实现各种标识符相关的功能。

二、创建

1、Symbol([description])

​ 通过Symbol([description])函数创建 Symbol 类型的值,但创建的 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 类型的一个内置静态属性,属性值是一个 Symbol 值,用于定义数组的默认的迭代器@@iterator@@iterator 方法是一个特殊的内部方法,用于返回一个迭代器对象。

​ 当一个数组使用 for...of 循环或扩展运算符 (...)进行遍历时,会调用该数组的 @@iterator 方法来获取一个迭代器对象。迭代器对象可以通过调用其 next() 方法来依次访问数组的每个元素。我们可以通过自定义 @@iterator 方法来实现自定义迭代器的行为,但自定义的迭代器只会影响 for...of 循环或扩展运算符 (...)。

​ 自定义的 @@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 转换方法的键。 @@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,因为该方法只在全局 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 数据中寻找寻找对应的值,如果这个值存在,则返回该 symbol 的 key 值;如果值不存在或者该 symbol 对应的 key 为空,则返回 undefined

​ 如果参数 symbol 是通过 Symbol() 创建的数据,则不会被查找到,因为该方法创建 symbol 数据并非全局的。

// 创建一个全局 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() 方法,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 可以用作对象属性的键,即属性名,可以确保属性名的唯一性,避免属性被意外覆盖或修改。

// 声明一个 symbol 数据
const a = Symbol('aaa');
// 声明一个全局 symbol 数据
const b = Symbol.for('bbb')
// 声明对象 利用 symbol 数据作为属性名
const obj = {
    [a]: '11111111',
    a: '22222222',
    [b]: '33333333'
};
// 再次声明一个 symbol 数据 与 变量a 有相同的description
// 以该symbol作为属性名 并修改数据 
// 虽然声明时的 description 相同 但这是两个不同的 symbol 数据 
// 所以obj[Symbol('aaa')] 与 obj[a] 是两个不同的属性
// 两者相互独立 修改其中一个不会影响另一个
obj[Symbol('aaa')] = '44444444';
// 输出属性值
console.log(obj[a]); // 11111111
// 输出对象的数据
console.log(obj);

// 可通过 symbol 数据作为 key 修改数据
// obj[a] = '555555';
执行结果:

在这里插入图片描述

2、声明唯一的常量

借助 Symbol 数据的唯一性,可以用于声明常量,并确保常量值的唯一性,防止重复。

const COLORS = {
    RED: Symbol('red'),
    GREEN: Symbol('green'),
    BLUE: Symbol('blue')
};

function getColorName(color) {
    switch (color) {
      case COLORS.RED:
        return '红色';
      case COLORS.GREEN:
        return '绿色';
      case COLORS.BLUE:
        return '蓝色';
      default:
        return '未知颜色';
    }
}

console.log(getColorName(COLORS.RED)); // 红色
console.log(getColorName(Symbol('red'))); // 未知颜色

3、改写对象的内置方法

通过利用 Symbol 和 内置属性,可以自定义对象内置属性和方法的行为,以适应特定的业务场景。但在改写内置方法时不能破坏原有的语言规范和其他代码的预期行为。

// 声明一个对象 并改写其内置迭代器
const myObj = {
  [Symbol.iterator]() {
    let i = 0;
    return {
      next() {
        return { value: i++, done: i > 5 };
      }
  };
};
// 调用该对象的迭代器
for (const num of myIterable) {
  console.log(num); // 输出:0, 1, 2, 3, 4
}

4、实现单例模式

利用Symbol 的唯一性,能够确保一个类只被实例化一次。

const INSTANCE = Symbol('instance');

class Singleton {
  constructor() {
    if (!Singleton[INSTANCE]) {
      Singleton[INSTANCE] = this;
    }
    return Singleton[INSTANCE];
  }
}

const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // 输出:true

5、模拟类的私有属性/方法

利用Symbol 的唯一性结合闭包,可以模拟实现私有属性/方法。

// 利用立即执行函数 构建闭包
const MyClass = (() => {
   // 定义在闭包内部 外部无法访问
   const privateKey = Symbol('private');
   // 返回一个类
   return class {
      constructor() {
        this[privateKey] = '私有值';
      }
      getPrivate() {
        return this[privateKey];
      }
      setPrivate(value) {
        this[privateKey] = value;
      }
    };
})();
// 实例化类
const obj = new MyClass();
console.log(obj.getPrivate()); // 私有值
console.log(obj[Symbol('private')]); // undefined
obj.setPrivate('新值');
console.log(obj.getPrivate()); // 新值

6、其他用法。。。

六、参考资料

Symbol

请关注公众号,优先查看更多优质资源:

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐