在JavaScript编程中我们用的很多的一个场景就是写模块。可以看成一个简单的封装或者是一个类库的开始,有哪些形式呢,先来一个简单的模块。
简单模块
var foo = (function() { var name = "foo"; function hello() { console.log("hello "+name); } function doWork() { console.log("do work"); } return { hello: hello, doWork: doWork }; })(); foo.hello(); // hello foo foo.doWork(); // do work
用IIFE创建一个闭包,隔离作用域,避免变量相互干扰。得到foo对象可以直接用了。这种适合小的模块,比如在ag中的写Service。
(function () { angular .module('readApp') .service('authentication', authentication); authentication.$inject = ['$window','$http']; function authentication($window, $http) { var saveToken = function (token) { $window.localStorage['read-token'] = token; }; var getToken = function () { return $window.localStorage['read-token']; }; var register = function(user) { return $http.post('/api/register', user).success(function(data) { saveToken(data.token); }); }; var login = function(user) { return $http.post('/api/login', user).success(function(data) { saveToken(data.token); }); }; var logout = function() { $window.localStorage.removeItem('read-token'); }; var isLoggedIn = function() { var token = getToken(); if (token) { var payload = JSON.parse($window.atob(token.split('.')[1])); return payload.exp > Date.now() / 1000; } else { return false; } }; var currentUser = function() { if (isLoggedIn()) { var token = getToken(); var payload = JSON.parse($window.atob(token.split('.')[1])); return { email: payload.email, name: payload.name, }; } }; return { saveToken: saveToken, getToken: getToken, register: register, login: login, logout: logout, isLoggedIn: isLoggedIn, currentUser: currentUser, }; } })();
也可以直接一个大括号的:
var foo = { other: function () { console.log("do other"); }, hello:function() { console.log("working"); }, doWork:function() { console.log("working"); foo.other(); } }
这个形式我们在jquery内部或者一些工具类js中见过。简单直接。如果有内部相互调用,建议直接用对象名。这样不必在每一个方法里面写一个 var self=this 。缺点就是太长了不好维护,多增加一个变量都要加个key:value的形式。只适合简单场景。但如果模块内部方法比较多,还是建议在内部创建对象。
扩展模块
var foo = (function () { var self = {},name = "foo"; function _wroking() { console.log("working"); } self.hello = function () { console.log("hello " + name); } self.doWork = function () { _wroking(); } return self; })();
这样内部方法和外部方法就有明确的区分,可以看下Zepto的大体结构:
var Zepto = (function() { var undefined, key, $, classList, emptyArray = [], slice = emptyArray.slice, filter = emptyArray.filter, zepto = {}, camelize, uniq; //........... function isObject(obj) { return type(obj) == "object" } //.......... zepto.matches = function (element, selector) { //... } zepto.init = function(selector, context) { //...... } $ = function (selector, context) { return zepto.init(selector, context) } function extend(target, source, deep) { //... } $.extend = function (target) { } })() window.Zepto = Zepto window.$ === undefined && (window.$ = Zepto)
内部定义了zepot对象,并注意到有一个extend方法,便于后面扩展zepot的这个模块,当然也可以扩展其他的模块。target是指定的。
function extend(target, source, deep) { for (key in source) if (deep && (isPlainObject(source[key]) || isArray(source[key]))) { if (isPlainObject(source[key]) && !isPlainObject(target[key])) target[key] = {} if (isArray(source[key]) && !isArray(target[key])) target[key] = [] extend(target[key], source[key], deep) } else if (source[key] !== undefined) target[key] = source[key] } // Copy all but undefined properties from one or more // objects to the `target` object. $.extend = function(target){ var deep, args = slice.call(arguments, 1) if (typeof target == 'boolean') { deep = target target = args.shift() } args.forEach(function(arg){ extend(target, arg, deep) }) return target }
再扩展子模块的时候就方便了:
;(function ($) { //... $.event = { add: add, remove: remove } $.proxy = function(fn, context) { //... } })(Zepto);
现代的模块机制
AMD和CMD都是将模块的定义封装进了一个友好的API。就是require.js和sea.js中的define方法。先看一个简单的现代模块实现:
var MyModules = (function () { var modules = {}; function define(name, deps, impl) { for (var i = 0; i < deps.length; i++) { deps[i] = modules[deps[i]]; } modules[name] = impl.apply(impl, deps); } function get(name) { return modules[name]; } return { define: define, get: get } })(); MyModules.define("bar", [], function () { function hello(who) { return "Let me introduce: " + who; } return { hello: hello }; }); MyModules.define("foo", ["bar"], function (bar) { var hungry = "jazz"; function awsome() { console.log(bar.hello(hungry)); } return { awsome: awsome } }); var bar = MyModules.get("bar"); var foo = MyModules.get("foo"); console.log(bar.hello("hippo"));//Let me introduce: hippo foo.awsome();//Let me introduce: jazz
MyModules的define方法包含name,deps,impl三个参数,name表示是模块名称,deps表示是依赖项,impl表示实现。关键是modules[name] = impl.apply( impl, deps );这一句,上面的for循环将一个模块名称数组先转成一个包含具体模块的数组,然后apply给具体的实现方法。相当于是注入了依赖项。注意到定义foo模块的时候,依赖了bar模块,只需要在deps这个参数加入[“bar”]即可。
在sea.js中,模块的状态做了区分:
var STATUS = Module.STATUS = { // 1 - The `module.uri` is being fetched 相当于初始化 FETCHING: 1, // 2 - The meta data has been saved to cachedMods 缓存在cacheMods SAVED: 2, // 3 - The `module.dependencies` are being loaded LOADING: 3, // 4 - The module are ready to execute LOADED: 4, // 5 - The module is being executed EXECUTING: 5, // 6 - The `module.exports` is available EXECUTED: 6 }
看下define方法:
Module.define = function (id, deps, factory) { var argsLen = arguments.length // define(factory) if (argsLen === 1) { factory = id id = undefined } else if (argsLen === 2) { factory = deps // define(deps, factory) if (isArray(id)) { deps = id id = undefined } // define(id, factory) else { deps = undefined } } // Parse dependencies according to the module factory code if (!isArray(deps) && isFunction(factory)) { deps = parseDependencies(factory.toString()) } var meta = { id: id, uri: resolve(id), deps: deps, factory: factory } // Try to derive uri in IE6-9 for anonymous modules if (!meta.uri && doc.attachEvent) { var script = getCurrentScript() if (script) { meta.uri = script.src } // NOTE: If the id-deriving methods above is failed, then falls back // to use onload event to get the uri } // Emit `define` event, used in nocache plugin, seajs node version etc emit("define", meta) meta.uri ? save(meta.uri, meta) : // Save information for "saving" work in the script onload event anonymousMeta = meta }
save的内部是通过Module.get 缓存起uri和deps(依赖项)构建的Module对象。
Module.get = function(uri, deps) { return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps)) }
require也是通Module.get来获取模块:
seajs.require = function(id) { return (cachedMods[resolve(id)] || {}).exports }
不像例子中是一次性加载模块,sea.js可以在需要的地方再加载对应的模块。因为我们在很多js框架中看到下面这一块:
(function (root, factory) { if (typeof define === 'function' && define.amd) { define(factory); } else if (typeof exports === 'object') { module.exports = factory(); } else { root.xxx= factory(); } })(this, function () { //。。。 }
支持amd规范和node模块。
ES6中的模块
以上的模块都是基于函数的,API在运行时都可以被修改。ES6中为模块增加了一级语法支持,通过模块系统进行加载时,ES6会将文件独立的模块来处理。使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。
// ES6模块 import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs
模块加载3个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。
下面是一个circle.js
文件,它输出两个方法area
和circumference
。
// circle.js export function area(radius) { return Math.PI * radius * radius; } export function circumference(radius) { return 2 * Math.PI * radius; }
现在,加载这个模块。
// main.js import { area, circumference } from './circle'; console.log('圆面积:' + area(4)); console.log('圆周长:' + circumference(14));
上面写法是逐一指定要加载的方法,整体加载的写法如下。
import * as circle from './circle';
这语法让人有点激动。而且是不允许修改的,这样就很大的确保了接口的稳定性。
import * as circle from './circle'; // 下面两行都是不允许的 circle.foo = 'hello'; circle.area = function () {};
小结:模块在JavaScript运用很广泛,好的模块构建能确定一个清晰的结构,有助于分工和维护。
参考文档:
ES6 Module: http://es6.ruanyifeng.com/#docs/module
CMD规范:https://github.com/seajs/seajs/issues/242
阅读书籍:
资源在读书群中。