资讯专栏INFORMATION COLUMN

javascript 之模块化篇

huangjinnan / 2010人阅读

摘要:模块的加载第一个参数,是一个数组,里面的成员就是要加载的模块第二个参数,则是加载成功之后的回调函数。异步加载,浏览器不会失去响应它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

什么是模块化?

模块化就是把系统分离成独立功能的方法,这样我们需要什么功能,就加载什么功能。

优点:
可维护性:根据定义,每个模块都是独立的,良好设计的模块会尽量与外部的代码撇清关系,以便于独立对其进行改进和维护。
可复用性:可以重复利用,而不用经常复制自己之前写过的代码

原始JS开发问题

1、污染全局变量
//a.js 文件:

var test1="aaaaaa";
//b.js 文件
var test1="bbbbbb";
 
console test1 输出"bbbbbb";悲剧啊

2、命名冲突

//a.js 文件:
function fun(){
    console.log("this is b");
}
 //b.js 文件
 
function fun(){
    console.log("this is b");
}
//main.js 文件



小张在a.js定义了fun(),小李在b.js又定义了fun(),a,b被小王引入到main.js,执行fun(),输出this is b; 

3、依赖关系
b.js依赖a.js,标签的书写顺序必须是:



这样在多人开发的时候很难协调啊,令人头疼的问题。

解决冲突的方式

1、使用java式的命名空间
2、变量前加“_”
3、对象写法

var module1={
    test1:"aaaaaa",
    fun:function(){
        console.log(this.test1);
    }
}
变量和函数封装在对象里面,使用时,调用对象的属性即可:
module1.fun();//aaaaaa
但是这样的写法会暴露所有模块成员,内部状态可以被外部改写,
module1.test1="cccccc";

4、匿名闭包函数

var  module1=(function(){
    var test1="aaaaaa";
    var fun=function(){
        console.log("this is a");
    }
    return{
        fun:fun
    }
}());

匿名函数有自己的作用域,这样外部代码无法读取 module1 function 里面的变量了,从而也不会修改变量或者是覆盖同名变量了,但是还是有缺陷的,module1这个的变量还是暴露到全局了,而去随着模块的增多,全局变量会越来越多。
5、全局引入
像jquery库使用的全局引入。和匿名闭包函数相似,只是传入全局变量的方法不同
(function(window){

var test1="aaaaaa";
window.testFun=function(){//通过给window添加属性而暴漏到全局
    console.log(test1);
}

}(window));

通过匿名函数包装代码,所依赖的外部变量传给这个函数,在函数内部可以使用这些依赖,然后在函数的最后把模块自身暴漏给window。

3,4,5解决方法都是通过定一个全局变量来把所有的代码包含在一个函数内,由此来创建私有的命名空间和闭包作用域。

本文着重介绍几种广受欢迎的解决方案:CommonJS,AMD,CMD,ES模块化。

CommonJs

根据CommonJs规范,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

commonJS中模块可以加载多次,但是只会在第一次加载的时候运行一次,然后运行结构被缓存,再次加载就是读取缓存的结果。

CommonJS规范加载模块是同步的,也就是说,加载完成才可以执行后面的操作,Node.js主要用于服务器编程,模块一般都是存在本地硬盘中,加载比较快,所以Node.js采用CommonJS规范。

CommonJS规范分为三部分:module(模块标识),require(模块引用), exports(模块定义),
module变量在每个模块内部,就代表当前模块;
exports属性是对外的接口,用于导出当前模块的方法或变量;
require()用来加载外部模块,读取并执行js文件,返回该模块的exports对象;

1、commonJs模块定义

module.exports定义模块:

//math.js
let add=(x,y)=>{
    return x+y;
}
let sub=(x,y)=>{
    return x-y;
}

module.exports={
    add:add,
    sub:sub
};

exports 定义模块:

let add=(x,y)=>{
    return x+y;
}
let sub=(x,y)=>{
    return x-y;
}
exports.add=add;
exports.sub=sub;

注意:不可以直接对exports赋值,exports=add;

exports和module.exports有什么区别呢?
在每个模块中Node都提供了一个Module 对象,代表当前模块。

//console.log(Module);
Module {
  id: ".",
  exports: {},
  parent: null,
  filename: "/Users/zss/node-Demo/my-app/testNOde/b.js",
  loaded: false,
  children: [],
  paths: 
   [ "/Users/zss/node-Demo/my-app/testNOde/node_modules",
     "/Users/zss/node-Demo/my-app/node_modules",
     "/Users/zss/node-Demo/node_modules",
     "/Users/zss/node_modules",
     "/Users/node_modules",
     "/node_modules" 
     ] 
   }

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
为了方便,Node为每个模块提供一个exports变量,指向module.exports。我们把它们都打印出来看看究竟,

//test.js
console.log(module.exports);
console.log(exports);
console.log(module.exports===exports);

exports.test = ()=>{
    console.log("exports 1");
};
module.exports.test1 = ()=>{
    console.log("module.exports 1");
};
console.log(module.exports);
console.log(exports);

//输出:
{}
{}
true
{ test: [Function], test1: [Function] }
{ test: [Function], test1: [Function] }

