iThome 網站首載:從ECMAScript看語言約束
開發者多半討厭語言約束,特別是對新手而言,語言約束總是令他們感到沮喪,為了讓開發者盡可能控制一切,幾近無約束的JavaScript誕生於1995年5月,這被視為玩具的語言鹹魚翻身之後,因為沒有太多語法約束,開發者可盡情地在上頭創造想要的元素,這其中也創造了不少約束開發者的元素,從ECMAScript 5到ECMAScript 6的演進,就能夠瞭解這十幾年來開發上必要的語言約束有哪些。
- ECMAScript 5嚴格模式
JavaScript在1996年進行標準化,第一個標準化版本ECMA-262於1997年採納,因為Java名稱上具有商標問題,ECMA-262採用了ECMAScript作為語言名稱,JavaScript此後成為了ECMA-262標準的實作語言,現在的瀏覽器幾乎都能支援1999年發佈的ECMA-262第三版以上的實作,又稱ECMAScript 3,對應的實作為JavaScript 1.5,由於一些政治因素,ECMAScript 4發展意見發生分岐,原有規範中的一小部份是就現有功能作了改進,被發佈為ECMAScript 3.1,後來ECMAScript 4被中止,ECMAScript 3.1被改名ECMAScript 5並於2009年發佈,後續2011年又發佈了ECMAscript 5.1。
ECMAScript 5的主要特點之一是可宣告嚴格模式(Strict mode),透過在程式碼中加入
'use strict'
字串,一些過去被認為不良實踐的語法就會無法使用,嚴格模式下有許多規範是關於識別字名稱使用上的限制,像是沒有使用var
宣告的變數無法使用,不能使用with
,不能有重複的參數或特性名稱,不能與保留字或既存的識別字名稱衝突(像是arguments
),eval
函式評估出來的變數不能在外部使用等,很顯然地,這反映了過去語言在範疇(Scope)與名稱空間(Namespace)管理上過於鬆散,因而必須加強範疇與名稱空間管理上的約束。JavaScript中
this
的實際對象可以依呼叫者(caller)自動變換,這雖是很強大的彈性但也經常難以掌握,像是ECMAScript 5之前,如果this
沒有實際對象,也就是this實際上是null
或undefined
時,會自動轉換為全域物件,這又會造成許多誤判或誤用的問題,嚴格模式下禁止了這個行為,實際上,為了讓開發者能明確掌握this,函式新增了bind
函式,可以讓你固定this
的對象傳回新函式,讓this
不會隨著呼叫者自動變換,實際上在這之前,開發者為了要能更精確掌握this實際對象,避免不清不楚下發生問題,亦會明確地在使用call
或apply
函式時指定this
的對象。- 針對物件個體化的約束
JavaScript中開發者幾乎可以控制物件的一切,你可以從一個幾乎完全身無分物的物件開始,根據需求賦予它必要的能力,你也可以將物件的能力剝奪到幾近一無所有,這種基於原型基礎的語言,有利於快速堆砌功能,這也是後來JavaScript鹹魚翻身之後,生態圈得以迅速發展的原因之一,然而就如我先前專欄〈類別與原型的物件管理學〉中談過,這種特性「容易造成維護上的混亂,在應用於工程性偏重的場合,往往得採取某種慣例來約束物件行為...避免開發者直接修改物件的行為與結構」。
ECMAScript 5中搭配嚴格模式,可以使用
Object.preventExtensions
函式來阻止對物件直接進行擴充,進一步地,還可以定義特性描述器(Property descriptor),搭配Object.defineProperty
、Object.defineProperties
函式來指定特性是否可修改、刪除或列舉,為了有更明確的語義,還有Object.seal
函式可以對物件彌封,被彌封的物件不能擴充或刪除物件上的特性,亦有Object.freeze
函式可用來凍結物件,讓物件成為唯讀物件。物件個體化能力在專案剛啟始、需求變化快的場合很有用,可以快速堆砌功能,然而當專案逐漸邁入維護份量加重的場合時,嚴格模式下對物件個體化的約束,可以讓JavaScript更實質地約束物件行為,語言的約束並非不好,實際上慣例本身也是種約束,如果專案一開始沒有相當程度的慣例約束,後來想貿然加上嚴格模式或直接對物件個體化做出限制,也只會驟然面對龐大的錯誤訊息,不可能直接享用到ECMAScript 5可選用的嚴格模式與物件個體化限制之好處。
- ECMAScript 6區塊範疇、類別與模組
被中止的ECMAScript 4中一部份後來演變為ECMAScript 6,於2013年凍結了ECMAScript 6草案停止增加新功能,預計於2015年中發佈正式版本,目前Google的V8實現了部份特性,例如,可使用Node.js 0.11版來體驗。在語言的約束上,ECMAScript 6增加了
let
宣告,與var
不同的是,let
宣告的變數範疇只限宣告當時所在區塊(Block),也不可以重複宣告同一變數,也不會如同var
宣告的變數一樣具有提昇(Hoisting)的特性,這比較符合多數程式語言中變數的特性。JavaScript是基於原型(Prototype-based)的語言,沒有類別的概念,然而,ECMAScript 6中增加了
class
、constructor
、extends
等語法,可支援基於類別(Class-based)的封裝、繼承,乍看似乎基於原型、基於類別兩個系統相互衝突,其實ECMAScript 6的類別語法極為精簡,主要目的讓過去類別風格封裝、繼承模擬的各種作法,能有個標準方式,拆解ECMAScript 6的類別語法後會發現,與現階段使用function
模擬類別的常用手法可以一對一對照,某些程度上,ECMAScript 6中規範的類別就像是語法蜜糖。對於基於類別的語言來說,類別規範了實例的行為與結構,我在RubyConf 2014的〈Understanding Typing. Understanding Ruby.〉中談過,即使是支援物件個體化的Ruby,也可以經由觀察與重構,讓必要的行為座落於適當的
module
與類別之中,而這概念在JavaScript中也是適用,如果過去曾經使用過慣例上function模擬類別的作法,直接使用ECMAScript 6支援class
等的語法,基本上不困難,語義上明確且程式碼也會簡潔許多。在JavaScript中,名稱空間管理與模組載入是個很大的問題,過去花了許多功夫在各種方案上,甚至有了專門處理這類問題的框架,像是RequireJS,ECMAScript 6中為了解決這類問題規範了模組特性,讓文件本身就是一個模組,可使用
export
來匯出名稱,使用import
來匯入名稱並創造名稱空間,照例地,這過去可能自行以慣例或運用框架來進行約束,ECMAScript 6讓這類議題有了標準作法且語義明確。- 語言約束背後是怎樣的慣例?
從ECMAScript標準化到ECMAScript 6是個有趣的過程,如同Brendan Eich在為《Effective JavaScript》寫序時提到,在一堆挑戰性高的需求及發瘋般的時程下,他能做的就是讓JavaScript成為一門可被從頭塑造的語言,開發者能依自己的需求來為JavaScript上patch,而這麼多年來,眾多開發者確實也為JavaScript形塑了它應有的樣貌,或說是在JavaScript上累積了許多最佳實踐(Best practice)。
Brendan Eich也談到「所有語言幾乎都是受限地實行常見的最佳實踐」,JavaScript因緣聚會,在幾近無約束的情況下,先累積了諸多開發經驗與慣例,接著回饋至語言本身成為可選的約束,從另一個角度看過來就是,沒有語言會為了整開發者而加入語法約束,語言若在一開始建立好相關的約束,應有其背後的原因,這也就是不要只從語法或關鍵字上來學習語言的原因,應思考相關語法或約束背後,是否為了解決哪些問題,或者是否曾經借鏡於其它語言的使用慣例。
實際上ECMAScript 6尚未正式發佈,而真正各平台會實作採行也需要相當時間,在這之前,可從ECMAScript 6的規範中先吸取其語法約束背後之精神,就當作目前可用的JavaScript實作平台上已有這類約束一般,將來若真要採用ECMAScript 6規範的語法,就不至面臨太大問題,否則在毫無約束之下,即使只是加上個
'use strict'
,也足夠讓現有程式一片混亂了!