Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Array Functions 相关源码拾遗 & 小结 #12

Open
lessfish opened this issue Jun 12, 2016 · 8 comments
Open

Array Functions 相关源码拾遗 & 小结 #12

lessfish opened this issue Jun 12, 2016 · 8 comments

Comments

@lessfish
Copy link
Owner

lessfish commented Jun 12, 2016

Why underscore

最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。

阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。

之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。

欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力

Main

很快,Array Functions 部分到了尾声,今天来做个了(xiao)结。

underscore 给数组(以及 arguments,这里特别说明下,underscore 的数组扩展方法,同样适用于 arguments)增加了 20 个扩展方法,值得一提的是,很多有意思的方法,比如 map,shuffle 等,都被放在了 Collection Functions 中。本文来看看 Array Functions 中还有哪些有意思的方法(之前没有被提及)。

_.compact

这个方法很有意思,它的作用是剔除数组中的假值,返回数组副本。

实现非常的简单:

_.compact = function(array) {
  return _.filter(array, _.identity);
};

_.filter 我们在以后会讲到,这里你可以把它理解为 Array.prototype.filter 的一个 polyfill,来看看 _.identity 是个什么东东。

_.identity = function(value) {
  return value;
};

乍一看,_.identity 似乎没什么卵用,传入一个参数,原封不动返回这个参数,什么鬼?而再看 _.compact 的实现,就会发现非常巧妙!细细品味下,直接过滤了数组的假值,而 _.identity 在源码中能在多个地方复用。

从这个方法可以想到 PHP 的 array_filter 函数。array_filter 的基本用法和 Array.prototype.filter 相似,都是为了过滤数组中的元素。

function isOdd($num) {
  return $num & 1;
}

$a = Array(1, 2, 3);

$a = array_filter($a, 'isOdd');

var_dump($a);

// array
//   0 => int 1
//   2 => int 3

但是,值得注意的是:

If no callback is supplied, all entries of array equal to FALSE (see converting to boolean) will be removed.

这就有点 6 了,直接把 _.filter 和 _.compact 两个方法合二为一了。

$a = Array(0, 1, 2, 3, null, false, 4);

$a = array_filter($a);

var_dump($a);

// array
//   1 => int 1
//   2 => int 2
//   3 => int 3
//   6 => int 4

Array.prototype.filter 为何不设计成这样呢?没有 callback 传入的时候,直接过滤假值...

_.difference & _.without

先来看 _.without,它的作用是从数组中剔除指定的元素。

var a = [1, 2, 3, 4, 5];
var ans = _.without(a, 1, 2, 3);
console.log(ans); // [4, 5]

恩,没错,剔除数组 a 中的 value 为 1, 2, 3 的元素,这个过程中用 === 来进行比较。该方法传入的第一个参数是数组,后面的参数为单个元素。

而 _.difference 呢?和 _.without 的唯一区别是,第二个参数开始传入的是数组。(分别和数组中的元素比较)

var a = [1, 2, 3, 4, 5];
var ans = _.difference(a, [1, 2, 3], [5, 6]);
console.log(ans); // [4]

从 a 数组中剔除 1,2,3,5,6。

仔细一想,如果已经实现了 _.difference,我们把 _.without 的参数放入数组,然后传入 _.difference 就 ok 了!倒过来就不行了(思考下为什么)。 倒过来也是可以实现的,不过会相对比较繁琐些,详见 comments 部分的讨论,感谢 WangBoxue 同学指出~

来看 _.difference 的实现,非常简单:

// _.difference(array, *others)
_.difference = function(array) {
  // 将 others 数组展开一层
  // rest[] 保存展开后的元素组成的数组
  // strict 参数为 true
  // 不可以这样用 _.difference([1, 2, 3, 4, 5], [5, 2], 10);
  // 10 就会取不到
  var rest = flatten(arguments, true, true, 1);

  // 遍历 array,过滤
  return _.filter(array, function(value){
    // 如果 value 存在在 rest 中,则过滤掉
    return !_.contains(rest, value);
  });
};

