从屌丝到架构师的飞越(JavaScript篇)-作用域链及闭包

一、介绍

作用域链就是根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。

想要知道js怎么链式查找,就得先了解js的执行环境

执行环境(execution context)

每个函数运行时都会产生一个执行环境,而这个执行环境怎么表示呢?js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。

全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。

js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。这个就是刚才说的链式结构。

JavaScript 变量可以是局部变量或全局变量。

私有变量可以用到闭包。

闭包就是一个函数引用另一个函数的变量,因为变量被引用着所以不会被回收,因此可以用来封装一个私有变量。这是优点也是缺点,不必要的闭包只会增加内存消耗。

或者说闭包就是子函数可以使用父函数的局部变量,还有父函数的参数。

二、知识点介绍

1、作用域链

2、闭包

3、闭包和作用域链

4、作用域链知识总结

5、this对象

三、上课对应视频的说明文档

1、作用域链

谈起作用域链,我们就不得不从作用域开始谈起。因为所谓的作用域链就是由多个作用域组成的。那么, 什么是作用域呢?

1.1、什么是作用域

1.1.1作用域是一个函数在执行时期的执行环境。

执行环境是JavaScript中的重要概念之一。执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

全局执行环境是最外围的一个执行环境。在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境知道应用程序退出–例如关闭网页或浏览器—时才会被销毁)

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行后,栈将其环境弹出,把控制权返回给之前的执行环境。

执行环境的建立分为两个阶段:进入执行上下文(创建阶段)和执行阶段(激活/执行阶段)

1)进入上下文阶段:发生在函数调用时,但在执行具体代码之前。具体完成创建作用域链;创建变量、函数和参数以及求this的值

2)执行代码阶段:主要完成变量赋值、函数引用和解释/执行其他代码

总的来说可以将执行上下文看作是一个对象

EC = {

VO:{/*函数中的arguments对象、参数、内部变量以及函数声明*/}

this:{},

Scope:{/*VO以及所有父执行上下文中的VO*/}

}

每一个函数在执行的时候都有着其特有的执行环境,ECMAScript标准规定,在javascript中只有函数才拥有作用域。换句话,也就是说,JS中不存在块级作用域。比如下面这样:

function getA() {

if (false) {

var a = 1;

}

console.log(a);  //undefined

}

getA();function getB() {

console.log(b);

}

getB();    // ReferenceError: b is not defined

上面的两段代码,区别在于 :getA()函数中,有变量a的声明,而getB()函数中没有变量b的声明。

另外还有一点,关于作用域中的声明提前。

1.1.2.作用域中声明提前

在上面的getA()函数中,或许你还存在着疑惑,为什么a="undefined"呢,具体原因就是因为作用域中的声明提前:所以getA()函数和下面的写法是等价的:

function getA(){

 var a;

  if(false){

    a=1

    };

  console.log(a);

}

既然提到变量的声明提前,那么只需要搞清楚三个问题即可:

1)什么是变量

2)什么是变量声明

3)声明提前到什么时候。

1)什么是变量?

每一个执行环境都对应一个变量对象,在该执行环境中定义的所有变量和函数都存放在其对应的变量对象中。

1)进入执行上下文时,VO的初始化过程如下:

函数的形参:变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为undefined;

函数声明:变量对象的一个属性,其属性名和属性值都是函数对象创建出来的,如果变量对象已经办好了相同名字的属性,则替换它的值

变量声明:变量对象的一个属性,其属性名即为变量名,其值为undefined;如果变量名和已经声明的函数名或者函数的参数名,则不会影响已经存在的属性

2)执行代码阶段,变量对象中的一些属性undefined值将会确定

这里需要说明一下:函数表达式不包含在变量对象之中

var foo = 10; 

function bar() {} // function declaration, FD 

(function baz() {}); // function expression, FE 

console.log( 

this.foo == foo, // true 

window.bar == bar // true 

); 

console.log(baz); // ReferenceError, "baz" is not defined 

之后,全局上下文的变量对象为

变量包括两种,普通变量和函数变量。

