搞清楚ECMAScript 6新增的基本数据类型Symbol及内置值Symbol.iterator

605 阅读9分钟

前言

ECMAScript 6(简称ES6),于2015年6月正式发布的JavaScript语言的标准,正式名为ECMAScript 2015(ES2015)。ES6之前,基本数据类型分五种:Boolean、Number、String、Null 和 Undefined,ES6为防止因对象合并发生属性名冲突而被覆盖,增加了一种 Symbol 基本数据类型,表示独一无二的值

下面将从创建方式(Symbol() 函数 和 Symbol.for())、Symbol.for() 和 Symbol.keyFor()使用场景(作对象的属性名和替代某些场景下的常量)和 遍历(Object.getOwnPrototypeSymbols()和Reflect.ownKeys())四个方面来学习。

Symbol 介绍

创建 Symbol 类型值

创建 Symbol 类型值,不是通过 new 关键字(因为 new 关键字是创建一个对象的实例),而是 Symbol() 函数,其 typeof 值为 "symbol"

const nameProp = Symbol()
console.log(nameProp, typeof nameProp) // Symbol() "symbol"

因为 Symbol 表示的是独一无二的值,即使连着创建两个 Symbol 类型的值,也是不同的,例如:

const nameProp = Symbol()
const ageProp = Symbol()
console.log(nameProp, ageProp, nameProp === ageProp) // Symbol() Symbol() false

后面还会介绍另一种方式(Symbol.for())创建 Symbol 类型值。

增加描述内容

Symbol.prototype.description:访问其”实例“的描述

为字面化,易理解,建议增加描述内容即在 Symbol() 函数传入参数,通过 .description 访问描述 (该描述也会被视作该 Symbol 类型值的 key):

const nameProp = Symbol("name")
const ageProp = Symbol("age")

console.log(nameProp, nameProp.description) // Symbol(name) "local"

Symbol.for() 和 Symbol.keyFor()

如果要使用前面已创建过的 Symbol 类型值,可使用 Symbol.for(key),该方法接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局(Symbol.for() 定义的 Symbol 类型的值具有全局登记特性)。

Symbol.for()Symbol() 区别在于, 调用 Symbol("name") 30 次,返回 30 个不同的 Symbol 类型值,而 Symbol.for("name") 调用 30 次,返回的是 30 个相同的 Symbol 类型值。例如:

const ageProp = Symbol("age")
const childAgeProp = Symbol("age")
console.log(ageProp, childAgeProp, ageProp === childAgeProp) // Symbol(age) Symbol(age) false

const nameProp = Symbol.for("name")
const childNameProp = Symbol.for("name")
console.log(nameProp, childNameProp, nameProp === childNameProp) // Symbol(name) Symbol(name) true

Symbol.keyFor()方法接收一个 Symbol 类型的值,返回一个已登记的 Symbol 类型值的key,如果 Symbol 类型值是通过 Symbol() 创建的,因为没有被全局注册则返回 undefined;如果是通过 Symbol.keyFor() 创建的则返回其key(描述内容)。

console.log(Symbol.keyFor(ageProp)) // undefined
console.log(Symbol.keyFor(nameProp)) // name

Symbol 作用

因为 Symbol 类型为对象合并引起的属性名相同而被改写或覆盖,扩展对象的属性名类型,首先便是可做对象的属性名(ES6之前属性名只能是字符串类型,ES6后支持两种类型:字符串和Symbol类型)。又因为代表的是独一无二的值,可代替某些场景下的 const 定义的常量为什么说是某些场景?举例说明:定义常量 storageType 表示本地存储方式(localStorage OR sessionStorage),而后通过 storageType.getItem() 获取指定值, 这种场景下,便不可将 storagteType 定义为 Symbol 类型,因为 storageType 不仅是一个常量而且还可通过该常量去访问其值的属性或方法)。

作对象的属性名

const nameProp = Symbol("name")
const ageProp = Symbol("age")

const person = {
    [nameProp]: "露水晰123",
}
person[ageProp] = 18
console.log(person) // {Symbol(age): 18,Symbol(name): "露水晰123",__proto__: Object}

代替某些场景下的 const 常量

function getArea(shape, options) {
  let area = 0;

  switch (shape) {
    case 'Triangle': // 魔术字符串
      area = .5 * options.width * options.height;
      break;
    /* ... more code ... */
  }

  return area;
}

getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串

