underscore 誕生記(一)—— 基本結構搭建

  • 2019 年 11 月 5 日
  • 筆記

1. 簡介

underscore 是一款成熟可靠的第三方開源庫,正如 jQuery 統一了不同瀏覽器之間的 DOM 操作的差異,讓我們可以簡單地對 DOM 進行操作,underscore 則提供了一套完善的函數式編程的介面,讓我們更方便地在 JavaScript 中實現函數式編程。

jQuery 在載入時,會把自身綁定到唯一的全局變數 $ 上,underscore 與其類似,會把自身綁定到唯一的全局變數 _ 上,這也是為啥它的名字叫 underscore 的原因。

在搭建 underscore 之前,讓我們先來了解一下什麼是 「立即執行函數(IIFE)」.

2. 立即執行函數(IIFE)

立即執行函數,顧名思義,就是定義好的匿名函數立即執行,寫法如下:

(function(name) {    console.log(name);  })('suporka');  複製程式碼

其作用是:通過定義一個匿名函數,創建了一個新的函數作用域,相當於創建了一個「私有」的命名空間,該命名空間的變數和方法,不會破壞污染全局的命名空間。

// 函數外部拿不到內部的變數,因此不會造成變數污染,內部的變數在內部使用即可  (function() {    var name = 'suporka';  })();    console.log(name); // name is undefinded  複製程式碼

3. 全局變數 _ 的掛載

當我們在瀏覽器中使用 _.map([1,2,3], function(item){console.log(item)}) 時, _ 是掛載在 Window對象上的,如果我們想在 node 環境中使用呢 ?

