Underscore 源码学习 (三) - 剩余参数

这次主要说剩余参数。
在 ES5 中,如果想要函数接收任意数量的参数,必须使用特殊变量 arguments,举个例子,我们要实现一个加法函数,要求第一个数乘2,然后与其他数相加。

function add() {
    var sum = arguments[0] * 2;
    for(var i = 1; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum;
}

在 ES6 中,我们可以使用 ... 操作符,例如:

function add(first, ...args) {
    var sum = first*2;
    for(let arg of args) {
        sum += arg;
    }
    return sum;
}

使用 ES5 我们无法给函数定义参数,而只能通过 arguments 来获取参数,这样写明显带来了可读性的降低。而 ES6 我们就可以在函数声明里面写明参数,对于不定长的参数,则可以使用 ... 操作符。
... 还有另一个常用的应用场景,比如下面例子:

function test() {
    console.log(arguments);
    console.log(arguments instanceof Array);
    console.log(arguments instanceof Object);
}
test(1, 2, 3);
// Result:
[1, 2, 3]
false
true

如果细看输出的[1, 2, 3]会发现他是这样的:
result1.png
我们再试试下面的

var arr = [1, 2, 3];
console.log(arr);
console.log(arr instanceof Array);
console.log(arr instanceof Object);
// Result:
[1, 2, 3]
true
true

再看下[1, 2, 3]这行输出里面是什么:
result2.png
instanceof 我们就知道了 arguments 并不是真正的数组。伪数组实质是一个对象。
要把一个伪数组转为数组,可以这样用

var arr = Array.prototype.slice.call(arguments);

上面这种做法在很多地方都可以看到。除了上面这样做之外,我们还可以使用 ES6 的 Array.from 来处理,如下:

var arr = Array.from(arguments);

但在 ES6 中,我们使用 ... 运算符并不存在这个问题,比如上面第二个例子,args 是一个数组。
鉴于此,我们应该尽量使用 ES6 剩余参数写法和 Array.from 的写法,因为这样更容易理解,而且写起来更简洁。
另外,我们还可以使用 ... 操作符来复制数组,如下:

var itemsCopy = [...items];

额,说多了,其实我是想说说 Underscore 中的 restArgs 这个东西...
看下:

var restArgs = function(func, startIndex) {
    startIndex = startIndex == null ? func.length -1 : +startIndex;
    return function() {
        var length = Math.max(arguments.length - startIndex, 0);
        var rest = Array(length);
        for(var index = 0; index < length; index++) {
            rest[index] = arguments[index + startIndex];
        }
        switch (startIndex) {
            case 0: return func.call(this, rest);
            case 1: return func.call(this, arguments[0], rest);
            case 2: return func.call(this, arguments[0], arguments[1], rest);
        }
        var args = Array(startIndex + 1);
        for (index = 0; index < startIndex; index++) {
            args[index] = arguments[index];
        }
        args[startIndex] = rest;
        return func.apply(this, args);
    };
};

这个的作用就类似与 ES6 中的 ... 操作符。这段代码作用是把 funcstartIndex 开始的(如果没有指定则为被函数声明参数的最后一位开始)后面的参数全部变为一个数组传入 func 中。
这里有几个可圈可点的地方:

  • fun.length 和 arguments.length
    函数也具有 length 方法,得到的值是函数定义的参数的个数,但注意如果中间有一个含默认值的参数,则这个数和后面的参数都不会计算进去。例如:
function test1(arg1, arg2, arg3 = 1, arg4) {};
function test2(arg1, arg2, arg3) {};
test1.length;   // 2
test2.length    // 3

arguments.length 则一直表示传入函数的参数个数。

  • 使用 + 转换为数字
    你可能注意到了下面这句话有个 + 运算符。
startIndex = startIndex == null ? func.length - 1 : +startIndex;

其用途就是尝试把 startIndex 转为数字,我们举例看下就明白了。

var a = '123', b = '123s', c = '0x321', d = '-0', e = '-Infinity'
+a;           // 123
+b;           // NaN
+c;           // 801
+d;           // -0
+e;           // -Infinity

应该很清楚了,就不说明了。
然后关于这里的 switch 其实就是一个优化而已,前面都提到过了,不提了。

我们看下 Underscore 运用到 restArgs 方法的地方:

_.invoke = restArgs(function(obj, method, args) {
var isFunc = _.isFunction(method);
    return _.map(obj, function(value) {
        var func = isFunc ? method : value[method];
        return func == null ? func : func.apply(value, args);
    });
});

这个方法的作用是在 obj 的每个元素上面执行 method 方法,例如:

_.invoke([[5, 1, 7], [3, 2, 1]], 'sort');
_.invoke([['a', 'b', 'c'], ['w', 'g', 's']], 'join', '#');
// Result:
1,5,7,1,2,3
a#b#c,w#g#s

由于 method 需要的参数个数是未知的,所以我们这里使用了 args 再用 restArgs 达到类似 ... 操作符的效果。
本来还想说说 Underscore 的几个方法的...但是好像已经写了挺多的了,还是下一次再介绍吧,后面的很多方法其实都不难理解,不过最好结合他的实际应用例子这样就更容易去理解些。