引题:为什么 JavaScript 中的 arguments 对象不是数组 http://www.zhihu.com/question/50803453

JavaScript 1.0

1995 年, Brendan Eich 在 Netscape Navigator 2.0 中实现了 JavaScript 1.0,arguments 对象在那时候就已经有了。当时的 arguments 对象很像我们现在的数组(现在也像),它有一些索引属性,对应每个实参,还有一个 length 属性,代表实参的数量,还有一个现在浏览器已经都没有了的 caller 属性(等价于当前函数的 caller 属性)。但是,和现在的 arguments 对象不同的是,当时的 arguments 不是一个特殊的本地变量,而是作为函数的属性(现在仍保留着)存在的:

function foo(a, b, c) {
alert(foo.arguments[0]) //
alert(foo.arguments[1]) //
alert(foo.arguments[2]) //
alert(foo.arguments.length) //
alert(foo.arguments.caller == foo.caller) // true,都等于 bar,现代浏览器里会是 false
} function bar() {
foo(1, 2, 3)
} bar()

虽然作为函数的属性,但和现在的 arguments 一样,它只能写在自己的函数体内,否则值就是 null:

function foo() {
alert(bar.arguments) // 当时是 null,现在不是了
} function bar() {
alert(bar.arguments) // {0: 1}
foo()
} bar(1) alert(bar.arguments) // null

同时,arguments 对象和各形参变量的“双向绑定”特性在那时候就已经存在了:

function foo(a) {
arguments[0] = 10
alert(a) //
a = 100
alert(arguments[0]) //
} foo(1)

然而,这个特性在当时算是个隐藏特性,Netscape 在自己的文档上从来没有提到过,而且即便到现在,还是有相当多的人不知道。为什么 20 年了还会有人不知道呢,我觉的是因为它基本没用,虽然很 cool。

大家都知道发明 JavaScript 1.0 只用了 10 天时间,当时时间非常有限,Brendan 为什么要额外实现这么一个特性?我看了下目前能找到的最旧的 js 源码,js1.4.1,发现无论是 arguments 对象的索引属性,还是形参变量,它们底层都是通过相同的 getter(js_GetArgument)和 setter(js_SetArgument)读取和设置着 fp->argv 这个 c 数组,所以它们才会有相互映射的能力。所以有没有可能并不是 Brendan 有意加的额外特性,而是他的第一反应就是应该这么去实现?

读到这里,很多同学觉的答案已经有了,arguments 对象不能是数组的原因就是:“数组没有 caller 属性” 和 “数组实现不了这种双向绑定”。前者并不是原因,因为给数组添加一个额外的非索引属性是很容易的事情,在 JS 层面也是一个简单的赋值语句即可实现(虽然一般不这么做),甚至现在引擎内部也会产出这样的数组 - 从 ES6 开始,引擎在调用模板字符串的标签函数时传入的第一个参数就是个拥有额外的 raw 属性的数组:

(function(arr) {
console.log(arr, arr.raw) // arr 和 arr.raw 都是数组
})
`\0`

我在两年前看到引擎会产生这样的数组也觉的很奇怪,还专门问了 ES 规范当时的编辑。

2016.10.5 追加,今天才想到,不仅 ES6 里有这样的数组,早在 ES3 里就已经有了:

arr = /./g.exec("123") // [ '1', index: 0, input: '123' ]
alert(Array.isArray(arr)) // true

正则的 exec 方法和字符串的 match 方法返回的就是个拥有额外的 index 及 input 属性的数组。

那后者算是个原因吗?一点点又或者完全不是,说一点点是因为在当时还没有 __defineSetter__/__defineGetter__/Object.defineProperty,如果把 arguments 设计成数组,同时引擎层面把它实现成和形参相互映射的,会让人觉的太 magic 了,因为当时还写不出下面这样代码:

let a = 1
let arguments = []
Object.defineProperty(arguments, 0, {
get() {
return a
},
set(v) {
a = v
}
})
alert(arguments[0]) //
arguments[0] = 10
alert(a) //

说完全不是呢,是因为我知道另外一个更明显的,在当时,arguments 不能是数组的原因,那就是,“当时还没有数组呢”。是的,不要一脸懵逼,你现在知道的 JavaScript 的特性,有很多在 JavaScript 1.0 里是不存在的,包括 typeof 运算符,undefined 属性,Object 字面量语法等等。这个消息是我在两年前查阅历史文档发现并经 Brendan 在 Twitter 上亲自确认过的。但,还是得眼见为实,我们得在 Netscape 2.0 里确认一下:

“What?说好的没有数组呢?”,不要着急,让我们再多试几次:

多试几次就会发现,原来虽然 Array 构造函数已经存在,但它构造出来的数组还没有实际的功能,length 是 undefined,元素都是 null,这。。。我猜,是还没写完就发布了吧。