消除魔术字符串常用的方式是将其改为一个变量,更改上述代码如下:

const shapeType = {
  triangle: 'Triangle'
};

function getArea(shape, options) {
  let area = 0;
  switch (shape) {
    case shapeType.triangle:
      area = .5 * options.width * options.height;
      break;
  }
  return area;
}

getArea(shapeType.triangle, { width: 100, height: 100 });

但是,仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用 Symbol 值。上面代码中,除了将shapeType.triangle的值设为一个 Symbol,其他地方都不用修改。

const shapeType = {
  triangle: Symbol()
};

Symbol 属性名的遍历

Object.getOwnPropertySymbols() 只获取一级 Symbol 类型的属性名

Symbol 类型的属性名不会出现在 for...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。即以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法(外部不能访问)。

但是可以通过 Object.getOwnPropertySymbols(obj) 获取对象 obj 里定义的 Symbol 类型的属性名,以数组的形式返回。

const nameProp = Symbol("name")
const ageProp = Symbol("age")
const studentProp = Symbol("student")
const selfProp = Symbol("self")

const person = {
    date: new Date(),
    [nameProp]: "露水晰123",
}
person[ageProp] = 18
person[studentProp] = [
    { [nameProp]: "小晰1", [ageProp]: 19 },
    { [nameProp]: "小晰2", [ageProp]: 20 },
]
person[selfProp] = {
    [selfProp]: 18,
}
console.log(person)
console.log(Object.getOwnPropertySymbols(person))

打印结果如下:

image.png

Obejct.getOwnPropertySymbols(person) 返回的是对象 person 的一级属性名称中 Symbol 类型值,不包含深层的。

Reflect.ownKeys() 获取对象的所有一级属性

console.log(Reflect.ownKeys(person))

image.png

内置的 Symbol 值

除了自定义的 Symbol 类型值,ES6提供了 11 个内置的 Symbol 值,指向语言内部的使用方法。

Symbol.iterator

ES6 新增了一个机制:遍历器(Iterator)机制,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历(Iterable)操作(即依次处理该数据结构的所有成员)。

基本的数据结构主要有:对象(Object)、数组(Array),还有 ES6 新增的 Set、WeakSet、Map、WeakMap 四种数据结构。实际项目中还有众多简单或复杂的数据结构,针对这部分的数据结构的遍历便需要手动设置遍历器。

遍历器(Iterator)机制 的作用有三个:

  • 一是为各种数据结构,提供一个统一的、简便的访问接口(通过 next() 方法更改指针对象依序指向下一个成员,并返回一个包含 done(是否结束遍历) 和 value(当前项的值) 属性的对象);
  • 二是使得数据结构的成员能够按某种次序排列(按顺序依次往下,可由用户手动指定该顺序;正是因为对象(Object)不确定哪个属性先遍历,哪个属性后遍历,故没有给默认的 Iterator(原生具备 Iterator 接口的数据结构:Array、 Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象),需要用户手动通过 Symbol.iterator 指定);
  • 三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。
console.log([1,2,3])
// 该数组的原型上有属性Symbol(Symbol.iterator): ƒ values()
// 该属性便是定义数组的遍历器, 也是数组可以直接遍历的原因

