【红宝书笔记】 数据类型之Symbol类型

103 阅读7分钟

本文内容主要来自《JavaScript高级程序设计(第4版)》章节 3.4.7,并结合MDN文档中的 Symbol 部分、阮一峰的《ECMASctipt 6 入门》章节13进行整理所得。

Xnip2024-04-25_19-54-54.jpg

是一种原始数据类型,表示独一无二的值

const s1 = Symbol('test');
const s2 = Symbol('test');

console.log(s1 === s2); // false
console.log(Symbol() === Symbol()); // false

引入背景:避免对象属性冲突

ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。

Symbol 实例的描述

Symbol() 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述。这主要是为了在控制台显示,或者转为字符串时,比较容易区分。

使用 Symbol.prototype.description 可以返回 Symbol 实例的描述。

作为对象的属性名的 Symbol

凡是可以使用字符串或数值作为属性的地方, 都可以使用 Symbol。 这就包括了对象字面量属性和 Object.defineProperty() / Object.defineProperties() 定义的属性。

不过,作为对象的属性名的 Symbol 值无法被这些方法遍历:for...in、for...of、Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()。

如果需要遍历,可以使用以下方法。

遍历方法:Object.getOwnPropertySmybols()Object.getOwnPropertyDescriptors()Reflect.ownkeys()

Object.getOwnPropertySmybols() 方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

注意,和该方法类似的 Object.getOwnPropertyNames() 只返回对象实例的常规属性数组。这两个方法的返回值彼此互斥。

Object.getOwnPropertyDescriptors() 方法返回同时包含常规和符号属性描述符的对象。

Reflect.ownkeys() 方法返回所有类型的键名,包括常规键名和 Symbol 键名。

由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。

全局注册:Symbol.for()、Symbol.keyFor()

有时,我们希望重新使用同一个 Symbol 值,Symbol.for() 方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。 Symbol.keyFor() 方法返回一个已登记的 Symbol 类型值的key。

注意: Symbol.for()为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。Symbol.for()的这个全局登记特性,可以用在不同的 iframe 或 service worker 中取到同一个值。

iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);

iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')
// true
// 这意味着,iframe 窗口生成的 Symbol 值,可以在主页面得到。

常用内置符号(well-known symbol)

除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。这些常用内置符号(well-known symbol)用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。

这些内置符号最重要的用途之一是重新定义它们, 从而改变原生结构的行为。

注意: 在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@iterator 指的就是 Symbol.iterator。

Symbol.asyncIterator

这个符号作为一个属性表示“一个方法,该方法返回对象默认的 AsyncIterator。 由 for-await-of 语句使用”。换句话说,这个符号表示实现异步迭代器 API 的函数。

Symbol.iterator

这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。 由 for-of 语句使用”。换句话说,这个符号表示实现迭代器 API 的函数。

Symbol.hasInstance

这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由 instanceof 操作符使用”。

Symbol.species

这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。

举几个例子:

// 默认行为
class MyArray1 extends Array {
}

const a = new MyArray1(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);

b instanceof MyArray1 // true
c instanceof MyArray1 // true

// 修改默认行为后
class MyArray2 extends Array {
  static get [Symbol.species]() { return Array; }
}

const a = new MyArray2();
const b = a.map(x => x);

b instanceof MyArray2 // false
b instanceof Array // true

// 另一个例子
// T2定义了Symbol.species属性,T1没有。
// 结果就导致了创建衍生对象时(then方法),
// T1调用的是自身的构造方法,而T2调用的是Promise的构造方法。
class T1 extends Promise {
}

class T2 extends Promise {
  static get [Symbol.species]() {
    return Promise;
  }
}

new T1(r => r()).then(v => v) instanceof T1 // true
new T2(r => r()).then(v => v) instanceof T2 // false

总之,Symbol.species 的作用在于,实例对象在运行过程中,需要再次调用自身的构造函数时,会调用该属性指定的构造函数。它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。

