持續構建我的的源碼庫(JavaScript 原生、經常使用庫、數據結構、算法)

*項目地址:awesome-coding-javascript.javascript

出發點

在平常開發中爲了提升開發效率,咱們通常是直接調用封裝好的函數方法。可是做爲工程師僅知道這些方法如何調用確定是不夠的,還須要瞭解這些方法背後的實現原理,以便以後更好地運用這些方法,並能夠基於此改進或者寫出本身以爲不錯的方法。java

這方面社區裏也有不少關於原理介紹和代碼實現的文章,能夠從中學到不少。參照着文章裏的描述,本身也能夠實現這些方法,用 console.log 打印一下輸出結果檢查看有沒有問題,可是這樣寫起來比較零碎分散不方便代碼邏輯的持續改進。node

例如:每次要驗證代碼邏輯的時候,都須要把代碼拷到控制檯或者一個文件裏去運行;測試的數據須要臨時構造或者也須要一個一個複製過去;測試用例都須要本身手動執行,不夠自動化;正是因爲測試不方便,因此寫的測試用例也比較粗略;有代碼修改時要保證邏輯正確,須要從新手動執行因此測試用例,繁瑣重複;沒有數據來評估代碼質量。git

持續構建的思路

如何解決?其實能夠從開源的庫和插件中找到答案,那就是爲這些實現代碼加上單元測試,並保證代碼覆蓋率。github

加上單元測試後的優點有:算法

  1. 單元測試就是一個命令式的運行環境,便可以在開發時調試,也能夠在開發完成後驗證。
  2. 執行寫好的單元測試用例,自動化地幫咱們驗證邏輯的正確性。
  3. 代碼有改動,從新執行全部測試用例,保證改動的代碼沒有問題。
  4. 單元測試即文檔,開發者能夠從一個一個的單元測試中看到都實現了哪些功能,實現功能的描述,調用方式和調用的數據,最後調用的結果是什麼。因此說單元測試也是一個很直觀的用戶文檔。
  5. 體驗 TDD 編程,先寫好單元測試用例整理思路,而後實現知足這些測試用例的邏輯。
  6. 提交代碼前全量跑一遍單元測試,保證全部邏輯沒有問題以後才能提交。

保證代碼覆蓋率的意義有:編程

  1. 檢測出程序中的無用代碼,逆向反推在代碼設計中思惟未考慮到的點,提高代碼質量。
  2. 分析未覆蓋部分的代碼,反推在前期單元測試設計是否充分,改進測試用例。

因此加上單元測試和保證代碼覆蓋率以後,能解決掉最開始提到的問題,讓咱們即方便又有保障地持續構建咱們的源碼庫。接下來就是列出一些咱們認爲須要實現的源碼庫。數組

待構建源碼列表

它包括 JavaScript 原生和經常使用方法的代碼實現、經常使用庫和插件的代碼實現以及使用 JavaScript 實現的數據結構和經常使用算法。因爲涉及的內容比較多,不可能短期內所有集齊,這裏先把大致的框架搭起來,以後再持續地構建本身的源碼庫。promise

  1. JavaScript 原生和經常使用方法的代碼實現
    • bind/apply/call 的實現
    • new 的實現
    • 深拷貝
    • 防抖/節流的實現
    • Promise 原理實現
    • async/await 原理實現
    • 柯里化
    • 模板字符串
    • 訂閱發佈機制
    • URL 參數解析
  2. 經常使用的庫和插件的代碼實現
    • Webpack 的模擬實現
    • Webpack Plugin 的實現
    • Babel 的模擬實現
    • Babel Plugin 的實現
    • React 的模擬實現
    • Redux 的模擬實現
    • Router 的模擬實現
    • Egg.js 的模擬實現
  3. JavaScript 實現的數據結構和經常使用算法
    • 數據結構
      • 棧、隊列、鏈表、集合
      • 堆、散列表、數組、樹
    • 算法
      • 排序、分治、遞歸
      • 深度優先/廣度優先、雙指針
      • 動態規劃、回溯算法、貪心算法
    • 其餘
      • 字符串
      • 數字

源碼及單測展現(部分)

爲了讓你們有個直觀的影響,這裏貼出一些比較有表明性的代碼實現和單元測試,供你們參考。完整的代碼展現請見 Repo : awesome-coding-javascriptbash

call 的實現

實現代碼:

export default function (...args) {
  // 使用 Symbol 建立一個全局惟一的函數名
  const func = Symbol('func');
  let context = args.shift();

  // 非嚴格模式下,傳 null 或者 undeinfed,context 等於 window 對象
  if (context == null) {
    context = window;
  }
  else {
    context = Object(context);
  }

  // 賦予函數屬性
  context[func] = this;
  // 函數執行
  const result = context[func](...args);
  // 刪除臨時的函數屬性
  Reflect.deleteProperty(context, func);

  return result;
}
複製代碼

