Arguments对象

Arguments 对象

arguments 基本定义

首先arguments是以内置对象出现的。换句话说:你不能够直接的去访问arguments对象,所以你会返现在浏览器中直接访问arguments对象是不存在的。
image.png
特别重要:
那么arguments对象本质上是什么东西呢?其实arguments是一个对应于传递给函数参数的类数组对象,arguments对象是所有(非箭头)函数中都可用的局部变量**。为什么箭头函数中不存在arguments对象呢?这个问题,我们等会再去讨论。
我们通过console.dir(Function.prototype)的方式发现arguments属性存在于Function.prototype对象上。为什么我们不能够直接通过Function.arguments的方式进行访问arguments呢?因为我们说过,arguments对象是内置对象,所以你不能够直接去调用访问。
Arguments对象_第1张图片
那么arguments对象本质上是什么东西呢?其实arguments是一个对应于传递给函数参数的类数组对象,arguments对象是所有(非箭头)函数中都可用的局部变量
。**为什么箭头函数中不存在arguments对象呢?这个问题,我们等会再去讨论。
我们通过console.dir(Function.prototype)的方式发现arguments属性存在于Function.prototype对象上。为什么我们不能够直接通过Function.arguments的方式进行访问arguments呢?因为我们说过,arguments对象是内置对象,所以你不能够直接去调用访问。
Arguments对象_第2张图片
如果说,我想要访问arguments的话,我该如何操作呢?因为arguments是所有(非箭头)函数中的局部变量,所以在函数内部中可以通过arguments直接对其访问。例如下面的例子:
我们终于揭开了arguments对象的面纱,现在来分析一下arguments对象内部的属性:

function fn() {
	console.log(arguments);
};
fn(1, 2, 3);

Arguments对象_第3张图片

  1. 先看arguments对象的形式,arguments对象很显然是类数组对象Array-like。为什么说arguments对象是类数组对象呢?首先arguments对象存在length属性,其次arguments对象存在有顺序依据的属性0,1,2,最后arguments对象本质上并不是数组对象。因为arguments[[prototype]]属性是由Object构造器构造出来的对象。
  2. callee属性:callee属性其实也比较简单,这个callee属性会指向宿主函数。什么是宿主函数呢?也就是指代当前正在执行的函数。比如说下面例子中,通过arguments.callee能够访问到当前function fn,其实我们也可以直接通过fn函数名称进行访问。为什么ES5严格模式要移除callee属性,这个问题我们也等会解释。
function fn() {
	console.log(arguments.callee); // function fn(){}
	console.log(fn); // function fn(){}
}
fn();
  1. 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对象。
Arguments对象_第4张图片

const fn = (...args) => {
	console.log(args);
}
fn(1, 2, 3);

ES6中为什么有一个argumentsargs的演变过程呢?其实也很简单,我们先从arguments中分析,首先arguments对象保存的是实际参数列表,我们通过拿到实际参数列表之后一般都是要将arguments对象转为数组,然后再去对数组进行操作。而现在args本身就是数组的形式,所以能够直接调用Array.prototype上的方法。
其次是arguments.callee属性,为什么args中不存在arguments.callee属性呢?因为callee属性指向当前正在执行的函数,而我们完全可以通过调用函数名称的方式去直接获取函数。所以此时callee属性存在与不存在的意义并不是很大。当然之后,我们会说为什么arguments.calleeES5严格模式下被移除的原因。
最后是arguments[Symbol.iterator]属性,因为arguments对象存在Symbol.iterator属性,所以arguments对象可以进行迭代。但是需要注意,args数组虽然自身不存在Symbol.iterator属性,但是Array.prototype对象是存在Symbol.iterator属性,所以args自然也可以进行迭代操作。所以在箭头函数中,arguments对象的代替方式是通过剩余语法的方式。
上面的原因只是针对于argumentsargs演变的过程总结的原因。但是最重要的原因在于形式参数与实际参数对应的关系,这点我们在接下来重点叙述。

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,3fn函数调用的时候作为实际参数传入到函数内部。而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, 3
	console.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 100
	console.log(b, arguments[1]); // 200 200
	console.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 100
	console.log(b, arguments[1]); // 2 200
	console.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 100
	console.log(args[1], arguments[1]); // 2 200
	console.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 100
	console.log(b, arguments[1]); // 2 200
	console.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 10
	console.log(b, arguments[1]); // 20 20
	console.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 1
	console.log(b, arguments[1]); // 20 2
	console.log(c, arguments[2]); // 30 3
};
fn(1, 2, 3);

那么为什么arguments.calleeES5严格模式中删除了呢?
原因:在早期版本的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;
});

然而,这实际上是一个非常糟糕的解决方案,因为这(以及其它的argumentscalleecaller)使得在通常的情况(你可以通过调试一些个别的例子去实现它,但即使最好的代码也是次优选择,因为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;
});

你可能感兴趣的:(前端集合,javascript,原型模式,前端,es6)