JS 基础

342 阅读13分钟

感谢b站up主峰华前端工程师的精彩分享!

感谢b站up主康文昌的编程语言的结构JS全景图视频!

感谢b站up主后盾人编程的JS系列课程

感谢饥人谷JS课程

感谢简书作者这波能反杀的前端基础进阶

感谢《JS百炼成仙》这本书的作者杨逸飞

感谢b站up主技术蛋老师的精彩分享

感谢网道文档

JS 基础

1. 前言

第2章浏览器,其本质是应用程序,它不直接和硬件打交道,而是通过操作系统间接操作硬件。浏览器对于前端工程师,就好比赛车对于赛车手的关系,需要深入了解;浏览器有三大模块:用户界面、浏览器引擎和渲染引擎。

第3章引擎,其本质是程序,作用是将JS高级语言转换为低级的机器语言并执行的程序,最终JS代码被CPU执行。

第4章加载网页的步骤、浏览器渲染网页的过程。

2. 浏览器及主要功能模块

浏览器是一种从Web获取和显示页面的应用程序,还可以让用户通过超链接访问更多页面。浏览器通过操作系统间接操作硬件。

浏览器结构模块主要分为:用户界面、浏览器引擎、渲染引擎。

用户界面:除标签也窗口外的其他用户界面内容

浏览器引擎:在用户界面和渲染引擎之间传递数据。其内部还有网络模块(负责网络请求、下载 JS)、JS解析器(解析和执行JS)

渲染引擎:渲染用户请求的页面内容,是浏览器的核心或内核,Chrome使用Blink渲染引擎

数据持久层:帮助浏览器存储各种数据,比如cookie。浏览器存储详见另一篇博客浏览器

3. V8引擎及其执行代码的步骤

JS代码被CPU执行前,需要通过某种程序将JS转换成低级的机器语言并执行,这种程序即为JS引擎。

JS代码会以人类可读的语法来编写,机器是无法理解的,需要通过环境内部的引擎来做中间处理。引擎的本质就是一个软件。

引擎的工作是:获取该代码并将其转换为用机器代码编写的代码,该代码最终可以由计算机处理器运行。

V8引擎是一个接收JS代码,编译代码然后执行的C++程序,编译后的代码可在多种操作系统、多种处理器上运行。

Chrome 和 Node 都用的是V8引擎,由C++编写(性能王者)

image.png

V8引擎在执行代码时有以下几个步骤:

3.1 解析器 Parser

解析器了解JS语法和规则,它的工作是逐行检查代码,并检查代码的语法是否正确。遇到错误,它将停止运行并发出错误。

3.2 抽象语法树 AST

如果代码有效,则解析器会生成称为“抽象语法树”(简称AST)的内容。

AST是一种数据结构,是代码的树形表示。

引擎创建AST而不是直接编译为机器代码的主要原因是,当您将代码包含在树数据结构中时,转换为机器代码更容易。

3.3 解释器 Interpreter

解释器的工作是获取已创建的AST,并将其转换为代码的中间表示(IR)。IR最流行的类型就是字节码。

IR可视为机器代码的抽象,代表源代码的数据结构或代码。它的作用是介于以JS之类的抽象语言编写的代码与机器代码之间的中间步骤。

3.4 编译器 Compiler

编译器的工作是获取解释器创建的IR(在我们的示例中为Bytecode),然后通过某些优化将其转换为机器代码。

4. 浏览器加载网页的步骤(以HTTP为例)

用户通过在地址栏输入一个 URL,都发生了什么?

4.1 DNS解析

如果输入的是关键词,浏览器会启用默认配置的搜索引擎来查询

输入URL,点击回车以后,浏览器进程的UI线程会捕捉输入内容,并启动一个网络线程来请求DNS进行域名解析,去寻找页面资源的位置,即IP地址。通过DNS解析,就可以把URL地址解析为IP地址。得到IP地址,就可以在互联网上找到对应的服务器。

DNS就是一个记录着很多URL和对应IP地址的数据库。

4.2 TCP三次握手

先建立 TCP 连接,(敲门同意再进门)客户端发送SYN数据包,表示请求连接;服务器响应SYN+ACK数据包,表示同意建立连接;客户端再发送ACK数据包,表示成功连接,此时通过TCP三次握手,在发送数据之前建立通道成功。

4.3 发送HTTP请求

浏览器帮助用户发送HTTP请求报文给服务器,报文格式为:

请求行: 请求方法 请求地址 HTTP协议版本
请求头部: 浏览器信息(由键值对组成)
空行
请求数据: 告诉浏览器具体需要什么数据 以何种形式获取数据

4.4 响应HTTP请求

状态行: 状态码
响应头部: 由键值对组成
空行
响应数据:

4.5 浏览器渲染网页

image.png

渲染网页就是解析接收到的HTML、CSS和JS等文件。

标题类比
HTML施工合同
DOM网页的迷你框架结构
CSS装饰工程
HTML网页的框架结构

4.5.1 构建DOM

浏览器发送请求,服务器响应给浏览器HTML文件,UI线程创建一个渲染器进程来渲染页面,浏览器进程通过IPC管道将数据传递给渲染器进程,此时为显示字节内容的;

字节内容的HTML转换为字符内容的HTML(程序员看懂的HTML代码);

字符内容的HTML转换为Token符号标签,即机器看得懂的HTML;

Token符号标签转换为节点对象,最后再连在一起,形成DOM(浏览器自己的语言),每个节点对象相连,形成父子关系,

4.5.2 构建CSSOM

浏览器在构建DOM时,遇到link标签,向服务器发送请求得到CSS文件。构建CSSOM不可以部分解析,因为层叠样式表需要全部都跑完,才是最后的样式。

字节-字符-Token-节点-CSS对象模型(CSSOM)

4.5.3 构建渲染树

DOM和CSSOM的合成即为渲染树。其任务是匹配DOM和CSSOM的节点,并捕获可见内容。

image.png

4.5.4 布局

获取渲染树的结构、节点位置和大小。布局依据盒子模型来进行,即每个元素都用一个盒子来表示,然后这些盒子在页面上进行排列和嵌套

4.5.5 绘制

把渲染树以像素的形式绘制在页面

5. JS语言特征

JS 难度在于广度、知识点多,需要花费时间去理解。最开始的几节最难是在于编程思想。JS 语言的底层是由 C 语言写的。

JS 是解释性语言

基于高级语言翻译为机器语言的过程不同,可分为编译性语言和解释性语言。

