美文网首页
JS进阶——underscore源码(1)

JS进阶——underscore源码(1)

作者: 金华每文 | 来源:发表于2017-07-03 14:40 被阅读0次

目录

说明

underscore是一个很有用的javaScript工具库,对函数式编程提供很多方法,所以读源码很适合了解函数式编程,笔者读的是1.8.3版,代码和注释都为手写,又不懂得也参考了网上资料,然后用了es6的let代替了var,等读完会将封装过的js文件放到github上:

_.VERSION = '1.8.3';

立即执行函数

underscore的最外层是一个立即执行函数,所有内容都放在函数内部,利用的思想是闭包。
(function ( ) {...}) ()
形成一个独立的作用域,这样的好处是可以不污染全局,也可以防止其他因素对内部函数的影响

全局变量声明

 let 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;

但在1.8.3版本中查看了github上的源码,发现它是最上面的封装方式,这样的好处有:

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

另一个值得注意的地方是:

(typeof self == 'object')

这里用的是==,而不是===。==和===的区别是一个不完全等,一个是全等,用==的好处是可以进行转义,因为全局命名空间不一定完全等价为对象,这跟浏览器的实现有关。

避免命名冲突

let previousUnderscore = root._; // 将全局变量中的变量_赋值给变量previousUnderscore进行缓存

previousUnderscore,从字面上理解就是“以前的 underscore”,最开始不明白这条语句的用意,但是从头到尾只有一个地方用到了 previousUnderscore,即(1352行):

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

如果发生冲突,可以用例如

var underscore_cache = _.noConflict();

用来重新定义 underscore 命名.

_(初始化)

    let _ = function (obj) {
        if (obj instanceof _){
            return obj;
        }
        // 如果obj已经是·_·函数的实例,则直接返回obj
        if (!(this instanceof _)) {
            return new _(obj);
        }
        // 如果不是`_`函数的实例
        // 则调用new运算符,返回实例化对象
        this._wrapped = obj;
        // 将obj赋值给this._wrapped属性
    };

其实核心函数——_其实是一个构造函数,支持无new调用的构造函数。当使用函数式风格的代码时并不会有太大影响,但使用面向对象风格的代码时,这里省去了使用者调用new的麻烦。

node服务端中:

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

这是 Node.js 中对通用模块的封装方法,通过对判断 exports 是否存在来决定将局部变量 _ 赋值给exports,顺便说一下 AMD 规范、CMD规范和 UMD规范,Underscore.js 是支持 AMD 的,在源码尾部有定义,这里简单叙述一下:

amd:AMDJS

define(['underscore'], function (_) {
    //todo
            });

cmd:Common Module Definition / draftCMD 模块定义规范

var _ = require('underscore');
module.exports = _;

值得一提的是使用nodeType来确保exports和module并不是HTML的元素。

保存原型

let ArrayProto = Array.prototype,
    ObjProto = Object.prototype,
    SymbolProto = ((typeof(Symbol)) !== ("undefined")) ? (Symbol.prototype) : (null);
    // 缓存变量,减少代码量,便于压缩代码
let push = ArrayProto.push,
    slice = ArrayProto.slice,
    // slice方法,可以从数组中返回选定的元素
    toString = ObjProto.toString,
    hasOwnProperty = ObjProto.hasOwnProperty;
    // 缓存变量,减少代码量,便于压缩代码
    // 同时可减少在原型链中的查找次数(提高代码效率)
let nativeIsArray = Array.isArray,
    nativeKeys = Object.keys,
    // 返回一个给定对象的所有可枚举属性的字符串数组
    nativeCreate = Object.create;
// 可以调用create方法来创建对象,对象的原型就是create方法的第一个参数
//ES5原生方法,如果浏览器支持,则underscore中会优先使用

值得一提的是:
var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;
2009年的 ES5 规定了六种语言类型:Null Undefined Number Boolean String Object,详见ES5/类型ES5/类型转换与测试。新出台的 ES6 则规定,包括六种原始类型:Null Undefined Number Boolean String 和 Symbol,还有一种 Object,详见JavaScript 数据类型和数据结构。新增加的 Symbol 很早就已经提出,其具体概念这里不再复述请移步参考 Symbol ,得益于 ES6 的渐渐普及,客户端浏览器也有很多已经支持 Symbol,比如 Firefox v36+ 和 Chrome v38+ 等,具体参考 ES6 支持情况,如果大家对 ES6 想要深入了解可以看 ES6 In Depth 这篇文章和 ES6草案

