underscore.js源码分析

一、介绍

提到underscore.js大家可能不太熟悉,但是大家肯定对于lodash很熟悉。在我们日常工作中主要用lodash操作一些js操作起来比较麻烦的方法。underscore和lodash类似,他们都是对于js表现力的增强,就是说原生js在处理数据方面是比较薄弱的,像lodash和underscore对于他们有一些增强,比如数组方法的增强,函数方法的增强,对象方法的增强以及字符串方法的增强。所以像underscore,像lodash看源码无非就是一堆工具函数,会有上百个工具函数。那我们有学习的必要吗,或者说如果我们学习他的源码是不是把工具函数的源码扒下来阅读,如果我们学习源码只是学习这些东西的话,咱们大家把代码考下来,自己摸索下,问题都不大。我们今天要说的是underscore里面最精髓的一部分。我们要讲的underscore和lodash比较像,为什么要讲underscore呢,因为underscore里面有一个比较好的架构的设计,值得我们学习。所以我们的主要目标不会放在工具函数一个一个分析,更多的是关注整体架构的设计,这是我们讲underscore的原因。lodash虽然和underscore雷同,但是lodash不具备这样的架构设计。

二、underscore的编程理念

首先我们讲下从零开发一个像underscore这样的工具库要怎么做

我们知道像underscore和lodash是针对原生js的一些能力增强。原生js在处理数据的过程中提供了很薄的一层方法,处理功能是有限的。如果我想要怎么增强办呢。举个例子,我们要做一个数组去重的功能,我们翻遍脑海,发现没有找到一个专门来处理的api。最常规的方式,我们需要通过一个function去定义一个像unique这样的函数

function unique(arr) {
  // 逻辑
}

unique([1,2,3,4,5,6,5,6,7])

我们把数组作为参数,传给一个方法,方法里面处理业务逻辑,最后返回去重的结果。

但是在underscore里面,他帮我们提供了另外一种思路。他帮我们提供了上百个类似于像unique这样的工具函数,他真正的好处是借鉴了像java里面的流式编程

流式编程是什么概念呢,举个例子,假设有这样一个数组,先去重,再过滤,再取最大值

unique([1,2,3,4,5,6,5,6,7]).filter().max()

把这个函数当作一个管道,先去重,再过滤,再取最大值,数据在管道里流动。先把数据给到unique去重,去重后在把数据流通到filter,过滤后在给他max

这个东西有点像链式调用。接触过jQuery的话肯定知道这样的写法

$('.div').css().show()

jq里面把这种方式叫做链式调用,在underscore里面把这种方式叫做流式编程。在链式调用中更多的关注的是对象,就是说当我们调用了css方法返回的是一个实例,调用其他方法返回的又是一个实例,都是通过实例去调用方法;流式编程更加关注的是数据的流通,我们的数据在上一道工序加工后怎么流通到下一道工序,然后在流通到下一道工序,更多关注的是数据。

三、undersocre的写法

在underscore里面提供了两种写法

_([1,2,3,4,5,6,5,6,7]).unique().filter().max()

_.unique([1,2,3,4,5,6,5,6,7]).filter().max()

我们先来看看这两种方式是怎么实现的,步骤模型是怎么搭建的,然后讲讲管道函数,这些数据是怎么流通的,最终在串起来,告诉大家什么是流式编程

我们简单分析下,要支持这两种方式的调用,同时支持静态方法和实例方法

_([1,2,3,4,5,6,5,6,7])  // 当作是一个函数,把数据传递给他,来开启流式编程

_.unique([1,2,3,4,5,6,5,6,7])  // 当作一个对象,通过underscore对象上的unique方法,把数据传递给他来开启流式编程 

四、无new化构建

(function(root) {
    var _ = function() {
    }
  // 函数也是对象
    root._ = _;
})(this)

这个方法写在一个闭包函数里,为了避免变量全局污染。我们定义一个变量并挂载到全局。这个变量如果定义成一个对象,那么 _() 的调用就不能生效。因此需要是一个函数,在js里面,函数也是对象

 我们在调用 _() 的时候,需要去创建实例。那我们扩展一个像数组去重这样一个方法的话,要找到 _ 的原型对象,在原型对象上扩展一个unique方法,给他的实例来使用

(function(root) {
    var _ = function() {
    }
    
    _.prototype.unique = {
    
    }
  // 函数也是对象
    root._ = _;
})(this)