Symbol.isConcatSpreadable

作用:用于配置某对象作为 Array.prototype.concat() 方法的参数时是否展开其数组元素。 对不同数据有不同的默认行为:

  • 对于数组,默认展开,但Symbol.isConcatSpreadable默认等于undefined
  • 对于类数组 (array-like) 对象,默认不展开。期望展开其元素用于连接,需要设置 Symbol.isConcatSpreadable 为 true。

Symbol.match

有 2 种作用:

  1. 重新定义从 String 继承的 match() 方法的行为

    class MyMatcher {
      [Symbol.match](string) {
        return 'hello world'.indexOf(string);
      }
    }
    
    'e'.match(new MyMatcher()) // 1
    
  2. 指定了匹配的是正则表达式而不是字符串

    // 默认行为:未修改 Symbol.match
    "/bar/".startsWith(/bar/);
    // Throws TypeError,因为 /bar/ 是一个正则表达式
    
    // 修改 Symbol.match 为 false,
    // 表示将 re 视为字符串而不是正则表达式
    var re = /foo/;
    re[Symbol.match] = false;
    "/foo/".startsWith(re); // true
    "/baz/".endsWith(re); // false
    

Symbol.replace

这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符 串中匹配的子串。由 String.prototype.replace()方法使用”。

Symbol.search

这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中 匹配正则表达式的索引。由 String.prototype.search()方法使用”。

Symbol.split

这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表 达式的索引位置拆分字符串。 由 String.prototype.split() 方法使用”。

Symbol.toPrimitive

指定了一种接受首选类型并返回对象原始值的表示的方法。它被所有的强类型转换制算法优先调用。

很多内置操作都会尝试强制将对象转换为原始值,包括字符串、 数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的 Symbol.toPrimitive 属性 上定义一个函数可以改变默认行为。

该函数被调用时,会被传递一个字符串参数 hint,表示要转换到的原始值的预期类型。hint 参数的取值是 "number"、"string" 和 "default" 中的任意一个。

let obj = {
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return 123;
      case 'string':
        return 'str';
      case 'default':
        return 'default';
      default:
        throw new Error();
     }
   }
};

2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'

Symbol.toStringTag

这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法 Object.prototype.toString()使用”。

也就是说,这个属性可以用来定制 [object Object] 或 [object Array] 中 object 后面的那个大写字符串。

ES6 新增内置对象的Symbol.toStringTag属性值如下;

  • JSON[Symbol.toStringTag]:'JSON'
  • Math[Symbol.toStringTag]:'Math'
  • Module 对象M[Symbol.toStringTag]:'Module'
  • ArrayBuffer.prototype[Symbol.toStringTag]:'ArrayBuffer'
  • DataView.prototype[Symbol.toStringTag]:'DataView'
  • Map.prototype[Symbol.toStringTag]:'Map'
  • Promise.prototype[Symbol.toStringTag]:'Promise'
  • Set.prototype[Symbol.toStringTag]:'Set'
  • %TypedArray%.prototype[Symbol.toStringTag]:'Uint8Array'等
  • WeakMap.prototype[Symbol.toStringTag]:'WeakMap'
  • WeakSet.prototype[Symbol.toStringTag]:'WeakSet'
  • %MapIteratorPrototype%[Symbol.toStringTag]:'Map Iterator'
  • %SetIteratorPrototype%[Symbol.toStringTag]:'Set Iterator'
  • %StringIteratorPrototype%[Symbol.toStringTag]:'String Iterator'
  • Symbol.prototype[Symbol.toStringTag]:'Symbol'
  • Generator.prototype[Symbol.toStringTag]:'Generator'
  • GeneratorFunction.prototype[Symbol.toStringTag]:'GeneratorFunction'

Symbol.unscopables(不推荐使用)

这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性, 都会从关联对象的 with 环境绑定中排除”。

注意:由于不推荐使用 with,因此也不推荐使用 Symbol.unscopables。