突破桎梏(五):一文详解 ECMAScript

一万八千字,先码后看,感谢收藏。

上篇我们介绍了函数式编程:【大前端专栏】突破桎梏(二):函数式编程

今天我们来介绍 【诶可码 · 斯柯瑞噗特】和它的新特性。

系统化的学习 ECMAScript 很有必要,而网上的 ECMAScript 资料比较零散,因此我们根据下面问题从 ES2015 开始梳理 ES2015 的发展与新特性介绍。

  1. ECMAScript 与 JavaScript 的关系?
  2. ES6 为什么指的是 ESMAScript 2015
  3. ECMAScript 2015(ES6)新特性介绍
  4. ES2015 let 与 块级作用域
  5. ES2015 const
  6. ES2015 数组的解构
  7. ES2015 对象的解构
  8. ES2015 模板字符串
  9. ES2015 字符串的扩展方法
  10. ES2015 函数形参列表新语法
  11. ES2015 对象字面量语法升级
  12. ES2015 对象的扩展方法
  13. ES2015 Promise
  14. ES2015 Class 类
  15. ES2015 Set 数据结构
  16. ES2015 Map 数据结构
  17. ES2015 Symbol 全新基础数据类型
  18. ES2015 遍历方法 for…of
  19. ES2015 可迭代对象 iterable
  20. ES2015 生成器对象 generator
  21. ES Modules
  22. ES2016 概述
  23. ES2017 概述
  24. 总结

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 Apis(fs + net 等)。

2. ES6 为什么指的是 ESMAScript 2015

名称 标准版本 发行时间
ECMAScript 2019(ES2019) 10 2019年6月
ECMAScript 2018(ES2018) 9 2018年6月
ECMAScript 2017(ES2017) 8 2017年6月
ECMAScript 2016(ES2016) 7 2016年6月
ECMAScript 2015(ES2015) 6 2015年6月
ECMAScript 5.1(ES5.1) 5.1 2011年6月
ECMAScript 5(ES5) 5 2009年12月
ECMAScript 4(ES4) 4
ECMAScript 3(ES3) 3
ECMAScript 2(ES2) 2
ECMAScript 1(ES1) 1

从上表我们能够讲出如下几点:

  • ES 的版本更迭从 ES5 跨向 ES6 这个阶段发生了巨变,中间间隔了 6 年之久(不谈 5.1
  • 6 年里,刚好是 Web 界快速发展的几年,ES6 一经发布,带来的更新内容特别多,因此算作一个新阶段的起始点。
  • ES6 发布时的版本应该是 ECMAScript 6,但从 2015 这年开始 ES 的版本不再使用版本号命名了,而是使用年号,因此 ES6 又叫 ESCMAScript 2015。
  • 从 2015 年开始,ES 的更新频率加速,达到每年一更,且规律是每年的 6 月时更新。

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

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

  • 相比于 ES5.1 的变化比较大
  • 自此,标准命名规则发生变化

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

ES2015 长达 26 个章节,链接:ES2015


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

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

包含到 ES2019 的 Node 版本号:12.14.0。

Nodemon 工具:修改完代码后自动执行代码。
执行 js 文件命令只发生如下变化:node index.js 这样执行变化为 nodemon index.js。

4. ES2015 let 与 块级作用域

  • 作用域的概念:代码中某个成员能够起作用的范围
  • 在 ES2015 前,ES 中只有两种作用域:全局作用域、函数作用域。在 ES2015 中新增了一个:块级作用域。
  • 通俗的说,块指的是我们代码中花括号所包裹起来的范围,如 if/for 的花括号内。

如下所示,在 if 花括号内使用 let 定义变量,在全局打印输出为:foo is not defined
![SharedScreenshot.jpg](https://img-blog.csdnimg.cn/img_convert/ecd083171f7d26803f85121403b07f24.png#align=left&display=inline&height=400&margin=[object Object]&name=SharedScreenshot.jpg&originHeight=400&originWidth=859&size=53743&status=done&style=none&width=859)

  • 值得一提的是: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++
  • let 声明的变量不会和 var 那样变量声明提升:
console.log(foo)
var foo = 'foo'

// undefined

5. ES2015 const

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

变量声明的最佳实践:不用 var,主用 const,配合 let

6. ES2015 数组的解构

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

// 使用解构 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

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

8. 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, name, 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

9. ES2015 字符串的扩展方法

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

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

10. 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  // 不带花括号的箭头函数的返回值是自动返回不需要 return 的
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)  // tom
    }, 1000)
  },
  // 箭头函数能够解决上面问题,this 可直接访问到 name
  sayHiAsyncTwo: function () {
    
    
  	setTimeout(() => {
    
    
    	console.log(this.name)  // tom
    }, 1000)
  },
}
person.sayHi()  // hi, my name is undefined

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

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

12. ES2015 对象的扩展方法

  • Object.assign

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

const source1 = {
    
    
  a: 123,
  b: 123
}
const target = {
    
    
  a: 456,
  c: 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)
  • 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
    target[property] = value
  }
})
console.log(person.name)  // 100
personProxy.gender = true

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

  • Proxy vs Object.defineProperty
    • defineProperty 只能监视对象属性的读写,Proxy 能够监视到更多对象操作,如:delete、对象方法的调用等,示例如下:
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'}

