Underscore源码分析系列(1)

Underscore源码分析系列(1)

LirilLiril

目录

Underscore源码分析系列(1) - Liril的文章 - 知乎专栏

Underscore源码分析系列(2) - Liril的文章 - 知乎专栏

Underscore源码分析系列(3) - Liril的文章 - 知乎专栏

Underscore源码分析系列(4) - Liril的文章 - 知乎专栏

Underscore源码分析系列(5) - Liril的文章 - 知乎专栏

Underscore源码分析系列(6) - Liril的文章 - 知乎专栏



说明

underscore是一套优秀的JavaScript工具库,它为函数式编程提供许多常用的方法,因此了解其源码对函数式编程有比较好的帮助。本文将基于underscore 1.8.3来进行源码分析的。

_.VERSION = "1.8.3" ;

立即执行函数

underscore最外层是一个立即执行函数,如下:

(function () {...}) (); // 立即执行函数

立即执行函数(IIFE)的主要特征是:

1. 使用函数表达式声明一个函数

2. 在其后使用括号直接调用

使用立即函数可以创建一个独立的沙箱似的作用域,它是函数的一种特殊调用方式,利用了JavaScript的函数作用域的概念。这样可以防止其他代码对该函数内部造成影响,而且不会创造全局变量防止污染全局空间。



全局命名空间

var
  root =
  (
    (
      (
        ((typeof(self)) == ("object")) // self表示window窗口自身,这是浏览器环境下的全局命名空间
        &&
        ((self.self) === (self)) // 如果存在self,判断self是否是自身引用,即window这一对象
      )
      &&
      (self) // 如果以上都满足,说明全局对象是window,并返回window作为root,这里self即window
    )
    ||
    (
      (
        ((typeof(global)) == ("object")) // global表示node环境下全局的命名空间
        &&
        ((global.global) === (global)) // 如果存在gloabl,判断global是否是自身引用
      )
      &&
      (global) // 如果以上都满足,说明全局对象是global,并返回global作为root
    )
  )
  ||
  (this); // 如果以上都不满足,直接返回this,这里应该处理既不是window这一浏览器环境,也不是global这一node环境的

该段代码主要用于确认环境的全局命名空间,并赋值给变量root。有趣的是,之前代码是这样的:

var root = this;

这引起了笔者的好奇,并进行了一定的研究,首先这一做法应该是出自学习了lodash的做法。它有如下两个好处:


  1. 向前兼容严格模式,在严格模式下直接使用this会得到undefined。这是因为ecma262第5版中,为了防止人们误将非new出来的对象的this指向全局命名空间,特地将之设置为undefined
  2. 用来支持WebWorker,在WebWorker里可以使用self但不能使用window

另一个有趣的地方的是,selfglobal使用typeof判断时使用==而非===,这样可以进行转义,因为全局命名空间不一定完全等价为对象,这跟浏览器的实现有关。



防冲突

防止与其他库对 _ 的使用冲突是十分重要的。underscore做了如下处理。


// 保存原有全局变量“_”
var previousUnderscore = root._;

// 使用noConflict方法返回自身
_.noConflict = function() {
  root._ = previousUnderscore;
  return this;
};

保存原型以及方法的快速引用

var
  ArrayProto = Array.prototype,
  ObjProto = Object.prototype;
var
  SymbolProto = ((typeof(Symbol)) !== ("undefined")) ? (Symbol.prototype) : (null);
var
  push = ArrayProto.push,
  slice = ArrayProto.slice,
  toString = ObjProto.toString,
  hasOwnProperty = ObjProto.hasOwnProperty;
var
  nativeIsArray = Array.isArray,
  nativeKeys = Object.keys,
  nativeCreate = Object.create;

其中值得注意的是,Symbolecma262第6版才正式发布的,所以要先判断是否存在。



对象创建的特殊处理

为了处理Object.create的跨浏览器的兼容性,underscore进行了特殊的处理。我们知道,原型是无法直接实例化的,因此我们先创建一个空对象,然后将其原型指向这个我们想要实例化的原型,最后返回该对象其一个实例。其代码如下:

var Ctor = function() {};  // 用于代理原型转换的空函数

var baseCreate = function(prototype) {
  if (!(_.isObject(prototype))) return {}; // 如果参数不是对象,直接返回空对象
  if (nativeCreate) return nativeCreate(prototype); // 如果原生的对象创建可以使用,返回该方法根据原型创建的对象
    
  // 处理没有原生对象创建的情况
  Ctor.prototype = prototype;  // 将空函数的原型指向要使用的原型
  var result = new Ctor();  // 创建一个实例
  Ctor.prototype = null;  // 恢复Ctor的原型供下次使用
  return result;  // 返回该实例
};

初始化

var _ = function(obj) {
  if (obj instanceof _) return obj;  // 如果参数是underscore的一个实例,就直接返回该参数
  if (!(this instanceof _)) return new _(obj);  // 实例化
  this._wrapped = obj;  // 将该参数保存
};

