关于 arguments 那些事儿 🤔

324 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

定义

所有函数(箭头函数外)中都有 arguments,就像 this 一样,可以直接在函数中打印查看:

// 例 1
function fn() {
  const age = 18
  console.log(arguments)
}
fn('Jay', 'Chou')

打印结果如下图:
image.png
只不过 this 是在函数调用执行时,在执行上下文栈(ECStack)中创建的函数执行上下文(FEC)里绑定的,而 arguments 是在 FEC 关联的 VO(Variable Object),也就是 AO(Activation Objec) 里的属性,以例 1 代码为例,内存示意图如下:
image.png
可以通过它找到传给函数的所有的参数,即使不定义形参也可以通过 arguments 使用实参。所以在 MDN 中,对 arguments 的定义是一个对应于传递给函数的参数的类数组(array-like)对象。所谓类数组,就是它有一些类似于数组的操作,下面举 3 个比较常见的例子:

  1. 可以通过 arguments.length 获取参数的个数;
  2. 可以根据索引值获取指定的参数;
  3. 可以通过 arguments.callee 获取当前 arguments 所在的函数。
// 例 2
function fn() {
  console.log(arguments.length)
  console.log(arguments[1])
  console.log(arguments.callee)
}
fn('Jay', 'Chou')

打印结果如下图:

image.png

但是 arguments 并不是数组,所以没有数组的一些诸如 forEach 等方法。如果想使用数组的方法,可以通过下面介绍的几种方法,将 arguments 转成数组。

转成数组

  1. 因为 arguments 有 length 属性,并且可以通过索引值获取,所以我们可以直接用一个 for 循环,遍历 arguments 里的每一项,并加入到一个空数组中:
// 例 3
function fn() {
  const arr = []
  for (let index = 0; index < arguments.length; index++) {
    arr.push(arguments[index])
  }
}
fn('Jay', 'Chou')
  1. 可以借用数组的 slice 方法:
// 例 4
function fn() {
  const arr = Array.prototype.slice.call(arguments)
  console.log(arr) // ['Jay', 'Chou']
}
fn('Jay', 'Chou')
  • Array.prototype.slice 是为了在没有数组实例的情况下能够调用到 slice 方法,所以直接 [].slice.call(arguments) 用一个空数组替换 Array.prototype 实现的效果是一样的;
  • .call(arguments) 的目的在于将 slice 方法内部的 this 指向 arguments。slice 的实现可以简单的认为是下面这样的:
// 例 5
Array.prototype.mySlice = function (begin = 0, end = this.length) {
  const arr = []
  for (let index = begin; index < end; index++) {
    arr.push(this[index])
  }
  return arr
}

.call(arguments) 就是让 mySlice 方法内的 this 变为 arguments,相当于是上面的第 1 个方法了(例 3)。

  1. 直接使用 Array 的静态方法 from:
function fn() {
  const arr = Array.from(arguments)
  console.log(arr) // ['Jay', 'Chou']
}
fn('Jay', 'Chou')

Array.from()方法可以将一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。所谓类似数组,就是拥有 length 属性和若干索引属性的对象,arguments 满足条件,所以它是一个类数组;所谓可迭代对象,就是在该对象或该对象的原型对象上有一个 Symbol.iterator 属性,用于实现 @@iterator 方法,图 1 中我们可以看到,打印 arguments 是能看到 Symbol.iterator 属性的,所以它又是一个可迭代对象。

  1. 可以用展开语法(Spread syntax)

因为 arguments 是可迭代对象,所以可以使用展开语法,直接构造一个字面量数组:

function fn() {
  const arr = [...arguments]
  console.log(arr) // ['Jay', 'Chou']
}
fn('Jay', 'Chou')

箭头函数

在箭头函数中,是没有 arguments 属性的。如果在箭头函数中打印 arguments,则与寻找一般的变量类似,会去箭头函数的父级作用域中寻找,直至全局作用域。注意,父级作用域是在函数定义时就决定了,而不是调用时。

  • 在浏览器环境下

箭头函数的上层作用域,也就是全局作用域中没有 arguments 属性,打印会报错,如下:

const fn = () => {
  console.log(arguments) // Uncaught ReferenceError: arguments is not defined
}
fn('Jay', 'Chou')
  • 在 node 环境下

有打印出内容 ,因为全局作用域中存在 arguments:
image.png
在箭头函数中,如果想要获取没有对应形参的实参,可以使用剩余参数(Rest parameters),它的写法和展开语法一样,但可以看成是相反的作用(展开语法也可以用在函数中,只不过是在调用时):

const fn = (firstName, lastName, ...reset) => {
  console.log(reset) // [18]
}
fn('Jay', 'Chou', 18)

剩余参数得到的是一个真正的数组。

感谢.gif 点赞.png