Arguments 对象
arguments 基本定义
首先arguments
是以内置对象出现的。换句话说:你不能够直接的去访问arguments
对象,所以你会返现在浏览器中直接访问arguments
对象是不存在的。
特别重要:
那么arguments
对象本质上是什么东西呢?其实arguments
是一个对应于传递给函数参数的类数组对象,arguments
对象是所有(非箭头)函数中都可用的局部变量**。为什么箭头函数中不存在arguments
对象呢?这个问题,我们等会再去讨论。
我们通过console.dir(Function.prototype)
的方式发现arguments
属性存在于Function.prototype
对象上。为什么我们不能够直接通过Function.arguments
的方式进行访问arguments
呢?因为我们说过,arguments
对象是内置对象,所以你不能够直接去调用访问。
那么arguments
对象本质上是什么东西呢?其实arguments
是一个对应于传递给函数参数的类数组对象,arguments
对象是所有(非箭头)函数中都可用的局部变量。**为什么箭头函数中不存在arguments
对象呢?这个问题,我们等会再去讨论。
我们通过console.dir(Function.prototype)
的方式发现arguments
属性存在于Function.prototype
对象上。为什么我们不能够直接通过Function.arguments
的方式进行访问arguments
呢?因为我们说过,arguments
对象是内置对象,所以你不能够直接去调用访问。
如果说,我想要访问arguments
的话,我该如何操作呢?因为arguments
是所有(非箭头)函数中的局部变量,所以在函数内部中可以通过arguments
直接对其访问。例如下面的例子:
我们终于揭开了arguments
对象的面纱,现在来分析一下arguments
对象内部的属性:
function fn() {console.log(arguments);
};
fn(1, 2, 3);
- 先看
arguments
对象的形式,arguments
对象很显然是类数组对象Array-like
。为什么说arguments
对象是类数组对象呢?首先arguments
对象存在length
属性,其次arguments
对象存在有顺序依据的属性0,1,2
,最后arguments
对象本质上并不是数组对象。因为arguments
的[[prototype]]
属性是由Object
构造器构造出来的对象。 callee
属性:callee
属性其实也比较简单,这个callee
属性会指向宿主函数。什么是宿主函数呢?也就是指代当前正在执行的函数。比如说下面例子中,通过arguments.callee
能够访问到当前function fn
,其实我们也可以直接通过fn
函数名称进行访问。为什么ES5
严格模式要移除callee
属性,这个问题我们也等会解释。
function fn() {console.log(arguments.callee); // function fn(){}console.log(fn); // function fn(){}
}
fn();
Symbol.iterator
属性:出现Symbol.iterator
属性的话,说明该数据是可以迭代的。比如说:我通过generator
生成器函数手动的对arguments
对象进行迭代,我也可以用for...of
语句直接对其arguments
对象进行迭代。
function fn() {const iterator = generator(arguments);console.log(iterator.next()); // { value: 1, done: false }console.log(iterator.next()); // { value: 2, done: false }console.log(iterator.next()); // { value: 3, done: false }console.log(iterator.next()); // { value: undefined, done: true }
};
fn(1, 2, 3);
// 生成器函数
function * generator(args) {for (let i = 0; i < args.length; i++) {yield args[i];}
}
function fn() {for (let value of arguments) {console.log(value); // 1, 2, 3}
};
fn(1, 2, 3);
特别重要:
分析完arguments
对象的属性之后,我们来证明一下arguments
对象是不是数组形式的。我们分别通过Array.isArray
方法、toString
方法去证明arguments
不是数组的形式。所以arguments
对象是不可以直接继承到Array.prototype
上的方法。
// 方式一
function fn() {console.log(Array.isArray(arguments)); // false
}
fn(1, 2, 3);// 方式二
function fn() {console.log(arguments.toString()); // [object Arguments]
}
fn(1, 2, 3);
为什么箭头函数中不存在arguments
如果我们在箭头函数中访问arguments
对象,那么此时程序会抛出异常:Uncaught ReferenceError: arguments is not defined
。这说明什么问题呢?说明箭头函数中是不存在arguments
对象的。为什么箭头函数中不存在arguments
对象呢?如果说我想在箭头函数中获取实际参数列表,我又该如何去操作呢?
首先如果你想在箭头函数中获取实际参数列表的话,此时你可以通过ES6
的剩余语法来处理,例如:你会发现args
变量此时是数组的形式,并且数组内部的元素对应实际参数的值。这说明什么问题呢?这说明箭头函数中剩余语法代替了原本的arguments
对象。
const fn = (...args) => {console.log(args);
}
fn(1, 2, 3);
ES6
中为什么有一个arguments
到args
的演变过程呢?其实也很简单,我们先从arguments
中分析,首先arguments
对象保存的是实际参数列表,我们通过拿到实际参数列表之后一般都是要将arguments
对象转为数组,然后再去对数组进行操作。而现在args
本身就是数组的形式,所以能够直接调用Array.prototype
上的方法。
其次是arguments.callee
属性,为什么args
中不存在arguments.callee
属性呢?因为callee
属性指向当前正在执行的函数,而我们完全可以通过调用函数名称的方式去直接获取函数。所以此时callee
属性存在与不存在的意义并不是很大。当然之后,我们会说为什么arguments.callee
在ES5
严格模式下被移除的原因。
最后是arguments[Symbol.iterator]
属性,因为arguments
对象存在Symbol.iterator
属性,所以arguments
对象可以进行迭代。但是需要注意,args
数组虽然自身不存在Symbol.iterator
属性,但是Array.prototype
对象是存在Symbol.iterator
属性,所以args
自然也可以进行迭代操作。所以在箭头函数中,arguments
对象的代替方式是通过剩余语法的方式。
上面的原因只是针对于arguments
到args
演变的过程总结的原因。但是最重要的原因在于形式参数与实际参数对应的关系,这点我们在接下来重点叙述。
arguments 对象转为数组的问题
说到arguments
对象转为数组的问题,可能我们一下子能够想到很多种。但是针对于ES5
来说,我们通过使用的是下面的方式:
我们通过[].slice.call(arguments)
的方式,将arguments
对象转换为数组的形式。因为slice
方法能够返回一个新数组,而我们改变slice
方法内部的this
指向。使arguments
对象从类数组对象转换为数组对象。
这是ES5
中比较常见的arguments
对象转为数组方式,但是这种方式对于V8
引擎优化非常不友好。在这篇文档中(Optimization-killer),说明了针对于V8
引擎优化不友好的方式,其中下面的这种方式就囊括其中。
function fn() {const argArr = [].slice.call(arguments);console.log(argArr); // [1, 2, 3]
};
fn(1, 2, 3);
当然MDN
上也指出了解决的方式:
function fn() {var args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments));console.log(args); // [1, 2, 3]
};
fn(1, 2, 3);
形式参数与实际参数的对应关系、arguments对象的行为
ES5
我们在ES5
学习函数的时候,我们知道函数实际参数与形式参数一一对应,比如说:
调用fn(1, 2, 3)
函数,1,2,3
在fn
函数调用的时候作为实际参数传入到函数内部。而a,b,c
作为形式参数来与其对应。也就是说,现在形式参数a,b,c
相当于fn
函数内部的临时变量,而a,b,c
变量内部存储的值与实际参数一一对应,所以a,b,c
的值分别是1,2,3
。
function fn(a, b, c) {console.log(a, b, c); // 1, 2, 3
};
fn(1, 2, 3);
形式参数与实际参数的对应关系很好理解,我们下面来看看比较奇怪的arguments
对象行为:
arguments
对象表示的是实际参数列表,所以打印arguments
对象内部的元素是1,2,3
。
function fn(a, b, c) {console.log(a, b, c); // 1, 2, 3console.log(arguments); // Arguments[1, 2, 3]
};
fn(1, 2, 3);
如果说我们现在,更改形式参数变量a
的值,那么arguments
对象内部的元素会受影响吗?
实际上是会发生变化的,我们可以看到此时arguments
对象内部的元素变为了100,2,3
。这是什么原因导致的呢?不是说arguments
对象是实际参数列表吗?arguments
对象内部的元素不应该是1,2,3
吗?
特别重要:其实这是由于形式参数与arguments
在内存中存在映射关系,实际上会使形式参数与arguments
对象的元素产生对应关系(共享关系)。注意一下,映射关系是指在内存中,比如a
映射b``a <=> b
,此时a、b
的值可以不相等。而对应关系是a
对应b``a <-> b
,此时a、b
值是相等的。
其实也就是说,arguments
对象内部元素与形式参数存在对应关系,这也是arguments
对象的行为。所以当形式参数a
发生变化的时候,arguments
对象中对应的元素也会发生变化。
function fn(a, b, c) {a = 100;console.log(arguments); // Arguments[100, 2, 3]
};
fn(1, 2, 3);
当然,如果你改变arguments
对象内部的值,此时对应的形式参数也会发生变化。这就是函数内部形式参数与arguments
对象的特殊关系。
function fn(a, b, c) {arguments[0] = 100;console.log(a); // 100
};
fn(1, 2, 3);
ES6
如果arguments
对象遇见某些ES6
的语法时,此时arguments
会被弱化。换句话说,在ES6
中,不希望你使用arguments
对象,而是利用剩余语法进行代替。比如说:
下面的两个例子中存在ES6
中的函数参数默认值的语法,此时我们会发现形式参数与arguments
之间对应关系似乎消失了。当我们手动去修改形式参数a
的值,或者是手动修改arguments
对象的元素的时候,与其对应的形式参数或者是arguments
内部元素并不会与其对应。换句话说:当在默认参数的存在,导致arguments
对象中的元素并不会再与形式参数的值对应。
function fn(a = 1, b) {a = 100;console.log(arguments); // Arguments[1, 2, 3]
};
fn(1, 2, 3);function fn(a = 1, b) {arguments[0] = 100;console.log(a); // 1
}
fn(1, 2, 3);
通过下面的几个例子:我们发现在非严格模式下ES6
的剩余参数、默认参数、解构赋值语法存在时,arguments
对象中的值是不会追踪形式参数的值。也就是说arguments
对象内部的值是不会对应形式参数的值。注意是非严格模式,严格模式我们之后再进行讨论。
为什么ES6
会出现这种现象呢?其实这和arguments
对象有关系,之前我们在ES5
讨论arguments
对象的时候。我们发现arguments
对象是存在与形式参数对应的行为,但是这种行为很怪异。arguments
对象本质上就是保存实际参数的类数组对象,而更改形式参数的值或者是手动修改arguments
对象中的值,都会影响到与之对应的形式参数或者是arguments
对象。
而这种行为伴随着剩余参数、默认参数、解构赋值语法出现,ES6
在慢慢的弱化arguments
对象的能力,我们在下面剩余参数、默认参数、解构赋值的例子中能够很明显的感受到。尤其是剩余参数的出现,如果函数接收不定数量的参数时,我们会发现形式参数已经完全的列表化,形式参数通过...
的语法进行收集,形式参数在函数内部中显得及其重要,并且arguments
对象的行为也失去了效果。所以在ES6
中,它并不希望你使用arguments
对象,而是推荐你使用剩余参数的方式去代替arguments
。
// ES5
function fn(a, b, c) {arguments[0] = 100;arguments[1] = 200;arguments[2] = 300;console.log(a, arguments[0]); // 100 100console.log(b, arguments[1]); // 200 200console.log(c, arguments[2]); // 300 300
}
fn(1, 2, 3);// ES6参数默认值
function fn(a, b, c = 1) {arguments[0] = 100;arguments[1] = 200;arguments[2] = 300;console.log(a, arguments[0]); // 1 100console.log(b, arguments[1]); // 2 200console.log(c, arguments[2]); // 3 300
}
fn(1, 2, 3);// ES6剩余参数语法
function fn(...args) {arguments[0] = 100;arguments[1] = 200;arguments[2] = 300;console.log(args[0], arguments[0]); // 1 100console.log(args[1], arguments[1]); // 2 200console.log(args[2], arguments[2]); // 3 300
};
fn(1, 2, 3);// ES6对象化解构
function fn({ a, b, c }) {arguments[0] = 100;arguments[1] = 200;arguments[2] = 300;console.log(a, arguments[0]); // 1 100console.log(b, arguments[1]); // 2 200console.log(c, arguments[2]); // 3 300
};
fn({a:1,b:2,c:3
});
ES5 严格模式中的arguments
我们上面讨论的例子都是处于非严格模式下的情况,现在我们探讨一下如果在严格模式下的话,arguments
对象与形式参数之间的关系会不会受到影响。
观察下面的例子,我们发现处于严格模式下的话,arguments
对象内部的元素并不会与形式参数对应,实际上的效果与ES6
相同。其实这真的很好理解,arguments
本质上就是保存实际参数的,你更改形式参数的值,本来就不应该去影响arguments
对象内部的值。并且如果我手动修改arguments
对象内部的值也是不应该去修改形式参数的值才对。
所以我们发现在严格模式下,arguments
对象失去了原本上对应的行为能力。而在非严格模式下,arguments
对象依旧保持着原本的对应行为能力。
特别重要:严格模式下,不仅仅移除了arguments
对象的对应能力,还将arguments
对象中的callee、caller
属性一并移除,在严格模式下是不能够使用callee、caller
属性。
// 非严格模式
function fn(a, b, c) {a = 10;b = 20;c = 30;console.log(a, arguments[0]); // 10 10console.log(b, arguments[1]); // 20 20console.log(c, arguments[2]); // 30 30
};
fn(1, 2, 3);// 严格模式
function fn(a, b, c) {'use strict';a = 10;b = 20;c = 30;console.log(a, arguments[0]); // 10 1console.log(b, arguments[1]); // 20 2console.log(c, arguments[2]); // 30 3
};
fn(1, 2, 3);
那么为什么arguments.callee
从ES5
严格模式中删除了呢?
原因:在早期版本的javascript
不允许使用命名函数表达式,处于这样的原因,你不能创建一个递归函数表达式:
function factorial (n) {return !(n > 1) ? 1 : factorial(n - 1) * n;
}[1,2,3,4,5].map(factorial);
[1,2,3,4,5].map(function (n) {return !(n > 1) ? 1 : /* what goes here? */ (n - 1) * n;
});
针对于不可行的方式,为了解决这个问题,arguments.callee
添加进来以后。然后你就可以这样去做:
[1,2,3,4,5].map(function (n) {return !(n > 1) ? 1 : arguments.callee(n - 1) * n;
});
然而,这实际上是一个非常糟糕的解决方案,因为这(以及其它的arguments
、callee
、caller
)使得在通常的情况(你可以通过调试一些个别的例子去实现它,但即使最好的代码也是次优选择,因为JS
引擎做了不必要的解释)不可能实现内联和尾递归。另外一个原因是递归调用会获取到一个不同的this
值。
var global = this;var sillyFunction = function (recursed) {if (!recursed) { return arguments.callee(true); }if (this !== global) {alert("This is: " + this);} else {alert("This is the global");}
}sillyFunction();
ECMAScript3
通过允许命名函数表达式解决这些问题。例如:
[1,2,3,4,5].map(function factorial (n) {return !(n > 1) ? 1 : factorial(n-1)*n;
});