这里十分有趣,当使用函数式风格的代码时并不会有太大影响,但使用面向对象风格的代码时,这里省去了使用者调用new的麻烦。


 if (typeof exports != 'undefined' && !exports.nodeType) {
  if (typeof module != 'undefined' && !module.nodeType && module.exports) {
  exports = module.exports = _;
  }
  exports._ = _;
} else {
  root._ = _;
}

这里笔者翻译一下原有注释。这一部分代码是为了在node.js环境下将underscore作为一个模块使用,并向后兼容旧版的模块API,即require。如果在浏览器环境中,则将underscore以_暴露到全局。值得注意的是使用nodeType来确保exportsmodule并不是HTML的元素。



回调处理

underscore有大量需要回调的函数,因此对于回调进行了特殊的处理。首先存在一个optimizeCb函数,它对正常传入的函数进行一层包装处理,这样可以在更好的重复使用,保证上下文即this正确。


var optimizeCb = function(func, context, argCount) {
  if (context === void 0) return func;  // void 0返回undefined,即未传入上下文信息时直接返回相应的函数
  switch (argCount == null ? 3 : argCount) {  // 如果传入了argCount,那么参数数量为argCount,如果传入等价为null,则为3,包括未传值得情况
    // 1个参数的时候,只需要传递当前值
    case 1: return function(value) {
      return func.call(context, value);  
    };
    // 并没有2个参数的时候,因为目前并没有用到2个参数的时候
    
    // 3个参数的时候,分别是当前值、当前索引以及整个集合
    case 3: return function(value, index, collection) {
      return func.call(context, value, index, collection);
    };
    // 4个参数的时候,分别是累计值、当前值、当前索引以及整个集合
    case 4: return function(accumulator, value, index, collection) {
      return func.call(context, accumulator, value, index, collection);
    };
  }
  // 如果都不符合上述的任一条件,直接使用apply调用相关函数
  return function() {
    return func.apply(context, arguments);
  };
};

接下来,是针对集合迭代的回调处理。

var builtinIteratee;  // 设置变量保存内置迭代

var cb = function(value, context, argCount) {
  if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);  // 如果用户修改了迭代器,则使用新的迭代器
  if (value == null) return _.identity;  // 如果不传value,表示返回等价的自身
  if (_.isFunction(value)) return optimizeCb(value, context, argCount);  // 如果传入函数,返回该函数的回调
  if (_.isObject(value)) return _.matcher(value);  // 如果传入对象,寻找匹配的属性值
  return _.property(value);  // 如果都不是,返回相应的属性访问器
};
  
// 默认的迭代器,是以无穷argCount为参数调用cb函数。用户可以自行修改。
_.iteratee = builtinIteratee = function(value, context) {
  return cb(value, context, Infinity);
};

杂项

在开始集合函数编写前还有一些处理。

// 属性访问器生成。
// 通过传入键名,返回可以访问以传入对象为参数,并可以获取该对象相应键值对的函数
var property = function(key) {
  return function(obj) {
    return obj == null ? void 0 : obj[key];
  };
};

// 额外参数。
// 等价于ES6的rest参数。它将起始索引后的参数放入一个数组中。
var restArgs = function(func, startIndex) {
  startIndex = startIndex == null ? func.length - 1 : +startIndex;  // startIndex为null时,为函数声明时的参数数量减1,即除了第一个参数后的其他参数,否则为传入的startIndex
  
  return function() {
    var length = Math.max(arguments.length - startIndex, 0),  // 长度为实际参数数量与起始索引差值,若为负数则取0
        rest = Array(length),  // 剩余参数是一个length长度的数组
        index = 0;
        
    // 将startIndex开始的length长度的参数放入rest数组中
    for (; index < length; index++) {
      rest[index] = arguments[index + startIndex];
    }
    
    // 根据开始索引进行处理
    switch (startIndex) {
      case 0: return func.call(this, rest);  // 起始索引为0,表示全部参数作为一个数组调用
      case 1: return func.call(this, arguments[0], rest);  // 表示将第一个参数以及后续参数的数组,作为两个参数进行调用
      case 2: return func.call(this, arguments[0], arguments[1], rest);  // 将第一个参数、第二个参数以及后续参数的数组作为三个参数进行调用
    }
    
    // 将起始索引前的参数保存为一个数组args,并将最后一个值存为之前的rest数组,即[,[]]
    var args = Array(startIndex + 1);
    for (index = 0; index < startIndex; index++) {
      args[index] = arguments[index];
    }
    args[startIndex] = rest;
    
    // 将args作为参数调用
    return func.apply(this, args);
  };
};

// 处理类数组对象。
// 类数组对象的长度应当是一个非负整数
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;  // 表示正无穷+∞
var getLength = property('length');  // 获取length属性访问器
var isArrayLike = function(collection) {
  var length = getLength(collection);
  return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
文章被以下专栏收录
15 条评论
推荐阅读