一名【合格】前端工程师的自检清单(作用域和闭包篇)

37 篇文章 0 订阅
24 篇文章 0 订阅

 

##作用域和闭包

 

###1.理解词法作用域和动态作用域

 

####作用域

 

####静态作用域。

 

作用域是指程序源代码中定义变量的区域。

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

 

function f1() {

alert(v);

}

function f2() {

var v = 100;

f1();

}

f2();

 

 

执行上述代码,会发现程序报错 v : undefined

 

为什么呢?这就和js的作用域有关了

 

f1在定义的时候, js解析器会给f1定义一个内部的属性叫scope, 按照f1定义时的词法环境,scope是指向window的,所以当f2调用f1的时候, 程序首先会在f1的函数体内寻找变量v, 没找到, 就去window中去寻找v,也没有变量v,所以程序就报错了。

 

也就是说,f1的作用域是在定义的时候就已经决定了,而不是在调用时决定的。这也就是所谓js的静态作用域

 

####动态作用域

 

因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。

而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

 

// 动态作用域:

function foo() {

console.log(a);

}



function bar() {

var a = 3;

foo();

}



var a = 2;

bar(); // 2;

 

 

bar 调用,bar里面foo被调用,foo函数需要查找变量a,由于JavaScript是词法作用域(即静态作用域),foo被解析时在全局作用域.

所以只能在全局作用域中找a,输出结果为2,而非bar作用域中的a。如果js采用的时动态作用域,那么foo在bar中调用,就会先在bar中查询a,输出为3。


 

###2.理解JavaScript的作用域和作用域链

 

####作用域

 

####全局作用域(globe scope)和局部作用域(local scope)

 

var name1="haha";

function changName(){

var name2="xixi";

console.log(name1); // haha

console.log(name2);// xixi

}

changName();

console.log(name1);//haha

console.log(name2);//Uncaught ReferenceError: name2 is not defined

 

 

其中,name1具有全局作用域,因此在第4行和第8行都会在控制台上输出 haha。name2定义在changName()函数内部,具有局部作用域,因此在第9行,解析器找不到变量name2,抛出错误。

另外,在函数中声明变量时,如果省略 var 操作符,那么声明的变量就是全局变量,拥有全局作用域,但是不推荐这种做法,因为在局部作用域中很难维护定义的全局变量。

再者,window对象的内置属性都拥有全局作用域。

局部作用域一般只在固定的代码片段内可以访问得到,例如上述代码中的name2,只有在函数内部可以访问得到。

 

####作用域链(scope chain)

 

全局作用域和局部作用域中变量的访问权限,其实是由作用域链决定的。

 

每次进入一个新的执行环境,都会创建一个用于搜索变量和函数的作用域链。

作用域链是函数被创建的作用域中对象的集合。作用域链可以保证对执行环境有权访问的所有变量和函数的有序访问。

 

作用域链的最前端始终是当前执行的代码所在环境的变量对象(如果该环境是函数,则将其活动对象作为变量对象),

下一个变量对象来自包含环境(包含当前还行环境的环境),下一个变量对象来自包含环境的包含环境,

依次往上,直到全局执行环境的变量对象。全局执行环境的变量对象始终是作用域链中的最后一个对象。

 

标识符解析是沿着作用域一级一级的向上搜索标识符的过程。搜索过程始终是从作用域的前端逐地向后回溯,直到找到标识符(找不到,就会导致错误发生)。

 

var name1 = "haha";

function changeName(){

    var name2="xixi";

    function swapName(){
    
        console.log(name1);//haha

        console.log(name2);//xixi

        var tempName=name2;

        name2=name1;

        name1=tempName;

        console.log(name1);//xixi

        console.log(name2);//haha

        console.log(tempName);//xixi

    }

    swapName();

    console.log(name1);//haha

    console.log(name2);//xixi

    //console.log(tempName);抛出错误:Uncaught ReferenceError: tempName is not defined

}

changName();

console.log(name1);

//console.log(name2); 抛出错误:Uncaught ReferenceError: name2 is not defined

//console.log(tempName);抛出错误:Uncaught ReferenceError: tempName is not defined

 

 

上述代码中,一共有三个执行环境:全局环境、changeName()的局部环境和 swapName() 的局部环境。所以,

 1.函数 swapName()的作用域链包含三个对象:自己的变量对象----->changeName()局部环境的变量对象 ----->全局环境的变量对象。

 2.函数changeName()的作用域包含两个对象:自己的变量对象----->全局环境的变量对象。

