你從未聽說過的 JavaScript 早期特性

紫雲飛發表於2017-12-06

最近這些年在對 JavaScript 進行考古時,發現網景時代的 JavaScipt 實現,存在一些鮮為人知的特性,我從中挑選幾個有趣的說一下。

Object.prototype.eval() 方法

在 JavaScript 1.0 中,eval 和現在一樣,只是個全域性函式。在 JavaScript 1.1 中,eval 還變成了所有物件的一個共用方法:

var foo = 1
var bar = 2

o = new Object
o.foo = 10
o.bar = 20

eval("this.foo + bar") // 3
o.eval("this.foo + bar") // 30

官方文件描述這個 eval 方法是說它能讓一段程式碼字串在執行時以當前物件為上下文:

The eval method evaluates a string of JavaScript code in the context of the specified object

描述很籠統,其實這個 eval 方法和普通的 eval 函式的區別有兩點:

1. this 指向當前物件而不是全域性物件

2. 類似 with 語句的作用,所有變數優先作為當前物件的屬性去查詢

該方法在隨後的 JavaScript 1.2 中被廢棄。

不對 Array(arrayLength) 進行特殊處理

Array() 建構函式實現於 JavaScirpt 1.1 中,和現在一樣,當時就已經對單數字引數的情況做了特殊處理:

Array(1, 2, 3, 4, 5) // 包含五個元素 1、2、3、4、5 的陣列 
Array(1, 2, 3) // 包含三個元素 1、2、3 的陣列  
Array(5) // 包含五個空元素的陣列  

為了不讓使用者踩這個坑,ES6 裡還專門新增了一個 API:Array.of()。 其實不是到了 ES6 時代才發現這是個坑的,98 年就已經發現了。在 JavaScript 1.2 中,嘗試去除了對單數字引數的特殊處理:

Array(5) // 只包含一個元素 5 的陣列

但並沒有被 ES 規範採納,之後 JavaScript 1.3 回滾了這個改動,直到今日仍如此。

相等運算子 == 不對運算元進行型別轉換

在 JavaScript 中如何對兩個值進行相等性判斷絕不是一個簡單的話題,MDN 上甚至有一篇專門的文件來講解。判斷等還是不等為啥這麼複雜,其中很大的一個原因就是,JavaScirpt 裡有兩套相等運算子:==/!= 和 ===/!==。

既然現在都推薦用嚴格相等 ===,不讓用 ==,那為啥當初不把 == 實現成嚴格相等呢。其實網景在 98 年的時候就已經發現非嚴格相等是個坑了,在 JavaScript 1.2 中,他們嘗試把 == 的強制型別轉換步驟去掉,讓它表現的就像現在的 === 一樣:

1 == "1" // false

不過在次年釋出的 ES3 裡,決定採用新增 === 運算子的方案,所以只好回滾了上面這個改動。

條件表示式不能是個賦值語句

if、while 語句都接受一個表示式作為是否要執行後面程式碼塊的判斷條件,比如 if ( a == b)。但是很多時候人們會因手誤漏掉個 =,成了 if (a = b),導致很難發現的 bug。

在 JavaScript 1.0 中,JS 引擎會認為你是不小心漏掉了一個 =,它會替你補上,並會發出警告資訊。如果你的本意真的是想使用賦值語句(的確有人想要這麼寫),那就給賦值語句加一個額外的括號:if((a = b))。

之前我問過 Brendan Eich 本人,這個特性是他從 GCC 抄來的。我看了一下現在留存最早的 JavaScript 1.4 的程式碼,實現該特性的程式碼還在,只不過其實從 1.3 開始,為了相容 ES 規範,這段程式碼實際已經不會執行了。

false 的包裝物件是個假值

現在我們知道,所有物件都是真值,即便是 false 的包裝物件,但在 JavaScript 1.3 之前,它是個假值:

if (new Boolean(false)) {
  // 不會執行
}

所有本地變數都同時作為 arguments 物件的屬性存在

我在arguments 物件的老歷史中講過這個,在 JavaScript 1.1 和 1.2 中存在的特性,所有本地變數包括形參的名字都會成為 arguments 物件的屬性:

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

foo(1, 2) 

Object.prototype.toString 不返回 [object type]

現在我們熟知,用 Object.prototype.toString 可以判斷物件型別,比如它會返回 [object Object]。也許認為這個特性沒啥用,在 JavaScript 1.2 中,它被改成了返回物件的原始碼形式,比如:

var o = {a:1}
o.toString() // "{a:1}"

Array.prototype.toString 也和現在不一樣:

var arr = [1, 2, 3]
arr.toString() // "[1, 2, 3]",而不是 "1,2,3"

在 1.3 中,這個方法重新起了個名字叫 toSource(Firefox 中至今存在),toString 迴歸到原來的功能。

邏輯與和邏輯或返回布林值

現在我們常用 obj.foo && obj.foo.bar 和 obj.foo || obj.bar 這種寫法,但是在 JavaScript 1.2 之前,&& 和 || 有個 bug:

0 && 1 // 返回 false,而不是 0
 
1 || 0 // 返回 true,而不是 1

之所以說 bug,而不是有意設計,是因為反過來就和現在的表現一樣了:

1 && 0 // 0
 
0 || 1 // 1

對 str.split(" ") 的特殊處理

如果傳入 split 方法的分隔符是單個空格,那麼會進行一個特殊處理:先 trim 掉字串兩邊的空白符(" \t\r\n"),然後以若干個連續的空白符作為分隔符去切割這個字串:

"         1               2            ".split(" ") // ["1", "2"]

現代瀏覽器上的話,會切割出很多空字串元素。JavaScript 一開始也和現在一樣,但在 JavaScript 1.2 中,Breandan Eich 抄了 Perl 4 中對 split(" ") 的特殊處理,Perl 4 是從 awk 抄來的。1.3 回滾了這一改動。

用索引也能訪問物件的屬性

在 JavaScript 1.0 裡,每個物件的非索引屬性都會自動關聯一個索性屬性:

var o = new Object
o.foo = 1
o.bar = 2

o[0] // 1
o[1] // 2

1.1 刪除了這個特性。

this 不能在全域性環境使用

現在我們都知道,this 在全域性環境下指向全域性物件,但在 JavaScript 1.0 裡,this 只允許在函式內部使用:

這一特性沒有文件說明,所以不確定是在之後的哪個版本中改成了可以在全域性環境中使用 this。

 

相關文章