arguments 对象的老历史

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

JavaScript 1.0

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

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:web

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 有意加的额外特性,而是他的第一反应就是应该这么去实现?ecmascript

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

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

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

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

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

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

那后者算是个缘由吗?一点点又或者彻底不是,说一点点是由于在当时尚未 __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

相关文章
相关标签/搜索