(function() {    // root 為掛載對象,為 self 或 global 或 this 或 {}    var root =      (typeof self == 'object' && self.self === self && self) ||      (typeof global == 'object' && global.global === global && global) ||      this ||      {};      // _ 應該是一個對象,對象內有屬性函數    var _ = {};    root._ = _;    _.VERSION = '1.9.1'; // 給我們的 underscore 一個版本號吧  })();  複製程式碼

4. 函數式風格 && 面向對象風格的雙重實現

首先我們實現一個倒裝字元串的方法

(function() {    // root 為掛載對象,為 self 或 global 或 this 或 {}    var root =      (typeof self == 'object' && self.self === self && self) ||      (typeof global == 'object' && global.global === global && global) ||      this ||      {};      // _ 應該是一個對象,對象內有屬性函數    var _ = {};      root._ = _;      _.VERSION = '1.9.1'; // 給我們的 underscore 一個版本號吧      /**     * 字元串倒裝     */    _.reverse = function(string) {      return string        .split('')        .reverse()        .join('');    };  })();    _.reverse('suporka'); // akropus  複製程式碼

不錯,很快實現,但是這種是函數式寫法,調用一個函數去實現,如果我們要實現面向對象寫法呢?如 _('suporka').reverse()! underscore 是支援這種寫法的,仔細觀察 _('suporka') , 你會發現,_ 是一個函數啊,和我們前面定義的 var _ = {}; 不一致,那麼該怎麼實現呢?

實例原型

我們先測試一下:如果 _ 為函數,我們需要保存其傳進來的參數 obj . new _() 生成一個實例原型對象

function _(obj) {    this._wrapped = obj;  }  _.reverse = function(string) {    return string      .split('')      .reverse()      .join('');  };  _.reverse('suporka'); // "akropus", 函數式調用沒問題    new _('suporka');  複製程式碼

從圖中我們可以看出,實例原型對象的 __proto__ (原型)的 constructor 構造函數指回了原來的 _(obj) 函數,要調用其 reverse() 方法只能 new _('suporka').constructor.reverse()多了一個層級,不符合我們原本的期望。那我們不如在_proto_ 屬性下增加一個和 reverse 一樣的函數,這樣不就可以直接調用了嗎?

let us try it !

function _(obj) {    this._wrapped = obj;  }  _.reverse = function(string) {    return string      .split('')      .reverse()      .join('');  };  _.reverse('suporka'); // "akropus", 函數式調用沒問題    _.prototype.reverse = function() {    return this._wrapped      .split('')      .reverse()      .join('');  };  new _('suporka').reverse(); // "akropus", 面向對象式調用沒問題  複製程式碼

5. 改造 _() function

new _('suporka').reverse() 有點累贅,去掉 new, 重寫 function _()

var _ = function(obj) {    // 如果傳入的是實例後對象,返回它    if (obj instanceof _) return obj;    // 如果還沒有實例化,new _(obj)    if (!(this instanceof _)) return new _(obj);    this._wrapped = obj;  };    _('suporka').reverse(); // "akropus", 面向對象式調用沒問題  複製程式碼

6. 寫一個迭代函數 map()

/**   * 數組或對象遍歷方法,並返回修改後的對象或數組   * @param iteratee 回調函數   * @param context 回調函數中this的指向   */  _.map = function(obj, iteratee, context) {    var length = obj.length,      results = Array(length);    for (var index = 0; index < length; index++) {      results[index] = iteratee.call(context, obj[index], index, obj);    }      return results;  };    _.prototype.map = function(iteratee, context) {    var length = this._wrapped.length,      results = Array(length);    for (var index = 0; index < length; index++) {      results[index] = iteratee.call(        context,        this._wrapped[index],        index,        this._wrapped      );    }      return results;  };    _([1, 2, 3]).map(    function(item) {      console.log(item + this.value);    },    { value: 1 }  ); // 2,3,4  _.map(    [1, 2, 3],    function(item) {      console.log(item + this.value);    },    { value: 1 }  ); // 2,3,4  複製程式碼

嗯嗯,真好,完美實現。到這裡你會發現一個問題,每次我新增一個方法,都得在 prototype 上同時寫多一次這個相似函數,你會發現兩者之間只是 obj 換成了 this._wrapped.有沒有辦法讓它自動生成呢?答案肯定是有!

7. 自動創建原型方法

在這之前,我們需要先實現一個遍歷方法 each(),如下:

// 最大數值  var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;  // 判斷是否為數組  var isArrayLike = function(collection) {    var length = collection.length;    return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;  };    /**   * 數組或對象遍歷方法   */  _.each = function(obj, callback) {    var length,      i = 0;      if (isArrayLike(obj)) {      // 數組      length = obj.length;      for (; i < length; i++) {        //   這裡隱式的調用了一次 callback.call(obj[i], obj[i], i);        if (callback.call(obj[i], obj[i], i) === false) {          break;        }      }    } else {      // 對象      for (i in obj) {        if (callback.call(obj[i], obj[i], i) === false) {          break;        }      }    }      return obj;  };  複製程式碼

用 each() 來遍歷 _ 上掛載的所有方法函數,並給 prototype 創建相應的方法函數。那麼,在此之前,我們需要知道 _ 上掛載了哪些方法名,來寫個 functions() 實現它

/**   * 判斷是否為 function   */  _.isFunction = function(obj) {    return typeof obj == 'function' || false;  };    /**   * 獲取_的所有屬性函數名   */  _.functions = function(obj) {    var names = [];    for (var key in obj) {      if (_.isFunction(obj[key])) names.push(key);    }    return names.sort();  };  複製程式碼

用 each()實現它:

var ArrayProto = Array.prototype;  var push = ArrayProto.push;  _.each(_.functions(_), function(name) {    var func = _[name];    _.prototype[name] = function() {      var args = [this._wrapped];      // args = [this._wrapped, arguments[0], arguments[1]...], 相當於用 this._wrapped 代替 obj 實現      push.apply(args, arguments);      return func.apply(_, args);    };  });  複製程式碼

7. 當前最終程式碼

(function() {    // root 為掛載對象,為 self 或 global 或 this 或 {}    var root =      (typeof self == 'object' && self.self === self && self) ||      (typeof global == 'object' && global.global === global && global) ||      this ||      {};      var _ = function(obj) {      // 如果傳入的是實例後對象,返回它      if (obj instanceof _) return obj;      // 如果還沒有實例化,new _(obj)      if (!(this instanceof _)) return new _(obj);      this._wrapped = obj;    };      // 最大數值    var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;    var ArrayProto = Array.prototype;    var push = ArrayProto.push;    // 判斷是否為數組    var isArrayLike = function(collection) {      var length = collection.length;      return (        typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX      );    };      root._ = _;      _.VERSION = '1.9.1'; // 給我們的 underscore 一個版本號吧      /**     * 字元串倒裝     */    _.reverse = function(string) {      return string        .split('')        .reverse()        .join('');    };    /**     * 判斷是否為 function     */    _.isFunction = function(obj) {      return typeof obj == 'function' || false;    };      /**     * 獲取_的所有屬性函數名     */    _.functions = function(obj) {      var names = [];      for (var key in obj) {        if (_.isFunction(obj[key])) names.push(key);      }      return names.sort();    };    /**     * 數組或對象遍歷方法,並返回修改後的對象或數組     * @param iteratee 回調函數     * @param context 回調函數中this的指向     */    _.map = function(obj, iteratee, context) {      var length = obj.length,        results = Array(length);      for (var index = 0; index < length; index++) {        results[index] = iteratee.call(context, obj[index], index, obj);      }        return results;    };      /**     * 數組或對象遍歷方法     */    _.each = function(obj, callback) {      var length,        i = 0;        if (isArrayLike(obj)) {        // 數組        length = obj.length;        for (; i < length; i++) {          //   這裡隱式的調用了一次 callback.call(obj[i], obj[i], i);          if (callback.call(obj[i], obj[i], i) === false) {            break;          }        }      } else {        // 對象        for (i in obj) {          if (callback.call(obj[i], obj[i], i) === false) {            break;          }        }      }        return obj;    };      _.each(_.functions(_), function(name) {      var func = _[name];      _.prototype[name] = function() {        var args = [this._wrapped];        // args = [this._wrapped, arguments[0], arguments[1]...], 相當於用 this._wrapped 代替 obj 實現        push.apply(args, arguments);        return func.apply(_, args);      };    });  })();  複製程式碼

Tags: