JavaScript模块:export和import语法详解

2020-10-06  本文已影响0人  读行笔记

脚本和模块

概念

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

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

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

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

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

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代码更加严谨,更加安全,以避免像在预处理机制中发生的那些奇怪现象,这是大型项目必然需要的一种约束。

上一篇下一篇

猜你喜欢

热点阅读