ECMAScript 简介

1. ECMAScript 与 JavaScript 的关系

  • ECMAScript 简称 ES。ES 是一个脚本语言。注意,是 ES 也是脚本语言。
  • ES 通常可以看作是 JavaScript 的标准化规则。
  • ES 只提供了最基本的语法。如:怎样定义变量和函数。
  • JS 实现了 ES 的语言标准。JS 还在此基础上新增了一些拓展,使得我们可以在浏览器可以操作DOM/BOM,在 Node 环境中可以读写文件等操作。
  • 因为 JS 是在 ES 基础上的拓展,所有 JS 是ES 的扩展语言。
  • JS 在浏览器环境中的组成关系:ES + Web Apis(BOM + DOM 等)。
  • JS 在 Node 环境中的组成关系:ES + Node A披萨(fs + net 等)。

2. ECMAScript 2015(ES6)新特性介绍

ES6 是 ECMAScript 标准的代表版本,原因如下:

  • 相比于 ES5.1 的变化比较大
  • 自此,标准命名规则发生变化,目前有很多开发者还喜欢用 ES6 这个名称泛指 ES5.1 以后所有的新版本。例如“使用 ES6 的 async 和 await”,实际上 async 是 ES2017 中指定的标准。因此我们需要注意分辨文章中的 ES6 是特指 ES2015 还是 泛指 ES2015之后的所有新标准。

接下来我们重点接收 ES2015 在 ES5.1 基础上的变化,变化归纳为 4 类。

  • 解决原有语法上的一些问题或者不足。如:let 和 const 提供的块级作用域。
  • 对原有语法进行增强。如:解构、展开、参数默认值、模板字符串。
  • 全新的对象、全新的方法、全新的功能。如:Promise
  • 全新的数据类型和数据解构。如:Symbol、Set、Map。

3. ES2015 let 与 块级作用域

  • 作用域的概念:代码中某个成员能够起作用的范围
  • 在 ES2015 前,ES 中只有两种作用域:全局作用域、函数作用域。在 ES 2015 中新增了一个:块级作用域。
  • 通俗的说,块指的是我们代码中花括号所包裹起来的范围,如 if/for 的花括号内。
  • 值得一提的是:for 的括号内和花括号内是两个不同的作用域,因此下面代码可以输出看一看结果:
for(let i = 0; i > 3; i++){
    let i = 'fir'
    console.log(i)
}

上面代码的执行过程类似下面这样:

let i = 0
if(i < 3) {
    let i = 'fir'
    console.log(i)
}
i++
  • let 声明的变量不会和 var 那样变量声明提升:
consoel.log(fir)
var fir = 'fir'
//undefined

4. ES2015 const

  • const 只是在 let 的基础上多了“只读”,即变量一旦使用 const 声明后就不允许再被修改。
// 错误示例 1
const name = 'mn'
name = 'nm'
// 错误示例 2
const name
name = 'nm'
  • 不允许修改的是变量指向的内存地址,而不是变量本身的值。
// 正确示例
const obj = {}
obj.name = 'mn'
// 错误示例
obj = {}

5. ES2015 数组的解构

const arr = [100, 200, 300]
// 不使用解构
const foo = arr[0]
const bar = arr[1]
const baz = arr[2]

// 使用解构 1
const [foo, bar, baz] = arr

// 使用解构 2 -> 只获取某个位置的值
const [, , baz] = arr
console.log(baz)    // 300

// 使用解构 3 -> 提取数组中剩余所有值,该写法仅可以用于最后一个位置
const [foo, ...rest] = arr
console.log(rest)    // [200, 300]

// 使用解构 4 -> 获取数量少于数组内实际数量
const [foo] = arr
console.log(foo)     // 100

// 使用解构 5 -> 获取数量大于数组内实际数量
const [foo, bar, baz, more] = arr
console.log(more)    // undefined

// 使用解构 6 -> 设置变量默认值,当未提取出值时默认给予该值
const [foo, bar, baz, more = 'default more'] = arr
console.log(more)    // default more

6. ES2015 对象的解构

对象的解构大都和数组的解构相同,基础使用和特殊用法如下示例:

// 基础使用
const obj = { name: 'mn', age: 18 }
const { name } = obj
console.log(name)    // mn

// 特殊用法 -> 由于解构时花括号内填写的必须是对象中存在的key,那么我们遇到厦门情况可以这样解决
const name = 'Tom'
const { name:objName = 'Jack' } = obj
console.log(objName)    // mn

