欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

作用域、变量提升、闭包

程序员文章站 2022-04-15 15:35:07
你不知道的js——作用域 1 作用域 1.1编译原理 在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编 译” 1分词/词法分析(Tokenizing/Lexing) 会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代 码块被称为词法单元(token) ......

你不知道的js——作用域

1 作用域

1.1编译原理

在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编 译”

  • 1分词/词法分析(tokenizing/lexing)
    会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代 码块被称为词法单元(token)
    var a = 2; 分解成 为下面这些词法单元:var、a、=、2 、;
  • 2解析/语法分析(parsing)
    是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法 结构的树——“抽象语法树”(abstract syntax tree,ast)
    var a = 2; 的抽象语法树中可能会有一个叫作 variabledeclaration (变量声明)的*节点,接下 来是一个叫作 identifier(识别码)(它的值是 a)的子节点,以及一个叫作 assignmentexpression 的子节点。assignmentexpression (作业表达)节点有一个叫作 numericliteral(数字文字)(它的值是 2)的子 节点。
  • 3代码生成
    将 ast 转换为可执行代码的过程称被称为代码生成。
    将 var a = 2; 的 ast 转化为一组机器指 令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。
  • javascript 引擎不会有大量的(像其他语言编译器那么多的)时间用来进行优化,因 为与其他语言不同,javascript 的编译过程不是发生在构建之前的。大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时 间内。

1.2理解作用域

  1. 引擎:从头到尾负责整个 javascript 程序的编译及执行过程。
  2. 编译器:负责语法分析及代码生成等脏活累活
  3. 作用域:责收集并维护由所有声明的标识符(变量)组成的一系列查 询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
  • var a = 2; ,引擎认为这里有两个完全不同的声明,一个由编译器在编译时处理,另一个则由引擎在运行时处理。
  • 总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对 它赋值。
  1. lhs 查询:变量出现在赋值操作左侧时进行,试图找到变量的容器本身。
    a = 2;
  2. rhs查询:(非lhs) retrieve his source value(取到它的源值),变量出现在赋值操作右侧时进行,查找变量的值。
    console.log( a );
    lhs 和 rhs 引用都会在当前楼层进行查找,如果没有找到,就会坐电梯前往上一层楼
    不成功的 rhs 引用会导致抛出 referenceerror 异常。不成功的 lhs 引用会导致自动隐式 地创建一个全局变量(非严格模式下),该变量使用 lhs 引用的目标作为标识符,或者抛 出 referenceerror 异常(严格模式下)。

1.3 作用域嵌套

当前作用域无法找到某个变量时,引擎会在外层嵌套的作用域中查找,直至抵达最外层(全局)时仍未找到,抛出错误。

1.4 异常

变量无法找到时,抛出referenceerror(参考错误),与作用域判别失败相关。 对变量的值进行不合理操作,抛出typeerror,与操作是否合法相关。

当引擎执行 lhs 查询时,如果在顶层(全局作用域)中也无法找到目标变量, 全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非 “严格模式”下。

(严格模式禁止自动或隐式地创建全局变量。)

(如果 rhs 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作, 比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的 属性,那么引擎会抛出另外一种类型的异常,叫作 typeerror。
referenceerror 同作用域判别失败相关,而 typeerror 则代表作用域判别成功了,但是对 结果的操作是非法或不合理的。)

2 词法作用域

词法分析阶段 基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它 们进行查找。

作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法 作用域,我们会对这种作用域进行深入讨论。另外一种叫作动态作用域,仍有一些编程语 言在使用(比如 bash 脚本、perl 中的一些模式等)。

2.1 词法阶段

词法作用域即定义在词法阶段的作用域。

查找
作用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息,引擎用这些信息 来查找标识符的位置。

作用域查找会在找到第一个匹配的标识符时停止。。在多层的嵌套作用域中可以定义同名的 标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。

全局变量会自动成为全局对象(比如浏览器中的 window 对象)的属性,因此 可以间接地通过对全局对象属性的引 用来对其进行访问。——→ window.a
通过这种技术可以访问那些被同名变量所遮蔽的全局变量。但非全局的变量 如果被遮蔽了,无论如何都无法被访问到。

