深刻javascript——做用域和闭包

做用域和做用域链是javascript中很是重要的特性,对于他们的理解直接关系到对于整个javascript体系的理解,而闭包又是对做用域的延伸,也是在实际开发中常用的一个特性,实际上,不只仅是javascript,在不少语言中都提供了闭包的特性。javascript

做用域

做用域是一个变量和函数的做用范围,javascript中函数内声明的全部变量在函数体内始终是可见的,在javascript中有全局做用域和局部做用域,可是没有块级做用域,局部变量的优先级高于全局变量,经过几个示例来了解下javascript中做用域的那些“潜规则”(这些也是在前端面试中常常问到的问题)。
1. 变量声明提早
示例1:前端

var scope="global";
function scopeTest(){
    console.log(scope);
    var scope="local"  
}
scopeTest(); //undefined

此处的输出是undefined,并无报错,这是由于在前面咱们提到的函数内的声明在函数体内始终可见,上面的函数等效于:java

var scope="global";
function scopeTest(){
    var scope;
    console.log(scope);
    scope="local"  
}
scopeTest(); //local

注意,若是忘记var,那么变量就被声明为全局变量了。
2. 没有块级做用域
和其余咱们经常使用的语言不一样,在Javascript中没有块级做用域:面试

function scopeTest() {
    var scope = {};
    if (scope instanceof Object) {
        var j = 1;
        for (var i = 0; i < 10; i++) {
            //console.log(i);
        }
        console.log(i); //输出10
    }
    console.log(j);//输出1

}

在javascript中变量的做用范围是函数级的,即在函数中全部的变量在整个函数中都有定义,这也带来了一些咱们稍不注意就会碰到的“潜规则”:segmentfault

var scope = "hello";
function scopeTest() {
    console.log(scope);//①
    var scope = "no";
    console.log(scope);//②
}

在①处输出的值居然是undefined,简直丧心病狂啊,咱们已经定义了全局变量的值啊,这地方不该该为hello吗?其实,上面的代码等效于:浏览器

var scope = "hello";
function scopeTest() {
    var scope;
    console.log(scope);//①
    scope = "no";
    console.log(scope);//②
}

声明提早、全局变量优先级低于局部变量,根据这两条规则就不难理解为何输出undefined了。闭包

做用域链

在javascript中,每一个函数都有本身的执行上下文环境,当代码在这个环境中执行时,会建立变量对象的做用域链,做用域链是一个对象列表或对象链,它保证了变量对象的有序访问。
做用域链的前端是当前代码执行环境的变量对象,常被称之为“活跃对象”,变量的查找会从第一个链的对象开始,若是对象中包含变量属性,那么就中止查找,若是没有就会继续向上级做用域链查找,直到找到全局对象中:函数

做用域链的逐级查找,也会影响到程序的性能,变量做用域链越长对性能影响越大,这也是咱们尽可能避免使用全局变量的一个主要缘由。性能

闭包

  • 基础概念

做用域是理解闭包的一个前提,闭包是指在当前做用域内老是能访问外部做用域中的变量。ui

function createClosure(){
    var name = "jack";
    return {
        setStr:function(){
            name = "rose";
        },
        getStr:function(){
            return name + ":hello";
        }
    }
}
var builder = new createClosure();
builder.setStr();
console.log(builder.getStr()); //rose:hello

上面的示例在函数中返回了两个闭包,这两个闭包都维持着对外部做用域的引用,所以无论在哪调用老是可以访问外部函数中的变量。在一个函数内部定义的函数,会将外部函数的活跃对象添加到本身的做用域链中,所以上面实例中经过内部函数可以访问外部函数的属性,这也是javascript模拟私有变量的一种方式。
请输入图片描述
注意:因为闭包会额外的附带函数的做用域(内部匿名函数携带外部函数的做用域),所以,闭包会比其它函数多占用些内存空间,过分的使用可能会致使内存占用的增长。

  • 闭包中的变量
    在使用闭包时,因为做用域链机制的影响,闭包只能取得内部函数的最后一个值,这引发的一个反作用就是若是内部函数在一个循环中,那么变量的值始终为最后一个值。
//该实例不太合理,有必定延迟因素,此处主要为了说明闭包循环中存在的问题
    function timeManage() {
        for (var i = 0; i < 5; i++) {
            setTimeout(function() {
                console.log(i);
            },1000)
        };
    }

上面的程序并无按照咱们预期的输入1-5的数字,而是5次所有输出了5。再来看一个示例:

function createClosure(){
    var result = [];
    for (var i = 0; i < 5; i++) {
        result[i] = function(){
            return i;
        }
    }
    return result;
}

调用createClosure()[0]()返回的是5,createClosure()[4]()返回值仍然是5。经过以上两个例子能够看出闭包在带有循环的内部函数使用时存在的问题:由于每一个函数的做用域链中都保存着对外部函数(timeManage、createClosure)的活跃对象,所以,他们都引用着同一变量i,当外部函数返回时,此时的i值为5,因此内部的每一个函数i的值也为5。
那么如何解决这个问题呢?咱们能够经过匿名包裹器(匿名自执行函数表达式)来强制返回预期的结果:

function timeManage() {
    for (var i = 0; i < 5; i++) {
        (function(num) {
            setTimeout(function() {
                console.log(num);
            }, 1000);
        })(i);
    }
}

或者在闭包匿名函数中再返回一个匿名函数赋值:

function timeManage() {
    for (var i = 0; i < 10; i++) {
        setTimeout((function(e) {
            return function() {
                console.log(e);
            }
        })(i), 1000)
    }
}
//timeManager();输出1,2,3,4,5
function createClosure() {
    var result = [];
    for (var i = 0; i < 5; i++) {
        result[i] = function(num) {
            return function() {
                console.log(num);
            }
        }(i);
    }
    return result;
}
//createClosure()[1]()输出1;createClosure()[2]()输出2

不管是匿名包裹器仍是经过嵌套匿名函数的方式,原理上都是因为函数是按值传递,所以会将变量i的值复制给实参num,在匿名函数的内部又建立了一个用于返回num的匿名函数,这样每一个函数都有了一个num的副本,互不影响了。

  • 闭包中的this

在闭包中使用this时要特别注意,稍微不慎可能会引发问题。一般咱们理解this对象是运行时基于函数绑定的,全局函数中this对象就是window对象,而当函数做为对象中的一个方法调用时,this等于这个对象(TODO 关于this作一次整理)。因为匿名函数的做用域是全局性的,所以闭包的this一般指向全局对象window:

var scope = "global";
var object = {
    scope:"local",
    getScope:function(){
        return function(){
            return this.scope;
        }
    }
}

调用object.getScope()()返回值为global而不是咱们预期的local,前面咱们说过闭包中内部匿名函数会携带外部函数的做用域,那为何没有取得外部函数的this呢?每一个函数在被调用时,都会自动建立thisarguments,内部匿名函数在查找时,搜索到活跃对象中存在咱们想要的变量,所以中止向外部函数中的查找,也就永远不可能直接访问外部函数中的变量了。总之,在闭包中函数做为某个对象的方法调用时,要特别注意,该方法内部匿名函数的this指向的是全局变量。
幸运的是咱们能够很简单的解决这个问题,只须要把外部函数做用域的this存放到一个闭包能访问的变量里面便可:

var scope = "global";
var object = {
    scope:"local",
    getScope:function(){
        var that = this;
        return function(){
            return that.scope;
        }
    }
}

object.getScope()()返回值为local

  • 内存与性能
    因为闭包中包含与函数运行期上下文相同的做用域链引用,所以,会产生必定的负面做用,当函数中活跃对象和运行期上下文销毁时,因为必要仍存在对活跃对象的引用,致使活跃对象没法销毁,这意味着闭包比普通函数占用更多的内存空间,在IE浏览器下还可能会致使内存泄漏的问题,以下:
function bindEvent(){
    var target = document.getElementById("elem");
    target.onclick = function(){
        console.log(target.name);
    }
 }

上面例子中匿名函数对外部对象target产生一个引用,只要是匿名函数存在,这个引用就不会消失,外部函数的target对象也不会被销毁,这就产生了一个循环引用。解决方案是经过建立target.name副本减小对外部变量的循环引用以及手动重置对象:

function bindEvent(){
    var target = document.getElementById("elem");
    var name = target.name;
    target.onclick = function(){
        console.log(name);
    }
    target = null;
 }

闭包中若是存在对外部变量的访问,无疑增长了标识符的查找路径,在必定的状况下,这也会形成性能方面的损失。解决此类问题的办法咱们前面也曾提到过:尽可能将外部变量存入到局部变量中,减小做用域链的查找长度。

总结:闭包不是javascript独有的特性,可是在javascript中有其独特的表现形式,使用闭包咱们能够在javascript中定义一些私有变量,甚至模仿出块级做用域,但闭包在使用过程当中,存在的问题咱们也须要了解,这样才能避免没必要要问题的出现。