JavaScript Symbol 的终极指南

摘要: 在本教程中,您将了解 JavaScript symbol 原语类型以及如何有效地使用 symbol。

创建符号

ES6 添加了 Symbol 作为一种新的原语类型。与其他原语类型(如 numberbooleannullundefinedstring)不同,symbol 类型没有字面量形式。

要创建一个新的 symbol,您可以使用全局 Symbol() 函数,如本例所示

let s = Symbol('foo');Code language: JavaScript (javascript)

每次调用 Symbol() 函数时,它都会创建一个新的唯一

console.log(Symbol() === Symbol()); // falseCode language: JavaScript (javascript)

Symbol() 函数接受一个 description 作为可选参数。description 参数将使您的 symbol 更具描述性。

以下示例创建了两个 symbol:firstNamelastName

let firstName = Symbol('first name'),
    lastName = Symbol('last name');Code language: JavaScript (javascript)

您可以使用 toString() 方法访问 symbol 的 description 属性。console.log() 方法隐式地调用 symbol 的 toString() 方法,如以下示例所示

console.log(firstName); // Symbol(first name)
console.log(lastName); // Symbol(last name)Code language: JavaScript (javascript)

由于 symbol 是原语值,因此您可以使用 typeof 运算符来检查变量是否为 symbol。ES6 扩展了 typeof,当您传入 symbol 变量时,它将返回 symbol 字符串

console.log(typeof firstName); // symbolCode language: JavaScript (javascript)

由于 symbol 是原语值,因此如果您尝试使用 new 运算符创建 symbol,则会收到错误

let s = new Symbol(); // errorCode language: JavaScript (javascript)

共享符号

ES6 为您提供了一个全局 symbol 注册表,允许您在全局范围内共享 symbol。如果您想创建一个将被共享的 symbol,请使用 Symbol.for() 方法而不是调用 Symbol() 函数。

Symbol.for() 方法接受一个参数,该参数可用于 symbol 的 description,如以下示例所示

let ssn = Symbol.for('ssn');Code language: JavaScript (javascript)

Symbol.for() 方法首先在全局 symbol 注册表中搜索具有 ssn 键的 symbol。如果存在,则返回现有 symbol。否则,Symbol.for() 方法创建一个新的 symbol,将其注册到全局 symbol 注册表中,并使用指定的键,并返回 symbol。

稍后,如果您使用相同的键调用 Symbol.for() 方法,Symbol.for() 方法将返回现有 symbol。

let citizenID = Symbol.for('ssn');
console.log(ssn === citizenID); // trueCode language: JavaScript (javascript)

在本例中,我们使用 Symbol.for() 方法查找具有 ssn 键的 symbol。由于全局 symbol 注册表中已经包含了它,因此 Symbol.for() 方法返回了现有 symbol。

要获取与 symbol 关联的键,您可以使用 Symbol.keyFor() 方法,如以下示例所示

console.log(Symbol.keyFor(citizenID)); // 'ssn'Code language: JavaScript (javascript)

如果 symbol 不存在于全局 symbol 注册表中,System.keyFor() 方法将返回 undefined

let systemID = Symbol('sys');
console.log(Symbol.keyFor(systemID)); // undefinedCode language: JavaScript (javascript)

Symbol 用法

A) 使用 symbol 作为唯一值

在代码中使用字符串或数字时,应始终使用 symbol。例如,您必须在任务管理应用程序中管理状态。

在 ES6 之前,您将使用 openin progresscompletedcanceledon hold 等字符串来表示任务的不同状态。在 ES6 中,您可以使用以下 symbol

let statuses = {
    OPEN: Symbol('Open'),
    IN_PROGRESS: Symbol('In progress'),
    COMPLETED: Symbol('Completed'),
    HOLD: Symbol('On hold'),
    CANCELED: Symbol('Canceled')
};
// complete a task
task.setStatus(statuses.COMPLETED);
Code language: JavaScript (javascript)

B) 使用 symbol 作为对象的计算属性名

您可以使用 symbol 作为 计算属性 名。请参见以下示例

let status = Symbol('status');
let task = {
    [status]: statuses.OPEN,
    description: 'Learn ES6 Symbol'
};
console.log(task);Code language: JavaScript (javascript)

要获取对象的全部可枚举属性,请使用 Object.keys() 方法。

console.log(Object.keys(task)); // ["description"]Code language: JavaScript (javascript)

要获取对象的全部属性(无论属性是否可枚举),请使用 Object.getOwnPropertyNames() 方法。

console.log(Object.getOwnPropertyNames(task)); // ["description"]Code language: JavaScript (javascript)

要获取对象的全部属性 symbol,请使用 Object.getOwnPropertySymbols() 方法,该方法已在 ES6 中添加。

console.log(Object.getOwnPropertySymbols(task)); //[Symbol(status)]Code language: JavaScript (javascript)

Object.getOwnPropertySymbols() 方法从对象返回其自身属性 symbol 的数组。

众所周知的符号

ES6 提供预定义的 symbol,称为众所周知的 symbol。众所周知的 symbol 代表 JavaScript 中的常见行为。每个众所周知的 symbol 都是 Symbol 对象的静态属性。

Symbol.hasInstance

Symbol.hasInstance 是一个 symbol,它改变了 instanceof 运算符的行为。通常,当您使用 instanceof 运算符时

obj instanceof type;Code language: JavaScript (javascript)

JavaScript 将按如下方式调用 Symbol.hasIntance 方法

type[Symbol.hasInstance](obj);Code language: JavaScript (javascript)

然后,它取决于方法来确定 obj 是否是 type 对象的实例。请参见以下示例。