單元測試用例:

import call from './call';

describe('Function.prototype.call', () => {

  Function.prototype._call = call;

  it('change the direction of this', () => {
    const foo = {
      value: 1,
    };
    function bar() {
      return this.value;
    }

    // 和原生的 call 操做進行比較驗證
    expect(bar._call(foo)).toBe(bar.call(foo));
  });

  it('change the direction of this, use in constructor', () => {
    function Product(name, price) {
      this.name = name;
      this.price = price;
    }

    function Food(name, price) {
      Product.call(this, name, price);
      this.category = 'food';
    }
    function Food2(name, price) {
      Product._call(this, name, price);
      this.category = 'food';
    }

    // 和原生的 call 操做進行比較驗證
    expect(new Food2('cheese', 5).name).toBe(new Food('cheese', 5).name);
  });

  it('when \'this\' argument is null or undefined', () => {
    window.value = 2;
    function bar() {
      return this.value;
    }
    // 這是非嚴格模式下的結果,嚴格模式下會報錯
    expect(bar._call(null)).toBe(bar.call(null));
    expect(bar._call(undefined)).toBe(bar.call(undefined));
  });

  it('when \'this\' is other primitive value', () => {
    function bar() {
      return this.length;
    }

    // 和原生的 call 操做進行比較驗證
    expect(bar._call('123')).toBe(bar.call('123'));
  });
});
複製代碼

執行單元測試用例:

PASS  src/javascript/call/call.test.js
Function.prototype.call
  ✓ change the direction of this (4ms)
  ✓ change the direction of this, use in constructor (1ms)
  ✓ when 'this' argument is null or undefined (1ms)
  ✓ when 'this' is other primitive value
複製代碼

bind 的實現

實現代碼:

export default function (...bindArgs) {
  // 函數自身
  const self = this;
  // 傳進來的 this 對象
  let context = bindArgs.shift();

  // 非嚴格模式下,傳 null 或者 undeinfed,context 等於 window 對象
  if (context == null) {
    context = window;
  }
  else {
    context = Object(context);
  }

  // 返回的新函數
  const fBound = function (...args) {
    // 當返回的新函數做爲構造函數使用時,以前指定的 this 對象會失效,此時 this 是指向實例對象
    if (this instanceof fBound) {
      context = this;
    }
    // 函數運行並返回結果
    return self.apply(context, [...bindArgs, ...args]);
  };
  // 修改返回函數的原型對象,實例對象就能夠從原函數的原型對象上繼承屬性和方法
  fBound.prototype = Object.create(self.prototype);

  return fBound;
}
複製代碼

單元測試用例:

import bind from './bind';

describe('Function.prototype.bind', () => {

  Function.prototype._bind = bind;

  it('change the direction of this, return a function', () => {
    const foo = {
      value: 1,
    };
    function bar(age1, age2) {
      age1 = age1 || 0;
      age2 = age2 || 0;
      return this.value + age1 + age2;
    }
    const newBar = bar._bind(foo, 3);
    expect(typeof newBar).toBe('function');
    // 和原生 bind 操做進行比較驗證
    expect(newBar(2)).toBe((bar.bind(foo, 3))(2));
  });

  it('when return function as a constructor, \'this\' points to the instance object', () => {
    const foo = { value: 1 };
    function bar(name, age) {
      this.name = name;
      this.age = age;
    }
    bar.prototype.friend = 'kevin';

    const bindFoo = bar.bind(foo);  // 原生 bind 操做生成的實例對象
    const bindFoo2 = bar._bind(foo);
    bindFoo2.prototype.address = 1; // 修改返回函數的原型對象

    // 驗證返回的函數做爲構造函數,實例對象會從原函數的原型對象上繼承屬性
    expect(new bindFoo2().friend).toBe(new bindFoo().friend);
    // 驗證返回的函數做爲構造函數,以前綁定的 this 對象會失效,this 會指向實例對象
    expect(new bindFoo2().value).toBe(undefined);
    expect(new bindFoo2().value).toBe(new bindFoo().value);
    // 驗證修改返回函數的原型對象,不會引發原始函數 bar 原型對象的修改
    expect(bar.prototype.address).toBe(undefined);
  });

  it('when rebind \'this\', cannot change the direction of this', () => {
    const foo = {
      value: 1,
    };
    function bar(age1, age2) {
      age1 = age1 || 0;
      age2 = age2 || 0;
      return this.value + age1 + age2;
    }
    const bindFoo = bar.bind(foo);  // 原生 bind 操做生成的實例對象
    const bindFoo2 = bar._bind(foo);

    // 對返回的函數用 call 或者 apply 從新綁定 this 對象時,this 對象不會發生改變
    expect(bindFoo2.call({ value: 2 })).toBe(1);
    expect(bindFoo2.call({ value: 2 })).toBe(bindFoo.call({ value: 2 }));
  });

  it('when \'this\' argument is null or undefined', () => {
    window.value = 2;
    function bar(age1, age2) {
      age1 = age1 || 0;
      age2 = age2 || 0;
      return this.value + age1 + age2;
    }

    // 這是非嚴格模式下的結果,嚴格模式下會報錯
    expect(bar._bind(null, 3)(1)).toBe(6);
    expect(bar._bind(undefined, 3)(1)).toBe(6);
  });
});
複製代碼

