import、import as、from import
December 20, 2021Toy Lang 的 lib 中定義了幾個模組。
Toy 語法
可以使用 import
匯入這些模組,例如,匯入 sys
模組:
import '/lib/sys'
sys.ownProperties().forEach(println)
println(sys.currentTimeMillis())
import
時必須加上 lib 的原因在於,〈Hello, Toy〉中將 TOY_MODUEL_PATH
設為 'toy_lang'
,而 import
的路徑起點是從 TOY_MODUEL_PATH
起算,就目前的設定來說,import '/lib/sys'
會使用 lib 中的 sys.toy 定義之模組。
每個模組在 Toy Lang 中,都是 Module
的實例,模組的主檔名會作為模組名稱,使用 import
時,模組的主檔名也會成為目前環境中的變數名稱。
取得 Module
的實例之後,因此透過它來取用模組中公開的變數、函式、類別等,上面的範例會顯示:
[currentTimeMillis,<Function currentTimeMillis>]
[loadedModules,<Function loadedModules>]
[unhandledExceptionHandler,<Function unhandledExceptionHandler>]
1536131150673
如果想要改變被匯入模組在當前環境中的變數名稱,可以使用 import as
。例如:
import '/lib/sys' as system
println(system.currentTimeMillis())
println(system)
import as
改變的是被匯入模組在目前環境中的名稱,而不是模組實例之名稱,例如以上會顯示:
import '/lib/sys' as system
println(system.currentTimeMillis())
println(system)
如果不想透過名稱來存取模組中公開的函式等元素,可以使用 from import
語句。例如:
from '/lib/sys' import currentTimeMillis
println(currentTimeMillis())
Toy 實作
Toy Lang 的模組有點像是 JavaScript 的情況,JavaScript 在 ES6 前並沒有模組管理功能,因此有著各種模擬模組的方式,也有各種模組管理程式庫。
也就是說,Toy Lang 的模組功能是最後才加上去的,而且特意仿造 JavaScript 在 ES6 前的情況,嚴格來說,並不是語言本身的一部份,它並沒有參與語法樹,雖然為了使用上比較方便,這是在語法上提供了 import
、import as
、from import
,也定義了模組對應的 Module
類別,然而除此之外,實作上比較像是個獨立的模組管理程式庫。
因為實作時的執行環境,都是在瀏覽器中,因此在取得模組檔案的部份,是透過 Fetch API,這部份主要都是集中在 js/module.js 之中。
簡單來說,Toy Lang 本來就是以一個 .toy 檔案為單位在解析執行,只要取得 .toy 檔案,同樣地經由剖析、建立語法樹、執行,就是模組的處理方式,這些定義在 Module
:
class Module {
constructor(fileName, moduleName, notImports, importers = []) {
this.fileName = fileName;
this.moduleName = moduleName;
this.notImports = notImports;
this.importers = importers;
}
... 略
}
notImports
指的是開頭 import
、import as
、from import
等以外的語句,因此,Toy Lang 中,import
、import as
、from import
只能寫在檔案開頭。
Module
主要負責使用 Fetch API 載入 .toy 檔案,剖析、建立語法樹、執行,最後得到 Toy Lang 中 Module
類別的實例,這些動作都是從 run
方法作為起點:
...
static run(fileName, code) {
const lines = tokenizer(code).tokenizableLines();
const notImports = notImportTokenizableLines(lines);
const imports = importTokenizableLines(lines);
if(imports.length !== 0) {
Promise.all(importPromises(fileName, imports))
.then(importers => new Module(fileName, moduleNameFrom(fileName), notImports, importers).play());
}
else {
new Module(fileName, moduleNameFrom(fileName), notImports).play();
}
}
我曾經為了瞭解 RequireJS 而寫了個 RequireJS-Toy 原型,在 Toy Lang 使用 Fetch API 載入 .toy 檔案的這部份,寫過該原型的幫助很大,在瀏覽器的環境,想要以非同步載入某些資源,又必須兼顧資源間順序的情況,RequireJS-Toy 是個可以參考的簡單專案。
除去 Module
使用 Fetch API 載入 .toy 檔案的部份,剖析、建立語法樹、執行的部份,基本上與沒有加入模組功能之前的作法,幾乎是沒有兩樣的。
因為關於 import
、import as
、from import
,完全是獨立在 ModuleImporter
之前進行:
class ModuleImporter {
constructor(sourceModule, type = 'default', name) {
this.sourceModule = sourceModule;
this.type = type;
this.name = name;
}
importTo(context) {
const moduleInstance = this.sourceModule.moduleInstance();
switch(this.type) {
case 'variableName': // from '...' import foo
context.variables.set(this.name, moduleInstance.properties.get(this.name));
break;
case 'all': // from '...' import *
Array.from(moduleInstance.properties.entries())
.forEach(entry => context.variables.set(entry[0], entry[1]));
break;
case 'moduleName': // import '...' as name
context.variables.set(this.name, moduleInstance);
break;
default: // import '....'
context.variables.set(this.sourceModule.moduleName, moduleInstance);
break;
}
}
}
說穿了也不難,就只是在目前環境物件中,必須設定哪些變數罷了;另外,每個模組在 Toy Lang 中,都是 Module
類別的實例,這是定義在 builtin\classes\module.js 之中:
class ModuleClass {}
ModuleClass.methods = new Map([
['name', func0('name', {
evaluate(context) {
const ctxNode = selfInternalNode(context);
return context.returned(new Primitive(ctxNode.moduleName));
}
})],
['toString', func0('toString', {
evaluate(context) {
const clzNode = self(context).clzNodeOfLang();
const ctxNode = selfInternalNode(context);
return context.returned(new Primitive(`<${clzNode.name} ${ctxNode.moduleName}>`));
}
})]
]);
主要就是提供模組名稱的 name
方法,並有 toString
方法實作,其他方法都是從 Object
繼承而來。