对象创建的特殊处理

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

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

回调处理

在看这个之前,先解释一下void 0是什么含义? 引文来源
在 JavaScript 中,假设我们想判断一个是否是 undefined,那么我们通常会这样写:

if(a === undefined){}

但是,JavaScript 中的 undefined 并不可靠,我们试着写这样一个函数:

function test(a) {
  var undefined = 1;
  console.log(undefined); // => 1
  if(a===undefined) {
    // ...
  }
}

现在我们能够明确的,标识符 undefined 并不能真正反映 “未定义”,所以我们得通过其他手段获得这一语义。幸好 JavaScript 还提供了 void 运算符,该运算符会对指定的表达式求值,并返回受信的 undefined:

void expression

最常见的用法是通过以下运算来获得 undefined,表达式为 0 时的运算开销最小:

void 0;
// or
void(0);

在 underscore 中,所有需要获得 undefined 地方,都通过 void 0 进行了替代。
当然,曲线救国的方式不只一种,我们看到包裹 jquery 的立即执行函数:

(function(window,undefined) {
    // ...
})(window)

在这个函数中,我们没有向其传递第二参数(形参名叫 undefined),那么第二个参数的值就会被传递上 “未定义”,因此,通过这种方式,在该函数的作用域中所有的 undefined 都为受信的 undefined。

    // underscore内部方法
    // 根据this指向(context参数)以及argCount参数二次操作返回一些回调、迭代方法
    let optimizeCb = function (func, context, argCount) {
        if (context === void 0) {
            return func;
        }
        // void 0返回undefined,即未传入上下文信息时直接返回相应的函数

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

其实不用上面的switch-case语句
直接执行下面的return语句,一样的效果
不这样做的原因call比apply快很多
apply在运行前要对作为参数的数组进行一系列的检验和深拷贝,.call则没有这些步骤
https://segmentfault.com/q/1010000007894513
http://www.ecma-international.org/ecma-262/5.1/#sec-15.3.4.3
http://www.ecma-international.org/ecma-262/5.1/#sec-15.3.4.4

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

    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) && !_.isArray(value)) return _.matcher(value);
        // 如果传入对象,寻找匹配的属性值
        return _.property(value);
        // 如果都不是,返回相应的属性访问器
    };
    // 默认的迭代器,是以无穷argCount为参数调用cb函数。用户可以自行修改。
    _.iteratee = builtinIteratee = function(value, context) {
        return cb(value, context, Infinity);
    };

剩余的处理

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

// 额外参数。
// 等价于ES6的rest参数。它将起始索引后的参数放入一个数组中。
       let restArgs = function(func, startIndex) {
        startIndex = startIndex == null ? func.length - 1 : +startIndex;
        // startIndex为null时,为函数声明时的参数数量减1,即除了第一个参数后的其他参数,否则为传入的startIndex
        // 返回一个支持rest参数的函数
        return function() {
            // 校正参数, 以免出现负值情况
            let length = Math.max(arguments.length - startIndex, 0),
                // 剩余参数是一个length长度的数组
                rest = Array(length),
                index = 0;
            // 假设参数从2个开始: func(a,b,*rest)
            // 调用: func(1,2,3,4,5); 实际的调用是:func.call(this, 1,2, [3,4,5]);
            for (; index < length; index++) {
                rest[index] = arguments[index + startIndex];
            }
            // 根据rest参数不同, 分情况调用函数, 需要注意的是, rest参数总是最后一个参数, 否则会有歧义
            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);
                // 将第一个参数、第二个参数以及后续参数的数组作为三个参数进行调用
            }
            // 如果不是上面三种情况, 而是更通用的apply
            let args = Array(startIndex + 1);
            // 先拿到前面参数
            for (index = 0; index < startIndex; index++) {
                args[index] = arguments[index];
            }
            // 拼接上剩余参数
            args[startIndex] = rest;
            return func.apply(this, args);
        };
    };

// 处理类数组对象。
// 类数组对象的长度应当是一个非负整数
    let MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
    // 表示正无穷+∞
    let getLength = shallowProperty('length');
    // 获取length属性访问器
    let isArrayLike = function(collection) {
        let length = getLength(collection);
        return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
    };

相关文章

网友评论

      本文标题:JS进阶——underscore源码(1)

      本文链接:https://www.haomeiwen.com/subject/iulacxtx.html