ECMAScript 新特性

242 阅读3分钟

概要

  • ECMAScript 与 JavaScript
  • ECMAScript的发展过程
  • ECMAScript 2015的新特性
  • And More...

ECMAScript与JavasScript

ECMAScript也是一门脚本语言,缩写为ES。通常看做为JavaScript的标准化规范。

实际上,JavaScript是ECMAScript的扩展语言,ECMAScript只提供了最基本的语法,约定了代码该如何编写,如何定义变量、函数,如何实现循环等,只停留在语言层面,并不能完成我们实际的功能开发。

而JavaScript实现了ECMAScript的语言标准,并且在这个基础之上做了扩展,使得我们可以在浏览器中环境中操作DOM和BOM以及在node环境中读写文件操作等。

总的来说,浏览器环境中 JavaScript等于ECMAScript + Web提供的API,Node环境中Javascript等于ECMAScript + Node提供的API(fs,net模块等)。

ECMAScript的发展过程

ECMAScript 2015的新特性

  • 解决原有语法的不足
  • 对原有语法进行增强
  • 全新的对象、全新的方法、全新的功能
  • 全新的数据类型和数据结构

let与块级作用域

  1. let 声明的成员只会在所声明的块中生效
if (true) {
  let foo = 'zce'
  console.log(foo)
}
console.log(foo) //ReferenceError: foo is not defined
  1. let 在 for 循环中的表现
for (var i = 0; i < 3; i++) {
  for (var i = 0; i < 3; i++) {
    console.log(i)
  }
  console.log('内层结束 i = ' + i) // 内层结束 i = 3
}

for (var i = 0; i < 3; i++) {
  for (let i = 0; i < 3; i++) {
    console.log(i)
  }
  console.log('内层结束 i = ' + i)
  // 0
  // 1
  // 2
  // 内层结束 i = 0
  // 0
  // 1
  // 2
  // 内层结束 i = 1
  // 0
  // 1
  // 2
  // 内层结束 i = 2
}
  1. let 应用场景:循环绑定事件,事件处理函数中获取正确索引
var elements = [{}, {}, {}]
for (var i = 0; i < elements.length; i++) {
  elements[i].onclick = function () {
    console.log(i)
  }
}
elements[0].onclick() // 3
elements[1].onclick() // 3
elements[2].onclick() // 3
// ------------使用let----------
var elements = [{}, {}, {}]
for (let i = 0; i < elements.length; i++) {
  elements[i].onclick = function () {
    console.log(i)
  }
}
elements[0].onclick() // 0
elements[1].onclick() // 1
elements[2].onclick() // 2
// ------------利用闭包实现----------
var elements = [{}, {}, {}]
for (var i = 0; i < elements.length; i++) {
  elements[i].onclick = (function (i) {
    return function () {
      console.log(i)
    }
  })(i)
}
elements[0].onclick() // 0
elements[1].onclick() // 1
elements[2].onclick() // 2
  1. for 循环会产生两层作用域
for (let i = 0; i < 3; i++) {
  let i = 'foo'
  console.log(i)
}

// => foo
// => foo
// => foo

相当于

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++
  1. let 修复了变量声明提升现象
console.log(foo) // undefined
var foo = 'zce'
console.log(foo) // ReferenceError: Cannot access 'foo' before initialization
let foo = 'zce'

const

let基础上多了「只读」

  1. 声明过后不允许重新赋值
const name = 'zce'
name = 'jack'  // TypeError: Assignment to constant variable.
  1. 要求声明同时赋值
const name
name = 'zce' // SyntaxError: Missing initializer in const declaration
  1. 只要求指向的地址不被修改,对于数据成员的修改是没有问题的
const obj = {}
obj.name = 'zce'

const obj = {} // TypeError: Assignment to constant variable.

数组的解构

  1. 提取指定位置的成员
const arr = [100, 200, 300]
const [foo, bar, baz] = arr
console.log(foo, bar, baz) // 100 200 300
const [foo, bar, baz, more] = arr
console.log(more)  // undefined
const [, , baz] = arr
console.log(baz) // 300
  1. 提取后面所有成员 ...res
