Skip to content

深入理解 ES6 #6-Symbol 和 Symbol 属性

在 ES5 及早期版本中,JS 语言包含 5 中原始类型:

  • 字符串型
  • 数字型
  • 布尔型
  • null
  • undefined

ES6 引入了第六种原始类型:

  • Symbol

创建 Symbol

js
let firstName = Symbol();
let person = {};
person[firstName] = "JiaJia";
console.log(person[firstName]); // "JiaJia"

Symbol 的辨识方法

使用 typeof 来检测辨识是否为 Symbol

js
let symbol = Symbol("test symbol");
console.log(typeof symbol); // "symbol"

Symbol 的使用方法

所有使用可计算属性名的地方,都可以使用 Symbol

js
let firstName = Symbol("first name");

let person = {
    [firstName]: "JiaJia"
};

// 将属性设置为只读
Object.defineProperty(person, firstName, { writable: false });

let lastName = Symbol("last name");

Object.defineProperties(person, {
    [lastName]: {
        value: "Liu",
        writable: false
    }
});

console.log(person[firstName]); // "JiaJia"
console.log(person[lastName]); // "Liu"

Symbol 共享体系

ES6 提供了一个可以随时访问的全局 Symbol 注册表。

使用 Symbol.for() 方法创建可共享的 Symbol,它只接受一个参数,也就是即将创建的 Symbol 的字符串标识符,这个参数同样也被用作 Symbol 的描述。

js
let uid = Symbol.for("uid");
let object = {};

object[uid] = "12345";

console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"

Symbol.for() 方法首先在全局 Symbol 注册表中搜索键为“uid”的 Symbol 是否存在,如果存在,直接返回已有的 Symbol;否则,创建一个新的 Symbol,并使用这个键在 Symbol 全局注册表中注册,随即返回新创建的 Symbol

随后如果再传入同样的键调用 Symbol.for() 方法会返回相同的 Symbol

js
let uid = Symbol.for("uid");
let object = {
    [uid]: "12345";
};

console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"

let uid2 = Symbol.for("uid");

console.log(uid === uid2); // true
console.log(object[uid2]); // "12345"
console.log(uid2); // "Symbol(uid)"

可以使用 Symbol.keyFor() 方法在 Symbol 全局注册表中检索与 Symbol 有关的键。

js
let uid = Symbol.for("uid");
console.log(Symbol.keyFor(uid)); // "uid"

let uid2 = Symbol.for("uid");
console.log(Symbol.keyFor(uid2)); // "uid"

let uid3 = Symbol("uid");
console.log(Symbol.keyFor(uid3)); // undefined

Symbol 与类型强制转换

其它类型没有与 Symbol 逻辑等价的值。

可以使用 SymboltoString() 方法返回 Symbol 描述里的内容,但是直接与字符串拼接或者参与数值计算,则会抛出错误。

js
let uid = Symbol.for("uid"),
    desc = String(uid);
console.log(desc); // "Symbol(uid)"

desc = uid + ""; // 报错
// Cannot convert a Symbol value to a string

let sum = uid / 1; // 报错
// Cannot convert a Symbol value to a number

Symbol 属性检索

Object.keys() 方法和 Object.getOwnPropertyNames() 方法可以检索对象中所有的属性名。

  • Object.keys() 方法返回所有可枚举的属性名;
  • Object.getOwnPropertyNames() 方法不考虑属性的可枚举性一律返回。

为了保持 ES5 函数的原有功能,这两个方法都不支持 Symbol 属性。

ES6 中添加了一个 Object.getOwnPropertySymbols() 方法来检索 Symbol 属性。

该方法返回一个包含所有 Symbol 自有属性的数组。

js
let uid = Symbol("uid");
let object = {
    [uid]: "12345"
};

let symbols = Object.getOwnPropertySymbols(object);

console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(uid)"
console.log(object[symbols[0]]); // "12345"

通过 well-known Symbol 暴露内部操作

通过在原型链上定义与 Symbol 相关的属性来暴露更多的语言内部逻辑。

Symbol.hasInstance 方法

每个函数都要一个 Symbol.hasInstance 方法,用于确定对象是否为函数的实例。

该方法在 Function.prototype 中定义,所以所有函数都继承了 instanceof 属性的默认行为。

为了确保 Symbol.hasInstance 不会被意外重写,该方法被定义为不可写、不可配置并且不可枚举。

Symbol.hasInstance 方法只接受一个参数,即要检查的值。如果传入的值是函数的实例,则返回 true

js
obj instanceof Array;

上述代码等价于

js
Array[Symbol.hasInstance](obj);

本质上,ES6 只是将 instanceof 操作符重新定义为此方法的简写语法。

现在引入方法调用后,就可以随意改变 instanceof 的运行方式了。

js
function MyObject() {

}

