ECMAScript 6 的新特性

161 阅读5分钟

ECMAScript一般缩写为ES,JavaScript是ECMAScript的拓展,ECMAScript只提供了最基本的语法。

Web中的JavaScript在ECMAScript的基础上拓展了BOM和DOM操作 Node中的JavaScript在ECMAScript的基础上拓展了fs,Net等。

所以说javascript语言本身指的就是ECMAScript。

从2015年开始ECMAScript保持着每年一个版本的迭代。 ES2015经历了近6年才被完全标准化,是ES的第六个版本,所以也称为ES6,而这六年恰好是web发展的黄金时期,而为了迎合互联网的高速发展,从ES2015开始,ECMAScript标准更改为一年一更新,同时以年份命名。

许多人喜欢使用ES6来泛指ES2015以后的所有版本,而例如ES6特性中的'await'、'async'等特性是在ES2017中的特性,也被人们称为是ES6的特性。

let const与块级作用域

作用域指代码中某个成员能够起作用的范围。

在ES2015之前,js只有两种作用域——全局作用域和函数作用域。而在ES2015中引入了新的作用域——块级作用域。

let

let 声明的成员只会在所声明的块中生效

foo变量在if块中如果为var定义,则在全局都可以访问,如果用let定义,那么编译会报错。

// let 声明的成员只会在所声明的块中生效 -------------------------------------------
if (true) {
  // var foo = 'zce'
  let foo = 'zce'
}
console.log(foo)

let 在 for 循环中的表现

在for循环中,内层定义的变量i会覆盖掉外层的变量i,当内层循环结束时,i的值变成3,外层循环同时结束。如果将内层循环的i用let定义,则在内层循环的i就是仅在内层循环块可用的i,和外层无关,循环会打印9次

// let 在 for 循环中的表现 ---------------------------------------------------
for (var i = 0; i < 3; i++) {
  for (var i = 0; i < 3; i++) {
    console.log(i)
  }
  console.log('内层结束 i = ' + i)
}
// 结果为
// 1
// 2
// 3
// 内层结束 i = 3

let 应用场景:循环绑定事件,事件处理函数中获取正确索引

当使用var循环为按钮绑定事件时,每次执行事件打印出的i都为3,因为打印的i始终是全局作用域中的i,这个时候要是用闭包将i改为函数作用域,就可以把循环中的i暂时保留下来,而使用let就免去了使用闭包的麻烦。

// let 应用场景:循环绑定事件,事件处理函数中获取正确索引 -----------------------------------------------------

var elements = [{}, {}, {}]
for (var i = 0; i < elements.length; i++) {
  elements[i].onclick = function () {
    console.log(i)
  }
}
elements[2].onclick()

var elements = [{}, {}, {}]
for (var i = 0; i < elements.length; i++) {
// 使用闭包将i固定到函数作用域
  elements[i].onclick = (function (i) {
    return function () {
      console.log(i)
    }
  })(i)
}
elements[0].onclick()

var elements = [{}, {}, {}]
for (let i = 0; i < elements.length; i++) {
  elements[i].onclick = function () {
    console.log(i)
  }
}
elements[0].onclick()

for 循环会产生两层作用域

我们在let定义的循环中使用let定义一个变量并不会覆盖掉for循环本身let 定义的变量,是因为let循环体内本身会形成一个if块级作用域。

// for 循环会产生两层作用域 ----------------------------------

for (let i = 0; i < 3; i++) {
  let i = 'foo'
  console.log(i)
}

let i = 0

if (i < 3) {
  let i = 'foo'
  console.log(i)
}

i++

if (i < 3) {
  let i = 'foo'
  console.log(i)
}

i++

if (i < 3) {
  let i = 'foo'
  console.log(i)
}

i++

let修复了变量声明提升的现象

js 可以在var定义之前访问变量,值为undefined,这是js的一个特性(bug),称为变量提升。为了修复这个特性呢,es2015中的let不再允许let定义之前访问变量。

// let 修复了变量声明提升现象 --------------------------------------------

console.log(foo) // undefined
var foo = 'zce'

console.log(foo) // 报错
let foo = 'zce'

Const

