JavaScript 模块封装

前言介绍

  在最早的时候JavaScript这门语言其实是并没有模块这一概念,但是随着时间的推移与技术的发展将一些复用性较强的代码封装成模块变成了必要的趋势。

  在这篇文章中主要介绍原生的 JavaScript封装的几种手段以及新增的 ES6 Module的语法,来实现模块封装。

  并且会简单的使用WebpackEs6代码向后兼容。

引入问题

  以下有两个Js文件,如果不采取任何封装手段直接导入会导致window环境污染。

  并且,如果文件中有相同名字的变量或函数会发生命名冲突,因为它们都是放在全局作用域window对象中的。

<script src="./js_m1.js"></script>
<script src="./js_m2.js"></script>
<script>

"use strict";

// 这是由于js_m2后引入,所以js_m1的同名变量以及函数都被覆盖掉了。

console.log(module_name); // js_m2

show(); // js_m2.show

</script>
var module_name = "js_m1";

function show(){
console.log("js_m1.show");
}
var module_name = "js_m2";

function show(){
console.log("js_m2.show");
}

简单解决

IIFE封装


  针对上述问题,采取函数的闭包及作用域特性我们为每个模块封装一个作用域。

  第一步:进行自执行函数包裹代码封装出局部作用域

  第二步:向外部暴露接口,为window对象添加新的对象

<script src="./js_m1.js"></script>
<script src="./js_m2.js"></script>
<script>

"use strict";

console.log(js_m1.module_name); // js_m1

js_m1.show(); // js_m1.show

console.log(js_m2.module_name); // js_m2

js_m2.show(); // js_m2.show

</script>
(function () {

var module_name = "js_m1";

function show() {
console.log("js_m1.show");
}

window.js_m1 = { module_name: module_name, show: show };
// 在es6中,可简写为 { module_name , show }
}())
(function () {

var module_name = "js_m2";

function show() {
console.log("js_m2.show");
}

window.js_m2 = { module_name: module_name, show: show };
// 在es6中,可简写为 { module_name , show }
}())

Es6块级封装


  在Es6之前,由于没有出现块级作用域的概念,那时候大家都使用上面的方式进行封装。

  在当Es6的块级作用域出现之后,又诞生出了新的封装方式即块级作用域封装。

  和IIFE封装相同,都是利用作用域的特性进行封装。

  注意一点,块级作用域只对letconst声明有效。

<script src="./js_m1.js"></script>
<script src="./js_m2.js"></script>
<script>

"use strict";

console.log(js_m1.module_name); // js_m1

js_m1.show(); // js_m1.show

console.log(js_m2.module_name); // js_m2

js_m2.show(); // js_m2.show

</script>
{
let module_name = "js_m1";

let show = function () {
console.log("js_m1.show");
}

window.js_m1 = { module_name, show };

}
{
let module_name = "js_m2"; let show = function () {
console.log("js_m2.show");
} window.js_m2 = { module_name, show }; }

Es6 module 语法

  上面的两种方式虽然都能达到模块封装的效果,但是我们依然有更好的选择。

  下面将介绍极力推荐的Es6 module语法进行导入。

  学习Es6 module从以下三个方面来入手:

  1.模块标签及其特性

  2.导出

  3.导入

模块标签

  要想使用Es6 module语法导入模块,必须使用模块标签来引入Js文件。

  模块标签与普通的<script>标签具有一些不太一样的地方,下面会从各个方面逐一进行介绍。

声明标签


  将<script>标签添加上type="module"的属性。

<script type="module"></script>

导入路径


  在浏览器中引用模块必须添加路径如./ ,但在打包工具如webpack中则不需要,因为他们有自己的存放方式。

  总而言之,即使是在当前目录也要添加上./,不可以进行省略。

  这也是推荐的一种引入文件方式,不管是何种语言中都推荐引入文件时不进行路径省略。

  正确的导入路径

<script type="module" src="./js_m1.js"></script>
<script type="module" src="./js_m2.js"></script>

  错误的导入路径

<script type="module" src="js_m1.js"></script>  // 不可省略!省略就会抛出异常
<script type="module" src="js_m2.js"></script>

