JS闭包学习笔记

什么是闭包

我们先来看看闭包的定义:

所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

上面这句话对于初学者来说毫无意义,要弄懂闭包,首先我们来看看js变量的作用域:

  • 同大多数语言一样,JS的变量分为全局变量和局部变量。
  • 函数内部可以直接调用全局变量
  • 外部无法调用函数内的局部变量
ps:如果在函数内部使用未被 var 定义的变量,相当于实际声明了一个全局变量!!!

下面是代码实例:

//函数内部调用全局变量
var value = 'abc';
function show() {
    alert(value);
}

//外部直接调用局部变量
function define() {
    var innerValue = 'hello';
}
alert(innerValue);//报错 Uncaught ReferenceError: innerValue is not defined

上面关于js的变量作用域的内容应该很容易理解。但似乎对于理解闭包人没有太大的帮助。
看完上边的内容,我仍然有以下的问题

  • 什么是闭包(毫无帮助)
  • 闭包有什么用?
  • 闭包的写法

不要放弃,我们继续往下看 》》》

我们先来看看闭包的写法:

//type 1:最基本的形式,方便理解
function a() {
    var value= 'local';
    function b() {
        alert(value);
    }
    return b;
}
var c = a();
c();

//type 2:构造方法赋值,
function a(r) {
   //do something;
}
a.value='value';
a.prototype.show= function() {
    return a.value ;
}
var c = new a(1.0);
alert(c.show());

//type 3:与type 1类似,不过是将被引用的变量和方法都放入了同一个对象obj中
var a= function() {
    var obj = new Object();
    obj.value = 'value';
    obj.show = function() {
        return obj.value;
    }
    return obj;
}
var c = new a();
alert(c.show());

//type 4:声明一个对象,定义对象的属性和方法
var a = new Object();
a.value = 'value';
a.show = function() {
    return this.value;
}
alert(a.show());

//type 5:对象的另一种形式
var a = {
    value:'value',
    show: function() {
        return this.value;
    }
}
alert(a.show());

//type 6:Function方式
var a= new Function("this.value = 'value';this.show= function() {return this.value;}");  
alert((new a()).show()); 

深入理解闭包

接下来,我们基于type 1的方式,深入的解析一下闭包。首先我们必须掌握以下几个概念:

  • 函数的执行环境(excution context)
  • 活动对象(call object)
  • 作用域(scope)
  • 作用域链(scope chain)

概念解释:

执行环境:每调用一个函数(执行函数)时,系统会为该函数创建一个封闭的局部的运行环境,即该函数的执行环境。函数总是在自己的执行环境中执行,如读取局部变量、函数参数、运行内部逻辑。创建执行环境的过程包含了创建函数的 作用域,函数也是在自己的作用域下执行的。从另一个角度说,每个函数执行环境都有一个 作用域链,子函数的 作用域链 包括它的父函数的 作用域链

函数作用域 分为 词法作用域(lexical scope)动态作用域
词法作用域:函数定义时的 作用域,即静态作用域。当一个函数定义时,它的 词法作用域 就确定了,词法作用域 说明的是在函数结构的嵌套关系下,函数作用的范围。这个时候也就形成了该函数的 作用域链作用域链 就是把那些具有嵌套层级关系的 作用域 串联起来。函数的内部[[scope]]属性指向了该 作用域链
动态作用域:函数调用时的 作用域。当一个函数被调用时,首先将函数内部[[scope]]属性指向了函数的 作用域链,然后会创建一个 调用对象,并用 该调用对象 记录 函数参数 和函数的 局部变量,将其至于 作用域链 的顶部。动态作用域 就是通过把该调用对象加到 作用域链 的顶部来创建的,此时的[[scope]]除了具有定义时的 作用域链,还具有了调用时创建的 调用对象。换句话说,执行环境下的 作用域 等于该函数定义时就确定的 作用域链 加上该函数刚刚创建的 调用对象,从而也形成了新的 作用域链。所以说是 动态的作用域,并且 作用域链 也随之发生了变化。再看这里的 作用域,其实是一个对象链,这些对象就是函数调用时创建的 调用对象,以及它上面一层层的 调用对象 直到最上层的全局对象。

a函数从定义到被调用的过程:

1.当定义函数a的时候,javascript解释器会将函数a的作用域链(scope chain)设置为 定义a时a所在的环境,如果a是一个全局函数,则a的作用域链中只有window对象;
2.当执行函数a的时候,a会进入相应的 执行环境(excution context)
3.在创建 执行环境 的过程中,首先会为a添加一个scope属性,即a的 作用域,这个值为第1步中的 作用域链
4.接下来 执行环境 会创建一个 活动对象(call object)活动对象 也是一个拥有属性的对象,但它不具有原型而且 不能通过JavaScript代码直接访问 。创建完 活动对象 后,把 活动对象 添加到a的 作用域链 的最顶端。此时a的 作用域链 包含了两个对象:a的 活动对象 和window对象;
5.接着在 活动对象 上添加一个arguments属性,用来保存调用函数a时所传递的参数;
6.最后,把所有函数a的形参和内部函数b的引用也添加到a的 活动对象 上。这一步中,完成了函数b的定义。
ps:当函数b执行的时候过程也如上边一样,因此,执行时b的 作用域链 包含了3个对象:b的 活动对象 、a的 活动对象 和window对象。