7. ES2015 模板字符串

模板字符串具有以下三个

// 1. 多行字符串,无需/n 直接使用回车即可
const str = `my name is
name`
// 2. 插入变量
const name = 'mn'
const str = `my name is ${name}`
// 3. 标签字符串
//    标签函数能够获取到译 ${} 分割后的字符串数组
//    标签函数的返回值就是模板字符串的值
const name = 'Tom'
const gender = true
function myTagFunc(strings, namem gender){
    console.log(strings, name, gender)    // ['hey', 'is a', '.'] Tom true
    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 man

8. ES2015 字符串的扩展方法

一组判断字符串内是否包含某些内容的方法

  • includes - - 字符串是否包含特定字符
const message = 'Error: foo is not defined.'
message.includes(foo)    // true
  • startsWith - 字符串是否为特定开头
const message = 'Error: foo is not defined.'
message.startsWith('Error')    // true
  • endsWith - 字符串是否为特定结尾
const message = 'Error: foo is not defined.'
message.endsWith('.')    // true

9. ES2015 函数形参列表新语法

  • 参数默认值
// 五参数默认值语法时,使用短路赋值而导致的错误示例
function foo(enable){
    enable = enable || true
    console.log(enable)    // true
}
foo(false)

// 正确的应该是下面这样,判断是否为 undefined 才赋予默认值
function foo(enable){
    enable = enable === undefined ? true : enable
    console.log(enable)    // true
}
foo()

// 使用参数默认值语法,原理和上面正确用法相同,没有传递实参或传递undefined时使用默认值
function foo(enable = true){
    console.log(enable)    // true
}
foo()
  • 剩余参数
// ES2015前,使用arguments 来接收所有参数,arguments 是一个伪数组
function foo(){
    console.log(arguments)    // {'0': 1, '1': 2, '2': 3, '3': 4}
}
// ES2015里,使用下面语法接收真实传入实参数量之后剩余的参数
function foo(a, ...args){
    console.log(args)    // [2, 3, 4]
}
foo(1, 2, 3, 4)
  • ES2015 展开数组
const arr = ['foo', 'bar', 'baz']
// ES2015 前
console.log(arr[0], arr[1], arr[2])
// ES2015
console.log(...arr)
  • ES2015 箭头函数 推荐插件:fire code
// 简化函数定义和增加新特性
// 示例 1
const inc = n => n + 1
console.log(inc(100))    // 101

// 示例 2
const inc = (n, m) => n + m

// 示例 3  // 不带花括号的箭头函数的返回值需手动返回
const inc = (n, m) => {
    return n + m
}
  • ES2015 箭头函数与this
  • 箭头函数不会改变 this 指向
// 普通函数里,this 指向调用者自身
const person = {
    name: 'Tom'
    sayHi: function() {
        console.log(`hi, my name is ${this.name}`)
    }
}
person.sayHi()    // hi, my name is Tom

// 箭头函数没有 this 的机制
const person = {
    name: 'Tom',
    sayHi: () => {
        console.log(`hi, my name is ${this.name}`)
    },
    // 下面函数只能通过声明 that 利用闭包来获取 name
    sayHiAsync: function() {
        const _that = this
        setTimeout(function() {
            console.log(_that.name)
        }, 1000)
    },
    // 箭头函数能够解决上面问题, this 可直接访问到 name
    sayHiAsyncTwo: function() {
        setTimeout(() => {
            console.log(this.name)    // Tom
        }, 1000)
    }
}
person.sayHi()    // hi, my name is undefined

10. ES2015 对象字面量语法升级

// 1. 属性名和变量名相同,可省略
// 2. 函数声明语法可精简,可省略冒号和function关键字。需要注意的是,这里的方法中的this和普通函数function中的this相同,指向调用者自身。
// 3. 计算属性名:对象的属性名可动态添加
const bar = '123'
const obj = {
    foo: 123,
    bar,    // 和 bar: bar 等价
    method1 () { console.log(this) }    // 和 method: function() 等价
    [bar]: 123
}

11. ES2015 对象的扩展方法

  • Object.assign

将多个源对象中的属性复制到一个目标对象中,如果对象之间有相同的属性,那么源对象中的属性会覆盖掉目标对象中的属性。

const source1 = {
    a: 123,
    b: 123
}
const target = {
    a: 456,
    b: 456
}
const result = Object.assign(target, source1)

console.log(target)    // {a: 123, c: 456, b: 123}
console.log(result === target)    // true

// 能够看到,target 居然和 result 相同,也就是说 target 确确实实被改变了,而不是重新生成新对象。但我们同样能够利用这个特性来复制一个对象,如下示例:

const result = Object.assign({}, {name: 'Tom'})
console.log(result)    // {name: 'Tom'}
  • Object.is
// 判断两个值是否相等,该方法不常用,通常我们使用严格等号 ===。
// ES2015 之前
console.log(0 == false)    // true
console.log(0 === false)    // false
console.log(+0 === -0)    // true
console.log(NaN === NaN)    // false
// ES2015 新的同值比较的方法
Object.is(NaN, NaN)    // true
  • Proxy
// 监视某个对象中的属性读写,我们可以使用 ES5 Object.defineProperty。ES2015 中 Proxy 是专门为对象设置访问代理器的,其中代理可以理解为门卫,我们进出屋子都要经过这个代理。通过 Proxy 就可以轻松监视属性的读写,它也比 defineProperty 更强大。

const person = {
    name: 'mn',
    age: 20
}
const personProxy = new Proxy(person, {
    get(target, property) {
        console.log(target, property)    // {name: 'mn', age: 20} name
        return 100
    },
    set(target, property, value) {
        console.log(target, property, value)    // {name: 'mn', age: 20}  gender true
    }
})

console.log(person.name)    // 100
personProxy.gender = true

第一个参数为需要代理的对象,第二个参数为代理的处理对象,其中 get 方法用来监视属性的访问,返回值作为访问结果。set 方法用来监视属性的设置

  • Proxy vs Object.defineProperty
  • defineProperty 只能监视对象属性的读写,Proxy 能够监视到更多对象操作,如:delet、对象方法的调用等,示例如下:
const person = {
    name: 'mn',
    age: 20
}
const personProxy = new Proxy(person, {
    deleteProperty(target, property) {
        console.log('delete', property)    // delete  name
        delete target[property]
    }
})
delete personProxy.age
console.log(person)    // {name: 'mn'}

// Peoxy 中处理对象的 deleteProperty 方法能够监视目标对象中的delete 操作。此外有更多方法都能够监视到目标对象的属性异动。
  • Proxy 可以更好的支持数组对象的监视,从而能够重写覆盖掉数组的操作方法:push、shift等,以此劫持对应方法的调用过程。这个特性的具体实现我们在之后在 Vue3.0 源码剖析时再介绍。
const list = []
const listProxy = new Proxy(list, {
    set(target, property, value) {
        console.log('set', property, value)    // set 0 100
        target[property] = value
        return true    // 表示设置成功
    }
})

listProxy.push(100)    // proxy 猜测到 proerty 下标 为 0
  • Proxy 是以非侵入的方法监管了对象的读写,一个已存在的对象通过单独定义 Proxy 即可被监视,而 defineProperty 则还需特地声明监听某个属性及其处理方法(如上述例子)。这个优势需要大量实践使用慢慢体会。
  • Reflect

统一的对象操作 API,Reflect 属于静态类,不可通过 new 方法构建实例对象,只能调用其中的静态方法,如同 Math 一样。Reflect 内部封装了一系列针对对象的底层操作(目前 13 个尚在使用的方法)。这些静态方法的方法名和 Proxy 能够监视到对象的方法名一致的,其实 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)
    }
})