延迟解析


  所谓延迟解析是指在模块标签中的代码会提到HTML代码以及嵌入式的<script>标签后才进行执行。

  注意看下面的示例,编码时模块标签在普通的<script>之上,但是结果却相反。

<script type="module">

    console.log("<script type='module'> code run...");

</script>

<script>

    "use strict";

    console.log("<script> code run...");

</script>

严格模式


  模块标签中的所有代码都是按严格模式运行的,请注意变量名的声明以及this指向问题,同时还有解构赋值等等。

<script type="module">

    username = "云崖";  // 抛出异常,未声明

</script>
<script type="module">

    let obj = {
show() {
console.log(this); // {show: ƒ}
(function () { console.log(this); }()) // undefined 严格模式下为undefined ,普通模式下为window对象
}
}; obj.show(); </script>

作用域


  每个模块标签中的代码都会为其创建一个专属的作用域,禁止相互之间进行访问。

  而普通的<script>标签中的代码全部在全局作用域下执行。

<script>

    let m1 = "m1...";

</script>

<script>

    console.log(m1);  // m1...

</script>
<script type="module">

    let m1 = "m1...";

</script>

<script type="module">

    console.log(m1);  // Uncaught ReferenceError: m1 is not defined

</script>

预解析


  模块在导入时只执行一次解析,之后的导入不会再执行模块代码,而使用第一次解析结果,并共享数据。

  可以在首次导入时完成一些初始化工作

  如果模块内有后台请求,也只执行一次即可

<script type="module" src="./js_m3.js"></script>
<script type="module" src="./js_m3.js"></script>
<script type="module" src="./js_m3.js"></script> <!-- 导入多次,也只执行一次代码 -->
<!-- 打印结果如下:import m3... --> <!-- js_m3内容如下: console.log("import m3..."); -->

导出模块

  ES6使用基于文件的模块,即一个文件一个模块。

  可以使用export来将模块中的接口进行导出,导出方式分为以下几种:

  1.单个导出

  2.默认导出

  3.多个导出

  4.混合导出

  5.别名导出

  另外,ES6的导出是是以引用方式导出,无论是标量还是对象,即模块内部变量发生变化将影响已经导入的变量。

单个导出


  下面将使用export来将模块中的接口进行单个单个的导出。

export let module_name = "js_m3.js";

export function test(){
console.log("测试功能");
} export class User{
constructor(username){
this.username = username;
} show(){
console.log(this.username);
}
}

默认导出


  一个模块中,只能默认导出一个接口。

  如果默认导出的是一个类,那么该类就可以不用起类名,此外函数同理。

export let module_name = "js_m3.js";

export function test(){
console.log("测试功能");
} export default class{ // 默认导出
constructor(username){
this.username = username;
} show(){
console.log(this.username);
}
}

多个导出


  可以使用exprot{}的形式进行接口的批量多个导出。

let module_name = "js_m3.js";

function test() {
console.log("测试功能");
} class User {
constructor(username) {
this.username = username;
} show() {
console.log(this.username);
}
} export { module_name, test, User };

混合导出


  使用export default 导出默认接口,使用 export {} 批量导入普通接口

let module_name = "js_m3.js";

function test() {
console.log("测试功能");
} export default class {
constructor(username) {
this.username = username;
} show() {
console.log(this.username);
}
} export { module_name, test };

  同时也可以使用as来为一个导出的接口取别名,如果该接口别名为default则将该接口当做默认导出。

let module_name = "js_m3.js";

function test() {
console.log("测试功能");
} class User {
constructor(username) {
this.username = username;
} show() {
console.log(this.username);
}
} export { module_name, test, User as default };

别名导出


  使用as来为导出的export {}中的导出接口起一个别名,当导入时也应该使用导出接口的别名进行接收。

  当一个接口的别名为default时,该接口将当做默认导出。

let module_name = "js_m3.js";

function test() {
console.log("测试功能");
} class User {
constructor(username) {
this.username = username;
} show() {
console.log(this.username);
}
} export { module_name as m_name, test as m_tst, User as default };