執行單元測試用例:

PASS  src/javascript/bind/bind.test.js
  Function.prototype.bind
    ✓ change the direction of this, return a function (3ms)
    ✓ when return function as a constructor, 'this' points to the instance object (1ms)
    ✓ when rebind 'this', cannot change the direction of this (1ms)
    ✓ when 'this' argument is null or undefined
複製代碼

new 的實現

實現代碼:

import { isArrowFunction } from './../../shared/is';

export function objectFactory(Factory, ...args) {
  if (typeof Factory !== 'function') {
    throw new Error('need be a function argument');
  }

  if (isArrowFunction(Factory)) {
    throw new Error('arrow function is not allowed');
  }

  const instance = Object.create(Factory.prototype);  // 建立實例對象
  const result = Factory.apply(instance, args);  // 執行構造函數

  return result instanceof Object ? result : instance;
}
複製代碼

單元測試用例:

import { objectFactory } from './new';

describe('new', () => {
  it('take a function as an argument', () => {
    const Factory = 123;
    function excute() {
      objectFactory(Factory);
    }

    expect(excute).toThrowError('need be a function argument');
  });

  it('cannot be an arrow function', () => {
    const Factory = (name, age) => {
      this.name = name;
      this.age = age;

      return 233;
    };

    function excute() {
      objectFactory(Factory);
    }

    expect(excute).toThrowError('arrow function is not allowed');
  });

  it('create a instance', () => {
    function Factory(name, age) {
      this.name = name;
      this.age = age;
    }

    Factory.prototype.getName = function () {
      return this.name;
    };

    const f = objectFactory(Factory, 'jack', 12);
    const nf = new Factory('jack', 12);  // 原生的 new 操做生成的實例對象

    expect(f.name).toBe(nf.name);
    expect(f.age).toBe(nf.age);
    expect(f.getName()).toBe(nf.getName());
  });

  it('if return a primitive value, return the newly instance', () => {
    const Factory = function (name, age) {
      this.name = name;
      this.age = age;

      return 233;
    };

    Factory.prototype.getName = function () {
      return this.name;
    };

    const f = objectFactory(Factory, 'jack', 12);
    const nf = new Factory('jack', 12);  // 原生的 new 操做生成的實例對象

    expect(f.name).toBe(nf.name);
    expect(f.age).toBe(nf.age);
    expect(f.getName()).toBe(nf.getName());
  });

  it('if return a object, return the object', () => {
    const Factory = function (name, age) {
      this.name = name;
      this.age = age;

      return {
        name: 'john',
      };
    };

    Factory.prototype.getName = function () {
      return this.name;
    };

    const f = objectFactory(Factory, 'jack', 12);
    const nf = new Factory('jack', 12);  // 原生的 new 操做生成的實例對象

    expect(f.name).toBe(nf.name);
    expect(f.age).toBe(nf.age);
    expect(f.getName).toBe(nf.getName);
  });
});
複製代碼

執行單元測試用例:

PASS  src/javascript/new/new.test.js
  new
    ✓ take a function as an argument (5ms)
    ✓ cannot be an arrow function (1ms)
    ✓ create a instance (1ms)
    ✓ if return a primitive value, return the newly instance (1ms)
    ✓ if return a object, return the object (1ms)
複製代碼

Throttle 節流實現

實現代碼:

export default function throttle(func, interval) {
  let startTime = new Date();

  return function (...args) {
    const curTime = new Date();
    // 大於間隔時才執行函數的邏輯
    if (curTime - startTime > interval) {
      // 重設開始時間
      startTime = curTime;
      // 執行函數
      func.apply(this, args);
    }
  };
}
複製代碼

單元測試用例:

import throttle from './throttle';
import sleep from './../../shared/sleep';

describe('throttle', () => {
  it('when less than interval, will throttle', async () => {
    let count = 0;
    const addCount = () => {
      count += 1;
    };

    const timer = setInterval(throttle(addCount, 400), 200);
    /** * 400 -> 1 * 800 -> 2 */
    setTimeout(() => {
      clearInterval(timer);
    }, 1000);
    await sleep(1500);
    expect(count).toBe(2);
  });

  it('when greate than interval, normally call', async () => {
    let count = 0;
    const addCount = () => {
      count += 1;
    };

    const timer = setInterval(throttle(addCount, 200), 300);
    /** * 300 -> 1 * 600 -> 2 * 900 -> 3 */
    setTimeout(() => {
      clearInterval(timer);
    }, 1000);
    await sleep(1500);
    expect(count).toBe(3);
  });
});
複製代碼

