ES6常用知识
ES6常用知识
你别睡这么晚ECMAScript 6
(简称 ES6
) 是 JavaScript
语言的下一代标准
ECMAScript
的提案流程
Stage 0 - Strawman
(展示阶段)Stage 1 - Proposal
(征求意见阶段)Stage 2 - Draft
(草案阶段)Stage 3 - Candidate
(候选人阶段)Stage 4 - Finished
(定案阶段)
一个提案只要能进入 Stage 2
就差不多肯定会包括在以后的正式标准里面
ES6 和 ES2015 的区别
ES2015
是一个年份标记,表示当年发布的 ECMAScript
标准的正式版本,其全称为《ECMAScript 2015
标准》(简称 ES2015
)ES6
是一个历史名词也是一个泛指,含义是 5.1
版以后的 JavaScript
的下一代标准,涵盖了 ES2015、ES2016、ES2017 ES2018
等等
let const
ES6
新增了 let
和 const
命令,用于声明变量,其声明的变量只在声明所在的块级作用域内有效
let const var 的区别
var
声明的变量会提升到作用域的顶部,let const
不存在变量提升var
声明的全局变量会被挂载到全局对象window
上,而let const
不会var
可以对一个变量进行重复声明,而let const
不能重复声明var
声明的变量作用域范围是函数作用域,let const
声明的变量作用域范围是块级作用域const
声明的是一个只读的常量,一旦声明常量的值就不能改变(必须对变量进行初始化)- 基本类型保证值不可变
- 引用类型保证内存指针不可变
变量提升
变量提升是指在代码执行过程中 var
变量和函数声明会在它们被实际执行之前被”提升”到它们所在作用域的顶部
1 | console.log(a) // 输出 undefined |
函数声明优先于变量声明,函数声明会覆盖变量声明
变量提升
1 | /* var 变量声明 */ |
在同一作用域内存在同名的函数声明和 var
变量声明时函数声明会覆盖变量声明
1 | console.log(a) // function a() { console.log('fn') } |
挂载到全局对象
1 | var a = 'var' |
重复声明
1 | var a = 'var' |
同一作用域下let和const不能声明同名变量,而var可以
作用域范围
1 | function fn() { |
let和const只能在块作用域里访问
const 常量定义
1 | const NAME = 'strawberry' |
const定义常量,而且不能修改,但是在定义的对象时对象属性值可以改变
模板字符串
模板字符串 (template string) 是增强版的字符串,用反引号(`)标识。它可以当作普通字符串、定义多行字符串或者在字符串中嵌入变量、函数调用以及表达式
1 | let name = 'strawberry' |
解构赋值
解构对象
1 | const obj = { |
解构数组
1 | const arr = ['strawberry', 18] |
解构字符串
字符串也可以解构赋值,因为字符串被转换成了一个类似数组的对象
1 | const [a, b, c] = 'strawberry' |
解构赋值注意点
- 解构数组和字符串时变量的取值由它的位置决定
- 解构对象时变量必须与属性同名,才能取到正确的值
- 变量在没有找到对应的值,多余的变量会被赋值为
undefined
- 在指定默认值时,只有属性值严格等于
undefined
才会生效 - 数组本质是特殊的对象,因此可以对数组进行对象属性的解构
- 解构数值和布尔值时会通过其对应的包装函数将其转换成对象再解构
undefined
和null
无法转为对象,在解构时会报错
1 | const { toString } = 123 |
函数的扩展
参数默认值
1 | /* ES5 */ |
函数参数的默认值
- 参数变量是默认声明的不能用
let
或const
再次声明,否则会报错 - 使用参数默认值时函数不能有同名参数
- 参数默认值的位置应该是函数的尾参数
剩余参数(rest 参数)
ES6
引入 rest
参数(形式为 ...变量名
) 用于获取函数的剩余参数(可以替换 arguments
对象)
1 | function log(name, ...params) { |
剩余参数(rest 参数)
rest
参数是一个真正的数组,数组特有的方法都可以使用rest
参数之后不能再有其他参数,否则会报错- 函数的
length
属性,不包括rest
参数
箭头函数
ES6
允许使用箭头(=>
)定义函数
1 | // 不需要参数时使用一个圆括号代表参数部分 |
箭头函数与普通函数的区别
this
- 普通函数
this
指向是动态的(取决于函数的调用方式)- 可以用
call apply bind
改变this
指向
- 箭头函数
this
指向是固定的,指向定义时上层作用域中的this
(它没有自己的this
)call apply bind
无法改变箭头函数的this
指向(上下文值始终按词法解析)- 全局作用域下
this
指向全局对象
- 普通函数
- 箭头函数不可以当作构造函数(不能使用
new
运算符,否则会报错) - 箭头函数的函数体内不可以使用
arguments super new.target
- 箭头函数不可以使用 yield 命令(不能用作
Generator
函数) - 在
class
中使用箭头函数其this
会和类实例进行绑定
注意点(以下场合不应该使用箭头函数)
- 定义对象方法且该方法内部包括
this
时 - 定义原型方法且该方法内部包括
this
时 - 需要动态
this
时
[利用 babel
编译箭头函数代码查看 this
的指向](https://www.babeljs.cn/repl#?browsers=defaults%2C not ie 11%2C not ie_mob 11&build=&builtIns=false&corejs=3.6&spec=false&loose=false&code_lz=MYewdgzgLgBANiA5jAvDAFASlQPhgbwCgYZRIQ4BTAOgUXQHI6YoALASwgC4GAaFjhEyEAvoUIAzAK5hgUduBgSwWAsRgRKUACrsAtpRBSo6VSjxESJMhAo06jZQM49-bTsJIj-ARgAMfsIiQA&debug=false&forceAllTransforms=true&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=script&lineWrap=true&presets=env%2Creact&prettier=false&targets=&version=7.17.11&externalPlugins=&assumptions={})
1 | /* ES6 */ |
扩展运算符
扩展运算符 (spread
)是三个点 (...
) 它好比 rest
参数的逆运算
- 数组的扩展运算符会将数组转为用逗号分隔的参数序列
- 对象的扩展运算符会取出参数对象的所有可遍历属性,拷贝到当前对象之中
函数调用
扩展运算符在函数调用时可以将一个数组变为参数序列,从而可以替代函数的 apply()
方法
1 | // 举个 🌰 求出一个数组最大元素 |
拷贝数组/对象
1 | /* 拷贝数组 */ |
合并数组/对象
1 | /* 合并数组 */ |
使用表达式
展开对象
1 | const obj = { |
展开数组
1 | const arr = [...(false ? [1] : []), 2] |
与解构赋值结合
1 | const arr1 = [1, 2, 3] |
扩展运算符
- 使用扩展运算符拷贝数组或对象时其都是浅拷贝
- 对象的扩展运算符等同于使用
Object.assign()
方法 - 只有函数调用时扩展运算符才可以放在圆括号中,否则会报错
- 扩展运算符用于赋值时只能放在参数的最后一位,否则会报错
- 在扩展运算符展开数组时,其参数必须要有
iterator
接口,否则会报错 - 在扩展运算符展开对象时,其参数如果不是对象时会自动转为对象(调用
Object()
)
1 | // 等同于 {...Object(1)} |
数组的扩展
Array.from()
Array.from()
用于将两类对象转为真正的数组
- 类似数组的对象 (
array-like object
)DOM
操作返回的NodeList
arguments
对象
- 可遍历 (
iterable
) 的对象 (包括ES6
新增的数据结构Set
和Map
)
1 | /* array-like object 转数组 */ |
Array.from()
Array.from()
可以接受一个函数作为第二个参数,作用类似于数组的map()
用来对每个元素进行处理,将处理后的值放入返回的数组
在字符串转为数组时 Array.from()
能正确处理各种 Unicode
字符,可以避免 JavaScript
将大于 \uFFFF
的 Unicode
字符算作两个字符的 bug
1 | '𠮷'.length // 2 |
Array.of()
Array.of()
用于将一组值转换为数组
1 | Array.of(3, 11, 8) // [3,11,8] |
Array.of()
Array.of()
方法的主要目的是弥补数组构造函数 Array()
的不足(因为参数个数的不同会导致Array()
的行为有差异)
1 | Array() // [] |
Array.of()
总是返回参数值组成的数组。如果没有参数就返回一个空数组
1 | /* Array.of() 的模拟实现 */ |
实例方法: includes()
includes()
方法返回一个布尔值,表示某个数组是否包含给定的值(ES2016
引入)
1 | const arr = [1, 2, NaN] |
includes() 和 indexOf() 的对比
indexOf()
不够语义化,其含义是找到参数值的第一个出现位置,所以要去比较是否不等于 -1
,表达起来不够直观indexOf()
内部使用严格相等运算符 (===
) 进行判断,这会导致对 NaN
的误判
实例方法: find() 和 findIndex()
find()
方法用于找出第一个符合条件的数组成员,如果没有符合条件的成员则返回 undefined
findIndex()
方法用于找出第一个符合条件的数组成员的位置,如果没有符合条件的成员则返回 -1
1 | const arr = [1, 5, 10, 15] |
实例方法: at()
at()
方法接受一个整数(支持负数)作为参数返回对应位置的成员,如果参数位置超出了数组范围则返回 undefined
1 | const arr = ['strawberry', 18] |
实例方法: flat() 和 flatMap()
flat()
方法用于将嵌套的数组拍平变成一维的数组,该方法返回一个新数组不改变原数组
flatMap()
方法会先对原数组的每个成员执行一个函数(相当于执行 Array.prototype.map()
) 然后对返回值组成的数组执行 flat()
方法,该方法返回一个新数组不改变原数组
1 | /* flat() */ |
flat() 和 flatMap() 注意点
flat()
方法默认只会拉平一层flat()
方法会跳过原数组中的空位flatMap()
只能展开一层数组
对象的扩展
属性简写
ES6
允许在大括号里面直接写入变量和函数作为对象的属性和方法
1 | /* 属性简写 */ |
属性简写
简写的对象方法不能用作构造函数否则会报错
1 | const obj = { |
属性名表达式
1 | // 定义属性名 |
属性名表达式
属性名表达式与属性简写不能同时使用否则会报错
1 | // 报错 |
属性名表达式如果是一个对象会自动将其转为字符串 [object Object]
1 | const keyA = { a: 1 } |
Object.is()
Object.is()
方法用来比较两个值是否严格相等,严格比较运算符 (===
) 的行为基本一致
1 | Object.is('key', 'key') // true |
Object.is() 与 === 的不同之处:+0 不等于 -0
1 | /* +0 不等于 -0 */ |
Object.assign()
Object.assign()
方法用于对象的合并,将源对象的所有可枚举属性复制到目标对象(第一个参数是目标对象后面的参数都是源对象)
1 | const target = { a: 1, b: 1 } |
只有一个参数时会直接返回该参数
1 | const obj = { a: 1 } |
传入参数不是对象时会先转成对象再返回
1 | typeof Object.assign(1) // "object" |
传入非对象类型的场景
1 | /* undefined 和 null */ |
传入数组时会把数组当对象处理
1 | Object.assign([1, 2, 3], [4, 5]) // [4, 5, 3] |
Object.assign() 总结和应用场景
总结
Object.assign()
是浅拷贝方法- 存在同名属性时,后面的属性会覆盖前面的属性
- 只有一个参数时会直接返回该参数
- 传入参数不是对象时会先转成对象再返回
- 传入
undefined
和null
时 - 如果为第一个参数会报错(无法转成对象)
- 如果不为第一个参数时会被忽略
- 传入数组时会把数组当对象处理
应用场景
1 | /* 为对象添加属性 */ |
Object.keys() Object.value() Object.entries()
Object.keys()
方法返回一个数组,其成员为参数对象自身的(不含继承的)所有可遍历属性的键名(ES5
引入)
Object.value()
方法返回一个数组,其成员为参数对象自身的(不含继承的)所有可遍历属性的键值(ES2017
引入)
Object.entries()
方法返回一个数组(二维数组),其成员为参数对象自身的(不含继承的)所有可遍历属性的键值对数组(ES2017
引入)
1 | const obj = { name: 'strawberry', age: 18 } |
Object.fromEntries()
Object.fromEntries()
方法是 Object.entries()
的逆操作,用于将键值对的数据结构还原为对象
1 | Object.fromEntries([['name', 'strawberry']]) // {name: 'strawberry'} |
对象遍历方法对比
方法名 | 说明 | 继承的原型属性 | 不可枚举属性 | Symbol 属性 | 返回值 |
---|---|---|---|---|---|
for…in | 遍历对象自身和继承的所有可枚举属性(不含 Symbol 属性) | ✅ | ❌ | ❌ | key |
Object.keys | 遍历对象自身所有可枚举属性(不含 Symbol 属性) | ❌ | ❌ | ❌ | [key…] |
Object.getOwnPropertyNames | 遍历对象自身所有属性(不含 Symbol 属性) | ❌ | ✅ | ❌ | [key…] |
Object.getOwnPropertySymbols | 遍历对象自身所有的 Symbol 属性 | ❌ | ✅ | ✅ | [key…] |
Reflect.ownKeys | 遍历对象自身所有的属性(包含不可枚举和 Symbol 属性) | ❌ | ✅ | ✅ | [key…] |
Object.values | 遍历对象自身所有可枚举属性(不含 Symbol 属性) | ❌ | ❌ | ❌ | [value…] |
Object.entries | 遍历对象自身所有可枚举属性(不含 Symbol 属性) | ❌ | ❌ | ❌ | [[key,value]…] |
遍历顺序
ES5
没有规定遍历顺序,其遍历顺序由浏览器厂商定义(可以简单理解为无序的)
ES6
之后规定遍历顺序将按如下规则进行
- 首先遍历所有数值键,按照数值升序排列。
- 其次遍历所有字符串键,按照加入时间升序排列。
- 最后遍历所有
Symbol
键,按照加入时间升序排列。
ES6
内部定义了 [[OwnPropertyKeys]]() 方法对属性进行分类和排序
运算符的扩展
?. 可选链操作符
ES2020 引入了可选链操作符(又名链判断运算符),其允许我们在读取对象内部的某个属性时,不需要判断属性的上层对象是否存在
1 | // 可选链操作符之前的写法 |
可选链操作符 ?.
的三种写法
1 | /* 属性是否存在 */ |
注意点
- 可选链操作符相当于一种短路机制,只要不满足条件就不再往下执行
- 当有括号时,可选链操作符对圆括号外部没有影响,只对圆括号内部有影响。
- 右侧不得为十进制数值。为了保证兼容以前的代码,允许
foo?.3:0
会被解析成foo ? .3 : 0
,因此规定如果?.
后面紧跟一个十进制数字,那么?.
不再被看成是一个完整的运算符,而会按照三元运算符进行处理,即小数点会归属于后面的十进制数字形成一个小数。 - 禁止使用以下写法
1 | // 构造函数 |
?? 空值合并运算符
ES2020 引入了空值合并运算符,只有运算符左侧的值为 null
或 undefined
时才会返回右侧的值
1 | /* ?? 运算符 */ |
?? 和 || 的区别
??
运算符只有左侧是null
或undefined
才会返回右侧的值||
运算符只要左侧是 假值 就会返回右侧的值
逻辑赋值运算符
ES2021
引了入三个新的逻辑赋值运算符,用于将逻辑运算符与赋值运算符进行结合
1 | /* 或赋值运算符 */ |
ESModule
ESModule
是 ES6
在语言标准的层面上实现的模块功能,其设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系以及输入和输出的变量
ESModule
是编译时加载,使静态分析成为可能- 模块内部自动使用严格模式
- 模块中的顶层
this
指向undefined
export 命令
export
命令用于规定模块的对外接口
- 一个模块就是一个独立的文件,该文件内部的所有变量外部无法获取,如果希望外部可以获取必须使用
export
关键字输出该变量 - 通常情况下
export
输出的变量就是本来的名字,但可以使用as
关键字重命名 export
命令规定的是对外的接口其必须与模块内部的变量建立一一对应关系export
命令输出的接口与其对应的值是动态绑定关系(可以取到模块内部实时的值)export
命令可以出现在模块的任何位置,只要处于模块顶层就可以(在块级作用域内使用会报错)
1 | // util.js |
import 命令
import
命令用于输入其他模块提供的功能(变量、函数、class 等)
import
命令输入的变量都是只读的(类似于常量,即基本类型不可重新赋值,引用类型可修改属性)import
命令具有提升效果,会提升到整个模块的头部首先执行import
命令是编译阶段执行的- 不能使用表达式和变量
重复执行同一句
import
命令只会执行一次import
后面的from
指定模块文件的位置,其可以是相对路径,也可以是绝对路径
1 | // index.js |
export default 命令
export default
命令可以为模块指定默认输出,是对 default
赋值的特例,本质上是一种赋值
1 | /* 输出匿名函数 */ |
export default 命令注意点
- 一个模块只能有一个默认输出(
export default
命令只能使用一次) export default
命令本质上是输出一个叫做default
的变量或方法,使用时可以为它取任意名字export default
命令后面是一个表达式,不能跟变量声明语句
1 | /* 错误 */ |
export 和 export default 的区别
export {}
导出的都是引用export default
导出的都是值而不是引用export default
是对default
赋值的特例,本质上是一种赋值(即export default
后的语句会被视为表达式)所以拿到的是值而不是引用export default function
是特例,导出的是引用
export { thing as default }
写法为引用导出导入时除
{} = await import()
外均为引用
如何保证导入都是引用?
- 保证导入总是引用
- 尽量使用命名导入(使用除
{} = await import()
外的写法) - 注意命名导出的写法
- 少用默认导出
- 尽量使用命名导入(使用除
- 做不到上面的要求时尽量把需要维持引用的变量使用
Object
封装,不要使用简单变量
相关文章
export 与 import 的复合写法
如果在一个模块之中,需要先输入后输出同一个模块,import
命令可以与 export
命令写在一起
1 | export { foo, bar } from 'my_module' |
注意点
在 export
与 import
的复合写法时,输入的接口不能在当前模块中使用,只是相当于对外转发了接口
import()
ES2020 引入 import()
函数支持动态加载模块
import()
函数可以用在任何地方,不仅仅是模块非模块的脚本也可以使用import()
函数是运行时执行import()
函数与所加载的模块没有静态连接关系import()
函数类似于Node.js
中的require()
函数,区别主要是前者是异步加载后者是同步加载import()
函数的返回值是Promise
对象
1 | import('./dialogBox.js') |
import() 函数的使用场景
- 按需加载
- 条件加载
- 动态的模块路径
浏览器对 ESModule 的加载规则
浏览器加载 ESModule
同样使用 <script>
标签但是需要设置 type="module
“ 属性
浏览器对于带有 type="module"
的 <script>
都是异步加载,不会堵塞浏览器,即等到整个页面渲染完再执行模块脚本,等同于设置了 <script>
标签的 defer
属性
有多个 <script type="module">
时会按照在页面出现的顺序依次执行
1 | <script type="module" src="./util.js"></script> |
当 <script>
同时设置了 type="module"
和 async
属性时,只要加载完成渲染引擎就会中断渲染立即执行,等执行完成后再恢复渲染,即不会按照在页面出现的顺序执行,而是只要该模块加载完成就执行该模块
在 script 中使用 ESModule
ESModule
内嵌在网页中使用时语法行为与加载外部脚本完全一致,只需注意以下几点
- 代码是在模块作用域之中运行而不是在全局作用域运行,模块内部的顶层变量外部不可见
- 自动采用严格模式不管有没有声明
use strict
- 可以使用
import
命令加载其他模块(.js
后缀不可省略,需要提供绝对URL
或相对URL
) 也可以使用export
命令输出对外接口 - 顶层的
this
关键字返回undefined
而不是指向window
- 同一个模块如果加载多次将只执行一次
1 | <script type="module"> |
小技巧
利用顶层的 this
等于 undefined
这个语法点可以判断当前代码是否在 ES6 模块之中
1 | const isNotModuleScript = this !== undefined |
Promise
Promise
是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大
Promise
对象具有以下 3
种状态
pending
等待(初始)fulfilled
成功rejected
拒绝
Promise 的特点
Promise
对象的状态不受外界影响- 状态一旦改变就不会再变(不可逆),任何时候都可以得到这个结果
- 无法取消
Promise
,一旦新建就会立即执行无法中途取消 - 当处于
pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
1 | /* 基本用法 */ |
Promise 实例方法
Promise.prototype.then()
用于实例添加状态改变时的回调函数(第一个参数是fulfilled
状态的回调函数,第二个参数是rejected
状态的回调函数),会返回的是一个新的Promise
实例Promise.prototype.catch()
用于指定rejected
状态的回调函数(是.then(null, rejection)
或.then(undefined, rejection)
的别名)Promise.prototype.finally()
(ES2018) 用于指定不管Promise
对象最后状态如何都会执行的操作 (finally
本质上是then
方法的特例)
1 | /* 实现 finally 方法 */ |
Promise 静态方法
Promise.resolve()
将传入的参数转为
Promise
对象参数是一个
Promise
实例则直接返回参数是一个
thenable
对象(具有then
方法的对象) 转为Promise
对象再立即执行thenable
对象的then
方法参数不是具有
then
方法的对象或根本就不是对象时返回一个fulfilled
状态的新Promise
对象- 没有参数时返回一个
fulfilled
状态的新Promise
对象
- 没有参数时返回一个
Promise.reject()
- 返回一个
rejected
状态的新Promise
对象
- 返回一个
Promise.all()
- 将多个
Promise
实例,包装成一个新的Promise
实例,只有所有的Promise
状态成功才会成功,如果其中一个Promise
的状态失败就会失败
- 将多个
Promise.race()
- 将多个
Promise
实例,包装成一个新的Promise
实例,新的Promise
实例状态会根据最先更改状态的参数实例而更改状态(可以轻松实现超时方法)
- 将多个
Promise.allSettled()
(ES2020)- 将多个
Promise
实例,包装成一个新的Promise
实例,新的Promise
实例只有等到所有这些参数实例都返回结果,不管是fulfilled
还是rejected
,包装实例才会结束,一旦结束,状态总是fulfilled
- 将多个
Promise.any()
(ES2021)- 将多个
Promise
实例,包装成一个新的Promise
实例,只要参数实例有一个变成fulfilled
状态,包装实例就会变成fulfilled
状态;如果所有参数实例都变成rejected
状态,包装实例才会变成rejected
状态
- 将多个
Promise 实现 简易实现、A+ 规范实现、原型方法、静态方法实现