除了 arguments,在当时的 Netscape 2.0 里,还有另外一些 DOM 0(当时还没这个叫法)对象也是我们现在说的类数组形式,比如 document.forms/anchors/links 等。

JavaScript 1.1

1996 年, Netscape Navigator 3.0 发布,JavaScript 也升级到了 1.1 版本,这时才有了我们的数组:

同时 arguments 不再仅仅是函数的属性,还像 this 一样成了函数内部的一个特殊变量(说是为了性能考虑):

function foo() {
alert(arguments == foo.arguments) // true,现代浏览器是 false
} foo()

此外还新增了个神奇的特性:

function foo(a, b) {
var c = 3
alert(arguments.a) //
alert(arguments.b) //
alert(arguments.c) //
alert(arguments.arguments == arguments) // true
} foo(1, 2) 

也就是说,所有的形参变量和本地变量都成了 arguments 对象的属性,有没有想起来点什么?这不就是 ES1-3 里的活动对象嘛。

虽然有数组了,但这个时候的 arguments 对象更不像数组了。

ES1

ES1 在这时候发布了,里面虽然提到了函数的 arguments 属性,但已经不推荐使用了。

JavaScript 1.2

实现于 1997 年发布的 Netscape Navigator 4.0 中,新增了 arguments.callee 属性,用来获取当前执行的函数。

ES2

函数的 arguments 属性相关的文字已经完全删除了。

JavaScript 1.3

实现于 1998 年发布的 Netscape Navigator 4.5 中。废弃了 arguments.caller 属性(用 arguments.callee.caller 代替),废弃并删除了上一版里加的形参变量和本地变量作为 arguments 属性的功能。

ES3

没有 arguments 相关的修改

ES4

有两个相关的提议(2007 年 3 月份):

2.4 Richer reflection capability on function and closures
It is not possible to determine the name of a function or the names of its arguments without parsing the function's source – this is a hole in the reflection functionality available through ES3. Functions should have a “name” property that returns the name of the function as a string. The “name” of anonymous functions can be the empty string.
 
Functions should also have an “arguments” array, containing the names of the arguments. So, for the example function foo(bar, baz) {…}, foo.name is "foo" and foo.arguments is ["bar", "baz"].
 
Similar reflection capability must be made available on closures.

2.5 arguments array as “Array”
Make the arguments array an “Array”. That will enable consumers to iterate over its properties using the for .. in loop.

2.4 是说想把函数的 arguments 属性重新规范化一下,让它从包含实参的值改成包含形参的名字,挺有用,对吧,不用再从函数 toString() 后的字符串中正则提取了;2.5 是说想把 arguments 对象变成真实的数组。

还有一个更新一点(2007 年 10 月份)的 ES4 文档,讲到 ES4 里会有剩余参数代替 arguments,还会有 this function 代替 arguments.callee,前者 ES6 里有了,后者 ES6 里还没有,还说了句有意思的话,把 arguments 变成数组是个 bug fix?

还有一个文档(2007 年 11 月)提到了,Opera 居然实现过带有数组方法的 arguments:

ES5

ES5的严格模式禁用了:函数的 arguments 属性、argument.callee/argument.caller 以及 arguments 和形参的绑定,也就是只能用最简单的索引和 length 属性了。

ES6

箭头函数没有 arguments 对象

拥有默认参数、剩余参数、解构参数的函数中的 arguments 对象不和形参绑定

arguments 对象是 iterable 的(拥有 Symbol.iterator 属性),所以可以用 for-of 来遍历了。

总结

这么多年来,arguments 对象给规范的设计和引擎的实现都带来相当大的复杂度,如果能在早期把它修正成一个最朴素的数组,就没这么多事了。本文仅仅是做了一些 arguments 对象的考古工作,至于 arguments 对象为什么这么多年来都没变成数组,笼统点说应该是缺少合适的契机,导致越拖越难改,结果就是再也无法修改了,明确点说就是我也不知道答案啊。如果看了这样的考古你还不过瘾,可以在 Twitter 上问问 Brendan 本人。

2016.10.6 追加,arguments.caller 只在 Netscape 和 IE 里真实存在过,从 MDN 的兼容性表格 看到:

除了 IE,没有一个 21 世纪的浏览器支持过它,ES1-3 规范里也从来没提到过 arguments.caller 这个东西。但也许就是因为 IE,在 ES5 引入严格模式的时候,规范中提到了在严格模式中访问 arguments.caller 要报错,即便在非严格模式中 arguments.caller 是 undefined 的浏览器,严格模式中也要报错:

onerror = alert;
(function(){"use strict";arguments.caller})()

如今,IE 已经停止开发,为了兼容老浏览器而在规范中记录 caller 已经没什么必要了,是时候给庞大的规范减减负了。上周,经过 TC39  开会讨论ES 2017 删除了规范中所有提到 arguments.caller 的文字。这也就意味着,严格模式中访问 arguments.caller 也可以不用报错了,但我估计引擎们短时间内是不会改的。

