es6 快速入门 系列 —— 模块

其他章节请看:

es6 快速入门 系列

模块

es6 以前,每个 javascript 都共享这一个全局作用域,随着代码量的增加,容易引发一些问题,比如命名冲突。

其他语言有包这样的概念来定义作用域,es6 的一个目标是解决作用域问题,也为了使 javascript 应用程序显得有序,于是引入了模块。

Tip:模块化开发规范有amd、commonjs等,而 es6 module 属于官方出品

准备环境

笔者提供了一个环境(来自”初步认识 webpack“一文),方便对下面介绍的语法进行测试、验证和学习。

项目结构如下:

es6-module        
  - src                 // 项目源码
    - index.html        // 页面模板
    - index.js          // 入口
  - package.json        // 存放了项目依赖的包
  - webpack.config.js   // webpack配置文件

src中的代码如下:

// index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=`, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <p>请查看控制台</p>
</body>
</html>

// index.js
console.log('我是入口')

package.json:

{
  "name": "webpack-example2",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "html-webpack-plugin": "^4.5.2",
    "webpack": "^4.46.0",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.2"
  }
}

webpack.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
        template: 'src/index.html'
    })
  ],
  mode: 'development',
  devServer: {
    open: true,
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000,
  },
};

在 es6-module 目录下运行项目:

// 安装项目依赖的包
> npm i
// 启动服务
> npm run dev

启动服务器后,浏览器会自动打开页面,如果看到”请查看控制台“,说明环境已准备就绪。

什么是模块

模块是自动运行在严格模式下并且没有办法退出运行的 javascript 代码。在模块顶部创建的变量不会自动被添加到全局作用域,仅在这个模块的顶级作用域中。

模块如果需要提供一些接口给其他模块使用,则可以通过 export 关键字导出;其他模块则可以通过 import 关键字导入其他模块

模块的真正魔力是仅导出和导入你需要的绑定,而不是将所用的东西都放到一个文件。只有很好的理解导出和导入才能理解模块与脚本的区别

Tip: 脚本,就是任何不是模块的 javascript 代码;模块顶部的 this 的值是 undefined

导出的基本语法

可以使用 export 关键字将一部分代码暴露给其他模块,最简单的方法,可以将 export 放在任何变量、函数或类声明的前面,就像这样:

// 导出数据
export var name = 'ph'
export let age = 18

// 导出函数
export function sum(v1, v2){
    return v1 + v2
}

// 导出类
export class Dog{
    constructor(name, color, age){
        this.name = name;
        this.color = color;
        this.age = age;
    }
    toString(){
        return `name=${this.name} color=${this.color} age=${this.age}`
    }
}

// 这个函数是模块私有的
function subtract(v1, v2){
    return v1 - v2;
}

除了 export 关键字,每一个声明与脚本中的一模一样。因为导出的函数、类等声明都需要一个名称,除非用 default 关键字,否则不能匿名导出函数或类。

// 正确。使用 default 匿名导出
export default function(v1, v2){
    return v1 * v2
}

// 正确
export default 1

// 错误
export 1

导入的基本语法

从模块中导出的功能可以通过 import 关键字在另一个模块中访问,import 语句中的两个部分是:要导入的标识符和标识符应当从哪个模块中导入。基本形式是:

import {name, age} from './x.js'

当从模块中导入一个绑定,就好像使用 const 定义的一样。结果是你无法重新定义另一个同名变量:

import moduleX from './x.js'

// 报错
moduleX = 4

// 报错
let moduleX = 4 

导入整个模块:

// 模块(z.js)
export var name = 'ph'
export let age = 18
export default 'man'

// 导入整个模块
import * as moduleZ from './z.js'
// [["name","ph"],["age",18],["default","man"]]
console.log(JSON.stringify(Object.entries(moduleZ)))

不管在 import 语句中把一个模块写了多少次,该模块将只执行一次:

// 模块(z.js)
export var name = 'ph'
export let age = 18
console.log('module z')
export default 'man'

// 重复导入模块c
import {name} from './z.js'
import {age} from './z.js'
console.log([name,age])

控制台只会输出一次module z

exportimport 必须在其他语句和函数之外使用:

if(flag){
    import {name} from './z.js' // 语法错误
}

模块语法存在的一个原因是要让 javascript 引擎静态的确定哪些可以导出,因此,只能在模块顶部使用 exportimport

导入绑定的一个微妙怪异之处:import 为变量、函数、类创建的是只读绑定,而不是像正常变量一样简单地引用原始绑定。

// 模块(x.js)
export let name = 'ph'
export function setName(newName){
    name = newName
}

// 模块(y.js)
import {name, setName} from './x.js'
// ph
console.log(name)
// 此更改会自动在导入的 name 绑定上体现
// 原因是 name 是导出的 name 标识符的本地名称
setName('lj')
// lj
console.log(name)

// webpack 中没有抛出错误
name = 3
// 3
console.log(name)

