What JS is or not? Ep.1 Hello world

・18min
本文目前為非公開草稿,內容非常雜亂,另外歡迎賜教

下面有幾個不同的 Hello World

看著文章的你,不一定認識這些語言。但寫 code 那麼久,光看語法本身,能夠了解不同語言中的每一個元素可能在作些什麼嗎?

Shell

#!/bin/bash
echo "Hello, World"

C

#include <stdio.h>  
  
int main() {  
printf("Hello, World\n");  
return 0;  
}

Python

print("Hello, World")

Kotlin

fun main() {  
    println("Hello, World")  
}

Java

public class HelloWorld {
    public static void main(String[] args)
    {
        System.out.println("Hello, World");
    }
}

go

package main
import "fmt"
func main() {
    fmt.Println("Hello, World")
}

最後是,JavaScript

console.log('Hello, World')

Hello World 是最簡單的程式碼,也是每個「程式語言」的入門磚。各個語言都有自己的特色,Hello World 也當然有所不同。但拋開複雜的語法或 API 操作,單單就「字面」而言,差異在哪裡?可以先歸納幾個特徵:

從哪裡開始聊起呢?就像一個應用程式會從 main 開始執行,我們也從 main 這點開始好了。

不論你有沒有寫過這些語言,上面提到的 C, Java, Go 的程式碼,都能知道會從 main 開始執行,中間作些有的沒的,正常會在 main 結束(好,當然也有可能作 I/O loop,總之就是你知道的那回事)。

用比較 nerdy 的說法會是:main 是整個 Application 的 entry point (入口點)

那沒有 main 的語言呢?採用了另一套模式,就像我們在紙上寫文章,寫故事 (scripting) 同樣的邏輯:「由上至下」

Shell, Python, JavaScript 正是這類風格的代表。雖然不精準,但我們姑且可以稱這類的程式語言叫做腳本語言 (scripitng language) ,而這樣的模式是這類語言的特色之一:從第一行開始執行,一行一行由上至下執行,直到結束。它沒有 main 作為 entry point。或可以從另一個角度來看,執行的「檔案本身」就是程式的 entry point。

但這樣的風格代表著什麼?這關乎到語言設計之初的定位。

Python 在設計之初就把易寫性作為核心哲學,而這樣的執行方式正是重要的實踐之一,比起 main 作為 entry point ,腳本式的寫法更加直覺。最初使用情境就是:

「比 Shell 更強更簡單,比 C 開發更快,寫系統腳本超讚」

它作到了,well done!除了執行方式之外

上面這些都是為了當初作為「腳本語言」的特性,是非常優秀而友善的設計。

反觀我們的主角 JavaScript ,也同樣使用腳本語言中,由上至下書寫的風格,但脈絡就沒 Python 這麼優雅和理想了。

#How JS is?

1995 年,Netscape 和 Sun 合作,準備把 Java 放進新一代瀏覽器 Navigator 2.0 。但 Netscape 發現,對於想要作一些簡單互動的設計師而言,Java 實在太過沈重;除此之外,他們也不希望整個瀏覽器的互動被 Java 壟斷。於是決定在同版本加入一種輕量、但語法與 Java 類似的腳本語言。

當時希望讓設計師能做到簡單互動大概像這樣: Melon’s TOWN

這個重要的任務交給 4 月才到職的 Brendan Eich,然而 Navigator 2.0 的 feature freeze 時程早就確定了:十天後,他需要做出第一版的 prototype。

ref: A brief history of JavaScript | Deno

上帝花了 7 天造人;JS 不遑多讓,Brendan Eich 花了 10 天

比起 Python 優雅的設計哲學,JS 完全是商業壓力驅動的產物。如此不同的動機下產生的語言卻有了微妙的共通點:

兩者都希望能夠簡化當時程式開發(使用 C, C++, Java 等等)的複雜度,你不希望寫個小互動還要去操作記憶體,不想知道到底「數字」要存成什麼。腳本語言則是這樣的背景發展出來的類型與風格,重心應該放在「簡單的描述想完成的操作」。

類似的出發點,讓 Python 跟 JavaScript,不謀而合的有著著同樣的品味:

開發者能用更接近自然語言的方式來表達意圖

1994 年 1 月 27 日, Guido van Rossum 在郵件群組中發布 Python 1.0.0 時,很興奮的寫著:

ref: Python 1.0.0 is out!

Maybe you should try Python, the next generation object-oriented scripting and prototyping language, with a readable syntax.

也許你應該嘗試 Python — 一款具備可讀性語法的次世代物件導向、腳本與原型設計語言