const [foo, ...rest] = arr
console.log(rest) // [200, 300]
  1. 默认值
const [foo, bar, baz = 123, more = 'default value'] = arr
console.log(bar, baz, more)  // 200 300 default value

对象的解构

  1. 根据属性名匹配
const obj = { name: 'zce', age: 18 }

const { name } = obj
console.log(name)
  1. 重命名
const { name: objName } = obj
  1. 默认值
const { name: objName = 'jack' } = obj

模板字符串

  1. 反引号包裹
const str = `hello es2015, this is a string`
  1. 允许换行
const str = `hello es2015,

this is a string`
  1. `在字符串中使用需要转义
const str = `hello es2015,

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

带标签的模板字符串

模板字符串的标签就是一个特殊的函数,使用这个标签就是调用这个函数

const str = console.log`hello world` // [ 'hello world' ]

const name = 'tom'
const gender = false

function myTagFunc (strings, name, gender) {
  console.log(strings, name, gender) // [ 'hey, ', ' is a ', '.' ] tom false
  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) // hey, tom is a woman.

字符串扩展方法

  • includes
  • startsWith
  • endsWith
const message = 'Error: foo is not defined.'

console.log(
  // message.startsWith('Error') true
  // message.endsWith('.') true
  message.includes('foo') //true
)

函数参数的默认值

// 默认参数一定是在形参列表的最后
function foo (paramter1, paramter2, enable = true) {
  console.log(enable) // false
}

foo(1, 2, false)

函数参数的剩余参数

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

foo(1, 2, 3, 4)  // [2, 3, 4]

展开数组

const arr = ['foo', 'bar', 'baz']
console.log(...arr) // foo bar baz

箭头函数

简化了函数的定义

// function inc (n) {
//   return n + 1
// }

// 最简方式
const inc = n => n + 1

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

箭头函数与 this

  • 不会改变 this 指向
  • 箭头函数外面的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()

// {
//   name: 'tom',
//   sayHi: [Function: sayHi],
//   sayHiAsync: [Function: sayHiAsync]
// }
// {
//   name: 'tom',
//   sayHi: [Function: sayHi],
//   sayHiAsync: [Function: sayHiAsync]
// }

对象字面量的增强

  • 属性名与变量名一致,可省略:变量名
  • 方法可直接写,不必再通过:function
  • 可使用计算属性名,不必在外面再通索引例如 obj[Math.random()] = 123
const bar = '345'

const obj = {
  foo: 123,
  // bar: bar
  // 属性名与变量名相同,可以省略 : bar
  bar,
  // method1: function () {
  //   console.log('method111')
  // }
  // 方法可以省略 : function
  method1 () {
    console.log('method11')
    console.log(this)
  },
  // Math.random(): 123 // 不允许
  // 通过 [] 让表达式的结果作为属性名
  [bar]: 123
}

// obj[Math.random()] = 123

console.log(obj)
obj.method1()

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) // { a: 123, c: 456, b: 789, d: 789 }
console.log(result === target) // true
// 复制对象
const obj = { name: 'old obj' }
const newObj = Object.assign({}, obj)
newObj.name = 'new obj'

Object.is 对象扩展方法

判断两个值是否相等

主要是注意以下两个值

+0 -0

NaN NaN

console.log(
  0 == false,              // => true
  0 === false,             // => false
  +0 === -0 ,              // => true
  NaN === NaN,             // => false
  Object.is(+0, -0),       // => false
  Object.is(NaN, NaN),     // => true
)

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.age) // 100
console.log(personProxy.xxx) // default
console.log(personProxy.gender) // true

Proxy 对比 Object.defineProperty()

  • 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)

// delete age
// { name: 'zce' }
  • Proxy 可以很方便的监视数组操作(Object.defineProperty()通过重写数组的操作方法实现监听)
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)

