JavaScript模块:export和import语法详解

脚本和模块

概念

在ES5和之前的版本中,JavaScript源代码只有一种类型:脚本。但是,从ES6开始,还加入了另一种源代码类型:模块。

从概念上,可以认为脚本是主动性的JavaScript代码段,是控制宿主完成特定任务的代码;而模块是被动性的JavaScript代码段,是等待被调用的库。

现代浏览器支持用script标签引入脚本或模块,但如果引入的是模块,则要加入type="module"

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

这样,一个JavaScript程序的代码结构如下:

  • 脚本
    • 语句
  • 模块
    • import声明
    • export声明
    • 语句

import

import声明表示引入某个模块,既可以引入整个模块中的内容,也可以引入部分内容,区别在于有没有from关键字。

// 引入整个模块
import "module1.js"
// 引入部分内容
import {Class1, Class2, function1} from "module2.js"
// 引入模块中的所有内容,并以类似类属性的方式调用。只有必要时才建议这么使用,因为可能可能会引入无用变量。
import * as x from "module3.js"

引入整个模块只能保证里面的代码被执行,无法获取里面的任何内容。相反,使用from可以引入模块中的一部分,并把它们变成本地变量。

另外,还有一种import写法:

import x from "module3.js"

这种写法引入的是模块中的默认值,x可自定义,默认值是和default搭配的export语句,后面有解释。

// 📃 m1.js
export var num = 1;

export function increaseNum(){
    num++;
    console.log("increased variable 'num'")
}

// 📃 m2.js
import {num, increaseNum} from "./m1.js"

console.log(num)    // 1
increaseNum()       // increased variable 'num'
console.log(num)    // 2

通过这个例子,我们知道导入的变量还是原来的变量,只是在修改名称之后放在了其他位置而已。在实际工作中,用这种方式在多个模块之间共享变量是一个可选的方案。

export

与声明连写

export和声明语句写在一起,比如:

// export an array
export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

// export a constant
export const MODULES_BECAME_STANDARD_YEAR = 2015;

// export a class
export class User {
  constructor(name) {
    this.name = name;
  }
}

与声明不连写

export和声明语句分开写,比如:

// 📃 say.js
function sayHi(user) {
  alert(`Hello, ${user}!`);
}

function sayBye(user) {
  alert(`Bye, ${user}!`);
}

export {sayHi, sayBye};

as

export也可以和as连用,给变量重命名,比如对于上面的代码,可以这么写:

// 📃 say.js
...
export {sayHi as hi, sayBye as bye};

// 📃 main.js
import * as say from './say.js';

say.hi('John');     // Hello, John!
say.bye('John');    // Bye, John!

default

defaultexport配合,表示导出一个默认变量值,可以和classfunction连用。

// 📃 user.js

// 方式1:直接添加default
export default class User {
  constructor(name) {
    this.name = name;
  }
}

// 方式2:声明和default分开写
class User {
  constructor(name) {
    this.name = name;
  }
}

export default User;
// or
export {User as default};

在这种场景中,import语句不需要使用大括号{ },而且可自定义变量值的名称。

Statement Named export Default export
export export class User {...} export default class User {...}
import import { User } from ... import User/MyUser/... from ...

在工程实践中,更建议一个模块中只包含一个变量,并作为默认变量值导出,同时结合文件结构组织代码,以形成良好的代码风格。

export ... from

export ... from表示从其他模块导出变量。这种方式适合将多个模块中的变量整合在一起,统一向外提供访问入口,能起到优化代码的作用。例如这种情况:

auth/
    index.js
    user.js
    helpers.js
    tests/
        login.js
    providers/
        github.js
        facebook.js
        ...

现在要把所有模块中的内容放在index.js中统一导出,既可以这样写:

import {login, logout} from './helpers.js';
export {login, logout};

// import default as User and export it
import User from './user.js';
export {User};
...

也可以这样写:

export {login, logout} from './helpers.js';
export {default as User} from './user.js';
...

显然,第二种方式比第一种方式简便多了。

预处理

预处理是指,在JavaScript引擎执行代码之前,会提前处理声明变量语句varletconst,函数声明function,类声明class,以明确所有变量的基本信息。

var

var声明永远作用于模块、脚本和函数体级别。在预处理阶段,不关心赋值部分,只管声明部分。

var a = 1;

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

foo();

在这个例子中,经过预处理之后得知,函数foo作用域内也声明a,所以不会访问外面的a,但是在执行到console.log时,还没有赋值,所以是undefined

如果给里面的声明语句加上控制语句if

var a = 1;

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

foo();

经过执行发现还是undefined,这是因为虽然if(false)里面的语句永远不会被执行,但是在预处理阶段并不管这些,var会穿透一切语句结构,所以结果和前面的一样。

function

functionvar类似,但是在新的JavaScript标准中,对其进行了一些修改,使其更加复杂。主要是function的声明在预处理阶段,不但会在作用域内加入变量,还会赋值。

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

经过执行验证,的确输出了函数值。如果再加入if

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

当加入控制语句if后,在预处理阶段只会声明变量,而不会赋值,所以得到了undefined

再看另一个例子:

console.log(b)
function b() { };
var b = 1 ;
console.log(b)

经过执行发现先打印函数值,然后打印1。这说明:在预处理阶段,function声明的优先级高于var

class

class声明在全局的行为和functionvar都不太一样。例如下面的例子:

console.log(c1)
class c1 {
    constructor(a) {
        this.a = a
    }
}

经过执行得知,在class声明之前就使用class,会报错。再来看看另一个例子:

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

同样,还是报错,但是去掉函数体内的class声明,则正常打印1。

这至少说明:函数体内的class声明语句经过了某些预处理,它会在作用域中创建变量,并且要求访问它时抛出错误。这样更符合我们一般的认知,如果还没有声明变量,那么应该更早地抛出错误信息。

指令序言

脚本和模块都支持一种特别的语法:指令序言,最早是为了use strict设计的,它规定了一种给JavaScript代码添加元信息的方式。

use strict

use strict表示JavaScript代码是在严格模式下执行,而非普通模式。顾名思义,在严格模式下,就是代码在执行时会被更加严格的规则来约束其行为。根据阮一峰老师的博客文章,严格模式有下面这些目的:

  • 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
  • 消除代码运行的一些不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的Javascript做好铺垫。

另外,严格模式有两种使用方式,一是在整个脚本中,另一个是在单个函数体内使用。

在整个脚本中使用时,必须放在文件第一行,否则无效。

'use strict';

console.log("this is on strict mode")
function f(){
    console.log(this);
};
f.call(null);   // null

在严格模式下,普通函数中的this将严格按照传入进去的值执行。而如果不在严格模式下,则为global

同样,在函数中使用严格模式,也必须将use strict放在第一行。

function useStrict() {
    "use strict";
    return this;
}
useStrict();    // undefined

no lint

no lint表示此文件不需要进行进行语法检查。

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

总结

JavaScript程序源代码可分为两种形式,一种是脚本,另一种是在ES6中才加入的模块。

模块的加入让代码的组织更加灵活,结合importexport可以灵活地控制变量作用域。import语句不但能够引入整个模块,让模块内的所有代码被执行,还可以引入模块中的部分内容作为本地变量来使用,而引入模块中的默认值是以值得形式引入的,也就是说,引入之后将和其他作用域没有关系。export语句既可以和声明变量语句连写,也可以分开写,当和default配合使用时,表示导出一个默认值。另外,export还支持从其他模块导出变量,主要为了将多个模块整合在一起,统一向外提供访问入口。

预处理机制是JavaScript引擎在执行代码之前,会对模块、脚本和函数体内的声明语句varfunctionclass进行处理,以明确变量的基本信息。对于不同的声明语句,预处理机制各不相同,需要单独记忆。理解这部分内容,对于理解JavaScript代码的某些执行逻辑,至关重要。

指令序言是一种为JavaScript代码添加元信息的方式,最早是为use strict设计的。严格模式use strict的出现,就是为了让JavaScript代码更加严谨,更加安全,以避免像在预处理机制中发生的那些奇怪现象,这是大型项目必然需要的一种约束。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350