ECMAScript 新特性
- 课程介绍
- 内容概要
- ECMAScript 概述
- ECMAScript2015(ES6) 概述
- ES2015 准备工作:
- ES2015 let与块级作用域
- ES2015 const
- ES2015 数组/对象的解构(Destructuring)
- ES2015 模板字符串
- ES2015 带标签的模板字符串
- ES2015 字符串的扩展方法
- ES2015 参数默认值
- ES2015 剩余参数
- ES2015 展开数组
- ES2015 箭头函数
- ES2015 箭头函数与 this
- ES2015 对象字面量的增强
- ES2015 Object.assign
- ES2015 Object.is
- ES2015 Proxy
- ES2015 Proxy 对比 defineProperty
- ES2015 Reflect:统一的对象操作 API
- ES2015 Promise
- ES2015 class 类
- ES2015 静态方法
- ES2015 类的继承
- ES2015 Set
- ES2015 Map
- ES2015 Symbol
- ES2015 Symbol 补充
- ES2015 for ... of 循环
- ES2015 可迭代接口
- ES2015 实现可迭代接口
- ES2015 迭代器模式
- ES2015 生成器
- ES2015 生成器应用
- ES2015 ES Modules
- ES2016 概述
- ES2017 概述
课程介绍
为什么再次聊起 ECMAScript ?
- 结合面试过程中的经验,发现很多前端开发者没理解语言和平台之间的关系
- 对 ES 的理解和掌握程度都不尽相同
- 整体的高阶课程中会重度依赖这些内容
所以说系统化的学习 ECMAScript 以及一些新特性很有必要,同时,对写出更现代化、更高质量的代码会有很大的帮助。
内容概要
- ECMAScript 与 javascript :弄明白语言和平台之间的关系
- ECMAScript 的发展过程
- ECMAScript 2015 的新特性:ES6 中具体出现了哪些新特性以及新特性出现的背景或者说优势
- And More …
目标:整体对 ECMAScript 新特性有一个完整的认识,建立一个完整的知识体系
ECMAScript 概述
ECMAScript :javascript 的语言本身
一门脚本语言,缩写为 ES,通常会把它看作为 javascript 的标准化规范,实际上 javascript 是 ECMAScript 的扩展语言。因为 ECMAScript 中只是提供了最基本的语法,只是停留在语言层面,并不能完成实际中的功能开发,而经常使用的 jacascript 实现了 ECMAScript 语言的标准,并且在基础之上做了一些扩展,使得我们可以在浏览器环境中可以操作 BOM 和 DOM ,在 node 环境中可以去做读写文件之类的一些操作。
总的来说,浏览器当中的 jacascript 就等于 ECMAScript 加上 web APIs (BOM/DOM),node 环境当中的 javascript 就等于 ECMAScript 加上 Node APIs (fs/net/etc.等等)。
所以说 javascript 中,语言本身指的就是 ECMAScript 。
2015 年开始,保持每年一个大版本迭代。
其中,ES2015 需要注意的东西有很多,因为这个版本相对比较特殊,它在上一个版本(ES5)发布过后,经历了 6 年的时间才被完全的标准化,而且这 6 年的时间也是 WEB 发展的黄金时间,所以说在这个版本中包含了很多颠覆式的新功能,也正式 ES2015 迭代的时间太长,导致发布的内容过多,所以从之后的版本开始 ES 的发布会变得频繁,更符合当下互联网小步快跑这种精神。
而且从 ES2015 过后,就决定不再按照版本号去命名,而是使用发行年份。
由于这样的决定是在 ES2015 诞生的过程中产生的,所以很多人习惯的将 ES2015 称之为 ES6。
ECMAScript2015(ES6) 概述
最新 ECMAScript 标准的代表版本
- 相比于 ES5.1 变化比较大
- 自此,标准命名规则发生变化,更准确的缩写名称叫做 ES2015
提示:现在有很多开发者习惯用 ES6 去泛指 ES5.1 之后所有的新版本
比如:有的资料中会有“使用 ES6 的 async 和 await ” 这种说法,实际上 async 是 ES2017 中制定的标准。
注意分辨 ES6 是特指还是泛指。
ES6 – http://www.ecma-international.org/ecma-262/6.0/
可以自行去完整的查看一下 ES2015 的新特性,这里讲的只是一些 ES2015 中比较重要,值得我们单独去了解的新特性。
简单的归为 4 大类:
- 解决原有语法上的一些问题或者不足
- 对原有语法进行增强,更为便捷易用
- 全新的对象,全新的方法,全新的功能
- 全新的数据类型和数据结构
ES2015 准备工作:
任何一个支持 ES2015 的环境都可以
为了方便调试,这里直接使用 NodeJS 环境。
用到了一个小工具,Nodemon 作用是修改完代码后自动重新执行代码
# 初始化 package.json
npm init
# 下方两种情况自己选:
# 1 不推荐:局部安装 nodemon 这种的需要在 package.json 里边自己添加 scripts 自定义命令,否则无法运行,总是报错
npm install nodemon --save-dev
# 2 推荐:全局安装 nodemon
npm install -g nodemon
# 全局的在这里直接使用 nodemon 运行写了代码的 js ,执行完之后不要退出,修改了代码一保存会自动执行
nodemon aaa.js
# 局部的:package.json 里边定义 scripts
"scripts": {
"nodemon": "nodemon a.js"
},
# 定义好了之后就可以在命令行直接运行 npm run nodemon,这里的 a.js 就时刻监控着了
npm run nodemon
# 注意:局部安装 nodemon 的话,每次改变了需要监控的文件,需要自己更改 package.json 里边对应的文件名,挺不方便调试的,所以推荐全局安装 nodemon
ES2015 let与块级作用域
作用域:代码中的成员能够起作用的范围
ES2015之前,有两种作用域,分别是全局作用域和函数作用域,在 ES2015 中新增了一个块级作用域。
块:代码中用花括号 { } 包裹起来的一个范围,例如 if 或者 for 后边的花括号都会产生块
以前块是没有独立的作用域,导致在块的外部可以访问到块的内部的成员,这点对于复杂代码非常不利和不安全,有了块之后就可以在块中通过新的关键词 let 声明变量,声明方式都是一样的,只不过通过 let 声明的变量只能在声明这个变量的代码 块 中被访问到。
还有一个是声明提升的问题,let 定义的不会做提升。
之所以没有在 var 的基础之上做一些升级,是因为直接升级 var 的话会导致以前的很多项目无法工作,所以重新定义了一个新的关键词 let ,互不影响。
// let 声明的成员只会在所声明的块中生效 -------------------------------------------
// if (true) {
// // var foo = 'zce'
// let foo = 'zce'
// console.log(foo)
// }
// let 在 for 循环中的表现 ---------------------------------------------------
// for (var i = 0; i < 3; i++) {
// for (var i = 0; i < 3; i++) {
// console.log(i)
// }
// console.log('内层结束 i = ' + i)
// }
// for (var i = 0; i < 3; i++) {
// for (let i = 0; i < 3; i++) {
// console.log(i)
// }
// console.log('内层结束 i = ' + i)
// }
// 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++) {
// 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 循环会产生两层作用域 ----------------------------------
// 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 修复了变量声明提升现象 --------------------------------------------
// console.log(foo)
// var foo = 'zce'
// console.log(foo)
// let foo = 'zce'
ES2015 const
ES2015 中新增的,可以声明一个只读的 恒量/常量 。特点就是在 let 之上多了一个只读特性,指的就是变量被声明之后就不能被修改。
声明和赋值不能放到两个语句当中。
不能被修改的意思是不能指向新的内存地址,并不是不能修改内部的成员属性。最典型的就是 const 一个对象,可以改变内部的成员属性,不能重定向变量本身的内存地址。
最佳实践:不用 var ,主用 const, 配合 let
// const name = 'zce'
// 恒量声明过后不允许重新赋值
// name = 'jack'
// 恒量要求声明同时赋值
// const name
// name = 'zce'
// 恒量只是要求内层指向不允许被修改
// const obj = {}
// 对于数据成员的修改是没有问题的
// obj.name = 'zce'
// obj = {}
ES2015 数组/对象的解构(Destructuring)
ES2015 新增了从数组或对象中获取指定元素的一种快捷方式,叫做解构。
数组中的解构是根据位置,对象的解构是根据属性名。
对象的解构可能存在一个同名的冲突,可以使用重命名的方式解决问题。
语法:{ name: objName} // objName是别名
// 数组的解构 array destructuring
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)
// 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)
/* =================================== */
// 对象的解构 object destructuring\
const obj = { name: 'zce', age: 18 }
// const { name } = obj
// console.log(name)
// const name = 'tom'
// const { name: objName } = obj
// console.log(objName)
// 同名冲突解决办法
// const name = 'tom'
// const { name: objName = 'jack' } = obj
// console.log(objName)
const { log } = console
log('foo')
log('bar')
log('123')
ES2015 模板字符串
ES2015 新增模板字符串,传统字符串使用单引号 ‘字符串’ 标识,模板字符串使用反引号 `字符串`
模板字符串新特性:
- 传统字符串不支持换行需要 \n 标识,模板字符串直接支持换行
- 插值表达式 `${n}` 嵌入
// 模板字符串
// 反引号包裹
// const str = `hello es2015, this is a string`
// 允许换行
// 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)
ES2015 带标签的模板字符串
定义模板字符串的前面添加一个标签,let str = tag`hello world` tag 实际上是一个函数,只要提前定义好,这里就会直接执行。
// 带标签的模板字符串
// 模板字符串的标签就是一个特殊的函数,
// 使用这个标签就是调用这个函数
// const str = console.log`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 字符串的扩展方法
.includes() // 是否包含
.startsWith() // 以 … 开头
.endsWith() // 以 … 结尾
// 字符串的扩展方法
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)
// }
// 默认参数一定是在形参列表的最后
function foo (enable = true) {
console.log('foo invoked - enable: ')
console.log(enable)
}
foo(false)
ES2015 剩余参数
对于未知个数的参数,以前都是使用 arguments 来接收,arguments 实际上是一个伪数组,操作起来不太方便,ES2015 当中提供了一个 … 操作符,可以在形参的前面加上 … 这时候,这个形参就会以数组的形式去接收从当前这个参数的位置开始,往后所有的实参。这种方式取代 arguments 来接收剩余参数。
因为接收的是所有的参数,所以这种操作符只能出现在形参的最后一位,而且只可以使用一次。
// 剩余参数
// function foo () {
// console.log(arguments)
// }
function foo (first, ...args) {
console.log(args)
}
foo(1, 2, 3, 4)
ES2015 展开数组
… 操作符除了收集外还可以展开。
// 展开数组参数
const arr = ['foo', 'bar', 'baz']
// console.log(
// arr[0],
// arr[1],
// arr[2],
// )
// console.log.apply(console, arr) // 以前
console.log(...arr) // 现在
ES2015 箭头函数
ES2015 允许使用等号、大于号(=>)这种类似箭头的符号来定义函数。
一来简化了函数的定义,二来,多了一些新得特性。
// 箭头函数
// function inc (number) {
// return number + 1
// }
// ES2015 最简方式
// const inc = n => n + 1
// 完整参数列表,函数体多条语句,返回值仍需 return
const inc = (n, m) => {
console.log('inc invoked')
return n + 1
}
console.log(inc(100))
const arr = [1, 2, 3, 4, 5, 6, 7]
// arr.filter(function (item) {
// return item % 2
// })
// 常用场景,回调函数
arr.filter(i => i % 2)
ES2015 箭头函数与 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()
ES2015 对象字面量的增强
对象,是 ECMAScript 中最常用的数据结构,ES2015 升级了对象字面量的语法。
// 对象字面量
const bar = '345'
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 // 计算属性名 [表达式]:值
}
// obj[Math.random()] = 123
console.log(obj)
obj.method1()
ES2015 Object.assign
ES2015 为 Object 提供了一些扩展方法。
Object.assign():将多个源对象中的属性复制到一个目标对象中。如果对象之间有相同的属性,那么源对象的属性会覆盖掉源对象中的属性。
// Object.assign 方法
// const source1 = {
// a: 123,
// b: 123
// }
// const source2 = {
// b: 789,
// d: 789
// }
// const target = {
// a: 456,
// c: 456
// }
/**
* target: 目标对象
* source1: 源对象1
* source2: 源对象2
* 这里覆盖顺序:source1 覆盖 target , source2 覆盖 source1 ... 后面如果还有同名成员属性的源对象,依次从后往前覆盖
*/
// const result = Object.assign(target, source1, source2)
// console.log(target)
// console.log(result === target)
// 应用场景
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)
ES2015 Object.is
用来判断两个值是否相等。
// Object.is
console.log(
// 0 == false // => true
// 0 === false // => false
// +0 === -0 // => true
// NaN === NaN // => false
// Object.is(+0, -0) // => false
// Object.is(NaN, NaN) // => true
)
ES2015 Proxy
如果想要监视对象中的属性读写,可以用 ES5 中提供的 Object.defineProperty 方法,去为对象添加属性,可以捕获到对象当中属性的读写过程。Vue 3.0 以前就是用的这样的方法实现的数据响应,从而完成的双向数据绑定。
ES2015 中全新设计了一个 Proxy 的类型,它是专门用来为对象设置访问代理器的。通过它可以轻松监视对象的读写过程。
相比于 defineProperty , proxy 功能更为强大,使用起来更加方便。
let person = {
name: 'Tom',
age: 200
}
const personProxy = new Proxy(person, {
get(target, property) {
console.log(target, property)
return target[property];
},
set(target, property, value) {
console.log(target, property, value)
target[property] = value
}
})
// console.log(personProxy.name)
personProxy.gender = '女'
console.log(person)
ES2015 Proxy 对比 defineProperty
- Proxy 更为强大一些,defineProperty 只能监视到对象属性的读取或者是写入,而 Proxy 它能够监视到很多 defineProperty 监视不到的行为(Proxy 能够监视到更多的对象操作),例如 delete 操作、对对象当中方法的调用等等。
- Proxy 更好的支持数组对象的监视
以往想要通过 defineProperty 监视数组的操作,最常见的方式是重写数组的操作方法(这也是 vue 当中所使用的方式),思路就是通过自定义的方法,覆盖掉数组原型上的方法,比如:push 、pop、shift 等等之类的一些方法,以此来劫持这个方法的调用过程。
这里用 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() ===============
// 优势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)
// 优势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)
// 优势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)
ES2015 Reflect:统一的对象操作 API
Reflect 是 ES2015 中提供的一个全新的内置对象,属于一个静态类(不能通过 new Reflect() 构建实例对象),只能通过 点 的方式调用静态类里边的一些静态方法,例如:Reflect.get(),和 Math 对象一样。
Reflect 内部封装了一系列对对象的底层操作,具体一共提供了 14 个,废弃掉一个,剩余 13 个,这 13 个方法的方法名与 Proxy 对象中的处理对象的方法成员是完全一致的;也就是说,Reflect 成员方法就是 Proxy 处理对象的默认实现。
为什么要有 Reflect 这样一个对象或者说它的价值体现在什么地方?
最大的价值就是:统一提供一套的操作对象的 API。
需要注意的是:目前,以前的对象的操作方式还是可以使用的,但是 ECMAScript 官方希望经过一段时间的过度过后,以后的标准当中,就会把之前的那些方法给废弃掉,而统一使用 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)
const obj = {
name: 'zce',
age: 18
}
/* 不统一的时候的操作 */
// console.log('name' in obj)
// console.log(delete obj['age'])
// console.log(Object.keys(obj))
/* 统一操作对象 API 使用 */
console.log(Reflect.has(obj, 'name'))
console.log(Reflect.deleteProperty(obj, 'age'))
console.log(Reflect.ownKeys(obj))
ES2015 Promise
这里先不讲,在 javascript 异步编程中详细介绍。这里提到 Promise 只是说它是 ES2015 新增的特性之一。
ES2015 class 类
在 class 之前,ECMAScript 中都是通过定义函数,以及函数的原型对象来实现的类型,自从 ES2015 开始,可以去使用一个叫 class 的关键词声明一个类型,这种独立定义类型的语法,相比较之前函数的方式要更容易理解,解构也会更加清晰。
// 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()
ES2015 静态方法
类型当中的方法,一般分为实例方法和静态方法。
实例方法就是要通过这个类型构造的实例对象去调用,而静态方法是直接通过类型本身去调用。
以前实现静态方法是直接在构造函数对象上去挂载方法去实现,因为 JS 中,函数也是对象,它也可以去添加一些方法成员,而在 ES2015 中,新增了一个专门用来添加静态方法的关键词,叫 static.
// 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()
ES2015 类的继承
继承,是面向对象中一个非常重要的特性,通过继承这种特性,我们就能够抽象出来相似类型之间重复的地方。
在 ES2015 之前,大多数情况都是使用原型的方式去实现继承,而在 ES2015 中,实现了一个专门用来继承的关键词,叫 extends 。
相比于原型继承要更方便,更清楚。
// 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()
ES2015 Set
ES2015 中提供了一个叫 Set 的全新数据结构,可以理解为 集合 。与传统的数组非常类似。不过 Set 内部的成员是不允许重复的,也就是说 每一个值,在 Set 中 都是唯一的。它是一个类型,通过这个类型构造的实例,可以用来存放不重复的数据。
常见应用场景:数组去重
// Set 数据结构
const s = new Set()
s.add(1).add(2).add(3).add(4).add(2)
// console.log(s)
// s.forEach(i => console.log(i))
// for (let i of s) {
// console.log(i)
// }
// console.log(s.size)
// console.log(s.has(100))
// console.log(s.delete(3))
// console.log(s)
// s.clear()
// console.log(s)
// 应用场景:数组去重
const arr = [1, 2, 1, 3, 4, 1]
// const result = Array.from(new Set(arr))
const result = [...new Set(arr)]
console.log(result)
// 弱引用版本 WeakSet
// 差异就是 Set 中会对所使用到的数据产生引用
// 即便这个数据在外面被消耗,但是由于 Set 引用了这个数据,所以依然不会回收
// 而 WeakSet 的特点就是不会产生引用,
// 一旦数据销毁,就可以被回收,所以不会产生内存泄漏问题。
ES2015 Map
Map 解构与 ECMAScript 中的对象非常类似,本质上都是键值对集合,但是对象中的键 只能是字符串的类型,所以去存放一些复杂数据结构的时候可能会存在一些问题。
Map 数据结构和对象最大的区别就是它可以用任意类型的数据作为键,而对象只能使用字符串作为键。
// Map 数据结构
// const obj = {}
// obj[true] = 'value'
// obj[123] = 'value'
// obj[{ a: 1 }] = 'value'
// console.log(Object.keys(obj))
// console.log(obj['[object Object]'])
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)
})
// 弱引用版本 WeakMap
// 差异就是 Map 中会对所使用到的数据产生引用
// 即便这个数据在外面被消耗,但是由于 Map 引用了这个数据,所以依然不会回收
// 而 WeakMap 的特点就是不会产生引用,
// 一旦数据销毁,就可以被回收,所以不会产生内存泄漏问题。
ES2015 Symbol
在 ECMAScript2015 之前,对象的属性名都是字符串,而字符串是有可能会重复的,如果重复的话就会产生冲突,解决这种问题最好的方式就是约定,添加前缀或者后缀等,但是约定的方式是规避了问题,并不是解决了问题,如果在这个过程当中,有人不遵守约定,这个问题将仍然存在。
ES2015 为了解决这样的问题,提供了一种全新的原始数据类型,叫做 Symbol(符号),它的作用就是表示一个独一无二的值(唯一,不会重复)。
从 ES2015 开始,对象可以直接使用 Symbol 类型的值作为属性名,也就是说现在对象的属性名是两种类型,分别是 String 和 Symbol 。
另外,Symbol 除了可以用来避免对象属性名重复产生的问题,还可以借助这种类型的特点去模拟实现对象的私有成员。
目前,这种类型的值主要的作用还是为对象添加一个独一无二的属性标识符。
截止到 ES2019 为止,一共定义了 6 种原始类型,加上 Object 类型,一共是 7 种数据类型。未来还会新增一个叫 Bigint 的原始数据类型,用于存放更长的数字。只是目前还处于 stage-4 阶段,待标准化过后就是 8 种原始数据类型了。
// Symbol 数据类型
// 场景1:扩展对象,属性名冲突问题
// // shared.js ====================================
// const cache = {}
// // a.js =========================================
// cache['a_foo'] = Math.random()
// // b.js =========================================
// cache['b_foo'] = '123'
// console.log(cache)
// =========================================================
// const s = Symbol()
// console.log(s)
// console.log(typeof s)
// 两个 Symbol 永远不会相等
// console.log(
// Symbol() === Symbol()
// )
// Symbol 描述文本
// console.log(Symbol('foo'))
// console.log(Symbol('bar'))
// console.log(Symbol('baz'))
// 使用 Symbol 为对象添加用不重复的键
// const obj = {}
// obj[Symbol()] = '123'
// obj[Symbol()] = '456'
// console.log(obj)
// 也可以在计算属性名中使用
// const obj = {
// [Symbol()]: 123
// }
// console.log(obj)
// =========================================================
// 案例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()
ES2015 Symbol 补充
// Symbol 补充
// console.log(
// // Symbol() === Symbol()
// Symbol('foo') === Symbol('foo')
// )
// Symbol 全局注册表 ----------------------------------------------------
// const s1 = Symbol.for('foo')
// const s2 = Symbol.for('foo')
// console.log(s1 === s2)
/* 注意:for 内部会将传入的值自动转为字符串 */
// console.log(
// Symbol.for(true) === Symbol.for('true')
// )
// 内置 Symbol 常量 ---------------------------------------------------
// console.log(Symbol.iterator)
// console.log(Symbol.hasInstance)
// const obj = {
// [Symbol.toStringTag]: 'XObject'
// }
// console.log(obj.toString())
// Symbol 属性名获取 ---------------------------------------------------
const obj = {
[Symbol()]: 'symbol value',
foo: 'normal value'
}
// for (var key in obj) {
// console.log(key)
// }
// console.log(Object.keys(obj))
// console.log(JSON.stringify(obj))
console.log(Object.getOwnPropertySymbols(obj))
ES2015 for … of 循环
ECMAScript 中,遍历数据有很多种方法,最基本的 for 循环,比较适用于遍历普通的数组;之后是 for … in 循环,它比较适合遍历键值对;再就是一些函数式的遍历方法,例如数组对象的 forEach 方法,这些遍历方式都有一定的局限性,所以 ES2015 借鉴了很多语言,引入了一种新的遍历方式,叫做 for … of 循环。
这种遍历方式,以后会作为遍历所有数据结构的统一方式。、
换句话说:只要明白 for … of 内部的工作原理,就可以用这种语法遍历任何一种自定义的数据结构。
// 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)
// }
ES2015 可迭代接口
for … of 循环是一种数据统一遍历方式,但是尝试之后发现只能遍历数组之类的数据结构,对于普通对象直接遍历,就会报错。
其实是酱紫的:ES 中,能够表示有结构的数据类型越来越多,为了给各种各样的数据结构提供一种统一的遍历方式,ES2015 就提出了一个叫做 Iterable 接口,意思就是可迭代的。
只要这个数据结构实现了可迭代接口(iterable),它就能够被 for … of 遍历。
也就是说之前所有能被 for … of 遍历的数据结构都实现了可迭代接口(iterable)。
总结:所有可以直接被 for … of 直接遍历的数据类型,都必须要实现叫做 iterable 的接口,也就是在内部必须挂在一个 iterator 方法,这个方法需要返回一个带有 next 方法的对象,不断调用这个方法就可以实现对内部所有数据的遍历。
// 迭代器(Iterator)
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())
while (true) {
const current = iterator.next()
if (current.done) {
break // 迭代已经结束了,没必要继续了
}
console.log(current.value)
}
ES2015 实现可迭代接口
了解了 for … of 循环内部的原理过后, 这里就可以说 for … of 循环是遍历所有数据结构的统一方式了,因为它内部就是去遍历被调用被遍历对象的 iterator 方法,得到一个迭代器,从而去遍历内部所有的数据。
同样的,如果我们的自定义对象也实现了 iterable 接口,那么就可以实现使用 for … of 循环去遍历这个对象。
// 实现可迭代接口(Iterable)
// const obj = {
// [Symbol.iterator]: function () {
// return {
// next: function () {
// return {
// value: 'zce',
// done: true
// }
// }
// }
// }
// }
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)
}
ES2015 迭代器模式
像这种的让我们的自定义对象实现可迭代接口,从而实现能够使用 for … of 循环去迭代我们的对象,这就是设计模式中的 迭代器模式 。
// 迭代器设计模式
// 场景:你我协同开发一个任务清单应用
// 我的代码 ===============================
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 () { // 现在的迭代器模式,是ES2015提供的一个语言层面的迭代器模式,所以可以适用任何数据结构,让外面直接适用 for ... of 就可以了
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.life) {
// console.log(item)
// }
// for (const item of todos.learn) {
// console.log(item)
// }
// for (const item of todos.work) {
// console.log(item)
// }
todos.each(function (item) {
console.log(item)
})
console.log('-------------------------------')
for (const item of todos) {
console.log(item)
}
ES2015 生成器
生成器(Generator),为了能够在复杂的异步代码中减少回调函数嵌套过深,产生的问题。从而提供的更好的异步编程解决方案。
最大的特性:惰性执行
// Generator 函数
// function * foo () { // 前边添加 *
// console.log('zce')
// return 100
// }
// const result = foo()
// console.log(result.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()) // 。。。
console.log(generator.next()) // 第四次调用,已经没有需要执行的内容了,所以直接得到 undefined
ES2015 生成器应用
// Generator 应用
// 案例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)
// 案例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)
}
ES2015 ES Modules
ES Modules 是 ES2015 语言层面的一套模块化标准规范,在模块化课程小节详细介绍。
ES2016 概述
仅包含两个小功能,
- 数组实例对象的 includes 方法(是否存在某个元素)。
- 指数运算符:两个星号(**)
// ECMAScript 2016
// Array.prototype.includes -----------------------------------
// 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))
// 指数运算符 ---------------------------------------------------
// console.log(Math.pow(2, 10))
console.log(2 ** 10)
ES2017 概述
最重要的一点:标准化了 Async 函数,配合 Await 关键词,就解决了异步编程中回调函数嵌套过深所产生的问题。使得代码变得简洁易读。
// ECMAScript 2017
// const obj = {
// foo: 'value1',
// bar: 'value2'
// }
// Object.values -----------------------------------------------------------
// console.log(Object.values(obj))
// Object.entries ----------------------------------------------------------
// console.log(Object.entries(obj))// 以数组的形式返回对象中所有的键值对
// for (const [key, value] of Object.entries(obj)) {
// console.log(key, value)
// }
// console.log(new Map(Object.entries(obj)))
// Object.getOwnPropertyDescriptors ----------------------------------------
// const p1 = {
// firstName: 'Lei',
// lastName: 'Wang',
// get fullName () {
// return this.firstName + ' ' + this.lastName
// }
// }
// // console.log(p1.fullName)
// // const p2 = Object.assign({}, p1)
// // p2.firstName = 'zce'
// // console.log(p2)
// const descriptors = Object.getOwnPropertyDescriptors(p1)
// // console.log(descriptors)
// const p2 = Object.defineProperties({}, descriptors)
// p2.firstName = 'zce'
// console.log(p2.fullName)
// 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, count)
// // }
// for (const [name, count] of Object.entries(books)) {
// console.log(`${name.padEnd(16, '-')}|${count.toString().padStart(3, '0')}`)
// }
// 在函数参数中添加尾逗号 -----------------------------------------------------
// function foo (
// bar,
// baz,
// ) {
// }
// const arr = [
// 100,
// 200,
// 300,
// ]
// const arr = [
// 100,
// 200,
// 300,
// 400,
// ]
// const arr = [
// 100,
// 200,
// 300
// ]
// const arr = [
// 100,
// 200,
// 300,
// 400
// ]