JavaScript ES6 Symbol 终极教程

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

今天我们来学习JavaScript新的原始类型Symbol,我们可以使用Symbol来创建唯一值作为对象属性或者值,也可以通过Symbolwell-known来修改JS语言内部的逻辑。

创建Symbol

ES6新增了Symbol作为原始数据类型,和其他原始数据类型,像number、boolean、null、undefined、string,symbol类型没有文字形式。

创建一个symbol,我们要使用全局函数Symbol()

let s = Symbol('foo');
复制代码

Symbol() 函数每次调用会创建一个新的唯一值

console.log(Symbol() === Symbol()); // false
复制代码

Symbol() 函数接受一个可选参数作为描述,这样使Symbol更具有语义性。

下面创建两个symbol分别为: firstName and lastName.

let firstName = Symbol('first name'),
    lastName = Symbol('last name');
复制代码

当我们使用console.log()去打印symbol的时候会隐式调用symbol的toString()方法。

console.log(firstName); // Symbol(first name)
console.log(lastName); // Symbol(last name)
复制代码

由于symbol为原始值,我们可以使用typeof去检查它的类型,同样ES6拓展了typeof关键字,在遇到symbol类型时会返回symbol

console.log(typeof firstName); 
// symbol
复制代码

由于是原始类型,也不能使用new去创建

let s = new Symbol(); // error
复制代码

共享 symbol

要创建一个共享的symbol,要使用Symbol.for()函数,而不是Symbol()

Symbol.for() 也接受一个可选参数作为描述

let ssn = Symbol.for('ssn');
复制代码

Symbol.for() 会首先在全局中查找是否有已经创建的ssn的symbol,如果有就会返回已经创建的symbol,如果没有就会创建一个新的symbol。

接下来,我们创建一个相同的symbol,然后看看不是同一个symbol

let nz = Symbol.for('ssn');
console.log(ssn === nz); // true
复制代码

因为上面已经创建ssn的symbol,所以nz变量的symbol和上面创建的将是同一个。

如果想要获取symbol的键,使用Symbol.keyFor()方法

console.log(Symbol.keyFor(nz)); // 'ssn'
复制代码

注意,如果symbol是通过Symbol()创建的,使用Symbol.keyFor()会返回undefined

let systemID = Symbol('sys');
console.log(Symbol.keyFor(systemID)); // undefined
复制代码

Symbol 有啥用

一) 使用Symbol作唯一值

我们在代码中经常会用字符串或者数字去表示一些状态,也经常会面临缺乏语义性或者重复定义的问题,这时使用Symbol是最好的选择,每次新创建的Symbol都是唯一的,不会产生重复,而且我们可以给Symbol传入相应的描述。

看下面的例子,我们使用Symbol来表达订单的几种状态,而不是字符串和数字

let statuses = {
    OPEN: Symbol('已下单'),
    IN_PROGRESS: Symbol('配送中'),
    COMPLETED: Symbol('订单完成'),
    CANCELED: Symbol('订单取消')
};

// 完成订单
task.setStatus(statuses.COMPLETED);
复制代码

二) 使用 symbol 作为对象属性

使用Symbol作为属性名称

let status = Symbol('status');

let task = {
    [status]: statuses.OPEN,
    description: '学习 ES6 Symbol'
};

console.log(task);
复制代码

使用Object.keys()获取对象的所有可枚举属性

console.log(Object.keys(task));
// ["description"]
复制代码

使用Object.getOwnPropertyNames() 获取所有属性,无论是否是可枚举

console.log(Object.getOwnPropertyNames(task));
// ["description"]
复制代码

那么要获取对象中的Symbol属性,需要使用ES6新增的Object.getOwnPropertySymbols()方法

console.log(Object.getOwnPropertySymbols(task));
//[Symbol(status)]
复制代码

Well-known symbol

ES6在原型链上定义了与 Symbol 相关的属性来暴露更多的语言内部逻辑。well-known Symbol 为标准对象定义了一些以前只在语言内部可见的功能。

Symbol.hasInstance

