arguments 物件的老歷史

紫雲飛發表於2016-09-21

引題:為什麼 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

相關文章