跳至主要內容

Symbols

樱桃茶大约 18 分钟

Symbols

Symbols in TypeScript 是一种唯一且不可变的数据类型,它经常用来作为对象属性的键。在 JavaScript 中 Symbol 是 ES2015 引入的原始数据类型,TypeScript 作为 JavaScript 的超集也支持这个特性。

  • 创建 Symbols

    • 使用 Symbol() 函数创建一个新的 Symbol。每次调用都会生成一个独一无二的值。
    let sym1 = Symbol();
    let sym2 = Symbol("key"); // 可选的字符串key参数可以用于描述符号。
    
  • Symbols 作为对象属性的键

    • Symbols 可以作为对象属性的键使用,确保不会和其他属性名发生冲突。
    let myObj = {
      [sym1]: "value for sym1",
      [sym2]: "value for sym2",
    };
    console.log(myObj[sym1]); // 输出: value for sym1
    
  • 共享 Symbols - Symbol.for() 和 Symbol.keyFor()

    • Symbol.for(key) 方法可以创建 Symbol,并将其存储在全局 Symbol 注册表中。如果该 key 已存在,则返回已有的 Symbol。
    • Symbol.keyFor(sym) 方法用来获取 Symbol 注册表中与某个 Symbol 关联的键。
    let sym3 = Symbol.for("app.key");
    let sym4 = Symbol.for("app.key");
    console.log(sym3 === sym4); // 输出: true,因为是从全局注册表中获得的同一个 Symbol
    
    let key = Symbol.keyFor(sym3);
    console.log(key); // 输出: app.key
    
  • Symbol 类型的注意点

    • Symbol 类型不能与其他类型进行算数运算。
    • Symbols 大多情况下是匿名的,除非给它们提供一个描述(在创建时作为参数传入),但即使有描述,不同的 Symbols 也是不相等的。
    • 在使用 JSON.stringify() 转换含有 Symbol 属性的对象时,Symbol 键会被完全忽略,不会包含在内。

通过上述例子可以看出,Symbols 提供了一种方式来生成一个保证不会与任何其他属性名冲突的标识符。这对于定义一些不需要外部访问或者依赖唯一性的属性是非常有用的。在学习 TypeScript 时,理解并能够适当地使用 Symbol 类型,能够帮助你更好地管理和维护你的代码,尤其是在处理大型项目或库时避免名称冲突。

众所周知的 Symbols

众所周知的 Symbols

在 JavaScript 中,Symbol是一种原始数据类型,它提供了一个独一无二的标识符。TypeScript 同样支持这种类型,而“众所周知的 Symbols”(Well-known Symbols)是 ES6 引入的一组具有特殊意义的 Symbols。这些 Symbols 用于表示语言内部的行为。

以下是一些众所周知的 Symbols 的使用场景:

  • Symbol.iterator

    • 用于定义一个对象的默认迭代器。当一个对象被for...of循环时,会查找该对象的Symbol.iterator属性。
    let array = [1, 2, 3];
    for (let value of array) {
      console.log(value); // 1, 2, 3
    }
    // 数组有默认的Symbol.iterator实现
    
  • Symbol.asyncIterator

    • 类似于Symbol.iterator,但用于定义对象的异步迭代器。当使用异步迭代如for await...of时,会使用到。
    async function* generateNumbers() {
      yield 1;
      yield 2;
    }
    
    async function printAsyncIterable() {
      for await (let number of generateNumbers()) {
        console.log(number); // 1, 2
      }
    }
    
    printAsyncIterable();
    
  • Symbol.toStringTag

    • 修改对象的toString方法返回值中的"[object XXX]"部分。
    class MyArray {
      get [Symbol.toStringTag]() {
        return "MyArray";
      }
    }
    
    let myArray = new MyArray();
    console.log(Object.prototype.toString.call(myArray)); // [object MyArray]
    
  • Symbol.species

    • 控制构造函数创建派生对象时使用的构造函数。
    class MyArray extends Array {
      static get [Symbol.species]() {
        return Array;
      }
    }
    
    let a = new MyArray(1, 2, 3);
    let mapped = a.map((x) => x * x);
    
    console.log(mapped instanceof MyArray); // false
    console.log(mapped instanceof Array); // true
    