執行單元測試用例:

PASS  src/javascript/throttle/throttle.test.js
  throttle
    ✓ when less than interval, will throttle (1506ms)
    ✓ when greate than interval, normally call (1505ms)
複製代碼

Promise 原理實現

實現代碼:

export default class Promise {
  constructor(fn) {
    // 記錄 promise 的狀態,根據狀態來操做 callbacks 隊列
    this.state = 'pending';
    // 記錄 resolve 或 reject 時的值
    this.value = null;
    // 裏面維護了一個隊列,是 resolve 時要執行的代碼
    this.callbacks = [];

    fn(this.resolve, this.reject);
  }

  static resolve = val => {
    return new Promise(function (resolve) {
      resolve(val);
    });
  }

  static reject = reason => {
    return new Promise(function (resolve, reject) {
      reject(reason);
    });
  }

  static stop = () => {
    return new Promise(function () {});
  }

  // 註冊 onFulfilled 和 onRejected 函數
  then = (onFulfilled, onRejected) => {
    return new Promise((resolve, reject) => {
      this.handle({
        onFulfilled: onFulfilled || null,
        onRejected: onRejected || null,
        resolve,
        reject,
      });
    });
  }

  catch = onRejected => {
    return this.then(null, onRejected);
  }

  finally = onFinally => {
    onFinally = typeof onFinally === 'function' ? onFinally : function noop() {};

    return this.then(
      val => Promise.resolve(onFinally()).then(() => val),
      reason => Promise.resolve(onFinally()).then(() => {
        throw reason;
      })
    );
  }

  // 更改狀態,調用執行 callbacks 的方法
  resolve = val => {
    // 狀態只能更改一次
    if (this.state !== 'pending') {
      return;
    }

    // resolve 的值是 Promise 的狀況
    if (val && (typeof val === 'function' || typeof val === 'object')) {
      const then = val.then;
      if (typeof then === 'function') {
        then.call(val, this.resolve, this.reject);
        return;
      }
    }

    this.state = 'fulfilled';
    this.value = val;
    this.execute();
  }

  reject = reason => {
    // 狀態只能更改一次
    if (this.state !== 'pending') {
      return;
    }

    this.state = 'rejected';
    this.value = reason;
    this.execute();
  }

  // 執行 callbacks
  execute = () => {
    setTimeout(() => {
      this.callbacks.forEach(callback => {
        this.handle(callback);
      });
    }, 0);
  }

  // 負責處理單個 callback
  handle = callback => {
    if (this.state === 'pending') {
      this.callbacks.push(callback);
      return;
    }

    if (this.state === 'fulfilled') {
      // 若是 then 中沒有傳遞任何東西
      if (!callback.onFulfilled) {
        // 直接執行下一個 promise
        callback.resolve(this.value);
        return;
      }

      try {
        // 執行當前的 promise
        const ret = callback.onFulfilled(this.value);
        // 執行下一個 promise
        callback.resolve(ret);
      }
      catch (err) {
        callback.reject(err);
      }
      return;
    }

    if (this.state === 'rejected') {
      // 若是沒有指定 callback.onRejected
      if (!callback.onRejected) {
        callback.reject(this.value);
        return;
      }

      try {
        const ret = callback.onRejected(this.value);
        callback.resolve(ret);
      }
      catch (err) {
        callback.reject(err);
      }
      return;
    }
  }
}
複製代碼

單元測試用例:

import Promise from './promise';
import sleep from './../../shared/sleep';