2.2 欺骗词法

在运行时来“修 改”(也可以说欺骗)词法作用域,:欺骗词法作用域会导致性能 下降(原因主要是无法对代码的词法进行静态分析,预先确认变量和函数的位置,从而快速寻找。)

【另外一个不推荐使用 eval(..) 和 with 的原因是会被严格模式所影响(限 制)。with 被完全禁止,而在保留核心功能的前提下,间接或非安全地使用 eval(..) 也被禁止了。】

2.2.1 eval

接受一个字符串为参数,并将其中的内容视为好像在书 写时就存在于程序中这个位置的代码

eval(..) 通常被用来执行动态创建的代码

eval(..) 调用中的 "var b = 3;" 这段代码会被当作本来就在那里一样来处理。由于那段代 码声明了一个新的变量 b,因此它对已经存在的 foo(..) 的词法作用域进行了修改。事实 上,和前面提到的原理一样,这段代码实际上在 foo(..) 内部创建了一个变量 b,并遮蔽 了外部(全局)作用域中的同名变量。

在严格模式的程序中,eval(..) 在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。

与之相似的有settimeout,setinterval,new function(),都可以接收字符串。

2.2.2 with

with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。with会创建一个全新的词法作用域。 在严格模式下,with被禁止。

例子:

function foo(obj) {    
with (obj) {         
a = 2;     
} } 
var o2 = {     
b: 3 
}; 
foo( o2 ); 
console.log( o2.a ); // undefined 
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!
(但当我们将 o2 作为作用域时,其中并没有 a 标识符, 因此进行了正常的 lhs 标识符查找;
o2 的作用域、foo(..) 的作用域和全局作用域中都没有找到标识符 a,因此当 a=2 执行 时,自动创建了一个全局变量(因为是非严格模式)。)

3 函数作用域和块作用域

3.1 函数中的作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复 用(事实上在嵌套的作用域中也可以使用)。js具有基于函数的作用域。内部能向外访问,外部不能向内访问。

3.2 隐藏内部实现

把变量和函数包裹在一个函数的作用域中,然后用这个作用域 来“隐藏”它们。

最小授权或最小暴露原则:在软件设计中,应该最小限度地暴露必要的内容,而将其他内容都隐藏起来。 隐藏作用域能够避免同名标识符之间的冲突。

规避冲突:“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突, 两个标识符可能具有相同的名字但用途却不一样,无意间可能造成命名冲突。冲突会导致 变量的值被意外覆盖。

3.2.1 全局命名空间

引入第三方库时,没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引发冲突

3.2.2 模块管理

另外一种避免冲突的办法和现代的模块机制很接近,就是从众多模块管理器中挑选一个来 使用。

无需将标识符加入到全局作用域中,而是通过依赖管理器 的机制将库的标识符显式地导入到另外一个特定的作用域中

3.3 函数作用域

如果function是声明中第一个词(前面没有其他词,甚至是括号),那么就是一个函数声明,否则就是一个函数表达式。函数表达式可以是匿名的, 而函数声明则不可以省略函数名

在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐 藏”起来,外部作用域无法访问包装函数内部的任何内容。但是它并不理想,因为会导致一些额外的问题。首先, 必须声明一个具名函数 foo(),意味着 foo 这个名称本身“污染”了所在作用域(在这个 例子中是全局作用域)。

函数不需要函数名(或者至少函数名可以不污染所在作用域),并且能够自动运行, 这将会更加理想。

