js底层知识汇总,大厂必备
面试官:说说JavaScript中的数据类型?存储上的差别?
前言
在JavaScript
中,我们可以分成两种类型:
- 基本类型
- 复杂类型
两种类型的区别是:存储位置不同
一、基本类型
基本类型主要为以下6种:
- Number
- String
- Boolean
- Undefined
- null
- symbol
Number
数值最常见的整数类型格式则为十进制,还可以设置八进制(零开头)、十六进制(0x开头)
let intNum = 55 // 10进制的55
let num1 = 070 // 8进制的56
let hexNum1 = 0xA //16进制的10
浮点类型则在数值汇总必须包含小数点,还可通过科学计数法表示
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效,但不推荐
let floatNum = 3.125e7; // 等于 31250000
在数值类型中,存在一个特殊数值NaN
,意为“不是数值”,用于表示本来要返回数值的操作失败了(而不是抛出错误)
console.log(0/0); // NaN
console.log(-0/+0); // NaN
Undefined
Undefined
类型只有一个值,就是特殊值 undefined
。当使用 var
或 let
声明了变量但没有初始化时,就相当于给变量赋予了 undefined
值
let message;
console.log(message == undefined); // true
包含 undefined
值的变量跟未定义变量是有区别的
let message; // 这个变量被声明了,只是值为 undefined
console.log(message); // "undefined"
console.log(age); // 没有声明过这个变量,报错
String
字符串可以使用双引号(")、单引号(')或反引号(`)标示
let firstName = "John";
let lastName = 'Jacob';
let lastName = `Jingleheimerschmidt`
字符串是不可变的,意思是一旦创建,它们的值就不能变了
let lang = "Java";
lang = lang + "Script"; // 先销毁再创建
Null
Null
类型同样只有一个值,即特殊值 null
逻辑上讲, null 值表示一个空对象指针,这也是给typeof
传一个 null
会返回 "object"
的原因
let car = null;
console.log(typeof car); // "object"
undefined
值是由 null
值派生而来
console.log(null == undefined); // true
只要变量要保存对象,而当时又没有那个对象可保存,就可用 null
来填充该变量
Boolean
Boolean
(布尔值)类型有两个字面值: true
和 false
通过Boolean
可以将其他类型的数据转化成布尔值
规则如下:
数据类型 转换为 true 的值 转换为 false 的值
String 非空字符串 ""
Number 非零数值(包括无穷值) 0 、 NaN
Object 任意对象 null
Undefined N/A (不存在) undefined
Symbol
Symbol (符号)是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
console.log(genericSymbol == otherGenericSymbol); // false
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(fooSymbol == otherFooSymbol); // false
二、引用类型
复杂类型统称为Object
,我们这里主要讲述下面三种:
- Object
- Array
- Function
Object
创建object
常用方式为对象字面量表示法,属性名可以是字符串或数值
let person = {
name: "Nicholas",
"age": 29,
5: true
};
Array
JavaScript
数组是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。并且,数组也是动态大小的,会随着数据添加而自动增长
let colors = ["red", 2, {age: 20 }]
colors.push(2)
Function
函数实际上是对象,每个函数都是 Function
类型的实例,而 Function
也有属性和方法,跟其他引用类型一样
函数存在三种常见的表达方式:
- 函数声明
// 函数声明
function sum (num1, num2) {
return num1 + num2;
}
- 函数表达式
let sum = function(num1, num2) {
return num1 + num2;
};
- 箭头函数
函数声明和函数表达式两种方式
let sum = (num1, num2) => {
return num1 + num2;
};
其他引用类型
除了上述说的三种之外,还包括Date
、RegExp
、Map
、Set
等......
三、存储区别
基本数据类型和引用数据类型存储在内存中的位置不同:
-
基本数据类型存储在栈中
-
引用类型的对象存储于堆中
当我们把变量赋值给一个变量时,解析器首先要确认的就是这个值是基本类型值还是引用类型值
下面来举个例子
基本类型
let a = 10;
let b = a; // 赋值操作
b = 20;
console.log(a); // 10值
a
的值为一个基本类型,是存储在栈中,将a
的值赋给b
,虽然两个变量的值相等,但是两个变量保存了两个不同的内存地址
下图演示了基本类型赋值的过程:
引用类型
var obj1 = {}
var obj2 = obj1;
obj2.name = "Xxx";
console.log(obj1.name); // xxx
引用类型数据存放在内对内中,每个堆内存中有一个引用地址,该引用地址存放在栈中
obj1
是一个引用类型,在赋值操作过程汇总,实际是将堆内存对象在栈内存的引用地址复制了一份给了obj2
,实际上他们共同指向了同一个堆内存对象,所以更改obj2
会对obj1
产生影响
下图演示这个引用类型赋值过程
小结
- 声明变量时不同的内存地址分配:
- 简单类型的值存放在栈中,在栈中存放的是对应的值
- 引用类型对应的值存储在堆中,在栈中存放的是指向堆内存的地址
- 不同的类型数据导致赋值变量时的不同:
- 简单类型赋值,是生成相同的值,两个对象对应不同的地址
- 复杂类型赋值,是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象
面试官:深拷贝浅拷贝的区别?如何实现一个深拷贝?
一、数据类型存储
前面文章我们讲到,JavaScript
中存在两大数据类型:
- 基本类型
- 引用类型
基本类型数据保存在在栈内存中
引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中
二、浅拷贝
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址
下面简单实现一个浅拷贝
function shallowClone(obj) {
const newObj = {};
for(let prop in obj) {
if(obj.hasOwnProperty(prop)){
newObj[prop] = obj[prop];
}
}
return newObj;
}
在JavaScript
中,存在浅拷贝的现象有:
Object.assign
Array.prototype.slice()
,Array.prototype.concat()
- 使用拓展运算符实现的复制
Object.assign
var obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'fx',
name2: 'xka'
},
love: function () {
console.log('fx is a great girl')
}
}
var newObj = Object.assign({}, fxObj);
slice()
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.slice(0)
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
concat()
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
拓展运算符
const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
三、深拷贝
深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝方式有:
-
_.cloneDeep()
-
jQuery.extend()
-
JSON.stringify()
-
手写循环递归
_.cloneDeep()
const _ = require('lodash');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
jQuery.extend()
const $ = require('jquery');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
JSON.stringify()
const obj2=JSON.parse(JSON.stringify(obj1));
但是这种方式存在弊端,会忽略undefined
、symbol
和函数
const obj = {
name: 'A',
name1: undefined,
name3: function() {},
name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}
循环递归
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
四、区别
下面首先借助两张图,可以更加清晰看到浅拷贝与深拷贝的区别
从上图发现,浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象
// 浅拷贝
const obj1 = {
name : 'init',
arr : [1,[2,3],4],
};
const obj3=shallowClone(obj1) // 一个浅拷贝方法
obj3.name = "update";
obj3.arr[1] = [5,6,7] ; // 新旧对象还是共享同一块内存
console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3',obj3) // obj3 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
// 深拷贝
const obj1 = {
name : 'init',
arr : [1,[2,3],4],
};
const obj4=deepClone(obj1) // 一个深拷贝方法
obj4.name = "update";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存
console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
小结
前提为拷贝类型为引用类型的情况下:
-
浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
-
深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址
面试官:什么是防抖和节流?有什么区别?如何实现?
一、是什么
本质上是优化高频率执行代码的一种手段
如:浏览器的 resize
、scroll
、keypress
、mousemove
等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能
为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用debounce
(防抖)和throttle
(节流)的方式来减少调用频率
定义
- 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
- 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
一个经典的比喻:
想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应
假设电梯有两种运行策略 debounce
和 throttle
,超时设定为15秒,不考虑容量限制
电梯第一个人进来后,15秒后准时运送一次,这是节流
电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖
代码实现
节流
完成节流可以使用时间戳与定时器的写法
使用时间戳写法,事件会立即执行,停止触发后没有办法再次执行
function throttled1(fn, delay = 500) {
let oldtime = Date.now()
return function (...args) {
let newtime = Date.now()
if (newtime - oldtime >= delay) {
fn.apply(null, args)
oldtime = Date.now()
}
}
}
使用定时器写法,delay
毫秒后第一次执行,第二次事件停止触发后依然会再一次执行
function throttled2(fn, delay = 500) {
let timer = null
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay);
}
}
}
可以将时间戳写法的特性与定时器写法的特性相结合,实现一个更加精确的节流。实现如下
function throttled(fn, delay) {
let timer = null
let starttime = Date.now()
return function () {
let curTime = Date.now() // 当前时间
let remaining = delay - (curTime - starttime) // 从上一次到现在,还剩下多少多余时间
let context = this
let args = arguments
clearTimeout(timer)
if (remaining <= 0) {
fn.apply(context, args)
starttime = Date.now()
} else {
timer = setTimeout(fn, remaining);
}
}
}
防抖
简单版本的实现
function debounce(func, wait) {
let timeout;
return function () {
let context = this; // 保存this指向
let args = arguments; // 拿到event对象
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
防抖如果需要立即执行,可加入第三个参数用于判断,实现如下:
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout); // timeout 不为null
if (immediate) {
let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) {
func.apply(context, args)
}
}
else {
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
}
二、区别
相同点:
- 都可以通过使用
setTimeout
实现 - 目的都是,降低回调执行频率。节省计算资源
不同点:
- 函数防抖,在一段连续操作结束后,处理回调,利用
clearTimeout
和setTimeout
实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能 - 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次
例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次
如下图所示:
三、应用场景
防抖在连续的事件,只需触发一次回调的场景有:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小
resize
。只需窗口调整完成后,计算窗口大小。防止重复渲染。
节流在间隔一段时间执行一次回调的场景有:
- 滚动加载,加载更多或滚到底部监听
- 搜索框,搜索联想功能
面试官:DOM常见的操作有哪些?
一、DOM
文档对象模型 (DOM) 是 HTML
和 XML
文档的编程接口
它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容
任何 HTML
或XML
文档都可以用 DOM
表示为一个由节点构成的层级结构
节点分很多类型,每种类型对应着文档中不同的信息和(或)标记,也都有自己不同的特性、数据和方法,而且与其他类型有某种关系,如下所示:
<html>
<head>
<title>Page</title>
</head>
<body>
<p>Hello World!</p >
</body>
</html>
DOM
像原子包含着亚原子微粒那样,也有很多类型的DOM
节点包含着其他类型的节点。接下来我们先看看其中的三种:
<div>
<p title="title">
content
</p >
</div>
上述结构中,div
、p
就是元素节点,content
就是文本节点,title
就是属性节点
二、操作
日常前端开发,我们都离不开DOM
操作
在以前,我们使用Jquery
,zepto
等库来操作DOM
,之后在vue
,Angular
,React
等框架出现后,我们通过操作数据来控制DOM
(绝大多数时候),越来越少的去直接操作DOM
但这并不代表原生操作不重要。相反,DOM
操作才能有助于我们理解框架深层的内容
下面就来分析DOM
常见的操作,主要分为:
- 创建节点
- 查询节点
- 更新节点
- 添加节点
- 删除节点
创建节点
createElement
创建新元素,接受一个参数,即要创建元素的标签名
const divEl = document.createElement("div");
createTextNode
创建一个文本节点
const textEl = document.createTextNode("content");
createDocumentFragment
用来创建一个文档碎片,它表示一种轻量级的文档,主要是用来存储临时节点,然后把文档碎片的内容一次性添加到DOM
中
const fragment = document.createDocumentFragment();
当请求把一个DocumentFragment
节点插入文档树时,插入的不是 DocumentFragment
自身,而是它的所有子孙节点
createAttribute
创建属性节点,可以是自定义属性
const dataAttribute = document.createAttribute('custom');
consle.log(dataAttribute);
获取节点
querySelector
传入任何有效的 css
选择器,即可选中单个 DOM
元素(首个):
document.querySelector('.element')
document.querySelector('#element')
document.querySelector('div')
document.querySelector('[name="username"]')
document.querySelector('div + p > span')
如果页面上没有指定的元素时,返回 null
querySelectorAll
返回一个包含节点子树内所有与之相匹配的Element
节点列表,如果没有相匹配的,则返回一个空节点列表
const notLive = document.querySelectorAll("p");
需要注意的是,该方法返回的是一个 NodeList
的静态实例,它是一个静态的“快照”,而非“实时”的查询
关于获取DOM
元素的方法还有如下,就不一一述说
document.getElementById('id属性值');返回拥有指定id的对象的引用
document.getElementsByClassName('class属性值');返回拥有指定class的对象集合
document.getElementsByTagName('标签名');返回拥有指定标签名的对象集合
document.getElementsByName('name属性值'); 返回拥有指定名称的对象结合
document/element.querySelector('CSS选择器'); 仅返回第一个匹配的元素
document/element.querySelectorAll('CSS选择器'); 返回所有匹配的元素
document.documentElement; 获取页面中的HTML标签
document.body; 获取页面中的BODY标签
document.all['']; 获取页面中的所有元素节点的对象集合型
除此之外,每个DOM
元素还有parentNode
、childNodes
、firstChild
、lastChild
、nextSibling
、previousSibling
属性,关系图如下图所示
更新节点
innerHTML
不但可以修改一个DOM
节点的文本内容,还可以直接通过HTML
片段修改DOM
节点内部的子树
// 获取<p id="p">...</p >
var p = document.getElementById('p');
// 设置文本为abc:
p.innerHTML = 'ABC'; // <p id="p">ABC</p >
// 设置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
// <p>...</p >的内部结构已修改
innerText、textContent
自动对字符串进行HTML
编码,保证无法设置任何HTML
标签
// 获取<p id="p-id">...</p >
var p = document.getElementById('p-id');
// 设置文本:
p.innerText = '<script>alert("Hi")</script>';
// HTML被自动编码,无法设置一个<script>节点:
// <p id="p-id"><script>alert("Hi")</script></p >
两者的区别在于读取属性时,innerText
不返回隐藏元素的文本,而textContent
返回所有文本
style
DOM
节点的style
属性对应所有的CSS
,可以直接获取或设置。遇到-
需要转化为驼峰命名
// 获取<p id="p-id">...</p >
const p = document.getElementById('p-id');
// 设置CSS:
p.style.color = '#ff0000';
p.style.fontSize = '20px'; // 驼峰命名
p.style.paddingTop = '2em';
添加节点
innerHTML
如果这个DOM节点是空的,例如,<div></div>
,那么,直接使用innerHTML = '<span>child</span>'
就可以修改DOM
节点的内容,相当于添加了新的DOM
节点
如果这个DOM节点不是空的,那就不能这么做,因为innerHTML
会直接替换掉原来的所有子节点
appendChild
把一个子节点添加到父节点的最后一个子节点
举个例子
<!-- HTML结构 -->
<p id="js">JavaScript</p >
<div id="list">
<p id="java">Java</p >
<p id="python">Python</p >
<p id="scheme">Scheme</p >
</div>
添加一个p
元素
const js = document.getElementById('js')
js.innerHTML = "JavaScript"
const list = document.getElementById('list');
list.appendChild(js);
现在HTML
结构变成了下面
<!-- HTML结构 -->
<div id="list">
<p id="java">Java</p >
<p id="python">Python</p >
<p id="scheme">Scheme</p >
<p id="js">JavaScript</p > <!-- 添加元素 -->
</div>
上述代码中,我们是获取DOM
元素后再进行添加操作,这个js
节点是已经存在当前文档树中,因此这个节点首先会从原先的位置删除,再插入到新的位置
如果动态添加新的节点,则先创建一个新的节点,然后插入到指定的位置
const list = document.getElementById('list'),
const haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerText = 'Haskell';
list.appendChild(haskell);
insertBefore
把子节点插入到指定的位置,使用方法如下:
parentElement.insertBefore(newElement, referenceElement)
子节点会插入到referenceElement
之前
setAttribute
在指定元素中添加一个属性节点,如果元素中已有该属性改变属性值
const div = document.getElementById('id')
div.setAttribute('class', 'white');//第一个参数属性名,第二个参数属性值。
删除节点
删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的removeChild
把自己删掉
// 拿到待删除节点:
const self = document.getElementById('to-be-removed');
// 拿到父节点:
const parent = self.parentElement;
// 删除:
const removed = parent.removeChild(self);
removed === self; // true
删除后的节点虽然不在文档树中了,但其实它还在内存中,可以随时再次被添加到别的位置
相关链接
developer.mozilla.org/zh-CN/docs/…
面试官:说说你对事件循环的理解
一、是什么
首先,JavaScript
是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环
在JavaScript
中,所有的任务都可以分为
-
同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
-
异步任务:异步执行的任务,比如
ajax
网络请求,setTimeout
定时函数等
同步任务与异步任务的运行流程图如下:
从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就事件循环
二、宏任务与微任务
如果将任务划分为同步任务和异步任务并不是那么的准确,举个例子:
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
如果按照上面流程图来分析代码,我们会得到下面的执行步骤:
console.log(1)
,同步任务,主线程中执行setTimeout()
,异步任务,放到Event Table
,0 毫秒后console.log(2)
回调推入Event Queue
中new Promise
,同步任务,主线程直接执行.then
,异步任务,放到Event Table
console.log(3)
,同步任务,主线程执行
所以按照分析,它的结果应该是 1
=> 'new Promise'
=> 3
=> 2
=> 'then'
但是实际结果是:1
=>'new Promise'
=> 3
=> 'then'
=> 2
出现分歧的原因在于异步任务执行顺序,事件队列其实是一个“先进先出”的数据结构,排在前面的事件会优先被主线程读取
例子中 setTimeout
回调事件是先进入队列中的,按理说应该先于 .then
中的执行,但是结果却偏偏相反
原因在于异步任务还可以细分为微任务与宏任务
微任务
一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前
常见的微任务有:
-
Promise.then
-
MutaionObserver
-
Object.observe(已废弃;Proxy 对象替代)
-
process.nextTick(Node.js)
宏任务
宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合
常见的宏任务有:
- script (可以理解为外层同步代码)
- setTimeout/setInterval
- UI rendering/UI事件
- postMessage、MessageChannel
- setImmediate、I/O(Node.js)
这时候,事件循环,宏任务,微任务的关系如图所示
按照这个流程,它的执行机制是:
- 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
- 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完
回到上面的题目
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
流程如下
// 遇到 console.log(1) ,直接打印 1
// 遇到定时器,属于新的宏任务,留着后面执行
// 遇到 new Promise,这个是直接执行的,打印 'new Promise'
// .then 属于微任务,放入微任务队列,后面再执行
// 遇到 console.log(3) 直接打印 3
// 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'
// 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2
三、async与await
async
是异步的意思,await
则可以理解为 async wait
。所以可以理解async
就是用来声明一个异步方法,而 await
是用来等待异步方法执行
async
async
函数返回一个promise
对象,下面两种方法是等效的
function f() {
return Promise.resolve('TEST');
}
// asyncF is equivalent to f!
async function asyncF() {
return 'TEST';
}
await
正常情况下,await
命令后面是一个 Promise
对象,返回该对象的结果。如果不是 Promise
对象,就直接返回对应的值
async function f(){
// 等同于
// return 123
return await 123
}
f().then(v => console.log(v)) // 123
不管await
后面跟着的是什么,await
都会阻塞后面的代码
async function fn1 (){
console.log(1)
await fn2()
console.log(2) // 阻塞
}
async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)
上面的例子中,await
会阻塞下面的代码(即加入微任务队列),先执行 async
外面的同步代码,同步代码执行完,再回到 async
函数中,再执行之前阻塞的代码
所以上述输出结果为:1
,fn2
,3
,2
四、流程分析
通过对上面的了解,我们对JavaScript
对各种场景的执行顺序有了大致的了解
这里直接上代码:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('settimeout')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
分析过程:
- 执行整段代码,遇到
console.log('script start')
直接打印结果,输出script start
- 遇到定时器了,它是宏任务,先放着不执行
- 遇到
async1()
,执行async1
函数,先打印async1 start
,下面遇到await
怎么办?先执行async2
,打印async2
,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码 - 跳到
new Promise
这里,直接执行,打印promise1
,下面遇到.then()
,它是微任务,放到微任务列表等待执行 - 最后一行直接打印
script end
,现在同步代码执行完了,开始执行微任务,即await
下面的代码,打印async1 end
- 继续执行下一个微任务,即执行
then
的回调,打印promise2
- 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印
settimeout
所以最后的结果是:script start
、async1 start
、async2
、promise1
、script end
、async1 end
、promise2
、settimeout
转载自:https://juejin.cn/post/7064834943951568926