JavaScript中各類源碼實現(前端面試筆試必備)

前言

最近不少人和我同樣在積極地準備前端的面試筆試,因此我也就整理了一些前端面試筆試中很是容易被問到的原生函數實現和各類前端原理實現。javascript

可以手寫實現各類JavaScript原生函數,能夠說是擺脫API調用師帽子的第一步,咱們不光要會用,更要去探究其實現原理!前端

對JavaScript源碼的學習和實現能幫助咱們快速和紮實地提高本身的前端編程能力。java

實現一個new操做符

咱們首先知道new作了什麼:node

  1. 建立一個空的簡單JavaScript對象(即{});
  2. 連接該對象(即設置該對象的構造函數)到另外一個對象 ;
  3. 將步驟(1)新建立的對象做爲this的上下文 ;
  4. 若是該函數沒有返回對象,則返回this。

知道new作了什麼,接下來咱們就來實現它面試

function create(Con, ...args){
  // 建立一個空的對象
  this.obj = {};
  // 將空對象指向構造函數的原型鏈
  Object.setPrototypeOf(this.obj, Con.prototype);
  // obj綁定到構造函數上,即可以訪問構造函數中的屬性,即this.obj.Con(args)
  let result = Con.apply(this.obj, args);
  // 若是返回的result是一個對象則返回
  // new方法失效,不然返回obj
  return result instanceof Object ? result : this.obj;
}

實現一個Array.isArray

思路很簡單,就是利用Object.prototype.toString編程

Array.myIsArray = function(o) { 
  return Object.prototype.toString.call(Object(o)) === '[object Array]'; 
};

實現一個Object.create()方法

function create =  function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
};

實現一個EventEmitter

真實經歷,最近在字節跳動的面試中就被面試官問到了,讓我手寫實現一個簡單的Event類。segmentfault

class Event {
  constructor () {
    // 儲存事件的數據結構
    // 爲查找迅速, 使用對象(字典)
    this._cache = {}
  }

  // 綁定
  on(type, callback) {
    // 爲了按類查找方便和節省空間
    // 將同一類型事件放到一個數組中
    // 這裏的數組是隊列, 遵循先進先出
    // 即新綁定的事件先觸發
    let fns = (this._cache[type] = this._cache[type] || [])
    if(fns.indexOf(callback) === -1) {
      fns.push(callback)
    }
    return this
    }

  // 解綁
  off (type, callback) {
    let fns = this._cache[type]
    if(Array.isArray(fns)) {
      if(callback) {
        let index = fns.indexOf(callback)
        if(index !== -1) {
          fns.splice(index, 1)
        }
      } else {
        // 所有清空
        fns.length = 0
      }
    }
    return this
  }
  // 觸發emit
  trigger(type, data) {
    let fns = this._cache[type]
    if(Array.isArray(fns)) {
      fns.forEach((fn) => {
        fn(data)
      })
    }
    return this
  }

  // 一次性綁定
  once(type, callback) {
    let wrapFun = () => {
      callback.call(this);
      this.off(type, callback);
    };
    this.on(wrapFun, callback);
    return this;
  }
}

let e = new Event()

e.on('click',function(){
  console.log('on')
})
e.on('click',function(){
  console.log('onon')
})
// e.trigger('click', '666')
console.log(e)

實現一個Array.prototype.reduce

首先觀察一下Array.prototype.reduce語法數組

Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

而後就能夠動手實現了:promise

Array.prototype.myReduce = function(callback, initialValue) {
  let accumulator = initialValue ? initialValue : this[0];
  for (let i = initialValue ? 0 : 1; i < this.length; i++) {
    let _this = this;
    accumulator = callback(accumulator, this[i], i, _this);
  }
  return accumulator;
};

// 使用
let arr = [1, 2, 3, 4];
let sum = arr.myReduce((acc, val) => {
  acc += val;
  return acc;
}, 5);

console.log(sum); // 15

實現一個call或apply

先來看一個call實例,看看call到底作了什麼:數據結構

let foo = {
  value: 1
};
function bar() {
  console.log(this.value);
}
bar.call(foo); // 1

從代碼的執行結果,咱們能夠看到,call首先改變了this的指向,使函數的this指向了foo,而後使bar函數執行了。
總結一下:

  1. call改變函數this指向
  2. 調用函數

思考一下:咱們如何實現上面的效果呢?代碼改造以下:

Function.prototype.myCall = function(context) {
  context = context || window;
  //將函數掛載到對象的fn屬性上
  context.fn = this;
  //處理傳入的參數
  const args = [...arguments].slice(1);
  //經過對象的屬性調用該方法
  const result = context.fn(...args);
  //刪除該屬性
  delete context.fn;
  return result
};

