javascript中爲何咱們不能直接使用export?

相信不少人最開始時都有過這樣的疑問
假如個人項目目錄下有一個 index.html, index.js 因而我像這樣寫html

<script src="index.js"></script>

在瀏覽器之間打開index.html,發現
圖片描述node

這究竟是爲何?爲何連chrome瀏覽器居然還不徹底支持es6的語法?
其實,ES6以前已經出現了js模塊加載的方案,最主要的是CommonJS和AMD規範。commonjs主要應用於服務器,實現同步加載,如nodejs。AMD規範應用於瀏覽器,如requirejs,爲異步加載。同時還有CMD規範,爲同步加載方案如seaJS。
ES6在語言規格的層面上,實現了模塊功能,並且實現得至關簡單,徹底能夠取代現有的CommonJS和AMD規範,成爲瀏覽器和服務器通用的模塊解決方案。話有回到咱們剛纔的問題 '爲何chrome瀏覽器居然還不徹底支持es6的語法'es6

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

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

從概念上,咱們能夠認爲腳本具備主動性的JavaScript代碼段,是控制宿主完成必定任務的代碼;而模塊是被動性的JavaScript代碼段,是等待被調用的庫。chrome

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

腳本是一種兼容以前的版本的定義,在這個模式下,沒有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值發生改變。

說到這裏,就來大體說說export和export default的區別

1.export與export default都可用於導出常量、函數、文件、模塊等
2.在一個文件或模塊中,export、import能夠有多個,export default僅有一個
3.經過export方式導出,在導入時要加{ },export default則不須要

或者咱們能夠這樣理解,export default的本質其實就是講後面的值付給default變量,而後你能夠爲它取你想要的變量

因此
export default 1   // 執行
export 1 // 報錯

第二行報錯正式是由於沒有指定對外的接口,而第一句指定爲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(){
    //
}
異步函數體,例如:
async function foo(){
    //
}
生成器函數體,例如:
function *foo(){
    //
}
異步生成器函數體,例如:
async function *foo(){
    //
}

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

說完了三種語法結構,再來介紹下JavaScript語法的全局機制(非嚴格模式):預處理。
這對於咱們解釋一些JavaScript的語法現象很是重要。不理解預處理機制咱們就沒法理解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更符合直覺,並且在遇到一些比較奇怪的用法時,傾向於拋出錯誤。

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

針對以上問題以及一些不嚴謹的問題和一些引擎難以優化的錯誤,出現了嚴格模式

設立"嚴格模式"的目的,主要有如下幾個:

  - 消除Javascript語法的一些不合理、不嚴謹之處,減小一些怪異行爲;

  - 消除代碼運行的一些不安全之處,保證代碼運行的安全;

  - 提升編譯器效率,增長運行速度;

  - 爲將來新版本的Javascript作好鋪墊。

其中 ES6 的模塊自動採用嚴格模式,無論你有沒有在模塊頭部加上"use strict";

至於日常開發時咱們到底要不要使用嚴格模式以及包括要不要使用typescript?每一個人都有每一個人的觀點!那麼,在開發中你是否推薦用嚴格模式來'約束'你的代碼及風格呢?

相關文章
相關標籤/搜索