const可以用来声明一个只读的常量,变量一旦声明便不能修改。

const既然不能修改,那么声明和赋值必须在一条语句中进行,不能像var和let一样先声明在复制。

const不能修改只是指不能修改指向的内存地址,而不是不能修改对象中是属性。

// 恒量只是要求内层指向不允许被修改
const obj = {}
// 对于数据成员的修改是没有问题的
obj.name = 'zce'
// 不能修改指向的地址
obj = {}

一般我们在开发中不用var,var中的特性都属于开发中的陋习

默认使用const——因为const更明确我们声明的成员是不是会被修改

ES2015中数组的解构

解构

如果我们要挨个给数组中的变量赋值比较麻烦。可以直接对数组进行解构

// 数组的解构

const arr = [100, 200, 300]
const foo = arr[0]
const bar = arr[1]
const baz = arr[2]
console.log(foo, bar, baz)
// 直接解构
const [foo, bar, baz] = arr
console.log(foo, bar, baz)

解构数组指定位置的值

// 解构数组中指定位置的值
const [, , baz] = arr
console.log(baz)

解构指定位置后面所有的值返回一个数组

// 取出第几个后所有的元素,返回一个数组
const [foo, ...rest] = arr
console.log(rest)

解构不存在的索引的值

// 当不存在时数组值为undefined
const [foo, bar, baz, more] = arr
console.log(more)

为解构元素设置初始值,当不存在时赋值为初始值

// 设置初始值
const [foo, bar, baz = 123, more = 'default value'] = arr
console.log(bar, more)

善用解构可以简化代码

const path = '/foo/bar/baz'
const tmp = path.split('/')
const rootdir = tmp[1]

const [, rootdir] = path.split('/')
console.log(rootdir)

对象的解构

对象的解构通过属性名去解构属性。

// 对象的解构
const obj = { name: 'zce', age: 18 }

const { name } = obj
console.log(name)

对象的解构和数组的解构类似,解构不存在的属性时都会出现undefined,都可以在解构的时候为不存在的属性设置默认值。

需要注意的是,如果解构的数组属性有和上文中定义的一致的属性那么编译就会报错,可以使用冒号属性名的方式为解构的属性附上一个新名字

const name = 'tom'
const { name: objName = 'jack' } = obj
console.log(objName)

解构可以在很多时候简化我们的代码,比如我们要多次调用console的log方法我们可以把log从console中解构出来

const { log } = console
log('foo')
log('bar')
log('123')

模板字符串

es2015中新增了一种模板字符串,他直接使用反引号``标识,它允许字符串换行。

// 允许换行
const str = `hello es2015,
this is a \`string\``

console.log(str)

可以通过${}插入表达式,比传统字符串相加的方式更易读。

const name = 'tom'
// 可以通过 ${} 插入表达式,表达式的执行结果将会输出到对应位置
const msg = `hey, ${name} --- ${1 + 2} ---- ${Math.random()}`
console.log(msg)

带标签的模板字符串

模板字符串还有一个特殊的用法,就是在模板字符串之前定义一个标签,这个标签就是一个函数,使用这个标签就是调用了这个函数。

// 使用这个标签就是调用这个函数
const str = console.log`hello world`
// 输出['hello world']

这个函数接受到的第一个参数是一个将模板字符串的静态内容分割过后的一个数组,后面的参数是接收到的是模板字符串的插值。 模板字符串标签的作用就是对模板字符串的内容进行加工,更适合用户的阅读。

const name = 'tom'
const gender = false

function myTagFunc (strings, name, gender) {
  // console.log(strings, name, gender)
  // return '123'
  const sex = gender ? 'man' : 'woman'
  return strings[0] + name + strings[1] + sex + strings[2]
}

const result = myTagFunc`hey, ${name} is a ${gender}.`

console.log(result)

字符串扩展方法

es2015的string拓展了

  • includes()
  • startWith()
  • endWith() 相对与之前的字符串分割和正则,这样的字符串查找方法更便捷
// 字符串的扩展方法
const message = 'Error: foo is not defined.'
console.log(
  message.startsWith('Error'),
  message.endsWith('.'),
  message.includes('foo')
)

参数设置默认值