就上述程序中出现的变量和函数来讲(不考虑隐形变量):

 1.swapName() 局部环境的变量对象中存放变量 tempName;

 2.changeName() 局部环境的变量对象中存放变量 name2 和 函数swapName();

 3.全局环境的变量对象中存放变量 name1 、函数changeName();

 

  在swapName()的执行环境中,在执行第5句代码时,解析器沿着函数 swapName()的作用域链一级级向后回溯查找变量 name1,直到在全局环境中找到变量 name1.并输出在控制台上。同样,在执行第6句代码时,解析器沿着函数 swapName()的作用域链一级级向后回溯,在函数changeName()的变量对象中发现变量 name2.通过代码对 name1 和 name2进行交换,并输出在控制台上,根据结果我们发现,这两个变量的值确实交换了。

因此我们可以得出结论,函数的局部环境可以访问函数作用域中的变量,也可以访问和操作父环境(包含环境)乃至全局环境中的变量。

 

  在changeName() 的执行环境中,执行第15行和第16行代码时,可以正确地输出 name1 和 name2 和两个变量的值(调用了函数swapName(),所以俩变量的值已相互交换),那是因为 name1 在changName()的父环境(全局环境)中, name2 在他自己的局部环境中,即 name1 和 name2 都在其作用域链上。但当执行第17行代码是发生错误 tempName is not defined。因为解析器沿着 函数changeName()的作用域链一级级的查找 变量 tempName时,并不能找到该变量的存在(变量 tempName不在其作用域链上),所以抛出错误。

因此,我们可以得出结论:父环境只能访问其包含环境和自己环境中的变量和函数,不能访问其子环境中的变量和函数。

 

  同理,在全局环境中,其变量对象中只存放变量 name1 、函数changeName(); 解析器只能访问变量 name1 和函数 changeName(), 而不能访问和操作 函数 changeName() 和函数 swapName() 中定义的变量或者函数。因此,在执行第21行和第22行代码时抛出变量没有定义的错误。

所以说,全局环境只能访问全局环境中的变量和函数,不能直接访问局部环境中的任何数据。

 

###3.理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题

 

####JavaScript的执行上下文栈

 

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。

 

执行上下文:也叫一个执行环境,有 全局执行环境 和 函数执行环境 和 eval。

 

每个执行环境中包含这三部分:变量对象/活动对象,作用域链,this的值。

 

JS 中有三种执行上下文

 

全局执行上下文,默认的,在浏览器中是 window 对象,并且 this 在非严格模式下指向它。

函数执行上下文,JS 的函数每当被调用时会创建一个上下文。

Eval 执行上下文,eval 函数会产生自己的上下文,这里不讨论。

 

通常,我们的代码中都不止一个上下文,那这些上下文的执行顺序应该是怎样的?从上往下依次执行?

 

栈,是一种数据结构,具有先进后出的原则。JS 中的执行栈就具有这样的结构,当引擎第一次遇到 JS 代码时,

会产生一个全局执行上下文并压入执行栈,每遇到一个函数调用,就会往栈中压入一个新的上下文。

引擎执行栈顶的函数,执行完毕,弹出当前执行上下文。

 

为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:

 

ECStack = [];

 

试想当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,

我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以 ECStack 最底部永远有个 globalContext。

 

ECStack = [

globalContext

];

 

现在 JavaScript 遇到下面的这段代码了:

function fun3() {

  console.log('fun3')

}

function fun2() {

  fun3();

}

function fun1() {

  fun2();

}

fun1();

 

执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,

就会将函数的执行上下文从栈中弹出。知道了这样的工作原理,让我们来看看如何处理上面这段代码:

 

// 伪代码

 

// fun1()

ECStack.push(<fun1> functionContext);

 

// fun1中调用了fun2,还要创建fun2的执行上下文

ECStack.push(<fun2> functionContext);

 

// fun2还调用了fun3

ECStack.push(<fun3> functionContext);

 

// fun3执行完毕

ECStack.pop();

 

// fun2执行完毕

ECStack.pop();

 

// fun1执行完毕

ECStack.pop();

 

// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext

 

####预处理

 

函数执行上下文

 

在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象

对局部数据进行预处理:

 

1.形参变量==>赋值(实参)

2.arguments==>赋值(实参列表)

3.处理 var定义的局部变量

4.处理 function声明的函数

