<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                # Module ES6的Class只是面向對象編程的語法糖,升級了ES5的構造函數的原型鏈繼承的寫法,并沒有解決模塊化問題。Module功能就是為了解決這個問題而提出的。 歷史上,JavaScript一直沒有模塊(module)體系,無法將一個大程序拆分成互相依賴的小文件,再用簡單的方法拼裝起來。其他語言都有這項功能,比如Ruby的`require`、Python的`import`,甚至就連CSS都有`@import`,但是JavaScript任何這方面的支持都沒有,這對開發大型的、復雜的項目形成了巨大障礙。 在ES6之前,社區制定了一些模塊加載方案,最主要的有CommonJS和AMD兩種。前者用于服務器,后者用于瀏覽器。ES6在語言規格的層面上,實現了模塊功能,而且實現得相當簡單,完全可以取代現有的CommonJS和AMD規范,成為瀏覽器和服務器通用的模塊解決方案。 ES6模塊的設計思想,是盡量的靜態化,使得編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量。CommonJS和AMD模塊,都只能在運行時確定這些東西。比如,CommonJS模塊就是對象,輸入時必須查找對象屬性。 ~~~ // CommonJS模塊 let { stat, exists, readFile } = require('fs'); // 等同于 let _fs = require('fs'); let stat = _fs.stat, exists = _fs.exists, readfile = _fs.readfile; ~~~ 上面代碼的實質是整體加載`fs`模塊(即加載`fs`的所有方法),生成一個對象(`_fs`),然后再從這個對象上面讀取3個方法。這種加載稱為“運行時加載”,因為只有運行時才能得到這個對象,導致完全沒辦法在編譯時做“靜態優化”。 ES6模塊不是對象,而是通過`export`命令顯式指定輸出的代碼,輸入時也采用靜態命令的形式。 ~~~ // ES6模塊 import { stat, exists, readFile } from 'fs'; ~~~ 上面代碼的實質是從`fs`模塊加載3個方法,其他方法不加載。這種加載稱為“編譯時加載”,即ES6可以在編譯時就完成模塊加載,效率要比CommonJS模塊的加載方式高。當然,這也導致了沒法引用ES6模塊本身,因為它不是對象。 由于ES6模塊是編譯時加載,使得靜態分析成為可能。有了它,就能進一步拓寬JavaScript的語法,比如引入宏(macro)和類型檢驗(type system)這些只能靠靜態分析實現的功能。 除了靜態加載帶來的各種好處,ES6模塊還有以下好處。 * 不再需要UMD模塊格式了,將來服務器和瀏覽器都會支持ES6模塊格式。目前,通過各種工具庫,其實已經做到了這一點。 * 將來瀏覽器的新API就能用模塊格式提供,不再必要做成全局變量或者`navigator`對象的屬性。 * 不再需要對象作為命名空間(比如`Math`對象),未來這些功能可以通過模塊提供。 瀏覽器使用ES6模塊的語法如下。 ~~~ <script type="module" src="foo.js"></script> ~~~ 上面代碼在網頁中插入一個模塊`foo.js`,由于`type`屬性設為`module`,所以瀏覽器知道這是一個ES6模塊。 Node的默認模塊格式是CommonJS,目前還沒決定怎么支持ES6模塊。所以,只能通過Babel這樣的轉碼器,在Node里面使用ES6模塊。 ## 嚴格模式 ES6的模塊自動采用嚴格模式,不管你有沒有在模塊頭部加上`"use strict";`。 嚴格模式主要有以下限制。 * 變量必須聲明后再使用 * 函數的參數不能有同名屬性,否則報錯 * 不能使用`with`語句 * 不能對只讀屬性賦值,否則報錯 * 不能使用前綴0表示八進制數,否則報錯 * 不能刪除不可刪除的屬性,否則報錯 * 不能刪除變量`delete prop`,會報錯,只能刪除屬性`delete global[prop]` * `eval`不會在它的外層作用域引入變量 * `eval`和`arguments`不能被重新賦值 * `arguments`不會自動反映函數參數的變化 * 不能使用`arguments.callee` * 不能使用`arguments.caller` * 禁止`this`指向全局對象 * 不能使用`fn.caller`和`fn.arguments`獲取函數調用的堆棧 * 增加了保留字(比如`protected`、`static`和`interface`) 上面這些限制,模塊都必須遵守。由于嚴格模式是ES5引入的,不屬于ES6,所以請參閱相關ES5書籍,本書不再詳細介紹了。 ## export命令 模塊功能主要由兩個命令構成:`export`和`import`。`export`命令用于規定模塊的對外接口,`import`命令用于輸入其他模塊提供的功能。 一個模塊就是一個獨立的文件。該文件內部的所有變量,外部無法獲取。如果你希望外部能夠讀取模塊內部的某個變量,就必須使用`export`關鍵字輸出該變量。下面是一個JS文件,里面使用`export`命令輸出變量。 ~~~ // profile.js export var firstName = 'Michael'; export var lastName = 'Jackson'; export var year = 1958; ~~~ 上面代碼是`profile.js`文件,保存了用戶信息。ES6將其視為一個模塊,里面用`export`命令對外部輸出了三個變量。 `export`的寫法,除了像上面這樣,還有另外一種。 ~~~ // profile.js var firstName = 'Michael'; var lastName = 'Jackson'; var year = 1958; export {firstName, lastName, year}; ~~~ 上面代碼在`export`命令后面,使用大括號指定所要輸出的一組變量。它與前一種寫法(直接放置在`var`語句前)是等價的,但是應該優先考慮使用這種寫法。因為這樣就可以在腳本尾部,一眼看清楚輸出了哪些變量。 export命令除了輸出變量,還可以輸出函數或類(class)。 ~~~ export function multiply(x, y) { return x * y; }; ~~~ 上面代碼對外輸出一個函數`multiply`。 通常情況下,`export`輸出的變量就是本來的名字,但是可以使用`as`關鍵字重命名。 ~~~ function v1() { ... } function v2() { ... } export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion }; ~~~ 上面代碼使用`as`關鍵字,重命名了函數`v1`和`v2`的對外接口。重命名后,`v2`可以用不同的名字輸出兩次。 需要特別注意的是,`export`命令規定的是對外的接口,必須與模塊內部的變量建立一一對應關系。 ~~~ // 報錯 export 1; // 報錯 var m = 1; export m; ~~~ 上面兩種寫法都會報錯,因為沒有提供對外的接口。第一種寫法直接輸出1,第二種寫法通過變量`m`,還是直接輸出1。`1`只是一個值,不是接口。正確的寫法是下面這樣。 ~~~ // 寫法一 export var m = 1; // 寫法二 var m = 1; export {m}; // 寫法三 var n = 1; export {n as m}; ~~~ 上面三種寫法都是正確的,規定了對外的接口`m`。其他腳本可以通過這個接口,取到值`1`。它們的實質是,在接口名與模塊內部變量之間,建立了一一對應的關系。 同樣的,`function`和`class`的輸出,也必須遵守這樣的寫法。 ~~~ // 報錯 function f() {} export f; // 正確 export function f() {}; // 正確 function f() {} export {f}; ~~~ 另外,`export`語句輸出的接口,與其對應的值是動態綁定關系,即通過該接口,可以取到模塊內部實時的值。 ~~~ export var foo = 'bar'; setTimeout(() => foo = 'baz', 500); ~~~ 上面代碼輸出變量`foo`,值為`bar`,500毫秒之后變成`baz`。 這一點與CommonJS規范完全不同。CommonJS模塊輸出的是值的緩存,不存在動態更新,詳見下文《ES6模塊加載的實質》一節。 最后,`export`命令可以出現在模塊的任何位置,只要處于模塊頂層就可以。如果處于塊級作用域內,就會報錯,下一節的`import`命令也是如此。這是因為處于條件代碼塊之中,就沒法做靜態優化了,違背了ES6模塊的設計初衷。 ~~~ function foo() { export default 'bar' // SyntaxError } foo() ~~~ 上面代碼中,`export`語句放在函數之中,結果報錯。 ## import命令 使用`export`命令定義了模塊的對外接口以后,其他JS文件就可以通過`import`命令加載這個模塊(文件)。 ~~~ // main.js import {firstName, lastName, year} from './profile'; function setName(element) { element.textContent = firstName + ' ' + lastName; } ~~~ 上面代碼的`import`命令,就用于加載`profile.js`文件,并從中輸入變量。`import`命令接受一個對象(用大括號表示),里面指定要從其他模塊導入的變量名。大括號里面的變量名,必須與被導入模塊(`profile.js`)對外接口的名稱相同。 如果想為輸入的變量重新取一個名字,import命令要使用`as`關鍵字,將輸入的變量重命名。 ~~~ import { lastName as surname } from './profile'; ~~~ 注意,`import`命令具有提升效果,會提升到整個模塊的頭部,首先執行。 ~~~ foo(); import { foo } from 'my_module'; ~~~ 上面的代碼不會報錯,因為`import`的執行早于`foo`的調用。 如果在一個模塊之中,先輸入后輸出同一個模塊,`import`語句可以與`export`語句寫在一起。 ~~~ export { es6 as default } from './someModule'; // 等同于 import { es6 } from './someModule'; export default es6; ~~~ 上面代碼中,`export`和`import`語句可以結合在一起,寫成一行。但是從可讀性考慮,不建議采用這種寫法,而應該采用標準寫法。 另外,ES7有一個[提案](https://github.com/leebyron/ecmascript-more-export-from),簡化先輸入后輸出的寫法,拿掉輸出時的大括號。 ~~~ // 提案的寫法 export v from 'mod'; // 現行的寫法 export {v} from 'mod'; ~~~ `import`語句會執行所加載的模塊,因此可以有下面的寫法。 ~~~ import 'lodash'; ~~~ 上面代碼僅僅執行`lodash`模塊,但是不輸入任何值。 ## 模塊的整體加載 除了指定加載某個輸出值,還可以使用整體加載,即用星號(`*`)指定一個對象,所有輸出值都加載在這個對象上面。 下面是一個`circle.js`文件,它輸出兩個方法`area`和`circumference`。 ~~~ // circle.js export function area(radius) { return Math.PI * radius * radius; } export function circumference(radius) { return 2 * Math.PI * radius; } ~~~ 現在,加載這個模塊。 ~~~ // main.js import { area, circumference } from './circle'; console.log('圓面積:' + area(4)); console.log('圓周長:' + circumference(14)); ~~~ 上面寫法是逐一指定要加載的方法,整體加載的寫法如下。 ~~~ import * as circle from './circle'; console.log('圓面積:' + circle.area(4)); console.log('圓周長:' + circle.circumference(14)); ~~~ ## export default命令 從前面的例子可以看出,使用`import`命令的時候,用戶需要知道所要加載的變量名或函數名,否則無法加載。但是,用戶肯定希望快速上手,未必愿意閱讀文檔,去了解模塊有哪些屬性和方法。 為了給用戶提供方便,讓他們不用閱讀文檔就能加載模塊,就要用到`export default`命令,為模塊指定默認輸出。 ~~~ // export-default.js export default function () { console.log('foo'); } ~~~ 上面代碼是一個模塊文件`export-default.js`,它的默認輸出是一個函數。 其他模塊加載該模塊時,`import`命令可以為該匿名函數指定任意名字。 ~~~ // import-default.js import customName from './export-default'; customName(); // 'foo' ~~~ 上面代碼的`import`命令,可以用任意名稱指向`export-default.js`輸出的方法,這時就不需要知道原模塊輸出的函數名。需要注意的是,這時`import`命令后面,不使用大括號。 `export default`命令用在非匿名函數前,也是可以的。 ~~~ // export-default.js export default function foo() { console.log('foo'); } // 或者寫成 function foo() { console.log('foo'); } export default foo; ~~~ 上面代碼中,`foo`函數的函數名`foo`,在模塊外部是無效的。加載的時候,視同匿名函數加載。 下面比較一下默認輸出和正常輸出。 ~~~ // 輸出 export default function crc32() { // ... } // 輸入 import crc32 from 'crc32'; // 輸出 export function crc32() { // ... }; // 輸入 import {crc32} from 'crc32'; ~~~ 上面代碼的兩組寫法,第一組是使用`export default`時,對應的`import`語句不需要使用大括號;第二組是不使用`export default`時,對應的`import`語句需要使用大括號。 `export default`命令用于指定模塊的默認輸出。顯然,一個模塊只能有一個默認輸出,因此`export deault`命令只能使用一次。所以,`import`命令后面才不用加大括號,因為只可能對應一個方法。 本質上,`export default`就是輸出一個叫做`default`的變量或方法,然后系統允許你為它取任意名字。所以,下面的寫法是有效的。 ~~~ // modules.js function add(x, y) { return x * y; } export {add as default}; // 等同于 // export default add; // app.js import { default as xxx } from 'modules'; // 等同于 // import xxx from 'modules'; ~~~ 正是因為`export default`命令其實只是輸出一個叫做`default`的變量,所以它后面不能跟變量聲明語句。 ~~~ // 正確 export var a = 1; // 正確 var a = 1; export default a; // 錯誤 export default var a = 1; ~~~ 上面代碼中,`export default a`的含義是將變量`a`的值賦給變量`default`。所以,最后一種寫法會報錯。 有了`export default`命令,輸入模塊時就非常直觀了,以輸入jQuery模塊為例。 ~~~ import $ from 'jquery'; ~~~ 如果想在一條import語句中,同時輸入默認方法和其他變量,可以寫成下面這樣。 ~~~ import customName, { otherMethod } from './export-default'; ~~~ 如果要輸出默認的值,只需將值跟在`export default`之后即可。 ~~~ export default 42; ~~~ `export default`也可以用來輸出類。 ~~~ // MyClass.js export default class { ... } // main.js import MyClass from 'MyClass'; let o = new MyClass(); ~~~ ## 模塊的繼承 模塊之間也可以繼承。 假設有一個`circleplus`模塊,繼承了`circle`模塊。 ~~~ // circleplus.js export * from 'circle'; export var e = 2.71828182846; export default function(x) { return Math.exp(x); } ~~~ 上面代碼中的`export *`,表示再輸出`circle`模塊的所有屬性和方法。注意,`export *`命令會忽略`circle`模塊的`default`方法。然后,上面代碼又輸出了自定義的`e`變量和默認方法。 這時,也可以將`circle`的屬性或方法,改名后再輸出。 ~~~ // circleplus.js export { area as circleArea } from 'circle'; ~~~ 上面代碼表示,只輸出`circle`模塊的`area`方法,且將其改名為`circleArea`。 加載上面模塊的寫法如下。 ~~~ // main.js import * as math from 'circleplus'; import exp from 'circleplus'; console.log(exp(math.e)); ~~~ 上面代碼中的`import exp`表示,將`circleplus`模塊的默認方法加載為`exp`方法。 ## ES6模塊加載的實質 ES6模塊加載的機制,與CommonJS模塊完全不同。CommonJS模塊輸出的是一個值的拷貝,而ES6模塊輸出的是值的引用。 CommonJS模塊輸出的是被輸出值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。請看下面這個模塊文件`lib.js`的例子。 ~~~ // lib.js var counter = 3; function incCounter() { counter++; } module.exports = { counter: counter, incCounter: incCounter, }; ~~~ 上面代碼輸出內部變量`counter`和改寫這個變量的內部方法`incCounter`。然后,在`main.js`里面加載這個模塊。 ~~~ // main.js var mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 3 ~~~ 上面代碼說明,`lib.js`模塊加載以后,它的內部變化就影響不到輸出的`mod.counter`了。這是因為`mod.counter`是一個原始類型的值,會被緩存。除非寫成一個函數,才能得到內部變動后的值。 ~~~ // lib.js var counter = 3; function incCounter() { counter++; } module.exports = { get counter() { return counter }, incCounter: incCounter, }; ~~~ 上面代碼中,輸出的`counter`屬性實際上是一個取值器函數。現在再執行`main.js`,就可以正確讀取內部變量`counter`的變動了。 ~~~ $ node main.js 3 4 ~~~ ES6模塊的運行機制與CommonJS不一樣,它遇到模塊加載命令`import`時,不會去執行模塊,而是只生成一個動態的只讀引用。等到真的需要用到時,再到模塊里面去取值,換句話說,ES6的輸入有點像Unix系統的“符號連接”,原始值變了,`import`輸入的值也會跟著變。因此,ES6模塊是動態引用,并且不會緩存值,模塊里面的變量綁定其所在的模塊。 還是舉上面的例子。 ~~~ // lib.js export let counter = 3; export function incCounter() { counter++; } // main.js import { counter, incCounter } from './lib'; console.log(counter); // 3 incCounter(); console.log(counter); // 4 ~~~ 上面代碼說明,ES6模塊輸入的變量`counter`是活的,完全反應其所在模塊`lib.js`內部的變化。 再舉一個出現在`export`一節中的例子。 ~~~ // m1.js export var foo = 'bar'; setTimeout(() => foo = 'baz', 500); // m2.js import {foo} from './m1.js'; console.log(foo); setTimeout(() => console.log(foo), 500); ~~~ 上面代碼中,`m1.js`的變量`foo`,在剛加載時等于`bar`,過了500毫秒,又變為等于`baz`。 讓我們看看,`m2.js`能否正確讀取這個變化。 ~~~ $ babel-node m2.js bar baz ~~~ 上面代碼表明,ES6模塊不會緩存運行結果,而是動態地去被加載的模塊取值,并且變量總是綁定其所在的模塊。 由于ES6輸入的模塊變量,只是一個“符號連接”,所以這個變量是只讀的,對它進行重新賦值會報錯。 ~~~ // lib.js export let obj = {}; // main.js import { obj } from './lib'; obj.prop = 123; // OK obj = {}; // TypeError ~~~ 上面代碼中,`main.js`從`lib.js`輸入變量`obj`,可以對`obj`添加屬性,但是重新賦值就會報錯。因為變量`obj`指向的地址是只讀的,不能重新賦值,這就好比`main.js`創造了一個名為`obj`的const變量。 最后,`export`通過接口,輸出的是同一個值。不同的腳本加載這個接口,得到的都是同樣的實例。 ~~~ // mod.js function C() { this.sum = 0; this.add = function () { this.sum += 1; }; this.show = function () { console.log(this.sum); }; } export let c = new C(); ~~~ 上面的腳本`mod.js`,輸出的是一個`C`的實例。不同的腳本加載這個模塊,得到的都是同一個實例。 ~~~ // x.js import {c} from './mod'; c.add(); // y.js import {c} from './mod'; c.show(); // main.js import './x'; import './y'; ~~~ 現在執行`main.js`,輸出的是1。 ~~~ $ babel-node main.js 1 ~~~ 這就證明了`x.js`和`y.js`加載的都是`C`的同一個實例。 ## 循環加載 “循環加載”(circular dependency)指的是,`a`腳本的執行依賴`b`腳本,而`b`腳本的執行又依賴`a`腳本。 ~~~ // a.js var b = require('b'); // b.js var a = require('a'); ~~~ 通常,“循環加載”表示存在強耦合,如果處理不好,還可能導致遞歸加載,使得程序無法執行,因此應該避免出現。 但是實際上,這是很難避免的,尤其是依賴關系復雜的大項目,很容易出現`a`依賴`b`,`b`依賴`c`,`c`又依賴`a`這樣的情況。這意味著,模塊加載機制必須考慮“循環加載”的情況。 對于JavaScript語言來說,目前最常見的兩種模塊格式CommonJS和ES6,處理“循環加載”的方法是不一樣的,返回的結果也不一樣。 ### CommonJS模塊的加載原理 介紹ES6如何處理"循環加載"之前,先介紹目前最流行的CommonJS模塊格式的加載原理。 CommonJS的一個模塊,就是一個腳本文件。`require`命令第一次加載該腳本,就會執行整個腳本,然后在內存生成一個對象。 ~~~ { id: '...', exports: { ... }, loaded: true, ... } ~~~ 上面代碼就是Node內部加載模塊后生成的一個對象。該對象的`id`屬性是模塊名,`exports`屬性是模塊輸出的各個接口,`loaded`屬性是一個布爾值,表示該模塊的腳本是否執行完畢。其他還有很多屬性,這里都省略了。 以后需要用到這個模塊的時候,就會到`exports`屬性上面取值。即使再次執行`require`命令,也不會再次執行該模塊,而是到緩存之中取值。也就是說,CommonJS模塊無論加載多少次,都只會在第一次加載時運行一次,以后再加載,就返回第一次運行的結果,除非手動清除系統緩存。 ### CommonJS模塊的循環加載 CommonJS模塊的重要特性是加載時執行,即腳本代碼在`require`的時候,就會全部執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。 讓我們來看,Node[官方文檔](https://nodejs.org/api/modules.html#modules_cycles)里面的例子。腳本文件`a.js`代碼如下。 ~~~ exports.done = false; var b = require('./b.js'); console.log('在 a.js 之中,b.done = %j', b.done); exports.done = true; console.log('a.js 執行完畢'); ~~~ 上面代碼之中,`a.js`腳本先輸出一個`done`變量,然后加載另一個腳本文件`b.js`。注意,此時`a.js`代碼就停在這里,等待`b.js`執行完畢,再往下執行。 再看`b.js`的代碼。 ~~~ exports.done = false; var a = require('./a.js'); console.log('在 b.js 之中,a.done = %j', a.done); exports.done = true; console.log('b.js 執行完畢'); ~~~ 上面代碼之中,`b.js`執行到第二行,就會去加載`a.js`,這時,就發生了“循環加載”。系統會去`a.js`模塊對應對象的`exports`屬性取值,可是因為`a.js`還沒有執行完,從`exports`屬性只能取回已經執行的部分,而不是最后的值。 `a.js`已經執行的部分,只有一行。 ~~~ exports.done = false; ~~~ 因此,對于`b.js`來說,它從`a.js`只輸入一個變量`done`,值為`false`。 然后,`b.js`接著往下執行,等到全部執行完畢,再把執行權交還給`a.js`。于是,`a.js`接著往下執行,直到執行完畢。我們寫一個腳本`main.js`,驗證這個過程。 ~~~ var a = require('./a.js'); var b = require('./b.js'); console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done); ~~~ 執行`main.js`,運行結果如下。 ~~~ $ node main.js 在 b.js 之中,a.done = false b.js 執行完畢 在 a.js 之中,b.done = true a.js 執行完畢 在 main.js 之中, a.done=true, b.done=true ~~~ 上面的代碼證明了兩件事。一是,在`b.js`之中,`a.js`沒有執行完畢,只執行了第一行。二是,`main.js`執行到第二行時,不會再次執行`b.js`,而是輸出緩存的`b.js`的執行結果,即它的第四行。 ~~~ exports.done = true; ~~~ 總之,CommonJS輸入的是被輸出值的拷貝,不是引用。 另外,由于CommonJS模塊遇到循環加載時,返回的是當前已經執行的部分的值,而不是代碼全部執行后的值,兩者可能會有差異。所以,輸入變量的時候,必須非常小心。 ~~~ var a = require('a'); // 安全的寫法 var foo = require('a').foo; // 危險的寫法 exports.good = function (arg) { return a.foo('good', arg); // 使用的是 a.foo 的最新值 }; exports.bad = function (arg) { return foo('bad', arg); // 使用的是一個部分加載時的值 }; ~~~ 上面代碼中,如果發生循環加載,`require('a').foo`的值很可能后面會被改寫,改用`require('a')`會更保險一點。 ### ES6模塊的循環加載 ES6處理“循環加載”與CommonJS有本質的不同。ES6模塊是動態引用,如果使用`import`從一個模塊加載變量(即`import foo from 'foo'`),那些變量不會被緩存,而是成為一個指向被加載模塊的引用,需要開發者自己保證,真正取值的時候能夠取到值。 請看下面這個例子。 ~~~ // a.js如下 import {bar} from './b.js'; console.log('a.js'); console.log(bar); export let foo = 'foo'; // b.js import {foo} from './a.js'; console.log('b.js'); console.log(foo); export let bar = 'bar'; ~~~ 上面代碼中,`a.js`加載`b.js`,`b.js`又加載`a.js`,構成循環加載。執行`a.js`,結果如下。 ~~~ $ babel-node a.js b.js undefined a.js bar ~~~ 上面代碼中,由于`a.js`的第一行是加載`b.js`,所以先執行的是`b.js`。而`b.js`的第一行又是加載`a.js`,這時由于`a.js`已經開始執行了,所以不會重復執行,而是繼續往下執行`b.js`,所以第一行輸出的是`b.js`。 接著,`b.js`要打印變量`foo`,這時`a.js`還沒執行完,取不到`foo`的值,導致打印出來是`undefined`。`b.js`執行完,開始執行`a.js`,這時就一切正常了。 再看一個稍微復雜的例子(摘自 Dr. Axel Rauschmayer 的[《Exploring ES6》](http://exploringjs.com/es6/ch_modules.html))。 ~~~ // a.js import {bar} from './b.js'; export function foo() { console.log('foo'); bar(); console.log('執行完畢'); } foo(); // b.js import {foo} from './a.js'; export function bar() { console.log('bar'); if (Math.random() > 0.5) { foo(); } } ~~~ 按照CommonJS規范,上面的代碼是沒法執行的。`a`先加載`b`,然后`b`又加載`a`,這時`a`還沒有任何執行結果,所以輸出結果為`null`,即對于`b.js`來說,變量`foo`的值等于`null`,后面的`foo()`就會報錯。 但是,ES6可以執行上面的代碼。 ~~~ $ babel-node a.js foo bar 執行完畢 // 執行結果也有可能是 foo bar foo bar 執行完畢 執行完畢 ~~~ 上面代碼中,`a.js`之所以能夠執行,原因就在于ES6加載的變量,都是動態引用其所在的模塊。只要引用存在,代碼就能執行。 下面,我們詳細分析這段代碼的運行過程。 ~~~ // a.js // 這一行建立一個引用, // 從`b.js`引用`bar` import {bar} from './b.js'; export function foo() { // 執行時第一行輸出 foo console.log('foo'); // 到 b.js 執行 bar bar(); console.log('執行完畢'); } foo(); // b.js // 建立`a.js`的`foo`引用 import {foo} from './a.js'; export function bar() { // 執行時,第二行輸出 bar console.log('bar'); // 遞歸執行 foo,一旦隨機數 // 小于等于0.5,就停止執行 if (Math.random() > 0.5) { foo(); } } ~~~ 我們再來看ES6模塊加載器[SystemJS](https://github.com/ModuleLoader/es6-module-loader/blob/master/docs/circular-references-bindings.md)給出的一個例子。 ~~~ // even.js import { odd } from './odd' export var counter = 0; export function even(n) { counter++; return n == 0 || odd(n - 1); } // odd.js import { even } from './even'; export function odd(n) { return n != 0 && even(n - 1); } ~~~ 上面代碼中,`even.js`里面的函數`even`有一個參數`n`,只要不等于0,就會減去1,傳入加載的`odd()`。`odd.js`也會做類似操作。 運行上面這段代碼,結果如下。 ~~~ $ babel-node > import * as m from './even.js'; > m.even(10); true > m.counter 6 > m.even(20) true > m.counter 17 ~~~ 上面代碼中,參數`n`從10變為0的過程中,`even()`一共會執行6次,所以變量`counter`等于6。第二次調用`even()`時,參數`n`從20變為0,`even()`一共會執行11次,加上前面的6次,所以變量`counter`等于17。 這個例子要是改寫成CommonJS,就根本無法執行,會報錯。 ~~~ // even.js var odd = require('./odd'); var counter = 0; exports.counter = counter; exports.even = function(n) { counter++; return n == 0 || odd(n - 1); } // odd.js var even = require('./even').even; module.exports = function(n) { return n != 0 && even(n - 1); } ~~~ 上面代碼中,`even.js`加載`odd.js`,而`odd.js`又去加載`even.js`,形成“循環加載”。這時,執行引擎就會輸出`even.js`已經執行的部分(不存在任何結果),所以在`odd.js`之中,變量`even`等于`null`,等到后面調用`even(n-1)`就會報錯。 ~~~ $ node > var m = require('./even'); > m.even(10) TypeError: even is not a function ~~~ ## 跨模塊常量 上面說過,`const`聲明的常量只在當前代碼塊有效。如果想設置跨模塊的常量(即跨多個文件),可以采用下面的寫法。 ~~~ // constants.js 模塊 export const A = 1; export const B = 3; export const C = 4; // test1.js 模塊 import * as constants from './constants'; console.log(constants.A); // 1 console.log(constants.B); // 3 // test2.js 模塊 import {A, B} from './constants'; console.log(A); // 1 console.log(B); // 3 ~~~ ## ES6模塊的轉碼 瀏覽器目前還不支持ES6模塊,為了現在就能使用,可以將轉為ES5的寫法。除了Babel可以用來轉碼之外,還有以下兩個方法,也可以用來轉碼。 ### ES6 module transpiler [ES6 module transpiler](https://github.com/esnext/es6-module-transpiler)是square公司開源的一個轉碼器,可以將ES6模塊轉為CommonJS模塊或AMD模塊的寫法,從而在瀏覽器中使用。 首先,安裝這個轉瑪器。 ~~~ $ npm install -g es6-module-transpiler ~~~ 然后,使用`compile-modules convert`命令,將ES6模塊文件轉碼。 ~~~ $ compile-modules convert file1.js file2.js ~~~ `-o`參數可以指定轉碼后的文件名。 ~~~ $ compile-modules convert -o out.js file1.js ~~~ ### SystemJS 另一種解決方法是使用[SystemJS](https://github.com/systemjs/systemjs)。它是一個墊片庫(polyfill),可以在瀏覽器內加載ES6模塊、AMD模塊和CommonJS模塊,將其轉為ES5格式。它在后臺調用的是Google的Traceur轉碼器。 使用時,先在網頁內載入system.js文件。 ~~~ <script src="system.js"></script> ~~~ 然后,使用`System.import`方法加載模塊文件。 ~~~ <script> System.import('./app.js'); </script> ~~~ 上面代碼中的`./app`,指的是當前目錄下的app.js文件。它可以是ES6模塊文件,`System.import`會自動將其轉碼。 需要注意的是,`System.import`使用異步加載,返回一個Promise對象,可以針對這個對象編程。下面是一個模塊文件。 ~~~ // app/es6-file.js: export class q { constructor() { this.es6 = 'hello'; } } ~~~ 然后,在網頁內加載這個模塊文件。 ~~~ <script> System.import('app/es6-file').then(function(m) { console.log(new m.q().es6); // hello }); </script> ~~~ 上面代碼中,`System.import`方法返回的是一個Promise對象,所以可以用then方法指定回調函數。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看