从上例可以看出:
1.每个模块文件一创建,有个var exports = module.exports = {};使exports和module.exports都指向一个空对象。
**2.module是全局内置对象,exports是被var创建的局部对象,module.exports和exports所指向的内存地址相同
所有的exports收集到的属性和方法,都赋值给了Module.exports,最终返回给模块调用的是module.exports而不是exports。**

再举个例子:

//test.js
exports.test = ()=>{
    console.log("exports 1");
};
module.exports={
    test:function(){
        console.log("module.exports 1");
    },
    testmodule:()=>{
        console.log("module.exports 2")
    }
}
console.log(module.exports);
console.log(exports);

 
 //输出
{ test: [Function: test], testmodule: [Function: testmodule] }
{ test: [Function] }

//在index.js文件中调用test2.js
let a=require("./test2");
a.test();
a.testmodule();
//输出:
module.exports 1
module.exports 2

所有的exports收集到的属性和方法,都赋值给了Module.exports,当直接把函数和属性传给module.exports时,module.exports与exports不想等了,在调用时候,exports的属性和方法会被忽略,所以最终返回给模块调用的是module.exports而不是exports。

2、模块分类

NodeJs的模块分为两类:
一类是原生模块,例如http,fs,path 等等。node在加载原生模块的时候,不需要传入路径,NodeJs将原生模块的代码编译到了二进制执行文件中,加载速度快。
一类是文件模块,动态加载模块,
但是NodeJs对原生模块和文件模块都进行了缓存,第二次require时,就是执行的内存中的文件。

3、commonJs模块加载规则

index.js调用math模块:

let math=require("./math");
let test=math.add(3,3);
console.log(test);

执行index.js 输出:6;

当我们执行node index.js的时候,第一语句就是“require("./math");” 加载 math文件。加载math文件这个动作是由原生模块module的runMain()实现的。

有没有注意到上面写的是加载math文件,并没有明确指出是js文件。
NodeJS加载文件模块基本流程:
1、根据名称按照‘.js’,‘.node‘,’.json‘的顺讯依次查找,如果是.node或者.json的文件最好加上扩展名,加载速度快。
2、查找到math.js,读取js内容,将使用function进行包装,这样可以避免污染全局环境,该函数的参数包括require、module、exports等等参数,以mathi.js为例:

(function(exports,require,module,__filename,__dirname){
        let add=(x,y)=>{
            return x+y;
        }
        let sub=(x,y)=>{
            return x-y;
        }

        module.exports={
            add:add,
            sub:sub
        };

 })

require 方法中的文件查找规则很复杂底,在网上copy了一个图:

更详细的加载规则可以参考:http://www.infoq.com/cn/artic...

4、commonJs模块的加载机制:
//lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
//index.js
var mod=require("./lib");
consoe.log(mod.counter);
mod.incCounter();
consoe.log(mod.counter);

输出:3
     3

commonJS中模块加载以后,它的内部变化不会影响其内部变量,因为它们会被缓存,所以它输出的是值的拷贝。

CommonJS规范比较适用服务器端,如果是浏览器就需要异步加载模块了,所以就有了AMD,CMD解决方案。

AMD(requireJS)

 AMD是"Asynchronous Module Definition"的简写,也就是异步模块定义。它采用异步方式加载模块。通过define方法去定义模块,require方法去加载模块。

AMD模块定义:
define(function(){
    let add=(x,y)=>{
        return x+y;
    }
    let sub=(x,y)=>{
        return x-y;
    }
    
    return {
        add:add,
        sub:sub
    };
});

如果这个模块还需要依赖其他模块,那么define函数的第一个参数,必须是一个数组,指明该模块的依赖。

define([tools],function(){
    //…………………………
})
AMD模块的加载:

require([module], callback);

第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。例如加载math.js。

require([math],function(){
    //……………………
})

require()异步加载math,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

CMD(SeaJS)

玉伯提出的CMD规范,并开发了前端模块化开发框架SeaJS,不过在2015年后SeaJS停止了在github上维护,CMD与AMD用法很相似,但是我个人更喜欢使用SeaJS,虽然在2016年后也被我抛弃啦。

SeaJs使用:

// 所有模块都通过 define 来定义
define(function(require, exports, module) {

   // 通过 require 引入依赖
   var $ = require("jquery");
   var Spinning = require("./spinning");

   // 通过 exports 对外提供接口
   exports.doSomething = ...

   // 或者通过 module.exports 提供整个接口
   module.exports = ...

});

有关于SeaJS与 RequireJS 的异同,可以参考:
https://github.com/seajs/seaj...
https://www.douban.com/note/2...

ES6 模块化

在es6 之前没有模块化的,为了解决问题,提出了commonJS,AMD,CMD,现在ES6模块化汲取了CommonJS 和 AMD 的优点,简洁的语法,异步加载
它完全可以成为浏览器和服务器通用的模块化解决方案。

ES6中模块的定义

ES6 新增了两个关键字 export 和 import,export 用于把 模块里的内容 暴露 出来, import 用于引入模块提供的功能。

