一次性搞懂javascript的arguments(实参对象)

444 阅读4分钟

在我们使用javascript时候,由于javascript不会对传入形参(函数声明时候定义的参数)的个数进行校验,对于不定实参的获取就需要用到我们的Arguments实参对象对于参数进行处理了

一、Arguments定义

arguments 是一个对应于传递给函数的参数的类数组对象(关于类数组对象参考掘金文章),arguments实参对象是在我们调用的函数的时候自动创建的,下面我们就通过实际案例来去看一下arguments

function f() {
 console.log(arguments)
 if(arguments.length === 3) {
   arguments.forEach(i => console.log(i))
 }
 if (arguments.length === 2) {
   console.log(arguments[0] + arguments[1])
 }
}
f(1,2) // 输出 3
f(1,2,3) // 报错,没有forEach方法,

从上面例子我们可以得出下面结论:

  • arguments有length属性,可以拿到参数个数
  • arguments可以通过类数组下标的方式获取实参值
  • arguments不能通过使用数组的一些方法进行遍历(forEach、map...)

因此可以看出他的结构更多的像是如下所示:

{
  0: 1,
  1: 1,
  length: 2,
  callee: xxxx,
  ....
}

二、arguments存在的场景

首先我们简单的执行一个例子,如下:

function a() {
  console.log(arguments, 'a');
}

const b = () => {
  console.log(arguments, 'b');
};

function c() {
  console.log(arguments, 'c');
  arguments[3] = '123'
  console.log(arguments, arguments.length)
}

a(1,2,3); // Arguments(3) .....
c(1,2,3); // arguments(3) ...
          // Arguments(3) [1, 2, 3, 3: '123', ...]  3
b(1,2,3); // arguments is not defined

从上面我们能总结出来一下两点:

  • 普通函数是有arguments实参对象的;
  • 箭头函数式没有arguments实参对象;
  • arguments实参对象我们无法通过增加值的方式去改变length长度

那么我们在看一看下面一个例子

function a() {
  console.log(arguments, 'a');
  function b() {
    console.log(arguments, 'b');
  }

  const c = () => {
    console.log(arguments, 'c');
  };

  b(1,2,3);

  c(1,2,3);
}

a('a', 'b', 'c');
// 结果
// Arguments(3) [ 'a', 'b', 'c' , ...]
// Arguments(3) [ 1, 2, 3, ...]
// Arguments(3) [ 'a', 'b', 'c' , ...]

这个其实我们可以理解为,箭头函数本身没有arguments 实参对象,就是去上级作用域寻找arguments,所有内部函数c里面的arguments其实和外部函数a是一致的;

如果你已经理解了上面,那么我们试着去解读一下防抖函数(debounce),具体代码如下

function debounce(fn, delay = 500) {
      let timer = null;
      console.log(arguments, '外层')
      
      return function () {
        if (timer) {
          clearTimeout(timer);
        }
        console.log(arguments, '内层')
        timer = setTimeout(() => {
          console.log(arguments, '函数执行时')
          // 执行事件的回调函数
          fn.apply(this, arguments);
          // 执行后清空定时器
          timer = null
        }, delay)
      }
    }

    // 调用
    const a = (a, b) => {
      console.log(a, b, '调用')
    }

    const debounceA = debounce(a, 1000)

    debounceA(1, 2)
// 输出结果
// [(a, b) => console.log(a,b, '调用'), 1000, ....] '外层'
// [1, 2, ...] '内层'
// [1, 2, ...] '函数执行时'

首先外层的arguments实参对象的值,我们很容易理解,无法就是在我通过const debounceA = debounce(a, 1000)来去生成防抖函数时候传入的 a, 1000,
那么对于内层函数,和定时器内部执行的的实参,我们需要理解到debounceA其实是一个不定形参的函数,当我们通过debounceA(1, 2)执行的时候,就等于给return出来没有具体形参的的function传入1,2作为参数,当然箭头函数没有arguments实参对象,需要去父级作用域去寻找,因此对于内层函数和定时器的arguments实参对象,两个是一致的;

三、对于arguments实参对象数据转换

arguments实参对象是类数组对象,如果我们需要对arguments进行处理,一般处理方法有三种,如下:

  • 第一种 [].slice.call(arguments)
[].slice.call(arguments) => 返回参数数组
// 核心机制如下:
let args = []; 
let obj = {0:'hello',1:'world',length:2};
for (let i = 0; i < obj.length; i++) { 
    args.push(obj[i]);
}
console.log(args); // ['hello', 'world']

简单说一下机制(前提是大家很懂call()和slice()这两个api的用法)
1、首先call(this, ...),我们传入arguments以后,就相当于 arguments.slice()这个意思(当然这个是通过call的方式临时赋能给arguments的能力,因此,你不能上来就很憨憨的写arguments.slice());
2、然后slice(start, end)如果两个参数都没有给,就会返回一个一模一样的新数组,不会影响原来的数组,这样的话我们就可以通过上面的的核心机制代码去试着理解其将arguments转化为数组的过程了;

  • 第二种:通过es6数组提供的新方法from这个方法进行转化,当然这个方法还有其他的妙用,后续在说明
Array.from(arguments)
  • 第三种: 通过扩展运算符 [...arguments]

四、arguments实参对象callee属性说明

参考下面代码,我们通过代码进行解读

let f = function(x) {
  if (x <= 1) {
    return 1
  }
  return x * arguments.callee(x - 1)
}

callee其实就是指代当前正在执行的函数,上面代码arguments.callee 等价于 f,一般用于匿名函数递归调用;
当然我们也需要注意,如果我们是在严格模式下('use strict';),我们就无法通过arguments.callee去做一些操作了,如下图所示;
image.png