跟underscore一起学如何写函数库

栏目: JavaScript · 发布时间: 5年前

内容简介:原文:https://zhehuaxuan.github.io/2019/03/07/%E8%B7%9Funderscore%E4%B8%80%E8%B5%B7%E5%AD%A6%E5%A6%82%E4%BD%95%E5%86%99%E5%87%BD%E6%95%B0%E5%BA%93/ 作者:zhehuaxuan本文主要用于梳理和研究underscore内部是如何组织和处理函数的。

原文:https://zhehuaxuan.github.io/2019/03/07/%E8%B7%9Funderscore%E4%B8%80%E8%B5%B7%E5%AD%A6%E5%A6%82%E4%BD%95%E5%86%99%E5%87%BD%E6%95%B0%E5%BA%93/ 作者:zhehuaxuan

目的

Underscore 是一个 JavaScript 工具库,它提供了一整套函数式编程的实用功能,但是没有扩展任何 JavaScript 内置对象。

本文主要用于梳理和研究underscore内部是如何组织和处理函数的。

通过这篇文章,我们可以:

了解underscore在函数组织方面的巧妙构思;

为自己书写函数库提供一定思路;

我们开始!

自己写个函数库

前端的小伙伴一定不会对jQuery陌生,经常使用 $.xxxx 的形式进行进行调用,underscore也使用 _.xxxx ,如果自己在ES5语法中写过自定义模块的话,就可以撸出下面一段代码:

(function(){
    //定义
    var root = this;
    var _ = {};
    _.first = function(arr,n=0){
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }
    root._ = _;
})();
console.log(this);
复制代码

在Chrome浏览器中打开之后,打印出如下结果:

跟underscore一起学如何写函数库

我们看到在全局对象下有一个 _ 属性,属性下面挂载了自定义函数,我们不妨使用 _.first(xxxx) 在全局环境下直接调用。

console.log(_.first([1,2,3,4]));
console.log(_.first([1,2,3,4],1));
console.log(_.first([1,2,3,4],3));
复制代码

输出结果如下:

跟underscore一起学如何写函数库

没问题,我们的函数库制作完成了,我们一般直接这么用,也不会有太大问题。

underscore是怎么做的?

underscore正是基于上述代码的完善,那么underscore是如何接着往下做的呢?容我娓娓道来!

对兼容性的考虑

// Establish the root object, `window` (`self`) in the browser, `global`
// on the server, or `this` in some virtual machines. We use `self`
// instead of `window` for `WebWorker` support.
var root = typeof self == 'object' && self.self === self && self ||
              typeof global == 'object' && global.global === global && global ||
              this ||
              {};
复制代码

上面是underscore1.9.1IIFE函数中的源码,对应于我们上面自己写的 var root = this;

在源码中作者也解释了:创建root对象,并且给root赋值:

浏览器端:window也可以是window.self或者直接self

服务端(node):global

WebWorker:self

虚拟机:this

underscore充分考虑了兼容性。

支持两种不同风格的函数调用

在underscore中我们可以使用两种方式调用函数:

console.log(_.first([1,2,3,4]));
console.log(_([1,2,3,4])).first();

在underscore中,它们返回的结果都是相同的。

第一种方式没有问题,难点就是第二种方式的调用调用。

对象式调用的实现

解决这个问题要达到两个目的:

_
_

我们来看看underscore对于 _ 的实现:

var _ = function(obj) {
      if (obj instanceof _) return obj;
      if (!(this instanceof _)) return new _(obj);
      this._wrapped = obj;
};
复制代码
跟underscore一起学如何写函数库

不怕,我们不妨调用 _([1,2,3,4])) 看看他是怎么执行的!

第一步: if (obj instanceof _) return obj; 传入的对象及其原型链上有 _ 类型的对象,则返回自身。我们这里的 [1,2,3,4] 显然不是,跳过。

第二步: if (!(this instanceof _)) return new _(obj); ,如果当前的this对象及其原型链上没有 _ 类型的对象,那么执行 new 操作。调用 _([1,2,3,4])) 时, thiswindow ,那么 (this instanceof _)false ,所以我们执行 new _([1,2,3,4])

第三步:执行 new _([1,2,3,4]) ,继续调用 _ 函数,这时

obj[1,2,3,4]