class Stack {
}
console.log([] instanceof Stack); // false
Code language: JavaScript (javascript)

[] 数组不是 Stack 类的实例,因此在本例中,instanceof 运算符返回 false

假设您希望 [] 数组是 Stack 类的实例,您可以添加 Symbol.hasInstance 方法,如下所示

class Stack {
    static [Symbol.hasInstance](obj) {
        return Array.isArray(obj);
    }
}
console.log([] instanceof Stack); // true
Code language: JavaScript (javascript)

Symbol.iterator

Symbol.iterator 指定函数是否将为对象返回迭代器。

具有 Symbol.iterator 属性的对象称为可迭代对象。

在 ES6 中,所有集合对象(数组SetMap)和字符串都是可迭代对象。

ES6 提供了 for…of 循环,该循环与可迭代对象一起使用,如以下示例所示。

var numbers = [1, 2, 3];
for (let num of numbers) {
    console.log(num);
}

// 1
// 2
// 3Code language: JavaScript (javascript)

在内部,JavaScript 引擎首先调用 numbers 数组的 Symbol.iterator 方法来获取迭代器对象。

然后,它调用 iterator.next() 方法并将迭代器对象的 value 属性复制到 num 变量中。

经过三次迭代后,结果对象的 done 属性为 true,循环退出。

您可以通过 System.iterator symbol 访问默认迭代器对象,如下所示

var iterator = numbers[Symbol.iterator]();

console.log(iterator.next()); // Object {value: 1, done: false}
console.log(iterator.next()); // Object {value: 2, done: false}
console.log(iterator.next()); // Object {value: 3, done: false}
console.log(iterator.next()); // Object {value: undefined, done: true}Code language: JavaScript (javascript)

默认情况下,集合不可迭代。但是,您可以使用 Symbol.iterator 使其可迭代,如以下示例所示

class List {
    constructor() {
        this.elements = [];
    }

    add(element) {
        this.elements.push(element);
        return this;
    }

    *[Symbol.iterator]() {
        for (let element of this.elements) {
            yield  element;
        }
    }
}

let chars = new List();
chars.add('A')
     .add('B')
     .add('C');

// because of the Symbol.iterator
for (let c of chars) {
    console.log(c);
}

// A
// B
// CCode language: JavaScript (javascript)

Symbol.isConcatSpreadable

要连接两个数组,请使用 concat() 方法,如以下示例所示

let odd  = [1, 3],
    even = [2, 4];
let all = odd.concat(even);
console.log(all); // [1, 3, 2, 4]Code language: JavaScript (javascript)

在本例中,结果数组包含两个数组的单个元素。此外,concat() 方法还接受非数组参数,如下所示。

let extras = all.concat(5);
console.log(extras); // [1, 3, 2, 4, 5]Code language: JavaScript (javascript)

数字 5 成为数组的第五个元素。

如您在上面的示例中所见,当我们将数组传递给 concat() 方法时,concat() 方法将数组扩展为单个元素。但是,它对单个原语参数的处理方式不同。在 ES6 之前,您无法更改此行为。

这就是 Symbol.isConcatSpreadable symbol 出现的原因。

Symbol.isConcatSpreadable 属性是一个布尔值,它决定对象是单独添加到 concat() 函数的结果中,还是作为一个整体添加。

请考虑以下示例

let list = {
    0: 'JavaScript',
    1: 'Symbol',
    length: 2
};
let message = ['Learning'].concat(list);
console.log(message); // ["Learning", Object]Code language: JavaScript (javascript)

list 对象连接到 ['Learning'] 数组中。但是,它的各个元素没有被扩展。

要启用将 list 对象的元素单独添加到数组中(当传递给 concat() 方法时),您需要将 Symbol.isConcatSpreadable 属性添加到 list 对象中,如下所示

let list = {
    0: 'JavaScript',
    1: 'Symbol',
    length: 2,
    [Symbol.isConcatSpreadable]: true
};
let message = ['Learning'].concat(list);
console.log(message); // ["Learning", "JavaScript", "Symbol"]Code language: JavaScript (javascript)

请注意,如果您将 Symbol.isConcatSpreadable 的值设置为 false 并将 list 对象传递给 concat() 方法,它将作为整个对象连接到数组中。

Symbol.toPrimitive

Symbol.toPrimitive 方法决定在将对象转换为原语值时应该发生什么。

JavaScript 引擎在每个标准类型的原型上定义了 Symbol.toPrimitive 方法。

Symbol.toPrimitive 方法接受一个 hint 参数,该参数具有三个值之一:“number”、“string” 和 “default”。hint 参数指定返回值的类型。hint 参数由 JavaScript 引擎根据使用对象的上下文填充。

以下是如何使用 Symbol.toPrimitive 方法的示例。

function Money(amount, currency) {
    this.amount = amount;
    this.currency = currency;
}
Money.prototype[Symbol.toPrimitive] = function(hint) {
    var result;
    switch (hint) {
        case 'string':
            result = this.amount + this.currency;
            break;
        case 'number':
            result = this.amount;
            break;
        case 'default':
            result = this.amount + this.currency;
            break;
    }
    return result;
}

var price = new Money(799, 'USD');

console.log('Price is ' + price); // Price is 799USD
console.log(+price + 1); // 800
console.log(String(price)); // 799USDCode language: JavaScript (javascript)

在本教程中,您学习了 JavaScript symbol 以及如何使用 symbol 作为唯一值和对象属性。此外,您还学习了如何使用众所周知的 symbol 来修改对象行为。

本教程是否有帮助?