☆☆☆☆☆包装函数的声明以 (function... 而不仅是以 function... 开始。,函数会被当作函数表达式而不是一 个标准的函数声明来处理。

(function foo(){ .. }) 作为函数表达式意味着foo 被绑定在函数表达式自身的函数中,外部作用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部作 用域。

3.3.1 匿名和具名

于函数表达式你最熟悉的场景可能就是回调参数

函数表达式可以匿名,函数声明必须具名。 匿名函数也有缺点:

  • 没有函数名,栈追踪调试困难。
  • 递归自身时,只能使用arguments.callee。或者是事件解绑时候找不到。
  • 没有函数名,降低了可读性。

选择性地给函数表达式具名,可以解决以上问题。

行内函数表达式非常强大且有用——匿名和具名之间的区别并不会对这点有任何影响,始终给函数表达式命名是一个最佳实践。

3.3.2 立即执行函数表达式iife(immediately invoked function expression)

用圆括号()将函数包裹,然后紧跟圆括号调用。 另一种可以将括号放在内部。 (function ( ){ …… } ( )); umd标准中,iife也被广泛运用,比如:

var a = 2;
(function iife( def ) {
    def( window )
})(function def ( global ) {
    var a = 3;
    console.log( a ); //3
    console.log( global.a ); //2
});
//函数表达式 def 定义在片段的第二部分,然后当作参数(这个参数也叫作 def)被传递进 iife 函数定义的第一部分中。最后,参数 def(也就是传递进去的函数)被调用,并将 window 传入当作 global 参数的值

3.4 块作用域

在 for 循环的头部直接定义了变量 i,通常是因为只想在 for 循环内部的上下文中使 用 i,而忽略了 i 会被绑定在外部作用域(函数或全局)中的事实。这就是块作用域的用处。变量的声明应该距离使用的地方越近越好,并最大限度地本地 化。

块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息 扩展为在块中隐藏信息。

(因多次多处访问而提前做缓存除外) 块级作用域在es6中得到广泛应用。在此之前需要注意,var声明的变量并不属于块级作用域。 可以生成块级作用域的有:

  • with :用 with 从对象中创建出的作用域仅在 with 声明中而非外 部作用域中有效
  • try/catch: catch 分句会创建一个块作 用域,其中声明的变量仅在 catch 内部有效
    try {
    undefined(); // 执行一个非法操作来强制制造一个异常
    } catch (err) {
    console.log( err ); // 能够正常执行!
    }
    console.log( err ); // referenceerror: err not found

  • let: 用 let 将变量附加在一个已经存在的块作用域上的行为是隐式的。只要声明是有效的,在声明中的任意位置都可以使用 { .. } 括号来为 let 创建一个用于绑 定的块。
    使用 let 进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不 “存在”
    {
    console.log( bar ); // referenceerror!
    let bar = 2;
    }
  • const(es6新特性),可以形成暂时性锁区。同样可以用来创建块作用域变量,但其值是固定的 (常量)。之后任何试图修改值的操作都会引起错误。
    *块级作用域的用处:**有利于垃圾收集。程序块在执行后,其中的变量如果不被后续需要(闭包等),就可以将内存回收 . 减少变量空间污染。

1 垃圾收集

2 let循环

4 提升

4.1 先有鸡还是先有蛋

function和var声明,会被提升到顶部。 其他的操作不会提升,处于自身原本的位置。

可以看到,函数声明会被提升,但是函数表达式却不会被提升。
foo(); // 不是 referenceerror, 而是 typeerror! 
var foo = function bar() {     
// ... 
};
//这段程序中的变量标识符 foo() 被提升并分配给所在作用域(在这里是全局作用域),因此 foo() 不会导致 referenceerror。但是 foo 此时并没有赋值(如果它是一个函数声明而不 是函数表达式,那么就会赋值)。foo() 由于对 undefined 值进行函数调用而导致非法操作, 因此抛出 typeerror 异常。

4.2 编译器再度来袭

当你看到 var a = 2; 时,可能会认为这是一个声明。但 javascript 实际上会将其看成两个声明:var a; 和 a = 2; 第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在 原地等待执行阶段。

每个作用域都会进行提升操作,其顶部为其自身作用域的顶部(注意不会跨越作用域)。 函数表达式不会提升,而是处于正常的位置。 具名的函数表达式也不存在提升

foo(); // typeerror,因为此时foo被初始化为undefined,还没有赋值为函数表达式
bar(); // referenceerror,因为函数表达式不提升,此时bar还没有声明
var foo = function bar(){
    // do sth
}

等效于

var foo;
foo(); // typeerror,因为此时foo被初始化为undefined,还没有赋值为函数表达式
bar(); // referenceerror,因为函数表达式不提升,此时bar还没有声明
foo = function (){
    var bar = self;
    // do sth
}

4.3 函数优先

函数首先被提升,然后是变量。函数与变量同名时,变量的声明会被省略,但是依旧可以进行赋值操作。 后出现的函数声明会覆盖前面的同名函数,所以在同一作用域内,千万不要声明同名的函数 在普通块内部的声明,也会提升到作用域顶部,而不是在块内。

//一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像下面的代 码暗示的那样可以被条件判断所控制:
foo() // 'b'

var a = true;
if(a){
    function foo(){ console.log('a') } 
    // 被提升到作用域顶部。先声明,被后面覆盖了
} else {
    function foo(){ console.log('b') } 
    // 被提升到第二个。后声明,覆盖了前面
}

因此应该 尽可能避免在块内部声明函数。

5 作用域闭包

5.1 启示

js中闭包无处不在。 闭包是基于词法作用域书写代码时产生的自然结果。 闭包特征就是将函数表达式,连带其词法作用域,进行传值。

5.2 实质问题

一个闭包的例子:

function foo(){
    var a = 2;
        function bar(){
        console.log(a);
    }
    return bar; //也可以直接返回函数表达式,function(){ console.log(a); }
}

var baz = foo();
baz(); // 2,读取到了foo中定义的变量a

本质上是,函数bar的词法作用域能够访问foo的内部作用域,因为它的作用域嵌套在了foo的作用域内。 也就是说,最终return的结果是带有scope(作用域)信息的,这个是能够找到需要调用变量的根本依据。 当bar在使用时,闭包会阻止垃圾回收器回收foo的作用域(有好有坏)。

5.3 现在我懂了

引擎在调用函数的同时,其词法作用域会保持完整。 如果将(访问他们各自词法作用域的)函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。 也就是说,只要使用了回调函数,实际上就是在使用闭包。(在定时器、事件监听器、 ajax 请求、跨窗口通信、web workers 或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!)

5.4 循环和闭包

异步时候,函数进行回调时,调用变量值的是回调发生时候的值,所以要考虑运行时的状态。

拓展解析:任务队列、进阶

for(var i=0; i<5; i++){
    settimeout( function(){
        console.log(i); //输出都是5
    }, 1000*i);
}

解决方法有iife

for(var i=0; i<5; i++){
    // 创建一个新的词法作用域,把它的值在这个作用域里面记录下来
    (function(j){
        settimeout(function(){
            console.log(j); //这样输出就是0,1,2,3,4了
        }, 1000*j);
    })(i);
}

或者利用作用块

// let可以作用于作用块
for(let i=0; i<5; i++){
    settimeout( function(){
        console.log(i); //这样输出就是0,1,2,3,4了
    }, 1000*i);
}

这里面本质就是

for(var i=0; i<5; i++){
    // 使用仅存在于该作用块的变量
    let _i = i;
    settimeout( function(){
        console.log(_i); //这样输出就是0,1,2,3,4了
    }, 1000*_i);
}

5.5 模块

  1. 实现模块的模式称为模块暴露。

    function mymodule(){
    var something = 'cool';
    var another = [1,2,3];

     function dosomething(){
         console.log(something);
     }
    
     function doanother(){
         console.log(another.join(','));
     }
    
     //es5的写法
     return {
         dosomething: dosomething,
         doanother: doanother,
     };
    
     // es6的写法
     /* return {
         dosomething,
         doanother,
     }; */

    }

    // 得到了这个闭包
    var foo = mymodule();

    foo.dosomething(); // cool
    foo.doanother(); // 1,2,3

  • 只需要使用一次的时候,记得改成iife。
  • 需要参数的时候,记得自己去定义参数表。
  1. 模块模式另一个简单但强大的变化用法是,命名将要作为公共 api 返回的对象:
    var foo = (function coolmodule(id) {
    function change() {
    // 修改公共 api
    publicapi.identify = identify2;
    }

     function identify1() {         
         console.log( id );     
        } 
    
     function identify2() {         
         console.log( id.touppercase() );    
        } 
    
     var publicapi = {         
         change: change,        
            identify: identify1    
        }; 
    
     return publicapi; 
    })( "foo module" ); 
    
    foo.identify(); // foo module 
    foo.change(); 
    foo.identify(); // foo module

    通过在模块实例的内部保留对公共 api 对象的内部引用,可以从内部对模块实例进行修 改,包括添加或删除方法和属性,以及修改它们的值。
    通过在模块实例的内部保留对公共 api 对象的内部引用,可以从内部对模块实例进行修 改,包括添加或删除方法和属性,以及修改它们的值。

5.5.1 现代的模块机制

一个典型的模块管理器可以定义为(应该是参考了require.js)

var mymodules = (function manager(){
    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,
    };
});