this为一个新对象,并且这个对象的 __proto__ 指向 _.prototype (对于new对象执行有疑问,请猛戳此处)

此时

(obj instanceof _)为 false

(obj instanceof _)为 true

所以此处会执行 this._wrapped = obj; ,在新对象中,添加 _wrapped 属性,将`[1,2,3,4]挂载进去。

综合上述函数实现的效果就是:

_([1,2,3,4]))<=====>new _([1,2,3,4])

然后执行如下构造函数:

var _ = function(obj){
    this._wrapped = obj
}
复制代码

最后得到的对象为:

跟underscore一起学如何写函数库
跟underscore一起学如何写函数库

我们执行如下代码:

console.log(_([1,2,3,4]));
console.log(_.prototype);
console.log(_([1,2,3,4]).__proto__ == _.prototype);
复制代码

看一下打印的信息:

跟underscore一起学如何写函数库

这表明通过 _(obj) 构建出来的对象确实具有两个特征:

  1. 下面挂载了我们传入的对象/数组
  2. 对象的 _proto_ 属性指向 _prototype

到此我们已经完成了第一个问题。

跟underscore一起学如何写函数库

接着解决第二个问题:

这个对象依然能够调用挂载在 _ 对象上声明的方法

我们先来执行如下代码:

_([1,2,3,4]).first();
复制代码

此时JavaScript执行器会先去找 _([1,2,3,4]) 返回的对象上是否有 first 属性,如果没有就会顺着对象的原型链上去找 first 属性,直到找到并执行它。

我们发现 _([1,2,3,4]) 返回的对象属性和原型链上都没有 first

跟underscore一起学如何写函数库

那我们自己先在 _.prototype 上面加一个上去试一下:

(function(){
    //定义
    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;
        if (!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
      };

    _.first = function(arr,n=0){
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }
    _.prototype.first = function(arr,n=0){
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }
    root._ = _;
})();
复制代码

我们在执行打印一下:

console.log(_([1,2,3,4]));
复制代码

效果如下:

跟underscore一起学如何写函数库

原型链上找到了 first 函数,我们可以调用 first 函数了。如下:

console.log(_([1,2,3,4]).first());
复制代码

可惜报错了:

跟underscore一起学如何写函数库

于是调试一下:

跟underscore一起学如何写函数库

我们发现 arrundefined ,但是我们希望 arr[1,2,3,4]

跟underscore一起学如何写函数库

我们马上改一下 _.prototype.first 的实现

(function(){
    
    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;
        if (!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
      };

    _.first = function(arr,n=0){
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }
    _.prototype.first = function(arr,n=0){
        arr = this._wrapped;
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }
    root._ = _;
})();
复制代码

我们在执行一下代码:

console.log(_([1,2,3,4]).first());
复制代码

效果如下:

跟underscore一起学如何写函数库

我们的效果似乎已经达到了!

跟underscore一起学如何写函数库

现在我们执行下面的代码:

console.log(_([1,2,3,4]).first(2));
复制代码

调试一下:

跟underscore一起学如何写函数库

凉凉了。

跟underscore一起学如何写函数库

其实我们希望的是:

[1,2,3,4]2arguments 的形式传入first函数

我们再来改一下:

//_.prototype.first = function(arr,n=0){
        // arr = this._wrapped;
        // if(n==0) return arr[0];
        // return arr.slice(0,n);
    //}
    _.prototype.first=function(){
        /**
         * 搜集待传入的参数
         */
        var that = this._wrapped;
        var args = [that].concat(Array.from(arguments));
        console.log(args); 
    }
复制代码

我们再执行下面代码:

_([1,2,3,4]).first(2);
复制代码

看一下打印的效果:

跟underscore一起学如何写函数库

参数都已经拿到了。

我们调用函数一下 first 函数,我们继续改代码:

_.prototype.first=function(){
     /**
      * 搜集待传入的参数
     */
     var that = this._wrapped;
     var args = [that].concat(Array.from(arguments));
     /**
      * 调用在_属性上的first函数
      */
     return _.first(...args);
}
复制代码

这样一来_.prototype上面的函数的实际实现都省掉了,相当于做一层 代理 ,调用一下。

一举两得!

执行一下最初我们的代码:

console.log(_.first([1,2,3,4]));
console.log(_.first([1,2,3,4],1));
console.log(_.first([1,2,3,4],3));
复制代码
跟underscore一起学如何写函数库

现在好像我们所有的问题都解决了。

跟underscore一起学如何写函数库

但是似乎每声明一个函数都得在原型链上也声明一个相同名字的函数。形如下面:

_.a = function(args){
    //a的实现
}
_.prototype.a = function(){
    //调用_.a(args)
}
_.b = function(args){
    //b的实现
}
_.prototype.b = function(){
    //调用_.b(args)
}
_.c = function(args){
    //c的实现
}
_.prototype.c = function(){
    //调用_.c(args)
}
.
.
.
1000个函数之后...
复制代码

会不会觉得太恐怖了!

跟underscore一起学如何写函数库

我们能不能改成如下这样呢?

_.a = function(args){
    //a的实现
}
_.b = function(args){
    //b的实现
}
_.c = function(args){
    //c的实现
}
1000个函数之后...
_.mixin = function(){
    //将_属性中声明的函数都挂载在_prototype上面
}
_.mixin(_);
复制代码

上面这么做好处大大的:

我们可以专注于函数库的实现,不用机械式的复写prototype上的函数。

underscore也正是这么做的!

我们看看 mixin 函数在underscore中的源码实现:

// Add your own custom functions to the Underscore object.
_.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 _;
};
  
// Add all of the Underscore functions to the wrapper object.
_.mixin(_);
复制代码

有了上面的铺垫,这个代码一点都不难看懂,首先调用 _.each 函数,形式如下:

_.each(arrs, function(item) {
     //遍历arrs数组中的每一个元素
 }
复制代码

我们一想就明白,我们在 _ 对象属性上实现了自己定义的函数,那么现在要把它们挂载到 _prototype 属性上面,当然先要遍历它们了。

所以我们可以猜到 _.functions(obj) 肯定返回的是一个数组,而且这个数组肯定是存储 _ 对象属性上面关于我们实现的各个函数的信息。

我们看一下 _.function(obj) 的实现:

_.functions = _.methods = function(obj) {
  var names = [];
  /**
   **  遍历对象中的属性
   **/
  for (var key in obj) {
      //如果属性值是函数,那么存入names数组中
     if (_.isFunction(obj[key])) names.push(key);
  }
  return names.sort();
};
复制代码

确实是这样的!

跟underscore一起学如何写函数库

我们把上述实现的代码整合起来:

(function(){
    /**
     * 保证兼容性
     */
    var root = typeof self == 'object' && self.self === self && self ||
    typeof global == 'object' && global.global === global && global ||
    this ||
    {};

    /**
     * 在调用_(obj)时,让其执行new _(obj),并将obj挂载在_wrapped属性之下
     */
    var _ = function(obj) {
        if (obj instanceof _) return obj;
        if (!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
      };
    
    //自己实现的first函数
    _.first = function(arr,n=0){
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }

    //判断是否是函数
    _.isFunction = function(obj) {
        return typeof obj == 'function' || false;
    };

    //遍历生成数组存储_对象的函数值属性
    _.functions = _.methods = function(obj) {
        var names = [];
        for (var key in obj) {
          if (_.isFunction(obj[key])) names.push(key);
        }
        return names.sort();
      };
  
    //自己实现的遍历数组的函数
    _.each = function(arrs,callback){
        for(let i=0;i<arrs.length;i++){
            callback(arrs[i]);
        }
    }
    
    var ArrayProto = Array.prototype;
    var push = ArrayProto.push;
 
    //underscore实现的mixin函数
    _.mixin = function(obj) {
        console.log(_.functions(obj)); //我们打印一下_.functions(_)到底存储了什么?
        _.each(_.functions(obj), function(name) {
          var func = _[name] = obj[name];
           _.prototype[name] = function() {
               var args = [this._wrapped];
               push.apply(args, arguments);
               return func.apply(_, args);
           };
        });
         return _;
    };

    //执行minxin函数
    _.mixin(_);
    root._ = _;
})();
复制代码

我们看一下 _.functions(obj) 返回的打印信息:

跟underscore一起学如何写函数库

确实是 _ 中自定义函数的属性值。

我们再来分析一下each中callback遍历各个属性的实现逻辑。

var func = _[name] = obj[name];
 _.prototype[name] = function() {
     var args = [this._wrapped];
      push.apply(args, arguments);
     return func.apply(_, args);
};
复制代码

第一句: func 变量存储每个自定义函数

第二句: _.prototype[name]=function(); 在_.prototype上面也声明相同属性的函数

第三句: args 变量存储 _wrapped 下面挂载的值

第四句:跟 var args = [that].concat(Array.from(arguments)); 作用相似,将两边的参数结合起来

第五句:执行 func 变量指向的函数,执行 apply 函数,将上下文对象 _ 和待传入的参数`args``传入即可。

