前端面試js高頻手寫大全

本文涵蓋了前端面試常考的各類重點手寫。
建議優先掌握:html

  • instanceof (考察對原型鏈的理解)
  • new (對建立對象實例過程的理解)
  • call&apply&bind (對this指向的理解)
  • 手寫promise (對異步的理解)
  • 手寫原生ajax (對http請求方式的理解,重點是get和post請求)

1. 手寫instanceof

instanceof做用:前端

判斷一個實例是不是其父類或者祖先類型的實例。es6

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中新增的又一個數組逐項處理方法ajax

參數:json

  • 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. 數組扁平化

數組扁平化就是把多維數組轉化成一維數組promise

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 功能,經過佔位符的方式來改變傳入參數的順序。

好比說,咱們傳入一個佔位符,本次調用傳遞的參數略過佔位符, 佔位符所在的位置由下次調用的參數來填充,好比這樣:

直接看一下官網的例子:

img

接下來咱們來思考,如何實現佔位符的功能。

對於 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指向操做符本身建立的那個空對象。可是實際上指向了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

步驟

  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向一個已存在的元素追加該元素。
}

參考:

數組扁平化 https://juejin.im/post/5c971ee16fb9a070ce31b64e#heading-3

函數柯里化 https://juejin.im/post/6844903882208837645

相關文章
相關標籤/搜索