1.介紹一下JS的數(shù)據類型有那些,值是如何存儲的?
JavaScript一共有8種數(shù)據類型,其中有7中基本數(shù)據類型:Undefined、Null、Boolean、Number、String、Symbol(ES6新增,表示獨一無二的值)和BigInt(ES10新增);
1種引用數(shù)據類型——Object(Object本質上是由一組無序的名值隊組成)。里面包含function、Array、Date等。JavaScript不支持任何創(chuàng)建自定義類型的機制,而所有值最終都將是上述8種數(shù)據類型之一。
原始數(shù)據類型:直接存儲在棧(stack)中,占據空間小、大小固定,屬于頻繁使用數(shù)據,所以放入棧中存儲。
引用數(shù)據類型:同事存儲在棧(stack)堆(heap)中,占據空間大、大小不固定。引用數(shù)據類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。當解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址后從堆中獲取實體。
2.JavaScript的作用域和作用域鏈
作用域:作用域是定義變量的區(qū)域,它有一套訪問變量的規(guī)則,這套規(guī)則來管理瀏覽器引擎如何在當前作用域以及嵌套的作用域中根據變量(標識符)進行變量查找。
作用域鏈:作用域鏈的作用是保證對執(zhí)行環(huán)境有權訪問的所有變量和函數(shù)的有序訪問,通過作用域鏈,我們可以訪問到外層環(huán)境的變量和函數(shù)。
作用域鏈的本質上是一個指向變量對象的指針列表。變量對象是一個包含了執(zhí)行環(huán)境中所有變量和函數(shù)的對象。作用域鏈的前端始終都是當前執(zhí)行上下文的變量對象。全局執(zhí)行上下文的變量對象(也就是全局對象)始終是作用域鏈的最后一個對象。
當我們查找一個變量時,如果當前執(zhí)行環(huán)境中沒有找到,我們可以沿著作用域鏈向后查找。
作用域鏈的創(chuàng)建過程跟執(zhí)行上下文的建立有關....
3.談談你對this、call、apply和bind的理解
1.在瀏覽器里,在全局范圍內this指向Windows對象;
2.在函數(shù)中,this永遠指向最后調用他的那個對象;
3.構造函數(shù)中,this指向new出現(xiàn)的那個新的對象;
4.call、apply、bind中的this被強綁定在指定的那個對象上;
5.箭頭函數(shù)中this比較特殊,箭頭函數(shù)this為父作用域的this,不是調用時的this要知道前四種方式都是調用時確定,也就是動態(tài)的,而箭頭函數(shù)的this指向是靜態(tài)的,生命的時候就確定了下來;
6.apply、call、bind都是js給函數(shù)內置的一些API,調用他們可以為函數(shù)指定this的執(zhí)行,同時也可以傳參。
4.什么是閉包,為什么要用它?
閉包是是有權訪問另一個函數(shù)作用域內變量的函數(shù)。創(chuàng)建閉包的最常見的方式就是在一個函數(shù)內創(chuàng)建另一個函數(shù),創(chuàng)建的函數(shù)可以訪問到當前函數(shù)的局部變量。
閉包有兩個常用的用途:
1.閉包的第一個用途是使我們在函數(shù)外部能訪問到函數(shù)內部的變量。通過使用閉包,我們可以通過外部調用閉包函數(shù),從而在外部訪問到函數(shù)內部的變量,可以使用這種方法來創(chuàng)建私有變量。
2.函數(shù)的另一個用途是使已經運行結束的函數(shù)上下文中的變量對象繼續(xù)留在內存中,因為閉包函數(shù)保留了這個變量對象的引用,所以這個變量對象不會被回收。
function a(){
var n = 0;
function add(){
n++;
console.log(n);
}
return add;
}
var a1 = a(); //注意,函數(shù)名只是一個標識(指向函數(shù)的指針),而()才是執(zhí)行函數(shù);
a1(); //1
a1(); //2 第二次調用n變量還在內存中
其實閉包的本質就是作用域鏈的一個特殊的應用,只要了解了作用域鏈的創(chuàng)建過程,就能夠理解閉包的實現(xiàn)原理。
5.Ajax是什么?如何創(chuàng)建一個Ajax?
它是一種異步通信的方法,通過直接由 js 腳本向服務器發(fā)起 http 通信,然后根據服務器返回的數(shù)據,更新網頁的相應部分,而不用刷新整個頁面的一種方法。
原生:
//1:創(chuàng)建Ajax對象
var xhr = window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');// 兼容IE6及以下版本
//2:配置 Ajax請求地址
xhr.open('get','index.xml',true);
//3:發(fā)送請求
xhr.send(null); // 嚴謹寫法
//4:監(jiān)聽請求,接受響應
xhr.onreadysatechange=function(){
if(xhr.readySate==4&&xhr.status==200 || xhr.status==304 )
console.log(xhr.responsetXML)
}
JQuery:
$.ajax({
type:'post',
url:'',
async:ture,//async 異步 sync 同步
data:data,//針對post請求
dataType:'jsonp',
success:function (msg) {
},
error:function (error) {
}
})
promise 封裝實現(xiàn)
// promise 封裝實現(xiàn):
function getJSON(url) {
// 創(chuàng)建一個 promise 對象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// 新建一個 http 請求
xhr.open("GET", url, true);
// 設置狀態(tài)的監(jiān)聽函數(shù)
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 當請求成功或失敗時,改變 promise 的狀態(tài)
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
// 設置錯誤監(jiān)聽函數(shù)
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 設置響應的數(shù)據類型
xhr.responseType = "json";
// 設置請求頭信息
xhr.setRequestHeader("Accept", "application/json");
// 發(fā)送 http 請求
xhr.send(null);
});
return promise;
}
6.簡單介紹一下 V8 引擎的垃圾回收機制
v8 的垃圾回收機制基于分代回收機制,這個機制又基于世代假說,這個假說有兩個特點,一是新生的對象容易早死,另一個是不死的對象會活得更久?;谶@個假說,v8 引擎將內存分為了新生代和老生代。
新創(chuàng)建的對象或者只經歷過一次的垃圾回收的對象被稱為新生代。經歷過多次垃圾回收的對象被稱為老生代。
新生代被分為 From 和 To 兩個空間,To 一般是閑置的。當 From 空間滿了的時候會執(zhí)行 Scavenge 算法進行垃圾回收。當我們執(zhí)行垃圾回收算法的時候應用邏輯將會停止,等垃圾回收結束后再繼續(xù)執(zhí)行。這個算法分為三步:
(1)首先檢查 From 空間的存活對象,如果對象存活則判斷對象是否滿足晉升到老生代的條件,如果滿足條件則晉升到老生代。如果不滿足條件則移動 To 空間。
(2)如果對象不存活,則釋放對象的空間。
(3)最后將 From 空間和 To 空間角色進行交換。
新生代對象晉升到老生代有兩個條件:
(1)第一個是判斷是對象否已經經過一次 Scavenge 回收。若經歷過,則將對象從 From 空間復制到老生代中;若沒有經歷,則復制到 To 空間。
(2)第二個是 To 空間的內存使用占比是否超過限制。當對象從 From 空間復制到 To 空間時,若 To 空間使用超過 25%,則對象直接晉升到老生代中。設置 25% 的原因主要是因為算法結束后,兩個空間結束后會交換位置,如果 To 空間的內存太小,會影響后續(xù)的內存分配。
老生代采用了標記清除法和標記壓縮法。標記清除法首先會對內存中存活的對象進行標記,標記結束后清除掉那些沒有標記的對象。由于標記清除后會造成很多的內存碎片,不便于后面的內存分配。所以了解決內存碎片的問題引入了標記壓縮法。
由于在進行垃圾回收的時候會暫停應用的邏輯,對于新生代方法由于內存小,每次停頓的時間不會太長,但對于老生代來說每次垃圾回收的時間長,停頓會造成很大的影響。 為了解決這個問題 V8 引入了增量標記的方法,將一次停頓進行的過程分為了多步,每次執(zhí)行完一小步就讓運行邏輯執(zhí)行一會,就這樣交替運行。
7.哪些操作會造成內存泄漏?
1.意外的全局變量
2.被遺忘的計時器或回調函數(shù)
3.脫離 DOM 的引用
4.閉包
第一種情況是我們由于使用未聲明的變量,而意外的創(chuàng)建了一個全局變量,而使這個變量一直留在內存中無法被回收。
第二種情況是我們設置了setInterval定時器,而忘記取消它,如果循環(huán)函數(shù)有對外部變量的引用的話,那么這個變量會被一直留在內存中,而無法被回收。
第三種情況是我們獲取一個DOM元素的引用,而后面這個元素被刪除,由于我們一直保留了對這個元素的引用,所以它也無法被回收。
第四種情況是不合理的使用閉包,從而導致某些變量一直被留在內存當中。
8.var、let和const的區(qū)別是什么?
var聲明的變量會掛載在window上,而let和const聲明的變量不會:
var a = 100;
console.log(a,window.a); // 100 100
let b = 10;
console.log(b,window.b); // 10 undefined
const c = 1;
console.log(c,window.c); // 1 undefined
var聲明變量存在變量提升,let和const不存在變量提升:
console.log(a); // undefined ===> a已聲明還沒賦值,默認得到undefined值
var a = 100;
console.log(b); // 報錯:b is not defined ===> 找不到b這個變量
let b = 10;
console.log(c); // 報錯:c is not defined ===> 找不到c這個變量
const c = 10;
let和const聲明形成塊作用域
if(1){
var a = 100;
let b = 10;
}
console.log(a); // 100
console.log(b) // 報錯:b is not defined ===> 找不到b這個變量
-------------------------------------------------------------
if(1){
var a = 100;
const c = 1;
}
console.log(a); // 100
console.log(c) // 報錯:c is not defined ===> 找不到c這個變量
同一作用域下let和const不能聲明同名變量,而var可以
var a = 100;
console.log(a); // 100
var a = 10;
console.log(a); // 10
-------------------------------------
let a = 100;
let a = 10;
// 控制臺報錯:Identifier 'a' has already been declared ===> 標識符a已經被聲明了。
暫存死區(qū)
var a = 100;
if(1){
a = 10;
//在當前塊作用域中存在a使用let/const聲明的情況下,給a賦值10時,只會在當前作用域找變量a,
// 而這時,還未到聲明時候,所以控制臺Error:a is not defined
let a = 1;
}
const
/*
* 1、一旦聲明必須賦值,不能使用null占位。
*
* 2、聲明后不能再修改
*
* 3、如果聲明的是復合類型數(shù)據,可以修改其屬性
*
* */
const a = 100;
const list = [];
list[0] = 10;
console.log(list); // [10]
const obj = {a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj); // {a:10000,name:'apple'}
9.什么是箭頭函數(shù)?
箭頭函數(shù)表達式的語法比函數(shù)表達式更簡潔,并且沒有自己的this,arguments,super或new.target。箭頭函數(shù)表達式更適用于那些本來需要匿名函數(shù)的地方,并且它不能用作構造函數(shù)。
var getCurrentDate = function (){
return new Date();
}
//ES6 Version
const getCurrentDate = () => new Date();
在本例中,ES5 版本中有function(){}聲明和return關鍵字,這兩個關鍵字分別是創(chuàng)建函數(shù)和返回值所需要的。在箭頭函數(shù)版本中,我們只需要()括號,不需要 return 語句,因為如果我們只有一個表達式或值需要返回,箭頭函數(shù)就會有一個隱式的返回。
//ES5 Version
function greet(name) {
return 'Hello ' + name + '!';
}
//ES6 Version
const greet = (name) => `Hello ${name}`;
const greet2 = name => `Hello ${name}`;
我們還可以在箭頭函數(shù)中使用與函數(shù)表達式和函數(shù)聲明相同的參數(shù)。如果我們在一個箭頭函數(shù)中有一個參數(shù),則可以省略括號。
const getArgs = () => arguments
const getArgs2 = (...rest) => rest
箭頭函數(shù)不能訪問arguments對象。所以調用第一個getArgs函數(shù)會拋出一個錯誤。相反,我們可以使用rest參數(shù)來獲得在箭頭函數(shù)中傳遞的所有參數(shù)。
const data = {
result: 0,
nums: [1, 2, 3, 4, 5],
computeResult() {
// 這里的“this”指的是“data”對象
const addAll = () => {
return this.nums.reduce((total, cur) => total + cur, 0)
};
this.result = addAll();
}
};
箭頭函數(shù)沒有自己的this值。它捕獲詞法作用域函數(shù)的this值,在此示例中,addAll函數(shù)將復制computeResult 方法中的this值,如果我們在全局作用域聲明箭頭函數(shù),則this值為 window 對象。
10. js的深淺拷貝
JavaScript的深淺拷貝一直是個難點,如果現(xiàn)在面試官讓我寫一個深拷貝,我可能也只是能寫出個基礎版的。所以在寫這條之前我拜讀了收藏夾里各路大佬寫的博文。具體可以看下面我貼的鏈接,這里只做簡單的總結。
淺拷貝:創(chuàng)建一個新對象,這個對象有著原始對象屬性值的一份精確拷貝。如果屬性是基本類型,拷貝的就是基本類型的值,如果屬性是引用類型,拷貝的就是內存地址 ,所以如果其中一個對象改變了這個地址,就會影響到另一個對象。
深拷貝:將一個對象從內存中完整的拷貝一份出來,從堆內存中開辟一個新的區(qū)域存放新對象,且修改新對象不會影響原對象。
淺拷貝的實現(xiàn)方式:
Object.assign()方法: 用于將所有可枚舉屬性的值從一個或多個源對象復制到目標對象。它將返回目標對象。
** Array.prototype.slice():slice() 方法**:返回一個新的數(shù)組對象,這一對象是一個由 begin和end(不包括end)決定的原數(shù)組的淺拷貝。原始數(shù)組不會被改變。
拓展運算符 ...:
let a = {
name: "Jake",
flag: {
title: "better day by day",
time: "2020-05-31"
}
}
let b = {...a};
深拷貝的實現(xiàn)方式
乞丐版: JSON.parse(JSON.stringify(object)),缺點諸多(會忽略undefined、symbol、函數(shù);不能解決循環(huán)引用;不能處理正則、new Date())
基礎版: 淺拷貝+遞歸 (只考慮了普通的 object和 array兩種數(shù)據類型)
function cloneDeep(target,map = new WeakMap()) {
if(typeOf taret ==='object'){
let cloneTarget = Array.isArray(target) ? [] : {};
if(map.get(target)) {
return target;
}
map.set(target, cloneTarget);
for(const key in target){
cloneTarget[key] = cloneDeep(target[key], map);
}
return cloneTarget
}else{
return target
}
}
終極版
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
function getType(target) {
return Object.prototype.toString.call(target);
}
function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}
function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}
function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}
function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
case funcTag:
return cloneFunction(targe);
default:
return null;
}
}
function clone(target, map = new WeakMap()) {
// 克隆原始類型
if (!isObject(target)) {
return target;
}
// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
} else {
return cloneOtherType(target, type);
}
// 防止循環(huán)引用
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value, map));
});
return cloneTarget;
}
// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value, map));
});
return cloneTarget;
}
// 克隆對象和數(shù)組
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});
return cloneTarget;
}
module.exports = {
clone
};
11.什么是回調函數(shù)?回調函數(shù)有什么缺點
回調函數(shù)是一段可執(zhí)行的代碼段,它作為一個參數(shù)傳遞給其他的代碼,其作用是在需要的時候方便調用這段(回調函數(shù))代碼。
在JavaScript中函數(shù)也是對象的一種,同樣對象可以作為參數(shù)傳遞給函數(shù),因此函數(shù)也可以作為參數(shù)傳遞給另外一個函數(shù),這個作為參數(shù)的函數(shù)就是回調函數(shù)。
const btnAdd = document.getElementById('btnAdd');
btnAdd.addEventListener('click', function clickCallback(e) {
// do something useless
});
在本例中,我們等待id為btnAdd的元素中的click事件,如果它被單擊,則執(zhí)行clickCallback函數(shù)。回調函數(shù)向某些數(shù)據或事件添加一些功能。
回調函數(shù)有一個致命的弱點,就是容易寫出回調地獄(Callback hell)。假設多個事件存在依賴性:
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
},3000)
},2000)
},1000)
這就是典型的回調地獄,以上代碼看起來不利于閱讀和維護,事件一旦多起來就更是亂糟糟,所以在es6中提出了Promise和async/await來解決回調地獄的問題。當然,回調函數(shù)還存在著別的幾個缺點,比如不能使用 try catch 捕獲錯誤,不能直接 return。
12.Promise是什么,可以手寫實現(xiàn)一下嗎?
Promise,翻譯過來是承諾,承諾它過一段時間會給你一個結果。從編程講Promise 是異步編程的一種解決方案。下面是Promise在MDN的相關說明:
Promise 對象是一個代理對象(代理一個值),被代理的值在Promise對象創(chuàng)建時可能是未知的。它允許你為異步操作的成功和失敗分別綁定相應的處理方法(handlers)。這讓異步方法可以像同步方法那樣返回值,但并不是立即返回最終執(zhí)行結果,而是一個能代表未來出現(xiàn)的結果的promise對象。
一個 Promise有以下幾種狀態(tài):
pending: 初始狀態(tài),既不是成功,也不是失敗狀態(tài)。
fulfilled: 意味著操作成功完成。
rejected: 意味著操作失敗。
這個承諾一旦從等待狀態(tài)變成為其他狀態(tài)就永遠不能更改狀態(tài)了,也就是說一旦狀態(tài)變?yōu)?fulfilled/rejected 后,就不能再次改變??赡芄饪锤拍畲蠹也焕斫釶romise,我們舉個簡單的栗子;
假如我有個女朋友,下周一是她生日,我答應她生日給她一個驚喜,那么從現(xiàn)在開始這個承諾就進入等待狀態(tài),等待下周一的到來,然后狀態(tài)改變。如果下周一我如約給了女朋友驚喜,那么這個承諾的狀態(tài)就會由pending切換為fulfilled,表示承諾成功兌現(xiàn),一旦是這個結果了,就不會再有其他結果,即狀態(tài)不會在發(fā)生改變;反之如果當天我因為工作太忙加班,把這事給忘了,說好的驚喜沒有兌現(xiàn),狀態(tài)就會由pending切換為rejected,時間不可倒流,所以狀態(tài)也不能再發(fā)生變化。
上一條我們說過Promise可以解決回調地獄的問題,沒錯,pending 狀態(tài)的 Promise 對象會觸發(fā) fulfilled/rejected 狀態(tài),一旦狀態(tài)改變,Promise 對象的 then 方法就會被調用;否則就會觸發(fā) catch。我們將上一條回調地獄的代碼改寫一下:
new Promise((resolve,reject) => {
setTimeout(() => {
console.log(1)
resolve()
},1000)
}).then((res) => {
setTimeout(() => {
console.log(2)
},2000)
}).then((res) => {
setTimeout(() => {
console.log(3)
},3000)
}).catch((err) => {
console.log(err)
})
其實Promise也是存在一些缺點的,比如無法取消 Promise,錯誤需要通過回調函數(shù)捕獲。
promise手寫實現(xiàn),面試夠用版:
function myPromise(constructor){
let self=this;
self.status="pending" //定義狀態(tài)改變前的初始狀態(tài)
self.value=undefined;//定義狀態(tài)為resolved的時候的狀態(tài)
self.reason=undefined;//定義狀態(tài)為rejected的時候的狀態(tài)
function resolve(value){
//兩個==="pending",保證了狀態(tài)的改變是不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//兩個==="pending",保證了狀態(tài)的改變是不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕獲構造異常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
// 定義鏈式調用的then方法
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
13.什么是 async/await 及其如何工作,有什么優(yōu)缺點?
async/await是一種建立在Promise之上的編寫異步或非阻塞代碼的新方法,被普遍認為是 JS異步操作的最終且最優(yōu)雅的解決方案。相對于 Promise 和回調,它的可讀性和簡潔度都更高。畢竟一直then()也很煩。
async 是異步的意思,而 await 是 async wait的簡寫,即異步等待。
所以從語義上就很好理解 async 用于聲明一個 function 是異步的,而await 用于等待一個異步方法執(zhí)行完成。
一個函數(shù)如果加上 async ,那么該函數(shù)就會返回一個 Promise
async function test() {
return "1"
}
console.log(test()) // -> Promise {<resolved>: "1"}
可以看到輸出的是一個Promise對象。所以,async 函數(shù)返回的是一個 Promise 對象,如果在 async 函數(shù)中直接 return 一個直接量,async 會把這個直接量通過 PromIse.resolve()封裝成Promise對象返回。
相比于Promise,async/await能更好地處理 then 鏈
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
現(xiàn)在分別用 Promise 和async/await來實現(xiàn)這三個步驟的處理。
使用Promise
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
});
}
doIt();
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
使用async/await
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
}
doIt();
結果和之前的 Promise 實現(xiàn)是一樣的,但是這個代碼看起來是不是清晰得多,優(yōu)雅整潔,幾乎跟同步代碼一樣。
await關鍵字只能在async function中使用。在任何非async function的函數(shù)中使用await關鍵字都會拋出錯誤。await關鍵字在執(zhí)行下一行代碼之前等待右側表達式(可能是一個Promise)返回。
優(yōu)缺點
async/await的優(yōu)勢在于處理 then 的調用鏈,能夠更清晰準確的寫出代碼,并且也能優(yōu)雅地解決回調地獄問題。當然也存在一些缺點,因為 await 將異步代碼改造成了同步代碼,如果多個異步代碼沒有依賴性卻使用了 await 會導致性能上的降低。
14.js 的節(jié)流與防抖
函數(shù)防抖 是指在事件被觸發(fā) n 秒后再執(zhí)行回調,如果在這 n 秒內事件又被觸發(fā),則重新計時。這可以使用在一些點擊請求的事件上,避免因為用戶的多次點擊向后端發(fā)送多次請求。
函數(shù)節(jié)流 是指規(guī)定一個單位時間,在這個單位時間內,只能有一次觸發(fā)事件的回調函數(shù)執(zhí)行,如果在同一個單位時間內某事件被觸發(fā)多次,只有一次能生效。節(jié)流可以使用在 scroll 函數(shù)的事件監(jiān)聽上,通過事件節(jié)流來降低事件調用的頻率。
// 函數(shù)防抖的實現(xiàn)
function debounce(fn, wait) {
var timer = null;
return function() {
var context = this,
args = arguments;
// 如果此時存在定時器的話,則取消之前的定時器重新記時
if (timer) {
clearTimeout(timer);
timer = null;
}
// 設置定時器,使事件間隔指定事件后執(zhí)行
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
}
// 函數(shù)節(jié)流的實現(xiàn);
function throttle(fn, delay) {
var preTime = Date.now();
return function() {
var context = this,
args = arguments,
nowTime = Date.now();
// 如果兩次時間間隔超過了指定時間,則執(zhí)行函數(shù)。
if (nowTime - preTime >= delay) {
preTime = Date.now();
return fn.apply(context, args);
}
};
}