Symbol.hasInstance 是一个改变instanceof操作符默认行为的symbol,通常我们会这样使用instanceof

obj instanceof type;
复制代码

那么JavaScript 就会执行 Symbol.hasIntance 方法,像下面这样

type[Symbol.hasInstance](obj);
复制代码

它会调用type的Symbol.hasInstance静态方法,将obj作为参数

class Stack {
}
console.log([] instanceof Stack);
// false
复制代码

[] 数组不是Stack类所创建的实例,所以返回false。

假设要使[] 数组是Stack类所创建的实例,返回true,我们可以重写Symbol.hasInstance的方法

class Stack {
    static [Symbol.hasInstance](obj) {
        return Array.isArray(obj);
    }
}
console.log([] instanceof Stack);
// true
复制代码

Symbol.iterator

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

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

在ES6中,Array、Set、Map和string都是可迭代对象。

ES6提供了for...of循环,它可以用在可迭代对象上。

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

// 1
// 2
// 3
复制代码

在背后,JavaScript引擎首先调用numbers数组的 Symbol.iterator 方法来获取迭代器对象,然后它调用 iterator.next() 方法并将迭代器对象的value属性复制到num变量中,3次迭代后,对象的done 属性为true,循环推出。

我们可以通过Symbol.iterator来获取数组的迭代器对象。

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}
复制代码

默认情况下,一个自己定义的集合是不可以迭代的,但是我们可以用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');

// 使用Symbol.iterator实现了迭代
for (let c of chars) {
  console.log(c);
}

// A
// B
// C
复制代码

Symbol.isConcatSpreadable

我们可以使用concat()方法来合并两个数组

let odd  = [1, 3],
    even = [2, 4];
let all = odd.concat(even);
console.log(all); // [1, 3, 2, 4]
复制代码

我们也可以使用concat()来传入单个元素,而非数组

let extras = all.concat(5);
console.log(extras); // [1, 3, 2, 4, 5]
复制代码

在上面的例子中,我们将一个数组传给concat()方法时,concat()方法会将数组拓展为单个元素。但是,它会以不同的方式去对待单个原始参数,在ES6之前,我们无法更改此行为。

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

将list对象合并到 ['Learning'] 数组中,但list对象中的各个元素并没有被合并到数组中。

要在concat()时将list对象中的元素单独添加到数组中的,我们需要将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"]
复制代码

如果将Symbol.isConcatSpreadable设置为false,concat()就会将list整个对象合并到数组中。

Symbol.toPrimitive

Symbol.toPrimitive 方法决定了当一个对象被转换成原始值时的行为。

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

Symbol.toPrimitive方法接受一个hint参数,该参数会是下面三个值,string、number、default,hint参数用来指定返回值的类型。hint参数由JavaScript引擎根据使用对象的上下文进行填充。

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(10000, '人民币');

console.log('我有 ' + price); // Price is 799USD
console.log(+price + 1); // 800
console.log(String(price)); // 799USD
复制代码

其他

  • Symbol.match(regex):一个在调用 String.prototype.match() 方法时调用的方法,用于比较字符串。

  • Symbol.replace(regex, replacement):一个在调用 String.prototype.replace() 方法时调用的方法,用于替换字符串的子串。

  • Symbol.search(regex):一个在调用 String.prototype.search() 方法时调用的方法,用于在字符串中定位子串。

  • Symbol.species(regex):用于创建派生对象的构造函数。

  • Symbol.split:一个在调用 String.prototype.split() 方法时调用的方法,用于分割字符串。

  • Symbol.toStringTag:一个在调用 String.prototype.toString() 方法时使用的字符串,用于创建对象描述。

  • Symbol.unscopables:一个定义了一些不可被 with 语句引用的对象属性名称的对象集合。

总结

今天我们学习了Symbol的所有的使用方法,以及在平时常用的一些方式。希望对你能有帮助。

如果本文有帮助,微信搜索【小帅的编程笔记】,让我们每天进步

猜你喜欢

转载自juejin.im/post/7017375285699952648