其使用方式为

// 名称为bar的模块,没有依赖
mymodules.define('bar', [], function(){
    function hello(who){
        return 'let me introduce: ' + who;
    }

    return {
        hello: hello,
    };
});

// 名称为foo,依赖了bar。
// 注意这里函数表达式能拿到bar,是因为define方法去modules中抽取了bar,然后传入给它。
mymodules.define('foo', ['bar'], function(bar){
    var hungry = 'hippo';

    function awesome(){
        // 因为外层参数已经传入了bar,所以这里也就能拿到bar的hello方法
        console.log(bar.hello(hungry).touppercase());
    }

    return {
        awesome: awesome,
    };
});

var foo = mymodules.get('foo');
foo.awesome(); // let me introduce hippo

5.5.2 未来的模块机制

es6 的模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)。浏览 器或引擎有一个默认的“模块加载器”(可以被重载,但这远超出了我们的讨论范围)可 以在导入模块时异步地加载模块文件。

bar.js

export function hello(who){
        return 'let me introduce: ' + who;
}

foo.js

// 直接获得了hello
import {hello} from 'bar';

var hungry = 'hippo';

export function awesome(){
    console.log(hello(hungry).touppercase());
}

baz.js

// 导入完整的foo模块
module foo from 'bar';

foo.awesome(); // let me introduce hippo

