前端面試JavaScript高頻手寫大全
目錄
- 1. 手寫instanceof
- 2. 實現陣列的map方法
- 3. reduce實現陣列的map方法
- 4. 手寫陣列的reduce方法
- 5. 陣列扁平化
- 5. 1 es6提供的新方法 flat(depth)
- 5.2 利用cancat
- 6. 函式柯里化
- 7. 淺拷貝和深拷貝的實現
- 7.1淺拷貝和深拷貝的區別
- 8. 手寫call,apply,bind
- 8.1 手寫call
- 8.2 手寫apply(arguments[this,[引數1,引數2.....] ])
- 8.3 手寫bind
- 9. 手動實現new
- 10. 手寫promise(常考promise.all,promise.race)
- 11. 手寫原生AJAX
- 12. 手寫節流防抖函式
- 12.1 防抖的實現
- 12.2 節流的實現
- 13. 手寫Promise載入圖片
- 14. 函式實現一秒鐘輸出一個數
- 15. 建立10個標籤,點選的時候彈出來對應的序號?
- 16. 實現事件訂閱釋出(eventBus)
在前端面試中,手撕程式碼顯然是不可避免的,並且佔很大的一部分比重。
一般來說,如果程式碼寫的好,即使理論知識答得不夠清楚,也能有大概率通過面試。並且其實很多手寫往往背後就考察了你對相關理論的認識。
題主要分為這幾種型別:
- 演算法題
- 涉及原理的題以及ajax請求
- 業務場景題: 實現一個具有某種功能的元件
- 其他(進階,對計算機綜合知識的考察,考的相對較少):實現訂閱釋出者模式;分別用面向物件程式設計,面向過程程式設計,函數語言程式設計實現把大象放進冰箱等等
其中前兩種型別所佔比重最大。
演算法題建議養成每天刷一道leetcode
的習慣,重點刷資料結構(棧,連結串列,佇列,樹),動態規劃,DFS
,BFS
本文主要涵蓋了第二種型別的各種重點手寫。
建議優先掌握:
instanceof
(考察對原型鏈的理解)new
(對建立物件例項過程的理解)call&apply&bind
(對this指向的理解)- 手寫
promise
(對非同步的理解) - 手寫原生
ajax
(對ajax原理和http請求方式的理解,重點是get和post請求的實現) - 事件訂閱釋出 (高頻考點)
- 其他:
reduce
方法比較難,這塊有餘力可以單獨看一些,即使面試沒讓你實現reduce
,寫其他題時用上它也是很加分的)
1. 手寫instanceof
instanceof作用:
判斷一個例項是否是其父類或者祖先型別的例項。
instanceof 在查詢的過程中會遍歷左邊變數的原型鏈,直到找到右邊變數的 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. 實現陣列的map方法
陣列的map()
方法會返回一個新的陣列,這個新陣列中的每個元素對應原陣列中的對應位置元素呼叫一次提供的函式後的返回值。
用法:
const a = [1,3,4]; const b = array1.map(x => x * 2); console.log(b); // Array [2,4,6,8]
實現前,我們先看一下map方法的引數有哪些
map
方法有兩個引數,一個是運算元組元素的方法fn,一個是this指向(可選),其中使用fn時可以獲取三個引數,實現時記得不要漏掉,這樣才算完整實現嘛
原生實現:
// 實現 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)) // 引數分別為this指向,當前陣列項,當前索引,當前陣列 } return res } // 使用 const a = [1,3]; const b = a.myMap((a,index)=> { return a+1; } ) console.log(b) // 輸出 [2,4]
3. reduce實現陣列的map方法
利用陣列內建的reduce
方法實現map方法,考察對reduce
原理的掌握
Array.prototype.myMap = function(fn,thisValue){ var res = []; thisValue = thisValue||[]; this.reduce(function(pre,cur,index,arr){ return res.push(fn.call(thisValue,arr)); },[]); return res; } var arr = [2,1,5]; arr.myMap(function(item,arr){ console.log(item,arr); })
4. 手寫陣列的reduce方法
reduce()
方法接收一個函式作為累加器,陣列中的每個值(從左到右)開始縮減,最終為一個值,是ES5中新增的又一個數組逐項處理方法
引數:
- callback(一個在陣列中每一項上呼叫的函式,接受四個函式:)
- previousValue(上一次呼叫回撥函式時的返回值,或者初始值)
- currentValue(當前正在處理的陣列元素)
- currentIndex(當前正在處理的陣列元素下標)
- array(呼叫reduce()方法的陣列)
- initialValue(可選的初始值。作為第一次呼叫回撥函式時傳給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,i) } return num } function fn(result,currentValue,index){ return result + currentValue } var arr = [2,5] var b = reduce(arr,fn,10) var c = reduce(arr,fn) console.log(b) // 24
5. 陣列扁平化
陣列扁平化就是把多維陣列轉化成一維陣列
5. 1 es6提供的新方法 flat(depth)
let a = [1,[2,3]];
a.flat(); // [1,3]
a.flat(1); //[1,3]
其實還有一種更簡單的辦法,無需知道陣列的維度,直接將目標陣列變成1維陣列。 depth的值設定為Infinity。
let a = [1,[4,[5]]]];
a.flat(Infinity); // [1,5] a是4維陣列
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 並不會改變原陣列 //res.push(...flatten(arr[i])); //或者用擴充套件運算子 } else { res.push(arr[i]); } } return res; } let arr1 = [1,[3,1],4]]] flatten(arr1); //[1,4]
補充:指定deep的flat
只需每次遞迴時將當前deep-1
,若大於0,則可以繼續展開
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,3],4],2]]],1));
6. 函式柯里化
用的這裡的方法 https://juejin.im/post/684490...
柯里化的定義:接收一部分引數,返回一個函式接收剩餘引數,接收足夠引數後,執行原函式。
當柯里化函式接收到足夠引數後,就會執行原函式,如何去確定何時達到足夠的引數呢?
有兩種思路:
- 通過函式的 length 屬性,獲取函式的形參個數,形參的個數就是所需的引數個數
- 在呼叫柯里化工具函式時,手動指定所需的引數個數
將這兩點結合一下,實現一個簡單 curry 函式:
/** * 將函式柯里化 * @param fn 待柯里化的原函式 * @param len 所需的引數個數,預設為原函式的形參個數 */ function curry(fn,len = fn.length) { return _curry.call(this,len) } /** * 中轉函式 * @param fn 待柯里化的原函式 * @param len 所需的引數個數 * @param args 已接收的引數列表 */ 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,..._args) } } }
我們來驗證一下:
let _fn = curry(function(a,b,c,d,e){ console.log(a,e) }); _fn(1,5); // print: 1,5 _fn(1)(2)(3,5); // print: 1,5 _fn(1,2)(3,4)(5); // print: 1,5 _fn(1)(2)(3)(4)(5); // print: 1,5
我們常用的工具庫 lodash
也提供了 curry
方法,並且增加了非常好玩的 placeholder
功能,通過佔位符的方式來改變傳入引數的順序。
比如說,我們傳入一個佔位符,本次呼叫傳遞的引數略過佔位符, 佔位符所在的位置由下次呼叫的引數來填充,比如這樣:
直接看一下官網的例子:
接下來我們來思考,如何實現佔位符的功能。
對於 lodash
的 curry
函式來說,curry 函式掛載在 lodash
物件上,所以將 lodash 物件當做預設佔位符來使用。
而我們的自己實現的 curry 函式,本身並沒有掛載在任何物件上,所以將 curry 函式當做預設佔位符
使用佔位符,目的是改變引數傳遞的順序,所以在 curry 函式實現中,每次需要記錄是否使用了佔位符,並且記錄佔位符所代表的引數位置。
直接上程式碼:
/** * @param fn 待柯里化的函式 * @param length 需要的引數個數,預設為函式的形參個數 * @param holder 佔位符,預設當前柯里化函式 * @return {Function} 柯里化後的函式 */ function curry(fn,length = fn.length,holder = curry){ return _curry.call(this,length,holder,[],[]) } /** * 中轉函式 * @param fn 柯里化的原函式 * @param length 原函式需要的引數個數 * @param holder 接收的佔位符 * @param args 已接收的引數列表 * @param holders 已接收的佔位符位置列表 * @return {Function} 繼續柯里化的函式 或 最終結果 */ function _curry(fn,args,holders){ return function(..._args){ //將引數複製一份,避免多次操作同一函式導致引數混亂 let params = args.slice(); //將佔位符位置列表複製一份,新增加的佔位符增加至此 let _holders = holders.slice(); //迴圈入參,追加引數 或 替換佔位符 _args.forEach((arg,i)=>{ //真實引數 之前存在佔位符 將佔位符替換為真實引數 if (arg !== holder && holders.length) { let index = holders.shift(); _holders.splice(_holders.indexOf(index),1); params[index] = arg; } //真實引數 之前不存在佔位符 將引數追加到引數列表中 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 條記錄中不包含佔位符,執行函式 if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){ return fn.apply(this,params); }else{ return _curry.call(this,params,_holders) } } }
驗證一下:
let fn = function(a,e) { console.log([a,e]); } let _ = {}; // 定義佔位符 let _fn = curry(fn,5,_); // 將函式柯里化,指定所需的引數個數,指定所需的佔位符 _fn(1,5); // print: 1,5 _fn(_,5)(1); // print: 1,_,5)(2); // print: 1,3)(_,_)(2)(5); // print: 1,4)(_,3)(2)(5); // print: 1,2)(_,4)(1)(3)(5); // print: 1,5
至此,我們已經完整實現了一個 curry
函式~~
7. 淺拷貝和深拷貝的實現
深拷貝和淺拷貝是隻針對Object
和Array
這樣的引用資料型別的。
7.1淺拷貝和深拷貝的區別
淺拷貝:建立一個新物件,這個物件有著原始物件屬性值的一份精確拷貝。如果屬性是基本型別,拷貝的就是基本型別的值,如果屬性是引用型別,拷貝的就是記憶體地址,如果其中一個物件改變了引用型別的屬性,就會影響到另一個物件。
深拷貝:將一個物件從記憶體中完整的複製一份出來,從堆記憶體中開闢一個新區域存放。這樣更改拷貝值就不影響舊的物件
淺拷貝實現:
方法一:
function shallowCopy(target,origin){ for(let item in origin) target[item] = origin[item]; return target; }
其他方法(內建api):
(1)Object.assign
var obj={a:1,b:[1,c:function(){console.log('i am c')}} var tar={}; Object.assign(tar,obj);
當然這個方法只適合於物件型別,如果是陣列可以使用slice
和concat
方法
(2)Array.prototype.slice
var arr=[1,4]]; var newArr=arr.slice(0); Array.prototype.concat var arr=[1,4]]; var newArr=arr.concat();
(3)Array.prototype.concat
var arr=[1,4]]; var newArr=arr.concat();
測試同上(assign用物件測試、slice concat用陣列測試),結合淺拷貝深拷貝的概念來理解效果更佳
深拷貝實現:
方法一:
轉為json格式再解析
const a = JSON.parse(JSON.stringify(b))
方法二:
// 實現深拷貝 遞迴 function deepCopy(newObj,oldObj){ for(var k in oldObj){ let item=oldObj[k] // 判斷是陣列、物件、簡單型別? if(item instanceof Array){ newObj[k]=[] deepCopy(newObj[k],item) }else if(item instanceof Object){ newObj[k]={} deepCopy(newObj[k],item) }else{ //簡單資料型別,直接賦值 newObj[k]=item } } }
8. 手寫call,bind
8.1 手寫call
Function.prototype.myCall=function(context=window){ // 函式的方法,所以寫在Fuction原型物件上 if(typeof this !=="function"){ // 這裡if其實沒必要,會自動丟擲錯誤 throw new Error("不是函式") } const obj=context||window //這裡可用ES6方法,為引數新增預設值,js嚴格模式全域性作用域this為undefined obj.fn=this //this為呼叫的上下文,this此處為函式,將這個函式作為obj的方法 const arg=[...arguments].slice(1) //第一個為obj所以刪除,偽陣列轉為陣列 res=obj.fn(...arg) delete obj.fn // 不刪除會導致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,2) //否則this指向window obj.greet.call({name: 'Spike'}) //打出來的是 Spike
8.2 手寫apply(arguments[this,[引數1,引數2.....] ])
Function.prototype.myApply=function(context){ // 箭頭函式從不具有引數物件!!!!!這裡不能寫成箭頭函式 let obj=context||window obj.fn=this const arg=arguments[1]||[] //若有引數,得到的是陣列 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,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) // '家裡蹲大學' } var result = bar.bind(foo,'An') //預置了部分引數'An' result(22,'家裡蹲大學') //這個引數會和預置的引數合併到一起放入bar中
簡單版本
Function.prototype.bind = function(context,...outerArgs) { var fn = this; return function(...innerArgs) { //返回了一個函式,...rest為實際呼叫時傳入的引數 return fn.apply(context,[...outerArgs,...innerArgs]); //返回改變了this的函式, //引數合併 } }
new失敗的原因:
例:
// 宣告一個上下文 let thovino = { name: 'thovino' } // 宣告一個建構函式 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') //實際上orange放到了thovino裡面 console.log('instance:',instance) // {}
生成的例項是個空物件
在new
操作符執行時,我們的thovinoEat
函式可以看作是這樣:
function thovinoEat (...innerArgs) { eat.call(thovino,...outerArgs,...innerArgs) }
在new操作符進行到第三步的操作thovinoEat.call(obj,...args
)時,這裡的obj是new操作符自己建立的那個簡單空物件{},但它其實並沒有替換掉thovinoEat
函式內部的那個上下文物件thovino。這已經超出了call的能力範圍,因為這個時候要替換的已經不是thovinoEat
函式內部的this指向,而應該是thovino
物件。
換句話說,我們希望的是new操作符將eat
內的this指向操作符自己建立的那個空物件。但是實際上指向了thovino
,new
操作符的第三步動作並沒有成功!
可new可繼承版本
Function.prototype.bind = function (context,...outerArgs) { let that = this; function res (...innerArgs) { if (this instanceof res) { // new操作符執行時 // 這裡的this在new操作符第三步操作時,會指向new自身建立的那個簡單空物件{} that.call(this,...innerArgs) } else { // 普通bind that.call(context,...innerArgs) } } res.prototype = this.prototype //!!! return res }
9. 手動實現new
new的過程文字描述:
- 建立一個空物件 obj;
- 將空物件的隱式原型(proto)指向建構函式的prototype。
- 使用 call 改變 this 的指向
- 如果無返回值或者返回一個非物件值,則將 obj 返回作為新物件;如果返回值是一個新物件的話那麼直接直接返回該物件。
function Person(name,age){ this.name=name this.age=age } Person.prototype.sayHi=function(){ console.log('Hi!我是'+this.name) } let p1=new Person('張三',18) ////手動實現new function create(){ let obj={} //獲取建構函式 let fn=[].shift.call(arguments) //將arguments物件提出來轉化為陣列,arguments並不是陣列而是物件 !!!這種方法刪除了arguments陣列的第一個元素,!!這裡的空數組裡面填不填元素都沒關係,不影響arguments的結果 或者let arg = [].slice.call(arguments,1) obj.__proto__=fn.prototype let res=fn.apply(obj,arguments) //改變this指向,為例項新增方法和屬性 //確保返回的是一個物件(萬一fn不是建構函式) return typeof res==='object'?res:obj } let p2=create(Person,'李四',19) p2.sayHi()
細節:
[].shift.call(arguments) 也可寫成: let arg=[...arguments] let fn=arg.shift() //使得arguments能呼叫陣列方法,第一個引數為建構函式 obj.__proto__=fn.prototype //改變this指向,為例項新增方法和屬性 let res=fn.apply(obj,arg)
10. 手寫promise(常考promise.all,promise.race)
// Promise/A+ 規範規定的三種狀態
const STATUS = {
PENDING: 'pending',FULFILLED: 'fulfilled',REJECTED: 'rejected'
}
class MyPromise {
// 建構函式接收一個執行回撥
constructor(executor) {
this._status = STATUS.PENDING // Promise初始狀態
this._value = undefined // then回撥的值
this._resolveQueue = [] // resolve時觸發的成功佇列
this._rejectQueue = [] // reject時觸發的失敗佇列
// 使用箭頭函式固定this(resolve函式在executor中觸發,不然找不到this)
const resolve = value => {
const run = () => {
// Promise/A+ 規範規定的Promise狀態只能從pending觸發,變成fulfilled
if (this._status === STATUS.PENDING) {
this._status = STATUS.FULFILLED // 更改狀態
this._value = value // 儲存當前值,用於then回撥
// 執行resolve回撥
while (this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(value)
}
}
}
//把resolve執行回撥的操作封裝成一個函式,放進setTimeout裡,以實現promise非同步呼叫的特性(規範上是微任務,這裡是巨集任務)
setTimeout(run)
}
// 同 resolve
const rejechttp://www.cppcns.comt = 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()時立即執行executor,並傳入resolve和reject
executor(resolve,reject)
}
// then方法,接收一個成功的回撥和一個失敗的回撥
function then(onFulfilled,onRejected) {
// 根據規範,如果then的引數不是function,則忽略它,讓值繼續往下傳遞,鏈式呼叫繼續往下執行
typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
typeof onRejected !== 'function' ? onRejected = error => error : null
// then 返回一個程式設計客棧新的promise
return new MyPromise((resolve,reject) => {
const resolveFn = value => {
try {
const x = onFulfilled(value)
// 分類討論返回值,如果是Promise,那麼等待Promise狀態變更,否則直接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)
})
}
// 靜態resolve方法
static resolve(value) {
return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
}
// 靜態reject方法
static reject(error) {
return new MyPromise((resolve,reject) => reject(error))
}
// 靜態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)
})
})
})
}
// 靜態race方法
static race(promiseArr) {
return new MyPromise((resolve,reject) => {
promiseArr.forEach(p => {
MyPromise.resolve(p).then(value => {
resolve(value)
},error => {
reject(error)
})
})
})
}
}
11. 手寫原生AJAX
步驟:
- 建立 XMLHttpRequest 例項
- 發出 HTTP 請求
- 伺服器返回 XML 格式的字串
- JS 解析 XML,並更新區域性頁面
不過隨著歷史程序的推進,XML 已經被淘汰,取而代之的是 JSON。
瞭解了屬性和方法之後,根據 AJAX 的步驟,手寫最簡單的 GET 請求。
version 1.0:
myButton.addEventListener('click',function () { ajax() }) function ajax() { let xhr = new XMLHttpRequest() //例項化,以呼叫方法 xhr.open('get','https://www.google.com') //引數2,url。引數三:非同步 xhr.onreadystatechange = () => { //每當 readyState 屬性改變時,就會呼叫該函式。 if (xhr.readyState === 4) { //XMLHttpRequest 代理當前所處狀態。 if (xhr.status >= 200 && xhr.status < 300) { //200-300請求成功 let string = request.responseText //JSON.parse() 方法用來解析JSON字串,構造由字串描述的值或物件 let object = JSON.parse(string) } } } request.send() //用於實際發出 HTTP 請求。不帶引數為GET請求 }
promise實現
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('請求出錯') } } } xhr.send() //傳送hppt請求 }) return p } let url = '/data.json' ajax(url).then(res => console.log(res)) .catch(reason => console.log(reason))
12. 手寫節流防抖函式
函式節流與函式防抖都是為了限制函式的執行頻次,是一種效能優化的方案,比如應用於window
物件的resize
、scroll
事件,拖拽時的mousemove
事件,文字輸入、自動完成的keyup
事件。
節流:連續觸發事件但是在 n 秒中只執行一次函式
例:(連續不斷動都需要呼叫時用,設一時間間隔),像dom的拖拽,如果用消抖的話,就會出現卡頓的感覺,因為只在停止的時候執行了一次,這個時候就應該用節流,在一定時間內多次執行,會流暢很多。
防抖:指觸發事件後在 n 秒內函式只能執行一次,如果在 n 秒內又觸發了事件,則會重新計算函式執行時間。
例:(連續不斷觸發時不呼叫,觸發完後過一段時間呼叫),像仿百度搜索,就應該用防抖,當我連續不斷輸入時,不會發送請求;當我一段時間內不輸入了,才會傳送一次請求;如果小於這段時間繼續輸入的話,時間會重新計算,也不會發送請求。
12.1 防抖的實現
function debounce(fn,delay) { if(typeof fn!=='function') { throw new TypeError('fn不是函式') } let timer; // 維護一個 timer return function () { var _this = this; // 取debounce執行作用域的this(原函式掛載到的物件) var args = arguments; if (timer) { clearTimeout(timer); } timer = setTimeout(function () { fn.apply(_this,args); // 用apply指向呼叫debounce的物件,相當於_this.fn(args); },delay); }; } // 呼叫 input1.addEventListener('keyup',debounce(() => { console.log(input1.value) }),600)
12.2 節流的實現
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接收的是外邊返回的函式的引數,不能用arguments // fn.apply(_this,arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受類陣列物件。如果傳入類陣列物件,它們會丟擲異常。 timer = null; // 在delay後執行完fn之後清空timer,此時timer為假,throttle觸發可以進入計時器 },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. 函式實現一秒鐘輸出一個數
(!!!這個題這兩天位元組校招面試被問到了,問var列印的什麼,改為let為什麼可以?
有沒有其他方法實現?我自己部落格裡都寫了不用let的寫法第二種方法,居然給忘了~~~白學了)
ES6:用let塊級作用域的原理實現
for(let i=0;i<=10;i++){ //用var列印的都是11 setTimeout(()=>{ console.log(i); },1000*i) }
不用let的寫法: 原理是用立即執行函式創造一個塊級作用域
for(var i = 1; i <= 10; i++){ (function (i) { setTimeout(function () { console.log(i); },1000 * i) })(i); }
15. 建立10個標籤,點選的時候彈出來對應的序號?
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為當前點選的<a> e.preventDefault() //如果呼叫這個方法,預設事件行為將不再觸發。 //例如,在執行這個方法後,如果點選一個連結(a標籤),瀏覽器不會跳轉到新的 URL 去了。我們可以用 event.isDefaultPrevented() 來確定這個方法是否(在那個事件物件上)被呼叫過了。 alert(i) }) const d=document.querySelector('div') d.appendChild(a) //append向一個已存在的元素追加該元素。 }
16. 實現事件訂閱釋出(eventBus)
實現EventBus類,有 on off once trigger
功能,分別對應繫結事件監聽器,解綁,執行一次後解除事件繫結,觸發事件監聽器。 這個題目面位元組和快手都問到了,最近忙,答案會在後續更新
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
實現:
//宣告類
class EventBus {
constructor() {
this.eventList = {} //建立物件收集事件
}
//釋出事件
$on(eventName,fn) {
//判斷是否釋出過事件名稱? 添加發布 : 建立並添加發布
this.eventList[eventName]
? this.eventList[eventName].push(fn)
: (this.eventList[eventName] = [fn])
}
//訂閱事件
$emit(eventName) {
if (!eventName) throw new Error('請傳入事件名')
//獲取訂閱傳參
const data = [...arguments].slice(1)
if (this.eventList[eventName]) {
this.eventList[eventName].forEach((i) => {
try {
i(...data) //輪詢事件
} catch (e) {
console.error(e + 'eventName:' + eventName) //收集執行時的報錯
}
})
}
}
//執行一次
$once(eventName,fn) {
const _this = this
function onceHandle() {
fn.apply(null,arguments)
_this.$off(eventName,onceHandle) //執行成功後取消監聽
}
this.$on(eventName,onceHandle)
}
//取消訂閱
$off(eventName,fn) {
//不傳入引數時取消全部訂閱
if (!arguments.length) {
return (this.eventList = {})
}
//eventName傳入的是陣列時,取消多個訂閱
if (Array.isArray(eventName)) {
www.cppcns.com return eventName.forEach((event) => {
this.$off(event,fn)
})
}
//不傳入fn時取消事件名下的所有佇列
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,v3)
}
let a = function () {
console.log('a')
}
event.$once('test',a)
event.$on('test',b)
event.$emit('test',45,123)
event.$off(['test'],b)
event.$emit('test',123)
到此這篇關於前端面試js高頻手寫大全的文章就介紹到這了,更多相關js高頻手寫大全內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!