JS
變量類型和計(jì)算
-
typeof
能判斷那些類型 - 何時(shí)使用 === 何時(shí)使用 ==
- 值類型和引用類型的區(qū)別
- 手寫深拷貝
值類型和引用類型
值類型存儲(chǔ)在棧內(nèi)存中诀紊,引用類型存儲(chǔ)在堆內(nèi)存中
// 值類型
let a = 100
let b = a
a = 200
console.log(b) // 100
// 引用類型
let a = { age: 20 }
let b = a
b.age = 21
console.log(a.age) // 21
常見值類型和引用類型
// 常見值類型
let u // undefined
const s = 'abc'
const n = 100
const b = true
const s = Symbol('s')
// 常見引用類型
const obj = { x: 100 }
const arr = ['a', 'b', 'c']
const n = null // 特殊引用類型,指針指向?yàn)榭盏刂?
// 特殊的引用類型隅俘,但不用于存儲(chǔ)數(shù)據(jù)邻奠,所以沒有拷貝、復(fù)制函數(shù)這一說
function fun() {}
類型判斷
typeof
運(yùn)算符
- 識(shí)別所以值類型
- 識(shí)別函數(shù)
- 判斷是否是引用類型(不可再細(xì)分)
深克隆
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj === null) {
// obj 是 null礼患,或是不是對(duì)象和數(shù)組愉舔,直接返回
return obj
}
// 初始化返回結(jié)果
let result
if (obj instanceof Array) {
result = []
} else {
result = {}
}
for (let key in obj) {
// 保證 key 不是原型的屬性
if (obj.hasOwnProperty(key)) {
// 遞歸
result[key] = deepClone(obj[key])
}
}
return result
}
類型轉(zhuǎn)換
- 字符串拼接
- ==
- if 語句邏輯運(yùn)算
const a = 100 + 10 // 110
const b = 100 + '10' // 10010
const c = true + '10' // true10
const d = 100 + parseInt('10') // 110
==
100 == '100' // true
0 == '0' // true
0 == false // true
false == '' // true
null == undefined // true
NaN == NaN // false
// 除了 == null 之外捏鱼,其他都一律用 === ,例如:
const obj = { x: 100 }
if (obj.a == null) {}
// 相當(dāng)于
if (obj.a === null || obj.a === undefined) {}
邏輯運(yùn)算
if 語句和邏輯運(yùn)算
- truly 變量:!!a === true 的變量
- falsely 變量:!!a === false 的變量
原型和原型鏈
- 如何判斷一個(gè)變量是不是數(shù)組贰镣?
- 手寫一個(gè)簡(jiǎn)易的 jQuery,考慮插件和擴(kuò)展性
- class 的原型本質(zhì)膳凝,怎么理解碑隆?
class
class Student {
constructor(name, number) {
this.name = name
this.number = number
}
greeting() {
console.log(`Hello, My name is ${this.name}, number is ${this.number}`)
}
}
// 實(shí)例化
const zhangsan = new Student('zhangsan', 1)
zhangsan.greeting() // Hello, My name is zhangsan, number is 1
繼承
// 父類
class Person {
constructor(name) {
this.name = name
}
eat() {
console.log(`${this.name} eat something`)
}
}
// 子類
class Student extends Person {
constructor(name, number) {
super(name)
this.number = number
}
greeting() {
console.log(`Hello, My name is ${this.name}, number is ${this.number}`)
}
}
// 子類
class Teacher extends Person {
constructor(name, subject) {
super(name)
this.subject = subject
}
teach() {
console.log(`My name is ${this.name}, and I am teaching ${this.subject}`)
}
}
// 實(shí)例化
const zhangsan = new Student('zhangsan', 1)
zhangsan.greeting() // Hello, My name is zhangsan, number is 1
zhangsan.eat() // zhangsan eat something
const mrWang = new Teacher('wang', 'Math')
mrWang.eat() // wang eat something
mrWang.teach() // My name is wang, and I am teaching Math
// 類型判斷
console.log(zhangsan instanceof Student) // true
console.log(zhangsan instanceof Person) // true
console.log(zhangsan instanceof Object) // true
// true
console.log([] instanceof Array)
console.log([] instanceof Object)
console.log({} instanceof Object)
原型
// class 實(shí)際上是函數(shù),語法糖而已
typeof Person // function
typeof Student // function
// 隱式原型和顯示原型
console.log(zhangsan.__proto__)
console.log(Student.prototype)
console.log(zhangsan.__proto__ === Student.prototype) // true
原型關(guān)系
- 每個(gè) class 都有顯示原型 prototype
- 每個(gè)實(shí)例都有隱式原型 __ proto__
- 實(shí)例的 __ proto__指向?qū)?yīng)的 class 的 prototype
原型鏈
console.log(Student.prototype.__proto__)
console.log(Person.prototype)
console.log(Person.prototype === Student.prototype.__proto__) // true
instanceof
手動(dòng)實(shí)現(xiàn) instanceof
function myInstanceof(left, right) {
// 獲取類的原型
let prototype = right.prototype
// 獲取對(duì)象的原型
left = left.__proto__
// 判斷對(duì)象的類型是否等于類型的原型
while (true) {
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
手寫一個(gè)簡(jiǎn)易的 jQuery蹬音,考慮插件和擴(kuò)展性
class jQuery {
constructor(selector) {
const result = document.querySelectorAll(selector)
const length = result.length
for (let i = 0; i < length; i++) {
this[i] = result[i]
}
this.length = length
this.selector = selector
}
get(index) {
return this[index]
}
each(fn) {
for (let i = 0; i < this.length; i ++) {
const elem = this[i]
fn(elem)
}
}
on(type, fn) {
return this.each(elem => {
elem.addEventListener(type, fn, false)
})
}
}
// 插件
jQuery.prototype.dialog = function (info) {
alert(info)
}
// “造輪子”
class myJQuery extends jQuery {
constructor(selector) {
super(selector)
}
// 擴(kuò)展自己的方法
addClass(className) {
// ...
}
style(data) {
// ...
}
}
作用域和閉包
- this 的不同應(yīng)用場(chǎng)景上煤,如何取值?
- 手寫 bind 函數(shù)
- 實(shí)際開發(fā)中閉包的應(yīng)用場(chǎng)景祟绊,舉例說明
- 創(chuàng)建 10 個(gè)
<a>
標(biāo)簽點(diǎn)擊的時(shí)候彈出對(duì)應(yīng)的序號(hào)
作用域
let a = 0
function fn1() {
let a1 = 100
function fn2() {
let a2 = 200
function fn3() {
let a3 = 300
return a + a1 + a2 + a3
}
fn3()
}
fn2()
}
fn1()
- 全局作用域
- 函數(shù)作用域
- 塊級(jí)作用域(ES6新增)
自由變量
- 一個(gè)變量在當(dāng)前作用域沒有定義楼入,但被使用了
- 向上級(jí)作用域哥捕,一層一層依次尋找,直到找到了為止
- 如果在全局作用域都沒有找到嘉熊,則報(bào)錯(cuò) xxx is not defined
閉包
- 作用域應(yīng)用的特殊情況遥赚,有兩種表現(xiàn)
- 函數(shù)作為參數(shù)被傳遞
- 函數(shù)作為值被返回
// 函數(shù)作為返回值
function create() {
let a = 100
return function () {
console.log(a)
}
}
let fn = create()
let a = 200
fn() // 100
// 函數(shù)作為參數(shù)
function print(fn) {
let a = 200
fn()
}
let a = 100
function fn() {
console.log(a)
}
print(fn) // 100
閉包:自由變量的查找,是在函數(shù)定義的地方阐肤,向上級(jí)作用域查找
不是在執(zhí)行的地方Y旆稹!孕惜!
this
- 作為普通函數(shù)
- 使用 call apply bind
- 作為對(duì)象方法被調(diào)用
- 在 class 方法中調(diào)用
- 箭頭函數(shù)
this 取什么值是在函數(shù)執(zhí)行的時(shí)候確定的愧薛,不是在定義的時(shí)候
function fn1() {
console.log(this)
}
fn1() // window
fn1.call({x: 100}) // {x: 100}
const fn2 = fn1.bind({x: 200})
fn2() // {x: 200}
箭頭函數(shù)
const zhangsan = {
name: 'zhangsan',
greeting() {
// this 即當(dāng)前對(duì)象
console.log(this)
},
wait() {
setTimeout(function() {
// this === window
console.log(this)
})
}
}
// 箭頭函數(shù)的 this 永遠(yuǎn)取上級(jí)作用域的 this
const zhangsan = {
name: 'zhangsan',
// this 即當(dāng)前對(duì)象
greeting() {
console.log(this)
},
wait() {
setTimeout(() => {
// this 即當(dāng)前對(duì)象
console.log(this)
})
}
}
創(chuàng)建 10 個(gè) <a>
標(biāo)簽點(diǎn)擊的時(shí)候彈出對(duì)應(yīng)的序號(hào)
let a
for (let i = 0; i < 10; i++) {
a = document.createElement('a')
a.innerHTML = i + '<br>'
a.addEventListener('click', function(e) {
e.preventDefault()
alert(i)
})
document.body.appendChild(a)
}
手寫 bind 函數(shù)
Function.prototype.myBind = function() {
// 將參數(shù)拆解為數(shù)組
const args = Array.prototype.slice.call(arguments)
// 獲取 this(數(shù)組第一項(xiàng))
const that = args.shift()
// fn.bind(...) 中的 fn
const self = this
// 返回一個(gè)函數(shù)
return function() {
return self.apply(that, args)
}
}
實(shí)際開發(fā)中閉包的應(yīng)用
隱藏?cái)?shù)據(jù)
如做一個(gè)簡(jiǎn)單的 cache 工具
function createCache() { const data = {} // 閉包中的數(shù)據(jù),被隱藏衫画,不被外界訪問 return { set(key, value) { data[key] = value }, get(key) { return data[key] } } } const c = createCache() c.set('a', 100) console.log(c.get('a'))
異步
同步和異步得區(qū)別是什么毫炉?
手寫 Promise 加載一張圖片
前端使用異步的場(chǎng)景
請(qǐng)描述 event loop (事件循環(huán)/事件輪詢)的機(jī)制,可畫圖
什么是宏認(rèn)為和微任務(wù)削罩,兩者有什么區(qū)別瞄勾?
Promise 有哪三種狀態(tài)?如何變化弥激?
Promise 的 then 和 catch 的連接問題
async/await 語法
Promise 和 setTimeout 的順序問題
單線程
- JS 是單線程語言进陡,只能同時(shí)做一件事
- 瀏覽器和 nodejs 已支持 JS 啟動(dòng)進(jìn)程,如 Web Worker
- JS 和 DOM 渲染共用同一個(gè)線程微服,因?yàn)?JS 可修改 DOM 結(jié)構(gòu)
- 異步基于 回調(diào) callback 函數(shù)形式
callback
// 異步
console.log(100)
setTimeout(function() {
console.log(200)
}, 1000)
console.log(300)
// 同步
console.log(100)
alert(200)
console.log(300)
異步不會(huì)阻塞代碼執(zhí)行
同步會(huì)阻塞代碼執(zhí)行
應(yīng)用場(chǎng)景
- 網(wǎng)絡(luò)請(qǐng)求趾疚,如 ajax 圖片加載
- 定時(shí)任務(wù),如 setTimeout
promise
基本使用
// 加載圖片
function loadImg(src) {
const p = new Promise(
(resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`圖片加載失敗 ${src}`)
reject(err)
}
img.src = src
}
)
return p
}
const url = 'www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png'
loadImg(url).then(img => {
console.log(img.width)
return img
}).then(img => {
console.log(img.height)
}).catch(ex => console.error(ex))
狀態(tài)
- 三種狀態(tài)
- pending
- resolved
- rejected
- 變化: pending => resolved 或 pending => rejected
- 變化是不可逆的
- 狀態(tài)的表現(xiàn)和變化
- pending 狀態(tài)以蕴,不會(huì)觸發(fā) then 和 catch
- resolved 狀態(tài)糙麦,會(huì)觸發(fā)后續(xù)的 then 回調(diào)函數(shù)
- rejected 狀態(tài),會(huì)觸發(fā)后續(xù)的 catch 回調(diào)函數(shù)
const p1 = new Promise((resolve, reject) => {})
console.log(p1) // pending
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
})
})
console.log(p2) // pending 一開始打印時(shí)
setTimeout(() => console.log(p2)) // resolved
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject()
})
})
console.log(p3) // pending 一開始打印時(shí)
setTimeout(() => console.log(p3)) // rejected
// 直接獲取一個(gè) resolved 狀態(tài)的 Promise
const resolved = Promise.resolve(100)
console.log(resolved) // resolved
// 直接獲取一個(gè) rejected 狀態(tài)的 Promise
const resolved = Promise.reject('err')
console.log(resolved) // rejected
- then 和 catch 對(duì)狀態(tài)的影響
- then 正常返回 resolved 舒裤,里面有報(bào)錯(cuò)則返回 rejected
- catch正常返回 resolved 喳资,里面有報(bào)錯(cuò)則返回 rejected
// then() 一般正常返回 resolved 狀態(tài)的 promise
Promise.resolve().then(() => {
return 100
})
// then() 里拋出錯(cuò)誤,會(huì)返回 rejected 狀態(tài)的 promise
Promise.resolve().then(() => {
throw new Error('err')
})
// catch() 不拋出錯(cuò)誤腾供,會(huì)返回 resolved 狀態(tài)的 promise
Promise.reject().catch(() => {
console.error('catch some error')
})
// catch() 拋出錯(cuò)誤仆邓,會(huì)返回 rejected 狀態(tài)的 promise
Promise.reject().catch(() => {
console.error('catch some error')
throw new Error('err')
})
題
// 第一題
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 第二題
Promise.resolve().then(() => { // 返回 rejected 狀態(tài)的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 狀態(tài)的 promise
console.log(2)
}).then(() => {
console.log(3)
})
// 第三題
Promise.resolve().then(() => { // 返回 rejected 狀態(tài)的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 狀態(tài)的 promise
console.log(2)
}).catch(() => {
console.log(3)
})
event-loop
- JS 是單線程運(yùn)行的
- 異步要基于回調(diào)來實(shí)現(xiàn)
- event loop 就是異步回調(diào)的實(shí)現(xiàn)原理
JS 如何執(zhí)行的
- 從前到后,一行一行執(zhí)行
- 如果某一行執(zhí)行報(bào)錯(cuò)伴鳖,則停止下面代碼的執(zhí)行
- 先把同步代碼執(zhí)行完节值,再執(zhí)行異步
event loop 執(zhí)行過程
- 同步代碼,一行一行放在 Call Stack 執(zhí)行
- 遇到異步榜聂,會(huì)先 “記錄” 下搞疗,等待時(shí)機(jī)(定時(shí),網(wǎng)絡(luò)請(qǐng)求)
- 時(shí)機(jī)到了须肆,就移動(dòng)到 Callback Queue
- 如果 Call Stack 為空(即同步代碼執(zhí)行完)Event Loop 開始工作
- 輪詢查找 Callback Queue 匿乃,如果有則移動(dòng)到 Call Stack 執(zhí)行
- 然后繼續(xù)輪詢查找(永動(dòng)機(jī)一樣)
DOM 事件和 event loop
- 異步(setTimeout桩皿,ajax 等)使用回調(diào),基于 event loop
- DOM 事件也使用回調(diào)幢炸,基于 event loop
async/await
- 異步回調(diào) callback hell
- Promise 基于 then catch 鏈?zhǔn)秸{(diào)用泄隔,但也是基于回調(diào)函數(shù)
- async/await 是同步語法,徹底消滅回調(diào)函數(shù)
有很多 async 的面試題宛徊,例如
- async 直接返回佛嬉,是什么
- async 直接返回 promise
- await 后面不加 promise
基本語法
function loadImg(src) {
const promise = new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
reject(new Error(`圖片加載失敗 ${src}`))
}
img.src = src
})
return promise
}
async function loadImg1() {
const src1 = 'www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png'
const img1 = await loadImg(src1)
return img1
}
async function loadImg2() {
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
const img2 = await loadImg(src2)
return img2
}
(async function () {
// 注意:await 必須放在 async 函數(shù)中,否則會(huì)報(bào)錯(cuò)
try {
// 加載第一張圖片
const img1 = await loadImg1()
console.log(img1)
// 加載第二張圖片
const img2 = await loadImg2()
console.log(img2)
} catch (ex) {
console.error(ex)
}
})()
async/await 和 promise 的關(guān)系
- async/await 是消滅異步回調(diào)的終極武器
- 但和 promise 并不互斥
- 兩者反而相輔相成
- await 相當(dāng)于 promise 的 then
- try...catch 可捕獲異常闸天,代替了 promise 的 catch
- async 函數(shù)返回結(jié)果都是 Promise 對(duì)象(如果函數(shù)內(nèi)沒返回 Promise 暖呕,則自動(dòng)封裝一下)
async function fn2() {
return new Promise(() => {})
}
console.log( fn2() )
async function fn1() {
return 100
}
console.log( fn1() ) // 相當(dāng)于 Promise.resolve(100)
- await 后面跟 Promise 對(duì)象:會(huì)阻斷后續(xù)代碼,等待狀態(tài)變?yōu)?resolved 苞氮,才獲取結(jié)果并繼續(xù)執(zhí)行
- await 后續(xù)跟非 Promise 對(duì)象:會(huì)直接返回
(async function () {
const p1 = new Promise(() => {})
await p1
console.log('p1') // 不會(huì)執(zhí)行
})()
(async function () {
const p2 = Promise.resolve(100)
const res = await p2
console.log(res) // 100
})()
(async function () {
const res = await 100
console.log(res) // 100
})()
(async function () {
const p3 = Promise.reject('some err')
const res = await p3
console.log(res) // 不會(huì)執(zhí)行
})()
- try...catch 捕獲 rejected 狀態(tài)
(async function () {
const p4 = Promise.reject('some err')
try {
const res = await p4
console.log(res)
} catch (ex) {
console.error(ex)
}
})()
總結(jié)來看:
- async 封裝 Promise
- await 處理 Promise 成功
- try...catch 處理 Promise 失敗
異步本質(zhì)
await 是同步寫法湾揽,但本質(zhì)還是異步調(diào)用。
async function async1 () {
console.log('async1 start')
await async2()
console.log('async1 end') // 關(guān)鍵在這一步笼吟,它相當(dāng)于放在 callback 中钝腺,最后執(zhí)行
}
async function async2 () {
console.log('async2')
}
console.log('script start')
async1()
console.log('script end')
即,只要遇到了 await
赞厕,后面的代碼都相當(dāng)于放在 callback 里。
for...of
- for ... in (以及 forEach for)是常規(guī)的同步遍歷
- for ... of 常用與異步的循環(huán)
// 定時(shí)算乘法
function multi(num) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(num * num)
}, 1000)
})
}
// // 使用 forEach 定硝,是 1s 之后打印出所有結(jié)果皿桑,即 3 個(gè)值是一起被計(jì)算出來的
// function test1 () {
// const nums = [1, 2, 3];
// nums.forEach(async x => {
// const res = await multi(x);
// console.log(res);
// })
// }
// test1();
// 使用 for...of ,可以讓計(jì)算挨個(gè)串行執(zhí)行
async function test2 () {
const nums = [1, 2, 3];
for (let x of nums) {
// 在 for...of 循環(huán)體的內(nèi)部蔬啡,遇到 await 會(huì)挨個(gè)串行計(jì)算
const res = await multi(x)
console.log(res)
}
}
test2()
微任務(wù)/宏任務(wù)
- 宏任務(wù):setTimeout setInterval DOM 事件
- 微任務(wù):Promise(對(duì)于前端來說)
- 微任務(wù)比宏任務(wù)執(zhí)行的更早
console.log(100)
setTimeout(() => {
console.log(200)
})
Promise.resolve().then(() => {
console.log(300)
})
console.log(400)
// 100 400 300 200
event loop 和 DOM 渲染
- 每一次 call stack 結(jié)束诲侮,都會(huì)觸發(fā) DOM 渲染(不一定非得渲染,就是給一次 DOM 渲染的機(jī)會(huì)O潴 )
- 然后再進(jìn)行 event loop
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)
console.log('length', $('#container').children().length )
alert('本次 call stack 結(jié)束沟绪,DOM 結(jié)構(gòu)已更新,但尚未觸發(fā)渲染')
// (alert 會(huì)阻斷 js 執(zhí)行空猜,也會(huì)阻斷 DOM 渲染绽慈,便于查看效果)
// 到此,即本次 call stack 結(jié)束后(同步任務(wù)都執(zhí)行完了)辈毯,瀏覽器會(huì)自動(dòng)觸發(fā)渲染坝疼,不用代碼干預(yù)
// 另外,按照 event loop 觸發(fā) DOM 渲染時(shí)機(jī)谆沃,setTimeout 時(shí) alert 钝凶,就能看到 DOM 渲染后的結(jié)果了
setTimeout(function () {
alert('setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出來的結(jié)果了')
})
宏任務(wù)和微任務(wù)的區(qū)別
- 宏任務(wù):DOM 渲染后再觸發(fā)
- 微任務(wù):DOM 渲染前會(huì)觸發(fā)
// 修改 DOM
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)
// // 微任務(wù):渲染之前執(zhí)行(DOM 結(jié)構(gòu)已更新)
// Promise.resolve().then(() => {
// const length = $('#container').children().length
// alert(`micro task ${length}`)
// })
// 宏任務(wù):渲染之后執(zhí)行(DOM 結(jié)構(gòu)已更新)
setTimeout(() => {
const length = $('#container').children().length
alert(`macro task ${length}`)
})
再深入思考一下:為何兩者會(huì)有以上區(qū)別唁影,一個(gè)在渲染前耕陷,一個(gè)在渲染后掂名?
- 微任務(wù):ES 語法標(biāo)準(zhǔn)之內(nèi),JS 引擎來統(tǒng)一處理哟沫。即饺蔑,不用瀏覽器有任何關(guān)于,即可一次性處理完南用,更快更及時(shí)膀钠。
- 宏任務(wù):ES 語法沒有,JS 引擎不處理裹虫,瀏覽器(或 nodejs)干預(yù)處理肿嘲。
經(jīng)典面試題
async function async1 () {
console.log('async1 start')
await async2() // 這一句會(huì)同步執(zhí)行,返回 Promise 筑公,其中的 `console.log('async2')` 也會(huì)同步執(zhí)行
console.log('async1 end') // 上面有 await 雳窟,下面就變成了“異步”,類似 cakkback 的功能(微任務(wù))
}
async function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(function () { // 異步匣屡,宏任務(wù)
console.log('setTimeout')
}, 0)
async1()
new Promise (function (resolve) { // 返回 Promise 之后封救,即同步執(zhí)行完成,then 是異步代碼
console.log('promise1') // Promise 的函數(shù)體會(huì)立刻執(zhí)行
resolve()
}).then (function () { // 異步捣作,微任務(wù)
console.log('promise2')
})
console.log('script end')
// 同步代碼執(zhí)行完之后誉结,屢一下現(xiàn)有的異步未執(zhí)行的,按照順序
// 1. async1 函數(shù)中 await 后面的內(nèi)容 —— 微任務(wù)
// 2. setTimeout —— 宏任務(wù)
// 3. then —— 微任務(wù)
模塊化
ES6 Module
略