在es2015之前我们为函数设置默认值需要在函数体中实现,很多人喜欢使用短路运算的方式设置默认符,实际上这是错误的。

function foo (enable) {
  // 短路运算很多情况下是不适合判断默认参数的,例如 0 '' false null
  // enable = enable || true
  //正确的写法
  enable = enable === undefined ? true : enable
  console.log('foo invoked - enable: ')
  console.log(enable)
}

当我们传入foo函数的值为0、false或者null时,enable也会被错误的设置上默认值true。所以应该去判断是否为undefined。

在es2015设置默认值就简单得多,需要注意的是,默认值一定要在形参列表的最后。

// 默认参数一定是在形参列表的最后
function foo (enable = true) {
  console.log('foo invoked - enable: ')
  console.log(enable)
}

foo(false)

剩余参数

在es5及之前,我们使用arguments对象来获取函数中的参数,arguments是一个伪数组。

// 剩余参数

// function foo () {
//   console.log(arguments)
// }

function foo (first, ...args) {
  console.log(args)
}

foo(1, 2, 3, 4)

我们可以使用...args去表示剩下的所有参数,args是所有参数的数组

展开数组

如果我们要打印数组中每一个元素的值,我们可能需要一个一个数组下标的打印,那么在es2015之前我们可以使用函数的apply方法去调用console.log()方法,在es2015之后我们可以使用...的方式去展开数组

const arr = ['foo', 'bar', 'baz']

console.log(
  arr[0],
  arr[1],
  arr[2],
)

console.log.apply(console, arr)

console.log(...arr)

箭头函数

箭头函数"=>"简化了普通函数的代码

c (number) {
  return number + 1
}
// 最简方式
const inc = n => n + 1
// 完整参数列表,函数体多条语句,返回值仍需 return
const inc = (n, m) => {
  console.log('inc invoked')
  return n + 1
}

箭头函数常用于回调中,能大大减少代码量,使程序更易读

const arr = [1, 2, 3, 4, 5, 6, 7]

arr.filter(function (item) {
  return item % 2
})

// 常用场景,回调函数
arr.filter(i => i % 2)

箭头函数的this

在普通函数中,this指向一直是调用这个函数的对象,而箭头函数不会改变this的指向

// 箭头函数不会改变 this 指向

const person = {
  name: 'tom',
  // sayHi: function () {
  //   console.log(`hi, my name is ${this.name}`)
  // }
  sayHi: () => {
    console.log(`hi, my name is ${this.name}`)
  },
  sayHiAsync: function () {
    const _this = this
    setTimeout(function () {
      console.log(_this.name)
    }, 1000)

    console.log(this)
    setTimeout(() => {
      console.log(this.name)
      console.log(this)
    }, 1000)
  }
}
person.sayHiAsync()

在sayHiAsync中普通函数this的指向发生了变化,需要在外层方法体内给重新给this赋值拿到外层的this,才能获取到person的this,而箭头函数则不会改变this的指向

对象字面量增强

传统对象要求我们定义对象要使用属性: 变量的方式,在es2015中,如果属性名和变量名相同,则可以省略属性:直接使用变量,同时可以省略方法的:funtion, 还可以通过[表达式]为对象添加动态属性名,属性名为表达式计算结果

const obj = {
  foo: 123,
  // bar: bar
  // 属性名与变量名相同,可以省略 : bar
  bar,
  // method1: function () {
  //   console.log('method111')
  // }
  // 方法可以省略 : function
  method1 () {
    console.log('method111')
    // 这种方法就是普通的函数,同样影响 this 指向。
    console.log(this)
  },
  // Math.random(): 123 // 不允许
  // 通过 [] 让表达式的结果作为属性名
  [bar]: 123
}

Object 对象方法

Object.assign

es2015中可以使用Object.assign方法,让后面对象的属性去覆盖前面对象的属性

// Object.assign 方法
const source1 = {
  a: 123,
  b: 123
}

const source2 = {
  b: 789,
  d: 789
}

const target = {
  a: 456,
  c: 456
}

const result = Object.assign(target, source1, source2)

console.log(target)
console.log(result === target)