标题内容优点不足
编译性语言
C C++
通篇翻译,生成一个翻译完的文件,程序执行这个文件。速度快跨平台受限
解释性语言
JS php
翻译一行并执行
单线程(JS引擎)
稍微慢
编译解释性语言
JAVA
结合两种

JS作为一种嵌入式语言,核心语法只能用来做一些数学和逻辑运算,需要寄生在宿主环境上,才能发挥功能。

特征内容(函数式+面向对象)
轻量级脚本语言script只用来编写控制浏览器(或其他应用程序)的脚本
嵌入式语言embedded靠调用运行环境(浏览器)提供的API来做交互
两个常用版本宿主环境备注
JS客户端浏览器
服务端Node.js服务器
运行在操作系统
开发后端应用程序
借助Chrome浏览器的V8引擎

6. JS核心语法

模块内容备注
基本的语法构造操作符、控制结构、语句
标准库一系列功能的对象Array / Date / Math等
浏览器提供的API用处备注
BOM类操作浏览器
DOM类操作网页的各种元素
Web类实现互联网的各种功能
服务器提供的API用处备注
文件操作APIFs
网络通信APIFetch

JS 语言借鉴了C语言基本语法、Java语言数据内存、Scheme语言函数式编程、Self语言原型继承。

7. 瓜分内存

JS 语言借鉴了Java语言数据内存

浏览器在执行JS代码之前,做了什么:提供运行环境及API,JS放进页面后,在内存里运行。

一个Tab内存划分内容备注
渲染进程
用户界面
JS引擎代码区、存变量区window
数据区:栈区Stack用于存连续数据
每个数据顺序存放
基础数据类型在其中维护
乒乓球存放方式
数据区:堆区Heap用于存链接数据
每个数据随机存放
引用数据类型在其中维护
调用栈
任务队列

7.1 栈数据结构

执行上下文的执行顺序借用了栈数据结构的存取方式。

image.png

上图中,处于盒子中最顶层的乒乓球5,它一定是最后被放进去,但可以最先被使用。而我们想要使用底层的乒乓球1,就必须将上面的4个乒乓球取出来,让乒乓球1处于盒子顶层。这就是栈空间先进后出,后进先出的特点。图中已经详细的表明了栈空间的存储原理。

7.2 堆数据结构

堆数据结构是一种树状结构。它的存取数据的方式,则与书架与书非常相似。

书虽然也整齐的存放在书架上,但是我们只要知道书的名字,就可以很方便的取出我们想要的书,而不用像从乒乓球盒子里取乒乓一样,非得将上面的所有乒乓球拿出来才能取到中间的某一个乒乓球。好比在JSON格式的数据中,我们存储的key-value是可以无序的,因为顺序的不同并不影响我们的使用,我们只需要关心书的名字。

7.3 任务队列

在 JS 中,理解队列数据结构的目的主要是为了清晰的明白事件循环。

队列是一种先进先出(FIFO)的数据结构。正如排队过安检一样,排在队伍前面的人一定是最先过检的人。用以下的图示可以清楚的理解队列的原理。

image.png

7.4 盘古开天辟地之前的JS世界

浏览器提供了window,然后把consoledocumentObject对象、Array数组、Function函数,都挂在window上。

image.png

8. 面向对象编程

将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。

编程范式内容
面向对象编程每个对象都是功能中心 可复用 模块化
过程式编程由函数或执行组成

对象是单个实物的抽象

需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。此处的模板就是“类”,即对象就是“类”的实例。JS语言是基于构造函数和原型的对象体系。

var Vehicle = function(){
  this.price = 1000;
}
// Vehicle是构造函数,this代表所要生成的对象实例

对象是一个容器

封装了属性和方法,属性是对象的状态,方法是对象的行为。

new命令

作用就是执行构造函数,返回一个实例对象

var Vehicle = function(){
  this.price = 1000;
}

var v = new Vehicle();
v.price // 1000

若忘记使用new命令,直接调用构造函数:

var Vehicle = function(){
  this.price = 1000;  // this 代表全局对象
}

var v = Vehicle();
v     // undefined
price // 1000

使用new命令时,它后面的函数依次执行下面的步骤。

  1. 创建一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的原型,指向构造函数的prototype属性。
  3. 将这个空对象赋值给函数内部的this关键字。
  4. 开始执行构造函数内部的代码

对象的继承

JS 语言借鉴了Self语言原型继承

JS 继承机制的设计思想:原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。

XXX.prototype (首字母大写)存储了 XXX 对象的共有属性,即原型。原型让你无需重复声明共有属性,省代码也省内存。

每个对象(首字母小写)都有一个隐藏属性,它指向原型。

prototype__proto__的区别:

两者都存着原型的地址,只不过前者挂在函数上,后者挂在新生成的对象上。

9. JS基本语法

JS 语言借鉴了C语言基本语法

9.1 数据与函数

所有的编程语言只是在某些功能的侧重点不同,本质都是数据和函数:数据就是保存在内存或硬盘上的信息;函数就是处理数据的逻辑和过程。

编程语言本身只有计算功能,即有数据有函数的环境,它能实现的所有功能都是不同环境提供的,不同环境是由不同的函数实现的,层层嵌套最终反映在调用硬件的电流开关,屏幕led灯,内存的电门,硬盘的电位。

9.2 变量

JS语言的每个值可以称为数据,数据是有类型的。为了方便表示数据引入的变量的概念。

变量是对“值”的具名引用。变量就是为“值”起名,然后引用这个名字,就等同于引用这个值。变量的名字就是变量名。

变量名和函数名是最常见的标识符,用来识别各种值的合法名称,其命名规则中第一个字符可以是任意Unicode字符,$和下划线_,第二个字符及以后,除第一个字符允许的三个,还可用数字0-9

var a = 1;
//  = 赋值就是将数值1与变量a之间建立引用关系
// var 为变量声明命令,表示通知解释引擎,要创建一个变量a

var a;
a  // 只声明不赋值,变量a的值为undefined,特殊值 表示无定义
几个概念内容备注
变量数据存放的容器或地址
只有知道了地址才能找到数据
存储数据
变量声明
创建数据
var name创建数据的名称 声明要了一块内存,右边的字符数据存在了内存中
变量赋值name = "小米"将数值和变量名建立引用关系
第一次赋值称为初始化
三种变量声明命令内容
var过时的声明方式,有很多bug
let遵循块作用域,使用范围不超过块
不能重复声明
可以赋值,也可不赋值
必须先声明再使用,否则报错
全局声明let变量不会变成window的属性
const
只读变量即常量
声明时就要赋值,赋值后不能改