// Reflect 最大的价值就是它统一了一套用于操作对象的 API。举例如下:

const obj = {
    name: 'mn',
    age: 18
}
// 之前我们需要使用不同的关键词或对象API
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))

12. ES2015 Promise

ES2015 新出的一种更优的异步编程解决方案,解决了传统异步编程中回调函数嵌套过深的问题。具体可看 JavaScript 异步编程话题中的详细分析;或你对 Promise 内部实现原理感兴趣,可以查看我的这篇文章:Promise实现原理。

13. ES2015 Class 类

在此之前,ECMASCript 通常使用 function 或 property 来实现类。如今我们能够使用 ES2015 提供的 Class 关键字语法来实现更容易理解且解构更清晰的类定义。如下所示:

// 在此之前
function Person (name) {
    this.name = name
}
Person.property.say = function() {
    console.log(`my name is ${this.name}`)
}

// ES2015 Class
class Person {
    // 当前类的构造函数
    constructor(name) {
        this.name = name
    }
    say() {
        console.log(`my name is ${this.name}`)
    }
    static create(name) {
        return new Person(name)
    }
}
const p = new Person('Tom')
console.log(p.say())    // my name is Tom

const Tom = Person.create('Tom')
Tom.say()    // my name is Tom
  • 类的方法定义。类中的方法存在两种:实例方法、静态方法。两者的区别就是实例方法需要构造的实例对象去调用,静态方法则是直接使用类本身去调用。ES2015 中新增静态方法定义的static 关键字,如上述代码中 create 方法。还需要注意的是,静态方法由于是类本身调用的,因此静态方法中的 this 指向当前类本身。
  • 累得继承。继承是面向对象非常重要的特性,通过继承我们可以抽象出相类似之间重复的部分。ES2015 前我们通常使用 property 来实现继承,而 ES2015 中产生了专门用于接触的关键字:extends。