Iterator 的遍历过程是这样的。

  • (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  • (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  • (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
  • (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

遍历器首先是一个函数,该函数返回一个对象且包含一个next方法,该next方法又返回一个包含value(是当前遍历得到的值)和done(布尔值,表示是否是结束遍历即done为true表示没有可遍历的元素了)的对象。

const it = makeArrayIterator([1,2])
console.log("it", it)
console.log(it.next()) // {value: 1, done: false}
console.log(it.next()) // {value: 2, done: false}
console.log(it.next()) // {value: undefined, done: true} 结束了

/**
 * 自定义遍历器
 * @param {*} arr 
 * @returns 
 */
function makeArrayIterator(arr) {
    let index = 0 // 闭包变量
    return {
        next() {
            // ==index++和++index的区别==
            // ==index++是先使用index再+1==
            // ==++index是先+1再使用index==
            return (index < arr.length) 
                ? {value:arr[index++], done:false} // 当前遍历的值
                : {value:undefined, done:true} // 表示已遍历完最后一个元素,结束遍历
        }
    }
}

给对象添加遍历器

对象的Symbol.iterator属性,指向该对象的默认遍历器方法。针对对象进行for...of循环时,调用Symbol.iterator方法(也可以在原型链上定义),返回该对象的默认遍历器。

例如,给对象 obj 定义一个遍历器,依序访问该对象的属性,代码如下:

let obj = {
  data: [ 'parent', 'name', 'age', 'brother', 'children' ],
  name: '露水晰123',
  age: 18,
  parent: [{
    name: '露水晰12',
    age: 36,
  }],
  brother: [{
    name: '露水晰124',
    age: 19,
  }],
  children: [{
    name: '露水晰1231',
    age: 1,
  }],
  [Symbol.iterator]() {
    const $self = this;
    const {data} = $self
    let index = 0;
    return {
      next() {
        if (index < data.length) {
          return {
            value: $self[data[index++]],
            done: false
          };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

for (var i of obj){
  console.log(i);
}

打印结果如下:

image.png

对对象使用数组形式的解构赋值

解构赋值是ES6新增的针对对象和数组数据结构中提取值的一种模式,例如let [a, b, c] = [1, 2, 3],对等式右边的数组进行解构,分别赋值到变量a,b,c。

// ==正常情况下对对象或对数组解构赋值==
// let {c,d} = {c:1,d:2}
// let [c,d] = [c:1, d:2]

// ==可以对数组做对象形式的解构赋值==
// ==但由于对象的解构赋值是按键名来提取的,数组[1,2]对应的键名为0,1, 没有e和f对应键名, 所以e和f是undeined==
// let {e,f} = [1,2]
// console.log(e,f) // undefined,undefined

// ==但若是对对象做数组形式的解构赋值则不行,程序报错 Uncaught TypeError: {(intermediate value)(intermediate value)} is not iterable, 表示该对象不具备迭代器属性==
let [c,d] = {c:1, d:2} // 报错

那为什么不可以对对象做数组形式的解构赋值?

因为对数组解构赋值会默认调用Symbol.iterator方法,而对象本身没有Symbol.iterator方法。

事实上,只要某种数据结构具有Iterator接口即设置了遍历器, 都可以采用数组形式的解构赋值

下面给对象原型添加Symbol.iterator方法即迭代器:

Object.prototype[Symbol.iterator] = function () {
    // ==方式一:获取对象自身定义的属性值(Object.values(this)),根据索引(index)一个一个取==
    const $self = this
    const curValues = Object.values($self) // 自身所有的键对应的值且不包含键类型为Symbol的值
    let index = 0
    return {
        next() {
            return index < curValues.length 
                ? { value: curValues[index++] } 
                : { done: true }
        }
    }
    // ==方式二:利用数组的遍历器(Object.values(this))==
    // ==Object.values(this)获取当前对象的值组成的数组,而数组本身包含迭代器,所以可直接将该迭代器返回,由于迭代器本身是一个函数,所以需要执行该函数以在函数中返回==
    // return Object.values(this)[Symbol.iterator]()
}

执行对对象解构赋值语句:

const targetObj = {c:1, d:2}

// ==对对象做解构赋值操作==
let [c,d] = targetObj
console.log(c, d) // 1,2

// ==遍历对象==
for (let i of targetObj) {
    console.log(i) // 1/2
}

ok,完成对对象做数组形式的解构赋值~

总结

  • Symbol 表示的是独一无二的值,通过 Symbol() 函数或者 Symbol.for() 函数创建,是基本数据类型;
  • Symbol 可以做为对象的属性名(ES6之前只能是字符串);
  • Symbol 可以代替某些场景下的 const 常量;
  • Symbol 不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回,因为他们涉及的属性名是字符串;
  • 遍历对象的 Symbol 类型的属性,通过 Object.getOwnPropertySymbols(obj),但是只是一级的,不包含深层;Reflect.ownKeys() 可以获取对象的所有类型的键名(字符串和Symbol类型的);
  • Symbol.for() 全局登记特性,与 Symbol() 的区别在于调用多次前者返回的是相同的而后者返回的是不同的;
  • Symbol.keyFor() 返回一个已登记的 Symbol 类型值的 key,如果未登记则返回 undefined;
  • Symbol.iterator是ES6提供的内置值,定义遍历器(遍历器首先是一个函数,返回一个对象,该对象包含next方法,而next方法又返回一个包含value和done的对象);
  • 可以给非数组的数据结构定义遍历器,从而可以使用for...of循环遍历
  • 对对象做数组形式的解构,需要给对象添加Symbol.iterator方法即定义迭代器。

参考