咱們看一下上面的代碼:

  1. 首先咱們對參數context作了兼容處理,不傳值,context默認值爲window;
  2. 而後咱們將函數掛載到context上面,context.fn = this;
  3. 處理參數,將傳入myCall的參數截取,去除第一位,而後轉爲數組;
  4. 調用context.fn,此時fn的this指向context;
  5. 刪除對象上的屬性 delete context.fn;
  6. 將結果返回。

以此類推,咱們順便實現一下apply,惟一不一樣的是參數的處理,代碼以下:

Function.prototype.myApply = function(context) {
  context = context || window
  context.fn = this
  let result
  // myApply的參數形式爲(obj,[arg1,arg2,arg3]);
  // 因此myApply的第二個參數爲[arg1,arg2,arg3]
  // 這裏咱們用擴展運算符來處理一下參數的傳入方式
  if (arguments[1]) {
    result = context.fn(…arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn;
  return result
};

以上即是call和apply的模擬實現,惟一不一樣的是對參數的處理方式。

實現一個Function.prototype.bind

function Person(){
  this.name="zs";
  this.age=18;
  this.gender="男"
}
let obj={
  hobby:"看書"
}
//  將構造函數的this綁定爲obj
let changePerson = Person.bind(obj);
//  直接調用構造函數,函數會操做obj對象,給其添加三個屬性;
changePerson();
//  一、輸出obj
console.log(obj);
//  用改變了this指向的構造函數,new一個實例出來
let p = new changePerson();
// 二、輸出obj
console.log(p);

仔細觀察上面的代碼,再看輸出結果。

咱們對Person類使用了bind將其this指向obj,獲得了changeperson函數,此處若是咱們直接調用changeperson會改變obj,若用new調用changeperson會獲得實例 p,而且其__proto__指向Person,咱們發現bind失效了。

咱們獲得結論:用bind改變了this指向的函數,若是用new操做符來調用,bind將會失效。

這個對象就是這個構造函數的實例,那麼只要在函數內部執行 this instanceof 構造函數 來判斷其結果是否爲true,就能判斷函數是不是經過new操做符來調用了,若結果爲true則是用new操做符調用的,代碼修正以下:

// bind實現
Function.prototype.mybind = function(){
  // 一、保存函數
  let _this = this;
  // 二、保存目標對象
  let context = arguments[0]||window;
  // 三、保存目標對象以外的參數,將其轉化爲數組;
  let rest = Array.prototype.slice.call(arguments,1);
  // 四、返回一個待執行的函數
  return function F(){
    // 五、將二次傳遞的參數轉化爲數組;
    let rest2 = Array.prototype.slice.call(arguments)
    if(this instanceof F){
      // 六、如果用new操做符調用,則直接用new 調用原函數,並用擴展運算符傳遞參數
      return new _this(...rest2)
    }else{
      //七、用apply調用第一步保存的函數,並綁定this,傳遞合併的參數數組,即context._this(rest.concat(rest2))
      _this.apply(context,rest.concat(rest2));
    }
  }
};

實現一個JS函數柯里化

Currying的概念其實並不複雜,用通俗易懂的話說:只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。

function progressCurrying(fn, args) {

    let _this = this
    let len = fn.length;
    let args = args || [];

    return function() {
        let _args = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, _args);

        // 若是參數個數小於最初的fn.length,則遞歸調用,繼續收集參數
        if (_args.length < len) {
            return progressCurrying.call(_this, fn, _args);
        }

        // 參數收集完畢,則執行fn
        return fn.apply(this, _args);
    }
}

手寫防抖(Debouncing)和節流(Throttling)

節流

防抖函數 onscroll 結束時觸發一次,延遲執行

function debounce(func, wait) {
  let timeout;
  return function() {
    let context = this; // 指向全局
    let args = arguments;
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(() => {
      func.apply(context, args); // context.func(args)
    }, wait);
  };
}
// 使用
window.onscroll = debounce(function() {
  console.log('debounce');
}, 1000);
節流

節流函數 onscroll 時,每隔一段時間觸發一次,像水滴同樣

function throttle(fn, delay) {
  let prevTime = Date.now();
  return function() {
    let curTime = Date.now();
    if (curTime - prevTime > delay) {
      fn.apply(this, arguments);
      prevTime = curTime;
    }
  };
}
// 使用
var throtteScroll = throttle(function() {
  console.log('throtte');
}, 1000);
window.onscroll = throtteScroll;

手寫一個JS深拷貝

乞丐版

JSON.parse(JSON.stringfy));

很是簡單,但缺陷也很明顯,好比拷貝其餘引用類型、拷貝函數、循環引用等狀況。

基礎版

function clone(target){
  if(typeof target === 'object'){
    let cloneTarget = {};
    for(const key in target){
      cloneTarget[key] = clone(target[key])
    }
    return cloneTarget;
  } else {
    return target
  }
}