普通变量:凡是用var标识的都是普通变量。比如下面 :

var x=1;              var object={};var  getA=function(){};  //以上三种均是普通变量,但是这三个等式都具有赋值操作。所以,要分清楚声明和赋值。声明是指 var x; 赋值是指 x=1;

函数变量:函数变量特指的是下面的这种,fun就是一个函数变量。

function fun(){} ;// 这是指函数变量. 函数变量一般也说成函数声明。

类似下面这样,不是函数声明,而是函数表达式

var getA=function(){}      //这是函数表达式var getA=function fun(){}; //这也是函数表达式,不存在函数声明。关于函数声明和函数表达式的区别,详情见javascript系列---函数篇第二部分

2)什么是变量声明?

变量有普通变量和函数变量,所以变量的声明就有普通变量声明和函数变量声明。

普通变量声明

var x=1; //声明+赋值var object={};  //声明+赋值

上面的两个变量执行的时候总是这样的

var x = undefined;      //声明var object = undefined; //声明

x = 1;                  //赋值

object = {};            //赋值

关于声明和赋值,请注意,声明是在函数第一行代码执行之前就已经完成,而赋值是在函数执行时期才开始赋值。所以,声明总是存在于赋值之前。而且,普通变量的声明时期总是等于undefined.

函数变量声明

函数变量声明指的是下面这样的:

function getA(){}; //函数声明

3)声明提前到什么时候?

所有变量的声明,在函数内部第一行代码开始执行的时候就已经完成。-----声明的顺序见

1.2、活动对象

当函数被调用的时候,一个特殊的对象–活动对象将会被创建。这个对象中包含形参和arguments对象。活动对象之后会作为函数上下文的变量对象来使用。换句话说,活动对象除了变量和函数声明之外,它还存储了形参和arguments对象。

1.3、作用域详解

由以上介绍可知,当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数对象处于第三位……直至作为作用域终点的全局执行环境

函数的作用域,也就是函数的执行环境,所以函数作用域内肯定保存着函数内部声明的所有的变量。

一个函数在执行时所用到的变量无外乎来源于下面三种:

1)函数的参数----来源于函数内部的作用域

2)在函数内部声明的变量(普通变量和函数变量)----也来源于函数内部作用域

3)来源于函数的外部作用域的变量,放在1.3中讲。

比如下面这样:

var x = 1;function add(num) () {

var y = 1;

return x + num + y;  //x来源于外部作用域,num来源于参数(参数也属于内部作用域),y来源于内部作用域。

}

那么一个函数的作用域到底是什么呢?

在一个函数被调用的时候,函数的作用域才会存在。此时,在函数还没有开始执行的时候,开始创建函数的作用域:

函数作用域的创建步骤:

1)函数形参的声明。

2)函数变量的声明

3)普通变量的声明。 

4)函数内部的this指针赋值

5)函数内部代码开始执行! 

所以,在这里也解释了,为什么说函数被调用时,声明提前,在创建函数作用域的时候就会先声明各种变量。

关于变量的声明,这里有几点需要强调

1)函数形参在声明的时候已经指定其形参的值。 

function add(num) {

var num;

console.log(num);  //1

}

add(1);

2)在第二步函数变量的生命中,函数变量会覆盖以前声明过的同名声明。

function add(num1, fun2) {

function fun2() {

var x = 2;

}

console.log(typeof num1); //function 

console.log(fun2.toString()) //functon fun2(){ var x=2;}

}

add(function () {

}, function () {

var x = 1

});

3)在第三步中,普通变量的声明,不会覆盖以前的同名参数

function add(fun,num) {

var fun,num;

console.log(typeof fun) //function

console.log(num);      //1

}

add(function(){},1);

在所有的声明结束后,函数才开始执行代码!!!

function compare(value1,value2){

if(value1 < value2){

return -1;

} else if( value1 > value2 ) {

return 1;

} else {

return 0;

}

}