这些 Symbols 使得开发者能够自定义或改变某些语言行为,为高级编程模式和 API 设计提供了更多的灵活性和控制力。理解并合理利用这些特殊 Symbols,可以帮助你写出更加强大和灵活的代码。

Symbol.hasInstance

Symbol.hasInstance 是一个特殊的内置 Symbol,它可以被用来定制 instanceof 操作符的行为。在 JavaScript 中,instanceof 运算符用于测试构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

以下是关于 Symbol.hasInstance 的一些重点:

  • 当你在代码中使用 object instanceof Constructor 时,JavaScript 引擎会去检查该构造器(Constructor)的 Symbol.hasInstance 属性。

  • 如果存在这个属性,它必须是一个函数,这个函数接受一个参数:即左侧的对象(object),并返回一个布尔值表示该对象是否为构造函数的实例。

  • 使用 Symbol.hasInstance 可以自定义 instanceof 行为,而不只是简单地检查原型链。

下面是一些使用 Symbol.hasInstance 的例子:

class MyArray {
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}

// 由于我们自定义了[Symbol.hasInstance],现在我们的MyArray类将认为所有数组都是它的实例
console.log([] instanceof MyArray); // true

let myArrayInstance = new MyArray();
console.log(myArrayInstance instanceof MyArray); // false, 因为实际上没有处理自身实例的情况

在这个例子中,我们定义了一个名为 MyArray 的类,并自定义了 [Symbol.hasInstance] 方法。此方法检查传入的实例是否是数组,而不是检查它是否是 MyArray 类的实例。因此,任何数组现在都被认为是 MyArray 类的实例。

function MyObject() {}

Object.defineProperty(MyObject, Symbol.hasInstance, {
  value: function (v) {
    return v instanceof Object && v.hasOwnProperty("specialProperty");
  },
});

const myInstance = { specialProperty: "specialValue" };

console.log(myInstance instanceof MyObject); // true,因为myInstance具有'specialProperty'

在第二个例子中,我们定义了一个函数 MyObject 并且用 defineProperty 方法覆写了 MyObjectSymbol.hasInstance 方法,使得其行为变为检查对象是否继承自 Object 并且拥有一个名为 'specialProperty' 的属性。

总之,通过自定义 Symbol.hasInstance,开发者可以控制 instanceof 这个操作符的语义,使其适应更复杂或非标准的类型检查需求。

Symbol.isConcatSpreadable

Symbol.isConcatSpreadable 是一个特殊的符号(Symbol)值,它被用于配置某个对象作为 Array.prototype.concat() 方法的参数时的行为。在 JavaScript 中,concat 方法通常用于连接两个或多个数组,返回一个新的数组。

  • 如果一个对象有 Symbol.isConcatSpreadable 属性,并且这个属性的值是 true,那么在使用 concat 方法连接数组时,该对象会被展开其元素。
  • 默认情况下,数组对象是可以展开的(isConcatSpreadable 默认为 true),但是其他类似数组的对象则不会展开,除非手动设置这个属性。
  • 相反,如果你不希望数组在使用 concat 时被展开,可以将其 Symbol.isConcatSpreadable 设置为 false

以下是一些例子:

// 数组默认是可以展开的
let alpha = ["a", "b"];
let numeric = [1, 2, 3];
let combined = alpha.concat(numeric);
console.log(combined); // 输出: ['a', 'b', 1, 2, 3]

// 类似数组的对象默认不会展开
let arrayLikeObject = { 0: "c", 1: "d", length: 2 };
combined = alpha.concat(arrayLikeObject);
console.log(combined); // 输出: ['a', 'b', { 0: 'c', 1: 'd', length: 2 }]

// 显式设置 Symbol.isConcatSpreadable 来使类似数组的对象能够展开
arrayLikeObject[Symbol.isConcatSpreadable] = true;
combined = alpha.concat(arrayLikeObject);
console.log(combined); // 输出: ['a', 'b', 'c', 'd']