导入模块

  使用importfrom进行静态的模块的导入,注意导入时必须将导入语句放在顶层。

  模块的导入分为以下几部分:

  1.具名导入

  2.批量导入

  3.默认导入

  4.混合导入

  5.别名导入

  6.动态导入

具名导入


  具名导入应该注意与导出的接口名一致。

  下面是模块导出的代码:

let module_name = "js_m3.js";

function test() {
console.log("测试功能");
} class User {
constructor(username) {
this.username = username;
} show() {
console.log(this.username);
}
} export { module_name, test, User };

  使用具名导入:

<script type="module">

    import { module_name, test, User} from "./js_m3.js"; 

    console.log(module_name);  // js_m3.js

    test(); // 测试功能

    let u1 = new User("云崖");  

    u1.show(); // 云崖

</script>

批量导入


  如果导入的内容过多,可使用*进行批量导入,注意批量导入后应该使用as来取一个别名方便调用。

  下面是模块导出的代码:

let module_name = "js_m3.js";

function test() {
console.log("测试功能");
} class User {
constructor(username) {
this.username = username;
} show() {
console.log(this.username);
}
} export { module_name, test, User };

  使用批量导入:

<script type="module">

    import * as m3 from "./js_m3.js";   // 别名为m3,下面使用都要以m3开头

    console.log(m3.module_name);  // js_m3.js

    m3.test(); // 测试功能

    let u1 = new m3.User("云崖");  

    u1.show(); // 云崖

</script>

默认导入


  使用默认导入时不需要用{}进行接收,并且可以使用任意名字来接收默认导出的接口。

  下面是模块导出的代码:

let module_name = "js_m3.js";

function test() {
console.log("测试功能");
} class User {
constructor(username) {
this.username = username;
} show() {
console.log(this.username);
}
} export { module_name, test, User as default };

  使用默认导入,我们只导入默认导出的接口,可以随便取一个名字。

<script type="module">

    import m3U from "./js_m3.js";  

    let u1 = new m3U("云崖");  

    u1.show(); // 云崖

</script>

混合导入


  当一个模块中导出的又有默认导出的接口,又有其他的导出接口时,我们可以使用混合导入。

  使用{}来接收其他的导出接口,对于默认导出的接口而言只需要取一个名字即可。

  下面是模块导出的代码:

let module_name = "js_m3.js";

function test() {
console.log("测试功能");
} class User {
constructor(username) {
this.username = username;
} show() {
console.log(this.username);
}
} export { module_name, test, User as default };

  使用混合导入:

<script type="module">

    import m3U, { module_name, test } from "./js_m3.js";  

    console.log(module_name);  // js_m3.js

    test();  // 测试功能

    let u1 = new m3U("云崖");

    u1.show(); // 云崖

</script>

别名导入


  为了防止多个模块下接口名相同,我们可以使用as别名导入,再使用时也应该按照别名进行使用。

  下面是m1模块导出的代码:

let module_name = "js_m1";

let show = function () {
console.log("js_m1.show");
} export { module_name, show };

  下面是m2模块导出的代码:

let module_name = "js_m2";

let show = function () {
console.log("js_m2.show");
} export { module_name, show };

  下面是使用别名导入这两个模块的接口并进行使用:

<script type="module">

    import { module_name as m1_name, show as m1_show } from "./js_m1.js";
import { module_name as m2_name, show as m2_show } from "./js_m2.js"; console.log(m1_name); // js_m1
console.log(m2_name); // js_m2 m1_show(); // js_m1.show
m2_show(); // js_m2.show </script>

动态导入


  使用importfrom的导入方式属于静态导入,必须将导入语句放在最顶层,如果不是则抛出异常。

  这是模块中的导出接口:

export function test() {
console.log("测试功能");
}

  如果我们想在某种特定条件下才导入并调用改接口,使用importfrom的方式会抛出异常。

<script type="module">

    if (true) {

        import { test } from "./js_m3.js"; // Error

        test();  // 想在特定条件下执行模块中的测试功能
} </script>

  这个时候就需要用到动态导入,使用 import() 函数可以动态导入,实现按需加载,它返回一个 promise 对象。

<script type="module">

    if (true) {

        let m3 = import("./js_m3.js");

        m3.then((module)=> module.test()); // 测试功能

    }

