幾種常見的手寫源碼實現

閱讀源碼的好處,不用說都知道,首先進大廠必備,還可以提升自己的能力,學習前人的經驗。源碼往往是前人留下的最佳實踐,我們跟著前人的腳步去學習會讓我們事半功倍。

  • call、aplly、bind 實現
  • new 實現
  • class 實現繼承
  • async/await 實現
  • reduce 實現
  • 實現一個雙向數據綁定
  • instanceof 實現
  • Array.isArray 實現
  • Object.create 的基本實現原理
  • getOwnPropertyNames 實現
  • promise 實現
  • 手寫一個防抖/節流函數
  • 柯里化函數的實現
  • 手寫一個深拷貝

call、aplly、bind 實現

call、aplly、bind 本質都是改變 this 的指向,不同點 call、aplly 是直接調用函數,bind 是返回一個新的函數。callaplly 就只有參數上不同。

bind 實現

  • 箭頭函數的 this 永遠指向它所在的作用域
  • 函數作為構造函數用 new 關鍵字調用時,不應該改變其 this 指向,因為 new綁定 的優先順序高於 顯示綁定硬綁定
Function.prototype.mybind = function(thisArg) {      if (typeofthis !== 'function') {        throwTypeError("Bind must be called on a function");      }      // 拿到參數,為了傳給調用者      const args = Array.prototype.slice.call(arguments, 1),        // 保存 this        self = this,        // 構建一個乾淨的函數,用於保存原函數的原型        nop = function() {},        // 綁定的函數        bound = function() {          // this instanceof nop, 判斷是否使用 new 來調用 bound          // 如果是 new 來調用的話,this的指向就是其實例,          // 如果不是 new 調用的話,就改變 this 指向到指定的對象 o          return self.apply(            thisinstanceof nop ? this : thisArg,            args.concat(Array.prototype.slice.call(arguments))          );        };        // 箭頭函數沒有 prototype,箭頭函數this永遠指向它所在的作用域      if (this.prototype) {        nop.prototype = this.prototype;      }      // 修改綁定函數的原型指向      bound.prototype = new nop();        return bound;    }  }
  1. 測試 mybind
const bar = function() {    console.log(this.name, arguments);  };    bar.prototype.name = 'bar';    const foo = {    name: 'foo'  };    const bound = bar.mybind(foo, 22, 33, 44);  new bound(); // bar, [22, 33, 44]  bound(); // foo, [22, 33, 44]

call 實現

bind 是封裝了 call 的方法改變了 this 的指向並返回一個新的函數,那麼 call 是如何做到改變 this 的指向呢?原理很簡單,在方法調用模式下,this 總是指向調用它所在方法的對象,this 的指向與所在方法的調用位置有關,而與方法的聲明位置無關(箭頭函數特殊)。先寫一個小 demo 來理解一下下。

const foo = { name: 'foo' };    foo.fn = function() {    // 這裡的 this 指向了 foo    // 因為 foo 調用了 fn,    // fn 的 this 就指向了調用它所在方法的對象 foo 上    console.log(this.name); // foo  };

利用 this 的機制來實現 call

Function.prototype.mycall = function(thisArg) {    // this指向調用call的對象    if (typeofthis !== 'function') {      // 調用call的若不是函數則報錯      thrownewTypeError('Error');    }      const args = [...arguments].slice(1);    thisArg = thisArg || window;    // 將調用call函數的對象添加到thisArg的屬性中    thisArg.fn = this;    // 執行該屬性    const result = thisArg.fn(...arg);    // 刪除該屬性    delete thisArg.fn;    // 返回函數執行結果    return result;  };

aplly 實現

Function.prototype.myapply = function(thisArg) {    if (typeofthis !== 'function') {      throwthis + ' is not a function';    }      const args = arguments[1];      thisArg.fn = this;      const result = thisArg.fn(...arg);      delete thisArg.fn;      return result;  };

測試 mycall myaplly

const bar = function() {    console.log(this.name, arguments);  };    bar.prototype.name = 'bar';    const foo = {    name: 'foo'  };    bar.mycall(foo, 1, 2, 3); // foo [1, 2, 3]  bar.myaplly(foo, [1, 2, 3]); // foo [1, 2, 3]

reduce 實現原理

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

Array.prototype.myreduce = function reduce(callbackfn) {    // 拿到數組    const O = this,      len = O.length;    // 下標值    let k = 0,      // 累加器      accumulator = undefined,      // k下標對應的值是否存在      kPresent = false,      // 初始值      initialValue = arguments.length > 1 ? arguments[1] : undefined;      if (typeof callbackfn !== 'function') {      thrownewTypeError(callbackfn + ' is not a function');    }      // 數組為空,並且有初始值,報錯    if (len === 0 && arguments.length < 2) {      thrownewTypeError('Reduce of empty array with no initial value');    }      // 如果初始值存在    if (arguments.length > 1) {      // 設置累加器為初始值      accumulator = initialValue;      // 初始值不存在    } else {      accumulator = O[k];      ++k;    }      while (k < len) {      // 判斷是否為 empty [,,,]      kPresent = O.hasOwnProperty(k);        if (kPresent) {        const kValue = O[k];        // 調用 callbackfn        accumulator = callbackfn.apply(undefined, [accumulator, kValue, k, O]);      }      ++k;    }      return accumulator;  };

測試

const rReduce = ['1', null, undefined, , 3, 4].reduce((a, b) => a + b, 3);  const mReduce = ['1', null, undefined, , 3, 4].myreduce((a, b) => a + b, 3);    console.log(rReduce, mReduce);  // 31nullundefined34 31nullundefined34

new 實現

我們需要知道當 new 的時候做了什麼事情

  1. 創建一個新對象;
  2. 將構造函數的作用域賦給新對象(因此 this 就指向了這個新對象)
  3. 執行構造函數中的程式碼(為這個新對象添加屬性)
  4. 返回新對象。

因為 new 沒辦法重寫,我們使用 myNew 函數來模擬 new

function myNew() {    // 創建一個實例對象    var obj = new Object();    // 取得外部傳入的構造器    var Constructor = Array.prototype.shift.call(arguments);    // 實現繼承,實例可以訪問構造器的屬性    obj.__proto__ = Constructor.prototype;    // 調用構造器,並改變其 this 指向到實例    var ret = Constructor.apply(obj, arguments);    // 如果構造函數返回值是對象則返回這個對象,如果不是對象則返回新的實例對象    returntypeof ret === 'object' ? ret : obj;  }

測試 myNew

// ========= 無返回值 =============  const testNewFun = function(name) {    this.name = name;  };    const newObj = myNew(testNewFun, 'foo');    console.log(newObj); // { name: "foo" }  console.log(newObj instanceof testNewFun); // true  // ========= 有返回值 =============  const testNewFun = function(name) {    this.name = name;    return {};  };    const newObj = myNew(testNewFun, 'foo');    console.log(newObj); // {}  console.log(newObj instanceof testNewFun); // false

class 實現繼承

主要使用 es5es6 對比看下 class 繼承的原理

實現繼承 A extends B

使用 es6 語法

class B {    constructor(opt) {      this.BName = opt.name;    }  }  class A extends B {    constructor() {      // 向父類傳參      super({ name: 'B' });      // this 必須在 super() 下面使用      console.log(this);    }  }

使用 es5 語法

使用寄生組合繼承的方式

  1. 原型鏈繼承,使子類可以調用父類原型上的方法和屬性
  2. 借用構造函數繼承,可以實現向父類傳參
  3. 寄生繼承,創造乾淨的沒有構造方法的函數,用來寄生父類的 prototype
// 實現繼承,通過繼承父類 prototype  function __extends(child, parent) {    // 修改對象原型    Object.setPrototypeOf(child, parent);    // 寄生繼承,創建一個乾淨的構造函數,用於繼承父類的 prototype    // 這樣做的好處是,修改子類的 prototype 不會影響父類的 prototype    function __() {      // 修正 constructor 指向子類      this.constructor = child;    }    // 原型繼承,繼承父類原型屬性,但是無法向父類構造函數傳參    child.prototype =      parent === null        ? Object.create(parent)        : ((__.prototype = parent.prototype), new __());  }    var B = (function() {    function B(opt) {      this.name = opt.name;    }    return B;  })();    var A = (function(_super) {    __extends(A, _super);    function A() {      // 借用繼承,可以實現向父類傳參, 使用 super 可以向父類傳參      return (_super !== null && _super.apply(this, { name: 'B' })) || this;    }    return A;  })(B);

測試 class

const a = new A();    console.log(a.BName, a.constructor); // B ,ƒ A() {}

async/await 實現

原理就是利用 generator(生成器)分割程式碼片段。然後我們使用一個函數讓其自迭代,每一個yieldpromise 包裹起來。執行下一步的時機由 promise 來控制

async/await 是關鍵字,不能重寫它的方法,我們使用函數來模擬

非同步迭代,模擬非同步函數

function _asyncToGenerator(fn) {    returnfunction() {      var self = this,        args = arguments;      // 將返回值promise化      returnnewPromise(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);      });    };  }

執行迭代步驟,處理下次迭代結果

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {    try {      var info = gen[key](arg);      var value = info.value;    } catch (error) {      reject(error);      return;    }    if (info.done) {      // 迭代器完成      resolve(value);    } else {      // -- 這行程式碼就是精髓 --      // 將所有值promise化      // 比如 yield 1      // const a = Promise.resolve(1) a 是一個 promise      // const b = Promise.resolve(a) b 是一個 promise      // 可以做到統一 promise 輸出      // 當 promise 執行完之後再執行下一步      // 遞歸調用 next 函數,直到 done == true      Promise.resolve(value).then(_next, _throw);    }  }

測試 _asyncToGenerator

const asyncFunc = _asyncToGenerator(function*() {    const e = yieldnewPromise(resolve => {      setTimeout(() => {        resolve('e');      }, 1000);    });    const a = yieldPromise.resolve('a');    const d = yield'd';    const b = yieldPromise.resolve('b');    const c = yieldPromise.resolve('c');    return [a, b, c, d, e];  });    asyncFunc().then(res => {    console.log(res); // ['a', 'b', 'c', 'd', 'e']  });

實現一個雙向綁定

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

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 = newProxy(data);    // 視圖更改 --> 數據變化  input.addEventListener('keyup', function(e) {    proxy.text = e.target.value;  });

Object.create 的基本實現原理

function create(obj) {    function F() {}    F.prototype = obj;    returnnew F();  }

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) returnfalse;      // 這裡重點:當 O 嚴格等於 L 時,返回 true      if (O === L) returntrue;      L = L.__proto__;    }  }

Array.isArray 實現

Array.myIsArray = function(o) {    returnObject.prototype.toString.call(Object(o)) === '[object Array]';  };    console.log(Array.myIsArray([])); // true

getOwnPropertyNames 實現

if (typeofObject.getOwnPropertyNames !== 'function') {    Object.getOwnPropertyNames = function(o) {      if (o !== Object(o)) {        throwTypeError('Object.getOwnPropertyNames called on non-object');      }      var props = [],        p;      for (p in o) {        if (Object.prototype.hasOwnProperty.call(o, p)) {          props.push(p);        }      }      return props;    };  }

Promise 實現

實現原理:其實就是一個發布訂閱者模式

  1. 構造函數接收一個 executor 函數,並會在 new Promise() 時立即執行該函數
  2. then 時收集依賴,將回調函數收集到 成功/失敗隊列
  3. executor 函數中調用 resolve/reject 函數
  4. resolve/reject 函數被調用時會通知觸發隊列中的回調

先看一下整體程式碼,有一個大致的概念

完整程式碼

const isFunction = variable =>typeof variable === 'function';    // 定義Promise的三種狀態常量  const PENDING = 'pending';  const FULFILLED = 'fulfilled';  const REJECTED = 'rejected';    class MyPromise {    // 構造函數,new 時觸發    constructor(handle: Function) {      try {        handle(this._resolve, this._reject);      } catch (err) {        this._reject(err);      }    }    // 狀態 pending fulfilled rejected    private _status: string = PENDING;    // 儲存 value,用於 then 返回    private _value: string | undefined = undefined;    // 失敗隊列,在 then 時注入,resolve 時觸發    private _rejectedQueues: any = [];    // 成功隊列,在 then 時注入,resolve 時觸發    private _fulfilledQueues: any = [];    // resovle 時執行的函數    private _resolve = val => {      const run = () => {        if (this._status !== PENDING) return;        this._status = FULFILLED;        // 依次執行成功隊列中的函數,並清空隊列        const runFulfilled = value => {          let cb;          while ((cb = this._fulfilledQueues.shift())) {            cb(value);          }        };        // 依次執行失敗隊列中的函數,並清空隊列        const runRejected = error => {          let cb;          while ((cb = this._rejectedQueues.shift())) {            cb(error);          }        };        /*         * 如果resolve的參數為Promise對象,         * 則必須等待該Promise對象狀態改變後當前Promsie的狀態才會改變         * 且狀態取決於參數Promsie對象的狀態         */        if (val instanceof MyPromise) {          val.then(            value => {              this._value = value;              runFulfilled(value);            },            err => {              this._value = err;              runRejected(err);            }          );        } else {          this._value = val;          runFulfilled(val);        }      };      // 非同步調用      setTimeout(run);    };    // reject 時執行的函數    private _reject = err => {      if (this._status !== PENDING) return;      // 依次執行失敗隊列中的函數,並清空隊列      const run = () => {        this._status = REJECTED;        this._value = err;        let cb;        while ((cb = this._rejectedQueues.shift())) {          cb(err);        }      };      // 為了支援同步的Promise,這裡採用非同步調用      setTimeout(run);    };    // then 方法    then(onFulfilled?, onRejected?) {      const { _value, _status } = this;      // 返回一個新的Promise對象      returnnew MyPromise((onFulfilledNext, onRejectedNext) => {        // 封裝一個成功時執行的函數        const fulfilled = value => {          try {            if (!isFunction(onFulfilled)) {              onFulfilledNext(value);            } else {              const res = onFulfilled(value);              if (res instanceof MyPromise) {                // 如果當前回調函數返回MyPromise對象,必須等待其狀態改變後在執行下一個回調                res.then(onFulfilledNext, onRejectedNext);              } else {                //否則會將返回結果直接作為參數,傳入下一個then的回調函數,並立即執行下一個then的回調函數                onFulfilledNext(res);              }            }          } catch (err) {            // 如果函數執行出錯,新的Promise對象的狀態為失敗            onRejectedNext(err);          }        };          // 封裝一個失敗時執行的函數        const rejected = error => {          try {            if (!isFunction(onRejected)) {              onRejectedNext(error);            } else {              const res = onRejected(error);              if (res instanceof MyPromise) {                // 如果當前回調函數返回MyPromise對象,必須等待其狀態改變後在執行下一個回調                res.then(onFulfilledNext, onRejectedNext);              } else {                //否則會將返回結果直接作為參數,傳入下一個then的回調函數,並立即執行下一個then的回調函數                onFulfilledNext(res);              }            }          } catch (err) {            // 如果函數執行出錯,新的Promise對象的狀態為失敗            onRejectedNext(err);          }        };          switch (_status) {          // 當狀態為pending時,將then方法回調函數加入執行隊列等待執行          case PENDING:            this._fulfilledQueues.push(fulfilled);            this._rejectedQueues.push(rejected);            break;          // 當狀態已經改變時,立即執行對應的回調函數          case FULFILLED:            fulfilled(_value);            break;          case REJECTED:            rejected(_value);            break;        }      });    }    // catch 方法    catch(onRejected) {      returnthis.then(undefined, onRejected);    }    // finally 方法    finally(cb) {      returnthis.then(        value => MyPromise.resolve(cb()).then(() => value),        reason =>          MyPromise.resolve(cb()).then(() => {            throw reason;          })      );    }    // 靜態 resolve 方法    static resolve(value) {      // 如果參數是MyPromise實例,直接返回這個實例      if (value instanceof MyPromise) return value;      returnnew MyPromise(resolve => resolve(value));    }    // 靜態 reject 方法    static reject(value) {      returnnew MyPromise((resolve, reject) => reject(value));    }    // 靜態 all 方法    static all(list) {      returnnew MyPromise((resolve, reject) => {        // 返回值的集合        let values = [];        let count = 0;        for (let [i, p] of list.entries()) {          // 數組參數如果不是MyPromise實例,先調用MyPromise.resolve          this.resolve(p).then(            res => {              values[i] = res;              count++;              // 所有狀態都變成fulfilled時返回的MyPromise狀態就變成fulfilled              if (count === list.length) resolve(values);            },            err => {              // 有一個被rejected時返回的MyPromise狀態就變成rejected              reject(err);            }          );        }      });    }    // 添加靜態race方法    static race(list) {      returnnew MyPromise((resolve, reject) => {        for (let p of list) {          // 只要有一個實例率先改變狀態,新的MyPromise的狀態就跟著改變          this.resolve(p).then(            res => {              resolve(res);            },            err => {              reject(err);            }          );        }      });    }  }

防抖/截流

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

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

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

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

函數柯里化實現

其實我們無時無刻不在使用柯里化函數,只是沒有將它總結出來而已。它的本質就是將一個參數很多的函數分解成單一參數的多個函數。

實際應用中:

  • 延遲計算 (用閉包把傳入參數保存起來,當傳入參數的數量足夠執行函數時,開始執行函數)
  • 動態創建函數 (參數不夠時會返回接受剩下參數的函數)
  • 參數復用(每個參數可以多次復用)
const curry = fn =>    (judge = (...args) =>      args.length === fn.length        ? fn(...args)        : (...arg) => judge(...args, ...arg));    const sum = (a, b, c, d) => a + b + c + d;  const currySum = curry(sum);    currySum(1)(2)(3)(4); // 10  currySum(1, 2)(3)(4); // 10  currySum(1)(2, 3)(4); // 10

手寫一個深拷貝

淺拷貝只複製地址值,實際上還是指向同一堆記憶體中的數據,深拷貝則是重新創建了一個相同的數據,二者指向的堆記憶體的地址值是不同的。這個時候修改賦值前的變數數據不會影響賦值後的變數。

要實現一個完美的神拷貝太複雜了,這裡簡單介紹一下吧,可以應用於大部分場景了

判斷類型函數

function getType(obj) {    const str = Object.prototype.toString.call(obj);    const map = {      '[object Boolean]': 'boolean',      '[object Number]': 'number',      '[object String]': 'string',      '[object Function]': 'function',      '[object Array]': 'array',      '[object Date]': 'date',      '[object RegExp]': 'regExp',      '[object Undefined]': 'undefined',      '[object Null]': 'null',      '[object Object]': 'object'    };    if (obj instanceof Element) {      // 判斷是否是dom元素,如div等      return'element';    }    return map[str];  }

簡單版深拷貝,列舉三個例子 array object function,可以自行擴展。主要是引發大家的思考

function deepCopy(ori) {    const type = getType(ori);    let copy;    switch (type) {      case'array':        return copyArray(ori, type, copy);      case'object':        return copyObject(ori, type, copy);      case'function':        return copyFunction(ori, type, copy);      default:        return ori;    }  }    function copyArray(ori, type, copy = []) {    for (const [index, value] of ori.entries()) {      copy[index] = deepCopy(value);    }    return copy;  }    function copyObject(ori, type, copy = {}) {    for (const [key, value] ofObject.entries(ori)) {      copy[key] = deepCopy(value);    }    return copy;  }    function copyFunction(ori, type, copy = () => {}) {    const fun = eval(ori.toString());    fun.prototype = ori.prototype    return fun  }

參考文章

https://cloud.tencent.com/developer/article/1431398 https://www.jianshu.com/p/b4f0425b22a1 https://blog.csdn.net/LL18781132750/article/details/79700089