let obj = new MyObject();

console.log(obj instanceof MyObject); // true

Object.defineProperty(MyObject, Symbol.hasInstance, {
    value: function(v) {
        return false;
    }
});

console.log(obj instanceof MyObject); // false

只有通过 Object.defineProperty() 才能改写一个不可写属性。

Symbol.isConcatSpreadable 属性

JS 数组的 concat() 方法被设计用于拼接两个数组,但也可以接受非数组参数。

js
let colors1 = [ "red", "green" ],
    colors2 = colors1.concat([ "blue", "black" ], "brown");
console.log(colors2.length); // 5
console.log(colors2); // ["red", "green", "blue", "black", "brown"]

JS 规范声明,凡是传入数组参数,就会自动将它们分解为独立元素。 ES6 之前无法调整这个特性。

Symbol.isConcatSpreadable 属性是一个布尔值,如果该属性值为 true,则表示对象有 length 属性和数字键,故它的数值型属性值应该被独立添加到 concat() 调用的结果中。

这个 Symbol.isConcatSpreadable 属性默认情况下不会出现在标准对象中,它只是一个可选属性,用于增强作用于特定对象类型的 concat() 方法的功能,有效简化其默认特性。

下面方法自定义了一个在 concat() 调用中与数组类型的新类型:

js
let collection = {
    0: "Hello",
    1: "World",
    length: 2,
    [Symbol.isConcatSpreadable]: true
};

let message = [ "Hi" ].concat(collection);

console.log(message.length); // 3
console.log(message); // ["Hi", "Hello", "World"]

也可以将 Symbol.isConcatSpreadable 设置 false,来防止元素在调用 concat() 方法时被分解。

js
let collection = {
    0: "Hello",
    1: "World",
    length: 2,
    [Symbol.isConcatSpreadable]: false
};

let message = [ "Hi" ].concat(collection);

console.log(message.length); // 2
console.log(message); // ["Hi", {0: "Hello", 1: "World", length: 2, Symbol(Symbol.isConcatSpreadable): false}]

Symbol.match、Symbol.replace、Symbol.search 和 Symbol.split 属性

字符串类型的几个方法可以接受正则表达式作为参数:

  • match(regex)
    确定给定字符串是否匹配正则表达式 regex

  • replace(regex, replacement)
    将字符串中匹配正则表达式 regex 的部分替换为 replacement

  • search(regex)
    在字符串中定位匹配正则表达式 regex 的位置索引

  • split(regex)
    按照匹配正则表达式 regex 的元素将字符串分切,并将结果存入数组中

在 ES6 中,可以使用对应的 4 个 Symbol,自定义对象来替换正则表达式来进行匹配。

  • Symbol.match
    接受一个字符串类型的参数,如果匹配成功则返回匹配元素的数组,否则返回 null

  • Symbol.replace
    接受一个字符串类型的参数和一个替换用的字符串,最终依然返回一个字符串

  • Symbol.search
    接受一个字符串参数,如果匹配到内容,则返回数字类型的索引位置,否则返回 -1

  • Symbol.split
    接受一个字符串参数,根据匹配内容将字符串分解,并返回一个包含分解后片段的数组

js
// 实际上等价于 /^.{10}$/
let hasLengthOf10 = {
    [Symbol.match]: function(value) {
        return value.length === 10 ? [value.substring(0, 10)] : null;
    },
    [Symbol.replace]: function(value, replacement) {
        return value.length === 10 ? replacement : value;
    },
    [Symbol.search]: function(value) {
        return value.length === 10 ? 0 : -1;
    },
    [Symbol.split]: function(value) {
        return value.length === 10 ? ["", ""] : [value];
    }
};

let message1 = "Hello world", // 11 个字符
    message2 = "Hello Dlph"; // 10 个字符

let match1 = message1.match(hasLengthOf10),
    match2 = message2.match(hasLengthOf10);

console.log(match1); // null
console.log(match2); // ["Hello Dlph"]

let replace1 = message1.replace(hasLengthOf10),
    replace2 = message2.replace(hasLengthOf10);

console.log(replace1); // "Hello world"
console.log(replace2); // "Hello Dlph"

let search1 = message1.search(hasLengthOf10),
    search2 = message2.search(hasLengthOf10);

console.log(search1); // -1
console.log(search2); // 0

let split1 = message1.split(hasLengthOf10),
    split2 = message2.split(hasLengthOf10);

console.log(split1); // ["Hello world"]
console.log(split2); // ["", ""]

Symbol.toPrimitive 方法

在 JS 引擎中,当执行特定操作时,经常会尝试将对象转换到相应的原始值。

在 ES6 中,可以通过 Symbol.toPrimitive 方法更改这个原始值。