当我们想对对象的属性做出操作又不想修改源对象时,我们可以利用Object.assign方法去做对象的拷贝

// 应用场景
function func (obj) {
  // obj.name = 'func obj'
  // console.log(obj)

  const funcObj = Object.assign({}, obj)
  funcObj.name = 'func obj'
  console.log(funcObj)
}

const obj = { name: 'global obj' }

func(obj)
console.log(obj)

Object.is

es5中我们去判断两个对象是否相等一般采用===的方式,用===去判断+0和-0是相等的,NaNNaN是不相等的,使用Object.is可以正确的判断他们

Proxy

Proxy是访问代理器,可以想象成对象的门卫,es5中使用object.defineProperty()来进行对象的访问控制,Proxy比object.defineProperty()更为强大。

Proxy构造函数的第一个参数为被代理的对象,第二个对象是处理对象,处理对象传入处理的函数。处理函数的返回值作为对Proxy做各种处理后返回的结果。

// Proxy 对象

const person = {
  name: 'zce',
  age: 20
}

const personProxy = new Proxy(person, {
  // 监视属性读取
  get (target, property) {
    return property in target ? target[property] : 'default'
    // console.log(target, property)
    // return 100
  },
  // 监视属性设置
  set (target, property, value) {
    if (property === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError(`${value} is not an int`)
      }
    }

    target[property] = value
    // console.log(target, property, value)
  }
})

personProxy.age = 100

personProxy.gender = true

console.log(personProxy.name)
console.log(personProxy.xxx)

Proxy对比object.defineProperty()的优势

监视读写以外的操作

object.defineProperty()只能监视到对象的读写操作,而Proxy可以监视到Delete等对象操作,和方法的调用等等。

// 优势1:Proxy 可以监视读写以外的操作 --------------------------
const person = {
  name: 'zce',
  age: 20
}

const personProxy = new Proxy(person, {
  deleteProperty (target, property) {
    console.log('delete', property)
    delete target[property]
  }
})
delete personProxy.age
console.log(person)

可以监视到数组的操作

object.defineProperty()中监视数组的实现依靠通过重写数组的操作方法来监视数组操作,vue2中就是这么做的。Proxy可以直接监视到数组

// 优势2:Proxy 可以很方便的监视数组操作 --------------------------
const list = []

const listProxy = new Proxy(list, {
  set (target, property, value) {
    console.log('set', property, value)
    target[property] = value
    return true // 表示设置成功
  }
})

listProxy.push(100)
listProxy.push(100)

以非侵入的方式监视成员的操作

object.defineProperty()靠重写对象的方法来实现监视成员操作,而Proxy不需要对原对象做任何的修改即可监视操作

// 优势3:Proxy 不需要侵入对象 --------------------------

const person = {}

Object.defineProperty(person, 'name', {
  get () {
    console.log('name 被访问')
    return person._name
  },
  set (value) {
    console.log('name 被设置')
    person._name = value
  }
})
Object.defineProperty(person, 'age', {
  get () {
    console.log('age 被访问')
    return person._age
  },
  set (value) {
    console.log('age 被设置')
    person._age = value
  }
})

person.name = 'jack'

console.log(person.name)

// Proxy 方式更为合理
const person2 = {
  name: 'zce',
  age: 20
}

const personProxy = new Proxy(person2, {
  get (target, property) {
    console.log('get', property)
    return target[property]
  },
  set (target, property, value) {
    console.log('set', property, value)
    target[property] = value
  }
})

personProxy.name = 'jack'

console.log(personProxy.name)

Reflect

Reflect是ES2015中提供的一个统一对象操作API,Reflect是一个静态类,他不能通过new去生成一个对象,Reflect内部封装了一系列对对象底层的操作。提供了14个静态方法,有一个已被废弃。

image.png image.png 我们查阅文档即可发现,Reflect对象提供的方法和Proxy处理方法对象成员完全一致。意思是说当我们不给Proxy传入第二个参数时,我们调用Proxy的get方法依然会返回对象的属性,那么get方法的默认实现就是使用了Reflect.get()方法,所以当我们为Proxy的处理对象增加逻辑时,返回值最终应该返回Reflect的静态方法。

// Reflect 对象