// 将数组的 isConcatSpreadable 属性设置为 false,则不会展开
let nonSpreadableArray = ["c", "d"];
nonSpreadableArray[Symbol.isConcatSpreadable] = false;
combined = alpha.concat(nonSpreadableArray);
console.log(combined); // 输出: ['a', 'b', ['c', 'd']] - 数组没有被展开,而是作为单独的项添加

对于初学 TypeScript 的学生来说,了解 Symbol.isConcatSpreadable 更多是关于了解 JavaScript 的高级功能和如何通过 TypeScript 进行类型安全的编程。Symbols 在 TypeScript 中仍然是符号数据类型,TypeScript 提供了更严格的类型检查和接口定义功能来辅助开发。记住,在 TypeScript 中使用 Symbols 或任何新的 ECMAScript 特性,你需要确保你的编译目标设置正确,以便生成兼容旧环境的代码。

Symbol.iterator

  • Symbol.iterator 是一个众所周知的 Symbol,它用于定义一个对象的默认迭代器。当你想要一个对象能够被 for...of 循环遍历时,这个 Symbol 就非常有用。

  • 在 JavaScript(和 TypeScript)中,数组和许多内置类型(如 MapSet)已经内置了对 Symbol.iterator 的支持,意味着它们是“可迭代的”。

  • 当你使用 for...of 循环来遍历这些可迭代对象时,循环内部会自动调用对象的 Symbol.iterator 方法,从而获取到一个迭代器(Iterator),该迭代器允许逐一访问集合中的元素。

  • 自定义对象也可以通过定义自己的 Symbol.iterator 方法来成为可迭代的。这样做可以使得对象兼容 for...of 循环和其他期望可迭代接口的语言特性,比如扩展运算符(...)。

示例:

class MyNumbers {
  // 定义类的起始值和结束值
  constructor(public start: number, public end: number) {}

  // 实现 Symbol.iterator 方法
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;

    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        } else {
          return { done: true };
        }
      },
    };
  }
}

// 使用 for...of 循环遍历 MyNumbers 实例
const myRange = new MyNumbers(1, 5);
for (let num of myRange) {
  console.log(num); // 依次输出:1, 2, 3, 4, 5
}
  • 在这个例子中,MyNumbers 类实现了一个 Symbol.iterator 方法,使得其实例变成可迭代对象。这意味着你可以在 MyNumbers 实例上使用 for...of 循环,正如数组或其他内置可迭代对象一样。

  • 通过实现 Symbol.iterator,你可以定制对象的迭代行为,这在处理复杂数据结构或者需要特定迭代逻辑的场景下非常有用。

Symbol.match

Symbol.match 是一个在 JavaScript 中的众所周知的(well-known)Symbol,它被用于定义一个匹配操作。

  • 当一个对象被 String.prototype.match 方法使用时,这个方法会调用对象的 Symbol.match 属性,如果存在的话。
  • 这个属性指向一个函数,该函数作为正则表达式的匹配算法。

例子: 自定义匹配行为

假设你想创建一个自定义对象,让它能够使用字符串的 match 方法进行匹配操作,你可以给这个对象添加一个 Symbol.match 函数:

let myMatcher = {
  [Symbol.match](string) {
    return string.length; // 简单地返回字符串长度,而不是真正的匹配
  },
};

console.log("Hello, world!".match(myMatcher)); // 输出: 13

在上面的例子中:

  • 创建了一个对象 myMatcher
  • 给这个对象定义了一个方法 [Symbol.match],这个方法仅返回传入字符串的长度。
  • 当使用 'Hello, world!'.match(myMatcher) 时,实际上调用的是 myMatcher[Symbol.match],并将 'Hello, world!' 传递给这个方法。
  • 输出结果是 13,因为 'Hello, world!' 这个字符串的长度是 13。

注意,通常 Symbol.match 应关联到一个更复杂的匹配逻辑,例如使用正则表达式来确定字符串内的特定模式。上面的例子仅用来说明如何自定义 Symbol.match 行为。

Symbol.replace

Symbol.replace 是一个众所周知(well-known)的 Symbol,它指定了一个方法,该方法会被 String.prototype.replace 方法调用去替换某些字符。这个特性允许你自定义在字符串中进行替换操作时是如何替换的。

