前言
一直没有把ES6标准系统地进行学习,虽然很多新特性已经用上了,但也该系统地学习一下
还是阮一峰讲得全面些:ECMAScript 6 入门 | 阮一峰 很多新特性,多用就记住了
ES6简介
ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言
ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准
let和const
let 声明变量,const声明常量(内存地址所保存的数据不得改动)
1、块级作用域
1 | var a = 2; |
2、同一个块内不允许覆盖、重复声明
1 | var a = 1; |
3、没有变量提升
1 | console.log(a); // undefined |
4、暂时性死区
只要块级作用域内存在let命令,它所声明的变量就绑定这个区域,不再受外部的影响
只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
1 | console.log(typeof a); // undefined |
Tip:
1、循环中的 var 与 let:
var 会提升进行声明,数组中所有函数访问的都是同一个 i
1 | var a = []; |
变量 i 是 let 声明的,当前的 i 只在本轮循环有效,所以每一次循环的 i 都是一个新的变量,但在循环中,JavaScript 引擎会记住每一次 let 的值,下一次创建的时候,直接在这个值上递进
1 | var a = []; |
2、const 声明的对象仍然是可写的,可以使用 Object.freeze()
冻结对象
当对象嵌套时,需要递归冻结
1 | var constantize = (obj) => { |
解构赋值
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值
数组解构
1、只要等号两边的模式相同,左边的变量就会被赋予对应的值
1 | let [a, b, c, ...d] = [1, 2, 3, 4, 5, 6, 7, 8]; |
2、如果解构不成功,变量的值就等于undefined
1 | let [a] = []; |
3、解构赋值允许指定默认值,只有当一个数组成员严格等于undefined,默认值才会生效
1 | let [b, c = 2] = [1]; |
如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined
1 | let [x = 1] = [undefined]; |
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值
1 | function f() { |
对象解构
1、解构提取对象中的属性
1 | const obj = { |
变量名与属性名不一致,需写成下面这样
1 | const obj = { |
2、嵌套结构的对象
1 | const obj = { |
3、剩余运算符,将其它属性展开到一个对象中存储
1 | const obj = { |
4、可以取到继承的属性
1 | const obj1 = {}; |
函数参数解构
函数的参数也可以使用解构赋值
1 | function add([x, y]){ |
技巧
1、交换变量的值
1 | let x = 1; |
2、函数返回多个值
1 | // 返回一个数组 |
函数的扩展
1、带参数默认值的函数
1 | function f(a, b = 1) { } |
2、rest 参数
获取函数的多余参数,不需要使用 arguments 对象,rest 参数之后不能再有其他参数
1 | function f(a, ...b) { |
3、name 属性
返回函数名
1 | function foo() {} |
箭头函数
使用“箭头”(=>)定义函数
1 | var f = v => v; |
多种写法
1 | var f = v => v; |
简化回调函数
1 | [1,2,3].map(function (x) { |
箭头函数的特性:
1、没有自己的 this
2、不可以当作构造函数
3、不可以使用 arguments 对象
4、不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
对象的扩展
1、属性简洁表示
1 | let [a, b] = [1, 2]; |
2、属性名表达式
属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串
属性名表达式与简洁表示法,不能同时使用
1 | obj['a' + 'bc'] = 123; // obj.abc = 123 |
Object方法
1、is()
判断两个值是否相同,解决了 NaN 问题
1 | Object.is(null,undefined); // false |
2、assign()
合并多个对象,并返回第一个参数的引用,冲突属性后面覆盖前面
1 | let obj1 = { a: 1 } |
3、keys()
返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键名
4、values()
返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值
5、entries()
返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值对数组
6、fromEntries()
entries() 的逆操作,用于将一个键值对数组转为对象
7、hasOwn()
判断某个属性是否为自身的属性
数组的扩展
1、扩展运算符 ...
扩展运算符将一个数组转为用逗号分隔的参数序列
1 | console.log(...[1, 2, 3]) // 1 2 3 |
替代用到 apply() 和 push() 的某些操作
1 | Math.max.apply(null, [11, 33, 22]) // 33 |
Array方法
1、Array.from()
将两类对象转为真正的数组:类似数组的对象和可遍历的对象(包括 ES6 新增的 Set 和 Map)
1 | let arrayLike = { |
接收第二个参数,回调函数,对每个元素进行处理
1 | let arrayLike = { |
2、Array.of()
将一组任意类型的值,转换为数组
1 | Array.of(1, 2) // [1, 2] |
实例方法
1、copyWithin(target, start = 0, end = this.length)
将指定位置的成员复制到其他位置(会覆盖),然后返回当前数组
target(必需):从该位置开始替换数据。如果为负值,表示倒数。
start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
1 | [1,2,3,4,5].copyWithin(0,3) // [4, 5, 3, 4, 5] |
2、find()
找出第一个符合条件的数组成员。传入一个回调函数,没有符合条件的则返回 undefined
1 | [1, -2, -5, 10].find(n => n < 0) // -2 |
findIndex()
返回第一个符合条件的数组成员的位置,没有符合条件的则返回 -1findLast() findLastIndex()
从数组的最后一个成员开始,依次向前检查
可以接受第二个参数,用来绑定回调函数的this对象
3、fill()
使用给定值,覆盖填充一个数组
可以接受第二、三个参数,用于指定填充的起始位置和结束位置
1 | ['a', 'b', 'c'].fill(7) // [7, 7, 7] |
若填充对象,数组中填充的是该对象的引用
4、entries() keys() values()
用于遍历数组,都返回一个遍历器对象
keys() 是键名的遍历,values() 是键值的遍历,entries() 是键值对的遍历
1 | for (let index of ['a', 'b'].keys()) { |
5、includes()
判断数组中是否包含某个值,可选第二个参数表示搜索的起始位置,默认为0
1 | [1, 2, 3].includes(2) // true |
6、flat()
将多维数组解构为一维,传入参数表示总共解构多少层,默认 1
该方法返回一个新数组,对原数据没有影响
如果不管有多少维,都要转成一维数组,可以用 Infinity 作为参数。
如果原数组有空位,flat()方法会跳过空位
1 | [1, 2, [3, 4]].flat() // [1, 2, 3, 4] |
flatMap()
传入一个遍历函数,先执行 map()
再 flat()
只能展开一层数组。
可选第二个参数,用来绑定遍历函数里面的this
1 | // 相当于 [[2, 4], [3, 6], [4, 8]].flat() |
7、at()
返回指定索引上的值,允许负索引
该方法不仅可用于数组,也可用于字符串和类型数组
1 | let arr = [1, 2, 3] |
8、toReversed() toSorted() toSpliced() with()
含义和用法完全一样,但不改变原数组,而返回一个原数组的拷贝:
toReversed() 对应 reverse(),用来颠倒数组成员的位置。
toSorted() 对应 sort(),用来对数组成员排序。
toSpliced() 对应 splice(),用来在指定位置,删除指定数量的成员,并插入新成员。
with(index, value) 对应 splice(index, 1, value),用来将指定位置的成员替换为新的值。
9、group((item, index, array) => {})
数组成员分组
根据分组函数的运行结果,将数组成员分组,分组函数的返回值应该是字符串(或者可以自动转为字符串),以作为分组后的组名
还未实装的提案
1 | let arr = [1,2,3,4,5]; |
运算符的扩展
1、指数运算符**
多个指数运算符连用时,是从最右边开始计算的
1 | 2 ** 2 // 4 |
2、链判断运算符?.
如果读取对象内部的某个属性,往往需要判断一下,属性的上层对象是否存在
1 | // 错误的写法 |
使用 ?.
运算符简化写法
在链式调用的时候判断左侧的对象是否为 null 或 undefined。如果是,则不再往下运算,直接返回 undefined
1 | const firstName = message?.body?.user?.firstName || 'default'; |
判断对象方法是否存在,如果存在就立即执行:
1 | obj.fun?.() |
3、NULL 判断运算符??
使用 ||
运算符指定默认值时,只要属性的值为 null 或 undefined,默认值就会生效,但是属性的值如果为空字符串或 false 或 0,默认值也会生效
Null 判断运算符 ??
类似 ||
,但是只有运算符左侧的值为 null 或 undefined 时,才会返回右侧的值
与链判断运算符 ?.
配合使用,为 null 或 undefined 的值设置默认值。
1 | const a = obj?.a ?? 10; |
Symbol类型
Symbol 是新的基本数据类型,表示独一无二的值
1 | let s = Symbol(); |
Symbol 值通过 Symbol()
函数生成,允许接受一个字符串作为参数,表示对 Symbol 实例的描述。这主要是为了在控制台显示,或者转为字符串时,比较容易区分。
如果 Symbol 的参数是一个对象,就会调用该对象的 toString()
方法,将其转为字符串,然后再生成一个 Symbol 值
1 | let s = Symbol('sss'); |
Symbol 值也可以转为布尔值(true),但是不能转为数值
1 | let s = Symbol(); |
作为属性名
现在,对象的属性名可以是字符串或 Symbol 值
由于每一个 Symbol 值都是不相等的,这意味着只要 Symbol 值用于对象的属性名,就能保证不会出现同名的属性
1 | let s = Symbol('123'); |
Symbol 值作为对象属性名时,用点运算符无法获取该属性
1 | let s = Symbol('s'); |
Symbol 值作为属性名,遍历对象的时候,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回,所以通常可以当做私有属性来使用
Object.getOwnPropertySymbols()
方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值Reflect.ownKeys()
方法可以返回所有类型的键名,包括常规键名和 Symbol 键名
1 | const obj = { |
Symbol方法
1、Symbol.for()
将生成的 Symbol 值登记在全局环境中供搜索,若有相同描述的 Symbol 值则复用返回
Symbol() 写法没有登记机制
其登记机制可以用在不同的 iframe 或 service worker 中取到同一个值
1 | let s1 = Symbol('s'); |
2、Symbol.keyFor()
返回一个已登记的 Symbol 类型值的参数 key
1 | let s1 = Symbol('s'); |
Map对象
Map对象保存键值对,元素会保持其插入时的顺序。
Map的键可以是任意数据类型,包括函数、对象或任意基本类型。
在需要进行很多新增操作,且需要储存许多数据的时候,使用 Map 会更高效
1 | var m = new Map(); |
Object与Map增删改查基本操作。
1 | var o = {}; |
Map的键值对个数可以通过size属性获取
1 | var m = new Map(); |
Map的方法
基本方法:
get()
:获取元素set()
:设置元素has()
:检查是否有指定keyclear()
:清空mapdelete()
:删除指定元素
遍历方法:
keys()
:提取键并返回键的迭代器MapIterator对象values()
:提取值并返回值的迭代器MapIterator对象entries()
:提取键值对并返回取键值对的迭代器MapIterator对象forEach()
:传入回调函数(value, key)=>{}
1 | var m = new Map([ |
Set对象
Set是唯一值的集合,与map类似,map存放的是键值对,而set只存放唯一值。
创建set对象:
1 | var s = new Set(); |
set对象与数组也很像,可以互相转换
1 | var arr1 = [1, 2, 3]; |
可利用set值唯一的特性做数组去重、并集、交集、差集操作
1 | //1、去重 |
Set值个数可以通过size属性获取
1 | var s = new Set([1,2,3]); |
Set的方法
add()
:添加新元素delete()
:删除指定元素clear()
:清空所有元素has()
:判断是否存在某值forEach()
:遍历每个元素,传入回调函数keys()
:返回一个 Iterator 对象,这个对象以插入Set 对象的顺序包含了原 Set 对象里的每个元素values()
:同keys()entries()
:返回 [value, value] 形式的数组迭代器对象,value 是给定集合中的每个元素,迭代器对象元素的顺序即集合对象中元素插入的顺序
add()、delete()、clear()、has() :
1 | var s = new Set([1,2,3]); |
keys()、values() :返回一个 Iterator 对象,这个对象以插入Set 对象的顺序包含了原 Set 对象里的每个元素
1 | var s = new Set([1,2,3]); |
entries() :返回 [value, value] 形式的数组迭代器对象,value 是给定集合中的每个元素,迭代器对象元素的顺序即集合对象中元素插入的顺序
1 | var s = new Set([1,2,3]); |
forEach() :遍历每个元素,传入回调函数,参数:回调函数、thisArg执行回调函数时可以当作this来使用。
回调函数参数:值(key)、值(value)、set对象
1 | var s = new Set([1,2,3]); |
迭代/遍历器Iterator
ES6之后,表示“集合”的数据结构,在原先 Array 和 Object 基础上,又增加了 Map 和 Set
遍历器 Iterator 用于规范统一地对集合进行遍历操作,是一个能访问数据的接口
Iterator 的遍历过程:
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
每一次调用next方法,都会返回数据结构的当前成员的信息(一个包含 value、done 属性的对象)
value:当前成员的值
done:布尔值,表示遍历是否结束
总之,遍历器使得不同的数据结构都可以被 for-of 遍历,只要实现了 Symbol.iterator
Symbol.iterator
Symbol.iterator 是一个预定义好的、类型为 Symbol 的特殊值,是 Iterator 接口
一个数据结构只要具有 Symbol.iterator 属性,就是可遍历的,也就是可以用 for-of 进行遍历
Symbol.iterator 属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器
1 | const obj = { |
原生具备 Iterator 接口的数据结构:Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象
1 | let arr = ['a', 'b', 'c']; |
生成器Generator
生成器是一种函数,function关键字与函数名之间有一个星号 *,函数体内部使用yield表达式
调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是一个遍历器对象
yield 关键字会暂停函数的执行,调用遍历器对象的 next 方法,能让函数执行到下一个 yield 暂停处(或是return)
总之 Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
1 | function* fun() { |
next() 会返回一个包含 value、done 属性的对象,其中 value 值是 yield 后面表达式的值
next() 可以传入参数,yield 是将其返回后,函数再继续执行到下一个 yield
1 | function* fun() { |
使用 Generator 函数能够很方便地实现 Symbol.iterator,其目的也正是如此
1 | function* objectEntries(obj){ |
Generator的应用
1、异步代码同步化
1 | function* main(){ |
2、正确的执行顺序
1 | function* main(){ |
for-in 和 for-of
for-in(ES5) 和 for-of(ES6) 都是遍历数据的语法。
for-in 用于遍历对象的可枚举的属性(名称),会顺着原型链查找可枚举属性。
1 | let obj = { |
而 for-of 用于遍历可迭代对象,即实现了 Symbol.iterator 接口的对象,会顺着原型链查找 Symbol.iterator 属性。
Array,Map,Set,String 等都实现了该接口,可以直接遍历其值。
1 | let arr = ['a', 'b', 'c'] |
Promise
之前已经记过笔记了:Promise异步编程
Class类
在 ES5 中生成实例对象的传统方法是通过构造函数
1 | function Person(name, age) { |
ES6新增了class语法糖,类本质上是函数,类本身指向其构造函数 constructor()
,所以类的构造函数也就是类本身
1 | class A { } |
构造函数和普通函数容易混淆,现在,可以通过 Class 关键字来声明一个类了,其内置的 constructor()
在实例化对象时会立即被调用
用法还是大差不差,并且和之前一样,类方法都在原型上
1 | class Person { |
由于类中定义的方法都在其构造函数的原型上,所以可以用 Object.assign() 向类添加多个方法
1 | class Person { |
constructor函数
constructor()
方法是类的默认方法,在创建实例对象时,自动调用
默认返回实例对象(即this),也可以指定返回(return)另外一个对象
1 | constructor() { |
如果一个实例属性不需要接受外部的传参赋值,可以不在 constructor 方法中定义
1 | class Person { |
Class表达式
可以写成表达式的形式
1 | const MyClass = class { |
可以写出立即执行的 Class
1 | let person = new class { |
Static
在类成员前,加上static关键字,就表示静态属性或方法
静态即不会被实例继承,而是直接通过类来调用
静态方法可以与非静态方法重名
1 | class Class{ |
父类的静态成员,可以被子类继承
1 | class Foo { |
私有方法和属性
私有,即只能在类的内部访问的方法和属性,外部不能访问,有利于代码的封装
早期并没有途径实现真正的私有,只能通过特殊的命名规则加以区别
1 | class Class { |
或者借助 Symbol 来让成员不易被获取,但仍然可以被 Reflect.ownKeys() 等方法获取到
1 | let s1 = Symbol('name'); |
ES2022 正式为 class 添加了私有成员,方法是在属性或方法名之前使用 #
表示
1 | class Class { |
不管在类的内部或外部,如果读取一个不存在的私有成员,会报错
私有成员前面,也可以加上 static 关键字,表示这是一个静态的私有成员
类的继承
Class 通过 extends
关键字实现继承,写法比 ES5 的原型链继承,要清晰和方便很多
1 | class Animal { |
继承的原型链
类的继承是原型链继承,子类的原型的proto指向父类的原型,详见:原型与原型链
同时为了静态方法能够继承,子类的构造函数的proto指向父类的构造函数
1 | class A { |
两条原型链:
1 | B.__proto__ === A |
super
super 关键字,既可以当作函数使用,也可以当作对象使用
super 作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次 super() 函数
调用super()的作用是形成子类的this对象,任何对子类 this 的操作都要放在 super() 的后面
super() 相当于 A.prototype.constructor.call(this)(在子类的this上运行父类的构造函数)
1 | class A {} |
super 作为对象时,在普通方法中,指向父类的原型对象,在静态方法中,指向父类
定义在父类实例上的方法或属性,无法通过 super 调用
1 | class A { |
Module模块化
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量,而 CommonJS 和 AMD 模块,都是运行时的
1 | // CommonJS模块 |
ES6 模块主要有两个命令构成:export
和 import
export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量
1 | const name = '张三'; |
默认暴露 export default
为模块指定默认输出,import命令可以为该匿名函数指定任意名字
1 | export default function(){ |
整体加载:模块暴露的内容都会作为该对象的成员
1 | import * as obj from './modules/index.js'; |
在浏览器环境中,需要给 script 标签加上 type='module'
属性
1 | <script type='module'> |
动态import()
使用 import()
函数动态引入模块
1 | if(true){ |