// set 0 100
// set length 1
// set 1 100
// set length 2
  • Proxy 以非侵入的方式监管了对象的读写
// -----------Object.defineProperty-----------

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)

// name 被设置
// name 被访问
// jack
// ------------------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)

// set name jack
// get name
// jack

Reflect

  • Reflect属于一个静态类(不能new实例,只能调用其静态方法)
  • Reflect成员方法就是Proxy处理处理对象的默认实现
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)

// watch logic~
// 123
  • 提供一套用于操作对象的API,统一了对象的操作方式
const obj = {
  name: 'zce',
  age: 18
}

// console.log('name' in obj)
// console.log(delete obj['age'])
// console.log(Object.keys(obj))

console.log(Reflect.has(obj, 'name'))
console.log(Reflect.deleteProperty(obj, 'age'))
console.log(Reflect.ownKeys(obj))

Promise

  • 更优的异步编程方案,解决了异步编程中回调函数嵌套过深的问题
  • 后面将会有独立的文章讲解

class 类

  • 基本使用
// function Person (name) {
//   this.name = name
// }

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

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

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

const p = new Person('tom')
p.say()
  • 静态方法(只能通过类名.访问)
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()
  • 类的继承(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

  • 里面的元素不允许重复
  • 常用API
const s = new Set()

s.add(1).add(2).add(3).add(4).add(2)
s.has(100)
s.delete(3)
s.clear()
...
  • 应用场景:数组去重
const arr = [1, 2, 1, 3, 4, 1]

// const result = Array.from(new Set(arr))
const result = [...new Set(arr)]
console.log(result) // => [ 1, 2, 3, 4 ]

Map

  • 普通对象都会将键toString
const obj = {}
obj[true] = 'value'
obj[123] = 'value'
obj[{ a: 1 }] = 'value'

console.log(Object.keys(obj))
console.log(obj['[object Object]'])

// => [ '123', 'true', '[object Object]' ]
// => value
  • Map是严格意义上的键值对集合,映射任意两个数值类型的对应关系
const m = new Map()

const tom = { name: 'tom' }

m.set(tom, 90)

console.log(m)

console.log(m.get(tom))

// => Map { { name: 'tom' } => 90 }
// => 90

Symbol

  • 符号,表示独一无二的值
// 使用 Symbol 为对象添加用不重复的键
const obj = {}
obj[Symbol()] = '123'
obj[Symbol()] = '456'
console.log(obj)
  • Symbol 模拟实现私有成员
// a.js ======================================

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

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

// 由于无法创建出一样的 Symbol 值,
// 所以无法直接访问到 person 中的「私有」成员
person[Symbol()] // => undefined
person.say() // => zce
  • 每次调用Symbol创建的值都是唯一的,无论传进的描述是否一样
console.log(
  Symbol() === Symbol(),
  Symbol('foo') === Symbol('foo')
)
// => false false
  • 调用相同值可通过全局变量,或Symbol.for('xxxx') xxx自动toString
const s1 = Symbol.for('foo')
const s2 = Symbol.for('foo')
console.log(s1 === s2)

// => true
  • 提供内置常量做内部方法的标识,这些表示可以让自定义对象去实现一些JS内置接口
// console.log(Symbol.iterator)
// console.log(Symbol.hasInstance)

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

// => [object XObject]
  • 使用Symbol作为对象的属性值,通过传统的for in 循环等是无法拿到的,所以适合对象的私有属性
const obj = {
  [Symbol()]: 'symbol value',
  foo: 'normal value'
}

for (var key in obj) {
  console.log(key) // => foo
}
console.log(Object.keys(obj)) // => [ 'foo' ]
console.log(JSON.stringify(obj)) // => {"foo":"normal value"}
// 可获取到Symbol类型的属性名
console.log(Object.getOwnPropertySymbols(obj)) // [ Symbol() ]

for...of循环

  • 遍历数组
const arr = [100, 200, 300, 400]

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

// 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()
  • 遍历Set
const s = new Set(['foo', 'bar'])

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

  • 遍历 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 遍历
const obj = { foo: 123, bar: 456 }

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

// => TypeError: obj is not iterable

可迭代接口

  • 实现Iterable接口是for...of的前提(返回带有next方法的对象,不断调用next方法可实现遍历)
const set = new Set(['foo', 'bar', 'baz'])

const iterator = set[Symbol.iterator]()

console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

// => { value: 'foo', done: false }
// => { value: 'bar', done: false }
// => { value: 'baz', done: false }
// => { value: undefined, done: true }
// => { value: undefined, done: true }

while (true) {
  const current = iterator.next()
  if (current.done) {
    break // 迭代已经结束了,没必要继续了
  }
  console.log(current.value)
}

// => foo
// => bar
// => baz

实现可迭代接口

  • 挂载interator方法,返回一个迭代器对象
const obj = {
  store: ['foo', 'bar', 'baz'],

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

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

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

// => 循环体 foo
// => 循环体 bar
// => 循环体 baz

迭代器模式

  • 对外提供统一的遍历接口,让外部不用关心内部的数据结构
  • 场景:你我协同开发一个任务清单应用
const todos = {
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '外语'],
  work: ['喝茶'],

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

  // 提供迭代器(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
        }
      }
    }
  }
}