需要做到的是当我们调用 _() 的时候,也就要创建一个实例 new _()

当调用 _() 函数的时候,是在全局环境下调用,this指向window,window不是 _ 实例,所以代码如下

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

  // 函数也是对象
  root._ = _;
})()

 在全局引入并调用下 _()

// underscore.html

console.log(_().unique)
// 可以发现在控制台有实例对象,原型上有unique方法

我们在通过对象的方式调用,所以就要在 _ 对象本身扩展unique方法

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

    _.unique = function() {
    console.log(2)
  }   _.prototype.unique
= function() {
    console.log(1)
  }   
// 函数也是对象   root._ = _; })()

五、mixin

同时执行下这两种方法

_([1,2,3,4,5,6,5,6,7]).unique()

_.unique([1,2,3,4,5,6,5,6,7])

做数组去重,如果要通过上面两种方式调用,unique方法写两遍,这样冗余程度太高了,况且underscore有上百个类似于这样的工具函数。所以要有个办法来解决这个问题

underscore中有个方法mixin,通过他来解决这个事情

mixin主要做了这么几件事

1:可枚举的属性  2:存储在数组中  3:遍历数组,给原型对象扩展

(function(root) {

    // ...
    
    _.functions = function(target) {
        var ref = [];
        for (var name in target) {
            ref.push(name);
        }
        return ref;
    }

    _.each = function(arr, callback) {
         for (var i=0; i<arr.length; i++) {
            callback(arr[i])
        }
    }

   _.mixin = function(target) {
    _.each(_.functions(target), function(key) {
        target.prototype[key] = function() {
            console.log(1)
        }
    })
  }

   _.mixin(_);

    // 函数也是对象
    root._ = _;

})(this)        

此时通过函数和对象两种方式调用,会发现执行结果返回1 和 2。实际上应该不管我们通过函数还是对象,都应该执行对象属性上的方法,所以mixin 修改如下

_.mixin = function(target) {
    _.each(_.functions(target), function(key) {
        var func = target[key];
        target.prototype[key] = function() {
            func();
        }
    })
  }

 目前没有真实接口进行测试,修改上面的unique方法

_.unique = function(source) {
  var ref = [];
  for (var i=0; i<source.length; i++) {
    var target = source[i];
    if (ref.indexOf(target) === -1) {
      ref.push(target);
    }
  }
  return ref;
}

执行 _.unique([1,2,3,4,5,6,5,6,7]),输出结果 [1,2,3,4,5,6,7]

此时参数修改下,并不区分大小写 _.unique([1,2,3,4,5,6,5,6,7,'a','A'])

增加回调函数,来处理

_.unique([1,2,3,4,5,6,5,6,7,'a','A'], function(item) {
  return typeof item === 'string' ? item.toLowerCase() : item;
})

同时修改下unique方法

_.unique = function(source, callback) {
  var ref = [];
  for (var i=0; i<source.length; i++) {
    var target = callback ? callback(source[i]) : source[i];
    if (ref.indexOf(target) === -1) {
      ref.push(target);
    }
  }
  return ref;
}

去重最终结果是[1,2,3,4,5,6,7,'a']

下面通过 另外一种方式调用

_.unique([1,2,3,4,5,6,5,6,7,'a','A'], function(item) {
  return typeof item === 'string' ? item.toLowerCase() : item;
})

控制台会报错,主要问题是通过实例调用方法的时候,没有传参。下面要把数据推送给实例调用的方法,修改 _ 方法

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

报错没有了,但是数组没有把大小写 a 去重,所以mixin 方法中的参数加上arguments

_.mixin = function(target) {
    _.each(_.functions(target), function(key) {
        var func = target[key];
        target.prototype[key] = function() {
            return func(this._wrapper, arguments[0]);
        }
    })
  }

可以实现大小写去重,但是明显这么写代码不够健壮,我们可以通过数组合并来解决

_.mixin = function(target) {
    _.each(_.functions(target), function(key) {
        var func = target[key];
        target.prototype[key] = function() {
            var decon = [this._wrapper];
            Array.prototype.push.apply(decon, arguments);
            return func.apply(_, decon);
        }
    })
  }

 

posted @ 2023-07-17 00:32  两只小老虎  阅读(86)  评论(0编辑  收藏  举报