当你使用 String.prototype.replace 方法时,例如 "hello".replace("l", "x"),JavaScript 引擎内部会查找和调用被替换值的 Symbol.replace 方法。如果被替换值不含有这样的方法,则会按照默认行为执行。

使用 Symbol.replace 可以让你重写默认的替换逻辑,并且自定义如何处理替换。下面用几个实例来说明:

  • 基本使用Symbol.replace的例子

    class ReplaceWithDash {
      [Symbol.replace](string, replacement) {
        return string.split("").join(replacement);
      }
    }
    
    let result = "hello".replace(new ReplaceWithDash(), "-");
    console.log(result); // h-e-l-l-o
    

    在这个例子中,我们创建了一个名为ReplaceWithDash的类,并实现了Symbol.replace方法。这个方法简单地把传入的字符串分割成单个字符并用传入的替换字符连接起来。接下来我们用这个类的实例作为.replace()方法的第一个参数,这将触发我们定义的替换逻辑,输出结果是用 - 连接的 'h-e-l-l-o'

  • 考虑正则表达式的Symbol.replace示例

    let re = /-/g;
    re[Symbol.replace] = function (str, replacement) {
      return str.split(this).join(replacement);
    };
    
    let result = "123-456-789".replace(re, ":");
    console.log(result); // "123:456:789"
    

    这里我们修改了正则表达式对象reSymbol.replace属性,使得每次使用这个正则对象进行替换操作时,都会调用我们提供的函数。这个函数将给定的字符串以正则匹配的模式分割,并用所提供的替换字符串来连接它们。因此,当我们用 ":" 来替换 "123-456-789" 中的所有 "-" 时,结果是 "123:456:789"

  • ECMAScript 2015(又名 ES6)引入了Symbol类型,旨在创建一个独一无二的值。Symbol.search是这些众所周知的符号之一,它通过String.prototype.search()方法暴露给开发者。

  • 当你调用一个字符串对象的.search()方法时,实际上内部使用的是Symbol.search属性指定的函数来执行搜索操作。

  • 例如,如果你想自定义怎样在特定对象上进行搜索,你可以为这个对象的Symbol.search属性赋值一个函数,这个函数实现了你的搜索逻辑。

class CustomSearch {
  constructor(private value: string) {}

  [Symbol.search](searchString: string) {
    return this.value.indexOf(searchString);
  }
}

const obj = new CustomSearch("hello, world");

// 使用自定义的搜索逻辑
console.log("world".search(obj)); // 输出:7
  • 上面的代码中,我们定义了一个CustomSearch类,它有一个私有属性value和一个实现了Symbol.search接口的方法。这个方法简单地调用了字符串的indexOf方法来查找子串。

  • 当我们将objCustomSearch的一个实例)作为参数传递给"world".search(obj)时,"world"字符串的.search()方法内部会调用obj[Symbol.search],而不是执行默认的搜索逻辑。

  • 这种机制让你有机会自定义字符串搜索行为,可以根据不同需求实现各种复杂且高效的搜索策略。

Symbol.species

  • Symbol.species 是 JavaScript 的一个特殊且众所周知的 Symbol,它用于指定一个构造函数创建派生对象时应该使用的构造函数。这听起来可能有点抽象,我们通过几个例子来具体理解。

  • 当你在扩展一个内置类(如 Array、Map 等)并希望方法返回新的实例时,可以使用 Symbol.species 来控制这些实例是哪个构造器创建的。

  • 例子 1:假设我们有一个继承自 Array 的类 MyArray,我们可能希望一些基于数组的操作(比如 map、filter 等)返回 MyArray 的实例而不是普通的 Array 实例。

    class MyArray extends Array {
      static get [Symbol.species]() {
        return MyArray;
      }
    }
    
    let myArray = new MyArray(1, 2, 3);
    let mapped = myArray.map((x) => x * x);
    
    console.log(mapped instanceof MyArray); // true
    

    在这个例子中,Symbol.species 被用于指示当调用 map 方法时应当使用 MyArray 构造器来创建新的实例,因此 mapped 是一个 MyArray 的实例,而不是一个普通的 Array 实例。

  • 例子 2:如果你不希望扩展后的类(比如 MyCoolArray)在某些操作后还保持原有类的性质,可以通过改变 Symbol.species 的返回值来改变这一行为。

    class MyCoolArray extends Array {
      // Override species to the parent Array constructor
      static get [Symbol.species]() {
        return Array;
      }
    }
    
    let coolArray = new MyCoolArray(1, 2, 3);
    let filtered = coolArray.filter((x) => x > 1);
    
    console.log(filtered instanceof MyCoolArray); // false
    console.log(filtered instanceof Array); // true
    

    这里,虽然 filtered 是通过 MyCoolArray 的实例调用 filter 方法得到的,但由于我们将 Symbol.species 指定为了 Array,所以 filtered 实际上是一个标准的 Array 实例。

  • 通过使用 Symbol.species,可以在扩展内置对象时精确控制方法返回值的类型,让代码更加灵活和符合预期。这对于构建复杂的类结构特别有用,能够确保即使在继承链中也能保持适当的实例类型。