class Person {
    constructor(name) {
        this.name = name
    }
    say() {
        console.log(`my name is ${this.name}`)
    }
}
class Student extends Person {
    construct(name, number) {
        super(name)
        this.number = number
    }
    hello() {
        super.say()
        console.log(`my number is ${this.number}`)
    }
}
const student = new Student('mn', 20)
student.hello()
// my name is mn
// my number is 20

子类中的 super 就代表了父类,调用 super(name) 也就调用了父类的构造方法,同样地,super.say() 即调用了父类的 say 方法。

14. ES2015 Set 数据结构

ES2015 中提供了一个全新的数据结构,和数组类似,但其中的元素不允许重复,也就是每个元素在其中都是唯一的,我们可以称之为:集合。

  • 集合中新增元素
const s = new Set()
// add 方法会返回集合本身,因此可链式调用
s.add(1).add(2).add(3).add(2)
console.log(s)    // Set {1,3,2}  重复添加的元素会被忽略
  • 集合的遍历
// 方法 1 使用Set 自带 forEach 方法
s.forEach(i => console.log(i))
// 方法 2 使用 ES2015 新语法 for
for(let i of s) {
    console.log(i)
}
  • 获取集合长度等自有方法
// 1. 获取集合长度
console.log(s.size)

// 2. 判断集合当中是否存在某个值
console.log(s.has(100))        // false

// 3. 删除集合中某个指定值,方法会返回是否删除成功
console.log(s.delete(3))        // true

// 4. 清空集合
s.clear()
console.log(s)        // Set {}
  • 集合能够便于数组去重
const arr = [1,2,1,3,4,2]
// 方法 1
const result_1 = Array.from(new Set(arr))
console.log(result_1)    // [1,2,3,4]

// 方法 2
const result_2 = [...new Set(arr)]
console.log(result_2)    // [1,2,3,4]

15. ES2015 Map 数据结构

Map 和 对象 很像,它们的本质上都是键值对集合。不同的是,对象的键只能存放字符串类型,这就会导致我们在存放复杂数据时遇到一些问题,如下所示:

// object 键上存放复杂数据时遇到的问题
const obj = {}
obj[true] = 'value'
obj[123] = 'value'
obj[{a:1}] = 'value'
console.log(Object.keys(obj))    // ['123','true','[object object]']

Map 就是为了解决上面的问题诞生的,Map 是严格意义上的键值对集合,它能够映射两个任意类型的数据之间的对应关系。

const m = new Map()
const tom = { name: 'mn'}
// 1. 设置键值映射关系
m.set(tom, 90)
console.log(m)    // Map {{name: 'tom'} => 90}
// 2. 根据键获取对应值
m.get(tom)
// 3. 判断某键是否存在
m.has(tom)
// 4. 删除某个键
m.delete(tom)
// 5. 清空所有键
m.clear()
// 6. 遍历所有键值,需要注意的是首个参数是值,第二参数是键
m.forEach((value, key) => {
    console.log(value, key)
})

16. ES2015 Symbol

一种全新的原始数据类型,这里的原始指的可是基本数据类型,也就是说自 ES2015 起,JavaScript 的基础类型会变为7种:String、Number、Boolean、Object、Null、Undefined、Symbol。ES2015 之前我们对象中的属性名都是字符串类型,而字符串是也可以能重复的,重复就会导致冲突,冲突的场景可能如下:我们使用第三方模块时,很多时候我们会去扩展第三方模块中提供的一些对象,而我们并不知道对象是否已经存在某一个指定的键,如果我们冒然扩展,就会出现冲突问题。那么 ES2015 为了解决这个问题,提供了一个【独一无二的值】的原始数据类型:Symbol。

  • Symbol 最大的特点就是我们通过它创建的每一个值都是独一无二的,永远不会重复,示例如下:

console.log(Symbol() === Symbol())

  • 为了我们在开发时方便调试,Symbol 支持传入一个文本作为描述,这样我们就可以在控制台辨别输出的是哪一个Symbol。

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

console.log(Symbol('bar'))        // Symbol(bar)

console.log(Symbol('baz'))        // Symbol(baz)

  • 从 ES2015 开始,对象支持使用 Symbol 作为属性名,也就是说对象的属性类型不再单单局限于字符串类型了,它如今支持字符串、Symbol 两种类型。
const obj = {
    [Symbol()]: '123',    // 使用对象字面量方法动态声明键值
    [Symbol()]: '456'
}
console.log(obj)    // {[Symbol()]: '123', [Symbol()]: '456'}
  • Symbol 还可以帮助对象实现私有成员的定义
// a.js ===============================
const name = Symbol()
const person = {
    [name]: 'mn',
    say() {
        console.log(this[name])
    }
}

// b.js ================================
person.say()    // 只能这样才能调用,因为没有 name

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

  • Symbol

由于 Symbol 创建出的值始终是独一无二的,因此即使传入的描述相同,产生出的值也会仍然不相等

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

 那么我们如何多次创建相同的Symbol 呢?我们可以使用Symbol的 for 方法,示例如下:

console.log(Symbol.for('foo') === Symbol.for('foo'))        // true

需要特别注意的点:for 方法的实现实际上是由于 Symbol 内部维护了一个注册表,这个注册表为了字符串和值创建一一对应的关系,重点是:字符串,所以有 for 方法中我们传入的值都会被转换为字符串,因此会出现下面情况:

console.log(Symbol.for(True) === Symbol.for('true'))        // true

console.log(Symbol.for(0) === Symbol.for('0'))        // true 

  • Symbol 内置了一些常用的 Symbol 常量

Symbol 内置了一些常用的 Symbol 常量,作用是为了内部方法的标识,这些标识可以让自定义对象实现一些 JS 当中内置的接口。

const obj = {}
console.log(obj.toString())    // [object object]
// 想要自定义对象的 toString 标签,我们就可以向这个对象添加一个特定的成员来标识
// 如果用普通的字符串进行标识,那么就也可能和对象内部成员重复导致冲突,所以 ECMAScript 要求我们使用 Symbol 值实现这个接口
const obj = {
    [Symbol.toStringTag]: 'XObject'
}
console.log(obj.toString())    // [object XObject]

toString 就是 Symbol 内置的 Symbol 常量,这种 Symbol 在外面为对象实现迭代器时会经常用到。

  • 对象使用 Symbol 需要注意的点
  • 传统的 for in 循环是无法获取到对象中的 Symbol 键的
  • Object.keys 也获取不到对象中使用 Symbol 定义的属性
  • JSON.stringify 也会忽略 Symbol
const obj = {
    [Symbol()]: 'symbol value',
    foo: 'normal value'
}
// 示例 1 - for in 获取不到 Symbol
for(let i in obj) {
    console.log(key)        // foo
}

// 示例 2 - Object.keys 获取不到 Symbol
console.log(Object.keys(obj))    // ['foo']

// 示例 3 - JSON.stringify 会忽略 Symbol
console.log(JSON.stringify(obj))    // {'foo': 'normal value'}

上面这些点都表名:Symbol 特别适合用来定义对象的私有属性。那么上面方法我们都获取不到 Symbol 键值,我们又该如何正常获取到 Symbol 属性和对应值呢?

// 获取对象中所有 Symbol 属性名
console.log(Object.getOwnPropertySymbols(obj))        // [Symbol()]

17. ES2015 遍历方法 for...of

ECMAScript 中的循环方法有很多:for(let i=0;i<3;i++) 循环适合遍历数组,for...in 循环适合遍历对象键值对,再如一些对象的遍历方法:forEach,但这些遍历方式都有一定的局限性,所有ES2015 借鉴了许多语言引入了全新的 for...of 循环,这种遍历方法以后将作为遍历所有数据结构的统一方式。也就是说:明白了 for...of 的原理也就可以遍历任意自定义的数据结构。

  • for...of 遍历数组时获取的是当前值,而不是下标
