arguments 对象的老历史

引题:为什么 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]) // 1
  alert(foo.arguments[1]) // 2
  alert(foo.arguments[2]) // 3
  alert(foo.arguments.length) // 3
  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) // 10
  a = 100
  alert(arguments[0]) // 100
}

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]) // 1
arguments[0] = 10
alert(a) // 10

说完全不是呢,是因为我知道另外一个更明显的,在当时,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) // 1
  alert(arguments.b) // 2
  alert(arguments.c) // 3
  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

posted @ 2016-09-21 20:48  紫云飞  阅读(1697)  评论(2编辑  收藏  举报