5.提升

6.this==>赋值(调用函数的对象)

 

开始执行函数体代码

 

####应用堆栈信息快速定位问题

 

####1.Error对象和错误处理

 

当程序运行出现错误时, 通常会抛出一个 Error 对象. Error 对象可以作为用户自定义错误对象继承的原型.

 

Error.prototype 对象包含如下属性:

 

constructor–指向实例的构造函数

 

message–错误信息

 

name–错误的名字(类型)

 

上述是 Error.prototype 的标准属性, 此外, 不同的运行环境都有其特定的属性. 在例如 Node, Firefox, Chrome, Edge, IE 10+, Opera 以及 Safari 6+

 

这样的环境中, Error 对象具备 stack 属性, 该属性包含了错误的堆栈轨迹. 一个错误实例的堆栈轨迹包含了自构造函数之后的所有堆栈结构.


 

####2.如何查看调用栈

 

只查看调用栈:console.trace

a()

function a() {

  b();

}

function b() {

  c()

}

function c() {

  let aa = 1;

  console.trace()
}

 


 

####3.debugger打断点形式

 

###4.this的原理以及几种不同使用场景的取值

 

####JavaScript 的 this 原理

 

参考阮一峰的http://www.ruanyifeng.com/blog/2018/06/javascript-this.html

 

总结: this 代表了当前函数的执行上下文, 方便开发者在函数中使用上下文变量

 

####this的几种不同使用场景的取值

 

1作为对象方法调用

在 JavaScript 中,函数也是对象,因此函数可以作为一个对象的属性,此时该函数被称为该对象的方法,在使用这种调用方式时,this 被自然绑定到该对象

var test = {

  a:0,

  b:0,

  get:function(){

    return this.a;

  }

}

 

 

2.作为函数调用

函数也可以直接被调用,此时 this 绑定到全局对象。在浏览器中,window 就是该全局对象。比如下面的例子:函数被调用时,this 被绑定到全局对象,

接下来执行赋值语句,相当于隐式的声明了一个全局变量,这显然不是调用者希望的。

function makeNoSense(x) {

  this.x = x;

}

 

3.作为构造函数调用

javaScript 支持面向对象式编程,与主流的面向对象式编程语言不同,JavaScript 并没有类(class)的概念,而是使用基于原型(prototype)的继承方式。

相应的,JavaScript 中的构造函数也很特殊,如果不使用 new 调用,则和普通函数一样。作为又一项约定俗成的准则,构造函数以大写字母开头,

提醒调用者使用正确的方式调用。如果调用正确,this 绑定到新创建的对象上。

function Point(x, y){

  this.x = x;

  this.y = y;

}

 

 

4.在call或者apply,bind中调用

让我们再一次重申,在 JavaScript 中函数也是对象,对象则有方法,apply 和 call 就是函数对象的方法。

这两个方法异常强大,他们允许切换函数执行的上下文环境(context),即 this 绑定的对象。

很多 JavaScript 中的技巧以及类库都用到了该方法。让我们看一个具体的例子:

function Point(x, y){

   this.x = x;

   this.y = y;

   this.moveTo = function(x, y){

      this.x = x;

      this.y = y;

    }

}



var p1 = new Point(0, 0);

var p2 = {x: 0, y: 0};

p1.moveTo(1, 1);

p1.moveTo.apply(p2, [10, 10])

 

 

###5.闭包的实现原理和作用,可以列举几个开发中闭包的实际应用

 

####闭包的实现原理

 

参考资料:https://segmentfault.com/a/1190000011504517

 

JS中,在函数内部可以读取函数外部的变量

 

function outer(){

   var localVal = 30;

   return localVal;

}

outer();//30

 

 

但,在函数外部自然无法读取函数内的局部变量

 

function outer(){

   var localVal = 30;

}

alert(localVal);//error

 

 

这里有个需要注意的地方,函数内部声明变量的时候,一定要使用var命令。如果不用的话,实际上是声明了一个全局变量。

 

function outer(){

  localVal = 30;

  return localVal;

}

outer();

alert(localVal);//30

 

 

以上的表述,是JS变量的作用域的知识,它包括全局变量和局部变量。

 

Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。

 

function outer(){

  var localVal = 30;

  function inner(){

    alert(localVal);

   }

   return inner;

}

var func = outer();

func();//30

 

 

我们看到在上面的代码中,outer函数内又定义一个函数inner,outer函数的返回值是inner函数,inner函数把localVal alert出来。

 