以上代码定义了compare()函数,然后又在全局作用域中调用了它。当调用compare()时,会创建一个包含arguments、value1、value2的活动对象。全局执行环境的变量对象(包含result和compare)在compare()执行环境的作用域链中则处于第二位。下图包含了上述关系的compare()函数执行时的作用域链。

后台的每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。在创建compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链会被保存在内部的[[Scope]]属性中。当调用compare()函数时,会为函数创建一个执行环境,然后通过赋值函数的[[Scope]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对象别创建并被推入执行环境作用域链的前端。对于这个例子中,compare()函数的执行函数而言,其作用域链中包含两个变量对象:本地活动对象和全局便朗对象。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

1.4.作用域链的组成

在JS中,函数的可以允许嵌套的。即,在一个函数的内部声明另一个函数

类似这样:

function A(){

var  a=1;

function B(){  //在A函数内部,声明了函数B,这就是所谓的函数嵌套。

var b=2; 

}

}

对于A来说,A函数在执行的时候,会创建其A函数的作用域, 那么函数B在创建的时候,会引用A的作用域,类似下面这样

函数B在执行的时候,其作用域类似于下面这样:

从上面的两幅图中可以看出,函数B在执行的时候,是会引用函数A的作用域的。所以,像这种函数作用域的嵌套就组成了所谓的函数作用域链。当在自身作用域内找不到该变量的时候,会沿着作用域链逐步向上查找,若在全局作用域内部仍找不到该变量,则会抛出异常。

2、闭包

2.1、什么是闭包

一般来说,当某个环境中的所有代码执行完毕后,该环境被销毁(弹出环境栈),保存在其中的所有变量和函数也随之销毁(全局执行环境变量直到应用程序退出,如网页关闭才会被销毁)

但是像上面那种有内部函数的又有所不同,当outer()函数执行结束,执行环境被销毁,但是其关联的活动对象并没有随之销毁,而是一直存在于内存中,因为该活动对象被其内部函数的作用域链所引用。

具体如下图:

outer执行结束,内部函数开始被调用

outer执行环境等待被回收,outer的作用域链对全局变量对象和outer的活动对象引用都断了

像上面这种内部函数的作用域链仍然保持着对父函数活动对象的引用,就是闭包(closure)

2.2、闭包的作用

闭包有两个作用:

第一个就是可以读取自身函数外部的变量(沿着作用域链寻找)

第二个就是让这些外部变量始终保存在内存中

关于第二点,来看一下以下的代码:

返回结果很出乎意料吧,你肯定以为依次返回0,1,但事实并非如此

来看一下调用fn[0]()的作用域链图:

可以看到result[0]函数的活动对象里并没有定义i这个变量,于是沿着作用域链去找i变量,结果在父函数outer的活动对象里找到变量i(值为2),而这个变量i是父函数执行结束后将最终值保存在内存里的结果。

由此也可以得出,js函数内的变量值不是在编译的时候就确定的,而是等在运行时期再去寻找的。

那怎么才能让result数组函数返回我们所期望的值呢?

看一下result的活动对象里有一个arguments,arguments对象是一个参数的集合,是用来保存对象的。

那么我们就可以把i当成参数传进去,这样一调用函数生成的活动对象内的arguments就有当前i的副本。

改进之后:

虽然得到了期望的结果,但是又有人问这算闭包吗?调用内部函数的时候,父函数的环境变量还没被销毁呢,而且result返回的是一个整型数组,而不是一个函数数组!

确实如此,那就让arg(num)函数内部再定义一个内部函数就好了:

这样result返回的其实是innerarg()函数

当调用outer,for循环内i=0时的作用域链图如下:

由上图可知,当调用innerarg()时,它会沿作用域链找到父函数arg()活动对象里的arguments参数num=0.

上面代码中,函数arg在outer函数内预先被调用执行了,对于这种方法,js有一种简洁的写法

function outer(){

var result = new Array();

for(var i = 0; i < 2; i++){

//定义一个带参函数

result[i] = function(num){

function innerarg(){

return num;

}

return innerarg;

}(i);//预先执行函数写法

//把i当成参数传进去

}

return result;

}

闭包的概念:有权访问另一个作用域的函数。

这句话就告诉我们,第一,闭包是一个函数。第二,闭包是一个能够访问另一个函数作用域。

那么,类似下面这样,

function A(){

var a=1;

function B(){  //闭包函数,函数b能够访问函数a的作用域。所以,像类似这么样的函数,我们就称为闭包

}

}

所以,创建闭包的方式就是在一个函数的内部,创建另外一个函数。那么,当外部函数被调用的时候,内部函数也就随着创建,这样就形成了闭包。比如下面。

var fun = undefined;function a() {

var a = 1;

fun = function () {

}

}

2.3、闭包所引起的问题

其实,理解什么是闭包并不难,难的是闭包很容易引起各种各样的问题。

2.3.1、变量污染

看下面的这道例题:

var funB,

funC;

(function() {

var a = 1;

funB = function () {

a = a + 1;

console.log(a);

}

funC = function () {

a = a + 1;

console.log(a);

}

}());

funB();  //2

funC();  //3.

对于 funB和funC两个闭包函数,无论是哪个函数在运行的时候,都会改变匿名函数中变量a的值,这种情况就会污染了a变量。

两个函数的在运行的时候作用域如下图:

在这幅图中,变量a可以被函数funB和funC改变,就相当于外部作用域链上的变量对内部作用域来说都是静态的变量,这样,就很容易造成变量的污染。还有一道最经典的关于闭包的例题:

var array = [

];for (var i = 0; i < 10; i++) {

var fun = function () {

console.log(i);

}

array.push(fun);

}var index = array.length;while (index > 0) {

array[--index]();

} //输出结果 全是10;

想这种类似问题产生的根源就在于,没有注意到外部作用域链上的所有变量均是静态的。

所以,为了解决这种变量的污染问题---而引入的闭包的另外一种使用方式。

那么它是如何解决这种变量污染的呢?  思想就是: 既然外部作用域链上的变量时静态的,那么将外部作用域链上的变量拷贝到内部作用域不就可以啦!! 具体怎么拷贝,当然是通过函数传参的形式啊。

以第一道例题为例:

var funB,funC;

(function () {

var a = 1;

(function () {

funB = function () {

a = a + 1;

console.log(a);

}

}(a));

(function (a) {

funC = function () {

a = a + 1;

console.log(a);

}

}(a));

}());

funB()||funC();  //输出结果全是2 另外也没有改变作用域链上a的值。

在函数执行时,内存的结构如图所示:

由图中内存结构示意图可见,为了解决闭包的这种变量污染的问题,而加了一层函数嵌套(通过匿名函数自执行),这种方式延长了闭包函数的作用域链。

2.3.2、内存泄露

内存泄露其实严格来说,就是内存溢出了,所谓的内存溢出,当时就是内存空间不够用了啊。

那么,闭包为什么会引起内存泄露呢?

var fun = undefined;function A() {

var a = 1;

fun = function () {

}

}

看上面的例题,只要函数fun存在,那么函数A中的变量a就会一直存在。也就是说,函数A的作用域一直得不到释放,函数A的作用域链也不能得到释放。如果,作用域链上没有很多的变量,这种牺牲还可有可无,但是如果牵扯到DOM操作呢?

var element = document.getElementById('myButton');

(function () {

var myDiv = document.getElementById('myDiv')

element.onclick = function () {

//处理程序  }

}())

像这样,变量myDiv如果是一个占用内存很大的DOM....如果持续这么下去,内存空间岂不是一直得不到释放。久而久之,变引起了内存泄露(也是就内存空间不足)。

3、闭包与作用域链

无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是闭包的情况又有所不同。

function createComparisionFunction(propertyName) {

return function(object1,object2) {

var value1 = object1[propertyName];

var value2 = object2[propertyName];

if(value1 < value2){

return -1;

} else if( value1 > value2 ) {

return 1;

} else {

return 0;

}

}

}

在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在createComparisonFunction()函数内部定义的匿名函数作用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象。

var compare = createComparisonFunction('name');var result = compare({name:'Nicolas'},{name:'Greg'});

//解除对匿名函数的引用,以便释放内存

compareName = null;

当上述代码执行时,下图展示了包含函数与内部匿名函数的作用域链

在匿名函数从createComparisonFunction()中被返回后,它的作用域链被初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在createComparisonFunction()中定义的所有变量。更为重要的是, createComparisonFunction()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。即当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象任然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。

4、作用域链知识总结

当代码在一个环境中执行时,都会创建一个作用域链。 作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。整个作用域链的本质是一个指向变量对象的指针列表。作用域链的最前端,始终是当前正在执行的代码所在环境的变量对象。

  如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,就是函数内部的arguments对象。作用域链中的下一个变量对象来自该函数的包含环境,而再下一个变量对象来自再下一个包含环境。这样,一直延续到全局执行环境,全局执行环境的变量对象始终是作用域链中的最后一个对象。

作用域

先来谈谈变量的作用域

变量的作用域无非就是两种:全局变量和局部变量。

4.1.1、全局作用域:

最外层函数定义的变量拥有全局作用域,即对任何内部函数来说,都是可以访问的:

4.1.2、局部作用域:

和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的,最常见的例如函数内部

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

再来看一个代码:

很有趣吧,第一个输出居然是undefined,原本以为它会访问外部的全局变量(scope=”global”),但是并没有。这可以算是javascript的一个特点,只要函数内定义了一个局部变量,函数在解析的时候都会将这个变量“提前声明”:

然而,也不能因此草率地将局部作用域定义为:用var声明的变量作用范围起止于花括号之间。

javascript并没有块级作用域

4.1.3、块级作用域

像在C/C++中,花括号内中的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,比如下面的c语言代码:

for(int i = 0; i < 10; i++){

//i的作用范围只在这个for循环

}

printf("%d",&i);//error

但是javascript不同,并没有所谓的块级作用域,javascript的作用域是相对函数而言的,可以称为函数作用域:

4.2、作用域链(Scope Chain)

那什么是作用域链?

我的理解就是,根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。

想要知道js怎么链式查找,就得先了解js的执行环境

4.2.1、执行环境(execution context)

每个函数运行时都会产生一个执行环境,而这个执行环境怎么表示呢?js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。

全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。

js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。

举个例子:

上面代码执行情况演示:

了解了环境变量,再详细讲讲作用域链。

当某个函数第一次被调用时,就会创建一个执行环境(execution context)以及相应的作用域链,并把作用域链赋值给一个特殊的内部属性([scope])。然后使用this,arguments(arguments在全局环境中不存在)和其他命名参数的值来初始化函数的活动对象(activation object)。当前执行环境的变量对象始终在作用域链的第0位。

以上面的代码为例,当第一次调用fn1()时的作用域链如下图所示:

(因为fn2()还没有被调用,所以没有fn2的执行环境)

可以看到fn1活动对象里并没有scope变量,于是沿着作用域链(scope chain)向后寻找,结果在全局变量对象里找到了scope,所以就返回全局变量对象里的scope值。

标识符解析是沿着作用域链一级一级地搜索标识符地过程。搜索过程始终从作用域链地前端开始,然后逐级向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致错误发生

那作用域链地作用仅仅只是为了搜索标识符吗?

再来看一段代码:

outer()内部返回了一个inner函数,当调用outer时,inner函数的作用域链就已经被初始化了(复制父函数的作用域链,再在前端插入自己的活动对象),具体如下图:

5、this对象

关于闭包经常会看到这么一道题:

  var name = "The Window";

  var object = {

    name : "My Object",

    getNameFunc : function(){

      return function(){

        return this.name;

      };

    }

  };

  alert(object.getNameFunc()());//result:The Window

《javascript高级程序设计》一书给出的解释是:

this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象调用时,this等于那个对象。不过,匿名函数具有全局性,因此this对象同常指向window.

你可能感兴趣的:(从屌丝到架构师的飞越(JavaScript篇)-作用域链及闭包)