【读书笔记】-- JavaScript模块

在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);

现代的模块机制

AMDCMD都是将模块的定义封装进了一个友好的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文件,它输出两个方法areacircumference

// 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

阅读书籍:

【读书笔记】-- JavaScript模块

资源在读书群中。

上一篇:apple配置WIFI热点


下一篇:算法模板——平衡树Treap 2