9.3 变量和函数提升

JS引擎在代码正式执行之前会做一个预处理的工作:收集变量和收集函数,也就是先解析代码,获取所有被声明的变量,然后再逐行运行,这造成所有的变量声明语句都会提升到代码的头部,即为变量提升。

提升可以在代码中先使用后声明,可以把函数统一声明到底部,在上面调用,有需要再去看底部的定义。这样的话代码结构清晰,更多关注逻辑,而不是函数的声明和定义。

x = 5;            
console.log(x);   // 5
var x;            // 变量提升到最上方
console.log(divide(8, 4));

function divide(a, b){
    return a / b;
}

9.4 如何避免变量提升

为了避免变量提升可能造成的影响,对于var关键字声明的变量,采用先声明后使用。后来的letconst关键字声明变量的方法也是严格遵守这个规定的。

console.log(name);  // Uncaught ReferenceError:Cannot access 'name' before initialization
let name = "小米"
// 使用 let 声明会产生临时性死区

9.5 语句

JS程序的执行单位为行,每一行就是一个语句,是为了完成某种任务而进行的操作。

var a  = 1+ 3;
// 用 var 命令声明变量 a ,然后将表达式的结果赋值给 a
// 1+3 为表达式,为得到返回值的计算式
区别内容举例
表达式一般都有值1 + 2 表达式的为 3
add(1,2)表达式的值为函数的返回值
console.log表达式的值为函数本身
console.log(3)表达式的值为undefined 打印结果为3
语句一般会改变环境声明、赋值 var a = 1

9.6 语句块

代码的集合,用花括号包围:

{
    var name = "小米"console.log(name);  // 小米
    
    let age = 18;       // let 只在代码块中有效
    console.log(age);   // 18
    
    const YEAR = 2022;   // const 只在代码块中有效
    console.log(YEAR);   // 2022
}

console.log(age);  // error age is not defined 访问不到
console.log(YEAR); // error YEAR is not defined 访问不到 

9.7 数据类型

所有数据在底层都是对象,即一堆数据的集合。然后划分为原始值、函数、数组和正则。

所有=号后面的值,称为字面值,固定不会变化,条件判断语句可以比较变量值和字面值。

原始值,也是特殊的对象,依然有对象的函数方法和属性;

对象就是各种值组成的集合,可看作是一个存放各种值的容器,也称为合成类型的值。

// 数值、字符串、布尔值这三种类型,合称为原始类型的值
var visible = true;  // 布尔值:表示真伪的两个特殊值true 或 false
var num = 100;       // 数值:整数、小数、负数
var str = "hello";    // 字符串:文本,用单、双引号

// 两个特殊值
var notinit = undefined;  // undefined 类型,变量声明未赋值或赋值为undefined的类型
var emptyValue = null;            // null:变量赋值为空 null

var sym = symbol;     // symbol 类型 唯一且不可修改
var big = bigint;     // 大数字 类型

9.8 判断数据类型

JS在代码执行的时候,会动态判断数据类型,JS 有三种方法确定一个值的类型。

  • typeof 运算符
  • instanceof 运算符 (可区分数组和对象)
  • Object.prototype.toString 方法
console.log(typeof visible);      // boolean
console.log(typeof num);         // number 
console.log(typeof str);         // string 
console.log(typeof notinit);     // undefined 
console.log(typeof emptyValue);  // object,早期的es规范
console.log(typeof sym);         // symbol 

function f() {}
typeof f  // function
console.log(num + str);         // 100hello,+号拼接
var strNum = "123";
console.log(parseInt(strNum) + num);  // 223

10. 对象

对象实质上就是属性和方法的容器,它的主要作用就是存储属性和方法,这就是所谓的封装

无序的数据集合;键值对的集合。键名只能是字符串,不是标识符。属性值可以是任意一种数据类型。

变量作属性名:

let p1 = 'name';
let obj = {
  [p]:'frank'  // 加了[]会当做变量求值,属性名为 'name'
}

对象的每一个键名又称为“属性”,它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用,对象的属性之间用逗号分割。

// obj 对象的属性p,指向一个函数
var obj = {
  p: function (x) {
    return 2 * x;
  }
};

obj.p(1) // 2

10.1 对象的引用

如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。具体可以看我的另一篇博文,深拷贝和浅拷贝

10.2 对象的隐藏属性

JS中每一个对象都有一个隐藏属性。这个隐藏属性储存着其共有属性组成的对象的地址,这个共有属性组成的对象叫做原型,即隐藏属性储存着原型的地址。

对象.__proto__ === 其构造函数.prototype

10.3 对象分类

需要分类的原因是:

有很多对象拥有一样的属性和行为,需要把它们分为同一类,这样创建类似对象的时候就会很方便。常见的有:数组Array和函数Function,还有Date、RegExp等。

区别自身属性共有属性
数组对象'0'/ '1'/ '2'/lengthpush/pop/shift/join/unshift
函数对象name/lengthcall/apply/bind

10.4 对象的表示

对象采用大括号表示,若在行首是一个大括号,JS引擎一律解释为代码块;若解释为对象,要在大括号前加上圆括号。

{ foo: 123 }    // 代码块
({ foo: 123 })  // 对象

10.5 属性的操作

  • 属性的读取

点运算符和方括号运算符

var obj = { p:'Hello World' };

obj.p     // 'Hello World'
obj.['p'] // 'Hello World' ,键名必须放引号里,否则会被当做变量
  • 属性的赋值
var obj = {};

obj.foo = 'Hello';
obj['bar'] = 'World';
  • 属性的查看

查看一个对象本身的所有属性,用Object.keys方法

var obj = {
  key1: 1,
  key2: 2
};

Object.keys(obj);
// ['key1', 'key2']
  • 属性的删除
  • 属性的遍历

for...in 循环

11. 数组

11.1 非典型数组

标题内容
典型的数组元素的数据类型相同
通过数字下标获取元素
JS的数组元素的数据类型可以不同
通过字符串下标获取元素

其本质是一种特殊的对象,特殊在于它的键名是按次序排列的一组整数。

var arr = ['a', 'b', 'c'];

Object.keys(arr)
// ["0", "1", "2"]

11.2 数组的定义

数组是按次序排列的一组值。每个值的位置都有编号(从0开始),整个数组用方括号表示。任何类型的数据都可以放入数组。