const obj = {
  foo: '123',
  bar: '456'
}

const proxy = new Proxy(obj, {
  get (target, property) {
    console.log('watch logic~')
    
    return Reflect.get(target, property)
  }
})

console.log(proxy.foo)

那么为什么要有Reflect静态类呢,我们之前在做对象操作的时候,可能使用Object对象提供的方法,也可能使用delete,in等操作符,让代码显得更混乱,使用Reflect统一了对象的操作方式。

console.log('name' in obj)
console.log(delete obj['age'])
console.log(Object.keys(obj))
// 使用Reflect方法
console.log(Reflect.has(obj, 'name'))
console.log(Reflect.deleteProperty(obj, 'age'))
console.log(Reflect.ownKeys(obj))

Promise

Promise是es2015中一种更优的异步编程解决方案,链式调用的方式解决了js中回调函数嵌套过深的问题。在另一篇笔记有详细的介绍Promise的用法和实现。

Class类

在es5中是通过定义函数和函数的原型对象实现的类型。

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

Person.prototype.say = function () {
  console.log(`hi, my name is ${this.name}`)
}

在es2015中新增了class关键字来声明对象类型,写法类似于主流的后端语法,更易于理解。

class Person {
  constructor (name) {
    this.name = name
  }

  say () {
    console.log(`hi, my name is ${this.name}`)
  }
}

const p = new Person('tom')
p.say()

静态方法

静态方法可以通过类名直接访问函数

// static 方法

class Person {
  constructor (name) {
    this.name = name
  }

  say () {
    console.log(`hi, my name is ${this.name}`)
  }

  static create (name) {
    return new Person(name)
  }
}

const tom = Person.create('tom')
tom.say()

需要注意的是静态方法是挂载在类型上的,内部的this不会指向实例对象,而是当前的类型。

类的继承

通过extends可以继承父类的属性和方法,super可以返回父类对象

// extends 继承

class Person {
  constructor (name) {
    this.name = name
  }

  say () {
    console.log(`hi, my name is ${this.name}`)
  }
}

class Student extends Person {
  constructor (name, number) {
    super(name) // 父类构造函数
    this.number = number
  }

  hello () {
    super.say() // 调用父类成员
    console.log(`my school number is ${this.number}`)
  }
}

const s = new Student('jack', '100')
s.hello()

Set数据结构

Set是ES2015提供的一个全新的数据集合,Set中的值不允许重复,它是一个类型。

通过new构建Set实例,add()方法向集合中添加数据,返回集合本身,所以可以链式调用。如果添加了已存在的,那么这个值会被忽略掉

// Set 数据结构

const s = new Set()

s.add(1).add(2).add(3).add(4).add(2)

console.log(s)

可以通过size属性获取集合的长度

通过has()判断集合中是否存在这个元素

通过delete()删除元素,delete()会返回删除成功true

clear()方法会清除集合中的全部内容

console.log(s.size)

console.log(s.has(100))

console.log(s.delete(3))

s.clear()
console.log(s)

Set最常见的应用场景就是为数组中重复的元素去重。

const arr = [1, 2, 1, 3, 4, 1]

const result = Array.from(new Set(arr))
const result = [...new Set(arr)]

console.log(result)

new Set()返回的值是一个Set集合,可以使用Array.from()或者[...new Set(arr)]将Set转回数组。

map数据结构

假如我们尝试给对象的键设置一个bool值,数字或者对象等,都会将toString()的结果作为这个对象的键。

const obj = {}
obj[true] = 'value'
obj[123] = 'value'
obj[{ a: 1 }] = 'value'

console.log(Object.keys(obj)) // ['true','123,'[object Object]']
console.log(obj['[object Object]'])

使用map数据结构可以将特殊值设置成数据的键。

const m = new Map()

const tom = { name: 'tom' }

m.set(tom, 90)

console.log(m)

console.log(m.get(tom))

// m.has()
// m.delete()
// m.clear()

m.forEach((value, key) => {
  console.log(value, key)
})

map 使用set()方法赋值,get()方法获取某一个值,还有has() delete() clear()等方法。

WeakMap