export命令输出变量:

//lib.js
let bar=function(){
    console.log("this is bar funciton");
};

let foo=function(){
    console.log("this is foo function");
};

export {bar,foo}

上面的代码还有另一种写法:

export let bar=function(){
    console.log("this is bar funciton");
};

export let foo=function(){
    console.log("this is foo function");
};

export 不止可以导出函数,还可以导出对象,类,字符串等等

const test="aaa";
const obj={
    str:"hello!"
}
export {test,obj};

注:使用export在尾部输出变量时,一定要加大括号,

ES6中模块的加载

import 加载模块:

 //加载 lib.js文件
 import {bar,foo,test,obj} from "./lib"
 
 foo();//this is foo function

注:import 命令具有提升效果,会提升到整个模块的头部,首先执行

上面的是逐一指定要加载的方法,我们还可以使用 * 可以整体加载模块:

import * as lib from "./lib"
lib.foo();

上面的加载模块的方式需要知道变量名和函数名,否则是无法加载的,我们可以使用export default 命令,为模块指定默认输出。

//lib.js
let foo=function(){
    console.log("this is foo");
} 
export default foo; 

其他文件加载时,可以为该匿名函数指定任意名字。

import  lib from "lib";

注:export default 命令适用于指定默认模块的输出,一个模块只能有一个默认输出,所以export default 只能使用一次。

ES6 模块运行机制

ES6模块是动态引用,如果使用import从一个模块加载变量(即import foo from "foo"),变量不会被缓存,而是成为一个指向被加载模块的引用。等脚本执行时,根据只读引用,到被加载的那个模块中去取值。
举一个NodeJS模块化的例子:

//lib.js
export let counter = 3;
exoprt function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
//index.js
import {counter,incCounter} from "./lib";
consoe.log(mod.counter);
mod.incCounter();
consoe.log(mod.counter);

输出:3
     4
调用 incCounter()方法后,lib 模块里的counter变量值改变了。

参考:
http://www.cnblogs.com/TomXu/...
http://blog.csdn.net/tyro_jav...
http://javascript.ruanyifeng....
http://www.ruanyifeng.com/blo...
https://zhuanlan.zhihu.com/p/...
https://segmentfault.com/a/11...
http://web.jobbole.com/83761/
http://es6.ruanyifeng.com/#do...
http://www.cnblogs.com/lishux...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88428.html

相关文章

  • 王下邀月熊_Chevalier的前端每周清单系列文章索引

    摘要:感谢王下邀月熊分享的前端每周清单,为方便大家阅读,特整理一份索引。王下邀月熊大大也于年月日整理了自己的前端每周清单系列,并以年月为单位进行分类,具体内容看这里前端每周清单年度总结与盘点。 感谢 王下邀月熊_Chevalier 分享的前端每周清单,为方便大家阅读,特整理一份索引。 王下邀月熊大大也于 2018 年 3 月 31 日整理了自己的前端每周清单系列,并以年/月为单位进行分类,具...

    2501207950 评论0 收藏0
  • javascript知识点

    摘要:模块化是随着前端技术的发展,前端代码爆炸式增长后,工程化所采取的必然措施。目前模块化的思想分为和。特别指出,事件不等同于异步,回调也不等同于异步。将会讨论安全的类型检测惰性载入函数冻结对象定时器等话题。 Vue.js 前后端同构方案之准备篇——代码优化 目前 Vue.js 的火爆不亚于当初的 React,本人对写代码有洁癖,代码也是艺术。此篇是准备篇,工欲善其事,必先利其器。我们先在代...

    Karrdy 评论0 收藏0
  • 前端面试Js

    摘要:作为构造函数使用,绑定到新创建的对象。内部实现类和类的继承构造函数构造函数调用父类构造函数参考请尽可能详尽的解释的工作原理的原理简单来说通过对象来向服务器发异步请求,从服务器获得数据,然后用来操作而更新页面。 1 . 请解释事件代理 (event delegation) 当需要对很多元素添加事件的时,可以通过将事件添加到它们的父节点通过委托来触发处理函数。其中利用到了浏览器的事件冒泡机...

    anyway 评论0 收藏0
  • 前端每周清单半年盘点 Node.js

    摘要:前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。对该漏洞的综合评级为高危。目前,相关利用方式已经在互联网上公开,近期出现攻击尝试爆发的可能。 前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点;分为新闻热点、开发教程、工程实践、深度阅读、开源项目、巅峰人生等栏目。欢...

    kid143 评论0 收藏0
  • 前端资源系列(4)-前端学习资源分享&前端面试资源汇总

    摘要:特意对前端学习资源做一个汇总,方便自己学习查阅参考,和好友们共同进步。 特意对前端学习资源做一个汇总,方便自己学习查阅参考,和好友们共同进步。 本以为自己收藏的站点多,可以很快搞定,没想到一入汇总深似海。还有很多不足&遗漏的地方,欢迎补充。有错误的地方,还请斧正... 托管: welcome to git,欢迎交流,感谢star 有好友反应和斧正,会及时更新,平时业务工作时也会不定期更...

    princekin 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<