var arr = ['a', 'b', 'c'];

11.3 数组的操作

数组常用方法内容备注
改变原数组push / pop / shift / unshift / sort / reverse / splice
不改变原数组concat / join
map返回一个新的数组,数组中的元素为原始数组调用函数处理后的值n 变 n
filter创建一个新数组,新数组中的元素是通过检查指定数组中符合条件的所有元素n 变少
forEach方法类似于 map
sort用于对数组的元素进行排序
reduce数组中的每个值(从左到右)开始缩减,最终计算为一个值n 变 1
find用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回

创建数组

let arr = [1,2,3]

let arr = new Array(1,2,3)

let str = '1,2,3'
str.split(',')     // ["1", "2", "3"]

let str2 = '123'
str2.split('')     // ["1", "2", "3"]

Array.from('123')  // ["1", "2", "3"]

// 伪数组:没有数组共用属性的数组
Array.from({0:'a', 1:'b', 2:'c', length:2})  //  ["a", "b"]

arr1.concat(arr2)  // 合并两个数组,得到新数组
arr1.slice(1)  // 从第2个元素开始截取一个数组的一部分
arr1.slice(0)  // 全部截取,复制数组

JS原生提供的仅有浅拷贝

删数组元素

let arr = ['a', 'b', 'c']
delete arr['0']
arr    // [empty, 'b', 'c']

let arr2 = [1,2,3]
arr2.shift()  // 1  删第一个元素
arr2    // [2,3]

let arr3 = [1,2,3]
arr3.pop()  // 3 删最后一个元素
arr3        // [1,2]

let arr4 = [1,2,3,4,5,6,7,8]
arr4.splice(2,3)  // 删掉从下标为2的元素,往后数3个元素  [3,4,5]
arr4        // [1,2,6,7,8]

let arr5 = [1,2,3,4,5,6,7,8]
arr5.splice(5,1,666)  // 删掉下标为5的元素,添加666元素
arr5       //  [1,2,3,4,5,666,7,8]

查数组元素

let arr = [1,2,3,4,5]
// for 循环遍历数组 可支持 breack、continue
for(let i = 0; i<arr.length; i++){
  console.log(`${i}:${arr[i]}`)  // 0:1  1:2  2:3  3:4  4:5
}
// forEach遍历数组

function forEach(array, fn){  // forEach函数接收两个参数:数组array和一个函数fn
  for(let i=0; i<arr.length; i++){  // forEach函数体是遍历得到数组的每个元素作为参数传入给fn
    fn(array[i])
  }
}
forEach(['a','b','c'], function(){console.log('执行了一次')}) 
// 执行了一次 x3
forEach(['a','b','c'], function(x){console.log(x)}) 
// a 
// b 
// c 

// 上例中函数forEach在前 数组在后 与下面的写法几乎等价的,要素都有数组、forEach和函数
// 这是一种回调函数
arr.forEach(function(item, index)){
  console.log(`${index}:${item}`)
}

function forEach(array, fn){  
  for(let i=0; i<arr.length; i++){  
    fn(array[i], i)
  }
}
forEach(['a','b','c'], function(x, y){console.log(x, y)}) 
// a 0 
// b 1 
// c 2 

增加数组元素

let arr = [1,2,3]  
arr.push(4)  // 在尾部加元素
arr   // [1,2,3,4]
arr.push('a','b','c')
arr   // [1,2,3,4,'a','b','c']

let arr2 = [1,2,3]
arr.unshift(4)  // 在头部加元素
arr   // [4,1,2,3]

let arr3 = [1,2,3,4,5]
arr.splice(3, 0, 3.5)  // 在中间加元素 在下标为3处插入 3.5
arr    // [1,2,3,3.5,4,5]

let arr = [1,2,3,4]
arr.reverse()   // 反转数组
arr  // [4,3,2,1]

let arr = [5,2,4,3,1]
arr.sort()   // 重排序
arr   // [1,2,3,4,5]
arr.sort(function(a,b){}) // 自定义排序方式
arr.sort((a,b)=>a-b)

数组变换

map([cow, potato, cock, yumi], cook)=>[汉堡, 薯片, 鸡腿, 爆米花]
filter([汉堡, 薯片, 鸡腿, 爆米花], isVegetarian)=>[薯条, 爆米花]
reduce([汉堡, 薯片, 鸡腿, 爆米花], eat)=> shit
let arr = [1,2,3,4,5,6]
arr.map(item => item * item)  // n 变 n
arr  // [1,4,9,16,25,36]
let arr = [1,2,3,4,5,6]
arr.filter(item => item %2 === 0true : false)  // n 变少,留下偶数
arr.filter(item => item %2 === 0)  // 简写
arr  // [2,4,6]
let arr = [20, 40, 90, 100];
let newArr = arr.forEach((item,index) => {
  console.log(item,index);
});
//20 0
//40 1
//90 2
//100 3
let arr = [1,2,3,4,5,6]
let sum = 0
for(let i=0; i<arr.length; i++){
  sum += arr[i]
}
console.log(sum)  // 21

arr.reduce((sum, item)=>{return sum+item}, 0) // 21 n变1
let arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
    if (x < y) {
        return -1;
    }
    if (x > y) {
        return 1;
    }
    return 0;
});
console.log(arr); // [1, 2, 10, 20]
let arr = [11, 20, 51, 82];
let result = arr.find((item) => {
  return item > 50;
}, 0);
console.log(result);  //51

reduce模型:天下无贼范伟打劫!

image.png

12. JS函数

借鉴Scheme语言函数式编程

函数的本质是处理数据的方法,JS 把它当成一种数据类型,可以赋值给变量,这为编程带来了很大的灵活性,也为 JS 的“函数式编程”奠定了基础。

定义函数

具名函数

function 函数名(形式参数1, 形式参数2){
  语句
  return 返回值
}

匿名函数(函数表达式)

let a = function(x, y){return x+y}   // 变量 a 容纳了函数的地址

let a = function fn(x,y){return x+y}  
fn(1,2)  // fn 未定义,fn作用域仅在等号右侧
var mul = function(a, b){  // 匿名函数 省略函数名
    return a * b;
};
console.log(mul(6, 2));  // 12

箭头函数

let gre = () => {
    console.log("hello");
};
gre();   // hello
//  若形参只有一个,小括号可省略
let gre = name => {
    console.log("hello" + name);
};
gre("小米");   // hello 小米
//  若有返回值,只有一行代码,可省略花括号
let inc = x => x + 1;
console.log(inc(6));  // 7