Proxy 中处理对象的 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 猜测到 property 下标 为 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))

ECMAScript 之后定会逐渐的把之前的方法废弃掉。

13. ES2015 Promise

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

14. 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  // this 访问当前类的实例对象
  }
  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 方法。

15. ES2015 Set 数据结构

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

  • 集合中新增元素
const s = new Set()
// add 方法会返回集合本身,因此可链式调用
s.add(1).add(2).add(3).add(2)
console.log(s)  // Set {1, 2, 3}  重复添加的元素会被忽略
  • 集合的遍历
// 方法 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]

16. 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)
})

17. 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 最主要的作用就是为对象添加独一无二的属性名。

之后还会增加一个 bigint 基本数据类型,用于存放更长的数字。
目前该类型还处在 stage-4 阶段,预计下个版本会被标准化。
到时候就有 8 个基本数据类型了。

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

toStringTag 就是 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 key 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()]

18. 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 是无法终止遍历的。

19. 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_ 原型对象上我们能够很容易地找到它们都具有下面这个相同的属性实现:
![image.png](https://img-blog.csdnimg.cn/img_convert/b91d2a01152219512c6e90444acfab28.png#align=left&display=inline&height=235&margin=[object Object]&name=image.png&originHeight=235&originWidth=372&size=58988&status=done&style=none&width=372)
三个能够被 for…of 遍历的数据结构的原型对象上都实现了属性名为 Symbol 常量:Symbol.iterator 的方法,它是一个函数方法。
所以,iterable 接口约定的就是对象中必须挂载 iterator 这个方法。

  • iterator 方法究竟是什么

    ![image.png](https://img-blog.csdnimg.cn/img_convert/0a1be7e632a95cbb2b1eddd3e5d98e6d.png#align=left&display=inline&height=320&margin=[object Object]&name=image.png&originHeight=320&originWidth=431&size=72462&status=done&style=none&width=431)
    我们定义一个数组并通过 Symbol.iterator 常量调用这个方法,能够看到这个方法调用后会返回一个包含 next 方法的迭代器对象。
    我们再次将返回的迭代器对象赋值给变量,然后调用它内部的 next 方法,输出如下:
    ![image.png](https://img-blog.csdnimg.cn/img_convert/caf7e3711b3b3176637319a2ed522991.png#align=left&display=inline&height=113&margin=[object Object]&name=image.png&originHeight=113&originWidth=373&size=25666&status=done&style=none&width=373)
    next 方法的返回值是一个包含 value 和 done 属性的对象。value 是数组第一个值,done 是一个布尔值 false。我们再连续调用三次该迭代器对象的 next 方法,输出如下:
    ![image.png](https://img-blog.csdnimg.cn/img_convert/0de86671ad9882500ffcd53c55496add.png#align=left&display=inline&height=276&margin=[object Object]&name=image.png&originHeight=276&originWidth=377&size=60200&status=done&style=none&width=377)
    到这里我们就应该能够想到,在这个迭代器当中内部应该是维护了一个数据指针,next 每调用一次指针就会向后移动一位,而 done 属性则表示数组中的元素是否全部被遍历完了。
    这也就是 iteratable 接口的实现原理,我们也就理解了为什么 for…of 能够遍历所有的数据结构:所有对象都可以自定义实现 iteratable 接口,只要实现了这个接口,for…of 自然而然就可以遍历实现了 iteratable 接口的对象了。

  • 自定义实现普通对象的 iteratable 接口,使其能够通过 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 的迭代器是语言层面的定义,因此适合任何数据结构。

20. 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 }

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

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

const idMaker = createIdMaker()
console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)

21. ES Modules

ES Modules 是在语言层面的模块化标准,之后我在模块化开发的文章中再详细介绍。到时候我会把链接再贴到这里。

22. 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)

23. 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: 'ning',
  lastName: 'mai',
  get fullName() {
    
    
  	return this.firstName + ' ' + this.lastName
  }
}
// 错误示例
const p2 = Object.assign({
    
    }, p1)
p2.firstName = 'li'
console.log(p2.fullName)  // ning mai

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

总结

这篇文章总共从 23 个点上叙述了关于 ECMAScript 的发展过程和特性剖析,这实际上是我在学习过程中的笔记,我们学技术的都知道好记性不如烂笔头,更何况在学习技术的过程中不动手敲下来的话,在实际应用过程中就根本无从下手,因此强烈建议读者能够按照给定的代码串下来,这篇文章坚持下来的话,相信诸位关于 ECMAScript 的知识积累不再零散。

该文章总结于我学习于拉勾教育出品的前端高薪训练营,因为我之前的学习也都过于零碎,所以综合考虑还是报名了这个课程来系统学习,学习成果你们也都看得到,如果需要报名可以私信找我,新老学员都会有五百元的返现,我的五百元返现不会拿在手里,其中一半会再返还给你,另一半我将捐赠给慈善机构(届时截图),有兴趣的朋友可以在评论区留言,感谢阅读。

猜你喜欢

转载自blog.csdn.net/qq_28827635/article/details/109913672
今日推荐