</script>

  我们可以使用解构语法来将模块中的接口一个一个全部拿出来。

<script type="module">

    if (true) {

        let m3 = import("./js_m3.js");

        m3.then(({ test, }) => test()); // 拿出test接口

    }

</script>

合并使用

  如果有多个模块都需要被使用,我们可以先定义一个Js文件将这些需要用到的模块中的接口做一个合并,然后再将该文件导出即可。

  合并导出请将exportfrom结合使用。

// js_m1

export default class{  // 默认导出
static register(){
console.log("注册功能");
}
}
// js_m2

export class Login{
static login(){
console.log("登录功能");
}
} export function test(){
console.log("js_m2测试功能");
}
// index.js

// 合并导出

import js_m1 from "./js_m1.js";

// js_m1中有接口是默认导出,因此我们需要不同的导出方式 , 注意这里就导出了一个接口,即js_m1的注册类
export {default as js_m1_register} from "./js_m1.js"; // 导出js_m2中的接口,共导出两个接口。登录类和测试函数。
export * as js_m2 from "./js_m2.js";

  导入与使用:

<script type="module">

    import * as index from "./index.js";

    index.js_m1_register.register();  // 注册功能

    index.js_m2.Login.login();  // 登录功能

    index.js_m2.test();  //  js_m2测试功能

</script>

指令总结

表达式 说明
export function show(){} 导出函数
export const name="Yunya" 导出变量
export class User{} 导出类
export default show 默认导出

const name = "Yunya"

export {name}

导出已经存在变量
export {name as m1_name} 别名导出
import m1_default from './m1_js.js' 导入默认导出
import {name,show} from '/m1_js.js' 导入命名导出
Import {name as m1_name,show} from 'm1_js.js' 别名导入
Import * as m1 from '/m1_js.js' 导入全部接口

编译打包

  由于module语法是Es6推出的,所以对老旧的浏览器兼容不太友好,这个时候就需要用到打包工具进行打包处理使其能让老旧的浏览器上进行兼容。

  首先登录 https://nodejs.org/en/ 官网下载安装Node.js,我们将使用其他的npm命令,npm用来安装第三方类库。

  在命令行输入 node -v 显示版本信息表示安装成功。

安装配置


  cd到你的项目路径,并使用以下命令生成配置文件 package.json

npm init -y

  修改package.json添加打包命令

...
"main": "index.js",
"scripts": {
"dev": "webpack --mode development --watch" // 添加这一句
},
...

  安装webpack工具包,如果安装慢可以使用淘宝 cnpm 命令

npm i webpack webpack-cli --save-dev

目录结构


index.html

--dist #压缩打包后的文件

--src

----index.js #合并入口

----style.js //模块

  index.html内容如下

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<script src="dist/main.js"></script>
</body>
</html>

  index.js内容如下

import style from "./style";
new style().init();

  style.js

export default class User {
constructor() {}
init() {
document.body.style.backgroundColor = "green";
}
}

执行打包


  运行以下命令将生成打包文件到 dist目录,因为在命令中添加了 --watch参数,所以源文件编辑后自动生成打包文件。

npm run dev

