ECMAscript 新特性
一概述
1.了解的意义
- 语言与平台之间的关系
- 系统化学习 ECMAscript 高级进阶的需要
- 现代化、高质量的代码的需要
2.基本概念
- 简写为 ES , 也是一门脚本语言, 通常看作 javascript 的标准化规范
- javascript 实际是 ECMAscript 的扩展语言, ECMAscript 只提供了最基本的语法, 并不能完成实际功能开发
- javascript 实现了 ECMAscript 标准, 并做了扩展, 在浏览器能够操作 dom、bom , 在node 环境中读写文件
- javascript 语言本身即 ECMAscript
2015年开始, ES 保持每年一个版本的迭代, ES 新特性陆续出现, 使 javascript 越来越高级、便捷. ES2015 在 ES5 发布后经过六年才完全标准化, 包含了许多颠覆式的新功能, 需要我们深入学习. 从 ES2015 后, ES 不再使用 版本号命名, 而使用年份, 如 ES2016、 ES2017. 在 ES5 之后发布的 ES2015 也称为 ES6.
3.ES6
ES2015, 也称 ES6, 新时代 ECMAscript 的代表版本: 相比较于 ES5.1 变化较大 ; 自此, 命名标准也发生了变化 很多开发者用 ES6 泛指 ES5.1 之后的所有新标准, 如 ES6 新规范 async/await 属于 ES2017 的新规范
文档地址: 262.ecma-international.org/6.0/
主要变化:
- 解决原有语法的问题和不足, 如 let、 cont 提供的块级作用域
- 对原有语法进行增强, 使语言便捷、易用. 如解构、展开、参数默认值、模版字符串
- 全新的对象、全新的功能、全新的方法,如: Promise、Proxy、Object的方法等
- 全新的数据结构, 如 set、map、symbol等
运行环境
- chrome 浏览器
- nodeJs, -v 12.13.0,
- nodemon 工具, 修改完成自动执行代码,
// 安装
npm i nodemon
// 使用:
nodemon index.js
二语法介绍
1.变量声明
let 与块级作用域
作用域: 代码中某成员起作用的范围. 在 ES2015 之前, 只有函数作用域和函数作用域, ES2015 中新增了块级作用域, 块指的是用 {} 包裹起来的范围.
if(true){
var foo = true;
}
console.log(foo) // true
if(true){
let foo = true;
}
console.log(foo) // foo is not defined
循环嵌套案例中的问题:
for(var i=0; i<3; i++){
for(var i=0; i<3; i++){
console.log(i)
}
console.log("循环结束之后的i:" , i)
}
// 0
// 1
// 2
// 循环结束之后的i: 3
上面代码,在内层循环完毕之后, 内部的变量 i 已经改变, 并且将外部的变量覆盖, 使用块级作用域:
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
循环注册事件时候, 访问计数器
var elements = [{}, {}, {}]
for(var i=0; i<elements.length; i++){
elements[i].onclick = function(){
console.log(i)
}
}
// 闭包解决
for(var i=0; i<elements.length; i++){
elements[i].onclick = (function(){
return function(){
console.log(i)
}
})(i)
}
// 块级作用域解决
for(let i=0; i<elements.length; i++){
elements[i].onclick = function(){
console.log(i)
}
}
关于 for 循环的两层作用域:
for(let i=0; i<3; i++){
let i = 'foo'
console.log(i)
}
// foo
// foo
// foo
我们看到内部定义的 i 变量并没有受到循环 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++
for 循环内部是两级嵌套的作用域, 循环体内部 与 循环内部是两个独立的作用域 let 声明不会变量提升
console.log(a);
console.log(b);
var a = 1;
let b = 2;
// undefined
// Cannot access 'b' before initialization
const
const : 声明一个只读的衡量,常量, 声明之后不可修改. 声明之后必须赋值 不可修改是指声明之后不可修改内存地址, 并不是不可以修改内部属性
const a = 12
console.log(a)
a = 11
//Assignment to constant variable.
const obj = {}
console.log(obj)
obj.a = 12
console.log(obj)
// {}
// { a: 12 }
最佳实践:不用 var(避免先使用再声明等开发陋习), 主用 const (更明确开发成员是否会修改), 配合 let
2.解构
Destructuring, 从数组或者对象中获取指定元素的快捷方式.
数组解构
const arr = [100, 200, 300, 700, 1000]
// 原始写法
const arr1 = arr[0]
const arr2 = arr[1]
const arr3 = arr[2]
// 新写法
const [arr1, arr2, arr3] = arr
console.log(arr1, arr2, arr3)
通过解构获取指定位置成员
const arr = [100, 200, 300, 700, 1000]
const [ , , , arr4] = arr
console.log(arr4) // 700
解构位置的变量名之前添加三个点提取当前位置开始的所有元素
const arr = [100, 200, 300, 700, 1000]
const [arr1, , ...rest] = arr
console.log(rest) // [ 300, 700, 1000 ]
解构位置的成员长度小于被解构数组的长度, 按照从前到后的顺序解构 解构位置的成员长度大于被解构数组的长度, 解构值为undefined, 类似访问数组不存在的下标
const arr = [100, 200]
const [foo] = arr;
console.log(foo) // 100
const [bar, bbr, bcr] = arr
console.log(bar, bbr, bcr) // 100, 200 undefined
可以给解构位置成员设置默认值
const arr = [100, 200]
const [bar, bbr, bcr = '默认'] = arr
console.log(bar, bbr, bcr) // 100, 200 默认
应用场景
const path = "/foo/bar/test"
const [ , rootDir] = path.split('/')
console.log(rootDir) // foo
对象解构
根据属性名匹配提取
const obj = {
name: "a",
age: "b"
}
const { name } = obj
console.log(name)
其他特点与数组一致, 如未匹配到的成员为 undefined , 可以设置默认值. 需要注意的是, 由于解构的变量名是用来匹配解构对象的属性名的, 当该变量名在作用域中存在同名的成员时, 会出现冲突
const obj = {
name: "a",
age: "b"
}
const name = "quanju"
const { name } = obj
console.log(name) // Identifier 'name' has already been declared
可以通过重命名的方式解决, 在解构位置的成员名后加冒号和新的成员名.
const obj = {
name: "a",
age: "b"
}
const name = "quanju"
const { name:objName } = obj
console.log(objName) // Identifier 'name' has already been declared
应用场景
const { log } = console
log(1)
3.模版字符串
模版字符串字面量
在 ES2015 中还增强了定义字符串的方式, 传统定义字符串的方式是单引号或者双引号, ES2015可以通过反引号的方式定义字符串,
const str = "hello world , my name is javascript"
const strNew = `hello world , my name is \`javascript\``
console.log(str)
console.log(strNew)
// hello world , my name is javascript
// hello world , my name is `javascript`
传统字符串不支持换行, 需要用 \n 表示, 模版字符串支持多行字符串. 模版字符串支持插值表达式的方式嵌入所对应的数值
const name = "tom"
const msg = `hey ${name}`
console.log(msg)
带标签的模版字符串, 可以接收到字符串的数据, 以数组的形式获取
const str = console.log`hello world`
[ 'hello world' ]
const name = "tom"
const gender = true
function TagFunc (strings) {
console.log(strings)
}
const result = TagFunc`hey, ${name} is a ${gender} `
// [ 'hey, ', ' is a ', ' ' ]
带标签的模版字符串, 可以接收到变量的数据
const name = "tom"
const gender = true
function TagFunc2 (strings, name, gender) {
console.log(strings,name, gender)
}
const result2 = TagFunc2`hey, ${name} is a ${gender} `
// [ 'hey, ', ' is a ', ' ' ] tom true
带标签的模版字符串, 返回值就是字符串的结果
const name = "tom"
const gender = true
function TagFunc3 (strings, name, gender) {
return 123
}
const result3 = TagFunc3`hey, ${name} is a ${gender} `
console.log(result3)
// 123
带标签的模版字符串, 可以对字符串数据进行处理,返回需要的字符串数据
const name = "tom"
const gender = true
function TagFunc4 (strings, name, gender) {
const sex = gender ? "man" : "women"
return strings[0] + name + strings[1] + sex + strings[2]
}
const result4 = TagFunc4`hey, ${name} is a ${gender} `
console.log(result4)
// hey, tom is a man
4,.字符串的扩展方法
一组方法判断字符串中是否包含指定内容
- includes()
- startsWith()
- endsWidth()
const msg = 'Error: foo is not defined.'
console.log(msg.includes('foo'))
console.log(msg.startsWith('Error'))
console.log(msg.endsWith('.'))
// true
// true
// true
5.函数参数
ECMAscript2015 中为函数的形参列表扩展了新语法
参数默认值
以前为函数参数定义默认值需要在函数体中实现 ECMAscript2015 可以在参数重直接设置默认值 一般带有默认值的参数需要在函数多个参数的最后
function foo(enable){
enable = enable === undefined ? true : enable;
}
function foo( enable = true ){
enable = enable === undefined ? true : enable;
}
剩余参数
对于未知个数的参数, 以前通过 arguments 伪数组接收 ECMAscript2015 可以在通过 ... 操作符实现 由于接收的是所有的参数,所以该操作符只能出现在形参的最后一位,只能出现一次
function foo(...args){
console.log(args)
}
foo(1,2,3,4)
// [ 1, 2, 3, 4 ]
6.展开数组
... 操作符除了用于收取剩余参数(rest), 还可以展开数组(spread)
const arr = ['foo', 'bar', 'dear']
console.log(
arr[0],
arr[1],
arr[2],
)
console.log.apply(console, arr)
console.log(...arr)
// foo bar dear
7.箭头函数
简化了函数表达式的定义方式, 多了一些新特性 箭头左边是参数列表,多个参数可以用 () 定义, 箭头右边是函数体, 多条表达式语句需要使用 {} 包裹 用 return 返回
function foo () { }
const inf = n => n+1
箭头函数的方式定义函数, 使代码更加简短易读.
const arr = [1, 2, 3, 5, 7, 11, 24, 9]
arr.filter(function(v){
return v>6
})
arr.filter(v => v>6)
箭头函数最大的改变就是不会改变 this 指向
const person = {
name: 'tom',
sayHello: function(){
console.log(
`hello, my name is ${this.name}`
)
},
sayHello2: () => {
console.log(
`hello, my name is ${this.name}`
)
},
sayHiAsync1: function(){
setTimeout(function(){
console.log(this.name)
},1000)
},
sayHiAsync2: function(){
let _this = this
setTimeout(function(){
console.log(_this.name)
},1000)
},
sayHiAsync3: function(){
setTimeout(() => {
console.log(this.name)
},1000)
},
}
person.sayHello()
person.sayHello2()
// hello, my name is tom
// hello, my name is undefined
person.sayHiAsync1()
person.sayHiAsync2()
person.sayHiAsync3()
// undefined
// tom
// tom
8.对象字面量增强
对象是我们最常用的数据结构, 在 ECMAscript2015 中升级了字面量的语法. 传统的对象字面量要求我们使用 属性名 冒号 属性值 一一对应的方式, 在新语法中, 变量名与添加到对象中的属性名一致的时候, 可以简写.
const foo = "foo"
const obj = {
bar: "bar" , // 传统
foo , // 等价于 foo: foo
}
对象添加方法: 传统方式 属性名 冒号 function, 新语法, 省略 冒号、function
const obj = {
// 传统方式
methods1: function(){
},
// 新语法
methods2 () {
// 也是普通的function
// this指向的是当前对象
}
}
对象可以使用表达式的返回值作为对象属性名. 在 ECMAscript2015 之后可以通过 [] 直接使用动态值, 称为 计算属性名 用 [] 包裹表达式, 将表达式的运行结果作为对象的属性名
// 传统方式
const obj = {
a: "11",
Math.random(): 123 // 报错
}
obj[Math.random()] = 123
// 新语法
const obj = {
a: "11",
[Math.random()]: 123 ,// 正确
}
9.对象扩展方法
Object.assign
将多个原对象中的属性, 复制到一个目标对象中, 如果存在相同的属性,原对象中的属性覆盖目标对象的属性 从原对象去 向目标对象放 支持传入任意个数的对象, 第一个参数为目标对象, 返回目标对象
const source = {
a: 12,
b: 34
}
const target = {
a: 1,
c: 55
}
const result = Object.assign(target, source)
console.log(result)
console.log(result === target)
// { a: 12, c: 55, b: 34 }
// true
应用场景:复制对象
const obj1 = {name:"tom"}
function fun1(obj){
obj.name = 'lili'
}
fun1(obj1) // { name: 'lili' }
上面代码中, 运行函数, 改变了原来对象上属性的值, 我们可以通过复制一个新对象的方式避免这种问题的发生
const obj2 = {name:"tom"}
function fun2(obj){
const funobj = Object.assign({}, obj)
funobj.name = 'lili'
}
fun2(obj2)
console.log(obj2) // { name: 'tom' }
Object.is
判断两个值是否相等, 在 ECMAscript2015 之前一般使用 相等(==)或 严格相等(===) 比较两个数据是否相等
console.log( 0 == false) // true
console.log( 0 === false) // false
console.log( +0 === -0 ) // true
console.log( NaN === NaN ) // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
10.代理对象 Proxy
如果我们想要监视对象的某个属性的读写, 我们可以使用 ES5 提供的 Object.defineProperty() 为对象添加属性, 从而捕获对象的读写过程. ECMAscript2015 中新增了 Proxy 为对象添加代理,可以轻松监视对象的读写过程.
const person = {
name: "tom",
age: 20
}
const personProxy = new Proxy(person, {
get(target, property){
console.log(target, property)
return property in target ? target[property] : "default"
},
set(target, property, value){
console.log(target, property, value)
// 拦截 做数据校验
if(property === 'age'){
if( !Number.isInteger(value) ){
throw new TypeError(`${value} is not int`)
}
}
target[property] = value
}
})
personProxy.name // { name: 'tom', age: 20 } name
personProxy.sex // { name: 'tom', age: 20 } sex
personProxy.age = "sss"
// { name: 'tom', age: 20 } age sss
// sss is not int
personProxy.age = 18
// { name: 'tom', age: 20 } age 18
与Object.defineProperty()区别
- defineProperty只能监视属性的读写, Proxy 能够监视对象更多的操作(如delete等)
const person = {
name: "tom",
age: 20
}
const personProxy = new Proxy(person, {
deleteProperty(target, property){
console.log(target, property)
delete target[property]
}
})
delete personProxy.age // name: 'tom', age: 18 } age
console.log(person) // { name: 'tom' }
- 对数组的操作, defineProperty 监听数组需要重写数组方法.Proxy 可以直接监听数组变化
const list = []
const listProxy = new Proxy(list, {
set(target, property, value){
console.log(target, property, value)
target[property] = value
return true
}
})
listProxy.push(100)
// [] 0 100
// [ 100 ] length 1
- Proxy 以非侵入的方式监管了对象的读写, 对于定义好的对象, 不需要对对象本身做任何操作, 就可以监视对象读写. defineProperty 要求我们以特定的方式单独定义对象中需要监视的属性.
11.Reflect
ECMAscript2015 提供的一个全新的内置对象, 是一个统一的对象操作 API,. Reflect 属于静态类, 不能通过 New 构造实例对象, 只能调用其中的静态方法, 如 Reflect.get() Reflect 封装了一系列对对象的底层操作, 一共封装了 13 个方法, 是 Proxy 处理对象方法的默认实现, 也就是当我们没有定义对像的处理方法, 内部的方法默认实现的逻辑为 Reflect 对象中的方法.
const obj = {
name: "tom",
age: 14
}
const ProxyObj = new Proxy(obj, {
get (target, property){
return Reflect.get(target, property)
}
})
意义: 提供了统一用于操作对象的 API, 愿先对于对象的操作, 我们可以使用 Object 上面的方法, 也可能使用 delete、in 等操作符, Reflect统一了对象的操作方式.
const obj = {
name: "tom",
age: 14
}
// 判断属性是否存在
'name' in obj
Reflect.has("name")
// 删除属性
delete obj.name
Reflect.deleteProperty("name")
// 获取对象的属性
Object.keys(obj)
Reflect.ownKeys("name")
12.Promise
全新的异步编程解决方案, 链式调用解决了回调函数嵌套过多的问题.详细请看 Javascript异步编程.
13.class 类
基本使用
在 ECMAscript2015 之前, javascript 通过定义函数以及函数的原型对象实现类型.
// 定义函数作为构造函数
function Person () {
// 通过this访问当前的实例对象
this.name = name;
}
// 通过 prototype 共享成员方法
Person.prototype.say(){
console.log(this.name )
}
ECMAscript2015 之后, 通过 class 关键字独立定义类型.
class Person {
// 构造函数
constructor(name){
this.name = name
}
// 直接定义实例方法
say () {
console.log(this.name )
}
}
// 使用new关键词创建实例
const p = new Person('aa')
p.say()
类中的方法
类中的方法分为静态方法和实例方法两种. 实例方法是通过类构造的实例对象调用 静态方法可以直接通过类型本身调用. 以前我们实现静态方法直接在构造函数对象上挂载方法. ECMAscript2015 之后新增了 static 关键词定义静态方法.
class Person {
// 构造函数
constructor(name){
this.name = name
}
// 直接定义实例方法
say () {
console.log(this.name )
}
// 静态方法
static create (name) {
return new Person(name)
}
}
// 使用new关键词创建实例
const p = new Person('aa')
p.say()
const p2 = Person.create('aa')
需要注意的是, 静态方法是直接挂载在类型上的, 所以静态方法内部的 this 不会指向实例, 而是指向当前类型.
继承
继承是面向对象一个非常重要的特性, 通过继承我们可以将对象之间相同的特性进行抽象, 在 ECMAscript2015 之前, 通过原型的方式实现, 在 ECMAscript2015中定义了 extends 关键词实现继承
class Person {
constructor(name){
this.name = name
}
say () {
console.log(this.name )
}
static create (name) {
return new Person(name)
}
}
class Students extends Person {
constructor (name, studentId) {
super(name) // 始终指向父类, 相当于调用父类的构造函数
this.studentId = studentId
}
hello(){
super.say();
console.log(`my school id is ${this.studentId}`)
}
}
const s = new Students("bb")
s.hello()
14.Set 数据结构
ECMAscript2015 新增了 Set 的全新数据结构, 与数组类似, 不同的是 Set 的内部成员不允许重复. Set 是一个类型, 通过构造的实例存放不重复的数据 可以通过实例的 add 方法添加数据,并返回对象本身, 可以链式调用. 通过 forEach 方法 或者 of 方法进行遍历 通过 size 属性 获取集合的长度 通过 has 方法判断集合中是否存在特定的值 通过 delete 方法删除指定值, 返回 true 或 false 通过 clear 方法清除所有数据
const s = new Set();
s.add(1);
s.add(12);
s.add(15);
s.add(17);
console.log(s)
s.forEach(i => cosnole.log(i))
console.log(s.size)
console.log(s.has(11))
console.log(s.delete(15))
s.clear()
console.log(s)
/**
Set { 1, 12, 15, 17 }
1
12
15
17
4
false
true
Set {}
*/
最常见的应用场景是数组去重.
const arr = [1, 3, 34, 1, 15, 6, 35, 5, 15]
const ss = new Set(arr)
console.log(ss)
// Set { 1, 3, 34, 15, 6, 35, 5 }
const resArr = Array.from(ss)
console.log(resArr)
/* [
1, 3,
34, 15,
6, 35,
5
]
*/
const resArr2 = [...new Set(arr)]
console.log(resArr2)
/* [
1, 3,
34, 15,
6, 35,
5
]
*/
15.Map 数据解构
Map 数据解构与对象类似,本质都是键值对集合. 一般对象结构中的键只能是字符串, 所以在存放复杂数据结构的时候会有问题.
const obj = {}
obj[true] = "value"
obj[1] = "value"
obj[{a:1}] = "value"
console.log(Object.keys(obj))
// [ '1', 'true', '[object Object]' ]
上面代码我们可以看到, 我们设置的布尔值、数字、对象类型的键盘都转换为了字符串. 也就是说如果我们给对象添加的键不是字符串, 就会被用 toString 转换为字符串. ECMAscript2015 中的 Map 解决了这种问题, Map 可以说才是从严格意义上键值对的集合, 用来映射两个任意类型数据之间的关系. 通过 set 方法存数据 通过 get 方法获取数据 通过 has 方法判断是否存在 判断 claer 方法清空所有数据 通过 forEach 方法遍历数据
const m = new Map();
m.set({a:1}, false)
console.log(m)
// Map { { a: 1 } => false }
16.Symbol
基本使用
一种全新的原始数据类型, 在 ECMAscript2015 之前, 对象的属性名都是字符串, 字符串是可能重复的, 重复就会产生冲突.
/// a.js
cache["aa"] = foo;
// b.js
cache["aa"] = 123;
这种情况在我们引用第三方库,扩展第三方模块对象进行开发的时候, 很容易发生, 一般我们通过约定的方式规避这样的问题
/// a.js
cache["a_aa"] = foo;
// b.js
cache["b_aa"] = 123;
ECMAscript2015 提供了一种全新的数据类型 Symbol 解决这样的问题.
const sm = Symbol();
console.log(sm) // Symbol()
console.log(typeof sm) // symbol
这种类型最大的特点就是独一无二,也就是说我们通过 Symbol 函数创建的值永远不会重复.
const sm1 = Symbol();
const sm2 = Symbol();
console.log( sm1=== sm2) // false
考虑到开发过程中的调试, Symbol 函数允许我们传递一个字符串作为参数表示对数据的描述
const sm3 = Symbol("sm3");
console.log(sm3) // Symbol(sm3)
ECMAscript2015 之后开始, 对象可以使用 Symbol 类型的值作为属性名.
const objSm = {
[Symbol("sm3")]: 123
}
objSm[Symbol()] = 333;
objSm[Symbol()] = 322;
console.log(objSm)
// { [Symbol(sm3)]: 123, [Symbol()]: 333, [Symbol()]: 322 }
Symbol 除了能够解决对象名重复产生的问题, 还可以实现对象的私有成员. 以前我们通过约定来表示对象的私有成员, 如用下划线开头( _a ).
// a.js
const name = Symbol()
class Person {
[name]: "aa",
say(){
}
}
/// b.js
const p = new Person()
p.say();
如上面代码所演示, 在对象内部我们可以使用创建属性时的 Symbol 拿到对应的属性成员, 在外部文件中 我们无法创建一个完全相同的Symbol, 所以实现了私有成员. Symbol 目前最主要的作用就是为对象添加独一无二的属性标识.
注意事项
唯一性: 每次调用 Symbol 函数产生的结果都是唯一的, 不管我们传入的描述文本是否相同.
Symbol('foo') === Symbol('foo') // false
我们可以使用全局变量或者静态的 for 方法 实现在全局复用一个相同的值
for 方法接收一个字符串作为参数, 相同的字符串返回相同的 Symbol 值, Symbol.for("foo") === Symbol.for("foo")
for 提供了一个全局注册表, 提供了 字符串 和 Symbol 一一对应的关系.
for 传入非字符串的时候, 内部方法会自动转换为字符串 .Symbol.for("true") === Symbol.for(true)
Symbol 对象提供了内置的常量, 用来作为内部方法的标识, 这些标识符可以让自定义对象实现js中内置的接口
const obj = {}
console.log(obj.toString())
console.log(obj.toString())
// [object Object]
const obj13 = {
[Symbol.toStringTag]:'xxObject'
}
console.log(obj13.toString())
// [object xxObject]
使用 Symbol 作为对象的属性名, 通过 forEach 或 Object.keys() 是无法拿到的. 使用 JSON.stringfy() 序列化对象也会忽略该属性. 只可以通过 getOwnPropertySymbols 获取所有 Symbol 类型的属性名.
const objSymbol = {
Symbol():'Symbolonly',
aa: 15
}
console.log(Object.keys(objSymbol) )// [ 'aa' ]
console.log(JSON.stringify(objSymbol)) // {"aa":15}
console.log(Object.getOwnPropertySymbols(objSymbol)) // [ Symbol() ]
17.for... of 循环
基础语法
ECMAscript 中遍历数据有很多种方法, for 循环适合遍历普通数组, for... in 循环适合遍历键值对, 还有一些函数式的遍历方法. ECMAscript2015 引用了全新的遍历方式, 作为遍历所有数据结构的统一方式. 对数组的遍历
const array = [133, 345, 233, 445]
for(let item of array){
console.log(item)
}
// 133, 345, 233, 445
for of 循环 可以使用 break 跳出循环.
const array = [133, 345, 233, 445]
for(let item of array){
console.log(item)
if(item > 200){
break
}
}
// 133, 345
for of 除了可以遍历数组对象, 还可以对伪数组进行遍历.
function foo(){
for(let item of arguments){
console.log(item)
}
}
Fun('1',"22","333") // 1 22 333
Set 和 Map 对象也可以通过 for of 进行遍历
const set = new Set(["aa", "bb", "cc"])
for(let item of set){
console.log(item)
}
需要注意的是, Map 对象的遍历,我们得到的是对象的键和值组成的数组.
const map = new Map()
map.add('name', "tom")
map.add('age', "18")
for(let item of map){
console.log(item)
}
// [ 'name', 'tom' ]
// [ 'age', '18' ]
for(let [key, value] of map){
console.log(key, value)
}
对象的遍历
const obj = {name: "tom", age: 18}
for(let item of obj){
console.log(item)
}
// objet is not iterable 不可迭代
18.迭代器
可迭代接口
for...of 作为一种统一的遍历数据的方式, 我们遍历普通的对象却发生了错误.这是什么原因呢? 在 ES 中, 能够表示有结构的数据越来越多, 如 Set、Map、Array, 为了为各种各样的数据结构提供统一的遍历方法, ES2015 提供了 Iterable 的接口. 接口可以理解为一种统一的规格标准. Iterable 接口就是一种能够被 for...of 循环遍历访问的规格标准, 所以含有 Iterable 接口的对象就可以被 for...of 遍历.
我们继续观察到 Symbol.iterator 是一个方法, 在 Symbol.iterator 内部有一个 next 方法.
const arr = [1,2,3];
const iterator = arr[Symbol.iterator]()
iterator.next();
// {value: 1, done: false}
iterator.next();
// {value: 2, done: false}
iterator.next();
// {value: 3, done: false}
iterator.next();
// {value: undefined, done: true}
总结
所有能被 for...of 循环遍历的数据类型都需要实现 Iterable 的接口, Iterable 内部挂载了一个 iterator 方法, 返回一个带有 next 方法的对象, 调用 next 方法就可以实现对数据的遍历. 这就是 for...of 循环的工作原理.
实现
for...of 循环通过调用被循环对象的 iterator 方法,得到一个迭代器从而得到所有数据. 了解 for...of 的工作原理之后, 我们就能够理解为什么 for...of 循环为什么能作为遍历所有数据的统一方法了. 我们可以在普通对象上挂载一个 iterator 方法, 使普通对象能够通过 for...of 循环遍历.
const obj = {
[Symbol.iterator]: function () {
return {
next: function(){
return {
value: '123',
done: false
}
}
}
}
}
三个对象
Iterable: 可迭代接口, 规定内部必须有一个返回迭代器的 iterator 方法.
Iterator: 迭代器接口, 内部必须有一个用于迭代的 next 方法.
IterationResult: 迭代结果接口, value表示当前迭代数据, done 表示迭代是否结束
const objIterator = {
store:["foo", "bar", "aa"],
[Symbol.iterator]: function () {
let index = 0;
let self = this;
return {
next: function(){
let result = {
value: self.store[index],
done: index === self.store.length
}
index++
return result
}
}
}
}
for(let item of objIterator){
console.log(item)
}
// foo
// bar
// aa
迭代器模式
下面我们通过一个案例演示一下迭代器模式. 案例 协同工作完成一个任务清单
// aa 生成任务列表
const todoList = {
life :["吃饭", "睡觉", "打豆豆"],
works:["语文", "数学". "英语"],
}
// bb 处理任务列表数据
for(let item of todoList.life){
console.log(item)
}
for(let item of todoList.works){
console.log(item)
}
如上面的代码所示, 当我们需要协同完成一个任务列表的开发任务, A 负责生成任务列表, B 负责处理数据. 按照上面的处理方式, 当 A 的数据发生变化时, B 的数据处理逻辑也需要发生变化.如下:
// aa 生成任务列表
const todoList = {
life :["吃饭", "睡觉", "打豆豆"],
learn:["语文", "数学". "英语"],
workes:["写代码"],
}
// bb 处理任务列表数据
for(let item of todoList.life){
console.log(item)
}
for(let item of todoList.learn){
console.log(item)
}
for(let item of todoList.workes){
console.log(item)
}
像这样极度耦合的代码大大增强了维护的难度, 我们可以对外暴露一个遍历数据的接口, 使代码更加通用.
// aa 生成任务列表
const todoList = {
life :["吃饭", "睡觉", "打豆豆"],
learn:["语文", "数学". "英语"],
workes:["写代码"],
each (callBack) {
let alls = [].concat(this.life, this.learn, this.workes)
for(let item of alls){
callBack(item)
}
}
}
// bb 处理任务列表数据
todoList.each(todo=>{
console.log(todo)
})
如上代码所示, 通过对外暴露接口的方式, 减少了重复代码, 使代码维护更加方便了, 我们可以通过迭代器实现同样的功能.
// aa 生成任务列表
const todoList = {
life :["吃饭", "睡觉", "打豆豆"],
learn:["语文", "数学". "英语"],
workes:["写代码"],
each (callBack) {
let alls = [].concat(this.life, this.learn, this.workes)
for(let item of alls){
callBack(item)
}
},
[Symbol.iterator]: function(){
let alls = [].concat(this.life, this.learn, this.workes)
let index = 0;
return {
next: function(){
let result = {
value: alls[index],
done: index >= alls.length,
}
index++;
return result
}
}
}
}
// bb 处理任务列表数据
for(let todo of todoList){
console.log(todo)
}
迭代器的核心就是, 对外提供统一遍历数据的接口, 让外部不需要关心数据内部的结构. 上面我们自己的 each 方法只能适用于当前数据结构, 而 ES2015 提供的迭代器是语言层面实现的迭代器模式, 通用于任何数据结构, 只要在内部实现 iterator 方法就可以了.
19.生成器
基本语法
为了避免回调函数嵌套产生的问题, ES2015 中新增了生成器函数.
- 在普通的函数 function 关键字后加*
- 生成器函数运行之后返回一个 Generator 对象
- Generator 对象上有一个 next 方法, 调用 next 方法, 函数开始执行.
- 与 yield 关键词配合使用, 可以是函数执行暂停.
- yield 后面的值作为 next 的值.
function * foo (){
console.log(111)
yield 100
console.log(222)
yield 200
console.log(333)
yield 300
}
const generator = foo()
console.log(generator.next())
// 111
// { value: 100, done: false }
console.log(generator.next())
// 222
// { value: 200, done: false }
console.log(generator.next())
// 333
// { value: 300, done: false }
console.log(generator.next())
// { value: undefined, done: true }
案例实现
自增id 发号器
function * createId(){
let id = 1;
while(true){
yield id++;
}
}
const idMaker = createId();
console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)
我们可以使用生成器函数实现 iterator 方法
const todoList = {
life :["吃饭", "睡觉", "打豆豆"],
learn:["语文", "数学". "英语"],
workes:["写代码"],
each (callBack) {
let alls = [].concat(this.life, this.learn, this.workes)
for(let item of alls){
callBack(item)
}
},
[Symbol.iterator]: function * (){
let alls = [].concat(this.life, this.learn, this.workes)
for(let item of alls){
yield item
}
}
}
// bb 处理任务列表数据
for(let todo of todoList){
console.log(todo)
}
生成器函数最主要的功能是解决异步回调函数嵌套的问题, 详细请看上一篇 javascript 异步编程.
20.ES2016概述
数组-includs
直接查找数组中是否包含某个值. 返回布尔值. 可以直接查找 NaN.
// Array.prototype.includes
const arr = ["foo", 12, "bar", NaN]
console.log(arrInclu.indexOf('bar')) // 2
console.log(arrInclu.includes('bar')) // true
console.log(arrInclu.indexOf(NaN)) // -1
console.log(arrInclu.includes(NaN)) // true
数组-指数运算 **
与加减乘除一样, 称为语言本身的运算符.
Math.power(2,4)
console.log(Math.pow(2,4)) // 16
console.log( 2 ** 4 ) // 16
21.ES2017概述
Object.values
与 Object.keys 类似, Object.keys 以数组的形式返回对象的键, Object.values 以数组的形式返回对象的值.
console.log(Object.keys({name:"li", age:12}))
// [ 'name', 'age' ]
console.log(Object.values({name:"li", age:12}))
// [ 'li', 12 ]
Object.entries
将对象转换为数组形式.
- 用 for of 遍历
- 转换为 ma p对象
console.log(Object.entries({name:"li", age:12}))
// [ [ 'name', 'li' ], [ 'age', 12 ] ]
// for of 遍历
for(let [key, value] of Object.entries({name:"li", age:12})){
console.log(key, value)
}
// name li
// age 12
// 转为 map
console.log(new Map(Object.entries({name:"li", age:12})))
// Map { 'name' => 'li', 'age' => 12 }
Object.getOwnPropertyDescriptors
可以获取对象 get set 方法的完整信息, 解决 Object.assign 方法难以复制对象这些属性的弊端.
const nameObj = {
firstName:"li",
lastname:"lei",
get FullName(){
return this.firstName + " " + this.lastname
}
}
Object.getOwnPropertyDescriptors(nameObj)
console.log(nameObj.FullName)
// li lei
const nameSelf = Object.assign({},nameObj)
nameSelf.firstName = "wang"
console.log(nameSelf.FullName)
// li lei
const descriptors = Object.getOwnPropertyDescriptors(nameObj)
console.log(descriptors)
/*
{
firstName: { value: 'li', writable: true, enumerable: true, configurable: true },
lastname: {
value: 'lei',
writable: true,
enumerable: true,
configurable: true
},
FullName: {
get: [Function: get FullName],
set: undefined,
enumerable: true,
configurable: true
}
}
*/
const nameSelf2 = Object.defineProperties({}, descriptors)
nameSelf2.firstName = "wang"
console.log(nameSelf2.FullName)
// wang lei
padStart、padEnd
用给定的字符串填充目标字符串开始或结束位置, 直到达到指定长度为止.
const nameObj = {
firstName: "li",
lastname: "lei",
age: 17,
score: 99
}
for(let [key, value] of Object.entries(nameObj)){
console.log(key, value)
}
/*
firstName li
lastname lei
age 17
score 99
*/
for(let [key, value] of Object.entries(nameObj)){
console.log(
`${key.padEnd(16,"-")} | ${value.padStart(5," ")}`
)
}
/*
firstName------- | li
lastname-------- | lei
age------------- | 17
score----------- | 99
*/
Async / Await
Generator 语法糖, 彻底解决异步编程中回调函数嵌套过深的问题.详细看 javascript 异步编程.
(ps: 笔记来源拉钩大前端高薪训练营)
笔记日期:2021.04.01