javascript笔记整理系列

来源:互联网 发布:sql case语句用法实例 编辑:程序博客网 时间:2024/06/05 18:10

1 模块

模块根据一些标准将程序划分为不同的代码块。

2 模块的好处

  1. 代码的结构便于不熟悉代码的人寻找他们想要的代码。
  2. 便于程序猿将相关代码组织到一起。

2.1 命名空间

命名空间污染是JavaScript语言的一个问题,为了避免将大量无关变量全部挂在全局变量下。有如下方式解决这个问题:
1. 使用对象来创建公开访问的子命名空间。
2. 使用函数来创建独立的、私有的模块内部命名空间。

2.2 代码复用

参考npm的方便之处

2.3 代码解耦

  1. 功能对外接口应该具有统一和内聚的特性,尽量少暴露模块的内部细节,同时让对外的接口足够强大灵活。
  2. 定义良好的的模块接口在模块更新后保持不变。

3 使用函数作为命名空间

  1. 在JavaScript中只能使用函数来创建新的作用域,所以使用函数来创建模块,使模块有自己的作用域。
  2. 像前面所说,少暴露模块内部细节,只提供接口。

4 使用对象作为接口

(function(exports){    var _privateAttr = ["cyz","18","male",...];    exports.name = function(){        return _privateAttr[0];    };    exports.age = function(){        return _privateAttr[1];    }})(this.userClass = {});

这样只在全局作用域下暴露了userClass对象,我们能访问到它的属性只有name、age。

这样的套路就是,写一个自执行函数,在自执行数内组织对外暴露出来的对象及其属性。

5 与全局作用域分离

对于上面使用对象作为接口的方式,如果多个模块使用了同一个名字,那么这些版本就不能同时存在了,因为他们名字相同,且都挂在了全局对象上。
换种思路实现模块,这种模块可以直接调用其他模块的接口对象,而无需访问全局作用域。

按照这个思路,接下来我们构建一个require函数,调用该函数时指定一个模块名称,该函数会装载模块文件,并返回合适的接口对象。

使用两个函数编写require函数:
1. 首先需要一个readFile函数,将特定文件内容以字符串形式返回。
2. 其次需要将返回的字符串作为JavaScript代码执行。

6 将数据作为代码执行

方法很多,最简单的方法就是使用特殊运算符eval,在当前作用域中执行代码字符串。但一般不推荐这种方案,因为这样会破坏一些作用域的重要特性,比如无法将内部作用域与外界代码隔离。

