引題:為什么 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
文章列表