本文主要是学习ES6既ES2015一些新特征之后一些总结以及个人理解,介绍一些在日常开发中使用,来进一步提高我们在开发中的代码质量。
ECMAScript概述
ECMAScript是一门脚本语言,一般缩写为ES,通常看做JavaScript的标准化规范,实际上JavaScript是ECMAScript的扩展语言,因为在ECMAScript中提供了最基本的语法,通俗来说就是约定了代码该如何编写,怎么定义变量和函数,或者是怎么实现分支循环的语句等等,ECMAScript只是停留在语言层面,并不能直接应用于实际开发中,而JavaScript实现ECMAScript的标准,并且在基础上做了扩展。
- 在浏览器环境中操作DOM和BOM
- node中读写文件等操作
ECMAScript中目前版本记录如下
其中ES6是最新ECMAScript标准的代表版本,泛指所有的新标准,相比于ES5.1的变化比较大,而且自此标准命名规则发生了变化,由以往的版本命名改为使用发布年份来命名,接下来重点来介绍在ES5.1基础上的变化,大概归纳为4类:
- 解决原有语法上的一些问题或者不足,如let、const、块级作用域
- 对原有语法进行增强 ,使之变得简洁易用,如解构、默认值、rest、模板字符串
- 全新的对象、全新的方法、全新的功能,如promise、Object.assign
- 全新的数据类型和结构,如symbol、set、map
Let与块级作用域
作用域:某个成员能够起作用的范围,包括全局作用域和函数作用域,以及ES6中新增的块级作用域。
具体的介绍查看ES6 入门教程-阮一峰,下面通过简单的示例看下let vs var使用时候的作用域有何不同:
1 | // 使用var |
对比下var,let,const三种声明变量的方式之间的具体差别:
1. 使用var声明变量
- 存在变量提升的情况
- 访问在后续定义的变量会返回 undefined
- var声明的变量,可以在其他作用域访问到
- var可以重复声明变量
2. 使用let声明变量
- 使用let命令,会创建一个块级作用域
- let声明的变量只在块级作用域内有效
- 同个作用域里,使用let不能重复声明变量
- let定义的变量必须要先声明然后在使用,不存在变量的提升
3. 使用const声明常量
- 使用const声明的常量不能被修改
- 使用const声明常量时就要赋值
- 使用const声明一个引用类型数据的常量,常量内部的属性可修改,但是指向的内存地址不能更改的
在日常开发中的最佳实践:不用var,主用const,配合let
数组的解构与对象的解构
数组的解构
1.获取最后一个值,需要保留逗号,确保结构位置的格式与数组一致,这样就可以提取指定位置的成员
1
2
3
4
5
6
7
8const arr =[100,200,300]
const [foo,bar,baz]=arr
const [ , ,box]=arr
console.log(foo,bar,baz); // 100,200,300
// 开发中常用获取路径中的某一项
const path ='foo/bar/baz'
const[,rootdir]=path.split('/')
console.log(rootdir);2.提取当前位置往后的所有成员返回一个数组,…只能在最后一个成员中使用
1
2
3const arr =[100,200,300]
const [foo,...rest]=arr
console.log(rest); // [200,300]3.成员个数小于被解构数组的长度,按照从前到后顺序提取,成员个数大于被解构数组的长度,提取到的是undefined,相当于访问数组中不存在的下标一样
1
2
3const arr =[100,200,300]
const [foo,bar,baz,more]=arr
console.log(more); // undefined4.设置默认值,当成员个数在数组中没有数据时候,可以设置默认值
1
2
3const arr =[100,200,300]
const [foo,bar,baz,more='default value']=arr
console.log(more); // default value对象的解构
与数组解构类似,不同的是对象的解构是通过属性名来匹配提取,而不是和数组一样通过位置(数组有固定的顺序),
解构的使用主要是简化代码,减少代码体积1
2
3
4
5
6const { log }=console
const obj ={ name: 'tom', age: 18 }
const name='jack'
// 名称冲突时候通过重命名来解决
const { name:objName='ame',sex }=obj
log(objName); //tom
模板字符串和标签函数
模板字符串就不做介绍了,标签函数在定义时和普通函数没什么区别。区别在调用上,标签函数以模板字符串作为参数输入,并且有独特的规则完成形实参匹配。接下来看一个简单的例子:
1 | // 带标签的模板字符串 |
通过这个代码可以看出,标签函数的作用就是对模板字符串进行加工,在日常中可以使用标签函数可以实现文本的多元化,或者是检查模板字符串中是否存在不安全字符,或者使用这种特性来实现小型的模板引擎
剩余参数(rest)和数组扩展运算符(spread)
ES6引入rest
参数(形式为…变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
1 | // function foo() { |
上面代码的两种写法,比较后可以发现,rest
参数的写法更自然也更简洁。需要注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
数组扩展运算符(spread)也是三个点(…),它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列,这里就不在多少了。
箭头函数与this
ES6 允许使用“箭头”(=>)定义函数即为箭头函数,具体内容这里不做介绍,主要来看些箭头函数中的this
1 | const person={ |
箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
this的总结:
- 沿着作用域上找最近的一个function(不是箭头函数),看这个function最终是怎样执行的
- 普通函数this的执行取决于所属function的调用方式,而不是定义
- function调用一般分为一下几种情况:
- 作为函数调用,即foo() 指向全局对象(globalThis),注意严格模式问题,严格模式下是undefined
- 作为方法调用 即foo.bar() 指向最终调用这个方法的对象
- 作为构造函数调用 即 new Foo{} 指向一个新对象 Foo{}
- 特殊调用 即foo.ball() / foo.apply() / foo.bind() 参数指定成员
- 找到所属的function,就是全局对象
内置对象Proxy
Vue3.0以前通过Object.defineProperty()
监视对象属性读写,捕获到对象的读写过程方法实现数据响应,从而完成数据的双向绑定,但是Object.defineProperty
是一个相对比较昂贵的操作,因为它直接操作对象的属性,颗粒度比较小,V3.0中将它替换为ES6的Proxy
,在目标对象之上架了一层拦截(可以抽象为门卫),代理的是对象而不是对象的属性,这样可以将原本对对象属性的操作变为对整个对象的操作,颗粒度变大,下面我们来具体了解下Proxy的使用:
1 | // Proxy实现属性的读写操作 |
Proxy对比Object.defineProperty()
Proxy
可以监视读写以外的操作,如delete操作,对象中方法调用等,defineProperty只能监视对象属性的读写
1 | const person = { |
2.Proxy
更好的支持数组对象的监视,例如重写数组方法的操作,通过自定义方法去覆盖原型对象上的方法,以此来劫持对应方法的调用过程
1 | let list = [] |
Proxy
不需要侵入对象
Proxy是以非侵入的方式监管了对象的读写,就是说一个已经定义好的对象,不需要对对象本身去做任何的操作,就可以监视到他内部成员的读写,而Object.defineProperty要求必需通过特定的方式单独去定义对象中需要被监视的属性,对一个已存在的对象如果要去监视它的属性,我们需要需要做很多额外的操作
1 | // Object.defineProperty实现对象的读写 |
内置对象Refelect
Refelect是JavaScript的一个新内置对象(非函数类型对象),与Math对象上挂载了很多用于数学处理方面的方法一样,Refelect对象身上挂在了一套用于操作对象的方法,Refelect成员方法就是Proxy处理对象的默认实现。需要注意的是Refelect属于一个静态类,不能通过new的方式构建实例对象,只能去调用静态类中的方法,如Refelect.get()
1 | const obj ={ |
下表总结列举了Refelect对象上的13个操作对象的静态方法的作用,以及在Reflect出现之前的实现方案:
作用 | 不用Reflect实现 | 用Reflect闪现 |
---|---|---|
属性写入 | target.propertyKey = value | Reflect.set(target, propertyKey, value[, receiver]) |
属性读取 | target.propertyKey | Reflect.get(target, propertyKey[, receiver]) |
属性删除 | delete target.propertyKey | Reflect.deleteProperty(target, propertyKey) |
属性包含 | propertyKey in target | Reflect.has(target, propertyKey) |
属性遍历 | Object.keys(target) | Reflect.ownKeys(target) |
属性描述定义属性 | Object.defineProperty(target, propertyKey, attributes) | Reflect.defineProperty(target, propertyKey, attributes) |
属性描述读取 | Object.getOwnPropertyDescriptor(target, propertyKey) | Reflect.getOwnPropertyDescriptor(target, propertyKey) |
原型读取 | target.prototype / Object.getPrototypeOf(target) | Reflect.getPrototypeOf(target) |
原型写入 | target.prototype = prototype / Object.setPrototypeOf(target, prototype) | Reflect.setPrototypeOf(target, prototype) |
获取对象可扩展标记 | Object.isExtensible(target) | Reflect.isExtensible(target) |
设置对象不可扩展 | Object.preventExtensions(target) | Reflect.preventExtensions(target) |
函数对象调用 | target(…argumentsList) / target.apply(this, argumentsList) | Reflect.apply(target, thisArgument, argumentsList) |
构造函数对象调用 | new target(…args) | Reflect.construct(target, argumentsList[, newTarget]) |
由上面刚刚总结出的表格内容可以得知,Reflect在对象层面以及属性层面的Api都有相应的实现,并且比单独的Object原型更加全面。那么我们在日常开发中如何选择呢,出于代码的运行性能、可读性以及统一操作思想考虑,个人是这么选择的,,日常简洁的属性读写、函数对象调用操作不用Reflect,其它都统一使用Reflect对象操作(也就是不用操作符delete、in以及重叠的Object原型上的方法)。
面向对象:类class
面向对象的特征为:封装,继承,多态,下面我们来具体看下:
封装
封装是面向对象的重要原则,它在代码中的体现主要是以下两点:
- 封装整体:把对象的属性和行为封装为一个整体,其中内部成员可以分为静态成员(也叫类成员)和实例成员,成员之间又可细分为属性和方法。
- 访问控制:外部对对象内部属性和行为的访问权限,简单来分时就是私有和公有两种权限。
以下是基本封装示例:
1 | class Person{ |
继承
继承是面向对象最显著的一个特性,继承在ES6之前使用原型方式实现,ES6中使用extends关键字,继承在代码中的体现主要是以下两点:
- 子类对象具有父类对象的属性和行为
- 子类对象可以有它自己的属性和行为
1 | // 父类 |
多态
多态指允许不同的对象对同一消息做出不同响应,在Java中,实现多态有以下三个条件:
- 继承
- 重写
- 父类引用指向子类对象
由于JavaScript是弱类型语言,所以JavaScript实现多态,不存在父类引用指向子类对象的问题。
通过示例具体看下
1 | class Person{ |
数据结构Set
- 数组去重、字符串去重等任何可迭代类型的去重
1 | // 数组去重 |
- 集合间操作:交集、并集、差集
下面截取阮一峰ES6对Set的说明案例:
1 | let a = new Set([1, 2, 3]); |
数据结构Map
Map对于JavaScript而言也是一种新的数据结构,用于存储键值对形式的字典 / 双列集合。在Map对象出现之前,我们通常使用Object对象来做键值对的存储,下面对比一下Map对象实现键值对存储与普通对象存储键值对的区别:
- 功能角度:Object对象只能使用字符串或者Symbol类型作为键,而Map对象可以使用任何数据类型作为键。Map对象使用引用类型作为键时,以内存地址是否一致来作为判断两个键是否相同的标准。
1 | const obj ={} |
- 构造与读写角度:Object对象字面量构造并存储键值对的方式比Map方便,其读写操作也比Map需要调用get、set方法而言性能更好(性能分析工具初步对比分析)。
- 常用Api角度:Object对象的原型为Object.protoype,而Map对象的原型为Map.prototype,两者对常用的键值对操作都有相应的api可以调用,不过Map原型上定义的Api更加纯粹一些。
- 序列化角度:Object对象存储键值时支持序列化,而Map对象不支持。
经过上面的对比分析可以得出结论,不到必须使用引用类型作为键的情况下,我们都用Object对象字面量的方式来定义并存储键值对会更好一些。
数据类型Symbol
ES6之前,对象的属性名都是字符串,这容易造成属性名的冲突,ES6提出Symbol是一种新的原始数据类型(一种类似于字符串的数据类型),用来表示独一无二的值,此外也是对象属性名的第二种数据类型(另一个是字符串)
- 消除魔法字符串
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。
如下含有魔法字符串的代码示例:
1 | const obj = {type: 'type2'}; |
上面代码中,字符串type1与type2就是一个魔术字符串,它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。常用的消除魔术字符串的方法,就是把它写成一个变量,接下来使用Symbol对上述代码改造:
1 | const obj = {type: 'type2'}; |
- 实现对象的私有属性名
1 | // Symbol()===Symbol() //false |
Symbol.for()
需要在全局复用相同的一个symbol,通过全局变量实现,或者是symbol提供的静态方法实现,for()内部维护了一个全局注册表,为字符串和symbol值提供了一一对应关系
1 | const s1 =Symbol.for('foo') |
- Symbol.toStringTag
是内置的symbol常量,这种symbol在后面为对象实现迭代器时候会经常使用
1 | const obj = { |
- Object.getOwnPropertySymbols()
可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。此方法的作用类似于Object.key,不同的是Object.key获取的是字符串类型的属性名
1 | const obj = { |
迭代器Iterator和for of
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。而for…of循环是ES6 创造出的一种新的遍历命令,它可以配合迭代器使用,只要实现了Iterator接口的任意对象就可以使用for…of循环遍历。
在JavaScript常见的数据结构如Array、Set、Map、伪数组arguments等等一系列对象的原型上都有Symbol.iterator标识,并且有默认的Iterator实现。普通对象是没有这个接口标识以及iterator的实现的,但是我们可以手动为普通对象添加这个标识以及对应的iterator实现,示例代码如下:
- for of
1 | // 遍历数组 |
- Iterator
迭代器中维护了一个数据指针,我们每调用一次next,这个指针都会往后移一位,done属性的作用表示我们内部的数据是否全部被遍历完,所以被for..of循环遍历的数据类型都必须实现这个iterator的接口,也就是说在内部必须要挂载一个iterator方法,这个方法需要返回带有next()的对象,不断调用next()就会实现对内部所有数据的遍历
对象中没有实现这个iterator的接口,所以无法使用for..of循环遍历对象
这里有个疑问为什么for…of可以作为遍历所有数据结构的统一方式?
因为它内部就是去调用被遍历对象的Iterator方法,得到一个迭代器,从而去遍历内部所有的数据,这也是Iterator接口所约定的内容,换句话说只要对象实现了Iterator接口,那么我们就可以实现使用for…of来遍历对象
实现可迭代的接口
核心思路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const obj = {
// Iterable实现可迭代的接口,约定内部必须要有一个返回迭代器的iterato方法
[Symbol.iterator]:function(){
return {
// Iterator实现了迭代器接口,约定内部必须要有一个用于迭代的next方法
next: function(){
/**
* IterationResult 迭代结果接口
* 约定必须有个value(表示被迭代到的数据,值是任意类型)
* 和done(Boolean,表示迭代有没有结束)
*/
return {
value:'tom',
done:true
}
}
}
}
}实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// test1.js:封装者封装
const todos = {
life: ['吃饭', '睡觉', '打豆豆'],
learn: ['语文', '数学', '外语'],
work: ['喝茶'],
// 添加Symbol.iterator标识接口以及iterator实现
[Symbol.iterator]: function () {
const all = [...this.life, ...this.learn, ...this.work]
let index = 0
return {
next: function () {
return {
value: all[index],
done: index++ >= all.length
}
}
}
}
}
// test2.js:调用者遍历
for (const item of todos) {
console.log(item)
}
迭代器的核心就是对外提供统一变量的接口,让外部不在关系内部的数据结构是怎样的
生成器Generator,promise,Async
生成器函数会自动返回一个生成器对象,调用对象的next方法,才会让函数的函数体开始执行,执行过程中一旦遇到了yield,函数执行会被暂停下来,而且yield后面的值将会作为next的结果返回,如果在继续调用生成器对象的next,函数就会从暂停的位置继续开始执行,周而复始一直到这个函数完全结束,next所返回的done的值会变为true,生成器函数最大的特点是惰性执行
通过Generator函数实现iterator方法
1 | const todos = { |
此处不再介绍,具体查看文章javascript的异步编程
ES2016、ES2017、ES2020
ES2016(ES7)
1. Array.prototype.includes()
用于检查数组是否包含元素,返回Boolean类型,需要注意的是使用 ES6 和更低版本,要检查数组是否包含某个元素项,您必须使用 indexOf ,它检查数组中的索引,如果元素不存在则返回 -1 。
1 | const arr = ['foo',1,NaN,false] |
2. 求幂运算符
求幂运算符 ** 等价于 Math.pow(),但是它被引入语言本身,而不是库函数。
1 | console.log(2 ** 10); //1024 |
ES2017(ES8)
1. values和entries函数Object.values()
返回一个给定对象自身的所有可枚举属性值的数组Object.entries()
返回一个给定对象自身可枚举属性的键值对数组
1 | const obj = { |
2. 字符串填充
padStart() 和 padEnd()这俩函数的作用就是在字符串的头部和尾部增加新的字符串,并且返回一个具有指定长度的新的字符串。你可以使用指定的字符、字符串或者使用函数提供的默认值-空格来填充源字符串。
填充字符串的用例包括
- 以等宽字体显示平整的数据。
- 在文件名或URL中添加计数或ID:’file 001.txt’。
- 对齐控制台输出: ‘Test 001: ?’。
- 打印具有固定位数的十六进制或二进制数字:’0x00FF’。
1 | // padStart('字符长度','填充物') 和 padEnd('字符长度','填充物') |
3. Object.getOwnPropertyDescriptors()
返回指定对象 obj 上自有属性对应的属性描述信息
1 | const p1 = { |
4. ES2017 函数参数列表和调用尾逗号
此处结尾逗号指的是在函数参数列表中最后一个参数之后的逗号以及函数调用时最后一个参数之后的逗号。ES8 允许在函数定义或者函数调用时,最后一个参数之后存在一个结尾逗号而不报 SyntaxError 的错误。
1 | // 函数声明 |
5. 共享内存和Atomics
详见ES2017 新特性:共享内存和Atomics
6.异步函数 Async
async函数就是Generator函数的语法糖,详见async 函数
ES2020
1. 通过#给class添加私有变量
在ES2020中通过#可以给class添加私有变量,在class的外部没办法获取到它的值这样不需要使⽤闭包来隐藏不想暴露给外界的私有变量
1 | class Couter { |
2. 空值合并运算符
来⾃ undefined
或 null
值的另⼀个问题是,如果我们想要的变量为 undefined
或null
则必须给变量设置默认值。例如:
1 | const a = b || 123; |
当使⽤ || 运算符将 b
设置为 a
时,如果 b
被定义为 undefined
,我们必须设置⼀个默认值。运算符 || 的问题在于,所有类似于 0,false 或空字符串之类的值都将被我们不想要的默认值覆盖。为了解决这个问题,创建了“nullish”合并运算符,⽤ ??
表示。有了它,我们仅在第⼀项为 null
或 undefined
时设置默认值。使⽤⽆效的合并运算符,以上表达式将变
为:
1 | const a = b ?? 123; |
3. 可选链运算符
如果要访问对象的深层嵌套属性,则必须通过很⻓的布尔表达式去检查每个嵌套级别中的属性。必须检查每个级别中定义的每个属性,直到所需的深度嵌套的属性为⽌,如下代码所示:
1 | let name = user && user.info && user.info.name; |
如果在任何级别的对象中都有 undefined
或 null
的嵌套对象,如果不进⾏检查,那么的程序将会崩溃。这意味着我们必须检查每个级别,以确保当它遇到 undefined
或 null
对象时不会崩溃。使⽤可选链运算符,只需要使⽤ ?.
来访问嵌套对象。⽽且如果碰到undefined
或 null
属性,那么它只会返回 undefined
。通过可选链,可以把上⾯的代码改为:
1 | let name = user?.info?.name; |
4. BigIntJavascript
中 Number
类型只能安全的表示-(2^53-1)至 2^53-1 范的值,即Number.MIN_SAFE_INTEGER
至Number
.MAX_SAFE_INTEGER
,超出这个范围的整数计算或者表示会丢失精度
1 | var num = Number.MAX_SAFE_INTEGER; // 9007199254740991 |
于是 BigInt
应运而生,它是第7个原始类型,可安全地进行大数整型计算。你可以在BigInt上使用与普通数字相同的运算符,例如 +, -, /, *, %等等。
创建 BigInt 类型的值也非常简单,只需要在数字后面加上 n 即可。例如,123 变为 123n。也可以使用全局方法 BigInt(value) 转化,入参 value 为数字或数字字符串。
1 | const aNumber = 111; |
只要在数字末尾加上 n,就可以正确计算大数了
1 | 1234567890123456789n * 123n; |
需要注意的是:在大多数操作中,不能将 BigInt与Number混合使用。比较Number和 BigInt是可以的,但是不能把它们相加。
1 | 1n < 2 |
BigInt支持情况:
参考
ES6 入门教程-阮一峰
ES6理解进阶
探索ES2016与ES2017](https://www.html.cn/archives/7753)