自执行函数

函数在定义完之后,直接调用它自己。这样做的好处是这个自执行内部的代码,外部是访问不到的,防止篡改,内部形成了自己的作用域。

var num1 = 10;

(function(){
    var num1 = 20;
    console.log(num1);   // 20
})();

console.log(num1);    // 10

声明函数

JS 有三种声明函数的方法:

  1. function 命令
  2. 函数表达式
  3. Function 构造函数
// 定义函数用 function 关键字,( )形式参数,{ } 函数体
function putInRef(){
  console.log("打开冰箱门");
  console.log("把大象放进去");
  console.log("关上冰箱门");
}

function putAnyInRef(something){
  console.log("打开冰箱门");
  console.log("把" + something + "放进去");
  console.log("关上冰箱门");
}

// 函数有返回值 用 return 关键字,把返回值赋值给调用函数的地方/另一个变量
function add(a, b){
    return a + b;
}

// 强制函数返回,但不需要返回值,只需要加 return 关键字
function testNum(num){
    if(num < 0) return;
    return num > 10;
}

调用函数

putInRef();          // 打开冰箱门 把大象放进去 关上冰箱门
putAnyInRef("冷兔");  // 打开冰箱门 把冷兔放进去 关上冰箱门

add(1, 2)
console.log(add(1, 2)); // 3

var result = add(1,2);
console.log(result);   // 3

// 参数为变量
console.log(add(result, 5));  // 8

console.log(testNum(-5));  // undefined
console.log(testNum(15));  // true

调用栈

JS引擎在调用一个函数前,需要把函数所在的环境push到一个数组里,这个数组叫做调用栈。等函数执行完了,就会把环境弹出来,然后return到之前的环境,继续执行后续代码。

函数的作用

标题内容备注
处理数据数据的增删改查隐式转换/比较运算符转换/函数转换
实现逻辑函数的计算过程
交互人机交互、系统交互

异步函数

回调函数

回调函数是一段代码执行完之后要调用的函数,作为另一个函数的参数传进去,然后在另一个函数里去调用它。

function request(cb){
  console.log("请求数据");
  cb();
  console.log("请求结束");
}

function callback(){
  console.log("执行回调");
}

request(callback);  // 请求数据、执行回调、请求结束
  • 回调函数也可以有参数
function request(cb){
  console.log("请求数据");
  cb("success");
  console.log("请求结束");
}

function callback(result){  // 有参数
  console.log("执行回调");
  console.log("执行结果是:" + result);
}

request(callback);  // 请求数据、执行回调、执行结果是:success、请求结束
  • 用箭头函数简化:
function request(cb){
  console.log("请求数据");
  cb("success");
  console.log("请求结束");
}

request(result => {
  console.log("执行回调");
  console.log("执行结果是:" + result);
});  // 请求数据、执行回调、执行结果是:success、请求结束

Promise函数

返回的Promise数据可以通过then函数进一步处理。

new Promise(function(resolve, reject){
    resolve(返回结果);
    reject(表示失败)
});

async函数

处理异步任务时,该函数内可使用await关键字等待计算结果

类 class

函数的要素

作用域

作用域也叫做词法环境。在JS中,将作用域定义为一套规则,用来管理JS引擎如何在当前作用域以及嵌套的子作用域中根据变量名或函数名进行变量查找