// ref: https://github.com/promises-aplus/promises-tests
describe('Promise', () => {
  const time = 500;

  it('take a function as an argument', () => {
    expect(() => {
      new Promise(1);
    }).toThrowError('is not a function');
  });

  it('return a promise instace, exposes the public API', () => {
    const promise = new Promise(function (resolve, reject) {
      +new Date() % 2 === 0 ? resolve() : reject();
    });
    expect(promise).toHaveProperty('then');
    expect(promise).toHaveProperty('catch');
    expect(promise).toHaveProperty('finally');
  });

  it('promise.then, onFulfilled', done => {
    const promise = new Promise(function (resolve) {
      setTimeout(function () {
        resolve(time);
      }, time);
    });
    promise.then(ms => {
      expect(ms).toBe(time);
      done();
    });
  });

  it('promise.then, onRejected', done => {
    const promise = new Promise(function (resolve, reject) {
      setTimeout(function () {
        reject(time);
      }, time);
    });
    promise.then(() => {
      // onFulfilled
    }, reason => {
      // onRejected
      expect(reason).toBe(time);
      done();
    });
  });

  it('promise.catch', done => {
    const promise = new Promise(function (resolve, reject) {
      setTimeout(function () {
        reject(time);
      }, time);
    });
    promise.then(() => {
      // onFulfilled
    }).catch(reason => {
      expect(reason).toBe(time);
      done();
    });
  });

  it('promise.finally', done => {
    const promise = new Promise(function (resolve, reject) {
      setTimeout(function () {
        reject(time);
      }, time);
    });
    promise.then(() => {
      // onFulfilled
    }).catch(() => {
      // onRejected
    }).finally(() => {
      // Finally
      expect(true).toBe(true);
      done();
    });
  });

  it('promise chain call, onFulfilled', done => {
    new Promise(function (resolve) {
      setTimeout(function () {
        resolve(time);
      }, time);
    }).then(ms => {
      return new Promise(function (resolve) {
        setTimeout(function () {
          resolve(ms + time);
        }, time);
      });
    }).then(total => {
      expect(total).toBe(time * 2);
      done();
    });
  });

  it('promise chain call, onRejected', done => {
    new Promise(function (resolve, reject) {
      setTimeout(function () {
        reject(time);
      }, time);
    }).then(ms => {
      return new Promise(function (resolve) {
        setTimeout(function () {
          resolve(ms + time);
        }, time);
      });
    }).then(total => {
      return new Promise(function (resolve) {
        setTimeout(function () {
          resolve(total + time);
        }, time);
      });
    }).catch(reason => {
      expect(reason).toBe(time);
      done();
    });
  });

  it('can only change status once, cannot from fulfilled to rejected', async () => {
    let result = '';
    const promise = new Promise(function (resolve, reject) {
      setTimeout(function () {
        resolve(time);
        setTimeout(function () {  // 設定以後再 reject 一次
          reject(time);
        }, 0);
      }, time);
    });
    promise.then(() => {
      result += '=fulfilled=';
    }).catch(() => {
      result += '=rejected=';
    });

    await sleep(2000);
    // 不等於 'fulfilled rejected'
    expect(result).not.toBe('=fulfilled==rejected=');
    // 等於 'fulfilled'
    expect(result).toBe('=fulfilled=');
  });

  it('can only change status once, cannot from rejected to fulfilled', async () => {
    let result = '';
    const promise = new Promise(function (resolve, reject) {
      setTimeout(function () {
        reject(time);
        setTimeout(function () {  // 設定以後再 resolve 一次
          resolve(time);
        }, 0);
      }, time);
    });
    promise.then(() => {
      result += '=fulfilled=';
    }).catch(() => {
      result += '=rejected=';
    });

    await sleep(2000);
    // 不等於 'fulfilled rejected'
    expect(result).not.toBe('=rejected==fulfilled=');
    // 等於 'rejected'
    expect(result).toBe('=rejected=');
  });

  it('Promise.resolve', done => {
    Promise.resolve(1).then(num => {
      expect(num).toBe(1);
      done();
    });
  });

  it('Promise.reject', done => {
    Promise.reject(1).catch(num => {
      expect(num).toBe(1);
      done();
    });
  });
});
複製代碼

執行單元測試用例:

PASS  src/javascript/promise/promise.test.js (8.328s)
  Promise
    ✓ take a function as an argument (13ms)
    ✓ return a promise instace, exposes the public API (2ms)
    ✓ promise.then, onFulfilled (507ms)
    ✓ promise.then, onRejected (505ms)
    ✓ promise.catch (506ms)
    ✓ promise.finally (509ms)
    ✓ chain call, onFulfilled (1007ms)
    ✓ chain call, onRejected (505ms)
    ✓ can only change status once, cannot from fulfilled to rejected (2002ms)
    ✓ can only change status once, cannot from rejected to fulfilled (2002ms)
    ✓ Promise.resolve
    ✓ Promise.reject (1ms)
複製代碼

async/await 原理實現

實現代碼:

export function asyncGenerator(fn) {
  return function () {
    const gen = fn.apply(this, arguments);

    return new Promise(function(resolve, reject) {
      function step(result) {
        try {
          // 執行結束
          if (result.done) {
            return resolve(result.value);
          }
          // 繼續往下執行
          Promise.resolve(result.value)
            .then(val => step(gen.next(val)))
            .catch(err => reject(err));
        }
        catch(e) {
          return reject(e);
        }
      }
      // 遞歸地一步一步執行 gen.next()
      step(gen.next());
    });
  };
}
複製代碼

單元測試用例:

import { asyncGenerator } from './async';

describe('async generator', () => {
  it('auto excute generator', done => {
    const asyncFunc = asyncGenerator(function*() {
      const a = yield new Promise(resolve => {
        setTimeout(() => {
          resolve('a');
        }, 1000);
      });
      const b = yield Promise.resolve('b');
      const c = yield 'c';
      const d = yield Promise.resolve('d');
      return [a, b, c, d];
    });

    asyncFunc().then(res => {
      expect(res).toEqual(['a', 'b', 'c', 'd']);
      done();
    });
  });

  it('auto excute generator, when get reject', done => {
    const errorMsg = 'error';
    const asyncFunc = asyncGenerator(function*() {
      const s = yield 's';
      yield Promise.reject(errorMsg);
      return s;
    });

    asyncFunc()
      .catch(res => {
        expect(res).toBe(errorMsg);
        done();
      });
  });
});
複製代碼