map还有一个弱引用版本WeakMap,差异就是 Map 中会对所使用到的数据产生引用 即便这个数据在外面被消耗,但是由于 Map 引用了这个数据,所以依然不会回收 而 WeakMap 的特点就是不会产生引用,一旦数据销毁,就可以被回收,所以不会产生内存泄漏问题。

  • WeakMap只接受对象作为key,如果设置其他类型的数据作为key,会报错。
  • WeakMap的key所引用的对象都是弱引用,只要对象的其他引用被删除,垃圾回收机制就会释放该对象占用的内存,从而避免内存泄漏。
  • 由于WeakMap的成员随时可能被垃圾回收机制回收,成员的数量不稳定,所以没有size属性。
  • 没有clear()方法
  • 不能遍历

Symbol

假设我们有一个全局共享的缓存对象,如果我们多个js文件中都像缓存对象中添加内容,当不同的文件像对象中添加key相同的string就会产生冲突。symbol就可以解决这个问题。

symbol意思是符号,表示一个独一无二的值,两个Symbol永远不会相等。

// 两个 Symbol 永远不会相等
console.log(
  Symbol() === Symbol() // false
)

从es2015开始,对象的属性名可以是string也可以是symbol。

// 使用 Symbol 为对象添加用不重复的键

const obj = {}
obj[Symbol()] = '123'
obj[Symbol()] = '456'
console.log(obj)

symbol也可以在计算属性名中使用

// 也可以在计算属性名中使用

const obj = {
  [Symbol()]: 123
}
console.log(obj)

我们可以使用symbol作为计算属性名去模拟对象的私有成员。

// =========================================================

// 案例2:Symbol 模拟实现私有成员

// a.js ======================================

const name = Symbol()
const person = {
  [name]: 'zce',
  say () {
    console.log(this[name])
  }
}
// 只对外暴露 person

// b.js =======================================

// 由于无法创建出一样的 Symbol 值,
// 所以无法直接访问到 person 中的「私有」成员
// person[Symbol()]
person.say()

Symbol最主要的作用就是为对象添加一个独一无二的属性名

因为Symbol是独一无二的,无论传入的描述文本是否相同,Symbol都是不相等的

console.log(
    Symbol() === Symbol()           // false
    Symbol('foo') === Symbol('foo') // false
)

如果要在全局复用一个相同Symbol的值可以用全局变量的方式实现,或者使用Symbol提供的一个静态方法for去实现。

// Symbol 全局注册表 ----------------------------------------------------

const s1 = Symbol.for('foo')
const s2 = Symbol.for('foo')
console.log(s1 === s2)

这个方法维护了一个全局注册表,为传入的字符串和Symbol值提供了一个一一对应的关系。

如果for方法传入的不是字符串,会被转化为字符串 console.log(Symbol.for(true) === Symbol.for('true'))

Symbol类型中还提供了很多内置方法的常量,来作为标识,比如

console.log(Symbol.iterator)
console.log(Symbol.hasInstance)

类似于String的toString方法,我们也可以为对象添加一个Symbol的tag方法

const obj = {
  [Symbol.toStringTag]: 'XObject'
}
console.log(obj.toString()) //[object XObject]

for...in...和Object.keys方法还有JSON.stringfy序列化获取不到这样的Symbol属性名。 我们可以通过Object.getOwnPropertySymbols()方法去获取Symbol属性名。

for...of...循环

在es2015之前我们可以使用for循环,for..in循环foreach方法等,这些方法都有一定局限性。es2015定义了for...of...循环,作为遍历所有数据解构的统一方式。

在for of中可以随时使用break终止循环,而在foreach不能跳出循环。

// for...of 循环可以替代 数组对象的 forEach 方法

arr.forEach(item => {
  console.log(item)
})

for (const item of arr) {
  console.log(item)
  if (item > 100) {
    break
  }
}

// forEach 无法跳出循环,必须使用 some 或者 every 方法

arr.forEach() // 不能跳出循环
arr.some()
arr.every()

不仅数组可以用for of遍历,伪数组也可以用for of遍历

Set对象和Map对象都可以使用for of遍历

不同的是因为Map是键值结构,每次遍历的值都是一个数组,存放键和值,我们可以直接把他解构出来返回结果