Symbol.toPrimitive 方法被定义在每一个标准类型的原型上,并且规定了当对象被转换为原始值时应当执行的操作。

该方法接受一个参数 类型提示hint),该值只有三种选择:"number"、"string"和"default"。根据参数返回值分别为 数字、字符和无类型偏好的值。

数字模式

  1. 调用 valueOf() 方法,如果结果为原始值,则返回;
  2. 否则,调用 toString() 方法,如果结果为原始值,则返回;
  3. 如果再无可选值,则抛出错误。

字符串模式

  1. 调用 toString() 方法,如果结果为原始值,则返回;
  2. 否则,调用 valueOf() 方法,如果结果为原始值,则返回;
  3. 如果再无可选值,则抛出错误。

默认模式

  1. 在大多数情况下,标准对象会将默认模式按数字模式处理(除了 Date 对象,在这种情况下,会将默认模式按照字符串模式处理)。

如果自定义了 Symbol.toPrimitive 方法,则可以覆盖这些默认的强制转换类型。

NOTE

默认模式只用于 == 运算、+ 运算及给 Date 构造函数传递一个参数时。
在大多数的操作中,使用的都是字符串模式或数字模式。

js
function Temperature(degrees) {
    this.degrees = degrees;
}

Temperature.prototype[Symbol.toPrimitive] = function(hint) {
    switch (hint) {
        case "string":
            return this.degrees + "\u00b0"; // degrees symbol
        case "number":
            return this.degrees;
        case "default":
            return this.degrees + " degrees";
    }
};

var freezing = new Temperature(32);

console.log(freezing + "!"); // "32 degrees!"
console.log(freezing / 2); // 16
console.log(String(freezing)); // "32°"
  • + 操作符触发的是默认模式;
  • / 操作符触发的是数字模式;
  • String() 函数触发字符串模式。

NOTE

针对三种模式返回不同的值是可行的,但更常见的做法是,将默认模式设置设置成与字符串模式或数字模式相同的处理逻辑。

Symbol.toStringTag 属性

Symbol.toStringTag 所代表的属性在每一个对象中都存在,其定义了调用对象的 Object.prototype.toString.call() 方法时返回的值。

对于数组,调用该函数返回的值通常是 “Array”,它正是存储在对象的 Symbol.toStringTag 属性中的。

同样的,也可以为自己的对象定义 Symbol.toStringTag 的值。

js
function Person(name) {
    this.name = name;
}

var me = new Person("JiaJia");

console.log(me.toString()); // "[object Object]"
console.log(Object.prototype.toString.call(me)); // "[object Object]"

// 为对象定义自己的 Symbol.toStringTag 值
Person.prototype[Symbol.toStringTag] = "Person";

// toString() 方法默认返回 Symbol.toStringTag 的值
console.log(me.toString()); // "[object Person]"
console.log(Object.prototype.toString.call(me)); // "[object Person]"

// 自定义 toString 方法
Person.prototype.toString = function() {
    return this.name;
}

console.log(me.toString()); // "JiaJia"
// 自定义 toString() 方法后,不会影响 Object.prototype.toString.call() 方法的值
console.log(Object.prototype.toString.call(me)); // "[object Person]"
  • toString() 方法默认返回 Symbol.toStringTag 的值。
  • 自定义 toString() 方法后,不会影响 Object.prototype.toString.call() 方法的值

Symbol.unscopables 属性

with 语句的初衷是可以免于编写重复的代码。但加入 with 语句后,代码变的难以理解,执行性能很差且容易导致程序出错。最终,标准固定,在严格模式下不可以使用 with 语句。

尽管未来不会使用 with 语句,但是 ES6 仍在非严格模式下提供了向后兼容性。

js
var values = [1, 2, 3],
    colors = ["red", "green", "blue"],
    color = "black";

with(colors) {
    // 相当于调用了 colors.push 方法
    push(color);
    push(...values);
}

console.log(colors); // ["red", "green", "blue", "black", 1, 2, 3]

Symbol.unscopables 通常用于 Array.prototype,以在 with 语句中标识出不创建绑定的属性名。

Symbol.unscopables 是以对象的形式出现的,它的键是在 with 语句中要忽略的标识符,其对应的值必须是 true

这里是一个为数组添加默认的 Symbol.unscopables 属性的示例:

js
// 已默认内置到 ES6 中
Array.prototype[Symbol.unscopables] = Object.assign(Object.create(null), {
    copyWithin: true
    entries: true
    fill: true
    find: true
    findIndex: true
    includes: true
    keys: true
});

Page Layout Max Width

Adjust the exact value of the page width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the page layout
A ranged slider for user to choose and customize their desired width of the maximum width of the page layout can go.

Content Layout Max Width

Adjust the exact value of the document content width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the content layout
A ranged slider for user to choose and customize their desired width of the maximum width of the content layout can go.