執行單元測試用例:

PASS  src/javascript/async/async.test.js
  async generator
    ✓ auto excute generator (1012ms)
複製代碼

訂閱發佈機制

實現代碼:

export default class EventEmitter {
  constructor() {
    this.listeners = {};
  }

  on = (event, listener) => {
    this.bindEvent(event, listener, false);
  }

  once = (event, listener) => {
    this.bindEvent(event, listener, true);
  }

  emit = (event, ...args) => {
    if (!this.hasBind(event)) {
      console.warn(`this event: ${event} don't has bind listener.`);
      return;
    }

    const { listeners, isOnce } = this.listeners[event];
    listeners.forEach(listener => listener.call(this, ...args));
    if (isOnce) {
      this.off(event);
    }
  }

  off = (event, listener) => {
    if (!this.hasBind(event)) {
      console.warn(`this event: ${event} don't exist.`);
      return;
    }

    // remove all listener
    if (!listener) {
      delete this.listeners[event];
      return;
    }

    // remove specific listener
    const listeners = this.listeners[event].listeners;
    listeners.forEach(listener => {
      const index = listeners.indexOf(listener);
      if (index !== -1) {
        listeners.splice(index, 1);
      }
    });
  }

  hasBind = event => {
    return this.listeners[event]
      && this.listeners[event].listeners
      && this.listeners[event].listeners.length;
  }

  bindEvent = (event, listener, isOnce = false) => {
    if (!event || !listener) {
      return;
    }

    this.listeners[event] = this.listeners[event] || {
      isOnce: false,
      listeners: []
    };
    this.listeners[event].isOnce = isOnce;

    if (isOnce) {
      this.listeners[event].listeners = [listener];
    }
    else {
      this.listeners[event].listeners.push(listener);
    }
  }
}
複製代碼

單元測試用例:

import EventEmitter from './event-emitter';

describe('EventEmitter', () => {
  let emitter;

  beforeEach(() => {
    emitter = new EventEmitter();
  });

  it('exposes the public API', () => {
    expect(emitter).toHaveProperty('on');
    expect(emitter).toHaveProperty('emit');
    expect(emitter).toHaveProperty('once');
    expect(emitter).toHaveProperty('off');
  });

  it('emitter.on', () => {
    const foo = jest.fn();
    const bar = jest.fn();
    emitter.on('foo', foo);
    expect(emitter.listeners['foo'].listeners).toEqual([foo]);
    emitter.on('foo', bar);
    expect(emitter.listeners['foo'].listeners).toEqual([foo, bar]);
  });

  it('emitter.once', () => {
    const foo = jest.fn();
    const bar = jest.fn();
    emitter.once('foo', foo);
    expect(emitter.listeners['foo'].listeners).toEqual([foo]);
    emitter.once('foo', bar);
    expect(emitter.listeners['foo'].listeners).toEqual([bar]);
  });

  it('emitter.emit', () => {
    // emitter.on
    const foo = jest.fn();
    emitter.on('foo', foo);
    emitter.emit('foo', 'x');
    expect(foo).toHaveBeenNthCalledWith(1, 'x');
    emitter.emit('foo', 'x');
    expect(foo).toHaveBeenCalledTimes(2);

    // emitter.once
    const bar = jest.fn();
    emitter.once('bar', bar);
    emitter.emit('bar', 'x');
    expect(bar).toHaveBeenNthCalledWith(1, 'x');
    emitter.emit('bar', 'x');
    expect(bar).toHaveBeenCalledTimes(1);
  });

  it('emitter.off, remove all listener', () => {
    const foo = jest.fn();
    emitter.on('foo', foo);
    emitter.emit('foo', 'x');
    emitter.off('foo');
    emitter.emit('foo', 'x');
    expect(foo).toHaveBeenCalledTimes(1);
  });

  it('emitter.off, remove specific listener', () => {
    const foo = jest.fn();
    const bar = jest.fn();
    emitter.on('foo', foo);
    emitter.on('foo', bar);
    emitter.emit('foo', 'x');
    expect(foo).toHaveBeenCalledTimes(1);
    expect(bar).toHaveBeenCalledTimes(1);
    emitter.off('foo', foo);
    emitter.emit('foo', 'x');
    expect(foo).toHaveBeenCalledTimes(1);
    expect(bar).toHaveBeenCalledTimes(2);
  });
});
複製代碼

執行單元測試用例:

PASS  src/javascript/event-emitter/event-emitter.test.js
  EventEmitter
    ✓ exposes the public API (4ms)
    ✓ emitter.on (2ms)
    ✓ emitter.once (1ms)
    ✓ emitter.emit (14ms)
    ✓ emitter.off, remove all listener (2ms)
    ✓ emitter.off, remove specific listener (1ms)
複製代碼

數據結構:堆

實現代碼:

/** * 堆(Heap)是計算機科學中的一種特別的樹狀數據結構,如果知足一下特性,便可成爲堆:給定堆中任意節點 P 和 C, * 若 P 是 C 的母節點,那麼 P 的值會小於等於(或大於等於)C 的值。若母節點的值恆小於等於子節點的值,此堆成爲最小堆。 * 若母節點恆小於等於子節點的值,此堆成爲最小堆(min heap) * 若母節點恆大於等於子節點的值,此堆成爲堆大堆(max heap) * 在堆中最頂端的那一個節點,稱做根節點(root node),根節點自己沒有母節點(parent node) * * 在隊列中,調度程序反覆提取隊列中第一個做業並運行,由於實際狀況中某些事件較短的任務將等待很長事件才能結束。 * 或者某些不短小,但具備重要性的做業,一樣應當具備優先權。堆即爲解決此類問題設計的一種數據結構。 * 也就是優先隊列:一種特殊的隊列,隊列中元素出棧的順序是按照元素的優先權大小,而不是元素入隊的前後順序。 * * 堆的特性: * - 堆是一棵徹底二叉樹。即除了最底層,其餘層的節點都被元素填滿,且最底層儘量地從左到右填入。 * - 任意節點小於(或大於)它的全部子節點。 * - 能夠用數組來存儲二叉堆。 */

/** * 實現堆(最大堆、最小堆) */
export default class Heap {
  constructor(type, nums) {
    this.type = type || 'max';  // 默認爲最大堆
    this.items = [];

    if (Array.isArray(nums) && nums.length) {
      nums.forEach(n => this.add(n));
    }
  }

  isMaxHeap = () => {
    return this.type === 'max';
  }

  isMinHeap = () => {
    return this.type === 'min';
  }

  size = () => {
    return this.items.length;
  }

  getParentIndex = i => {
    return Math.floor((i - 1) / 2);
  }

  getLeftChildIndex = i => {
    return i * 2 + 1;
  }

  getRightChildIndex = i => {
    return i * 2 + 2;
  }

  swap = (i, j) => {
    const temp = this.items[i];
    this.items[i] = this.items[j];
    this.items[j] = temp;
  }

  // 向堆底插入元素
  add = el => {
    this.items.push(el);
    this.siftUP(this.size() - 1);
  }

  siftUP = index => {
    // 遞歸終止條件
    if (index <= 0) {
      return;
    }

    // 找到父元素的索引和值
    const item = this.items[index];
    const parentIndex = this.getParentIndex(index);
    const parent = this.items[parentIndex];

    // 若是是最大堆
    if (this.isMaxHeap()) {
      // 若是母節點的值小於子節點,則該節點須要上浮,即須要交換位置
      if (item > parent) {
        this.swap(index, parentIndex);
        // 再遞歸對 parent 作上浮操做
        this.siftUP(parentIndex);
      }
    }
    else if (this.isMinHeap()) {
      // 若是母節點的值大於子節點,則該節點須要上浮,即須要交換位置
      if (item < parent) {
        this.swap(index, parentIndex);
        // 再遞歸對 parent 作上浮操做
        this.siftUP(parentIndex);
      }
    }
  }

  // 取出棧頂元素,並從新構建徹底二叉樹
  extract = () => {
    // 獲取堆頂元素
    const item = this.items.shift();

    // 若是當前堆的元素個數小於 2 時,能夠直接返回,不須要從新構建徹底二叉樹
    if (this.size() < 2) {
      return item;
    }

    // 如今分離成了兩個徹底二叉樹,須要從新構建成一顆徹底二叉樹
    // 獲取最後一個元素,並把它放到堆頂
    this.items.unshift(this.items.pop());
    // 進行 siftDown 操做,從新構建一顆徹底二叉樹
    this.siftDown(0);

    // 返回堆頂元素
    return item;
  }

  siftDown = index => {
    const leftChildIndex = this.getLeftChildIndex(index);

    // 當沒有左子樹(也就沒有右子樹)時,遞歸終止
    if (leftChildIndex >= this.size()) {
      return;
    }

    const leftChild = this.items[leftChildIndex];
    const rightChildIndex = this.getRightChildIndex(index);
    const rightChild = this.items[rightChildIndex];
    let nextIndex = leftChildIndex;

    if (this.isMaxHeap()) {
      // 找到左右子樹的最大值
      if (typeof rightChild !== undefined && rightChild > leftChild) {
        nextIndex = rightChildIndex;
      }
    }
    else if (this.isMinHeap()) {
      // 找到左右子樹的最小值
      if (typeof rightChild !== undefined && rightChild < leftChild) {
        nextIndex = rightChildIndex;
      }
    }

    const parent = this.items[index];
    const next = this.items[nextIndex];

    if (this.isMaxHeap()) {
      // 若是左右子樹的最大值大於母節點的值,則母節點須要下沉,即須要交換位置
      if (next > parent) {
        this.swap(index, nextIndex);
        // 再遞歸對母節點進行下沉
        this.siftDown(nextIndex);
      }
    }
    else if (this.isMinHeap()) {
      // 若是左右子樹的最小值小於母節點的值,則母節點須要下沉,即須要交換位置
      if (next < parent) {
        this.swap(index, nextIndex);
        // 再遞歸對母節點進行下沉
        this.siftDown(nextIndex);
      }
    }
  }