5.6 小结

闭包类似于一个标准,关于如何在函数作为值按需传递的词法环境中书写代码的

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时 就产生了闭包。

时闭包也是一个非常强大的工具,可以用多种形式来实现模块等模式。

模块有两个特征:

  1. 为创建内部作用域而调用了一个包装函数。
  2. 包装函数的返回值至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。

补充:模块机制、amd、require.js

立即执行函数写法

使用"立即执行函数"(immediately-invoked function expression,iife),可以达到不暴露私有成员的目的。

  var module1 = (function(){
    var _count = 0;
    var m1 = function(){
      //...
    };
    var m2 = function(){
      //...
    };
    return {
      m1 : m1,
      m2 : m2
    };
  })();

使用上面的写法,外部代码无法读取内部的_count变量。

 console.info(module1._count); //undefined

module1就是javascript模块的基本写法。下面,再对这种写法进行加工。

放大模式:,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"放大模式"(augmentation)。

  var module1 = (function (mod){
    mod.m3 = function () {
      //...
    };
    return mod;
  })(module1);

上面的代码为module1模块添加了一个新方法m3(),然后返回新的module1模块。

宽放大模式(loose augmentation) :在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"。

  var module1 = ( function (mod){
    //...
    return mod;
  })(window.module1 || {});

与"放大模式"相比,"宽放大模式"就是"立即执行函数"的参数可以是空对象。

输入全局变量

独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。

为了在模块内部调用全局变量,必须显式地将其他变量输入模块。

  var module1 = (function ($, yahoo) {
    //...
  })(jquery, yahoo);

上面的module1模块需要使用jquery库和yui库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。

amd规范

通行的javascript模块规范共有两种:commonjs和amd。

commonjs

2009年,美国程序员ryan dahl创造了node.js项目,将javascript语言用于服务器端编程。

node.js的模块系统,就是参照commonjs规范实现的。在commonjs中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。

  var math = require('math');