一个程序会有很多创建数据的地方,为了避免变量太多互相冲突,就引入作用域的功能,即数据起作用的区域。作用域就是自定义变量的可用范围,分为全局作用域和块级作用域(if块、while块、函数块、for循环块、单独块{}

全局作用域

没有在函数里定义的变量;在JS文件最外层定义的变量的作用域

局部作用域

定义在函数内部的变量,只能在函数内部使用,局部作用域里的代码可以访问全局作用域的变量。但对于花括号内的代码块里的作用域会有不同。

var x = 5; // 全局作用域

function add(a){
    var y = 10;
    return a + x;
}
console.log(add(8));  // 13
console.log(y);       // error 访问不到

局部作用域变量名与全局作用域变量名相同,全局将会被局部变量覆盖:

var num = 100;
function mul(num){
    return num * 10;
}
cosole.log(mul(4));  // 40

作用域嵌套

如果多个作用域有同名变量a,查找a的声明时,向上取最近的作用域,简称就近原则。

function f1(){
  let a = 1
  function f2(){
    let a = 2
    console.log(a)
  }
  console.log(a)
  a = 3
  f2()  // 2
}
f1()  //  1

作用域链

作用域链,是作用域的具体实现。是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

块作用域

var i = 99;
for(var i = 0; i < 5; i++){
  console.log(i); // 0 1 2 3 4
}
console.log(i); // 5
var i = 99;
for(let i = 0; i < 5; i++){
  console.log(i); // 0 1 2 3 4
}
console.log(i); // 99

arguments 对象

arguments 对象包含了函数运行时的所有参数,即函数里内置的参数集合,跟数组类似。

function log(){
    console.log(arguments[0]);  // 只打印第一个参数
}
log("abc","bcd");  // abc
function log(){
    for(let i = 0; i < arguments.length; i++){
    console.log(arguments[i]);  // 打印出所有实参
}
log("abc","bcd");  // abc bcd

形式参数 与 实际参数

function add(x,y){
  return x+y
}
add(1,2)
// 调用add时,1 和 2 是实际参数,会被赋值给 x y

// x,y即为形参,本质就是变量声明
function add(){
  var x = arguments[0]
  var y = arguments[1]
  return x+y
}

返回值

函数体内部的return语句,表示返回。JS 引擎遇到return语句,就直接返回return后面的那个表达式的值,后面即使还有语句,也不会得到执行。也就是说,return语句所带的那个表达式,就是函数的返回值。return语句不是必需的,如果没有的话,该函数就不返回任何值,或者说返回undefined

函数执行完了后,才会返回;每个函数都有返回值,没写return,返回值是undefined

letvar的比较

相同点:都是用来定义变量的;出了函数本身,就不能再访问了;两者在全局定义的变量,在哪里都可以访问。

不同点:除函数外,其他花括号或小括号代码块中定义的变量,在出了代码块后是否能够访问。var可以访问,let不可访问。比如elsefor循环。

var z = 6;
if(z > 2){
    console.log(z);
    var innerZ = 17;  // 把 var 改成 let 代码块外就不能访问了
}
console.log(innerZ);   // 17
for(var i = 0; i < 5; i++){
    console.log(i);  // 0 1 2 3 4 
}
console.log(i);  // 0 1 2 3 4 5
for(let i = 0; i < 5; i++){
    console.log(i);  // 0 1 2 3 4 
}
console.log(i);  // error, 无法访问
for(var i = 0; i < 5; i++){
    console.log(i);  // 0 1 2 3 4
    var innerI = 33;
}
console.log(i);  // 0 1 2 3 4 5
console.log(innerI);  // 0 1 2 3 4 5 33

递归

递归就是函数自己调用自己,类似于循环,为避免无限递归,需要一个递归的退出条件。

function sum(n){
    if(n === 1){  
        return 1;  // 递归退出
    }
    return n + sum(n - 1);
}

console.log(sum(10));  // 55
  • 斐波那契数列,每个数都是前两个数之和 1 1 2 3 5 8 13 ...
function fib(num){
    if(num <= 1){
      return 1;
    }
    return fib(num - 1) + fib(num - 2);
}

console.log(fib(0));  // 1  ,数组的下标从0开始取数
console.log(fib(1));  // 1
console.log(fib(2));  // 2  fib(2) = fib(1) +fib(0)
console.log(fib(5));  // 8  

fib(5) = fib(4) +fib(3) = 2fib(3) + fib(2) = 2(fib(2)+fib(1))+fib(2) = 8

执行上下文

也叫执行上下文对象,可简单理解为当前代码的执行环境,它会形成一个作用域。代码在正式执行之前作用域是在代码定义时产生的)会进入到执行环境,主要做的事有:

JS执行环境:全局环境(JS代码运行起来会首先进入该环境)、函数环境(当函数被调用执行时,会进入当前函数中执行代码)、eval(不建议使用,可忽略)

  1. 创建变量对象(变量提升):收集变量、函数、函数的参数
  2. 确认 this 的指向:全局的指向 window;局部的指向调用其的对象
  3. 创建作用域链:父级作用域链 + 当前的变量对象
ECObj = {
  变量对象: {变量, 函数, 函数的形参},
  scopeChain: 父级作用域链 + 当前的变量对象,
  this: {window || 调用其的对象}
}

函数调用栈

在一个JS程序中,会产生多个执行上下文,JS引擎会以栈的方式来处理它们,这个栈,我们称其为函数调用栈。栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文。

当代码在执行过程中,遇到以上三种JS执行环境,都会生成一个执行上下文,放入栈中,而处于栈顶的上下文执行完毕之后,就会自动出栈。

执行上下文的栈组

const color = 'blue';
function changeColor(){
  const anotherColor = 'red';
  function swapColors(){
    const tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;
  }
  swapColors();
}
changeColor()

image.png

整个过程为:

  1. 全局上下文入栈
  2. changeColor的执行上下文入栈。具体为:步骤1之后,其中的可执行代码开始执行,直到遇到changeColor(),激活函数changeColor创建它自己的执行上下文。
  3. swapColors的执行上下文入栈。具体为:步骤2之后,控制器开始执行其中的可执行代码,遇到swapColors()之后又激活了一个执行上下文。
  4. swapColors的上下文弹栈。在swapColors的可执行代码中,再没有遇到其他能生成执行上下文的情况,因此这段代码顺利执行完毕,swapColors的上下文从栈中弹出。
  5. swapColors的执行上下文弹出之后,继续执行changeColor的可执行代码,也没有再遇到其他执行上下文,顺利执行完毕之后弹出。这样,ECStack中就只剩下全局上下文了,全局上下文在浏览器窗口关闭后出栈。

函数中,遇到return能直接终止可执行代码的执行,因此会直接将当前上下文弹出栈。

函数的结构

结构内容备注
1. 参数参数可以是函数
2. 计算过程运算符号
控制过程
算术/赋值/比较/逻辑/扩展/一元/位
条件/循环
3. 输出返回给调用方输出可以没有,可以是数据/函数

运算符

运算符是处理数据的基本方法,用来从现有的值得到新的值。JS 提供了多种运算符,覆盖了所有主要的运算。

圆括号(())可以用来提高运算的优先级,因为它的优先级是最高的,即圆括号中的表达式会第一个运算。

算术运算符

console.log("1+5=", 1 + 5);    // 1+5 = 6
console.log("5-1=", 5 - 1);    // 5-1 = 4
console.log("3*9=", 3 * 9);    // 3*9 = 27
console.log("7/2=", 7 / 2);    // 7/2 = 3.5
console.log("7 % 2=", 7 % 2);    // 7 % 2 = 1 , % 为取模
console.log("4 ** 2=", 4 ** 2);    // 4 ** 2= = 16 , 指数操作 4 的2次方

赋值操作符

var x = 10;     
var y = x;
console.log(y);  // 10

一元运算符

var neg = -5;      
console.log(neg);  // -5
var strNum = +"3";   
console.log(strNum, typeof strNum);  // 3 "number"
var num = 8;
console.log(num++);  // ++ 在变量的后面,先返回变量的值,然后再 +1 操作;
console.log(num);    // 9

console.log(++num);  // ++ 在变量的前面,先 +1 ,再返回 +1 的结果;
console.log(num);    // 10

console.log(num--);    // 10
console.log(num);      // 9

console.log(--num);    // 8

比较运算符

用于比较两个值的大小,然后返回一个布尔值,表示是否满足指定的条件。

分类内容
相等运算符比较不同数据类型时,先将数据类型转换,再用严格相等运算符比较
非相等运算符先看两个运算子是否都是字符串
若是,按字典顺序比较;否则都转成数值,再比大小
==   // 相等运算符
===  // 严格相等运算符

console.log("1>=5", 1 >= 5);          // 1>=5 false
console.log("5>=5", 5 >= 5);          // 5>=5 true
console.log("5==5", 5 == 5);          // 5==5 true
console.log('5=="5"', 5 == "5");      // 5=="5" true
console.log('5==="5"', 5 === "5");    // 5==="5" false

console.log(undefined == null);     //  true
console.log(undefined === null);    //  false

逻辑运算符

// 且运算符 && 用于多个表达式的求值
console.log("true && false");   // false
// 或运算符 || 用于多个表达式的求值
console.log("true || false");   // true
// 取反运算符 ! 
console.log(!true);   // false
// 三元运算符 ?:

// false、0、""、null、undefined  都是falsy值
console.log(!4);  //  false , 4 不是falsy值,故取反为false

取反运算符 ! 用于将布尔值变为相反值,对于非布尔值,先将其转为布尔值,以下六个值取反后为true,其他值都为false:

  • undefined
  • null
  • false
  • 0
  • NaN
  • 空字符串(''

表达式就是能计算出结果的一段代码。

console.log(true && "hello");   // hello
// 满足某一个条件,再去执行后面的代码或显示某一个组件

console.log(false || "default");   // default
// 用于给变量一个默认值

console.log(false && "not printed");   // false
console.log(true || "not printed"4);   // 4

位运算符

流程控制

JS提供if结构和switch结构,完成条件判断,只有满足预设的条件,才会执行相应的语句。

if 结构

if结构先判断一个表达式的布尔值,然后根据布尔值的真伪,执行不同的语句。所谓布尔值,指的是 JavaScript 的两个特殊值,true表示“真”,false表示“伪”

if...else 结构

在if判断某个数据时,只要不是falsy值,就都是true

var passcode = prompt("请输入暗号");
if(passcode === "彪哥最勇猛"){
    alert("恭喜入会");
} else {
    alert("早日回府");
}
var role = prompt("请输入用户权限");
if(role === "超级管理员"){
    alert("跳转到超级管理员页面");
} else if(role === "管理员"){
    alert("跳转到管理员页面");
} else {
    alert("跳转到用户页面");
}

switch...case 结构

var role = prompt("请输入用户权限");
switch(role){
    case "超级管理员":
      alert("跳转到超级管理员页面");
      break;
    case "管理员":
      alert("跳转到管理员页面");
      break;
    case "特殊用户":
      alert("跳转到特殊用户页面");
      break;
    case "一般用户":
      alert("跳转到一般用户页面");
      break;
    default:
      alert("跳转到其他页面");
}

三元运算符

(条件) ? 表达式1 : 表达式2

若问号?前面的判断条件为真,则执行后面的代码,若为假,则执行冒号:后面的代码。

var temp = 10;
console.log(temp > 15 ? "出门" : "在家")  // 在家

for 循环

循环语句用于重复执行某个操作。

for 循环 可指定循环的起点、终点和终止条件。

for( 初始化表达式; 判断条件; 增量表达式){
  语句
}
// 增量表达式 每一次执行完后要进行的操作

for(let i = 0; i < 10; i++){
    console.log(i); // 0 1 2 3 4 5 6 7 8 9 
}
// i += 2 是 i = i + 2 的缩写
for(let i = 0; i < 10; i +=2){
    console.log(i); // 0 2 4 6 8 
}

for语句后面的括号里面,有三个表达式。

  • 初始化表达式(initialize):确定循环变量的初始值,只在循环开始时执行一次。
  • 条件表达式(test):每轮循环开始时,都要执行这个条件表达式,只有值为真,才继续进行循环。
  • 递增表达式(increment):每轮循环的最后一个操作,通常用来递增循环变量。

while 循环

while语句包括一个循环条件和一段代码块,只要条件为真,就不断循环执行代码块。

var password = "";
while (password !=== "123456"){
    password = prompt("请输入密码");
}
console.log("登录成功");

while语句的循环条件是一个表达式,必须放在圆括号中。代码块部分,如果只有一条语句,可以省略大括号,否则就必须加上大括号。

do while 循环

while循环类似,唯一的区别就是先运行一次循环体,然后判断循环条件。

不管条件是否为真,do...while循环至少运行一次,这是这种结构最大的特点。另外,while语句后面的分号注意不要省略。

var x = 5;
do {
   console.log(x);  // 5
   console.log(x++);  // 5 6 7 8 9 10
} while (x > 5 && x <= 10);

13. 闭包

变量作用域

函数外部无法读取函数内部声明的变量,但是通过在函数的内部再定义一个函数的方法,可以实现读取到函数内的局部变量。

function f1() {
  var n = 999;
}

console.log(n) 
// Uncaught ReferenceError: n is not defined
// 函数 f1 内部声明的变量 n ,函数外是无法读取的
function f1() {
  var n = 999;
  function f2(){
    console.log(n);  // 999
  }
}
  • 上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的,反之则不行,f2内部的局部变量,对f1就是不可见的。这就是JS语言特有的“链式作用域”结构,子对象会逐级向上寻找所有父对象的变量,即父对象的所有变量,对子对象都是可见的,反之则不成立。
  • 既然f2可以读取f1的局部变量,那么把f2作为返回值,就可以在f1外部读取它的内部变量了。
function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}

var result = f1();
result(); // 999

上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。

闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在 JS 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。

闭包的另一个用处,是封装对象的私有属性和私有方法。

异步操作

14. 单线程模型

JS 同时只能执行一个任务,其他任务都必须在后面排队等待。而 JS 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。

JS 语言本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果。这个时候,不得不等着结果出来,再往下执行。排队是因为IO 操作慢,并不是CPU忙。

CPU 可以不管 IO 操作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 JS 内部采用的“事件循环”机制。

15. 同步任务和异步任务

程序里面所有的任务,可以分成两类:同步任务(sync)和异步任务(async)。

同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务(阻塞式)。比如:食堂排队打饭,按照排队的顺序,依次打饭,只有前面一个人打完饭后,才轮到后面一个人。

console.log('a')
console.log('b')
console.log('c')

异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。是非阻塞的,异步逻辑与主逻辑相互独立,主逻辑不需要等待异步逻辑完成,而是可以立刻继续下去。比如:排队打饭,有同学忘记带饭卡,他就先出队,让后面的人先打饭,等他的饭卡找到了,再排队打饭。

setTimeout(() => {console.log('饭卡找到了')}, 2000) 
console.log('b')
console.log('c')

16. 任务队列和事件循环

JS 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列,里面是各种需要当前程序处理的异步任务。

首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。

异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。

JS 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环。

17. 异步操作的模式

17.1 回调函数

回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。

17.2 事件监听

采用事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以去耦合,有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。

17.3 发布/订阅

事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”一个信号,其他任务可以向信号中心“订阅”这个信号,从而知道什么时候自己可以开始执行。这就叫做发布/订阅模式,又称观察者模式。

18. 异步操作的流程控制

如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守这种顺序。

我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。

流程控制函数也可以是并行执行,即所有异步任务同时执行,等到全部完成以后,才执行函数。

所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行n个异步任务,这样就避免了过分占用系统资源。

19. 定时器

JS 提供定时执行代码的功能,叫做定时器,主要由setTimeout()setInterval()这两个函数来完成。它们向任务队列添加定时任务。

setTimeout()

用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。

var timerId = setTimeout(func|code, delay);

setInterval()

指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。

var i = 1
var timer = setInterval(function() {
  console.log(2);
}, 1000)

运行机制

setTimeoutsetInterval的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。

这意味着,setTimeoutsetInterval指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeoutsetInterval指定的任务,一定会按照预定时间执行。

setTimeout(someTask, 100);
veryLongTask();

上面代码的setTimeout,指定100毫秒以后运行一个任务。但是,如果后面的veryLongTask函数(同步任务)运行时间非常长,过了100毫秒还无法结束,那么被推迟运行的someTask就只有等着,等到veryLongTask运行结束,才轮到它执行。

setTimeout()

作用是将代码推迟到指定时间执行,如果指定时间为0,即setTimeout(f, 0),不会立刻执行,必须要等到当前脚本的同步任务,全部处理完以后,才会执行setTimeout指定的回调函数f,setTimeout(f, 0)会在下一轮事件循环一开始就执行.

可以调整事件的发生顺序。比如,网页开发中,某个事件先发生在子元素,然后冒泡到父元素,即子元素的事件回调函数,会早于父元素的事件回调函数触发。如果,想让父元素的事件回调函数先发生,就要用到setTimeout(f, 0)

20. Promise 对象

image.png

Promise 的含义

Promise就是一个容器,保存着某个未来才会结束的事件(异步操作)的结果,可获取异步操作的消息,提供统一API。可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。

function f1(resolve, reject){
  // 异步操作代码
}
// 返回的 p1 就是一个Promise实例
var p1 = new Promise(f1); // 接受一个回调函数 f1 作为参数

Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个then方法,用来指定下一步的回调函数。

const isPregnant = true;
const promise = new Promise((resolve, reject)=>{
  if(isPregant){
    resolve('孩子他爹')
  } else {
    reject('老公')
  }
});

promise
  .then(name => {  // name 为 resolve 保留的参数
    console.log(`男人成为了${name}!`);
  })
  .catch(name => {  // name 为 reject 保留的参数
    console.log(`男人成为了${name}!`);
  })
  .finally(()=>{
    console.log(`两人最终结婚了`);
  });
  
// 第一行代码传入 true 打印 男人成为了孩子他爹 两人最终结婚了
// 第一行代码传入 false 打印 男人成为了老公 两人最终结婚了

Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

const promise = new Promise(传一个函数);

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);  
  } else {
    reject(error);
  }
});