而過了幾年來到 1996 ,當時為了跟微軟的 JScript 競爭,Netscape 選擇了 ECMA 來幫 JavaScript 制定標準,而隔年的 6 月,TC39 便光速的發佈了第一版的 ECMA-262,JS 進入標準化的時代(但還是一片混沌),而標準上寫著 :

ECMAScript was originally designed to be a Web scripting language, providing a mechanism to enliven Web pages in browsers and to perform server computation as part of a Web-based client-server architecture.

ECMAScript最初是作為網頁腳本語言設計的,旨在提供一種機制,使網頁能在瀏覽器中動態呈現,並作為基於網頁的客戶端-伺服器架構的一部分執行伺服器端運算。

ECMA-262, 1st edition, June 1997

兩個語言都有著同樣腳本語言的風格,但一個是精心設計的哲學實現,另一個則是商業策略的產物。這也注定,雖然兩者同樣是抽象程度相當高的語言,也有著類似的「味道」,但在應用方面,未來的發展卻截然不同。

了解了一點 JS 的歷史,我們好像更知道為什麼會是這樣的寫法。但接下來,我們將進入另一個有趣的發現:

什麼是 print?為什麼 JS 沒有所謂的 print,只有 console.logconsole 是什麼?是 library 嗎?

但我們先從其他語言開始,這樣才能了解 JS 的有趣之處。就從什麼是「print」開始吧

#What the print is?

print 通常是幾乎所有程式語言都會內建的 method,功能是把程式中的值,呈現在 CLI (Command line Interface) 上面。簡單說就是這個:

print 看起來只是簡單的東西印出來,但裡面作的事情,比你想像的複雜很多。大致的步驟會是

  1. 序列化:把程式內部的「值」轉成字串後,進行編碼
  2. 輸出:把編碼後的結果寫入到設定好的「輸出通道」

每個語言都有不同的資料型別,在序列化的過程中,會將這些資料型別:可能是 float, integer, pinter, function… 任何一種資料,嘗試轉換成「可顯示的文字表示」。再來,每個語言、每個程式中,表示「文字」的方式是不同的,需要把程式中的「文字」轉換成作業系統可以處理 bytes 資料格式,例如 UTF-8 等。

當你使用 CLI 介面執行程式時,預設的輸出的管道(stdout)就已經會接到你的 terminal 上。而程式會把前面準備好的 bytes 「寫入」到這個輸出,最終讓我們看到「Hello World」。

是不是超級麻煩的 🧐

不只有你這麼覺得,所以大部分的語言不會讓你自幹。不是內建,就是包在標準庫裡面。:

那 JS 呢?這就複雜了,寫過 JS 的人都知道,這關乎你要在哪裡「執行」JS。如果你是在 Node.js 上執行,那的確下面的程式碼是可以直接執行的。

console.log('Hello world')

但如果你想要在 Browser 執行,上面是不夠的。你需要完整的 .html 檔案。JS 是操作網頁行為的工具,所以要寫在 script 的 tag 裡面。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Hello World</title>
  <script>
    console.log('Hello World')
  </script>
</head>
<body>
  <h1>Hello World</h1>
</body>
</html>

寫過了這麼久的 JS,你不可能沒有用過 console.log

不論你處理的是 Server side , Browser 又或是 React Native 等其他平台。有些 API 是平台專屬的,例如瀏覽器上的 document.addEventListener ,又者是 node.jsfs 系列。但不論每個平台,我們都使用 console.log 來印出一些資訊,那難道 console.log 是語言提供的功能嗎?

來看看 console.log 是怎麼誕生的吧,可以說是我們最熟悉的陌生人了。

#The Birth of a console.log

在瀏覽器被發明之時,大概 1990 年代左右,當時的瀏覽器是沒有 debugger 的。或者說,為什麼要有?這是全世界的第一個網站

是的。當初的網頁只有 HTML,沒有 CSS,也沒有 JS,那理所當然也不用 debug。網頁只是單純的渲染 HTML 語法而已。debug 這件事情,要直到網頁開始可以「互動」開始才有意義,也就是 JavaScript 誕生之後。而在當時,大家最常用的除錯工具有幾個,到現在也能夠運作:

但這樣是不行的,不加上 alert,就完全不知道 JS 有沒有寫錯,甚至也不知道錯在哪裡。隨著應用複雜了起來,這樣的開發模式勢必會遇到了瓶頸。神說:「要有光」,就有了光;開發者說:「要有除錯工具啊!」,於是也有了除錯工具!當時的瀏覽器 Netscape Navigator / Microsoft IE 在推出 JS 之後,也紛紛推出一些工具。像是 Netscape 有個獨立的 JS Debugger。可以告訴你哪一行發生錯誤