Symbol.split

Symbol.split 是 TypeScript (同时也是 JavaScript) 中的一个众所周知(well-known)的 Symbol,它被用于定制一个对象的 split 方法如何被调用。简单来说,你可以使用 Symbol.split 来改变一个字符串被 .split() 方法分割的行为。

  • 当你调用一个字符串的 .split() 方法时,如果传入的参数是一个拥有 Symbol.split 属性的对象,那么这个方法会调用该属性指向的函数,而不是执行默认的分割操作。
  • 这个功能允许我们更加灵活地处理字符串的分割,可以根据自定义的规则去分割字符串。

示例:

  1. 不使用 Symbol.split 的标准 .split() 用法:
let text = "Hello World!";
let result = text.split(" "); // 分割字符串,使用空格作为分隔符
console.log(result); // 输出: ["Hello", "World!"]
  1. 使用 Symbol.split 自定义分割行为:
class MySplitter {
  [Symbol.split](text: string) {
    // 每遇到一个字符就分割
    return text.split("");
  }
}

let text = "Hello";
let splitter = new MySplitter();
let result = text.split(splitter); // 使用 MySplitter 的[Symbol.split]来分割字符串
console.log(result); // 输出: ["H", "e", "l", "l", "o"]

在第二个示例中,通过定义一个包含 [Symbol.split] 属性的类 MySplitter,并在该属性上实现自定义的分割逻辑(本例中是将字符串按每个字符分割),我们可以控制字符串的 .split() 方法的行为。当 text.split(splitter) 被调用时,由于 splitter 对象含有 Symbol.split 属性,.split() 方法会调用这个属性指向的函数来执行分割操作,而不是使用默认的行为。这就是 Symbol.split 的魅力所在,它使得字符串处理变得更加灵活和强大。

Symbol.toPrimitive

Symbol.toPrimitive 是一个内建的 Symbol 值。它是一个函数值属性,当一个对象被转换为对应的原始值时,会调用这个函数。

  • Symbol.toPrimitive 允许你定义如何将对象转换成原始数据类型(数值、字符串和布尔值)。

场景和例子:

  • 当对象需要被隐式转换为原始类型时,比如数学运算或字符串拼接,JavaScript 引擎会自动调用 Symbol.toPrimitive 方法。
let obj = {
  [Symbol.toPrimitive](hint) {
    if (hint == "number") {
      return 10;
    }
    if (hint == "string") {
      return "hello";
    }
    if (hint == "default") {
      return "default";
    }
  },
};

// 当期望一个数值时
console.log(+obj); // 输出: 10

// 当期望一个字符串时
console.log(`${obj}`); // 输出: 'hello'

// 不确定期望哪种类型时(默认情况)
console.log(obj + ""); // 输出: 'default'
  • hint参数表示预期转换的类型,它可以是'string'、'number'或'default'。

注意点:

  • 如果对象没有 Symbol.toPrimitive 方法,JavaScript 引擎会尝试使用 valueOf()和 toString()方法来进行类型转换。
  • 定义 Symbol.toPrimitive 方法时,确保它能处理所有三种提示类型:'number','string'和'default'。
  • 正确地实现转换逻辑非常重要,因为错误的实现可能导致代码行为异常或难以调试的问题。

