首頁 > 軟體

前端面試JavaScript高頻手寫大全

2021-09-15 16:01:08

在前端面試中,手撕程式碼顯然是不可避免的,並且佔很大的一部分比重。
一般來說,如果程式碼寫的好,即使理論知識答得不夠清楚,也能有大概率通過面試。並且其實很多手寫往往背後就考察了你對相關理論的認識。

程式設計題主要分為這幾種型別:

  • 演演算法題
  • 涉及js原理的題以及ajax請求
  • 業務場景題: 實現一個具有某種功能的元件
  • 其他(進階,對計算機綜合知識的考察,考的相對較少):實現訂閱釋出者模式;分別用物件導向程式設計,程式導向程式設計,函數語言程式設計實現把大象放進冰箱等等

其中前兩種型別所佔比重最大。
演演算法題建議養成每天刷一道leetcode的習慣,重點刷資料結構(棧,連結串列,佇列,樹),動態規劃,DFSBFS

本文主要涵蓋了第二種型別的各種重點手寫。

建議優先掌握:

  • instanceof (考察對原型鏈的理解)
  • new (對建立物件範例過程的理解)
  • call&apply&bind (對this指向的理解)
  • 手寫promise (對非同步的理解)
  • 手寫原生ajax (對ajax原理和http請求方式的理解,重點是get和post請求的實現)
  • 事件訂閱釋出 (高頻考點)
  • 其他:陣列,字串的api的實現,難度相對較低。只要瞭解陣列,字串的常用方法的用法,現場就能寫出來個大概。(ps:筆者認為陣列的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, 2, 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,2,3];
        const b = a.myMap((a,index)=> {
                return a+1; 
            }
        )
        console.log(b)   // 輸出 [2, 3, 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,cur,index,arr));
     },[]);
     return res;
}
​
var arr = [2,3,1,5];
arr.myMap(function(item,index,arr){
 console.log(item,index,arr);
})


4. 手寫陣列的reduce方法

reduce() 方法接收一個函數作為累加器,陣列中的每個值(從左到右)開始縮減,最終為一個值,是ES5中新增的又一個陣列逐項處理方法

引數:

  • callback(一個在陣列中每一項上呼叫的函數,接受四個函數:)
  1. previousValue(上一次呼叫回撥函數時的返回值,或者初始值)
  2. currentValue(當前正在處理的陣列元素)
  3. currentIndex(當前正在處理的陣列元素下標)
  4. 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,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. 陣列扁平化

陣列扁平化就是把多維陣列轉化成一維陣列

5. 1 es6提供的新方法 flat(depth)

let a = [1,[2,3]];
a.flat(); // [1,2,3]
a.flat(1); //[1,2,3]

其實還有一種更簡單的辦法,無需知道陣列的維度,直接將目標陣列變成1維陣列。 depth的值設定為Infinity。

let a = [1,[2,3,[4,[5]]]];
a.flat(Infinity); // [1,2,3,4,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, 2,[3,1],[2,3,4,[2,3,4]]]
flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 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,2,3],3,[2,4,[4,[3,4],2]]],1));


6. 函數柯里化

用之前可以瞭解之前的文章前端JavaScript徹底弄懂函數柯里化curry與這裡用的同樣方法

柯里化的定義:接收一部分引數,返回一個函數接收剩餘引數,接收足夠引數後,執行原函數。

當柯里化函數接收到足夠引數後,就會執行原函數,如何去確定何時達到足夠的引數呢?

有兩種思路:

  • 通過函數的 length 屬性,獲取函數的形參個數,形參的個數就是所需的引數個數
  • 在呼叫柯里化工具函數時,手動指定所需的引數個數

將這兩點結合一下,實現一個簡單 curry 函數:

/**
 * 將函數柯里化
 * @param fn    待柯里化的原函數
 * @param len   所需的引數個數,預設為原函數的形參個數
 */
function curry(fn,len = fn.length) {
 return _curry.call(this,fn,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,fn,len,..._args)
         }
    }
}


我們來驗證一下:

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 功能,通過預留位置的方式來改變傳入引數的順序。

比如說,我們傳入一個預留位置,本次呼叫傳遞的引數略過預留位置, 預留位置所在的位置由下次呼叫的引數來填充,比如這樣:

直接看一下官網的例子:

接下來我們來思考,如何實現預留位置的功能。

對於 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,fn,length,holder,[],[])
}
/**
 * 中轉函數
 * @param fn            柯里化的原函數
 * @param length        原函數需要的引數個數
 * @param holder        接收的預留位置
 * @param args          已接收的參數列
 * @param holders       已接收的預留位置位置列表
 * @return {Function}   繼續柯里化的函數 或 最終結果
 */
function _curry(fn,length,holder,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,fn,length,holder,params,_holders)
 }
 }
}


驗證一下:

let fn = function(a, b, c, d, e) {
 console.log([a, b, c, d, e]);
}
​
let _ = {}; // 定義預留位置
let _fn = curry(fn,5,_);  // 將函數柯里化,指定所需的引數個數,指定所需的預留位置
​
_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


至此,我們已經完整實現了一個 curry 函數~~

7. 淺拷貝和深拷貝的實現

深拷貝和淺拷貝是隻針對ObjectArray這樣的參照資料型別的。

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,2,3],c:function(){console.log('i am c')}}
var tar={};
Object.assign(tar,obj);


當然這個方法只適合於物件型別,如果是陣列可以使用sliceconcat方法

(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();

測試同上(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, apply, 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,1,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,[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) // '家裡蹲大學'
}
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指向操作符自己建立的那個空物件。但是實際上指向了thovinonew操作符的第三步動作並沒有成功!

可new可繼承版本

Function.prototype.bind = function (context, ...outerArgs) {
 let that = this;
​
function res (...innerArgs) {
     if (this instanceof res) {
         // new操作符執行時
         // 這裡的this在new操作符第三步操作時,會指向new自身建立的那個簡單空物件{}
         that.call(this, ...outerArgs, ...innerArgs)
     } else {
         // 普通bind
         that.call(context, ...outerArgs, ...innerArgs)
     }
     }
     res.prototype = this.prototype //!!!
     return res
}


9. 手動實現new

new的過程文字描述:

  1. 建立一個空物件 obj;
  2. 將空物件的隱式原型(proto)指向建構函式的prototype。
  3. 使用 call 改變 this 的指向
  4. 如果無返回值或者返回一個非物件值,則將 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 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()時立即執行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字串,構造由字串描述的JavaScript值或物件
        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物件的resizescroll事件,拖拽時的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)) {
            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, 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)

到此這篇關於前端面試js高頻手寫大全的文章就介紹到這了,更多相關js高頻手寫大全內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com