  toString = connector => {
    return this.items.join(connector);
  }
}
複製代碼

單元測試用例:

import Heap from './heap';

describe('Heap', () => {
  let heap;

  beforeEach(() => {
    heap = new Heap();
  });

  it('exposes the public API', () => {
    expect(heap).toHaveProperty('add');
    expect(heap).toHaveProperty('extract');
    expect(heap).toHaveProperty('toString');
  });
});

describe('Max Heap', () => {
  let heap;

  beforeEach(() => {
    heap = new Heap();
  });

  it('maxHeap.add', () => {
    // 亂序加入
    heap.add(3);
    heap.add(1);
    heap.add(5);
    heap.add(2);
    heap.add(4);
    heap.add(6);
    expect(heap.toString()).toBe('6,4,5,1,2,3');
  });

  it('maxHeap.extract', () => {
    // 亂序加入
    heap.add(1);
    heap.add(4);
    heap.add(2);
    heap.add(5);
    heap.add(3);
    heap.add(6);
    expect(heap.extract()).toBe(6);
    expect(heap.toString()).toBe('5,4,2,1,3');
  });
});

describe('Min Heap', () => {
  let heap;

  beforeEach(() => {
    heap = new Heap('min');
  });

  it('minHeap.add', () => {
    // 亂序加入
    heap.add(3);
    heap.add(1);
    heap.add(5);
    heap.add(2);
    heap.add(4);
    heap.add(6);
    expect(heap.toString()).toBe('1,2,5,3,4,6');
  });

  it('minHeap.extract', () => {
    // 亂序加入
    heap.add(1);
    heap.add(4);
    heap.add(2);
    heap.add(5);
    heap.add(3);
    heap.add(6);
    expect(heap.extract()).toBe(1);
    expect(heap.toString()).toBe('2,3,6,5,4');
  });
});
複製代碼

執行單元測試用例:

PASS  src/dsa/heap/heap.test.js
  Heap
    ✓ exposes the public API (4ms)
  Max Heap
    ✓ maxHeap.add (1ms)
    ✓ maxHeap.extract (1ms)
  Min Heap
    ✓ minHeap.add
    ✓ minHeap.extract (1ms)
複製代碼

算法:雙指針

實現代碼:

/** * 輸入一個鏈表,輸出該鏈表中倒數第 k 個結點。 */
export default function findKthToTail(head, k) {
  if (!head || !k) {
    return null;
  }

  // 雙指針
  let first = head;
  let second = head;
  let index = 1;

  while (first.next) {
    first = first.next;
    // 當 first 和 second 相距 k 時,second 開始逐漸前進
    if (index >= k) {
      second = second.next;
    }
    index++;
  }

  // 循環結束後, k 若是大於 index 則說明倒數第 k 個節點不存在
  if (k > index) {
    return null;
  }
  return second;
}
複製代碼

單元測試用例:

import { LinkedList } from './../linked-list/linkedList';
import findKthToTail from './findKthToTail';

describe('find Kth to tail', () => {
  it('when k is less or equal than linked list size', () => {
    const sourceArr = [1, 2, 3, 4, 5];
    const linkedList = LinkedList.from(sourceArr);
    expect(findKthToTail(linkedList.head, 3).val).toEqual(3);
    expect(findKthToTail(linkedList.head, 5).val).toEqual(1);
  });

  it('when k is greate than linked list size', () => {
    const sourceArr = [1, 2, 3, 4, 5];
    const linkedList = LinkedList.from(sourceArr);
    expect(findKthToTail(linkedList.head, 6)).toEqual(null);
  });
});
複製代碼

執行單元測試用例:

PASS  src/dsa/doublePointer/findKthToTail.test.js
  find Kth to tail
    ✓ when k is less or equal than linked list size (4ms)
    ✓ when k is greate than linked list size (9ms)
複製代碼

能夠看出來,寫單測並不複雜,測試框架的接口簡單易用,寫單測用例也比較容易。目前的代碼覆蓋率在 90% 以上。

結語

但願這篇文章能對你積累本身的源碼庫有所幫助,若是有其餘想要加入的內容,歡迎提 IssuePR

相關文章
相關標籤/搜索