// 示例 1 - 遍历数组
const arr = [100,200,300,400]
for(const item of arr) {
    console.log(item)
}
// 100
// 200
// 300
// 400

// 示例 2 - 遍历 Set 对象,和遍历数组无差异
const s = new Set(['foo' ,'bar', 'baz'])
for(const item of s) {
    console.log(item)
}
// foo
// bar
// baz

// 示例 3 - 遍历 Map 对象,注意此时的输出,会同时输出键和值
const m = new Map()
m.set('foo', '123')
m.set('bar', '456')
for(const item of m) {
    console.log(item)
}
// ['foo', '123']
// ['bar', '456']

// 示例 4 - 遍历普通对象,无法正常遍历!!!
const obj = { foo: 123, bar: 456 }
for(const item of obj) {
    console.log(item)
}
// obj is not iterable -> obj 对象是不可迭代的

这样,for...of 就可以替代数组的 forEach 方法,同时 for...of 方法内也可以使用 break 随时终止循环,而 forEach 是无法终止遍历的。

18. ES2015 可迭代对象 iterable

  • 为什么 ECMAScript 要制定 iterable?

从上面 for...of 的最后例子我们能够看到大多数据结构都能够被 for...of 遍历,而普通对象确不能够,这是为什么呢?这就要我们从 ES2015 提供的iterable 接口谈起了:在数据结构的不断发展过程中,ES 从原有的数据结构数组、对象等延伸出了愈来愈多的数据结构:Set、Map 等等,之后还会诞生更多数据结构,我们也能够通过复杂组合来产生自定义的数据结构。ECMAScript 为了给各种各样的数据结构提供统一的遍历方式,ES2015 提供了 iterable 接口来解决这个问题。接口实际上就是对外提供的方法,如大多数据结构都向外提供了 toString 接口,这个接口实际上就是 ECMAScript 所制定出的标准接口,这些数据接口都据此实现了该方法。在这里的 iterable 可迭代接口,就是提供可以被 for...of 统一遍历访问的标准。可以这么说:只要这个数据结构实现了可迭代接口,那么它就能够被 for...of 遍历。因此,我们上面能够被 for...of 遍历的数据、Set、Map 等内部都已经实现了 iterable 接口,普通对象却没有实现。

  • 那么,iterable 接口标准是什么,Set、Map 它们又是如何实现满足标准呢?
console.log([])
console.log(new Set())
console.log(new Map())

我们可以在控制台分别输出数组、Set、Map 来观察它们的内部方法,在它们的__proto__原型对象上我们能够很容易地找到它们都具有下面这个相同的属性实现:

三个能够被 for...of 遍历的数据结构的原型对象上都实现了属性名为 Symbol 常量:Symbol.iterator 的方法,它是一个函数方法。所以,iterable 接口约定的就是对象中必须挂载iterator 这个方法。

  •  iterator 方法究竟是什么

我们定义一个数组并通过 Symbol.iterator 常量调用这个方法,能够看到这个方法调用后会返回一个包含 next 方法的迭代器对象。我们再次将返回的迭代器对象赋值给变量,然后调用它内部的 next 方法,输出如下:

next 方法的返回值是一个包含 value 和done 属性的对象。value 是数组第一个值,done 是一个布尔值 false。我们再连续调用三次该迭代器对象的 next 方法,输出如下:

到这里我们就应该能够想到,在这个迭代器当中内部应该是维护了一个数据指针,next 每调用一次指针就会向后移动一位,而 done 属性则表示数组中的元素是否全部被遍历完了。这也就是 iterable 接口的实现原理,我们也就理解了为什么 for...of 能够遍历所有的数据结构:所有对象都可以自定义实现 iterable 接口,只要实现了这个接口,for...of 自然而然就可以遍历实现了 iterable接口的对象了。

  • 自定义实现普通对象的 iterable接口,使其能够通过 for...of 进行遍历
// 实现可迭代接口 <iterable>
const obj = {
    name: 'mn',
    age: 20,
    [Symbol.iterator]() {    // iterator
        const keys = Object.keys(this)
        return {
            next: () => {
                const done = keys.length === 0 ? true : false
                const key = done ? undefined : keys.pop()
                const value = done ? undefined : [key, this[key]]
                return { value, done}    // iteration result
            }
        }
    }
}

