天天看點

前端面試js高頻手寫大全

本文涵蓋了前端面試常考的各種重點手寫。

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方法

Array.prototype.myMap = function(fn, thisValue) {
     let res = []
     thisValue = thisValue||[]
     let arr = this
     for(let i in arr) {
        res.push(fn(arr[i]))
     }
     return res
 }      

3. reduce實作數組的map方法

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(一個在數組中每一項上調用的函數,接受四個函數:)
    • 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,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. 數組扁平化

數組扁平化就是把多元數組轉化成一維數組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維數組      

2. 利用cancat

var arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]];
 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;
 }
 flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]      

6. 函數柯裡化

柯裡化的定義:接收一部分參數,傳回一個函數接收剩餘參數,接收足夠參數後,執行原函數。當柯裡化函數接收到足夠參數後,就會執行原函數,如何去确定何時達到足夠的參數呢?有兩種思路:

  1. 通過函數的 length 屬性,擷取函數的形參個數,形參的個數就是所需的參數個數
  2. 在調用柯裡化工具函數時,手動指定所需的參數個數

将這兩點結合一下,實作一個簡單 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 功能,通過占位符的方式來改變傳入參數的順序。比如說,我們傳入一個占位符,本次調用傳遞的參數略過占位符, 占位符所在的位置由下次調用的參數來填充,比如這樣:直接看一下官網的例子:

前端面試js高頻手寫大全

接下來我們來思考,如何實作占位符的功能。對于 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. 實作深拷貝

淺拷貝和深拷貝的差別:淺拷貝:隻拷貝一層,更深層的對象級别的隻拷貝引用深拷貝:拷貝多層,每一級别的資料都會拷貝。這樣更改拷貝值就不影響另外的對象ES6淺拷貝方法:Object.assign(target,...sources)

let obj={
 id:1,
 name:'Tom',
 msg:{
 age:18
 }
}
let o={}
//實作深拷貝  遞歸    可以用于生命遊戲那個題對二維數組的拷貝,
//但比較麻煩,因為已知元素都是值,直接複制就行,無需判斷
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

手寫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      

手寫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]      

手寫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, ...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

步驟

  1. 建立 XMLHttpRequest 執行個體
  2. 發出 HTTP 請求
  3. 伺服器傳回 XML 格式的字元串
  4. 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. 手寫節流防抖函數

防抖:

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)      

節流:

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. 函數實作一秒鐘輸出一個數

for(let i=0;i<=10;i++){   //用var列印的都是11
 setTimeout(()=>{
    console.log(i);
 },1000*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向一個已存在的元素追加該元素。
}      

- EOF -

推薦閱讀  點選标題可跳轉

手寫面試代碼大全

手撕 32 個面試高頻知識,輕松應對程式設計題

公衆号也開始通過互動率推送了,互動少了可能就很晚或者收不到文章了。