Symbol.toStringTag

Symbol.toStringTag 是一个内置的 Symbol 值,它对应一个属性名,这个属性可以用来创建对象默认的 toString() 方法返回的字符串描述。

这里有几点要理解:

  • Symbol 是 TypeScript (和 JavaScript) 中的一种原始数据类型,就像 number, string, 和 boolean
  • 每个 Symbol 都是唯一的,即便你用相同的描述创建两个 Symbol,它们也是不同的。
  • toStringTag 就是这样一个特殊的 Symbol,它被用于对象的 toString 方法。

以下是 Symbol.toStringTag 的一些常见用法:

  • 当你调用一个对象的 toString() 方法时(例如 myObject.toString()),JavaScript 内部会查找这个对象上的 Symbol.toStringTag 属性,并使用它的值来构造返回的字符串。
  • 如果一个对象没有自定义的 Symbol.toStringTag 属性,toString() 方法通常会返回一个如 "[object Object]" 这样的字符串。
  • 当你为对象定义了 Symbol.toStringTag 后,toString() 方法返回的字符串中会包含你所指定的值。

示例代码:

// 自定义类
class MyArray {
  get [Symbol.toStringTag]() {
    return "MyArray";
  }
}

const arr = new MyArray();

// 调用 toString 时获取 "[object MyArray]"
console.log(Object.prototype.toString.call(arr)); // => "[object MyArray]"

// 内置对象已经定义了 Symbol.toStringTag
console.log(Math[Symbol.toStringTag]); // => "Math"
console.log(JSON[Symbol.toStringTag]); // => "JSON"

// 数组的 toStringTag 是 "Array"
console.log(Object.prototype.toString.call([])); // => "[object Array]"

// 函数的 toStringTag 是 "Function"
console.log(Object.prototype.toString.call(function () {})); // => "[object Function]"

通过上面的例子,我们可以看到,通过自定义 Symbol.toStringTag 可以改变对象 toString() 方法的输出,使得对对象的描述更加清晰易懂。这在调试或者编写需要类型检查的代码时非常有用。

Symbol.unscopables

Symbols 是 TypeScript 和 JavaScript 中的一种原始数据类型,类似于字符串和数字。Symbol.unscopables 是 Symbols 的一个特殊属性,它是用来指定哪些对象属性不能被 with 语句环境绑定。

  • Symbol.unscopables 本身是一个 Symbol,它可以被用作对象属性键。
  • 当在 ES2015 或更高版本中使用 with 语句时,Symbol.unscopables 属性所对应的对象将会标明哪些属性不应该被 with 环境包含。
  • 使用 Symbol.unscopables 可以避免对象的某些属性在 with 语句块中被意外使用。

举个例子,假设有一个数组:

let numbers = [1, 2, 3];

ES6 中 Array.prototype 有一个 Symbol.unscopables 属性,这个属性列出了一些不应当在 with 环境中自动出现的方法名,比如 copyWithin, entries, fill, 等等。

如果你尝试在 with 环境中使用这些属性:

with (numbers) {
  console.log(fill); // 在有 unscopables 之前,这会输出 Array.prototype.fill 方法
}

然而,因为 fill 函数在 Array.prototype[Symbol.unscopables] 对象中被列出,上面的代码实际上不会输出 Array.prototype.fill 方法,而是产生一个错误(在严格模式下)或者 undefined(在非严格模式下)。

显式查看 unscopables

console.log(Array.prototype[Symbol.unscopables]);
// 输出像这样的对象:
// {
//   copyWithin: true,
//   entries: true,
//   fill: true,
//   find: true,
//   findIndex: true,
//   ...
// }

这表明,所有设置为 true 的方法(如 fill),如果尝试在 with 语句中访问,将不会找到它们,以防止可能的冲突和混淆。

注意,由于 with 语句已经不被推荐使用,并且在严格模式下不可用,你可能不会实际需要在日常开发中使用 Symbol.unscopables。不过理解它的存在和目的有助于更深入地理解 JavaScript 和 TypeScript 的工作机制。