// 示例 1 - 手动调用
const iterator = obj[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

// 示例 2 - for...of 调用
for(const item of obj) {
    console.log(item)
}
  • 实现迭代器的目的 - 迭代器模式

迭代器模式的核心:对外提供统一遍历接口,让外部不用再关心数据内部的结构是怎样的。ES 的迭代器是语言层面的定义,因此适合任何数据结构。

19. ES2015 生成器 generator

Generator 的意义和 Promise 相同:避免异步编程中回调嵌套过深,从而提供更好的异步编程解决方案。

  • 生成器的语法和基本应用
function * foo() {
    console.log('mn')
    return 100
}
const result = foo()
console.log(result)    // Object [Generator] {}

在函数前添加 * 来定义一个生成器函数,调用后该生成器函数会返回一个生成器对象,生成器对象拥有和迭代器对象相同的 next 方法。

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

实际上,生成器也实现了 iterable 接口。

  • 配合 yield 使用
function * foo() {
    yield 100
    yield 200
    yield 300
}
const generator = foo() 
console.log(generator.next())    // { value: 100, done: false}
console.log(generator.next())    // { value: 200, done: false}
console.log(generator.next())    // { value: 300, done: false}
console.log(generator.next())    // { value: undefined, done: ture}
  • 总结
  • 生成器函数会为我们生成一个生成器对象。
  • 调用生成器对象的 next 方法才会让函数体开始执行。
  • 执行过程中一旦遇到yield 关键词,函数的执行过程就会被暂停下来,同事 yield 后面的只会被作为 next 的值返回。
  • 我们继续调用 next 方法,函数体会从上一个 yield 暂停处继续执行,周而复始直到函数完全结束,届时 done 的值才会变为 true。
  • 生成器应用
// 示例 1 - 生成 id
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)

20. ES2016 概述

ES2016 毕竟是个小版本,所以它只比 ES2015 多两个小功能

  • 数组的 includes 方法:判断数组中是否包含指定元素,返回值是布尔值。
// Array.prototype.includes
const arr = ['foo', 1, NaN, false]
console.log(arr.includes(1))
  • 指数运算符
// 在之前我们需要进行指数运算时要依赖于 Math 中的 pow 方法
console.log(Math.pow(2, 10))
// ES2016,使用两个星号即可
console.log(2 ** 10)

 21. ES2017 概述

ES2017 也是一个小版本,它共扩充了几个小功能:Object 三个扩展方法、String 两个扩张方法、函数参数中添加伪逗号、Async/Await 标准化。

  • Object.values - 获取对象所有值
const obj = {
    foo: 'value1',
    bar: 'value2'
}
console.log(Object.values(obj))    // ['value1', 'value2']
  • Object.entries - 返回对象中所有键值对
console.log(Object.entries(obj))    // [['foo', 'value1'], ['bar', 'value2']]
// 作用 1 - 便于遍历操作
for (const [key, value] of Object.entries(obj)) {
    console.log(key, value)
}
// 作用 2 - 便于 Object 数据类型转换为 Map 数据类型,因为 Map 初始化需要的数据类型就是 entries 返回的数据类型
console.log(new Map(Object.entries(obj)))    // Map { 'foo' => 'value1', 'bar' => 'value2' }
  • Object.getOwnPropertyDescriptors - 获取对象中所有属性的完整的信息

ES5 之后对象中就可以定义getter、setter 属性,但这些属性是不能通过 Object.assign 方法正常复制过去的,这种情况下,我们就可以先通过 getOwnPropertyDescriptors 来获取对象中所有属性的完整信息,再通过 assign 复制过去,示例如下:

const p1 = {
    firstName: 'san',
    lastName: 'wu',
    get fullName() {
        return this.firstName + ' ' + this.lastName 
    }
}
// 错误示例
const p2 = Object.assign({}, p1)
p2.firstName = 'li'
console.log(p2.fullName)    //san wu

// 正确示例
const descriptors = Object.getOwnPropertyDescriptors(p1)
const p2 = Object.assign({}, descriptors)
p2.firstName = 'li'
console.log(p2.fullName)    // li wu
  • String.padStart / String.padEnd - 给字符串补充指定位数的指定字符
console.log('0.1'.padEnd(4, '0'))    // 0.10
console.log('0.1'.padStart(4, '0'))    //00.1
  • 函数参数中添加伪逗号,方便代码书写体验,不是功能层面的更新
function foo {
    bar,
    baz,
} {}
  • 标准化了 Async / Await,实质上就是 Promise 的语法糖
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值