JavaScript 模块封装的更多相关文章

  1. 简易的highcharts公共绘图模块封装--基于.net mvc

    运行效果: 之所以选择这个图表插件,是因为它较其他同类插件轻量且中文文档详细完整,Demo丰富,配置使用简单.具体内容请登录中文官网:http://www.hcharts.cn/ 项目详细: 项目环境 ...

  2. 转: javascript模块加载框架seajs详解

    javascript模块加载框架seajs详解 SeaJS是一个遵循commonJS规范的javascript模块加载框架,可以实现javascript的模块化开发和模块化加载(模块可按需加载或全部加 ...

  3. javascript模块加载框架seajs详解

    SeaJS是一个遵循commonJS规范的javascript模块加载框架,可以实现javascript的模块化开发和模块化加载(模块可按需加载或全部加载).SeaJS可以和jQuery完美集成,使用 ...

  4. Openlayer3之C++接口在javaScript的封装使用

    0.写在前面: 1)涉及的关键词定义: 传入:JavaScript向CAPI传值 传出:CAPI向JavaScript传值 2)关于类和结构体的封装,需要严格执行内存对齐,以防止读取越界,但是避免不了 ...

  5. React-Native开发之原生模块封装(Android)升级版

     本文主题:如何实现原生代码的复用,即如何将原生模块封装. (尊重劳动成果,转载请注明出处:http://blog.csdn.net/qq_25827845/article/details/52862 ...

  6. 【JavaScript框架封装】使用原生js封装的类似于JQuery的框架及核心源码分享(多文件版本)

    这个版本的JQuery是对上一个版本的JQuery,使用了require.js进行了二次封装,基本上把前面的每一个框架封装成为一个单独的模块,最终的目录结构如下: 由于代码量和目录比较多,这个封装好的 ...

  7. 使用ECMAScript 6 模块封装代码

    JavaScript 用"共享一切"的方法加载代码,这是该语言中最容易出错且最容易让人感到困惑的地方.其他语言使用诸如包这样的概念来定义代码作用域,但在 ECMAScript 6 ...

  8. 2016/11/17 周四 <javascript的封装简单示例>

    这是一个简单的javascript代码封装的示例以及封装后的调用方法: var ticker={ n:0, add:function() { this.n++; }, show:function() ...

  9. 关于javascript模块加载技术的一些思考

    前不久有个网友问我在前端使用requireJs和seajs的问题,我当时问他你们公司以前有没有自己编写的javascript库,或者javascript框架,他的回答是什么都没有,他只是听说像requ ...

随机推荐

  1. python 装饰器(六):装饰器实例(三)内置装饰器

    内置的装饰器和普通的装饰器原理是一样的,只不过返回的不是函数,而是类对象,所以更难理解一些. @property 在了解这个装饰器前,你需要知道在不使用装饰器怎么写一个属性. def getx(sel ...

  2. 曹工改bug--本来以为很简单的数据库字段长度不足的问题,最后竟然靠抓包才解决

    问题描述 这两天本来忙着新功能开发,结果之前的一个项目最近要上了,然后又在测试,然后就喜提bug一枚(not mine),看bug描述,很简单,而且本地环境也重现了,只要刷入2000个英文字符就可以复 ...

  3. linux 安装 mysql8

    1. 下载地址: https://dev.mysql.com/downloads/file/?id=484922 2. 安装 mysql80-community-release-el7-3.noarc ...

  4. Spring AOP底层实现分析

    Spring AOP代理对象的生成 Spring提供了两种方式来生成代理对象: JdkProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的 ...

  5. 想用@Autowired注入static静态成员?官方不推荐你却还偏要这么做

    生命太短暂,不要去做一些根本没有人想要的东西.本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈.MyBatis.JVM.中间件等小而美的专栏供以免费学习 ...

  6. 学会Markdown不仅可以用来编写文档,还可以制作自己的简历,真香!

    程序员的简历要简洁明了,不要太多花哨的修饰,突出重点即可,使用markdown就可以很好的满足写一份简历的需求 Markdown 简历模板 这里我贡献一下我自己的markdown简历模板,简历效果如下 ...

  7. 题解 CF1354D 【Multiset】

    考试拿到题,一看,这不是权值线段树吗? 思路 使用线段树每个节点维护该区间内元素出现次数. 根据题目,对于加入.删除元素,我们可以单点修改(\(+1\).\(-1\)),对于输出,我们可 随便 遍历找 ...

  8. 硬核干货:5W字17张高清图理解同步器框架AbstractQueuedSynchronizer

    前提 并发编程大师Doug Lea在编写JUC(java.util.concurrent)包的时候引入了java.util.concurrent.locks.AbstractQueuedSynchro ...

  9. Android集成JPush(极光推送)

    目前只是简单的集成 1.在极光推送官网注册用户 2.创建应用 3.配置包名,获得APPKEY 去设置 输入应用包名 确定然后返回查看APPKEY 3.在应用中集成极光推送 用的jcenter自动集成的 ...

  10. Bug--时区问题导致IDEA连接数据库失败

    打开cmd进入mysql,设置 set global time_zone='+8:00';