在script標籤寫export爲什麼會拋錯|type module import ES5 ES6 預處理 指令序言 JavaScript JS

今天我們進入到語法部分的學習。在講解具體的語法結構之前,這一堂課我首先要給你介紹一下 JavaScript 語法的一些基本規則。

腳本和模塊

首先,JavaScript 有兩種源文件,一種叫做腳本,一種叫做模塊。這個區分是在 ES6 引入了模塊機制開始的,在 ES5 和之前的版本中,就只有一種源文件類型(就只有腳本)。

腳本是可以由瀏覽器或者 node 環境引入執行的,而模塊只能由 JavaScript 代碼用 import 引入執行。

從概念上,我們可以認爲腳本具有主動性的 JavaScript 代碼段,是控制宿主完成一定任務的代碼;而模塊是被動性的 JavaScript 代碼段,是等待被調用的庫。

我們對標準中的語法產生式做一些對比,不難發現,實際上模塊和腳本之間的區別僅僅在於是否包含 import 和 export。

腳本是一種兼容之前的版本的定義,在這個模式下,沒有 import 就不需要處理加載“.js”文件問題。

現代瀏覽器可以支持用 script 標籤引入模塊或者腳本,如果要引入模塊,必須給 script 標籤添加 type=“module”。如果引入腳本,則不需要 type。

<script type="module" src="xxxxx.js"></script>

這樣,就回答了我們標題中的問題,script 標籤如果不加type=“module”,默認認爲我們加載的文件是腳本而非模塊,如果我們在腳本中寫了 export,當然會拋錯。

腳本中可以包含語句。模塊中可以包含三種內容:import 聲明,export 聲明和語句。普通語句我們會在下一課專門給你講解,下面我們就來講講 import 聲明和 export 聲明。

import 聲明

我們首先來介紹一下 import 聲明,import 聲明有兩種用法,一個是直接 import 一個模塊,另一個是帶 from 的 import,它能引入模塊裏的一些信息。

import "mod"; //引入一個模塊import v from "mod";  //把模塊默認的導出值放入變量v

直接 import 一個模塊,只是保證了這個模塊代碼被執行,引用它的模塊是無法獲得它的任何信息的。

帶 from 的 import 意思是引入模塊中的一部分信息,可以把它們變成本地的變量。

帶 from 的 import 細分又有三種用法,我們可以分別看下例子:

  • ​import x from "./a.js"​​ 引入模塊中導出的默認值。
  • ​import {a as x, modify} from "./a.js";​​ 引入模塊中的變量。
  • ​import * as x from "./a.js"​​ 把模塊中所有的變量以類似對象屬性的方式引入。

第一種方式還可以跟後兩種組合使用。

  • ​import d, {a as x, modify} from "./a.js"​
  • ​import d, * as x from "./a.js"​

語法要求不帶 as 的默認值永遠在最前。注意,這裏的變量實際上仍然可以受到原來模塊的控制。

我們看一個例子,假設有兩個模塊 a 和 b。我們在模塊 a 中聲明瞭變量和一個修改變量的函數,並且把它們導出。我們用 b 模塊導入了變量和修改變量的函數。

模塊 a:

export var a = 1;export function modify(){
    a = 2;
}

模塊 b:

import {a, modify} from "./a.js";console.log(a);modify();console.log(a);

當我們調用修改變量的函數後,b 模塊變量也跟着發生了改變。這說明導入與一般的賦值不同,導入後的變量只是改變了名字,它仍然與原來的變量是同一個。

export 聲明

我們再來說說 export 聲明。與 import 相對,export 聲明承擔的是導出的任務。

模塊中導出變量的方式有兩種,一種是獨立使用 export 聲明,另一種是直接在聲明型語句前添加 export 關鍵字。

獨立使用 export 聲明就是一個 export 關鍵字加上變量名列表,例如:

export {a, b, c};

我們也可以直接在聲明型語句前添加 export 關鍵字,這裏的 export 可以加在任何聲明性質的語句之前,整理如下:

  • var
  • function (含 async 和 generator)
  • class
  • let
  • const

export 還有一種特殊的用法,就是跟 default 聯合使用。export default 表示導出一個默認變量值,它可以用於 function 和 class。這裏導出的變量是沒有名稱的,可以使用​​import x from "./a.js"​​這樣的語法,在模塊中引入。

​export default​​ 還支持一種語法,後面跟一個表達式,例如:

var a = {};export default a;

但是,這裏的行爲跟導出變量是不一致的,這裏導出的是值,導出的就是普通變量 a 的值,以後 a 的變化與導出的值就無關了,修改變量 a,不會使得其他模塊中引入的 default 值發生改變。

在 import 語句前無法加入 export,但是我們可以直接使用 export from 語法。

export a from "a.js"

JavaScript 引擎除了執行腳本和模塊之外,還可以執行函數。而函數體跟腳本和模塊有一定的相似之處,所以接下來,給你講講函數體的相關知識。

函數體