// 遍历 Map 可以配合数组结构语法,直接获取键值
const m = new Map()
m.set('foo', '123')
m.set('bar', '345')

for (const [key, value] of m) {
  console.log(key, value)
}

对象是不能通过for..of遍历的,会抛出异常obj is not iterable

可迭代接口iterable

ES中表示有结构的数据越来越多,为了提供统一的遍历方式,ES2015中提供了Iterable接口,类似于String实现了toString接口一样。实现Iterable接口是使用for..of的前提

在数组,Set,Map等实现了Iterable接口的数据集中,原型链上有一个Symbol(Symbol.iterator)的方法。

我们尝试定义一个数组并调用它的iterator方法,会发现这个方法返回一个Array Iterator对象。对象中有一个next方法,调用next方法返回一个对象,对象中有value,为数组的第一个元素,done为false。再次调用next方法,value为下一个元素,直到最后一个元素的下一个元素 value为undefined的时候done为true。

const set = new Set(['foo', 'bar', 'baz'])

const iterator = set[Symbol.iterator]()

console.log(iterator.next())  // {value: 'foo', done: false}
console.log(iterator.next())  // {value: 'bar', done: false}
console.log(iterator.next())  // {value: 'baz', done: false}
console.log(iterator.next())  // {value: undefined, done: true}
console.log(iterator.next())  // {value: undefined, done: true}

实现可迭代接口

对象是不能被for..of迭代的,我们可以给对象挂载上Symbol.iterable的计算属性来实现它

const obj = {
  [Symbol.iterator]: function () {
    return {
      next: function () {
        return {
          value: 'zce',
          done: true
        }
      }
    }
  }
}

迭代器接口要有一个返回迭代器函数的Symbol.iterator属性,这个函数属性叫做Iterable,函数返回一个Interator对象,对象中要实现next方法,next方法返回IterationResult对象。

向这个对象中添加一些元素,并修改next方法使得for of循环可以遍历这个对象的值。


const obj = {
  store: ['foo', 'bar', 'baz'],

  [Symbol.iterator]: function () {
    let index = 0

    return {
      next: ()  => {
        const result = {
          value: this.store[index],
          done: index >= this.store.length
        }
        index++
        return result
      }
    }
  }
}


for (const item of obj) {
  console.log('循环体', item)
}

迭代器模式

让自定义对象实现可迭代接口,从而让对象可以被for of遍历的设计模式叫做迭代器模式。

假设我们有一个对象中存放了两个属性分别为两个数组,如果外部的文件需要访问这两个数组则需要分别遍历对象的两个属性。如果此时对象中增加了一个属性,那么我们也需要相应的修改外部的代码,一是代码耦合严重,而是增加了不必要的循环代码。

const todos = {
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '外语'],
  work: ['喝茶'],
}
// 其他文件 ===============================
for (const item of todos.life) {
  console.log(item)
}
for (const item of todos.learn) {
  console.log(item)
}
for (const item of todos.work) {
  console.log(item)
}

我们可以对外开放一个each方法去对对象中的数组整合

const todos = {
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '外语'],
  work: ['喝茶'],

  // 提供统一遍历访问接口
  each: function (callback) {
    const all = [].concat(this.life, this.learn, this.work)
    for (const item of all) {
      callback(item)
    }
  },
}

// 其他文件 ===============================
todos.each(function (item) {
  console.log(item)
})

我们也使用迭代器设计模式,就能解耦


const todos = {
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '外语'],
  work: ['喝茶'],


  // 提供迭代器(ES2015 统一遍历访问接口)
  [Symbol.iterator]: function () {
    const all = [...this.life, ...this.learn, ...this.work]
    let index = 0
    return {
      next: function () {
        return {
          value: all[index],
          done: index++ >= all.length
        }
      }
    }
  }
}

// 其他文件 ===============================
for (const item of todos) {
  console.log(item)
}

生成器

Generator的出现是为了减少异步编程中回调函数的嵌套。 在函数的前面加*就是生成器函数。

function * foo () {
  console.log('zce')
  return 100
}

const result = foo()
console.log(result.next()) //{ value: 100, done: true}