寫到這裏已經能夠幫助你應付一些面試官考察你的遞歸解決問題的能力。可是顯然,這個深拷貝函數仍是有一些問題。

一個比較完整的深拷貝函數,須要同時考慮對象和數組,考慮循環引用:

function clone(target, map = new WeakMap()) {
  if(typeof target === 'object'){
    let cloneTarget = Array.isArray(target) ? [] : {};
    if(map.get(target)) {
      return target;
    }
    map.set(target, cloneTarget);
    for(const key in target) {
      cloneTarget[key] = clone(target[key], map)
    }
    return cloneTarget;
  } else {
    return target;
  }
}

實現一個instanceOf

原理: L 的 proto 是否是等於 R.prototype,不等於再找 L.__proto__.__proto__ 直到 proto 爲 null

// L 表示左表達式,R 表示右表達式
function instance_of(L, R) {
    var O = R.prototype;
  L = L.__proto__;
  while (true) {
        if (L === null){
            return false;
        }
        // 這裏重點:當 O 嚴格等於 L 時,返回 true
        if (O === L) {
            return true;
        }
        L = L.__proto__;
  }
}

實現原型鏈繼承

function myExtend(C, P) {
    var F = function(){};
    F.prototype = P.prototype;
    C.prototype = new F();
    C.prototype.constructor = C;
    C.super = P.prototype;
}

實現一個async/await

原理

就是利用 generator(生成器)分割代碼片斷。而後咱們使用一個函數讓其自迭代,每個yield 用 promise 包裹起來。執行下一步的時機由 promise 來控制

實現
function _asyncToGenerator(fn) {
  return function() {
    var self = this,
      args = arguments;
    // 將返回值promise化
    return new Promise(function(resolve, reject) {
      // 獲取迭代器實例
      var gen = fn.apply(self, args);
      // 執行下一步
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
      }
      // 拋出異常
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
      }
      // 第一次觸發
      _next(undefined);
    });
  };
}

實現一個Array.prototype.flat()函數

最近字節跳動的前端面試中也被面試官問到,要求手寫實現。

Array.prototype.myFlat = function(num = 1) {
  if (Array.isArray(this)) {
    let arr = [];
    if (!Number(num) || Number(num) < 0) {
      return this;
    }
    this.forEach(item => {
      if(Array.isArray(item)){
        let count = num
        arr = arr.concat(item.myFlat(--count))
      } else {
        arr.push(item)
      }  
    });
    return arr;
  } else {
    throw tihs + ".flat is not a function";
  }
};

實現一個事件代理

這個問題通常還會讓你講一講事件冒泡和事件捕獲機制

<ul id="color-list">
    <li>red</li>
    <li>yellow</li>
    <li>blue</li>
    <li>green</li>
    <li>black</li>
    <li>white</li>
  </ul>
  <script>
    (function () {
      var color_list = document.getElementById('color-list');
      color_list.addEventListener('click', showColor, true);
      function showColor(e) {
        var x = e.target;
        if (x.nodeName.toLowerCase() === 'li') {
          alert(x.innerHTML);
        }
      }
    })();
  </script>

實現一個雙向綁定

Vue 2.x的Object.defineProperty版本

// 數據
const data = {
  text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 數據劫持
Object.defineProperty(data, 'text', {
  // 數據變化 —> 修改視圖
  set(newVal) {
    input.value = newVal;
    span.innerHTML = newVal;
  }
});
// 視圖更改 --> 數據變化
input.addEventListener('keyup', function(e) {
  data.text = e.target.value;
});

Vue 3.x的proxy 版本

// 數據
const data = {
  text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 數據劫持
const handler = {
  set(target, key, value) {
    target[key] = value;
    // 數據變化 —> 修改視圖
    input.value = value;
    span.innerHTML = value;
    return value;
  }
};
const proxy = new Proxy(data, handler);

// 視圖更改 --> 數據變化
input.addEventListener('keyup', function(e) {
  proxy.text = e.target.value;
});

思考:Vue雙向綁定的實現,使用 ES6 的 Proxy 相比 Object.defineProperty 有什麼優點?

結語

推薦閱讀:
【專題:JavaScript進階之路】
深刻理解 ES6 Promise
JavaScript之函數柯理化
ES6 尾調用和尾遞歸
Git經常使用命令小結
淺談 MVC 和 MVVM 模型

參考:
重寫手動實現bind函數 - 雲+社區 - 騰訊雲
各類源碼實現,你想要的這裏都有


我是Cloudy,現居上海,年輕的前端攻城獅一枚,目前正在尋找新的工做機會中,愛專研,愛技術,愛分享。
我的筆記,整理不易,感謝關注閱讀點贊收藏
文章有任何問題歡迎你們指出,也歡迎你們一塊兒交流各類前端問題!
相關文章
相關標籤/搜索