导航

剖析JavaScript函数作用域与闭包

Posted on 2016-02-17 15:42  哈哈镜-力争上游  阅读(167)  评论(0编辑  收藏  举报

在我们写代码写到一定阶段的时候,就会想深究一下js,javascript是一种弱类型的编程语言,而js中一个最为重要的概念就是执行环境,或者说作用域。作用域重要性体现在哪呢?首先,函数在执行时会创建作用域链,我们可以通过作用域链,向上一层一层的找到我们想要的变量。其次,搞清楚作用域,有利于深层次的理解闭包。再次,可以写出更加优化的代码,提高执行速度,防止内存溢出。可以说,作用域是一个核心。

如果大家知道c语言,可以知道块级作用域的特点,每一块都是局部的作用域,在if(){}外边是访问不了if里面的变量的。但是js没有块级作用域概念,相对的是函数的作用域(执行环境)。如果我们把js跟c或c++结合起来理解的话,函数的执行就容易理解。

相信大家都知道,作用域(执行环境)分为全局作用域和局部作用域。在函数外部定义的变量或函数,都是具有全局作用域,而函数里面定义的变量,则具有局部作用域。

任何函数的执行依赖于函数中的变量作用域,变量作用域是在函数定义时决定的,而不是在函数执行时决定的。

先说说普通函数的执行。

function fn1(){
    var test = 1;
    fn2();
    alert(test);
}
function fn2(){
    test++;
}
fn1();

那结果是几呢?对不起,会直接报错。这就是上面所说的,虽然fn2执行时,会创建执行环境,但是fn2的作用域链中并没有fn1,因为fn2中的test的作用域是在fn2函数定义时确定的,画图表示就是:

从这个作用域链上,可以看出来,fn2在执行时,本身找不到test变量,则会顺着作用域链向上找,上层作用链就是全局变量,而不是fn1的作用域里的变量。在全局变量对象中找不到test变量,那么就报错了。

那么如果把代码变化一下:

function fn1(){
    var test = 1;
    (function fn2(){
        test++;
    }())
    alert(test);
}
                
fn1(); //2
fn1(); //2

这时结果就不会报错了,稍稍改动,变化如此之大,这是为什么?相信大家很清楚了。再画一下作用域链图:

在上面的图中出现了活动对象,什么是活动对象?

当fn1执行时,fn1会被压入栈中执行,这时会创建fn1的执行环境(作用域)。然后会使用arguments和参数、变量初始化活动对象。这个活动对象也就是变量对象

,当执行到fn2时,又会把fn2函数压入栈顶,执行fn2,创建执行环境、作用域链、活动对象,当fn2执行完成后,fn2的执行环境就会销毁,作用域链和活动对象也会被销毁,然后把执行权还给fn1执行。执行到alert(test),把2弹出。

当是闭包时,情况就有很大的不同。(从技术上,每个函数都是一个闭包,这里的闭包讨论的是一个函数返回另外一个函数的情况)

 

function outer(){
    var a= 0;
    return function inner(){
        return a++;
    }
}
var result = outer();
console.log(result()); //0
console.log(result()); //1
console.log(result()); //2

这个结果不是0,而是a进行了累加。这的原理又是什么呢?

原因还是函数的作用域链是在定义时确定的,而且在函数执行时仍然存在。

分析一下执行的步骤:

当执行outer()时,会把outer函数推入栈,当return时,返回的是一个函数而不是一个数值。这个inner()函数在定义时就有作用域链,作用域链中有外层outer函数中的变量,当outer返回后,inner()函数的作用域链还存在着,也就是我们平时说的inner函数占用了outer函数中的a变量。那为什么inner函数的作用域链中会有outer函数的变量呢?原因是在一个函数内部定义的函数会将包含函数(即外层函数)的活动对象添加到它的作用域链中。

再在上面的基础上继续变化一下,看看结果如何变化:

function outer(){
    var a= 0;
    return function inner(){
        return a++;
    }
}
var result = outer();
console.log(result()); //0
console.log(result()); //1
console.log(result()); //2

var ox = outer()();
console.log(ox); 

这时,console.log(ox)的值是多少呢? 答案是0,而不是3

原因也很简单,函数每次运行,都会产生独立的执行环境(作用域),也就是说两次outer()的执行没有关系。上一次outer()执行完以后,它的执行环境早就已经释放了。

使用闭包有明显的好处,可以分别保存变量的值。特别在处理一些点击事件时,需要分别保存不同的变量值。但是闭包还是要慎用,因为闭包会使一些变量无法释放,占用内存,不利于优化。

闭包优化方法:

在我们不再使用用返回的值是,将其置为null

如把result和ox置为null,就可以解除对函数的引用,释放内存。

function outer(){
    var a= 0;
    return function inner(){
        return a++;
    }
}
var result = outer();
console.log(result()); //0
console.log(result()); //1
console.log(result()); //2

var ox = outer()();
console.log(ox); 
result = null;
ox = null;

以上是我对作用域和闭包的一些认识,有错误或不足的地方欢迎大家指正!