我们可以看到调用生成器函数的next方法也会返回一个IterationResult对象。生成器对象也对接了Iterator接口。

生成器函数会生成一个生成器对象,当我们调用next()方法的时候生成器函数才会开始执行,直到执行到yield语句的地方返回一个yield的值,函数暂停,直到下一次调用next()方法才会继续执行,周而复始,直到执行到最后一个yield的值,再调用next()的时候会返回value为undefined

生成器的应用

最简单的生成器的应用场景,我们可以写一个发号器,每次调用这个方法我们都能返回一个新的id值,因为被yield暂停,所以这不是一个死循环。

// 案例1:发号器

function * createIdMaker () {
  let id = 1
  while (true) {
    yield id++
  }
}

const idMaker = createIdMaker()

console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)

可以使用Generator实现iterator方法

// 案例2:使用 Generator 函数实现 iterator 方法

const todos = {
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '外语'],
  work: ['喝茶'],
  [Symbol.iterator]: function * () {
    const all = [...this.life, ...this.learn, ...this.work]
    for (const item of all) {
      yield item
    }
  }
}

for (const item of todos) {
  console.log(item)
}

生成器函数最重要的作用还是为了解决JavaScript异步编程嵌套过深的问题。在异步编程的笔记中和Promise一起来看他们的具体用法。

ES 2016

发布于2016年6月,是一个小版本,只包含两个小功能

includes方法

过去我们检查数组中是否存在某个元素一般使用数组的indexOf()方法,如果存在则返回数组下标不存在返回-1。但他不能用于查找数组中的NaN。 includes方法可以直接返回bool值

指数运算符

过去要做指数运算我们需要调用Math.pow()方法,例如3的5次方就是Math.pow(3,5),在ES2016中指数运算符像加减乘除一样变成了一个运算符**。比如2的十次方就是2 ** 10

ES 2017

是ES的第八个版本

Object对象的拓展方法

Object.values

Object.key()返回的是对象中所有的键组成的数组 Object.values()返回的是对象中所有的值组成的数组。

Object.entries

Object.entries()方法以数组的形式返回对象中的键值对

const obj = { foo: 'value1', bar: 'value2' }
console.log(obj) // [['foo', 'value1'], ['bar', 'value2']]

那么我们就可以直接通过for..of循环去遍历数组中的属性

for (const [key, value] of Object.entries(obj)) {
    console.log(key,value)
}

因为Map类型的对象接受的就是这样的键值对,所以我们可以将Entries对象转换成Map对象new Map(Object.entries(obj))

Object.getOwnPropertyDescriptors

getOwnPropertyDescriptors的作用是获取属性的完整描述信息。

ES2015之后就可以为对象定义Getter Setter属性,Getter和Setter属性是不能通过Object.assign方法去完全复制的。

const p1 = {
    firstName: 'chu',
    lastName: 'bin'
    get fullName () {
        return this.firstName + ' ' + this.lastName
    }
}
p2 = Object.assign({}, p1)
p2.lastName = 'bonian'
console.log(p2.fullName) // chu bin

这是因为Object.assign去复制的时候只是将get当作一个普通的属性去复制了,所以才会出现这种情况。 这个时候我们可以使用getOwnPropertyDescriptors去获取对象中属性的完整描述信息,然后在使用Object.defineProperties()方法去复制对象。

const descriptors = Object.getOwnPropertyDescriptors(p1)
const p2 = Object.defineProperties({}, descriptors)
p2.firstName = 'bonian'
console.log(p2.fullname) // chu bonian

String.prototype.padStart/padEnd

padStart方法可以让字符串文本对齐。 比如我们要将字符串的后面固定16位,多出的部分使用'-'补齐,数字的前面保留三位,少的部分前面加0

const books = {
    html: 5,
    css: 16,
    javascript: 128
}
for ( const [name, count] of Object.entries(books)) {
    console.log(`${name.padEnd(16, '-')}|${count.toString().padStart(3,0)}`)
}

在函数参数列表中添加尾逗号

es2017允许在对象和函数的参数列表的最后一位添加尾逗号

Async/Await

async 和 await彻底解决了函数回调嵌套地狱的问题