ok,虽然这么两大段东西没有直接讲闭包,不过是不是大家的头脑都清晰了许多?现在再回头看看js里面闭包的定义,是不是也不在一头雾水了?清楚的掌握函数从定义到调用的整个细节,是掌握闭包的基础。所以小伙伴要是还不理解,请认真仔细的将上面的内容多读几遍。

为什么要使用闭包

最后我们来看看,为什么要使用闭包?什么情况需要用到闭包呢?
书面的解释是这样的:在动态执行环境中,数据实时地发生变化,为了保持这些非持久型变量的值,我们用闭包这种载体来存储这些动态数据。(哦,好像很有道理,不过我还是不懂)

闭包的应用举例,包括但不限于以下场景

  • setTimeout/setInterval
  • 数据缓存(这个有待商榷,看后面的实例,似乎并不需要用到闭包)
  • 对象封装(模拟面向对象的代码风格)
  • 回调函数(callback) //无实例
  • 事件句柄(event handle) //无实例
    ps:无实例表示技师还没理解透彻,,以后再补上。

代码示例:
1.setTimeout

var arr = [4, 5, 6, 8, 7, 9, 3, 2, 1, 0];
var $ = function(id) {
    return document.getElementById(id);
}
var Sort = {
    Insert: function() {
        for (var i = 1; i < arr.length; i++) {
            for (var j = 0; j < i; j++) {
                if (arr[i] < arr[j]) {
                    arr[i] = [arr[j], arr[j] = arr[i]][0];
                }
            }
            //非闭包,无法保存排序过程,每次输出都是最终结果
            setTimeout(function() {
                $("proc").innerHTML += arr + "<br/>";
            }, i * 500);
            //闭包写法
            /* //临时变量方式
                setTimeout((function() {
                var m = [];
                for (var j = 0; j < arr.length; j++) {
                    m[j] = arr[j];
                }
                return function() {
                    $("proc").innerHTML += m + "<br>";
                }
            })(), i * 500);*/
            //or参数方式
            /*
            setTimeout((function(m) {
                return function() {
                    $("proc").innerHTML += m + "<br>";
                }
            })(arr.join(",")), i * 500);
			*/
        }
        return arr;
    }
}
<!DOCTYPE html> 
<html> 
    <head> 
        <title></title> 
    </head> 
    <body> 
<div> 
v    ar a = [4, 5, 6, 8, 7, 9, 3, 2, 1, 0];</div> 
<div> 
<input type="button" value="插入排序" onclick="Sort.Insert();" /> 
</div> 
Proc:
<div id="proc"> 
</div> 
    </body> 
</html> 

2.数据缓存

var CachedBox = (function() {
    var cache = {}
      , catchNameArr = []
      , catchMax = 10000;
    return {
        getCatch: function(name) {
            if (name in cache) {
                return cache[name];
            }
            var value = name;
            cache[name] = value;
            catchNameArr.push(name);
            this.clearOldCatch();
            return value;
        },
        clearOldCatch: function() {
            if (catchNameArr.length > catchMax) {
                delete cache[catchNameArr.shift()];
            }
        }
    };
});
var box = new CachedBox();
//字段不在缓存中,需要重新创建value变量
var name = box.getCatch('cache');
alert(name);
//直接读取缓存数据
var nameAgain = box.getCatch('cache');
alert(nameAgain);

3.封装与面向对象模拟

//封装
var person = function(){    
    //变量作用域为函数内部,外部无法访问    
    var name = "default"; 
    return {    
       getName : function(){    
           return name;    
       },    
       setName : function(newName){    
           name = newName;    
       }    
    }    
}();    
     
console.log(person.name);    //直接访问,结果为undefined    
console.log(person.getName());    
console.log(person.setName("abruzzi"));    
console.log(person.getName());   
//模拟面向对象
function Person() {
    var name = "default";
    return {
       getName : function(){
           return name;
       },
       setName : function(newName){
           name = newName;
       }
    }
}
var jack = new Person();
//do something;

另外,从阮一峰大神博客看到的一个思考题:

var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      return function(){
        return this.name;
      };
    }
  };
  alert(object.getNameFunc()());

大家可以自己思考一下,预测一下结果,自己放到浏览器里运行一下,看是否预测正确,是否真的清楚结果是怎么得来的(没错,这道题集合了this和闭包)。我看阮大神博客下面的评论,貌似没有一个和我预期的一致,告诉我你的答案,欢迎一起交流学习。

参考:

posted @ 2016-04-09 14:15  铁手技师  阅读(420)  评论(0编辑  收藏  举报