ref: Netscape Debugging

IE 也有,但就是粗暴了點,直接跳一個 Dialog。而且這東西可不只開發者會看到而已,User 也會,很兇。

雖然只有行數和錯誤訊息,在當時已經是很大的進步了。雖然現代有著全套 devtool 的我們可能覺得:這是要除什麼東西?感覺大概像父母說以前都吃地瓜粥,但活在現在的我心理卻想著:「何不食肉糜?」。

Netscape 死掉後。繼承者 Mozilla Foundation 決定全部重來,推出了 Mozilla Suite 的瀏覽器套裝軟體,而其中就有作為 Debug 工具的 venkman。但真的讓 debug 的體驗接近到現在的 devtool,要來到 2006 。在這年,狐火化作螢火蟲,點亮了開發者的世界:Firebug 作為 Firefox 的其中一個 extension 推出了。

Firebug 的推出顛覆了開發者的體驗,現在你常用的所有 devtool 工具幾乎都源自於 Firebug:

而我們文章的主角:console.log 也是從 Firebug 誕生的。在當時,各家瀏覽器都實作了自己的 console API,作為除錯使用。但我們常用的 console.log, console.info, console.warn, console.error 這些 API,是直到 Firebug 出現的。搭配工具中的 Console panel,讓前端可以直接一邊運作,一邊操作頁面的內容還有修改值,讓開發體驗上升了不只一個量級,也讓前端可以開發更加複雜的應用。

這個功能出現之後,不只開發者回不去,JS 的生態也進入了另一個時代:2008 年 chrome 內建了 devtool,涵蓋了大部分 firebug 有的功能。隨後其他瀏覽器也跟上這樣的方式,開始內建類似的 API 以及 devtool。然而這都只是各自實作而已,直到 WHATWG 在 2007 年,開始著手關於 console 的草案,但一直到2014 年,才將各瀏覽器的實作整理並完整加入到 HTML Living Standard 裡面。

到這裡,都還只是瀏覽器的世界而已。2009 年,Node.js 把 JavaScript 帶入 Server side 的世界。Node.js 配合瀏覽器既有的 debug 介面,也引入了 console API,降低原本會使用 JS 開發者進入後端的門檻。

JavaScript: Firefox and Firebug | I’d Rather Be Writing Blog and API doc course

|| 然後這裡就可以解釋,為什麼是 console,然後為什麼是 Log || 後面回頭講:那這個 API 到底是什麼?隨著 || 最後要回看一下,文章個部分不會太喧賓奪主,然後有些地方會不會太囉唆。 || 了解了 為什麼用 log 而不是用 前面提到的 print => 因為這個本來就是用在 browser 的 API, 語言層並沒有 console.log。node 有其實是因為要兼容 browser 的 API 讓他更好 || 上面這些東西原本是拉到最後講的,其實好像也覺得不錯。然後從為什麼命名這件事情帶到 host / runtime 。但這個東西在 node 裡面,也不是像其他語言那樣是 Library…?JS 的 library 到底是什麼? || 但中間要怎麼接?應該是要講一下 JS 的 hello world 的觀察。 || 1. JS 是腳本語言 => 這點好像不太重要 || 2. console 是從 browser 開始的,然後 node 跟上 || 即使是在 node || 3. print 是語言定義的 I/O,console.log 是平台提供的 Debug API <= 這個 || 在後面要接到文章中最想講的東西:runtime 試什麼,host 試什麼(但這篇文章先不講 engine,那是下一篇要講的東西) || 但前面的例子也是要解釋後面講的東西


但其實開發還是挺辛苦的,這樣還是很難用

Console 在 1990s 時期,常被用在各種開發的 IDE 裡面。作為顯示錯誤訊息的視窗,而 Netscape Navigator 在 1996 年就在瀏覽器加入類似的功能,稱作 JAvascript Console

當時還有另一個 JavaScript Debugger,而這也是現代 Devtool 的前身

而真正要標準化 console.log 這個方法,則來到了 2006 年的 firebug 了。

ref: A (brief) history of DevTools for the web. What am I missing? - 1993: The web launched with View Source and alert(). - 1999: console.log is introduced in Netscape’s JavaScript Console. - 2002:… | Jonathan Kuperman

為什麼使用 log ,而不是 print 呢?剛剛有提到,print 這個 method 是一種 I/O,除了輸出到 Terminal 上以外,並能作為其他程式的 input。但在瀏覽器上就不是這麼一回事了,只是把資訊呈現在開發工具而已,比起 print 已經有了既定的語意在程式開發領域,log 這個詞早已經有了「紀錄資訊」這個語意,更適合在 Console 這個情境。