我们可以看出以上代码的特点:函数嵌套函数,内部函数可以引用外部函数的参数和变量,参数和变量不会被垃圾回收机制收回。

 

代码中的inner函数,就是闭包。简单的说,闭包(closure)就是能够读取其他函数内部变量的函数。

 

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

 

在上面的代码中,函数inner被包含在函数outer内部,这时outer内部的所有局部变量,对inner都是可见的。

但是inner内部的局部变量,对oute 是不可见的。

这是Javascript语言特有的“链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。

所以,父对象的所有变量,对子对象都是可见的,反之则不成立。


 

####闭包的作用

 

使用闭包的好处:

 

-希望一个变量长期驻扎在内存当中;

 

-避免全局变量的污染;

 

-私有成员的存在

 

1.模块化代码

使用自执行的匿名函数来模拟块级作用域

 

(function(){

// 这里为块级作用域

})();

 

 

该方法经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数影响全局作用域。

也可以减少如闭包这样的对内存的占用,由于匿名函数没有变量指向,执行完毕就可以立即销毁其作用域链。

 

2.循环闭包

循环给每个li注册一个click事件,点击alert序号。代码如下:

 

var aLi = document.getElementByClassName("test");

function showAllNum( aLi ){

  for( var i =0,len = aLi.length ;i<len;i++ ){

   aLi[i].onclick = function(){

       alert( i );//all are aLi.length!

    }

   }

}

 

 

点击后会一直弹出同一个值 aLi.length 而不是123。当点击之前,循环已经结束,i值为aLi.length。

 

利用闭包,建一个匿名函数,将每个i存在内存中,onclick函数用的时候提取出外部匿名函数的i值。代码如下:

 

var aLi = document.getElementByClassName("test");

function showAllNum( aLi ){

   for( var i =0,len = aLi.length ;i<len;i++ ){

        (function(i){

           aLi[i].onclick = function(){

              alert( i );

            }

         })(i);

   }

}

 

 

实现解释:

1.作用域链

 

2.闭包函数的赋值与运行

 

实际上只是通过函数的赋值表式方式付给了标签点击事件,并没有运行;当遍历完后,i变成标签组的长度,根据作用域的原理,

向上找到for函数里的i,所以点击执行的时候都会弹出标签组的长度。闭包可以使变量长期驻扎在内存当中,

我们在绑定事件的时候让它自执行一次,把每一次的变量存到内存中;点击执行的时候就会弹出对应本作用域i的序号。

 

3.封装

外部无法直接获取函数内的变量,可通过暴露的方法获取

 

var info = function(){

   var _userId = 23492;

   var _typeId = 'item';



   function getUserId(){

     alert(_userId);

   }



   function getTypeId(){

     alert(_typeId);

   }

};



info.getUserId();//23492

info.getTypeId();//item



info._userId//undefined

info._typeId//undefined

 

 

但是这种方式会使我们在每一次创建新对象的时候都会创建一个这种方法。

使用原型来创建一个这种方法,避免每个实例都创建不同的方法。

在这里不做深究(一般构造函数加属性,原型加方法)。


 

####可以列举几个开发中闭包的实际应用

 

前端三大框架都会用到闭包

 

###6.理解堆栈溢出和内存泄漏的原理,如何防止

 

####堆栈溢出和内存泄漏的原理

 

循环引用

  一个很简单的例子:一个DOM对象被一个Javascript对象引用,与此同时又引用同一个或其它的Javascript对象,

这个DOM对象可能会引发内存泄露。这个DOM对象的引用将不会在脚本停止的时候被垃圾回收器回收。要想破坏循环引用,

引用DOM元素的对象或DOM对象的引用需要被赋值为null。

 

闭包

在闭包中引入闭包外部的变量时,当闭包结束时此对象无法被垃圾回收(GC)。

 

DOM泄露

当原有的COM被移除时,子结点引用没有被移除则无法回收。

 

Timers计(定)时器泄露

定时器也是常见产生内存泄露的地方:

 

####防止

 

根据原理设防

 

###7.如何处理循环的异步操作

 

####一、回调函数(callback)

 

function f1(callback){

  setTimeout(function () {

    // f1的任务代码

    callback();

  }, 1000);

}

// 执行

f1(f2)

 

 

采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。

 

回调函数是异步编程最基本的方法,其优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),

 

流程会很混乱,而且每个任务只能指定一个回调函数。

 