然后,就可以调用模块提供的方法:

  var math = require('math');

  math.add(2,3); // 5

因为这个系列主要针对浏览器编程,不涉及node.js,所以对commonjs就不多做介绍了。我们在这里只要知道,require()用于加载模块就行了。

浏览器环境 对于浏览器,模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是amd规范诞生的背景。

amd是"asynchronous module definition"的缩写,意思就是"异步模块定义"。

amd也采用require()语句加载模块,但是不同于commonjs,它要求两个参数:

  require([module], callback);

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

目前,主要有两个javascript库实现了amd规范:require.js和curl.js。

require.js的用法

依次加载多个js文件会有很大的弊端。

首先,加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长;其次,由于js文件之间存在依赖关系,因此必须严格保证加载顺序(比如上例的1.js要在2.js的前面),依赖性最大的模块一定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。

require.js的诞生解决了两个问题

  1. 实现js文件的异步加载,避免网页失去响应;
  2. 管理模块之间的依赖性,便于代码的编写和维护。

require.js的加载

使用require.js的第一步,是先去官方网站下载最新版本。

下载后,假定把它放在js子目录下面,就可以加载了。

  <script src="js/require.js"></script>
//加载这个文件,也可能造成网页失去响应。解决办法有两个,一个是把它放在网页底部加载,另一个是写成下面这样:
  <script src="js/require.js" defer async="true" ></script>
//async属性表明这个文件需要异步加载,避免网页失去响应。ie不支持这个属性,只支持defer,所以把defer也写上。

/*加载require.js以后,下一步就要加载我们自己的代码了。假定我们自己的代码文件是main.js,也放在js目录下面。那么,只需要写成下面这样就行了:*/
  <script src="js/require.js" data-main="js/main"></script>
//data-main属性的作用是,指定网页程序的主模块。在上例中,就是js目录下面的main.js,这个文件会第一个被require.js加载。由于require.js默认的文件后缀名是js,所以可以把main.js简写成main。

主模块的写法

这时就要使用amd规范定义的的require()函数。

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

一个实际的例子:假定主模块依赖jquery、underscore和backbone这三个模块,main.js就可以这样写:

 require(['jquery', 'underscore', 'backbone'], function ($, _, backbone){
    // some code here
  });
//require.js会先加载jquery、underscore和backbone,然后再运行回调函数。主模块的代码就写在回调函数中。

模块的加载

默认情况下,require.js假定这三个模块与main.js在同一个目录,文件名分别为jquery.js,underscore.js和backbone.js,然后自动加载。

使用require.config()方法,我们可以对模块的加载行为进行自定义。require.config()就写在主模块(main.js)的头部。参数就是一个对象,这个对象的paths属性指定各个模块的加载路径。

  require.config({
    paths: {
      "jquery": "jquery.min",
      "underscore": "underscore.min",
      "backbone": "backbone.min"
    }
  });

如果这些模块在其他目录,比如js/lib目录,则有两种写法。一种是逐一指定路径;另一种则是直接改变基目录(baseurl) 。

require.config({//1
    paths: {
      "jquery": "**lib/**jquery.min",
      "underscore": "**lib/**underscore.min",
      "backbone": "**lib/**backbone.min"
    }
  });
//2
require.config({
    baseurl: "js/lib",
    paths: {
      "jquery": "jquery.min",
      "underscore": "underscore.min",
      "backbone": "backbone.min"
    }
  });
//也可以直接指定它的网址
require.config({
    paths: {
      "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"
    }
  });

amd模块的写法

模块必须采用特定的define()函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。

假定现在有一个math.js文件,它定义了一个math模块。那么,math.js就要这样写:

define(function (){
    var add = function (x,y){
      return x+y;
    };
    return {
      add: add
    };
  });
//加载方法如下:
 require(['math'], function (math){
    alert(math.add(1,1));
  });

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

  define(['mylib'], function(mylib){
    function foo(){
      mylib.dosomething();
    }
    return {
      foo : foo
    };
  });//当require()函数加载上面这个模块的时候,就会先加载mylib.js文件。