前端面試JavaScript高頻手寫大全
在前端面試中,手撕代碼顯然是不可避免的,并且占很大的一部分比重。
一般來說,如果代碼寫的好,即使理論知識(shí)答得不夠清楚,也能有大概率通過面試。并且其實(shí)很多手寫往往背后就考察了你對(duì)相關(guān)理論的認(rèn)識(shí)。
編程題主要分為這幾種類型:
- 算法題
- 涉及js原理的題以及ajax請(qǐng)求
- 業(yè)務(wù)場(chǎng)景題: 實(shí)現(xiàn)一個(gè)具有某種功能的組件
- 其他(進(jìn)階,對(duì)計(jì)算機(jī)綜合知識(shí)的考察,考的相對(duì)較少):實(shí)現(xiàn)訂閱發(fā)布者模式;分別用面向?qū)ο缶幊?,面向過程編程,函數(shù)式編程實(shí)現(xiàn)把大象放進(jìn)冰箱等等
其中前兩種類型所占比重最大。
算法題建議養(yǎng)成每天刷一道leetcode
的習(xí)慣,重點(diǎn)刷數(shù)據(jù)結(jié)構(gòu)(棧,鏈表,隊(duì)列,樹),動(dòng)態(tài)規(guī)劃,DFS
,BFS
本文主要涵蓋了第二種類型的各種重點(diǎn)手寫。
建議優(yōu)先掌握:
instanceof
(考察對(duì)原型鏈的理解)new
(對(duì)創(chuàng)建對(duì)象實(shí)例過程的理解)call&apply&bind
(對(duì)this指向的理解)- 手寫
promise
(對(duì)異步的理解) - 手寫原生
ajax
(對(duì)ajax原理和http請(qǐng)求方式的理解,重點(diǎn)是get和post請(qǐng)求的實(shí)現(xiàn)) - 事件訂閱發(fā)布 (高頻考點(diǎn))
- 其他:數(shù)組,字符串的api的實(shí)現(xiàn),難度相對(duì)較低。只要了解數(shù)組,字符串的常用方法的用法,現(xiàn)場(chǎng)就能寫出來個(gè)大概。(ps:筆者認(rèn)為數(shù)組的
reduce
方法比較難,這塊有余力可以單獨(dú)看一些,即使面試沒讓你實(shí)現(xiàn)reduce
,寫其他題時(shí)用上它也是很加分的)
1. 手寫instanceof
instanceof作用:
判斷一個(gè)實(shí)例是否是其父類或者祖先類型的實(shí)例。
instanceof 在查找的過程中會(huì)遍歷左邊變量的原型鏈,直到找到右邊變量的 prototype查找失敗,返回 false
let myInstanceof = (target,origin) => { while(target) { if(target.__proto__===origin.prototype) { return true } target = target.__proto__ } return false } let a = [1,2,3] console.log(myInstanceof(a,Array)); // true console.log(myInstanceof(a,Object)); // true
2. 實(shí)現(xiàn)數(shù)組的map方法
數(shù)組的map()
方法會(huì)返回一個(gè)新的數(shù)組,這個(gè)新數(shù)組中的每個(gè)元素對(duì)應(yīng)原數(shù)組中的對(duì)應(yīng)位置元素調(diào)用一次提供的函數(shù)后的返回值。
用法:
const a = [1, 2, 3, 4]; const b = array1.map(x => x * 2); console.log(b); // Array [2, 4, 6, 8]
實(shí)現(xiàn)前,我們先看一下map方法的參數(shù)有哪些
map
方法有兩個(gè)參數(shù),一個(gè)是操作數(shù)組元素的方法fn,一個(gè)是this指向(可選),其中使用fn時(shí)可以獲取三個(gè)參數(shù),實(shí)現(xiàn)時(shí)記得不要漏掉,這樣才算完整實(shí)現(xiàn)嘛
原生實(shí)現(xiàn):
// 實(shí)現(xiàn) Array.prototype.myMap = function(fn, thisValue) { let res = [] thisValue = thisValue||[] let arr = this for(let i=0; i<arr.length; i++) { res.push(fn.call(thisValue, arr[i],i,arr)) // 參數(shù)分別為this指向,當(dāng)前數(shù)組項(xiàng),當(dāng)前索引,當(dāng)前數(shù)組 } return res } // 使用 const a = [1,2,3]; const b = a.myMap((a,index)=> { return a+1; } ) console.log(b) // 輸出 [2, 3, 4]
3. reduce實(shí)現(xiàn)數(shù)組的map方法
利用數(shù)組內(nèi)置的reduce
方法實(shí)現(xiàn)map方法,考察對(duì)reduce
原理的掌握
Array.prototype.myMap = function(fn,thisValue){ var res = []; thisValue = thisValue||[]; this.reduce(function(pre,cur,index,arr){ return res.push(fn.call(thisValue,cur,index,arr)); },[]); return res; } var arr = [2,3,1,5]; arr.myMap(function(item,index,arr){ console.log(item,index,arr); })
4. 手寫數(shù)組的reduce方法
reduce()
方法接收一個(gè)函數(shù)作為累加器,數(shù)組中的每個(gè)值(從左到右)開始縮減,最終為一個(gè)值,是ES5中新增的又一個(gè)數(shù)組逐項(xiàng)處理方法
參數(shù):
- callback(一個(gè)在數(shù)組中每一項(xiàng)上調(diào)用的函數(shù),接受四個(gè)函數(shù):)
- previousValue(上一次調(diào)用回調(diào)函數(shù)時(shí)的返回值,或者初始值)
- currentValue(當(dāng)前正在處理的數(shù)組元素)
- currentIndex(當(dāng)前正在處理的數(shù)組元素下標(biāo))
- array(調(diào)用reduce()方法的數(shù)組)
- initialValue(可選的初始值。作為第一次調(diào)用回調(diào)函數(shù)時(shí)傳給previousValue的值)
function reduce(arr, cb, initialValue){ var num = initValue == undefined? num = arr[0]: initValue; var i = initValue == undefined? 1: 0 for (i; i< arr.length; i++){ num = cb(num,arr[i],i) } return num } function fn(result, currentValue, index){ return result + currentValue } var arr = [2,3,4,5] var b = reduce(arr, fn,10) var c = reduce(arr, fn) console.log(b) // 24
5. 數(shù)組扁平化
數(shù)組扁平化就是把多維數(shù)組轉(zhuǎn)化成一維數(shù)組
5. 1 es6提供的新方法 flat(depth)
let a = [1,[2,3]];
a.flat(); // [1,2,3]
a.flat(1); //[1,2,3]
其實(shí)還有一種更簡(jiǎn)單的辦法,無需知道數(shù)組的維度,直接將目標(biāo)數(shù)組變成1維數(shù)組。 depth的值設(shè)置為Infinity。
let a = [1,[2,3,[4,[5]]]];
a.flat(Infinity); // [1,2,3,4,5] a是4維數(shù)組
5.2 利用cancat
function flatten(arr) { var res = []; for (let i = 0, length = arr.length; i < length; i++) { if (Array.isArray(arr[i])) { res = res.concat(flatten(arr[i])); //concat 并不會(huì)改變?cè)瓟?shù)組 //res.push(...flatten(arr[i])); //或者用擴(kuò)展運(yùn)算符 } else { res.push(arr[i]); } } return res; } let arr1 = [1, 2,[3,1],[2,3,4,[2,3,4]]] flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
補(bǔ)充:指定deep的flat
只需每次遞歸時(shí)將當(dāng)前deep-1
,若大于0,則可以繼續(xù)展開
function flat(arr, deep) { let res = [] for(let i in arr) { if(Array.isArray(arr[i])&&deep) { res = res.concat(flat(arr[i],deep-1)) } else { res.push(arr[i]) } } return res } console.log(flat([12,[1,2,3],3,[2,4,[4,[3,4],2]]],1));
6. 函數(shù)柯里化
用之前可以了解之前的文章前端JavaScript徹底弄懂函數(shù)柯里化curry與這里用的同樣方法
柯里化的定義:接收一部分參數(shù),返回一個(gè)函數(shù)接收剩余參數(shù),接收足夠參數(shù)后,執(zhí)行原函數(shù)。
當(dāng)柯里化函數(shù)接收到足夠參數(shù)后,就會(huì)執(zhí)行原函數(shù),如何去確定何時(shí)達(dá)到足夠的參數(shù)呢?
有兩種思路:
- 通過函數(shù)的 length 屬性,獲取函數(shù)的形參個(gè)數(shù),形參的個(gè)數(shù)就是所需的參數(shù)個(gè)數(shù)
- 在調(diào)用柯里化工具函數(shù)時(shí),手動(dòng)指定所需的參數(shù)個(gè)數(shù)
將這兩點(diǎn)結(jié)合一下,實(shí)現(xiàn)一個(gè)簡(jiǎn)單 curry 函數(shù):
/** * 將函數(shù)柯里化 * @param fn 待柯里化的原函數(shù) * @param len 所需的參數(shù)個(gè)數(shù),默認(rèn)為原函數(shù)的形參個(gè)數(shù) */ function curry(fn,len = fn.length) { return _curry.call(this,fn,len) } /** * 中轉(zhuǎn)函數(shù) * @param fn 待柯里化的原函數(shù) * @param len 所需的參數(shù)個(gè)數(shù) * @param args 已接收的參數(shù)列表 */ function _curry(fn,len,...args) { return function (...params) { let _args = [...args,...params]; if(_args.length >= len){ return fn.apply(this,_args); }else{ return _curry.call(this,fn,len,..._args) } } }
我們來驗(yàn)證一下:
let _fn = curry(function(a,b,c,d,e){ console.log(a,b,c,d,e) }); _fn(1,2,3,4,5); // print: 1,2,3,4,5 _fn(1)(2)(3,4,5); // print: 1,2,3,4,5 _fn(1,2)(3,4)(5); // print: 1,2,3,4,5 _fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5
我們常用的工具庫 lodash
也提供了 curry
方法,并且增加了非常好玩的 placeholder
功能,通過占位符的方式來改變傳入?yún)?shù)的順序。
比如說,我們傳入一個(gè)占位符,本次調(diào)用傳遞的參數(shù)略過占位符, 占位符所在的位置由下次調(diào)用的參數(shù)來填充,比如這樣:
直接看一下官網(wǎng)的例子:
接下來我們來思考,如何實(shí)現(xiàn)占位符的功能。
對(duì)于 lodash
的 curry
函數(shù)來說,curry 函數(shù)掛載在 lodash
對(duì)象上,所以將 lodash 對(duì)象當(dāng)做默認(rèn)占位符來使用。
而我們的自己實(shí)現(xiàn)的 curry 函數(shù),本身并沒有掛載在任何對(duì)象上,所以將 curry 函數(shù)當(dāng)做默認(rèn)占位符
使用占位符,目的是改變參數(shù)傳遞的順序,所以在 curry 函數(shù)實(shí)現(xiàn)中,每次需要記錄是否使用了占位符,并且記錄占位符所代表的參數(shù)位置。
直接上代碼:
/** * @param fn 待柯里化的函數(shù) * @param length 需要的參數(shù)個(gè)數(shù),默認(rèn)為函數(shù)的形參個(gè)數(shù) * @param holder 占位符,默認(rèn)當(dāng)前柯里化函數(shù) * @return {Function} 柯里化后的函數(shù) */ function curry(fn,length = fn.length,holder = curry){ return _curry.call(this,fn,length,holder,[],[]) } /** * 中轉(zhuǎn)函數(shù) * @param fn 柯里化的原函數(shù) * @param length 原函數(shù)需要的參數(shù)個(gè)數(shù) * @param holder 接收的占位符 * @param args 已接收的參數(shù)列表 * @param holders 已接收的占位符位置列表 * @return {Function} 繼續(xù)柯里化的函數(shù) 或 最終結(jié)果 */ function _curry(fn,length,holder,args,holders){ return function(..._args){ //將參數(shù)復(fù)制一份,避免多次操作同一函數(shù)導(dǎo)致參數(shù)混亂 let params = args.slice(); //將占位符位置列表復(fù)制一份,新增加的占位符增加至此 let _holders = holders.slice(); //循環(huán)入?yún)?,追加參?shù) 或 替換占位符 _args.forEach((arg,i)=>{ //真實(shí)參數(shù) 之前存在占位符 將占位符替換為真實(shí)參數(shù) if (arg !== holder && holders.length) { let index = holders.shift(); _holders.splice(_holders.indexOf(index),1); params[index] = arg; } //真實(shí)參數(shù) 之前不存在占位符 將參數(shù)追加到參數(shù)列表中 else if(arg !== holder && !holders.length){ params.push(arg); } //傳入的是占位符,之前不存在占位符 記錄占位符的位置 else if(arg === holder && !holders.length){ params.push(arg); _holders.push(params.length - 1); } //傳入的是占位符,之前存在占位符 刪除原占位符位置 else if(arg === holder && holders.length){ holders.shift(); } }); // params 中前 length 條記錄中不包含占位符,執(zhí)行函數(shù) if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){ return fn.apply(this,params); }else{ return _curry.call(this,fn,length,holder,params,_holders) } } }
驗(yàn)證一下:
let fn = function(a, b, c, d, e) { console.log([a, b, c, d, e]); } let _ = {}; // 定義占位符 let _fn = curry(fn,5,_); // 將函數(shù)柯里化,指定所需的參數(shù)個(gè)數(shù),指定所需的占位符 _fn(1, 2, 3, 4, 5); // print: 1,2,3,4,5 _fn(_, 2, 3, 4, 5)(1); // print: 1,2,3,4,5 _fn(1, _, 3, 4, 5)(2); // print: 1,2,3,4,5 _fn(1, _, 3)(_, 4,_)(2)(5); // print: 1,2,3,4,5 _fn(1, _, _, 4)(_, 3)(2)(5); // print: 1,2,3,4,5 _fn(_, 2)(_, _, 4)(1)(3)(5); // print: 1,2,3,4,5
至此,我們已經(jīng)完整實(shí)現(xiàn)了一個(gè) curry
函數(shù)~~
7. 淺拷貝和深拷貝的實(shí)現(xiàn)
深拷貝和淺拷貝是只針對(duì)Object
和Array
這樣的引用數(shù)據(jù)類型的。
7.1淺拷貝和深拷貝的區(qū)別
淺拷貝:創(chuàng)建一個(gè)新對(duì)象,這個(gè)對(duì)象有著原始對(duì)象屬性值的一份精確拷貝。如果屬性是基本類型,拷貝的就是基本類型的值,如果屬性是引用類型,拷貝的就是內(nèi)存地址,如果其中一個(gè)對(duì)象改變了引用類型的屬性,就會(huì)影響到另一個(gè)對(duì)象。
深拷貝:將一個(gè)對(duì)象從內(nèi)存中完整的復(fù)制一份出來,從堆內(nèi)存中開辟一個(gè)新區(qū)域存放。這樣更改拷貝值就不影響舊的對(duì)象
淺拷貝實(shí)現(xiàn):
方法一:
function shallowCopy(target, origin){ for(let item in origin) target[item] = origin[item]; return target; }
其他方法(內(nèi)置api):
(1)Object.assign
var obj={a:1,b:[1,2,3],c:function(){console.log('i am c')}} var tar={}; Object.assign(tar,obj);
當(dāng)然這個(gè)方法只適合于對(duì)象類型,如果是數(shù)組可以使用slice
和concat
方法
(2)Array.prototype.slice
var arr=[1,2,[3,4]]; var newArr=arr.slice(0); Array.prototype.concat var arr=[1,2,[3,4]]; var newArr=arr.concat();
(3)Array.prototype.concat
var arr=[1,2,[3,4]]; var newArr=arr.concat();
測(cè)試同上(assign用對(duì)象測(cè)試、slice concat用數(shù)組測(cè)試),結(jié)合淺拷貝深拷貝的概念來理解效果更佳
深拷貝實(shí)現(xiàn):
方法一:
轉(zhuǎn)為json格式再解析
const a = JSON.parse(JSON.stringify(b))
方法二:
// 實(shí)現(xiàn)深拷貝 遞歸 function deepCopy(newObj,oldObj){ for(var k in oldObj){ let item=oldObj[k] // 判斷是數(shù)組、對(duì)象、簡(jiǎn)單類型? if(item instanceof Array){ newObj[k]=[] deepCopy(newObj[k],item) }else if(item instanceof Object){ newObj[k]={} deepCopy(newObj[k],item) }else{ //簡(jiǎn)單數(shù)據(jù)類型,直接賦值 newObj[k]=item } } }
8. 手寫call, apply, bind
8.1 手寫call
Function.prototype.myCall=function(context=window){ // 函數(shù)的方法,所以寫在Fuction原型對(duì)象上 if(typeof this !=="function"){ // 這里if其實(shí)沒必要,會(huì)自動(dòng)拋出錯(cuò)誤 throw new Error("不是函數(shù)") } const obj=context||window //這里可用ES6方法,為參數(shù)添加默認(rèn)值,js嚴(yán)格模式全局作用域this為undefined obj.fn=this //this為調(diào)用的上下文,this此處為函數(shù),將這個(gè)函數(shù)作為obj的方法 const arg=[...arguments].slice(1) //第一個(gè)為obj所以刪除,偽數(shù)組轉(zhuǎn)為數(shù)組 res=obj.fn(...arg) delete obj.fn // 不刪除會(huì)導(dǎo)致context屬性越來越多 return res } //用法:f.call(obj,arg1) function f(a,b){ console.log(a+b) console.log(this.name) } let obj={ name:1 } f.myCall(obj,1,2) //否則this指向window obj.greet.call({name: 'Spike'}) //打出來的是 Spike
8.2 手寫apply(arguments[this, [參數(shù)1,參數(shù)2.....] ])
Function.prototype.myApply=function(context){ // 箭頭函數(shù)從不具有參數(shù)對(duì)象?。。。。∵@里不能寫成箭頭函數(shù) let obj=context||window obj.fn=this const arg=arguments[1]||[] //若有參數(shù),得到的是數(shù)組 let res=obj.fn(...arg) delete obj.fn return res } function f(a,b){ console.log(a,b) console.log(this.name) } let obj={ name:'張三' } f.myApply(obj,[1,2]) //arguments[1]
8.3 手寫bind
this.value = 2 var foo = { value: 1 }; var bar = function(name, age, school){ console.log(name) // 'An' console.log(age) // 22 console.log(school) // '家里蹲大學(xué)' } var result = bar.bind(foo, 'An') //預(yù)置了部分參數(shù)'An' result(22, '家里蹲大學(xué)') //這個(gè)參數(shù)會(huì)和預(yù)置的參數(shù)合并到一起放入bar中
簡(jiǎn)單版本
Function.prototype.bind = function(context, ...outerArgs) { var fn = this; return function(...innerArgs) { //返回了一個(gè)函數(shù),...rest為實(shí)際調(diào)用時(shí)傳入的參數(shù) return fn.apply(context,[...outerArgs, ...innerArgs]); //返回改變了this的函數(shù), //參數(shù)合并 } }
new失敗的原因:
例:
// 聲明一個(gè)上下文 let thovino = { name: 'thovino' } // 聲明一個(gè)構(gòu)造函數(shù) let eat = function (food) { this.food = food console.log(`${this.name} eat ${this.food}`) } eat.prototype.sayFuncName = function () { console.log('func name : eat') } // bind一下 let thovinoEat = eat.bind(thovino) let instance = new thovinoEat('orange') //實(shí)際上orange放到了thovino里面 console.log('instance:', instance) // {}
生成的實(shí)例是個(gè)空對(duì)象
在new
操作符執(zhí)行時(shí),我們的thovinoEat
函數(shù)可以看作是這樣:
function thovinoEat (...innerArgs) { eat.call(thovino, ...outerArgs, ...innerArgs) }
在new操作符進(jìn)行到第三步的操作thovinoEat.call(obj, ...args
)時(shí),這里的obj是new操作符自己創(chuàng)建的那個(gè)簡(jiǎn)單空對(duì)象{},但它其實(shí)并沒有替換掉thovinoEat
函數(shù)內(nèi)部的那個(gè)上下文對(duì)象thovino。這已經(jīng)超出了call的能力范圍,因?yàn)檫@個(gè)時(shí)候要替換的已經(jīng)不是thovinoEat
函數(shù)內(nèi)部的this指向,而應(yīng)該是thovino
對(duì)象。
換句話說,我們希望的是new操作符將eat
內(nèi)的this指向操作符自己創(chuàng)建的那個(gè)空對(duì)象。但是實(shí)際上指向了thovino
,new
操作符的第三步動(dòng)作并沒有成功!
可new可繼承版本
Function.prototype.bind = function (context, ...outerArgs) { let that = this; function res (...innerArgs) { if (this instanceof res) { // new操作符執(zhí)行時(shí) // 這里的this在new操作符第三步操作時(shí),會(huì)指向new自身創(chuàng)建的那個(gè)簡(jiǎn)單空對(duì)象{} that.call(this, ...outerArgs, ...innerArgs) } else { // 普通bind that.call(context, ...outerArgs, ...innerArgs) } } res.prototype = this.prototype //?。?! return res }
9. 手動(dòng)實(shí)現(xiàn)new
new的過程文字描述:
- 創(chuàng)建一個(gè)空對(duì)象 obj;
- 將空對(duì)象的隱式原型(proto)指向構(gòu)造函數(shù)的prototype。
- 使用 call 改變 this 的指向
- 如果無返回值或者返回一個(gè)非對(duì)象值,則將 obj 返回作為新對(duì)象;如果返回值是一個(gè)新對(duì)象的話那么直接直接返回該對(duì)象。
function Person(name,age){ this.name=name this.age=age } Person.prototype.sayHi=function(){ console.log('Hi!我是'+this.name) } let p1=new Person('張三',18) ////手動(dòng)實(shí)現(xiàn)new function create(){ let obj={} //獲取構(gòu)造函數(shù) let fn=[].shift.call(arguments) //將arguments對(duì)象提出來轉(zhuǎn)化為數(shù)組,arguments并不是數(shù)組而是對(duì)象 !?。∵@種方法刪除了arguments數(shù)組的第一個(gè)元素,?。∵@里的空數(shù)組里面填不填元素都沒關(guān)系,不影響arguments的結(jié)果 或者let arg = [].slice.call(arguments,1) obj.__proto__=fn.prototype let res=fn.apply(obj,arguments) //改變this指向,為實(shí)例添加方法和屬性 //確保返回的是一個(gè)對(duì)象(萬一fn不是構(gòu)造函數(shù)) return typeof res==='object'?res:obj } let p2=create(Person,'李四',19) p2.sayHi()
細(xì)節(jié):
[].shift.call(arguments) 也可寫成: let arg=[...arguments] let fn=arg.shift() //使得arguments能調(diào)用數(shù)組方法,第一個(gè)參數(shù)為構(gòu)造函數(shù) obj.__proto__=fn.prototype //改變this指向,為實(shí)例添加方法和屬性 let res=fn.apply(obj,arg)
10. 手寫promise(常考promise.all, promise.race)
// Promise/A+ 規(guī)范規(guī)定的三種狀態(tài) const STATUS = { PENDING: 'pending', FULFILLED: 'fulfilled', REJECTED: 'rejected' } class MyPromise { // 構(gòu)造函數(shù)接收一個(gè)執(zhí)行回調(diào) constructor(executor) { this._status = STATUS.PENDING // Promise初始狀態(tài) this._value = undefined // then回調(diào)的值 this._resolveQueue = [] // resolve時(shí)觸發(fā)的成功隊(duì)列 this._rejectQueue = [] // reject時(shí)觸發(fā)的失敗隊(duì)列 // 使用箭頭函數(shù)固定this(resolve函數(shù)在executor中觸發(fā),不然找不到this) const resolve = value => { const run = () => { // Promise/A+ 規(guī)范規(guī)定的Promise狀態(tài)只能從pending觸發(fā),變成fulfilled if (this._status === STATUS.PENDING) { this._status = STATUS.FULFILLED // 更改狀態(tài) this._value = value // 儲(chǔ)存當(dāng)前值,用于then回調(diào) // 執(zhí)行resolve回調(diào) while (this._resolveQueue.length) { const callback = this._resolveQueue.shift() callback(value) } } } //把resolve執(zhí)行回調(diào)的操作封裝成一個(gè)函數(shù),放進(jìn)setTimeout里,以實(shí)現(xiàn)promise異步調(diào)用的特性(規(guī)范上是微任務(wù),這里是宏任務(wù)) setTimeout(run) } // 同 resolve const reject = value => { const run = () => { if (this._status === STATUS.PENDING) { this._status = STATUS.REJECTED this._value = value while (this._rejectQueue.length) { const callback = this._rejectQueue.shift() callback(value) } } } setTimeout(run) } // new Promise()時(shí)立即執(zhí)行executor,并傳入resolve和reject executor(resolve, reject) } // then方法,接收一個(gè)成功的回調(diào)和一個(gè)失敗的回調(diào) function then(onFulfilled, onRejected) { // 根據(jù)規(guī)范,如果then的參數(shù)不是function,則忽略它, 讓值繼續(xù)往下傳遞,鏈?zhǔn)秸{(diào)用繼續(xù)往下執(zhí)行 typeof onFulfilled !== 'function' ? onFulfilled = value => value : null typeof onRejected !== 'function' ? onRejected = error => error : null // then 返回一個(gè)新的promise return new MyPromise((resolve, reject) => { const resolveFn = value => { try { const x = onFulfilled(value) // 分類討論返回值,如果是Promise,那么等待Promise狀態(tài)變更,否則直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } } } const rejectFn = error => { try { const x = onRejected(error) x instanceof MyPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } switch (this._status) { case STATUS.PENDING: this._resolveQueue.push(resolveFn) this._rejectQueue.push(rejectFn) break; case STATUS.FULFILLED: resolveFn(this._value) break; case STATUS.REJECTED: rejectFn(this._value) break; } }) } catch (rejectFn) { return this.then(undefined, rejectFn) } // promise.finally方法 finally(callback) { return this.then(value => MyPromise.resolve(callback()).then(() => value), error => { MyPromise.resolve(callback()).then(() => error) }) } // 靜態(tài)resolve方法 static resolve(value) { return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value)) } // 靜態(tài)reject方法 static reject(error) { return new MyPromise((resolve, reject) => reject(error)) } // 靜態(tài)all方法 static all(promiseArr) { let count = 0 let result = [] return new MyPromise((resolve, reject) => { if (!promiseArr.length) { return resolve(result) } promiseArr.forEach((p, i) => { MyPromise.resolve(p).then(value => { count++ result[i] = value if (count === promiseArr.length) { resolve(result) } }, error => { reject(error) }) }) }) } // 靜態(tài)race方法 static race(promiseArr) { return new MyPromise((resolve, reject) => { promiseArr.forEach(p => { MyPromise.resolve(p).then(value => { resolve(value) }, error => { reject(error) }) }) }) } }
11. 手寫原生AJAX
步驟:
- 創(chuàng)建 XMLHttpRequest 實(shí)例
- 發(fā)出 HTTP 請(qǐng)求
- 服務(wù)器返回 XML 格式的字符串
- JS 解析 XML,并更新局部頁面
不過隨著歷史進(jìn)程的推進(jìn),XML 已經(jīng)被淘汰,取而代之的是 JSON。
了解了屬性和方法之后,根據(jù) AJAX 的步驟,手寫最簡(jiǎn)單的 GET 請(qǐng)求。
version 1.0:
myButton.addEventListener('click', function () { ajax() }) function ajax() { let xhr = new XMLHttpRequest() //實(shí)例化,以調(diào)用方法 xhr.open('get', 'https://www.google.com') //參數(shù)2,url。參數(shù)三:異步 xhr.onreadystatechange = () => { //每當(dāng) readyState 屬性改變時(shí),就會(huì)調(diào)用該函數(shù)。 if (xhr.readyState === 4) { //XMLHttpRequest 代理當(dāng)前所處狀態(tài)。 if (xhr.status >= 200 && xhr.status < 300) { //200-300請(qǐng)求成功 let string = request.responseText //JSON.parse() 方法用來解析JSON字符串,構(gòu)造由字符串描述的JavaScript值或?qū)ο? let object = JSON.parse(string) } } } request.send() //用于實(shí)際發(fā)出 HTTP 請(qǐng)求。不帶參數(shù)為GET請(qǐng)求 }
promise實(shí)現(xiàn)
function ajax(url) { const p = new Promise((resolve, reject) => { let xhr = new XMLHttpRequest() xhr.open('get', url) xhr.onreadystatechange = () => { if (xhr.readyState == 4) { if (xhr.status >= 200 && xhr.status <= 300) { resolve(JSON.parse(xhr.responseText)) } else { reject('請(qǐng)求出錯(cuò)') } } } xhr.send() //發(fā)送hppt請(qǐng)求 }) return p } let url = '/data.json' ajax(url).then(res => console.log(res)) .catch(reason => console.log(reason))
12. 手寫節(jié)流防抖函數(shù)
函數(shù)節(jié)流與函數(shù)防抖都是為了限制函數(shù)的執(zhí)行頻次,是一種性能優(yōu)化的方案,比如應(yīng)用于window
對(duì)象的resize
、scroll
事件,拖拽時(shí)的mousemove
事件,文字輸入、自動(dòng)完成的keyup
事件。
節(jié)流:連續(xù)觸發(fā)事件但是在 n 秒中只執(zhí)行一次函數(shù)
例:(連續(xù)不斷動(dòng)都需要調(diào)用時(shí)用,設(shè)一時(shí)間間隔),像dom的拖拽,如果用消抖的話,就會(huì)出現(xiàn)卡頓的感覺,因?yàn)橹辉谕V沟臅r(shí)候執(zhí)行了一次,這個(gè)時(shí)候就應(yīng)該用節(jié)流,在一定時(shí)間內(nèi)多次執(zhí)行,會(huì)流暢很多。
防抖:指觸發(fā)事件后在 n 秒內(nèi)函數(shù)只能執(zhí)行一次,如果在 n 秒內(nèi)又觸發(fā)了事件,則會(huì)重新計(jì)算函數(shù)執(zhí)行時(shí)間。
例:(連續(xù)不斷觸發(fā)時(shí)不調(diào)用,觸發(fā)完后過一段時(shí)間調(diào)用),像仿百度搜索,就應(yīng)該用防抖,當(dāng)我連續(xù)不斷輸入時(shí),不會(huì)發(fā)送請(qǐng)求;當(dāng)我一段時(shí)間內(nèi)不輸入了,才會(huì)發(fā)送一次請(qǐng)求;如果小于這段時(shí)間繼續(xù)輸入的話,時(shí)間會(huì)重新計(jì)算,也不會(huì)發(fā)送請(qǐng)求。
12.1 防抖的實(shí)現(xiàn)
function debounce(fn, delay) { if(typeof fn!=='function') { throw new TypeError('fn不是函數(shù)') } let timer; // 維護(hù)一個(gè) timer return function () { var _this = this; // 取debounce執(zhí)行作用域的this(原函數(shù)掛載到的對(duì)象) var args = arguments; if (timer) { clearTimeout(timer); } timer = setTimeout(function () { fn.apply(_this, args); // 用apply指向調(diào)用debounce的對(duì)象,相當(dāng)于_this.fn(args); }, delay); }; } // 調(diào)用 input1.addEventListener('keyup', debounce(() => { console.log(input1.value) }), 600)
12.2 節(jié)流的實(shí)現(xiàn)
function throttle(fn, delay) { let timer; return function () { var _this = this; var args = arguments; if (timer) { return; } timer = setTimeout(function () { fn.apply(_this, args); // 這里args接收的是外邊返回的函數(shù)的參數(shù),不能用arguments // fn.apply(_this, arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受類數(shù)組對(duì)象。如果傳入類數(shù)組對(duì)象,它們會(huì)拋出異常。 timer = null; // 在delay后執(zhí)行完fn之后清空timer,此時(shí)timer為假,throttle觸發(fā)可以進(jìn)入計(jì)時(shí)器 }, delay) } } div1.addEventListener('drag', throttle((e) => { console.log(e.offsetX, e.offsetY) }, 100))
13. 手寫Promise加載圖片
function getData(url) { return new Promise((resolve, reject) => { $.ajax({ url, success(data) { resolve(data) }, error(err) { reject(err) } }) }) } const url1 = './data1.json' const url2 = './data2.json' const url3 = './data3.json' getData(url1).then(data1 => { console.log(data1) return getData(url2) }).then(data2 => { console.log(data2) return getData(url3) }).then(data3 => console.log(data3) ).catch(err => console.error(err) )
14. 函數(shù)實(shí)現(xiàn)一秒鐘輸出一個(gè)數(shù)
(!!!這個(gè)題這兩天字節(jié)校招面試被問到了,問var打印的什么,改為let為什么可以?
有沒有其他方法實(shí)現(xiàn)?我自己博客里都寫了不用let的寫法第二種方法,居然給忘了~~~白學(xué)了)
ES6:用let塊級(jí)作用域的原理實(shí)現(xiàn)
for(let i=0;i<=10;i++){ //用var打印的都是11 setTimeout(()=>{ console.log(i); },1000*i) }
不用let的寫法: 原理是用立即執(zhí)行函數(shù)創(chuàng)造一個(gè)塊級(jí)作用域
for(var i = 1; i <= 10; i++){ (function (i) { setTimeout(function () { console.log(i); }, 1000 * i) })(i); }
15. 創(chuàng)建10個(gè)標(biāo)簽,點(diǎn)擊的時(shí)候彈出來對(duì)應(yīng)的序號(hào)?
var a for(let i=0;i<10;i++){ a=document.createElement('a') a.innerHTML=i+'<br>' a.addEventListener('click',function(e){ console.log(this) //this為當(dāng)前點(diǎn)擊的<a> e.preventDefault() //如果調(diào)用這個(gè)方法,默認(rèn)事件行為將不再觸發(fā)。 //例如,在執(zhí)行這個(gè)方法后,如果點(diǎn)擊一個(gè)鏈接(a標(biāo)簽),瀏覽器不會(huì)跳轉(zhuǎn)到新的 URL 去了。我們可以用 event.isDefaultPrevented() 來確定這個(gè)方法是否(在那個(gè)事件對(duì)象上)被調(diào)用過了。 alert(i) }) const d=document.querySelector('div') d.appendChild(a) //append向一個(gè)已存在的元素追加該元素。 }
16. 實(shí)現(xiàn)事件訂閱發(fā)布(eventBus)
實(shí)現(xiàn)EventBus類,有 on off once trigger
功能,分別對(duì)應(yīng)綁定事件監(jiān)聽器,解綁,執(zhí)行一次后解除事件綁定,觸發(fā)事件監(jiān)聽器。 這個(gè)題目面字節(jié)和快手都問到了,最近忙,答案會(huì)在后續(xù)更新
class EventBus { on(eventName, listener) {} off(eventName, listener) {} once(eventName, listener) {} trigger(eventName) {} } const e = new EventBus(); // fn1 fn2 e.on('e1', fn1) e.once('e1', fn2) e.trigger('e1') // fn1() fn2() e.trigger('e1') // fn1() e.off('e1', fn1) e.trigger('e1') // null
實(shí)現(xiàn):
//聲明類 class EventBus { constructor() { this.eventList = {} //創(chuàng)建對(duì)象收集事件 } //發(fā)布事件 $on(eventName, fn) { //判斷是否發(fā)布過事件名稱? 添加發(fā)布 : 創(chuàng)建并添加發(fā)布 this.eventList[eventName] ? this.eventList[eventName].push(fn) : (this.eventList[eventName] = [fn]) } //訂閱事件 $emit(eventName) { if (!eventName) throw new Error('請(qǐng)傳入事件名') //獲取訂閱傳參 const data = [...arguments].slice(1) if (this.eventList[eventName]) { this.eventList[eventName].forEach((i) => { try { i(...data) //輪詢事件 } catch (e) { console.error(e + 'eventName:' + eventName) //收集執(zhí)行時(shí)的報(bào)錯(cuò) } }) } } //執(zhí)行一次 $once(eventName, fn) { const _this = this function onceHandle() { fn.apply(null, arguments) _this.$off(eventName, onceHandle) //執(zhí)行成功后取消監(jiān)聽 } this.$on(eventName, onceHandle) } //取消訂閱 $off(eventName, fn) { //不傳入?yún)?shù)時(shí)取消全部訂閱 if (!arguments.length) { return (this.eventList = {}) } //eventName傳入的是數(shù)組時(shí),取消多個(gè)訂閱 if (Array.isArray(eventName)) { return eventName.forEach((event) => { this.$off(event, fn) }) } //不傳入fn時(shí)取消事件名下的所有隊(duì)列 if (arguments.length === 1 || !fn) { this.eventList[eventName] = [] } //取消事件名下的fn this.eventList[eventName] = this.eventList[eventName].filter( (f) => f !== fn ) } } const event = new EventBus() let b = function (v1, v2, v3) { console.log('b', v1, v2, v3) } let a = function () { console.log('a') } event.$once('test', a) event.$on('test', b) event.$emit('test', 1, 2, 3, 45, 123) event.$off(['test'], b) event.$emit('test', 1, 2, 3, 45, 123)
到此這篇關(guān)于前端面試js高頻手寫大全的文章就介紹到這了,更多相關(guān)js高頻手寫大全內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS圖形編輯器實(shí)現(xiàn)標(biāo)尺功能示例詳解
這篇文章主要為大家介紹了JS圖形編輯器實(shí)現(xiàn)標(biāo)尺功能示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01Javascript中彈窗confirm與prompt的區(qū)別
今天小編就為大家分享一篇關(guān)于Javascript中彈窗confirm與prompt的區(qū)別,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2018-10-10C#微信小程序服務(wù)端獲取用戶解密信息實(shí)例代碼
這篇文章主要介紹了 C#微信小程序服務(wù)端獲取用戶解密信息實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-03-03JS輕量級(jí)函數(shù)式編程實(shí)現(xiàn)XDM二
這篇文章主要為大家介紹了JS函數(shù)式編程實(shí)現(xiàn)XDM示例詳解第2/3篇,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06微信小程序 (七)數(shù)據(jù)綁定詳細(xì)介紹
這篇文章主要介紹了微信小程序數(shù)據(jù)綁定詳細(xì)介紹的相關(guān)資料,需要的朋友可以參考下2016-09-09微信小程序?qū)崿F(xiàn)給循環(huán)列表添加點(diǎn)擊樣式實(shí)例
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)給循環(huán)列表添加點(diǎn)擊樣式實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-04-04微信小程序 數(shù)據(jù)綁定及運(yùn)算的簡(jiǎn)單實(shí)例
這篇文章主要介紹了微信小程序 數(shù)據(jù)綁定的簡(jiǎn)單實(shí)例的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-09-09TS?項(xiàng)目中高效處理接口返回?cái)?shù)據(jù)方法詳解
這篇文章主要為大家介紹了TS?項(xiàng)目中如何高效的處理接口返回的數(shù)據(jù)的方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01