注意 区分 回调函数和异步

 

回调并不一定就是异步。他们自己并没有直接关系。

 

简单区分 同步回调 和 异步回调

 

同步回调 :

 

function A(callback){

  console.log("I am A");

  callback(); //调用该函数

}

function B(){

  console.log("I am B");

}

A(B);

 

 

异步回调:因为js是单线程的,但是有很多情况的执行步骤(ajax请求远程数据,IO等)是非常耗时的,如果一直单线程的堵塞下去会导致程序的等待时间过长页面失去响应,影响用户体验了。

 

如何去解决这个问题呢,我们可以这么想。耗时的我们都扔给异步去做,做好了再通知下我们做完了,我们拿到数据继续往下走。

 

var xhr = new XMLHttpRequest();

xhr.open('POST', url, true); //第三个参数决定是否采用异步的方式

xhr.send(data);

xhr.onreadystatechange = function(){

    if(xhr.readystate === 4 && xhr.status === 200){

    ///do something

   }

}

 

 

####二、事件监听

 

采用事件驱动模式。

 

任务的执行不取决代码的顺序,而取决于某一个事件是否发生。

 

监听函数有:on,bind,listen,addEventListener,observe

 

还是以f1和f2为例。首先,为f1绑定一个事件(采用jquery写法)。



f1.on('done',f2);



上面代码意思是,当f1发生done事件,就执行f2。



然后对f1进行改写:



function f1(){

  settimeout(function(){

    //f1的任务代码

    f1.trigger('done');

  },1000);

}



f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2.

 

 

这种方法的优点:比较容易理解,可以绑定多个事件,每一个事件可以指定多个回调函数,而且可以去耦合,有利于实现模块化。

 

这种方法的缺点:整个程序都要变成事件驱动型,运行流程会变得不清晰。

 

事件监听方法:

(1)onclick方法

(2)attachEvent和addEvenListener方法

 

####三、发布/订阅

 

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,

从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

 

这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。

 

首先,f2向"信号中心"jQuery订阅"done"信号。



jQuery.subscribe("done", f2);



function f1(){

  setTimeout(function () {

    // f1的任务代码

    jQuery.publish("done");

  }, 1000);

}

 

jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。

 

此外,f2完成执行后,也可以取消订阅(unsubscribe)

 

jQuery.unsubscribe("done", f2);

 

这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

 

####四、promise对象(promise 模式)

 

(1)promise对象是commonJS工作组提出的一种规范,一种模式,目的是为了异步编程提供统一接口。

 

(2)promise是一种模式,promise可以帮忙管理异步方式返回的代码。他讲代码进行封装并添加一个类似于事件处理的管理层。我们可以使用promise来注册代码,这些代码会在在promise成功或者失败后运行。

 

(3)promise完成之后,对应的代码也会执行。我们可以注册任意数量的函数再成功或者失败后运行,也可以在任何时候注册事件处理程序。

 

(4)promise有两种状态:1、等待(pending);2、完成(settled)。

 

promise会一直处于等待状态,直到它所包装的异步调用返回/超时/结束。

 

(5)这时候promise状态变成完成。完成状态分成两类:1、解决(resolved);2、拒绝(rejected)。

 

(6)promise解决(resolved):意味着顺利结束。promise拒绝(rejected)意味着没有顺利结束。

 

//promise

var p=new Promise(function(resolved))

//在这里进行处理。也许可以使用ajax

setTimeout(function(){

  var result=10*5;

  if(result===50){

    resolve(50);

  }else{

    reject(new Error('Bad Math'));

   }

},1000);

});

p.then(function(result){

   console.log('Resolve with a values of %d',result);

});

p.catch(function(){

  console.error('Something went wrong');

});

 

 

(1)代码的 关键在于setTimeout()的调用。

 

(2)重要的是,他调用了函数resolve()和reject()。resolve()函数告诉promise用户promise已解决;reject()函数告诉promise用户promise未能顺利完成。

 

(3)另外还有一些使用了promise代码。注意then和catch用法,可以将他们想象成onsucess和onfailure事件的处理程序。

 

(4)巧妙地方是,我们将promise处理与状态分离。也就是说,我们可以调用p.then(或者p.catch)多少次都可以,不管promise是什么状态。

 

(5)promise是ECMAscript 6管理异步代码的标准方式,javascript库使用promise管理ajax,动画,和其他典型的异步交互。

 