2016.10.20 追加,半个月前我猜引擎们短时间内不会去掉这个报错,但马上就打脸了,V8 已经准备删掉这个报错,让 caller 变成一个普通的不存在的属性了:https://bugs.chromium.org/p/v8/issues/detail?id=5535

arguments 对象的老历史的更多相关文章

  1. arguments 对象

    在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象 arguments[0],arguments.length arguments是什么? 答:1:arguments是 ...

  2. javascript中的arguments对象

    在js中一切都是对象,连函数也是对象,函数名其实是引用函数定义对象的变量. 1.什么是arguments? 这个函数体内的arguments非常特殊,实际上是所在函数的一个内置类数组对象,可以用数组的 ...

  3. 永远不要修改arguments对象

    案例复现 var obj = { plus: function(arg0, arg1) { return arg0 + arg1; } }; function callMethod(context, ...

  4. [Effective JavaScript 笔记]第23条:永远不要修改arguments对象

    arguments对象并不是标准的Array类型的实例.arguments对象不能直接调用Array方法. arguments对象的救星call方法 使得arguments可以品尝到数组方法的美味,知 ...

  5. [Effective JavaScript 笔记]第24条:使用变量保存arguments对象

    迭代器(iterator)是一个可以顺序存取数据集合的对象.其一个典型的API是next方法.该方法获得序列中的下一个值. 迭代器示例 题目:希望编写一个便利的函数,它可以接收任意数量的参数,并为这些 ...

  6. 理解Javascript参数中的arguments对象

    ECMAScript中函数没有标签名的特性,所以ECMAScript函数中没有重载. Javascript中arguments的存在可以弥补javascript中函数没有重载的不足. Javascri ...

  7. 关于arguments对象以及函数的柯里化;

    1.arguments对象 Arguments是个类似数组但不是数组的对象,说他类似数组是因为其具备数组相同的访问性质及方式,能够由arguments[n]来访问对应的单个参数的值,并拥有数组长度属性 ...

  8. JavaScript之arguments对象讲解

    javascript的arguments对象类似于PHP的extract()函数实现. 在不确定函数参数个数的情况下,可以通过arguments访问参数,并以索引0为起始. function sayH ...

  9. JavaScript对象(正则表达式,Date对象,function对象 arguments对象)

    好用的技术教程:http://www.w3school.com.cn/index.html 1:正则表达式 正则表达式通常用于验证表单 定义语法为 / / 2:Date对象 var now = new ...

随机推荐

  1. PL/SQL入门理解(一)

    1.PL/SQL概述 1)概念 pl/sql(procedural language)是Oracle在标准sql语言的基础上的扩展,可以实现定义变量.使用逻辑控制语句等 作用:默认oracle一次只能 ...

  2. 【转载】Web移动端Fixed布局的解决方案

    特别声明:本文转载于EFE的<Web移动端Fixed布局的解决方案>.如需转载,烦请注明原文出处:http://efe.baidu.com/blog/mobile-fixed-layout ...

  3. POJ3422 Kaka's Matrix Travels[费用流]

    Kaka's Matrix Travels Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 9522   Accepted:  ...

  4. 第3章 Linux常用命令(6)_关机重启命令

    8. 关机重启命令 8.1 关机重启命令 (1)shutdown [选项] 时间     ①选项 -c:取消前一个关机命令 -h:关机 -r:重启 ②应用举例:#shutdown –h now.shu ...

  5. python BeautifulSoup模块的简要介绍

    常用介绍: pip install beautifulsoup4 # 安装模块 from bs4 import BeautifulSoup # 导入模块 soup = BeautifulSoup(ht ...

  6. spring有三种启动方式

    spring有三种启动方式,使用ContextLoaderServlet,ContextLoaderListener和ContextLoaderPlugIn spring3.0及以后版本中已经删除Co ...

  7. Intellij Idea中的Jetty报出Web application not found src/main/webapp错误的解决方案

    今天在Intellij Idea中编译项目的时候,运行起来一直会报出如下的错误: Web application not found src/main/webapp 当时感觉应该是什么文件缺少了.所以 ...

  8. 关于javascript中apply()和call()方法的区别

    如果没接触过动态语言,以编译型语言的思维方式去理解javaScript将会有种神奇而怪异的感觉,因为意识上往往不可能的事偏偏就发生了,甚至觉得不可理喻.如果在学JavaScript这自由而变幻无穷的语 ...

  9. Memcached 服务器端命令

    memcached的基本命令(安装.卸载.启动.配置相关): -p 监听的端口   -l 连接的IP地址, 默认是本机    -d start 启动memcached服务   -d restart 重 ...

  10. jQuery之Ajax--全局Ajax事件处理器

    1.这些方法用于注册事件处理器,用来处理页面上的任何 Ajax 请求,当某些事件触发后,这些事件处理器被调用.如果jQuery.ajaxSteup()中的 global 属性被设置为 true (这也 ...