美文网首页让前端飞
学习underscore整体架构,打造属于自己的函数式编程类库

学习underscore整体架构,打造属于自己的函数式编程类库

作者: 若川i | 来源:发表于2019-08-22 09:22 被阅读4次

    前言

    上一篇文章写了jQuery整体架构学习 jQuery 源码整体架构,打造属于自己的 js 类库

    虽然看过挺多underscore.js分析类的文章,但总感觉少点什么。这也许就是纸上得来终觉浅,绝知此事要躬行吧。于是决定自己写一篇学习underscore.js整体架构的文章。

    本文章学习的版本是v1.9.1
    unpkg.com源码地址:https://unpkg.com/underscore@1.9.1/underscore.js

    虽然很多人都没用过underscore.js,但看下官方文档都应该知道如何使用。

    从一个官方文档_.chain简单例子看起:

    _.chain([1, 2, 3]).reverse().value();
    // => [3, 2, 1]
    

    看例子中可以看出,这是支持链式调用。

    读者也可以顺着文章思路,自行打开下载源码进行调试,这样印象更加深刻。

    链式调用

    _.chain 函数源码:

    _.chain = function(obj) {
        var instance = _(obj);
        instance._chain = true;
        return instance;
    };
    

    这个函数比较简单,就是传递obj调用_()。但返回值变量竟然是instance实例对象。添加属性_chain赋值为true,并返回intance对象。但再看例子,实例对象竟然可以调用reverse方法,再调用value方法。猜测支持OOP(面向对象)调用。

    带着问题,笔者看了下定义 _ 函数对象的代码。

    _ 函数对象 支持OOP

    var _ = function(obj) {
        if (obj instanceof _) return obj;
        if (!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
    };
    

    如果参数obj已经是_的实例了,则返回obj
    如果this不是_的实例,则手动 new _(obj);
    再次new调用时,把obj对象赋值给_wrapped这个属性。
    也就是说最后得到的实例对象是这样的结构
    { _wrapped: '参数obj', }
    它的原型_(obj).__proto___.prototype;

    如果对这块不熟悉的读者,可以看下以下这张图(之前写面试官问:JS的继承画的图)。

    构造函数、原型对象和实例关系图

    继续分析官方的_.chain例子。这个例子拆开,写成三步。

    var part1 = _.chain([1, 2, 3]);
    var part2 = part1.reverse();
    var part3 = part2.value();
    
    // 没有后续part1.reverse()操作的情况下
    console.log(part1); // {__wrapped: [1, 2, 3], _chain: true}
    
    console.log(part2); // {__wrapped: [3, 2, 1], _chain: true}
    
    console.log(part3); // [3, 2, 1]
    

    思考问题:reverse本是Array.prototype上的方法呀。为啥支持链式调用呢。
    搜索reverse,可以看到如下这段代码:

    并将例子代入这段代码可得(怎么有种高中做数学题的既视感_):

    _.chain([1,2,3]).reverse().value()s
    
    var ArrayProto = Array.prototype;
    // 遍历 数组 Array.prototype 的这些方法,赋值到 _.prototype 上
    _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
        // 这里的`method`是 reverse 函数
        var method = ArrayProto[name];
        _.prototype[name] = function() {
        // 这里的obj 就是数组 [1, 2, 3]
        var obj = this._wrapped;
        // arguments  是参数集合,指定reverse 的this指向为obj,参数为arguments, 并执行这个函数函数。执行后 obj 则是 [3, 2, 1]
        method.apply(obj, arguments);
        if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
        // 重点在于这里 chainResult 函数。
        return chainResult(this, obj);
        };
    });
    
    // Helper function to continue chaining intermediate results.
    var chainResult = function(instance, obj) {
        // 如果实例中有_chain 为 true 这个属性,则返回实例 支持链式调用的实例对象  { _chain: true, this._wrapped: [3, 2, 1] },否则直接返回这个对象[3, 2, 1]。
        return instance._chain ? _(obj).chain() : obj;
    };
    

    if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
    提一下上面源码中的这一句,看到这句是百思不得其解。于是赶紧在github中搜索这句加上""双引号。表示全部搜索。

    搜索到两个在官方库中的ISSUE,大概意思就是兼容IE低版本的写法。有兴趣的可以点击去看看。

    I don't understand the meaning of this sentence.

    why delete obj[0]

    基于流的编程

    至此就算是分析完了链式调用_.chain()_ 函数对象。这种把数据存储在实例对象{_wrapped: '', _chain: true} 中,_chain判断是否支持链式调用,来传递给下一个函数处理。这种做法叫做 基于流的编程

    最后数据处理完,要返回这个数据怎么办呢。underscore提供了一个value的方法。

    _.prototype.value = function(){
        return this._wrapped;
    }
    

    顺便提供了几个别名。toJSONvalueOf
    _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;

    还提供了 toString的方法。

    _.prototype.toString = function() {
        return String(this._wrapped);
    };
    

    这里的String()new String() 效果是一样的。
    可以猜测内部实现和 _函数对象类似。

    var String = function(){
        if(!(this instanceOf String)) return new String(obj);
    }
    
    var chainResult = function(instance, obj) {
        return instance._chain ? _(obj).chain() : obj;
    };
    

    细心的读者会发现chainResult函数中的_(obj).chain(),是怎么实现实现链式调用的呢。

    _(obj)是返回的实例对象{_wrapped: obj}呀。怎么会有chain()方法,肯定有地方挂载了这个方法到_.prototype上或者其他操作,这就是_.mixin()

    _.mixin 挂载所有的静态方法到 _.prototype, 也可以挂载自定义的方法

    _.mixin 混入。但侵入性太强,经常容易出现覆盖之类的问题。记得之前Reactmixin功能,Vue也有mixin功能。但版本迭代更新后基本都是慢慢的都不推荐或者不支持mixin

    _.mixin = function(obj) {
        // 遍历对象上的所有方法
        _.each(_.functions(obj), function(name) {
            // 比如 chain, obj['chain'] 函数,自定义的,则赋值到_[name] 上,func 就是该函数。也就是说自定义的方法,不仅_函数对象上有,而且`_.prototype`上也有
        var func = _[name] = obj[name];
        _.prototype[name] = function() {
            // 处理的数据对象
            var args = [this._wrapped];
            // 处理的数据对象 和 arguments 结合
            push.apply(args, arguments);
            // 链式调用  chain.apply(_, args) 参数又被加上了 _chain属性,支持链式调用。
            // _.chain = function(obj) {
            //  var instance = _(obj);
            //  instance._chain = true;
            //  return instance;
            };
            return chainResult(this, func.apply(_, args));
        };
        });
        // 最终返回 _ 函数对象。
        return _;
    };
    
    _.mixin(_);
    

    _mixin(_) 把静态方法挂载到了_.prototype上,也就是_.prototype.chain方法 也就是 _.chain方法。

    所以_.chain(obj)_(obj).chain()效果一样,都能实现链式调用。

    关于上述的链式调用,笔者画了一张图,所谓一图胜千言。

    underscore.js 链式调用图解

    _.mixin 挂载自定义方法

    挂载自定义方法:
    举个例子:

    _.mixin({
        log: function(){
            console.log('哎呀,我被调用了');
        }
    })
    _.log() // 哎呀,我被调用了
    _().log() // 哎呀,我被调用了
    

    _.functions(obj)

    _.functions = _.methods = function(obj) {
        var names = [];
        for (var key in obj) {
        if (_.isFunction(obj[key])) names.push(key);
        }
        return names.sort();
    };
    

    _.functions_.methods 两个方法,遍历对象上的方法,放入一个数组,并且排序。返回排序后的数组。

    underscore.js 究竟在__.prototype挂载了多少方法和属性

    再来看下underscore.js究竟挂载在_函数对象上有多少静态方法和属性,和挂载_.prototype上有多少方法和属性。

    使用for in循环一试遍知。看如下代码:

    var staticMethods = [];
    var staticProperty = [];
    for(var name in _){
        if(typeof _[name] === 'function'){
            staticMethods.push(name);
        }
        else{
            staticProperty.push(name);
        }
    }
    console.log(staticProperty); // ["VERSION", "templateSettings"] 两个
    console.log(staticMethods); // ["after", "all", "allKeys", "any", "assign", ...] 138个
    
    var prototypeMethods = [];
    var prototypeProperty = [];
    for(var name in _.prototype){
        if(typeof _.prototype[name] === 'function'){
            prototypeMethods.push(name);
        }
        else{
            prototypeProperty.push(name);
        }
    }
    console.log(prototypeProperty); // []
    console.log(prototypeMethods); // ["after", "all", "allKeys", "any", "assign", ...] 152个
    

    根据这些,笔者又画了一张图underscore.js 原型关系图,毕竟一图胜千言。

    `underscore.js` 原型关系图

    整体架构概览

    匿名函数自执行

    (function(){
    
    }());
    

    这样保证不污染外界环境,同时隔离外界环境,不是外界影响内部环境。

    外界访问不到里面的变量和函数,里面可以访问到外界的变量,但里面定义了自己的变量,则不会访问外界的变量。
    匿名函数将代码包裹在里面,防止与其他代码冲突和污染全局环境。
    关于自执行函数不是很了解的读者可以参看这篇文章。
    [译] JavaScript:立即执行函数表达式(IIFE)

    root 处理

    var root = typeof self == 'object' && self.self === self && self ||
        typeof global == 'object' && global.global === global && global ||
        this ||
        {};
    

    支持浏览器环境nodeWeb Workernode vm微信小程序

    导出

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

    关于root处理导出的这两段代码的解释,推荐看这篇文章冴羽:underscore 系列之如何写自己的 underscore,讲得真的太好了。笔者在此就不赘述了。
    总之,underscore.js作者对这些处理也不是一蹴而就的,也是慢慢积累,和其他人提ISSUE之后不断改进的。

    支持 amd 模块化规范

    if (typeof define == 'function' && define.amd) {
        define('underscore', [], function() {
            return _;
        });
    }
    

    _.noConflict 防冲突函数

    源码:

    // 暂存在 root 上, 执行noConflict时再赋值回来
    var previousUnderscore = root._;
    _.noConflict = function() {
        root._ = previousUnderscore;
        return this;
    };
    

    使用:

    <script>
    var _ = '我就是我,不一样的烟火,其他可不要覆盖我呀';
    </script>
    <script src="https://unpkg.com/underscore@1.9.1/underscore.js">
    </script>
    <script>
    var underscore = _.noConflict();
    console.log(_); // '我就是我,不一样的烟火,其他可不要覆盖我呀'
    underscore.isArray([]) // true
    </script>
    

    总结

    全文根据官网提供的链式调用的例子, _.chain([1, 2, 3]).reverse().value();较为深入的调试和追踪代码,分析链式调用(_.chain()_(obj).chain())、OOP、基于流式编程、和_.mixin(_)_.prototype挂载方法,最后整体架构分析。学习underscore.js整体架构,利于打造属于自己的函数式编程类库。

    文章分析的源码整体结构。

    (function() {
        var root = typeof self == 'object' && self.self === self && self ||
            typeof global == 'object' && global.global === global && global ||
            this ||
            {};
        var previousUnderscore = root._;
    
        var _ = function(obj) {
          if (obj instanceof _) return obj;
          if (!(this instanceof _)) return new _(obj);
          this._wrapped = obj;
        };
    
        if (typeof exports != 'undefined' && !exports.nodeType) {
          if (typeof module != 'undefined' && !module.nodeType && module.exports) {
            exports = module.exports = _;
          }
          exports._ = _;
        } else {
          root._ = _;
        }
        _.VERSION = '1.9.1';
    
        _.chain = function(obj) {
          var instance = _(obj);
          instance._chain = true;
          return instance;
        };
    
        var chainResult = function(instance, obj) {
          return instance._chain ? _(obj).chain() : obj;
        };
    
        _.mixin = function(obj) {
          _.each(_.functions(obj), function(name) {
            var func = _[name] = obj[name];
            _.prototype[name] = function() {
              var args = [this._wrapped];
              push.apply(args, arguments);
              return chainResult(this, func.apply(_, args));
            };
          });
          return _;
        };
    
        _.mixin(_);
    
        _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
          var method = ArrayProto[name];
          _.prototype[name] = function() {
            var obj = this._wrapped;
            method.apply(obj, arguments);
            if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
            return chainResult(this, obj);
          };
        });
    
        _.each(['concat', 'join', 'slice'], function(name) {
          var method = ArrayProto[name];
          _.prototype[name] = function() {
            return chainResult(this, method.apply(this._wrapped, arguments));
          };
        });
    
        _.prototype.value = function() {
          return this._wrapped;
        };
    
        _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;
    
        _.prototype.toString = function() {
          return String(this._wrapped);
        };
    
        if (typeof define == 'function' && define.amd) {
          define('underscore', [], function() {
            return _;
          });
        }
    }());
    

    下一篇文章可能是学习lodash的源码整体架构。

    读者发现有不妥或可改善之处,欢迎评论指出。另外觉得写得不错,可以点赞、评论、转发,也是对笔者的一种支持。

    推荐阅读

    underscorejs.org 官网

    undersercore-analysis

    underscore 系列之如何写自己的 underscore

    笔者往期文章

    学习 jQuery 源码整体架构,打造属于自己的 js 类库

    面试官问:JS的继承

    面试官问:JS的this指向

    面试官问:能否模拟实现JS的call和apply方法

    面试官问:能否模拟实现JS的bind方法

    面试官问:能否模拟实现JS的new操作符

    前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并

    关于

    作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。

    个人博客

    掘金专栏,欢迎关注~

    segmentfault前端视野专栏,开通了前端视野专栏,欢迎关注~

    知乎前端视野专栏,开通了前端视野专栏,欢迎关注~

    github blog,相关源码和资源都放在这里,求个star_~

    微信公众号 若川视野

    可能比较有趣的微信公众号,长按扫码关注。也可以加微信 lxchuan12,注明来源,拉您进【前端视野交流群】。

    若川视野

    相关文章

      网友评论

        本文标题:学习underscore整体架构,打造属于自己的函数式编程类库

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