雖然兩者都會在程式語言的的教學中出現,但使用的方法在意義上卻完全不同

print 是語言定義的 I/O,console.log 是平台提供的 Debug API

|| 這裡要加個圖

JS 是跑在 Browser 的,怎麼會有 CLI 呢?所以 JS 的 Hello world 不同,是顯示在瀏覽器 devTool 中的(先不提 Node.js 等其他 runtime)。但實際上 console.log 和其他語言的 print 有更根本性的差異。

|| 好像不應該先提這個,應該先提 print 這個東西到底本質在在 CLI 上輸出是幹嘛。然後再提 library 的東西, 再來可以導入 Browser 。這樣就可以講到「runtime」 是什麼。然後再講 JS 又在幹嘛

|| JS 的 hello world 要怎麼講?

首先,一個語言的「標準」(language standard)通常包含以下三個部分:

|| #todo 這裡加上一張圖

python 的 print ,就如同大部分的語言,是屬於標準函式庫定義的方法。大部分的語言的標準函式庫都會實作類似 Print 的方法。這類的方法實際上是將程式中的值格式化後寫入到 OS 的 stdout stream,而 stdout 預設會綁定在 Terminal 上,所以才會在 CLI 介面上看到 print 出來的值,但也可以透過 pipe 輸出到其他 input。

而 console.log 就不是這麼一回事了。我們可以從 Console 是什麼開始聊起。

#JS 以及「宿主環境」

print 以及 console.log 在 JS 中,除了平台以及命名由來的差異之外,背後更透露著 JS 從設計之初,到現在百花齊放之際,就從來沒有改變過的角色定位:

操作宿主環境 (Host environment)的內嵌語言

宿主環境這個專有名詞聽起來很陌生,但其實就是就是所謂的「平台」,下面各項你一定聽過

這麼多的平台,那 JS 的語言標準要如何像 python 一樣制定 console.log 這個方法的一致性?答案很簡單,ECMA-262 寫著

Therefore the core language is specified in this document apart from any particular host environment.

語言規範根本沒有提到 console.log,那是宿主環境的事情。

ECMAScript is now used to provide core scripting capabilities for a variety of host environments.

JS 的語言標準只提到語言的核心規則本身,還有少到幾乎算沒有的標準函式庫 (那一點的 Math… 算嗎?),當然也不包含 VM, runtime 什麼的。其他的功能,像我們在討論的 console.log 這些通通是 宿主環境 (host environment) 的實作。對於 JS 這個「語言」而言,只是透過一個全域的物件,調用它的 method 而已,JS 的語言曾並不定義實際的 console.log 有哪行為。就舉上面的平台為例:

但他們也不是沒有共通點,雖然各有些微的差異不同,但所有的 console.log 都維持著相同語意:所有的平台都把 console.log 作為輸出除錯資訊的方法。

延續上面提到的 console.log。FireBug 是當時的一套 web 的開發工具,能作為 Firefox 的 extension 使用。現代全域的 console ,以及常見的 console.log, console.info, console.error 等,都是在 Firebug 才被第一次標準化。在此之間,只有一些除錯工具以及部分瀏覽器有內建類似的工具,大部分還是使用 alert() 之類的方式來印出值。

隨後其他瀏覽器也跟上這樣的方式,像是 Chrome, Safari, Opera 的內建 Devtool。然而這都只是各自實作而已,直到 WHATWG 在 2007 年,開始著手關於 console 的草案,但直到2014 年,才將各瀏覽器的實作整理並完整加入到 HTML Living Standard 裡面。

到這裡都只是瀏覽器而已,2009 年,Node.js 把 JavaScript 帶入 Server side 的世界。Node.js 配合瀏覽器既有的 debug 介面,也引入了 console API,降低原本會使用 JS 開發者進入後端的門檻。

而這也是 JavaScript 開始侵略世界的起點。

|| 這段要再串回整篇 Hello world 的東西,還有另外一份是要串到下一個篇章:SJ 的

|| 這篇文章要提要 runtime 這件事情。不然很難接

|| 這篇文章沒有講到核心思想:覺得在這篇文章想開始帶入的概念是,JS 是極為抽象的語言(可以從 sepc 看到),並且依賴著宿主 (從 console log 那邊提到)。console.log 跟 瀏覽器的歷史是為了方面解釋上面那兩點的包裝

|| 上面這個東西可以從 JS 語言的 spec 去講

附註:

# Comments