简单的说,它的思想是:每一个异步任务返回一个promise对象,该对象有一个then方法,允许指定回调函数。

 

####五、优雅的async/await

 

参考资料:http://www.ruanyifeng.com/blog/2015/05/async.html


 

###8.理解模块化解决的实际问题,可列举几个模块化方案并理解其中原理

 

####模块化的类型

 

####立即执行函数

 

可以通过立即执行函数,来达到隐藏细节的目的,看下面的代码,

 

var myModule = (function(){

   var var1 = 1;

   var var2 = 2;

   function fn1(){

   }

   function fn2(){

   }

   return {

     fn1: fn1,

     fn2: fn2

   };

})();

 

 

####CommonJS

 

我们先从CommonJS谈起,因为在网页端没有模块化编程只是页面JavaScript逻辑复杂,但也可以工作下去,

在服务器端却一定要有模块,所以虽然JavaScript在web端发展这么多年,第一个流行的模块化规范却由服务器端的JavaScript应用带来,

CommonJS规范是由NodeJS发扬光大,这标志着JavaScript模块化编程正式登上舞台。

一,定义模块:

根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块读取,除非定义为global对象的属性。

二,模块输出:

模块只有一个出口,module.exports对象,我们需要把模块希望输出的内容放入该对象。

三,加载模块:

加载模块使用require方法,该方法读取一个文件并执行,返回文件内部的module.exports对象。

 

var name = 'Byron';

function printName(){

   console.log(name);

}

function printFullName(firstName){

   console.log(firstName + name);

}

module.exports = {

   printName: printName,

   printFullName: printFullName

}

 

 

然后加载模块,

var nameModule = require('./myModel.js');

nameModule.printName();

不同的实现对require时的路径有不同要求,一般情况可以省略js拓展名,可以使用相对路径,

也可以使用绝对路径,甚至可以省略路径直接使用模块名(前提是该模块是系统内置模块)。

 

####AMD

 

AMD 即Asynchronous Module Definition,中文名是异步模块定义的意思。它是一个在浏览器端模块化开发的规范,

由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎RequireJS,

实际上AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出。

 

requireJS主要解决两个问题:

一,多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器;

二,js加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长。

 

// 定义模块 myModule.js



define(['dependency'], function(){

   var name = 'Byron';

   function printName(){

     console.log(name);

   }

   return {

   printName: printName

   };

});

// 加载模块

require(['myModule'], function (my){

  my.printName();

});

 


 

requireJS定义了一个函数 define,它是全局变量,用来定义模块。

define(id?, dependencies?, factory);

------id:可选参数,用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉拓展名);

------dependencies:是一个当前模块依赖的模块名称数组

------factory:工厂方法,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值;

 

在页面上使用require函数加载模块;

require([dependencies], function(){});

require()函数接受两个参数:

第一个参数是一个数组,表示所依赖的模块;

第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。

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

 

####CMD

 

CMD 即Common Module Definition通用模块定义,CMD规范是国内发展出来的,就像AMD有个requireJS,CMD有个浏览器的实现SeaJS,

SeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同

 

语法

Sea.js 推崇一个模块一个文件,遵循统一的写法

define

define(id?, deps?, factory)

因为CMD推崇一个文件一个模块,所以经常就用文件名作为模块id;CMD推崇依赖就近,所以一般不在define的参数中写依赖,而是在factory中写。

factory有三个参数:

function(require, exports, module){}

一,require

require 是 factory 函数的第一个参数,require 是一个方法,接受 模块标识 作为唯一参数,用来获取其他模块提供的接口;

二,exports

exports 是一个对象,用来向外提供模块接口;

三,module

module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。

 

demo

// 定义模块 myModule.js

define(function(require, exports, module) {

   var $ = require('jquery.js')

   $('div').addClass('active');

});



// 加载模块

seajs.use(['myModule.js'], function(my){



});

 

####ES6模块化


 

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块设计思想:尽量的静态化、使得编译时就能确定模块的依赖关系,以及输入和输出的变量(CommonJS和AMD模块,都只能在运行时确定这些东西)。

 

使用方式:

 

// 导入

import "/app";

import React from “react”;

import { Component } from “react”;

// 导出

export function multiply() {...};

export var year = 2018;

export default ...

...

 

 

优点:

容易进行静态分析

面向未来的 EcmaScript 标准

 

缺点:

原生浏览器端还没有实现该标准

全新的命令字,新版的 Node.js才支持。






 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值