当promise状态发生改变,就会触发then()里的响应函数,处理后续步骤,状态改变只有两种可能:从pending变为fulfilled和从pending变为rejected,这两种情况发生,状态即凝固,不会再变,此时称为resolved已定型。

const promise = new Promise();  // 第一步

const promise = new Promise(function(resolve, reject){
  resolve('abc');  // 成功
})

promise.then(function(res){
  console.log(res)  // abc
})
const promise = new Promise();  // 第一步

const promise = new Promise(function(resolve, reject){
  reject('abcd');  // 失败
})

promise.then(function(res){
  console.log(res)  
}).catch( err =>{
  console.log(err)  // abcd
})

Promise 的方法

Promise.all

可将多个Promise实例包装成一个新的Promise实例,同时,成功和失败的返回值是不同的,成功时返回一个结果数组,失败时返回最先被reject失败状态的值。

let p1 = new Promise((resolve, reject)=>{
  resolve('成功了')
})
let p2 = new Promise((resolve, reject)=>{
  resolve('success')
})

let p3 = Promise.reject('失败')

Promise.all([p1, p2]).then(()=>{
  console.log(result)  // ['成功了','success']
}).catch((error)=>{
  console.log(error)
})

Promise.all([p1, p3, p2]).then((result)=>{
  console.log(result)  
}).catch((error)=>{
  console.log(error)  // 失败了,打出‘失败’
})