todos.each(function (item) {
  console.log(item)
})

console.log('-------------------------------')

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

生成器 Generator

  • 普通函数名前添加 * 号,就变成了生成器函数
function * foo () {
  return 100
}

const result = foo()
console.log(result.next())

// => { value: 100, done: true }
  • 生成器函数会返回一个生成器对象,调用该对象的next方法才会让函数执行,遇到yield暂停,yield或return后面的值将会作为next()的结果返回
function * foo () {
  console.log('1111')
  yield 100
  console.log('2222')
  yield 200
  console.log('3333')
  yield 300
}

const generator = foo()

console.log(generator.next()) // 第一次调用,函数体开始执行,遇到第一个 yield 暂停
console.log(generator.next()) // 第二次调用,从暂停位置继续,直到遇到下一个 yield 再次暂停
console.log(generator.next()) // 第四次调用,已经没有需要执行的内容了,所以直接得到 undefined

// => 1111
// => { value: 100, done: false }
// => 2222
// => { value: 200, done: false }
// => 3333
// => { value: 300, done: false }
// => { value: undefined, done: true }

生成器应用

  • 发号器
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)

// => 1
// => 2
// => 3
// => 4
  • 实现 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)
}

ES Modules

  • 语言层面的模块化规范
  • 后面文章介绍

ECMAScript 2016

  • 新增数组的includes方法,以往是通过indexOf方法,indexOf方法的缺点是不能查找数组中的NaN
const arr = ['foo', 1, NaN, false]

// 找到返回元素下标
console.log(arr.indexOf('foo'))
// 找不到返回 -1
console.log(arr.indexOf('bar'))
// 无法找到数组中的 NaN
console.log(arr.indexOf(NaN))

// 直接返回是否存在指定元素
console.log(arr.includes('foo'))
// 能够查找 NaN
console.log(arr.includes(NaN))
  • 新增指数运算法,以往是通过Math.pow()实现
console.log(Math.pow(2, 10))

console.log(2 ** 10)

ECMAScript 2017

  • Object.values() 返回对象值的数组
const obj = {
  foo: 'value1',
  bar: 'value2'
}
console.log(Object.values(obj))

// => [ 'value1', 'value2' ]
  • Object.ectries 返回对象键值对数组
console.log(Object.entries(obj))

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

// => [ [ 'foo', 'value1' ], [ 'bar', 'value2' ] ]
// => foo value1
// => bar value2
  • String.prototype.padStart / String.prototype.padEnd
// 用给定字符串去填充目标字符串的开始和结束位置直到达到指定长度为止
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')}`)
}

// => html------------|005
// => css-------------|016
// => javascript------|128

  • 在函数参数中添加尾逗号
function foo (
  bar,
  baz,
) {

}