不熟悉 flatten 的可以看看 前文,当 shallow 和 strict 均为 true 时,展开一层,并且过滤非数组元素,即可以起到将多个数组合并的作用。之后利用 ._filter 进行过滤即可。

而 _.without 方法则建立在 _.difference 基础上。

_.without = function(array) {
  // slice.call(arguments, 1)
  // 将 arguments 转为数组(同时去掉第一个元素)
  // 之后便可以调用 _.difference 方法
  return _.difference(array, slice.call(arguments, 1));
};

总结

数组的扩展方法就解读到这里了,相关源码可以参考 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L450-L693 这部分。接下去要解读的是 Collection Functions 部分,所谓 Collection,正是 Object & Array,也就是说这部分方法既可以用于 Object 也能用于 Array,比如我们熟悉的 map,filter,shuffle 等等,都在这部分内。

放个预告,下一篇会暂缓下 Collection Functions,讲下 array-like 相关的东西,敬请期待。

PS:坚持一件事真的挺难,一个月来,每天坚持看点源码,几乎把所有业余时间花在了上面,写了 10 篇随笔,每篇文章写的时间不短,关键还需要构思,如何提炼出一个主题,如何写让人看了会有所收获,恩,继续坚持。请关注我的 Repo https://github.com/hanzichi/underscore-analysis 支持我~

@WangBoxue
Copy link

仔细一想,如果已经实现了 _.difference,我们把 _.without 的参数放入数组,然后传入 _.difference 就 ok 了!倒过来就不行了(思考下为什么)。

感觉倒过来也可以

var args = _.union(arg1, arg2, ...);
args.unshift(array);
_.without.apply(null, args);

我哪里错了吗?

@lessfish
Copy link
Owner Author

_.without.apply(null, args);

这步的结果还是 args 吧?不知道我有没有理解错 @WangBoxue

@WangBoxue
Copy link

WangBoxue commented Jun 15, 2016

@hanzichi
https://jsfiddle.net/WangBoxue/1z03b4h4/

apply() 方法在指定 this 值和参数(参数以数组或类数组对象的形式存在)的情况下调用某个函数。
所以执行_.without.apply(null, args)相当于_.without(array, elements in arg1, elements in arg2, ...)

@lessfish
Copy link
Owner Author

@WangBoxue 的确如此,感谢指出!

@aleen42
Copy link

aleen42 commented Feb 25, 2017

@WangBoxue @hanzichi

反过来实现可能需要通知 _.without() 是否是 _.difference() 来调用它。如果是,则展开参数中的所有数组,不然则不展开。当然,前提是 _.difference() 要保证调用 _.without() 时没有传递非数组元素。

_.without = function(array) {
    var rest = (this == 'difference') ? _.flatten(arguments, false, false, 1) : Array.prototype.slice.call(arugments, 1);
    
    return _.filter(array, function(value) {
        return !_.contains(rest, value);
    });
};

_.difference = function(array) {
    /** filter out arguments which is not an array firstly */
    var args = _.flatten(arguments, true, true, 1).unshift(array);

    /**
     * let the method `_.without()` know that whether it's called by `_.difference()`,
     * if so, then flatten all the arrays from argments,
     * if not, then do not flatten.
     */
    return _.without.apply('difference', args);
};

@shaunzeng
Copy link

hey I have another question if you dont mind?

So in _.sample function, what is the point of having a Math.max(0,n) passed in _.shuffle function, instead of passing just 'n' ? also why does it check for object before using shuffle? because shuffle function already checks for objects? it that redundant ? please let me know what you think?

@shaunzeng
Copy link

also, why does _.map function use cb(iteratee, context), instead of optimizeCb(iteratee, context) ? you cannot pass anything else but a function in _.map right? so it should be optimizeCb I think, i tried to pass other stuff but it kind of broke the _.map code. Please let me kow what you think? thanks a ton!

@WangNianyi2001
Copy link

@shaunzeng Math.max 是过滤负数= =

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants