学习underscore源码之处理剩余参数函数restArguments

2,063 阅读4分钟

前言

学习的underscore.js 源码版本为1.13.1

rest parameter (剩余参数)

ES6 剩余参数语法允许我们将一个不定数量的参数表示为一个数组.

function fn(a, b, ...args) {
  console.log(args); // [3, 4, 5]
}

fn(1, 2, 3, 4, 5);


function sum(...theArgs) {
  return theArgs.reduce((previous, current) => {
    return previous + current;
  });
}

console.log(sum(1, 2, 3));  // 6

console.log(sum(1, 2, 3, 4));  // 10

剩余参数和 arguments 对象的区别

  • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参。
  • arguments对象不是一个真正的数组(除了length属性和索引元素之外没有任何Array属性),而剩余参数是真正的 Array 实例,也就是说你能够在它上面直接使用所有的数组方法,比如 sortmapforEachpop
  • arguments对象还有一些附加的属性 (如callee属性)

类数组如何转换成数组?

类数组: 是指拥有一个length属性和若干属性的对象。
underscore中isArrayLike函数的实现:

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;

var isArrayLike = function(obj) {
  var sizeProperty = obj == null ? void 0 : obj.length
  return typeof sizeProperty == 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX;
}

即认为有 length 属性且length 属性值是不大于 Number.MAX_SAFE_INTEGER 的自然数的对象为类数组

常见的类数组对象包括:

  1. function 中的 arguments 对象
  2. 使用document.getElementsByTagName/ClassName()等方法获得的HTMLCollection;
  3. 使用querySelector获得的NodeList
  4. ...

如何转换?

1. ES6展开运算符(只能作用于 iterable 对象
一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。

原生具备 Iterator 接口的数据结构如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

所以对于没有 Iterator 接口的数据使用展开运算符是会报错的。
关于Iterator可以学习阮一峰 es6.ruanyifeng.com/#docs/itera…

function sum(a, b){
  var args = [...arguments];
  args.push(3);
  console.log(args.reduce((prev, next) => prev + next ,0));
}
sum(1, 2);  // 6

[...undefined];   // Uncaught TypeError: undefined is not iterable
[...{length: 3}]; // Uncaught TypeError: {(intermediate value)} is not iterable

2. 利用Array.from()

function sum(a, b){
  var args = Array.from(arguments);
  args.push(3);
  console.log(args.reduce((prev, next) => prev + next ,0));
}
sum(1, 2);

3. 在 ES5 中可以借用 Array API 通过 call/apply 改变 this 或者 arguments 来完成转化
如下面的类数组:

var arrayLike = {
  0: 3,
  1: 4,
  2: 5,
  length: 3
}
Array.apply(null, arrayLike);  // 借用 arguments
Array.prototype.concat.apply([], arrayLike)  // 借用 arguments
Array.prototype.slice.call(arrayLike)  // 借用 this
Array.prototype.map.call(arrayLike, x => x)  // 借用 this

考虑稀疏数组 (sparse array)
使用 Array(n) 将会创建一个稀疏数组,为了节省空间,稀疏数组内含非真实元素,在控制台上将以 empty 显示,如下所示
先来看下使用:

console.log([,,,]);  // [empty × 3]
console.log(Array(3));  // [empty × 3]

当类数组为 { length: 3 } 时,一切将类数组作为 this 的方法将都返回稀疏数组,而将类数组作为 arguments 的方法将都返回密集数组
即:

var arrayLike1 = {
  length: 3
}
Array.apply(null, arrayLike1);  // [undefined, undefined, undefined]
Array.prototype.concat.apply([], arrayLike1)  // [undefined, undefined, undefined]
Array.prototype.slice.call(arrayLike1)  // [empty × 3]
Array.prototype.map.call(arrayLike, x => x)  // [empty × 3]

restArguments

如果不使用 ... 拓展操作符,仅用 ES5 的内容,该怎么实现呢?

我们可以写一个 restArguments 函数,传入一个函数,使用函数的最后一个参数储存剩下的函数参数,使用效果如下:

var fn = restArguments(function (a, b, args) {
  console.log(args);  // [3,4,5]
});
fn(1, 2, 3, 4, 5);

在实现第一版代码前,先来看看Function.length

Function.length --- 函数的形参个数

length 是函数对象的一个属性值,指该函数有多少个必须要传入的参数,即形参的个数。

形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数

与之对比的是, arguments.length 是函数被调用时实际传参的个数

// Function 构造器的length 属性值为 1
console.log(Function.length);  // 1

// 且该属性 Writable: false, Enumerable: false, Configurable: true
console.log(Object.getOwnPropertyDescriptor(Function, 'length')); 
// {value: 1, writable: false, enumerable: false, configurable: true}

// Function.prototype对象的 length 属性值为 0
console.log(Function.prototype.length);  // 0

console.log((function(){}).length);   // 0
console.log((function(a, b){}).length);  // 2

// 形参的数量不包括剩余参数个数
console.log((function(...args) {}).length);  // 0

// 仅包括第一个具有默认值之前的参数个数
console.log((function(a, b = 1, c) {}).length);  // 1

第一版实现:

function restArguments(func) {
  return function() {
    // startIndex 表示使用哪个位置的参数用于储存剩余的参数
    // 默认使用传人的函数的最后一个参数存储剩余的参数
    var startIndex = func.length - 1;
    var length = arguments.length - startIndex,
        rest = Array(length),
        index = 0;
    // 使用一个数组储存剩余的参数
    // 以上面例子为例,rest结果为[3, 4, 5]
    for (; index < length; index++) {
      rest[index] = arguments[index + startIndex];
    }
    var args = Array(startIndex + 1);
    // args此时为[1, 2, undefined]
    for (index = 0; index < startIndex; index++) {
      args[index] = arguments[index];
    }
    // args此时为[1, 2, [3, 4, 5]]
    args[startIndex] = rest;
    return func.apply(this, args);
  };
}

优化最终版

优化点:

  • 增加参数startIndex,表明从第几个参数存储剩余的参数
  • 考虑剩余参数的个数
  • 性能考虑,参数少的时候尽量使用call
function restArguments(func, startIndex) {
  startIndex = startIndex == null ? func.length - 1 : +startIndex;
  return function() {
    var length = Math.max(arguments.length - startIndex, 0),
        rest = Array(length),
        index = 0;
    for (; 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);
  };
}

测试:

var fn = restArguments(function(a, b, c, d) {console.log(a, b, c, d);}, 5);
fn(1, 2, 3, 4);  // 1 2 3 4

var fn1 = restArguments(function(a, args) {console.log(a, args);});
fn1();  // undefined []

var fn2 = restArguments(function(a, b, c, d) {console.log(a, b, c, d);}, 2);
fn2(1, 2, 3, 4);  // 1 2 [3, 4] undefined

参考资料: github.com/mqyqingfeng…
developer.mozilla.org/zh-CN/docs/…
juejin.cn/post/684490…