导出和导入时重命名

如果要使用不同的名字导入一个函数,可以使用 as 关键字,代码示意如下:

// 解构导出并重命名
export const { age, sum: add } = o;

import {sum as add} from './x.js';

import {default as DefaultExport} from './x.js'  

模块的默认值

模块的默认值指的是通过 default 关键字指定的单个变量、函数或类。就像这样:

export default function(v1, v2){
    return v1 + v2
}

只能为每个模块设置一个默认的导出值:

// 语法错误
export default 1
export default 2

如果想通过一条语句同时指定多个导出,包括默认导出,下面这个语法非常有用:

const age = 18
function sum(v1, v2){
    return v1 + v2
}
export {sum as default, age}

可以通过一条语句导入所有导出的绑定,包括默认值:

import sum, {age} from './x.js'

用逗号将默认的本地名称与大括号包裹的非默认值分割开。默认值必须排在非默认值前面:

// 报错
import {age}, sum from './x.js'

与导出默认值一样,也可以在导入默认值时使用重命名语法:

import {default as sum, age} from './x.js'

这段代码中,默认值被重命名为 sum,并且还导入了 age。

重新导出一个绑定

在一个模块中导入 sum,又重新导出 sum,就像这样:

import {sum} from './x.js'
export {sum}

只用一条语句同样可以完成上面的工作:

export {sum} from './x.js'

这种形式的 export 在指定的模块中查找 sum 声明,然后将其导出。理解了这个语法后,我们在这种形式下,可以重命名或者导出一切。就像这样:

// 重命名导出
export {sum as add} from './x.js'

// 导出一切
export * from './x.js'

:测试发现此语法(export * from './x.js')没有导出默认值

无绑定导入

有些模块可能不导出任何东西。可能只修改全局作用域中的对象。

虽然模块中顶层的变量、函数和类不会自动出现在全局作用域中,但这并不意味着模块无法访问全局作用域,比如我在某模块中给数组添加一个变量,其他模块没有引入该模块,也是可以访问到数组新添加的变量,请看示例:

// x.js
Array.prototype.myFlag = 'aaron'

// y.js
console.log(Array.prototype.myFlag)

// index.js
// 由于不导出任何东西,因此可以使用简化的导入操作
import './x.js'
// 模块 y 输出:aaron
import './y.js'

Tip: 无绑定导入最有可能用于 polyfill 和 shim

综合测试

我们将上文介绍的知识点综合测试和验证一下。

入口文件(src/index.js):

// 无绑定导入
import './a.js'

// 重复导入。只会输出一次”i am moduleC“
import './c.js'
import './c.js'

import './d.js'

其他文件内容如下:

a.js:

// 用逗号将默认的本地名称与大括号包裹的非默认值分割开。默认值必须排在非默认值前面
import multiplication, {name,age,sum,address,tel,Dog,SEX,HOUSE} from './b.js'

// console.table,将数据以表格的形式显示
console.table({
    // name 和 age:导出数据
    name,
    age,
    // 导出函数
    'sum(3,3)': sum(3, 3),
    // address 和 tel:一次导出多个
    address,
    tel,
    "new Dog('乐乐', '黑色', 4)": new Dog('乐乐', '黑色', 4).toString(),
    // 默认导出
    'multiplication(3, 3)': multiplication(3, 3),
    // 重命名导出
    SEX,
    // 解构导出
    HOUSE
})

b.js:

// 导出数据
export var name = 'ph'
export let age = 18
const sex = 'man'

// 解构导出
export const { house: HOUSE} = {house:'别墅'}

// 一次导出多个
export let address='长沙', tel='123456789'

// 导出函数
export function sum(v1, v2){
    return v1 + v2
}

// 导出类
class Dog{
    constructor(name, color, age){
        this.name = name;
        this.color = color;
        this.age = age;
    }
    toString(){
        return `name=${this.name} color=${this.color} age=${this.age}`
    }
}

// 这个函数是模块私有的
function subtract(v1, v2){
    return v1 - v2;
}

// 定义一个函数
function multiplication(v1, v2){
    return v1 * v2
}

// 导出模块集合
export {Dog}
// 重命名导出
export {sex as SEX}
// 默认导出
export default multiplication

// 报错
// export multiplication

c.js:

// 导入整个模块
import * as moduleB from './b.js'

const {name, HOUSE, default: multiplication} = moduleB;
console.log('i am moduleC')
// ["ph", "别墅", 9]
console.log([name, HOUSE, multiplication(3,3)])

d.js:

import * as moduleE from './e.js'
console.log(`moduleE.default=${moduleE.default}`)

e.js:

// 此语法没有导出模块b的默认值
export * from './b.js'
export default 'moduleE default value'

其他章节请看:

es6 快速入门 系列

posted @ 2021-06-15 09:09  彭加李  阅读(332)  评论(2编辑  收藏  举报