執行函數的行爲通常是在 JavaScript 代碼執行時,註冊宿主環境的某些事件觸發的,而執行的過程,就是執行函數體(函數的花括號中間的部分)。

我們先看一個例子,感性地理解一下:

setTimeout(function(){    console.log("go go go");
}, 10000)

這段代碼通過 setTimeout 函數註冊了一個函數給宿主,當一定時間之後,宿主就會執行這個函數。

你還記得嗎,我們前面已經在運行時這部分講過,宿主會爲這樣的函數創建宏任務。

當我們學習了語法之後,我們可以認爲,宏任務中可能會執行的代碼包括“腳本 (script)”“模塊(module)”和“函數體(function body)”。正因爲這樣的相似性,我們把函數體也放到本課來講解。

函數體其實也是一個語句的列表。跟腳本和模塊比起來,函數體中的語句列表中多了 return 語句可以用。

函數體實際上有四種,下面,我來分別介紹一下。

普通函數體,例如:

function foo(){    //Function body}

異步函數體,例如:

async function foo(){    //Function body}

生成器函數體,例如:

function *foo(){    //Function body}

異步生成器函數體,例如:

async function *foo(){    //Function body}

上面四種函數體的區別在於:能否使用 await 或者 yield 語句。

關於函數體、模塊和腳本能使用的語句,我整理了一個表格,你可以參考一下:

講完了三種語法結構,我再來介紹兩個 JavaScript 語法的全局機制:預處理和指令序言。

這兩個機制對於我們解釋一些 JavaScript 的語法現象非常重要。不理解預處理機制我們就無法理解 var 等聲明類語句的行爲,而不理解指令序言,我們就無法解釋嚴格模式。

預處理

JavaScript 執行前,會對腳本、模塊和函數體中的語句進行預處理。預處理過程將會提前處理 var、函數聲明、class、const 和 let 這些語句,以確定其中變量的意義。

因爲一些歷史包袱,這一部分內容非常複雜,首先我們看一下 var 聲明。

var 聲明

var 聲明永遠作用於腳本、模塊和函數體這個級別,在預處理階段,不關心賦值的部分,只管在當前作用域聲明這個變量。

我們還是從實例來進行學習。

var a = 1;function foo() {    console.log(a);    var a = 2;
}foo();

這段代碼聲明瞭一個腳本級別的 a,又聲明瞭 foo 函數體級別的 a,我們注意到,函數體級的var出現在 ​​console.log​​ 語句之後。

但是預處理過程在執行之前,所以有函數體級的變量 a,就不會去訪問外層作用域中的變量 a 了,而函數體級的變量 a 此時還沒有賦值,所以是 undefined。我們再看一個情況:

var a = 1;function foo() {    console.log(a);    if(false) {        var a = 2;
    }
}foo();

這段代碼比上一段代碼在 var a = 2 之外多了一段 if,我們知道 if(false) 中的代碼永遠不會被執行,但是預處理階段並不管這個,var 的作用能夠穿透一切語句結構,它只認腳本、模塊和函數體三種語法結構。所以這裏結果跟前一段代碼完全一樣,我們會得到 undefined。

我們看下一個例子,我們在運行時部分講過類似的例子。

var a = 1;function foo() {    var o= {a:3}    with(o) {        var a = 2;
    }    console.log(o.a);    console.log(a);
}foo();

在這個例子中,我們引入了 with 語句,我們用 with(o) 創建了一個作用域,並把 o 對象加入詞法環境,在其中使用了 var a = 2;語句。

在預處理階段,只認var中聲明的變量,所以同樣爲 foo 的作用域創建了 a 這個變量,但是沒有賦值。

在執行階段,當執行到 var a = 2 時,作用域變成了 with 語句內,這時候的 a 被認爲訪問到了對象 o 的屬性 a,所以最終執行的結果,我們得到了 2 和 undefined。

這個行爲是 JavaScript 公認的設計失誤之一,一個語句中的 a 在預處理階段和執行階段被當做兩個不同的變量,嚴重違背了直覺,但是今天,在 JavaScript 設計原則“don’t break the web”之下,已經無法修正了,所以你需要特別注意。

因爲早年 JavaScript 沒有 let 和 const,只能用 var,又因爲 var 除了腳本和函數體都會穿透,人民羣衆發明了“立即執行的函數表達式(IIFE)”這一用法,用來產生作用域,例如:

for(var i = 0; i < 20; i ++) {    void function(i){        var div = document.createElement("div");
        div.innerHTML = i;
        div.onclick = function(){            console.log(i);
        }        document.body.appendChild(div);
    }(i);
}

這段代碼非常經典,常常在實際開發中見到,也經常被用作面試題,爲文檔添加了 20 個 div 元素,並且綁定了點擊事件,打印它們的序號。

我們通過 IIFE 在循環內構造了作用域,每次循環都產生一個新的環境記錄,這樣,每個 div 都能訪問到環境中的 i。

如果我們不用 IIFE:

for(var i = 0; i < 20; i ++) {    var div = document.createElement("div");
    div.innerHTML = i;
    div.onclick = function(){        console.log(i);
    }    document.body.appendChild(div);
}

這段代碼的結果將會是點每個 div 都打印 20,因爲全局只有一個 i,執行完循環後,i 變成了 20。

function 聲明

function 聲明的行爲原本跟 var 非常相似,但是在最新的 JavaScript 標準中,對它進行了一定的修改,這讓情況變得更加複雜了。

在全局(腳本、模塊和函數體),function 聲明表現跟 var 相似,不同之處在於,function 聲明不但在作用域中加入變量,還會給它賦值。

我們看一下 function 聲明的例子:

console.log(foo);function foo(){
}

這裏聲明瞭函數 foo,在聲明之前,我們用 console.log 打印函數 foo,我們可以發現,已經是函數 foo 的值了。

function 聲明出現在 if 等語句中的情況有點複雜,它仍然作用於腳本、模塊和函數體級別,在預處理階段,仍然會產生變量,它不再被提前賦值:

console.log(foo);if(true) {    function foo(){
    }
}

這段代碼得到 undefined。如果沒有函數聲明,則會拋出錯誤。

這說明 function 在預處理階段仍然發生了作用,在作用域中產生了變量,沒有產生賦值,賦值行爲發生在了執行階段。

出現在 if 等語句中的 function,在 if 創建的作用域中仍然會被提前,產生賦值效果,我們會在下一節課繼續討論。

class 聲明

class 聲明在全局的行爲跟 function 和 var 都不一樣。

在 class 聲明之前使用 class 名,會拋錯:

console.log(c);class c{
}

這段代碼我們試圖在 class 前打印變量 c,我們得到了個錯誤,這個行爲很像是 class 沒有預處理,但是實際上並非如此。

我們看個複雜一點的例子:

var c = 1;function foo(){    console.log(c);    class c {}
}foo();

這個例子中,我們把 class 放進了一個函數體中,在外層作用域中有變量 c。然後試圖在 class 之前打印 c。

執行後,我們看到,仍然拋出了錯誤,如果去掉 class 聲明,則會正常打印出 1,也就是說,出現在後面的 class 聲明影響了前面語句的結果。

這說明,class 聲明也是會被預處理的,它會在作用域中創建變量,並且要求訪問它時拋出錯誤。

class 的聲明作用不會穿透 if 等語句結構,所以只有寫在全局環境纔會有聲明作用,這部分我們將會在下一節課講解。

這樣的 class 設計比 function 和 var 更符合直覺,而且在遇到一些比較奇怪的用法時,傾向於拋出錯誤。

按照現代語言設計的評價標準,及早拋錯是好事,它能夠幫助我們儘量在開發階段就發現代碼的可能問題。

指令序言機制

腳本和模塊都支持一種特別的語法,叫做指令序言(Directive Prologs)。

這裏的指令序言最早是爲了 use strict 設計的,它規定了一種給 JavaScript 代碼添加元信息的方式。

"use strict";function f(){    console.log(this);
};
f.call(null);

這段代碼展示了嚴格模式的用法,我這裏定義了函數 f,f 中打印 this 值,然後用 call 的方法調用 f,傳入 null 作爲 this 值,我們可以看到最終結果是 null 原封不動地被當做 this 值打印了出來,這是嚴格模式的特徵。

如果我們去掉嚴格模式的指令需要,打印的結果將會變成 global。

"use strict"是 JavaScript 標準中規定的唯一一種指令序言,但是設計指令序言的目的是,留給 JavaScript 的引擎和實現者一些統一的表達方式,在靜態掃描時指定 JavaScript 代碼的一些特性。

例如,假設我們要設計一種聲明本文件不需要進行 lint 檢查的指令,我們可以這樣設計:

"no lint";"use strict";function doSth(){    //......}//......

JavaScript 的指令序言是隻有一個字符串直接量的表達式語句,它只能出現在腳本、模塊和函數體的最前面。

我們看兩個例子:

function doSth(){    //......}"use strict";var a = 1;//......

這個例子中,"use strict"沒有出現在最前,所以不是指令序言。

'use strict';function doSth(){    //......}var a = 1;//......

這個例子中,'use strict'是單引號,這不妨礙它仍然是指令序言。

結語

今天,我們一起進入了 JavaScript 的語法部分,在開始學習之前,我先介紹了一部分語法的基本規則。

我們首先介紹了 JavaScript 語法的全局結構,JavaScript 有兩種源文件,一種叫做腳本,一種叫做模塊。介紹完腳本和模塊的基礎概念,我們再來把它們往下分,腳本中可以包含語句。模塊中可以包含三種內容:import 聲明,export 聲明和語句。

最後,我介紹了兩個 JavaScript 語法的全局機制:預處理和指令序言。

最後,給你留一個小任務,我們試着用 babel,分析一段 JavaScript 的模塊代碼,並且找出它中間的所有 export 的變量。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章