本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。
購書連結 https://www.tenlong.com.tw/products/9789864344130
讓我們再次重新認識 JavaScript!
終於來到鐵人賽的最後一篇了,在這篇我想把主題從前面幾篇的框架介紹,再度把主軸拉回 JavaScript 本體。
我曾在系列文的前面介紹過,自 ECMAScript 6 開始,負責制定 ECMAScript 標準的委員會 (TC39) 決定將新標準改為一年一修,因此包括 ES6 開始往後的版本都會定為 ECMAScript 2015 (ES6)、ECMAScript 2016、ECMAScript 2017 持續下去。
在過去,JavaScript 的發展受到了瀏覽器的限制。 我們都知道,ES5 大約是在 2009 年發布的,但直到 2011 年的時候,其實大部分人還不敢貿然投入,很大的原因就是前端開發者被要求寫出來的程式必須能夠支援老舊的瀏覽器,像是 IE(6) 還有 IE(7) 以及 IE(8)。
相信經歷過那個年代的朋友一定喊過「 IE must DIE! 」
受害者的怒吼 推薦閱讀: 生在幸福的 JS 年代 - 閃光洽
幸好,在 ES5 發佈後的五年內,整個 JavaScript 的環境開始有了突破性的成長。 首先是 2011 年出現的 node.js 與 npm,它打破了過去十幾年來 JavaScript 幾乎只能在瀏覽器上執行的困境,讓 JavaScript 總算在實質上成為一門獨立的程式語言。而 npm 則解決了 JavaScript 多年來缺乏模組化套件管理的問題,各種前後端的 JavaScript 套件紛紛在 npm 上發佈。
另外,自 ES6 開始也打破了過去久久才發布一個大版本的更新方式,將未來的新標準改為一年一修,也就是說 ES2016 是基於 ES2015 版本的更新、ES2017 是基於 ES2016 版本的更新,每一年都會有一些新的特性發布,也不會出現像過去 ES3 到 ES4 那樣大破大立的結果。
換言之,未來的新標準不會破壞對過去 JavaScript 的相容性,像我們曾介紹過的: typeof(null) === 'object'
這種老問題,修改它雖然是小事,但改了之後影響層面難以估計,後來決議的結果也就維持原樣。
雖然原有的程式碼在未來仍能繼續使用,但學習新的標準仍然有它的好處在。 像 ES6 新有的 let
、const
它帶來了 Block scope 的特性,過去我們需要再用一層 IIFE 包裝的程式,自從有了 Block scope 之後,可以省下不少麻煩。 另外像是在過去沒有 Module 的年代,想要有類似效果,就必須透過 CommonJS / AMD 來實作,但規格的不統一很容易就會變成各做各的,未來就很難繼續推動。
const
與 let
相信跟著看這個系列文的朋友都知道,過去 JavaScript 在宣告變數的時候一律使用 var
,除了 scope 是以 function
作為切分的單位之外,還會有 Hoisting 的意外特性。但自從有了 const
與 let
之後,除了改以「大括號」 { }
作為切分 scope 的分界外,新增了 const
的語意,不僅在閱讀程式時更加明確,開發的時候也可以降低意外狀況發生。
以往我們在組合 JavaScript 的變數與 HTML 模板的時候,大多會透過「字串結合」的模式或透過陣列來新增字串,最後再用 [].join("")
的方式串接起來。 但自 ES6 起,我們可以透過字串模板的語法,將變數、運算式等插入至我們的網頁模版當中,像這樣:
`string text ${expression} string text`
我們在 DAY 20 介紹 this
的時候曾經提過 Arrow Function (箭頭函式) 這個東西。 Arrow Function 簡化了 function
的語法,將原本 function (x) { ... }.bind(this)
簡化為 (x) => { ... }
,同時也對 this
變數做強制綁定。 別小看這點優化,ES6 新增了這個特性之後,完全將 functional programming 的特點充份發揮出來,同時也提高了程式的可讀性與維護性。
Promises
先前在 Day 26 介紹「同步與非同步」的時候已經介紹過了,最大的優勢就是解決了過去處理非同步時遇到的「callback hell」。 在 ES5 時期,就已經有很多第三方 Promise 工具庫了,到 ES6 則更進一步把 Promise 納入標準,成為 JavaScript 的標準物件。
class
語法糖雖然 ES6 終於加入了 class
、extends
、constructor
這些在其他物件導向語言常見的的語法,但這些語法終究仍是屬於「語法糖」。 JavaScript 本體仍是 prototype-based 的程式語言,這部分我們之前介紹原型鍊與繼承的時候就說明過了。
過去我們會以 function
作為建構式使用,然後透過 prototype
與 Object.create()
來完成繼承:
var Rectangle = function (id, x, y, width, height) {
Shape.call(this, id, x, y);
this.width = width;
this.height = height;
};
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
var Circle = function (id, x, y, radius) {
Shape.call(this, id, x, y);
this.radius = radius;
};
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
但是在 ES6 的 class
語法,可以改寫成這樣:
class Rectangle extends Shape {
constructor (id, x, y, width, height) {
super(id, x, y);
this.width = width;
this.height = height;
}
}
class Circle extends Shape {
constructor (id, x, y, radius) {
super(id, x, y);
this.radius = radius;
}
}
是不是看起來簡潔多了?
不過要注意的是,由於 JavaScript 仍是 prototype-based 的程式語言,過去 constructor 函式與 ES6 Class 語法仍然是「一對一」關係的語法糖,只能宣告成員方法,無法宣告成員屬性。 若要用 class
來模擬 private 的作法的話,仍得透過 Object.defineProperty
加上 get
、set
來處理。 但比起過去來說,已經是相當大的進步了。
generator
應該算是 ES6 帶來最特別的特性。
由於 JavaScript 絕大多數的執行原則是採「單一執行緒」,換句話說,一旦函式開始執行,它就會一直執行到完成為止。 但透過 ES6 generator
語法,我們可以使用「特殊」的函式,它可以在執行到一半暫停,然後再回復執行,讓其他程式可以在暫停這段期間先跑。
function * gen() {
console.log('start');
yield "called";
}
// 回傳一個 Generator 物件
var g = gen();
// 開始執行
var a = g.next();
console.log(a.value); // "start"
console.log(a.done); // "called"
// 因為 g 已經結束,所以回傳 false
var b = g.next(); // false
// 透過 b.done 可以判斷是否結束
console.log(b.done); // true
像這樣,一開始呼叫 gen()
函式時,並不會直接執行,而是先回傳一個 Generator 物件。
接著執行 Generator 物件的 next 方法,函數才會開始執行。
推薦大家可以參閱 fillano 大公寫的初探 ES6(3)Generator ,上面的範例也是節錄自該篇文章修改而來。
由於篇幅的關係,這裡就簡單列出部分 ES6 帶來的新特性,詳細的內容可參照:
前面提到,以往開發者不敢貿然投入新語法,有很大的原因與瀏覽器的支援程度有關。但還好現在我們有了 Babel,透過 Babel 這個編譯器它可以將 ES6 「大部分」的新特性編譯成在過去瀏覽器上也能順利執行的 ES5 語法,例如 Arrow Function ,經過編譯後就會變成一般的普通 Function。
Stage0-4
是什麼東西在很多 Bable 環境安裝與配置的文件當中,你可能會看到 babel-preset-stage-x
像這樣的東西:
$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
$ npm install --save-dev babel-preset-stage-3
我們前面提到,在未來 ES 標準會變成一年一修,而 Stage0-4 代表的是 ES 實作標準草案的成熟度階段。
stage 0
代表的是最初的想法,推動者將某個新特性寫成規格書後送交 TC39 也就是制定 ECMAScript 標準的委員會。stage 1
代表委員會初步討論認為可行,然後指派一個負責人繼續執行跟進stage 2
時期時,委員會與新特性規格的負責人會開始進行語法以及程式語意進行討論stage 3
的時候,各大瀏覽器廠商就會開始做實驗性的實作一般來說,當規格到了 stage 4
階段的時候,這個新特性的提案也就差不多已經進入了實質的 ES 標準規格,所以也不再需要透過 Bable 來轉換了。
提到了 Bable,很多人會喜歡拿 CoffeeScript 或 Typescript 這類 JavaScript 方言來做比較。 就語法本身來說,CoffeeScript 或 Typescript 一開始的主要受眾通常會是那些原本以「後端語言」像 Ruby、C# 作為主力的開發者為主。 但後來隨著語言與規格的演進,會發現到,其實這些方言型語言,倒不一定是為了取代 JavaScript 而生。 像 Typescript 的目標就非常明確,在 JavaScript 的基礎上加強了對型別的支援,語言設計上完全與 JavaScript 向下相容。
跟 Babel 不同的是,Babel 的主要用途是在於語言的「轉譯」,就是說要如何將新的語法轉換成在舊版本的瀏覽器上也能執行的語法。 而 Typescript 雖然也可以完成語言的轉譯,但它更關心的是如何將靜態類型加入到 JavaScript 這個語言當中,使得程式在開發、編譯的階段就可以檢查出各種低級的錯誤,節省未來在 runtime 難以除錯的成本。
而兩者在設計的架構面也有不同,像 Babel 主要是以 Plugin 的形式載入,而 Typescript 則是以獨立語言的形式存在。
我知道其實很多人還停留在擔心舊有瀏覽器是不是不支援 ES5/6/7 等等的新語法。
如果單純以完成需求來看,新技術學不學倒不是那麼重要,反正就算我用 ES3 也是可以達成任務。 但即便如此,為什麼還是會有新的東西不斷推出?
當你已經開始使用 ES6 之類的新語法開發的時候,像 let
與 const
某種程度上你就不會遇到像過去 var
那些奇奇怪怪的特性,但這些東西是不是可以完全不用管它? 如果你不需要維護舊專案的話,其實不用回頭看也無所謂。
框架、工具都是因應問題而生,技術也是。
像是 task runner 過去我們有 grunt、gulp,現在有 npm script,透過自動化方式來協助我們處理繁雜的任務。 而透過 Webpack, Browserify 這類建構的工具,在開發階段像是 hot-reload 以及各種 loader 可以提供開發上更好的體驗之外,在程式打包的部分,搭配 ES6 的 Module 分析以及 rollup,甚至可以幫我們做到 Tree Shaking 的效果。處理 SSR 的時候,甚至可以做到 inlining Critical CSS,達到程式部署前的最佳優化,這些東西在過去都是難以想像的。 [註1]
比起過去,在現代的開發環境,幸運的我們已經有了各種工具支援,完整的前端生態圈。面對技術的未來,我們應該抱持著更開放的心態。
這也是為什麼我把這個主題留在「重新認識 JavaScript」系列的最後一篇。
感謝你看到這裡,「重新認識 JavaScript」就是希望能在這個主題當中,用 30 篇文章的內容與各位一起重新認識 JavaScript:這個號稱「世界上最被人誤解的程式語言」。
<head>
,後續的 CSS 再以 Lazy loading 的方式延遲載入,這樣可以做到節省 request、網頁渲染速度更快的效果。