我们再执行以下代码:

console.log(_.first([1,2,3,4]));
console.log(_.first([1,2,3,4],1));
console.log(_.first([1,2,3,4],3));
复制代码

结果如下:

跟underscore一起学如何写函数库

Perfect!

这个函数在我们的浏览器中使用已经没有问题。

但是在Node中呢?所以下面引出新的问题。

再回归兼容性问题

我们知道在Node中,我们是这样的:

//a.js
let a = 1;
module.exports = a;
//index.js
let b = require('./a.js');
console.log(b) //打印1
复制代码

那么:

let _ = require('./underscore.js')
_([1,2,3,4]).first(2);
复制代码

我们也希望上述的代码能够在Node中执行。

所以 root._ = _ 是不够的。

underscore是怎么做的呢?

如下:

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

我们看到当全局属性exports不存在或者不是DOM节点时,说明它在浏览器中,所以:

root._ = _;

如果exports存在,那么就是在Node环境下,我们再来进行判断:

如果module存在,并且不是DOM节点,并且module.exports也存在的话,那么执行:

exports = module.exports = _;

在统一执行:

exports._ = _;

附录

下面是最后整合的阉割版underscore代码:

(function(){
    /**
     * 保证兼容性
     */
    var root = typeof self == 'object' && self.self === self && self ||
    typeof global == 'object' && global.global === global && global ||
    this ||
    {};

    /**
     * 在调用_(obj)时,让其执行new _(obj),并将obj挂载在_wrapped属性之下
     */
    var _ = function(obj) {
        if (obj instanceof _) return obj;
        if (!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
      };
    
    //自己实现的first函数
    _.first = function(arr,n=0){
        if(n==0) return arr[0];
        return arr.slice(0,n);
    }

    //判断是否是函数
    _.isFunction = function(obj) {
        return typeof obj == 'function' || false;
    };

    //遍历生成数组存储_对象的函数值属性
    _.functions = _.methods = function(obj) {
        var names = [];
        for (var key in obj) {
          if (_.isFunction(obj[key])) names.push(key);
        }
        return names.sort();
      };
  
    //自己实现的遍历数组的函数
    _.each = function(arrs,callback){
        for(let i=0;i<arrs.length;i++){
            callback(arrs[i]);
        }
    }

    var ArrayProto = Array.prototype;
    var push = ArrayProto.push;
 
    //underscore实现的mixin函数
    _.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 func.apply(_, args);
           };
        });
        return _;
    };


    //执行minxin函数
    _.mixin(_);
    if (typeof exports != 'undefined' && !exports.nodeType) {
        if (typeof module != 'undefined' && !module.nodeType && module.exports) {
            exports = module.exports = _;
        }
        exports._ = _;
    } else {
        root._ = _;
    }
})();
复制代码

欢迎各位大佬拍砖!同时您的点赞是我写作的动力~谢谢。


以上所述就是小编给大家介绍的《跟underscore一起学如何写函数库》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Processing编程学习指南(原书第2版)

Processing编程学习指南(原书第2版)

[美]丹尼尔希夫曼(Daniel Shiffman) / 李存 / 机械工业出版社 / 2017-3-1 / 99.00元

在视觉化界面中学习电脑编程的基本原理! 本书介绍了编程的基本原理,涵盖了创建最前沿的图形应用程序(例如互动艺术、实时视频处理和数据可视化)所需要的基础知识。作为一本实验风格的手册,本书精心挑选了部分高级技术进行详尽解释,可以让图形和网页设计师、艺术家及平面设计师快速熟悉Processing编程环境。 从算法设计到数据可视化,从计算机视觉到3D图形,在有趣的互动视觉媒体和创意编程的背景之......一起来看看 《Processing编程学习指南(原书第2版)》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

MD5 加密
MD5 加密

MD5 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具