Promise.race

Promise.race([p1, p2, p3])里面的哪个结果获得快,就返回那个结果,不管结果本身是成功状态还是失败状态。

let p1 = new Promise((resolve, reject)=>{
  setTimeout(()=>{
    resolve('success')
  }, 1000)
})
let p2 = new Promise((resolve, reject)=>{
  setTimeout(()=>{
    reject('failed')
  }, 500)
})

Promise.race([p1, p2]).then((result)=>{
  console.log(result)  
}).catch((error)=>{
  console.log(error)  // 打开的是 failed
})

图片异步加载

const imgAddress = 'https://xxx'
const imgPromise = (url) =>{
  return new Promise ((resolve, reject)=>{
    const img = new Image();
    img.src = url;
    img.onload = () =>{
      resolve(img);
    };
    img.onerror = () =>{
      reject(new Error('图片有误'));
    };
  });
};

imgPromise(imgAddress)
  .then( img => {
    document.body.appendChild(img);
  })
  .catch( err => {
    document.body.innerHTML = err;
  })

21. async / await

是基于promise的语法糖,用于处理异步,是 Generator 函数的改进

async function bb(){
 return Promise.resolve( '别bb,专心学习');
}

bb();  // '别bb,专心学习'
console.log(bb());  // [object Promise]{ ... }

// 执行完 async 函数以后 用 then 来获取返回的值
bb().then( value => {
  console.log(value);  // '别bb,专心学习'
})
// 加上 await 后
async function bb(){
  console.log('1');
  let two = await Promise.resolve('2');
  console.log(two);
  console.log('3');
  return Promise.resolve( '别bb,专心学习');
}

bb().then( value => {
  console.log(value);  
});
// "1"
// "2"
// "3"
// '别bb,专心学习'

优化Fetch语法,对资源进行连续请求:

let bb = async() => {
  const url = 'https://xxx';
  try{
    let responses = await Promise.all(
      [fetch(`${url}/1/`), fetch(`${url}/2/`), fetch(`${url}/3/`)]
    );
    let jsons = responses.map(response => response.json());
    values.map(value => {
      console.log(value.data.name);
    });
  } catch (error){
    console.log(error);
  }
};
bb();

JS 进阶

编程最常用的是对数据的操作,前端经常用 JS 对数据进行转换、筛选、查找、排序,反复用到 arrayobject 这两个数据结构。

学会分析项目中用到了哪些数据,实现网页的功能要对这些数据做怎样的操作,比如:对于 array 而言,常见的三个操作就是 mapfilterreduce