更好的方法是使用Function构造函数来解释代码。该函数接收两个参数:第一个参数是包含参数名列表的字符串,参数之间以逗号分隔,第二个参数是包含函数体的字符串。
` var plusOne = new Function(“n”,”return n + 1;”);

将模块代码包裹在一个函数中,而函数的作用域将变成模块的作用域。

7 require函数

下面是简单实现:

function require(name){    var code = new Function("exports", "readFile(name)");    var exports = {};    code(exports);    return exports;}

使用这种模式时,一个模块通常会使用少量变量声明来加载其依赖的模块。

var weekDay = require('weekDay');var today = require('today');

上面的require函数存在一些问题:
1. 每次调用require时,该函数都会加载模块,因此如果有许多模块包含相同的依赖,或在频繁使用的函数中调用require,浪费大量时间和空间。
- 解决:我们可以将装载后的模块存储在一个对象中,如果重复装载则直接将存储的值返回。
2. 模块只能导出exports,无法直接导出其他值。比如一个模块可能只想导出其定义的对象类型的构造函数。
- 解决:一般解决方法是向模块提供另一个module变量,该变量有一个属性exports。require函数使用一个空对象初始化该属性,如果想导出一个值,只需要用另一个值覆盖掉exports属性即可。

function require(name){    if(name in require.cache){        return require.cache[name];    }    var code = new Function("exports, module",readFile(name));    var exports = {}, module = {exports: exports};    code(exports, module);    require.cache[name] = module.exports;    return module.exports;}require.cache = Object.create(null);

现在使用一个全局变量(require)来构建模块,模块可以搜索并使用其他的模块,而无需访问全局作用域。

人们将这种风格的模块系统命名为CommonJS模块。

8 模块加载过慢的问题

从互联网读取文件比硬盘读取慢得多,当浏览器运行脚本时,网站不会干其他事情。这也就意味着每个require调用都必须从Web服务器上获取代码,网页在装载脚本时会很长时间内毫无响应。

解决该问题的一个方法是在发布网页前,执行一个类似Browserify的程序。该程序会搜索代码中的require调用,解析所有依赖,并收寄所有代码,放到一个巨大的文件中。网站自身需要装载一次这个巨大的文件,就可以获取所有需要的模块。

另一个解决方案是将你自己的模块代码放在一个函数中,模块装载器首先在后台装载自身依赖,当其他依赖加载完成后调用函数初始化模块。这就是异步模块定义(AMD)模块系统所完成的任务。

如果采用异步模块定义,我们可以将前文需要依赖其他模块的代码改写成如下形式:

define(["weekDay","today"],function(weekDay,today){    //...})

该方法的核心是define函数。该函数第一个参数是模块名数组,第二个参数是一个函数,该函数中的每个参数代表一个依赖模块。define函数会在后台装载依赖,在获取模块文件的同时,网页可以继续运行。一旦依赖装载完成,define会调用我们指定的函数,并将这些依赖模块作为参数传递给该函数。

支持这种装载方式的模块必须包含define调用。我们将接口作为函数返回值传递给define函数。下面改写代码:

define([],function(){    var names = ["Sunday","monday",...];    return {        name: function(){ return names[number]; },        number: function(){ return names.indexOf(name); }    }})

为了展示如何实现简单的define的函数,我们假设有一个backgroundReadFile函数,该函数接受两个参数,第一个参数是文件名,第二个参数是另一个函数。backgroundReadFile函数会加载文件内容,加载完成后立即执行参数中指定的函数。

为了在模块加载时跟踪模块的加载状态,define实现会使用对象来描述模块的状态,告知我们模块是否就绪,并在模块就绪后提供模块接口。

函数getModule的参数是一个模块名,并返回与该模块对应的对象,同时确保系统随后会完成该模块的异步加载。该函数使用了一个缓存对象来避免重复加载模块。
““javascript
var defineCache = Object.create(null);
var currentMod = null;

function getModule(name){
if(name in defineCache){
return defineCache[name];
}

var module = {    exports: null;    loaded: false;    onLoad: []};defineCache[name] = module;backgroundReadFile(name,function(code){    currendMod = module;    new Function("",code)();});return module;

}

我们可以假设被加载的文件也包含一个define调用。其中currentMod变量用于告知函数调用方法当前被装载模块的情况,完成模块装载后我们会更新该对象。define函数自身使用getModule来为当前模块获取或创建模块对象。其任务是在完成依赖装载后,调度执行moduleFunction(包含模块实际代码的函数)。因此,define定义了一个函数whenDepsLoaded,并将其添加到尚未加载完成的模块的onLoad数组中。如果依然有依赖模块尚未完成加载,该函数也会立即返回,因此whenDepsLoaded函数实际上只会在最后一个依赖装载完成后工作一次。define自身也会在没有需要装载的模块时立即调用whenDepsLoaded函数。```javascriptfunction define(depNames, moduleFunction){    var myMod = currentMod;    var deps = depNames.map(getModule);    deps.forEach(function(mod){        if(!mod.loaded){            mod.onLoad.push(whenDepsLoaded);        }    });    function whenDepsLoaded(){        if(!deps.every(function(m){ return m.loaded; }))            return;        var args = deps.map(function(m){ return m.exports; });        var exports = moduleFunction.apply(null,args);        if(myMod){            myMod.exports = exports;            myMod.loaded = true;            myMod.onLoad.every(function(f){ f(); })        }    }    whenDepsLoaded();}

若所有依赖都装载完毕,whenDepsLoaded会调用持有模块的函数,将依赖项接口作为参数传递给该函数。

define所完成的第一个工作是将调用时的currentMod的值存储到myMod变量中。记住,getModule函数在执行模块代码之前会将对应的模块对象存储在currentMod中。这样whenDepsLoaded就可以将模块函数的返回值存储在模块的exports属性中,并将模块的Loaded属性设置为true,接着调用所有等待模块装载的函数。

实际的AMD实现可以更只能地将模块名解析成URL,而且比上述代码更为健壮。RequireJS项目是这类风格的模块装载器的一种实现,而且引用广泛。

9 接口设计

9.1 可预测性

如果有一个模块,或标准JavaScript环境中的一部分设计与你的实现非常相似,比较好的方式是模仿已有接口来设计你的模块接口。

9.2 可组合性

在你的接口中,尝试使用最简单的数据结构,尝试使用函数完成单一明确的任务。只要可行,最好将接口设计成纯函数。

9.3 层次化接口

当设计功能复杂的几口时,经常会遇到进退两难的情况。一方面,你不想让用户通过接口了解太多的细节,用户不必要话费大量时间学习你的接口;另一方面,你又不希望隐藏过多细节,用户可以使用你的模块完成复杂的功能。

此时解决方案往往是提供两种接口:为复杂状况准备的详细接口,以及为日常使用准备的高层次接口。

0 0