1.介紹一下JS的數(shù)據(jù)類型有那些,值是如何存儲的?
JavaScript一共有8種數(shù)據(jù)類型登钥,其中有7中基本數(shù)據(jù)類型:Undefined、Null娶靡、Boolean牧牢、Number、String姿锭、Symbol(ES6新增塔鳍,表示獨一無二的值)和BigInt(ES10新增);
1種引用數(shù)據(jù)類型——Object(Object本質(zhì)上是由一組無序的名值隊組成)呻此。里面包含function轮纫、Array、Date等趾诗。JavaScript不支持任何創(chuàng)建自定義類型的機制蜡感,而所有值最終都將是上述8種數(shù)據(jù)類型之一蹬蚁。
原始數(shù)據(jù)類型:直接存儲在棧(stack)中,占據(jù)空間小郑兴、大小固定犀斋,屬于頻繁使用數(shù)據(jù),所以放入棧中存儲情连。
引用數(shù)據(jù)類型:同事存儲在棧(stack)堆(heap)中叽粹,占據(jù)空間大、大小不固定却舀。引用數(shù)據(jù)類型在棧中存儲了指針虫几,該指針指向堆中該實體的起始地址。當解釋器尋找引用值時挽拔,會首先檢索其在棧中的地址辆脸,取得地址后從堆中獲取實體。
2.JavaScript的作用域和作用域鏈
作用域:作用域是定義變量的區(qū)域螃诅,它有一套訪問變量的規(guī)則啡氢,這套規(guī)則來管理瀏覽器引擎如何在當前作用域以及嵌套的作用域中根據(jù)變量(標識符)進行變量查找。
作用域鏈:作用域鏈的作用是保證對執(zhí)行環(huán)境有權(quán)訪問的所有變量和函數(shù)的有序訪問术裸,通過作用域鏈倘是,我們可以訪問到外層環(huán)境的變量和函數(shù)。
作用域鏈的本質(zhì)上是一個指向變量對象的指針列表袭艺。變量對象是一個包含了執(zhí)行環(huán)境中所有變量和函數(shù)的對象搀崭。作用域鏈的前端始終都是當前執(zhí)行上下文的變量對象。全局執(zhí)行上下文的變量對象(也就是全局對象)始終是作用域鏈的最后一個對象猾编。
當我們查找一個變量時瘤睹,如果當前執(zhí)行環(huán)境中沒有找到,我們可以沿著作用域鏈向后查找袍镀。
作用域鏈的創(chuàng)建過程跟執(zhí)行上下文的建立有關(guān)....
3.談?wù)勀銓his默蚌、call、apply和bind的理解
1.在瀏覽器里苇羡,在全局范圍內(nèi)this指向Windows對象绸吸;
2.在函數(shù)中,this永遠指向最后調(diào)用他的那個對象设江;
3.構(gòu)造函數(shù)中锦茁,this指向new出現(xiàn)的那個新的對象;
4.call叉存、apply码俩、bind中的this被強綁定在指定的那個對象上;
5.箭頭函數(shù)中this比較特殊歼捏,箭頭函數(shù)this為父作用域的this稿存,不是調(diào)用時的this要知道前四種方式都是調(diào)用時確定笨篷,也就是動態(tài)的,而箭頭函數(shù)的this指向是靜態(tài)的瓣履,生命的時候就確定了下來率翅;
6.apply、call袖迎、bind都是js給函數(shù)內(nèi)置的一些API冕臭,調(diào)用他們可以為函數(shù)指定this的執(zhí)行,同時也可以傳參燕锥。
4.什么是閉包辜贵,為什么要用它?
閉包是是有權(quán)訪問另一個函數(shù)作用域內(nèi)變量的函數(shù)归形。創(chuàng)建閉包的最常見的方式就是在一個函數(shù)內(nèi)創(chuàng)建另一個函數(shù)托慨,創(chuàng)建的函數(shù)可以訪問到當前函數(shù)的局部變量。
閉包有兩個常用的用途:
1.閉包的第一個用途是使我們在函數(shù)外部能訪問到函數(shù)內(nèi)部的變量暇榴。通過使用閉包榴芳,我們可以通過外部調(diào)用閉包函數(shù),從而在外部訪問到函數(shù)內(nèi)部的變量跺撼,可以使用這種方法來創(chuàng)建私有變量。
2.函數(shù)的另一個用途是使已經(jīng)運行結(jié)束的函數(shù)上下文中的變量對象繼續(xù)留在內(nèi)存中讨彼,因為閉包函數(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 第二次調(diào)用n變量還在內(nèi)存中
其實閉包的本質(zhì)就是作用域鏈的一個特殊的應(yīng)用蜜自,只要了解了作用域鏈的創(chuàng)建過程菩貌,就能夠理解閉包的實現(xiàn)原理。
5.Ajax是什么重荠?如何創(chuàng)建一個Ajax箭阶?
它是一種異步通信的方法,通過直接由 js 腳本向服務(wù)器發(fā)起 http 通信戈鲁,然后根據(jù)服務(wù)器返回的數(shù)據(jù)仇参,更新網(wǎng)頁的相應(yīng)部分,而不用刷新整個頁面的一種方法婆殿。
原生:
//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)聽請求诈乒,接受響應(yīng)
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);
// 設(shè)置狀態(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));
}
};
// 設(shè)置錯誤監(jiān)聽函數(shù)
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 設(shè)置響應(yīng)的數(shù)據(jù)類型
xhr.responseType = "json";
// 設(shè)置請求頭信息
xhr.setRequestHeader("Accept", "application/json");
// 發(fā)送 http 請求
xhr.send(null);
});
return promise;
}
6.簡單介紹一下 V8 引擎的垃圾回收機制
v8 的垃圾回收機制基于分代回收機制婆芦,這個機制又基于世代假說怕磨,這個假說有兩個特點喂饥,一是新生的對象容易早死,另一個是不死的對象會活得更久肠鲫≡卑铮基于這個假說,v8 引擎將內(nèi)存分為了新生代和老生代滩届。
新創(chuàng)建的對象或者只經(jīng)歷過一次的垃圾回收的對象被稱為新生代集侯。經(jīng)歷過多次垃圾回收的對象被稱為老生代。
新生代被分為 From 和 To 兩個空間帜消,To 一般是閑置的棠枉。當 From 空間滿了的時候會執(zhí)行 Scavenge 算法進行垃圾回收。當我們執(zhí)行垃圾回收算法的時候應(yīng)用邏輯將會停止泡挺,等垃圾回收結(jié)束后再繼續(xù)執(zhí)行辈讶。這個算法分為三步:
(1)首先檢查 From 空間的存活對象,如果對象存活則判斷對象是否滿足晉升到老生代的條件娄猫,如果滿足條件則晉升到老生代贱除。如果不滿足條件則移動 To 空間。
(2)如果對象不存活媳溺,則釋放對象的空間月幌。
(3)最后將 From 空間和 To 空間角色進行交換。
新生代對象晉升到老生代有兩個條件:
(1)第一個是判斷是對象否已經(jīng)經(jīng)過一次 Scavenge 回收悬蔽。若經(jīng)歷過扯躺,則將對象從 From 空間復(fù)制到老生代中;若沒有經(jīng)歷蝎困,則復(fù)制到 To 空間录语。
(2)第二個是 To 空間的內(nèi)存使用占比是否超過限制。當對象從 From 空間復(fù)制到 To 空間時禾乘,若 To 空間使用超過 25%澎埠,則對象直接晉升到老生代中。設(shè)置 25% 的原因主要是因為算法結(jié)束后始藕,兩個空間結(jié)束后會交換位置蒲稳,如果 To 空間的內(nèi)存太小,會影響后續(xù)的內(nèi)存分配鳄虱。
老生代采用了標記清除法和標記壓縮法弟塞。標記清除法首先會對內(nèi)存中存活的對象進行標記,標記結(jié)束后清除掉那些沒有標記的對象拙已。由于標記清除后會造成很多的內(nèi)存碎片决记,不便于后面的內(nèi)存分配。所以了解決內(nèi)存碎片的問題引入了標記壓縮法倍踪。
由于在進行垃圾回收的時候會暫停應(yīng)用的邏輯系宫,對于新生代方法由于內(nèi)存小索昂,每次停頓的時間不會太長,但對于老生代來說每次垃圾回收的時間長扩借,停頓會造成很大的影響椒惨。 為了解決這個問題 V8 引入了增量標記的方法,將一次停頓進行的過程分為了多步潮罪,每次執(zhí)行完一小步就讓運行邏輯執(zhí)行一會康谆,就這樣交替運行。
7.哪些操作會造成內(nèi)存泄漏嫉到?
1.意外的全局變量
2.被遺忘的計時器或回調(diào)函數(shù)
3.脫離 DOM 的引用
4.閉包
第一種情況是我們由于使用未聲明的變量沃暗,而意外的創(chuàng)建了一個全局變量,而使這個變量一直留在內(nèi)存中無法被回收何恶。
第二種情況是我們設(shè)置了setInterval定時器孽锥,而忘記取消它,如果循環(huán)函數(shù)有對外部變量的引用的話细层,那么這個變量會被一直留在內(nèi)存中惜辑,而無法被回收。
第三種情況是我們獲取一個DOM元素的引用疫赎,而后面這個元素被刪除盛撑,由于我們一直保留了對這個元素的引用,所以它也無法被回收捧搞。
第四種情況是不合理的使用閉包撵彻,從而導(dǎo)致某些變量一直被留在內(nèi)存當中。
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已經(jīng)被聲明了题涨。
暫存死區(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茂附、如果聲明的是復(fù)合類型數(shù)據(jù)正蛙,可以修改其屬性
*
* */
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ù)的地方锻全,并且它不能用作構(gòu)造函數(shù)。
var getCurrentDate = function (){
return new Date();
}
//ES6 Version
const getCurrentDate = () => new Date();
在本例中录煤,ES5 版本中有function(){}聲明和return關(guān)鍵字鳄厌,這兩個關(guān)鍵字分別是創(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對象。所以調(diào)用第一個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ù)將復(fù)制computeResult 方法中的this值肪虎,如果我們在全局作用域聲明箭頭函數(shù)劣砍,則this值為 window 對象。
10. js的深淺拷貝
JavaScript的深淺拷貝一直是個難點扇救,如果現(xiàn)在面試官讓我寫一個深拷貝刑枝,我可能也只是能寫出個基礎(chǔ)版的。所以在寫這條之前我拜讀了收藏夾里各路大佬寫的博文迅腔。具體可以看下面我貼的鏈接装畅,這里只做簡單的總結(jié)。
淺拷貝:創(chuàng)建一個新對象沧烈,這個對象有著原始對象屬性值的一份精確拷貝掠兄。如果屬性是基本類型,拷貝的就是基本類型的值,如果屬性是引用類型徽千,拷貝的就是內(nèi)存地址 苫费,所以如果其中一個對象改變了這個地址,就會影響到另一個對象双抽。
深拷貝:將一個對象從內(nèi)存中完整的拷貝一份出來,從堆內(nèi)存中開辟一個新的區(qū)域存放新對象,且修改新對象不會影響原對象百框。
淺拷貝的實現(xiàn)方式:
Object.assign()方法: 用于將所有可枚舉屬性的值從一個或多個源對象復(fù)制到目標對象。它將返回目標對象牍汹。
** 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())
基礎(chǔ)版: 淺拷貝+遞歸 (只考慮了普通的 object和 array兩種數(shù)據(jù)類型)
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.什么是回調(diào)函數(shù)抑党?回調(diào)函數(shù)有什么缺點
回調(diào)函數(shù)是一段可執(zhí)行的代碼段,它作為一個參數(shù)傳遞給其他的代碼撵摆,其作用是在需要的時候方便調(diào)用這段(回調(diào)函數(shù))代碼底靠。
在JavaScript中函數(shù)也是對象的一種,同樣對象可以作為參數(shù)傳遞給函數(shù)特铝,因此函數(shù)也可以作為參數(shù)傳遞給另外一個函數(shù)暑中,這個作為參數(shù)的函數(shù)就是回調(diào)函數(shù)。
const btnAdd = document.getElementById('btnAdd');
btnAdd.addEventListener('click', function clickCallback(e) {
// do something useless
});
在本例中鲫剿,我們等待id為btnAdd
的元素中的click
事件鳄逾,如果它被單擊,則執(zhí)行clickCallback
函數(shù)灵莲⊙铣模回調(diào)函數(shù)向某些數(shù)據(jù)或事件添加一些功能。
回調(diào)函數(shù)有一個致命的弱點笆呆,就是容易寫出回調(diào)地獄(Callback hell)。假設(shè)多個事件存在依賴性:
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
},3000)
},2000)
},1000)
這就是典型的回調(diào)地獄粱挡,以上代碼看起來不利于閱讀和維護赠幕,事件一旦多起來就更是亂糟糟,所以在es6中提出了Promise和async/await來解決回調(diào)地獄的問題询筏。當然榕堰,回調(diào)函數(shù)還存在著別的幾個缺點,比如不能使用 try catch 捕獲錯誤,不能直接 return逆屡。
12.Promise是什么圾旨,可以手寫實現(xiàn)一下嗎?
Promise魏蔗,翻譯過來是承諾砍的,承諾它過一段時間會給你一個結(jié)果。從編程講Promise 是異步編程的一種解決方案莺治。下面是Promise在MDN的相關(guān)說明:
Promise 對象是一個代理對象(代理一個值)廓鞠,被代理的值在Promise對象創(chuàng)建時可能是未知的。它允許你為異步操作的成功和失敗分別綁定相應(yīng)的處理方法(handlers)谣旁。這讓異步方法可以像同步方法那樣返回值床佳,但并不是立即返回最終執(zhí)行結(jié)果,而是一個能代表未來出現(xiàn)的結(jié)果的promise對象榄审。
一個 Promise有以下幾種狀態(tài):
pending: 初始狀態(tài)砌们,既不是成功,也不是失敗狀態(tài)搁进。
fulfilled: 意味著操作成功完成浪感。
rejected: 意味著操作失敗。
這個承諾一旦從等待狀態(tài)變成為其他狀態(tài)就永遠不能更改狀態(tài)了拷获,也就是說一旦狀態(tài)變?yōu)?fulfilled/rejected 后篮撑,就不能再次改變〈夜希可能光看概念大家不理解Promise赢笨,我們舉個簡單的栗子;
假如我有個女朋友驮吱,下周一是她生日茧妒,我答應(yīng)她生日給她一個驚喜,那么從現(xiàn)在開始這個承諾就進入等待狀態(tài)左冬,等待下周一的到來桐筏,然后狀態(tài)改變。如果下周一我如約給了女朋友驚喜拇砰,那么這個承諾的狀態(tài)就會由pending切換為fulfilled梅忌,表示承諾成功兌現(xiàn),一旦是這個結(jié)果了除破,就不會再有其他結(jié)果牧氮,即狀態(tài)不會在發(fā)生改變;反之如果當天我因為工作太忙加班瑰枫,把這事給忘了踱葛,說好的驚喜沒有兌現(xiàn),狀態(tài)就會由pending切換為rejected,時間不可倒流尸诽,所以狀態(tài)也不能再發(fā)生變化甥材。
上一條我們說過Promise可以解決回調(diào)地獄的問題,沒錯性含,pending 狀態(tài)的 Promise 對象會觸發(fā) fulfilled/rejected 狀態(tài)洲赵,一旦狀態(tài)改變,Promise 對象的 then 方法就會被調(diào)用胶滋;否則就會觸發(fā) catch板鬓。我們將上一條回調(diào)地獄的代碼改寫一下:
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俭令,錯誤需要通過回調(diào)函數(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";
}
}
//捕獲構(gòu)造異常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
// 定義鏈式調(diào)用的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 和回調(diào)雾叭,它的可讀性和簡潔度都更高悟耘。畢竟一直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();
結(jié)果和之前的 Promise 實現(xiàn)是一樣的担映,但是這個代碼看起來是不是清晰得多技扼,優(yōu)雅整潔诸狭,幾乎跟同步代碼一樣专甩。
await關(guān)鍵字只能在async function中使用遮晚。在任何非async function的函數(shù)中使用await關(guān)鍵字都會拋出錯誤。await關(guān)鍵字在執(zhí)行下一行代碼之前等待右側(cè)表達式(可能是一個Promise)返回渣蜗。
優(yōu)缺點
async/await
的優(yōu)勢在于處理 then 的調(diào)用鏈屠尊,能夠更清晰準確的寫出代碼,并且也能優(yōu)雅地解決回調(diào)地獄問題耕拷。當然也存在一些缺點讼昆,因為 await 將異步代碼改造成了同步代碼,如果多個異步代碼沒有依賴性卻使用了 await 會導(dǎo)致性能上的降低骚烧。
14.js 的節(jié)流與防抖
函數(shù)防抖 是指在事件被觸發(fā) n 秒后再執(zhí)行回調(diào)浸赫,如果在這 n 秒內(nèi)事件又被觸發(fā),則重新計時赃绊。這可以使用在一些點擊請求的事件上既峡,避免因為用戶的多次點擊向后端發(fā)送多次請求。
函數(shù)節(jié)流 是指規(guī)定一個單位時間碧查,在這個單位時間內(nèi)运敢,只能有一次觸發(fā)事件的回調(diào)函數(shù)執(zhí)行,如果在同一個單位時間內(nèi)某事件被觸發(fā)多次忠售,只有一次能生效传惠。節(jié)流可以使用在 scroll 函數(shù)的事件監(jiān)聽上,通過事件節(jié)流來降低事件調(diào)用的頻率稻扬。
// 函數(shù)防抖的實現(xiàn)
function debounce(fn, wait) {
var timer = null;
return function() {
var context = this,
args = arguments;
// 如果此時存在定時器的話卦方,則取消之前的定時器重新記時
if (timer) {
clearTimeout(timer);
timer = null;
}
// 設(shè)置定時器,使事件間隔指定事件后執(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);
}
};
}