1.声明变量的问题
使用 var 声明变量
- 允许重复的变量声明:导致数据被覆盖
- 变量提升:怪异的数据访问闭包问题
- 全局变量挂载到全局对象:全局对象成员污染问题
使用 let 声明变量
ES6 不仅引入 let 关键字用于解决变量声明的问题,同时引入了块级作用域的概念
块级作用域:代码执行时遇到花括号,会创建一个块级作用域,花括号结束,销毁块级作用域
let 解决了 var 声明变量的问题:
- let 声明的变量不会挂载到全局对象
- let 声明的变量,不允许当前作用域范围内重复声明
- 使用 let 不会有变量提升,因此,不能在定义 let 变量之前使用它。
底层实现上,let 声明的变量实际上也会有提升,但是,提升后会将其放入到“暂时性死区”,如果访问的变量位于暂时性死区,则会报错:" Cannot access ‘a’ before initialization"。
当代码运行到该变量的声明语句时,会将其从暂时性死区中移除。
在循环中,用 let 声明的循环变量,会特殊处理,每次进入循环,都会开启一个新的作用域,并且
将循环变量绑定到该作用域(每次循环,使用的是一个全新的循环变量)
在循环中使用 let 声明的循环变量,在循环结束后会销毁
使用 const 声明常量
const 和 let 完全相同,仅在于用 const 声明的变量,必须在声明时赋值,而且不可以重新赋值。
实际上在开发中,应该尽量使用 const 来声明变量,以保证变量的值不会随意篡改。
原因如下:
- 根据经验,开发中的很多变量都是不会更改,也不应该更改的。
- 后续的很多框架或者是第三方 js 库,都要求不可变数据,使用常量可以一定程度上保证这一点。
注意的细节:
-
常量不可变,是指声明的常量的内存空间不可变,并不保证内存空间中的地址指向的其他空间不可变。
-
常量的命名
- 特殊的常量:该常量从字面意义上,一定是不可变的,比如圆周率、月底距离或其他一些绝不可能变化的配置。通常,该常量的名称全部使用大写,多个单词之间用下划线分割
- 普通的常量:使用和之前一样的命名即可
-
在 for 循环中,循环变量不能使用 const
2.更好的 Unicode 支持
早期,由于存储空间宝贵,Unicode 使用 16 位二进制来存储文字。我们将一个 16 位的二进制编码叫做一个码元(Code Unit)。
后来,由于技术的发展,Unicode 对文字编码进行了扩展,将某些文字扩展到了 32 位(占用两个码元),并且将某个文字对应的二进制数字叫做码点(Code Point)。
ES6 为了解决这个困扰,为字符串提供了方法:codePointAt,根据字符串码元的位置得到其码点。
同时,ES6 为正则表达式添加了一个 flag:u,如果添加了该配置,则匹配时,使用码点匹配。
3.更多的字符串 API
以下均为字符串的实例(原型)方法
-
includes
判断字符串中是否包含指定的子字符串 -
startsWith
判断字符串中是否以指定的字符串开始 -
endsWith
判断字符串中是否以指定的字符串结尾 -
repeat
将字符串重复指定的次数,然后返回一个新字符串
模板字符串
ES6 之前处理字符串繁琐的两个方面:
- 多行字符串
- 字符串拼接
在 ES6 中,提供了模板字符串的书写,可以非常方便的换行和拼接,要做的,仅仅是将字符串的开始或结尾改为 ` 符号
如果要在字符串中拼接 js 表达式,只需要在模板字符串中使用 ${表达式}
4.函数
4-1. 参数默认值
使用
在书写形参时,直接给形参赋值,附的值即为默认值
这样一来,当调用函数时,如果没有给对应的参数赋值(给它的值是 undefined),则会自动使用默认值
[扩展]对 arguments 的影响
只要给函数加上参数默认值,该函数会自动变成严格模式下的规则:arguments 和形参脱离
加上"use strict"
arguments 与形参脱离
function test(a, b = 1) {
console.log("arguments", arguments[0], arguments[1]);
console.log("a:", a, "b:", b);
a = 3;
console.log("arguments", arguments[0], arguments[1]);
console.log("a:", a, "b:", b);
}
test(1, 2);
[扩展]留意暂时性死区
形参和 ES6 中的 let 或 const 声明一样,具有作用域,并且根据参数的声明顺序,存在暂时性死区。
function test(a = b, b) {
console.log(a, b);
}
test(undefined, 2);
函数将会报错!
4-2. 剩余参数
arguments 的缺陷:
- 如果和形参配合使用,容易导致混乱
- 从语义上,使用 arguments 获取参数,由于形参缺失,无法从函数定义上理解函数的真实意图
ES6 的剩余参数专门用于收集末尾的所有参数,将其放置到一个形参数组中。
语法:
function (...形参名) {
}
细节:
- 一个函数仅能出现一个剩余参数
- 一个函数如果有剩余参数,剩余参数必须是最后一个参数
4-3. 箭头函数
回顾:this 指向
- 通过对象调用函数,this 指向对象
- 直接调用函数,this 指向全局对象
- 如果通过 new 调用函数、this 指向新创建的对象
- 如果通过 apply,call,bind 调用函数,this 指向指定的数据
- 如果是 DOM 时间函数,this 指向事件源
使用语法
箭头函数是一个箭头表达式,理论上任何使用函数表达式的场景都可以使用箭头函数。
完整语法:
(参数1,参数2,...) => {
//函数体
}
如果参数只有 1 个,可以省略小括号
(参数) => {};
注意细节
- 箭头函数的函数体中的 this,取决于箭头函数定义的位置的 this 指向,而与如何调用无关。
- 箭头函数中,不存在 this、arguments、new.target,如果使用了,则使用的是外层的对应的 this、arguments、new.target
- 箭头函数没有原型
- 箭头函数不能作为构造函数使用
应用场景
- 临时性使用的函数,并不会刻意调用它,比如:
- 事件处理函数
- 异步处理函数
- 其他临时性函数
- 为了绑定外层 this 的函数
- 在不影响其他代码的情况下,保持代码的简洁(最常见的就是数组方法中的回调函数)
5-1. 新增的对象字面量语法
- 成员速写
如果对象字面量初始化时,成员的名称来自于一个变量,并且和变量的名称相同,则可以进行简写
- 方法速写
在对象字面量初始化时,方法可以省略冒号和 function 关键字
- 计算属性名
有时候,初始化对象时,某些属性可能来自于某个表达式的值,在 ES6,可以使用中括号来表示属性名是通过计算得到的。
Object 的新增 API
- Object.is
用于判断两个数据是否相等,基本上根严格相等(===)是一致的,除了以下两点:
1)NaN 和 NaN 相等
2)+0 和-0 不相等
- Object.assign
用于混合对象
- Object.getOwnPropertyNames 的枚举顺序
Object.getOwnPropertyNames 之前就存在,只不过,官方没有明确要求,对属性的顺序如何排序,
如何排序完全由浏览器厂商决定
ES6 规定了该方法返回的数组的排序方式如下:
- 先排数字,并按照升序排序
- 再排其它,按书写顺序排序
- Object.setPrototypeOf
该函数用于设置某个对象的隐式原型
比如:Object.setPrototypeOf(obj1,obj2)
相当于:obj1.proto=obj2
类:构造函数的语法糖
传统的构造函数的问题
- 属性和原型方法定义分离,降低了可读性
- 原型成员可以被枚举
- 默认情况下,构造函数仍然可以被当作普通函数使用
类的特点
- 类声明不会被提升,与 let 和 const 一样,存在暂时性死区
- 类中的所有代码均在严格模式下执行
- 类的所有方法都是不可枚举的
- 类的所有方法都无法被当作构造函数使用
- 类的构造器必须使用 new 来调用
5-5. 类的其它书写方法
-
可计算的成员名
-
getter 和 setter
Object.defineProperty 可定义某个对象成员属性的读取和设置
使用 getter 和 setter 控制的属性,不在原型上
-
静态成员
构造函数本身的成员 -
字段初始化器(ES7)
1).使用 static 的字段初始化器,添加的是静态成员
2).没有使用 static 的字段初始化器,添加的成员位于对象上
3).箭头函数在字段初始化器位置上,指向当前对象 -
类表达式
-
[扩展]装饰器(ES7)
5-6. 类的继承
如果两个类 A 和 B,如果可以描述为:B 是 A,则 A 和 B 形成继承关系
如果 B 是 A,则:
- B 继承自 A
- A 派生 B
- B 是 A 的子类
- A 是 B 的子类
如果 A 是 B 的父类,则 B 会自动拥有 A 中的所有实例成员。
新的关键字:
- extends:继承,用于类的定义
- super:
- 直接当作函数调用,表示父类构造函数
- 如果当作对象使用,则表示父类的原型
注意:ES6 要求,如果定义了 constructor,并且该类是子类,则必须在 constructor 的第一行手动调用父类的构造函数。
如果子类不写 constructor,则会有默认的构造器,该构造器需要的参数和父类一致,并且自动调用父类构造器。
【冷知识】
- 用 JS 制作抽象类
- 抽象类:一般是父类,不能通过该类创建对象
- this 指向:正常情况下,this 始终指向具体类的对象
6-1. 对象解构
什么是解构
使用 es6 的一种语法规则,将一个对象或数组的某个属性提取到某个变量中。
解构不会影响原始对象
在解构中使用默认值
{
同名变量 = 默认值;
}
非同名属性解构
{
属性名: 变量名;
}
符号
7-1. 普通符号
符号是 ES6 新增的一个数据类型,它通过使用函数Symbol(符号描述)
来创建
符号设计的初衷,是为了给对象设置私有属性
私有属性:只能在对象内部中使用,外部不能使用
符号具有以下特点:
- 没有字面量
- 使用 typeof 得到的类型是 symbol
- 每次调用 Symbol 函数得到的符号永远不相等,无论符号名是否相同
- 符号可以作为对象的属性名存在,这种属性称之为符号属性
- 开发者可以通过精心设计,让这些属性无法通过常规方式被外界访问
- 符号属性是不能枚举的,因此在 for-in 循环中无法读取到符号属性,Object.keys 方法也无法读取到符号属性
- Object.getOwnPropertyNames 尽管可以得到所有方法无法枚举的属性,但是仍然无法读取到符号属性
- ES6 新增 Object.getOwnPropertySymbols 方法,可以读取符号
- 符号无法转换为字符串,通过 String 构造函数进行转换即可,console.log 之所以可以输出符号,是它在内部进行了显式转换。
7-2. 共享符号
根据某个符号名称(符号描述)能够得到同一个符号
Symbol.for("符号名/符号描述"); //获取共享符号
7-3. 知名(公共、具名)符号
知名符号是一些具有特殊含义的共享符号,通过 Symbol 的静态属性得到
ES6 延续了 ES5 的思想:减少魔法,暴露内部实现!
因此,ES6 用知名符号暴露了某些场景的内部实现
- Symbol.hasInstance
该符号用于定义构造函数的静态成员,它将影响 instanceof 的判定
obj instanceof A;
//等效于
A[Symbol.hasInstance(obj)]; //Function.prototype[Symbol.hasInstance]
- 【扩展】 Symbol.isConcatSpreadable
该知名符号会影响数组的 concat 方法
- 【扩展】Symbol.toPrimitive
该知名符号会影响类型转换的结果
- 【扩展】Symbol.toStringTag
该知名符号会影响 Object.prototype.toString 的返回值
- 其他知名符号
Promise
//创建一个任务对象,该任务立即进入pending状态
const pro = new Promise((resolve, reject) => {
//任务的具体执行流程,该函数会立即被执行
//调用 resolve(data),可将任务变为fulfilled状态,data为需要传递的相关数据
//调用reject(reason),可将任务变为rejected 状态,reason为需要传递的失败原因
});
pro.then(
(data) => {
//onFulfilled 函数,当任务完成后,会自动运行该函数,data为任务完成的相关数据
},
(reason) => {
//onRejected函数,当任务失败后,会自动运行该函数,reason为任务失败的相关原因
}
);
catch 方法
.catch(onRejected) = .then(null,onRejected)
只传失败的处理
链式调用
- then 方法必定会返回一个新的 promise,可以理解为 : 后续处理也是一个任务
- 新任务的状态取决于后续处理:
- 若没有相关的后续处理,新任务的状态和前任务一致,数据为前任务的数据
- 若有后续处理但还未执行,新任务挂起
- 若后续处理执行了,则根据后续处理的情况确定新任务的状态
- 后续处理执行无错,新任务的状态为完成,数据为后续处理的返回值
- 后续处理执行有错,新任务的状态为失败,数据为异常对象
- 后续执行后返回的是一个任务对象,新任务的状态和数据与该任务对象一致
Promise 的静态方法
方法名 | 含义 |
---|---|
Promise.resolve(data) | 直接返回一个完成状态的任务 |
Promise.reject(reason) | 直接返回拒绝状态的任务 |
Promise.all(任务数组) | 返回一个任务 任务数组全部成功则成功 任何一个失败则失败 |
Promise.any(任务数组) | 返回一个任务 任务数组任一成功则成功 任务全部失败则失败 |
Promise.allSettled(任务数组) | 返回一个任务 任务数组全部已决则成功 该任务不会失败 |
Promise.race(任务数组) | 返回一个任务 任务数组任一已决则已决,状态和其一致 |
消除回调
有了 Promise,异步任务就有了一种统一的处理方式
有了统一的处理方式,ES 官方就可以对其进一步优化
ES7 提出了两个关键字async
和await
,用于更加优雅的表的 Promise
async
async 关键字用于修饰函数,被它修饰的函数,一定返回 Promise
async function method1() {
return 1; //该函数的返回值是Promise完成后的数据
}
method1(); Promise {1}
async function mothod2() {
return Promise.resolve(1); //若返回的是Promise,则method得到的Promise状态和其一致
}
method2(); Promise {1}
async function method3() {
return new Error(1); 若执行过程报错,则任务是rejected
}
method3(); //Promise {<rejected> Error(1)}
await
await
关键字表示等待某个 Promise 完成,它必须用于async
函数中
async function method() {
const n = await Promise.resolve(1);
console.log(n); // 1
}
//上面的函数等同于
function method() {
return new Promise((resolve, reject) => {
Promise.resolve(1).then((n) => {
console.log(n);
resolve(1);
});
});
}
await
也可以等待其他数据
async function method() {
const n = await 1; // 等同于 await Promise.resolve();
}
如果需要针对失败的任务进行处理,可以使用try-catch
语法
async function method() {
try {
const n = await Promise(123); // 这句代码将抛出异常
console.log("成功", n);
} catch (err) {
console.log("失败", err);
}
}
method(); // 输出:失败 123
9. Fetch Api 概述
XMLHttpRequest 的问题
- 所有的功能全部几乎在同一个对象上,容易书写出混乱不易维护的代码
- 采用传统的事件驱动模式,无法适配的 Promise Api
Fetch Api 的特点
- 并非取代 AJAX,而是对 AJAX 传统 API 的改进
- 精细的功能分割:头部信息、请求信息、响应信息等均分布到不同的对象,更利于处理各种复杂的 AJAX 场景
- 使用 Promise Api,更利于异步代码的书写
- Fetch Api 并非 ES6 的内容,属于 HTML5 新增的 Web Api
- 需要掌握网络通信的知识。
基本使用
使用fetch
函数即可立即向服务器发送网络请求
参数
该函数有两个参数:
- 必填:字符串、请求地址
- 选填:对象、请求配置
请求配置对象
- method:字符串、请求方法、默认值 GET
- headers:对象、请求头信息
- body:请求体的内容,必须匹配请求头中的 Content-Type
- mode:字符串,请求模式
- cors:默认值,配置为该值,会在请求头中加入 origin 和 referer
- no-cors:配置为该值,不会在请求头中加入 origin 和 referer,跨域的时候可能会出现问题
- same-origin:指示请求必须在同一个域中发生,如果请求其他域,则会报错
- credentials:如何携带凭据(cookie)
- omit:默认值,不携带 cookie
- same-origin:请求同源地址时携带 cookie
- include:请求任何地址都携带 cookie
- cache:配置缓存模式
- default:表示 fetch 请求之前将检查下 http 的缓存。
- no-store:表示 fetch 请求将完全忽略 http 缓存的存在。这意味着请求之前将不再检查下 http 的缓存,拿到响应后,它也不会更新 http 缓存。
- reload:表示 fetch 请求之前将忽略 http 缓存的存在,但是请求拿到响应后,它将主动更新 http 缓存。
- force-cache:表示 fetch 请求不顾一切的依赖缓存,即使缓存过期了,它依然从缓存中读取。除非没有任何缓存,那么它将发送一个正常的 request。
- only-if-cached:表示 fetch 请求不顾一切的依赖缓存,即使缓存过期了,它依然从缓冲中读取。如果没有缓存,它将抛出网络错误(该设置只在 mode 为"same-origin"时有效)
返回值
fetch 函数返回一个 Promise 对象
- 当收到服务器的返回结果后,Promise 进入 resolved 状态,状态数据为 Response 对象
- 当网络发生错误(或其他导致无法完成交互的错误)时,Promise 进入 rejected 状态,状态数据为错误信息。
Response 对象
- ok:boolean,当响应消息吗在 200~299 之间时为 true,其他为 false
- statu:number,响应的状态码
- text():用于处理二进制文件格式(比如图片或者电子表格)的 Ajax 响应。它读取文件的原始数据,一旦读取完整个文件,就返回一个被解决为 blob 对象的 Promise。
- json():用于处理 JSON 格式的 Ajax 的响应。它将 JSON 数据流转换为一个被解决为 JavaScript 对象的 promise。
- redirect():可以用于重定向到另一个 URL。它会创建一个新的 Promise,以解决来自重定向的 URL 的响应。
迭代器
背景知识
- 什么是迭代?
从一个数据集合中按照一定的顺序,不断取出数据的过程
- 迭代和遍历的区别?
迭代强调的是依次取数据,并不保证取多少,也不保证所有的数据取完
遍历强调的是要把整个数据依次全部取出
- 迭代器
对迭代过程的封装,在不同的语言中有不同的表现形式,通常为对象
- 迭代模式
一种设计模式,用于统一迭代过程,并规范了迭代器规格:
- 迭代器应该具有得到下一个数据的能力
- 迭代器应该具有判断是否还有后续数据的能力
JS 中的迭代器
JS 规定,如果一个对象具有 next 方法,并且该方法返回一个对象,该对象的格式如下:
{value:值,done:是否迭代完成}
则认为该对象是迭代器
含义:
- next 方法:用于得到下一个数据
- 返回的对象
- value:下一个数据的值
- done:boolean,是否迭代完成
可迭代协议 与 for-of 循环
可迭代协议
- 迭代器(iterator):一个具有 next 方法的对象,next 方法返回下一个数据并且能指示是否迭代完成
- 迭代器创建函数(iterator creator): 一个返回迭代器的函数
可迭代协议
ES6 规定,如果一个对象具有知名符号属性Symbol.iterator
,并且属性值是一个迭代器创建函数,则该对象是可迭代的(iterator)
思考:如何知晓一个对象是否是可迭代的?
思考:如何遍历一个可迭代对象?
for-of 循环
for-of 循环用于遍历可迭代对象,格式如下:
//迭代完成后循环结束
for (const item in iterator) {
//iterator:可迭代对象
//item:每次迭代得到的数据
}
展开运算符与可迭代对象
展开运算符可以用作可迭代对象,这样,就可以轻松的将可迭代对象转换为数组
生成器(Generator)
-
什么是生成器:
生成器是一个通过构造函数 Generator 创建的对象
生成器既是一个迭代器,同时又是一个可迭代对象 -
如何创建生成器?
生成器的创建,必须使用生成器函数(Generator Function) -
如何创建生成器函数呢?
//这是一个生成器函数,该函数一定返回一个生成器
function* method() {}
-
生成器函数内部是如何执行的?
生成器函数内部是为了给生成器的每次迭代提供数据的
每次调用生成器的 next 方法,将导致生成器函数运行到下一个 yield 关键字位置
yield 是一个关键字,该关键字只能在生成器函数内部使用,表达"产生"一个迭代数据。 -
需要注意的细节:
- 生成器函数可以有返回值,返回值出现在第一次 done 为 true 时的 value 属性中
- 调用生成器的 next 方法时,可以传递参数,传递的参数会交给 yield 表达式的返回值
- 第一次调用 next 方法时,传参没有任何意义
- 在生成器函数内部可以调用其他生成器函数,但是要注意,要加上*
-
生成器的其他 API
- return 方法:调用该方法,可以提前结束生成器函数,从而提前让整个迭代过程结束
- throw 方法:调用该方法,可以在生成器中产生一个错误
Reflect
1.Reflect 是什么?
Reflect 是一个内置的 JS 对象,它提供了一些列方法,可以让开发者通过调用这些方法,访问一些 JS 底层功能
由于它类似于其他语言的反射,因此取名为Reflect
- 它可以做什么?
使用 Reflect 可以实现诸如 属性的赋值与取值、调用普通函数、调用构造函数、判断属性是否存在于对象中 等等功能
- 这些功能不是已经存在了吗?为什么还需要用 Reflect 实现一次?
有一个重要的理念,在 ES5 就被提出:减少魔法、让代码更加纯粹
这种理念很大程度上是受到函数式编程的影响
ES6 进一步贯彻了这种理念,它认为,对属性内存的控制、原型链的修改、函数的调用等等
新增或数组 API
静态方法
- Array.of(…args):使用指定的数组项创建一个新数组
- Array.from(arg):通过给定的类数组 或 可迭代对象 创建一个新的数组
实例方法
- find(callback):用于查找满足条件的第一个元素
- findIndex(callback):用于查找满足条件的第一个元素的下标
- fill(data):用指定的数据充满数组所有的内容
- copyWithin(target,start?,end?):在数组内部完成复制
- includes(data):判断数组中是否包含某个值,使用 Object.is 匹配