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);      };    });  })();  复制代码