只能是粗浅的,毕竟js用法太灵活。编程
首先抛概念:闭包(closure)是函数对象与变量做用域链在某种形式上的关联,是一种对变量的获取机制。这样写鬼能看懂。数组
因此要大体搞清三个东西:函数对象(function object)、做用域链(scope chain)以及它们如何关联(combination)浏览器
首先要创建一个印象,在js中,几乎全部的东西能够看做对象,除了null和undefined。好比经常使用的数组对象、日期对象、正则对象等。闭包
var num = 123; // Number var arr = [1,2,3]; // Array var str = "hello"; // String
数字原始值能够看做Number对象,字符串原始值可看作String对象,数组原始值可看做Array对象。有的原始值还可直接调方法,如数组、正则,有的不行编程语言
[1,2,3].toString(); // 能够 /\w+/.toString(); // 能够 123.toString(); // 报错
好比数字,当用一个变量名呈接它时,或者加上括号,可以这样用函数
var num = 123; num.toString(); 123.toString(); // 报错,因解释器当成浮点数来解读 (123).toString(); // 能够
因此,函数也能够是一个对象,函数细提及来又要扯一大堆,毕竟是js精华,简单说这里有用的。函数的定义常见的是this
function fun1(val){ } fun1(5); var f1 = function(val){ }; // 定义一个函数赋给变量 f1(6); var f2 = function fun2(){ }; // 或者取一个函数名 f2();
由于函数也是对象、变量,因此能够赋给一个变量(更准确说是赋给左值),在这个变量后面使用()调用运算符就能够调用这个函数了,如f1()。还有一种方式:定义即调用google
var num = (function(val){ return val * val; }(5)); // num为25
在定义一个函数体后面加上()和参数(或者没有参数),就是对这个定义的函数进行了调用,直接传入参数5计算并赋值给num,所以num是一个数值变量而不是函数变量。spa
既然函数是对象,固然也有new关键字的表达式code
var a = new Array(1,2,3); // 数组的对象建立表达式 var f = new Function("x", "y", "x = 2 *x; return x + y;"); // 函数对象建立表达式 var f1 = function(x, y){ // 函数f的函数体相似f1的定义 x = 2 * x; return x + y; };
函数对象的建立使用了Function关键字,前面的参数均被当作对象建立函数的形参,如这里的x、y,最后一个字符串是函数的函数体,多行函数体仍以;相隔。
不少时候特别是使用jQuery时常常看到函数调用时传递函数,大多数时候直接写匿名函数,也可可传递一个函数变量
func(index, function(val){ /* 匿名函数 */ }); var f = function(val){ /* 函数变量 */ }; func1(index, f);
既然函数函数是对象,能够赋给一个变量,天然也能够做为返回值了,并且在js中,函数能够嵌套定义。
function func(){ return function(x){ // 返回一个函数变量 return x * x; } } var f = func(); f(5); // 对这个函数进行调用
function func1(){ function nested1(){ } // 嵌套定义函数 function nested2(){ } }
对函数有个大体了解,说说变量做用域问题,有几个原则:
1. 全局变量拥有全局做用域.
2. 局部变量(通常指定义在函数内部)拥有局部做用域(包括其嵌套的函数).
3. 在局部变量若跟全局变量重名,优先使用局部变量.
val = 'value'; // 变量定义能够不用var关键字 document.write("val=>" + val + "<br/>"); // g是全局变量,在全局做用域中有效,因此在给g初始化以前就能够访问,只是值是undefined document.write("g=>" + g ); var g = 'google';
全局变量的定义,至关因而全局对象的属性,通常咱们用this指代这个全局对象,好比在浏览器中运行的时候,它指的是window对象,即当前窗体,在全局做用域中,如下三种访问形式等效
var g = 'google'; document.write("g=>" + g + "<br/>"); document.write("g=>" + this.g + "<br/>"); document.write("g=>" + window.g + "<br/>");
而在函数定义内部访问变量时,遵循同名优先,函数做用域内部老是优先访问,例如
var scope = "global"; function func1(){ var scope = "local"; console.log(scope); // local }; func1(); function func2(){ scope = 'changed global'; // 如不加var,改变的是全局变量的值 }; func2(); console.log(scope); // changed global
比较有意思的地方是,若是在一个函数内部给一个全局变量赋值时没有加var关键字,如func2,它改变的是全局做用域变量的值!而前面说的this,也有个有意思的地方
var scope = "global"; function func(){ var scope = "local" console.log(scope); // local console.log(this.scope); // global } func();
在局部做用域(这里均指函数内部),若是有同名变量,以this引用的话,结果是全局变量,即,在函数内部(注意不是方法,方法通常指对象的属性方法),this指代的是全局的对象。再看一个
var scope = "global"; function func(){ "use strict"; // 开启ECMAScript5严格模式 console.log(this); // undefined console.log(this.scope); // 报错:TypeError: scope undefined } func();
在严格模式中,对语法检查更加严格。在一个全局做用域定义的普通函数中,log打印this是undefined,因此this引用scope固然也不存在。
在ECMAScript(为js脚本制定的一个标准)中,强制规定全局做用域定义的变量,是全局对象(好比在浏览器客户端运行时为window,默认的全局中的this关键字也是指它,一般咱们说的就是这个)的属性。通常咱们把跟某个变量做用域相关的对象为上下文环境(context),好比全局对象this只要涉及环境这类偏底层的东西确定就是编程语言层面本身规定的。
但这个全局对象this限于非严格模式的状况。js容许咱们在局部变量的环境中(函数)以this引用全局对象,在严格模式下却无法这样干,也许是它把这个对象给隐藏了,至少目前这样写会报错。
正是js的函数有局部做用域的特殊功能---全局做用域没法访问函数中定义的变量,因此在js中,也用函数规定命名空间,相比其余有的语言使用的是namespace关键字,js目前好像是把namespace做为保留字,你的变量的命令不能跟它重名,但没投入使用。
(function(){ var name = "Jeff"; var age = 28; var pos = "development"; }()); // 在函数中定义一堆变量,外边没法访问
在一个函数中定义一堆变量,固然得调用它才能生效,这堆变量就限于在这个空间内使用了,好像用得也很少。
一个重要的点,一个函数中规定的变量,在这个函数内部全部地方均可访问,包括嵌套函数。因此能够出现下面这个现象
function func(){ console.log(num); // undefined,先于定义访问 var num = 123; console.log(num); // 123 } func();
这种特性有时被称为声明提早(hoisting),至关于这样
function func(){ var num; console.log(num); // undefined,先于定义访问 var num = 123; console.log(num); // 123 }
而后是嵌套函数,只要在函数内定义的,该函数内均能访问,看例子
function func(){ var str = "hello"; function nested1(){ // 第一次嵌套 str = "nested1"; function nested2(){ str = "nested2"; } // 第二次嵌套 nested2(); } nested1(); console.log(str); } func(); // 打印nested2
除了明确在函数内定义的变量,还有定义函数时的形参,它们在整个函数内也是可访问的。
如今咱们大概了解函数也是对象,以及全局、局部做用域,在全局做用域定义的变量,是对应的全局对象的属性,也就是说有个全局对象关联它,就从这点进入做用域链(scope chain)吧,这是理解闭包的基础。
在一个局部做用域内,或者说定义的函数,想象它们关联着某个对象,这个对象是随着咱们定义这个函数而自动生成的,函数内定义的变量以及函数的形参均是这个对象的属性,因此在这个对象内部老是能够顺利访问到它们,相似于全局变量是全局对象的属性。这种对象关联在定义时就已经决定了,而不是在调用时才造成(这很重要)。
可是咱们定义的不少函数都是嵌套的,由外到内每一个函数都会有一个对应的自定义对象跟它关联
var a = "a"; var name = "Michel"; function fun(b){ var c = "c"; var name = "Clark"; function nested(d){ var name = "Bruce"; var e = "e"; /* TODO */ } /* TODO */ }
在上面的嵌套函数nested生成一个自定义对象时,fun函数、全局做用域也会生成对象,所以它们能够造成一个对象的列表或者链表,简单的将函数名做为对象名,全局对象用global表示,而且从内嵌函数nested函数出发的话,大概是这样:
列表上的一组对象定义了这段代码做用域中定义过的变量:即它们的属性。第一个对象的属性是当前函数的形参与内定义变量,第二个对象是它的外部函数的形参与内定义变量,一直到最后是全局对象和它的属性---全局定义的变量,也就是说,当前函数永远在这个列表的最前面,这样才能够保证该函数范围内的变量老是具备最访问高优先级。每次访问变量时便会顺着这个列表查找,这被称为变量解析(variable resolution),若是一直找到列表末尾都找不到对象中的这个属性,会抛一个ReferenceError错误。
每一个定义的函数对象都会有相似这样一个列表与之关联,它们之间经过这个做用域链相关联,而函数体内定义的变量都可保存在函数做用域内,这是在函数定义时及肯定的,这种特性称之为闭包。 一般直接把函数称做闭包,并且理论上来讲全部的js函数都是闭包,由于它们都是对象,闭包这种机制让js有能力来“捕捉”变量。第一个例子:
var scope = "global"; function func(){ var scope = "local"; function nested(){ return scope; } return nested(); // 调用并返回scope } console.log(func());
常规的定义和调用,嵌套函数的定义并调用在局部做用域中完成。它打印的是local。再看这个
var scope = "global"; function func(){ var scope = "local"; // 局部变量 function nested(){ return scope; } return nested; // 返回这个嵌套函数变量 } console.log(func()()); // 在全局做用域中调用局部的嵌套函数
前面说过,函数也是变量、对象,也能够做为函数返回值,func不直接返回变量,而返回一个内嵌函数,而后在全局做用域调用这个局部内嵌函数,它会返回什么呢?结果仍为local。因为闭包机制,nested函数在定义时就已经决定了,函数体内的scope变量值是local,这是一种绑定关系,不会随着调用环境的改变而改变,它去对象关联列表中查找按优先级分的话,老是func函数对应的scope值。
闭包的功能很强大,看这个例子(例A)
var integer = (function(){ var i = 0; return function(){ return i++; }; }()); console.log(integer()); // 0 console.log(integer()); // 1 console.log(integer()); // 2
若是有点C/C++基础的人,初看这个调用结果,极可能会说这个打印的是0,0,0,反正我是很小白的这样想,每次调用完,临时变量i就会被销毁。可是确实有打印0的状况,看下这个(例B)
function func(){ var i = 0; function nested(){ return i++; } return nested(); } console.log(func()); // 0 console.log(func()); // 0 console.log(func()); // 0
也就是说,这两个看起来差很少的函数仍是有点差异的。
在C/C++中,若是不是全局变量或局部静态变量,只要在局部函数中定义的变量在调用一次完成后就立刻被销毁,固然除使用malloc、realloc、new等函数开辟的动态空间除外,这种必须得手动释放,不然容易形成内存泄露。js中是垃圾自动回收机制,某些无用的东西会被自动销毁,在前两个例子中,例A显然没有被销毁,而例B中的变量被销毁了,由于每次调用都是新声明一个i变量。so why?
在C中,局部变量被临时保存在一个栈中,先调用的先入栈,后调用的后入栈,调用完从栈订弹出,变量内存被销毁,利用的是栈的后进先出特色。而js依靠的是做用域链,这是一个列表或者链表,并非栈,没有所谓的压入(push)、弹出(pop)操做,若是说定义时就有一个列表的话,每次调用一个函数时,都会建立一个新的、跟它关联的对象,保存着局部变量,而后把这些对象添加至一个列表中造成做用域链,即使调用同一个函数两次,生成也是两个列表。
当一个函数执行完要返回的时候,便把对应对象从列表中删除,对象中的属性也会被销毁,意味着局部函数中的变量将不复存在,在例B中,return nested(),执行完返回一个值,nested函数再无任何做用,被从列表中删掉了。
若是一个局部函数定义了嵌套函数,而且有一个外部引用指向这个嵌套函数,就不会被当作垃圾回收。何时会有一个外部引用指向它(内嵌函数)?当它做为返回值(即返回一个函数变量),或者它做为某个对象属性的值存储起来时。不会被当成垃圾回收,它绑定的对象也不会从对象列表中删掉,这个绑定对象的属性和值天然也不会被销毁,天然能够进行复用了。因此再次调用其建立一个新的对象列表时,变量的值是在上一次调用的基础上改变的。
例A返回的是一个函数变量,意味着有一个外部引用指向着它:Hey!你可能会被调用哦,我不会删除你的。这样每次i是累加的。相似例A,若是将函数保存为一个对象的属性也不会被删除,例C
function counter(){ var n = 5; return { count: function(){ return n++; }, reset: function(){ n = 0; } }; } var countA = counter(); console.log(countA.count()); // 5 console.log(countA.count()); // 6 console.log(countA.count()); // 7
例C返回一个对象,对象的属性均是函数,也符合上边的情形,因此n值是累加的。这些状况下,一般咱们会把相似定义的n称为这个外部函数的私有属性(成员),由于它们运行起来就像是函数的内嵌函数(闭包)共享的东西同样。前面说过,咱们有时直接将函数称做闭包,尤为是同一个函数内部定义的函数
function func(){ var funArr = []; for(var i = 0; i <= 2; ++i) funArr[i] = function(){ return i; }; return funArr; } var farr = func(); console.log(farr[0]()); console.log(farr[1]()); console.log(farr[2]());
能够说:Here we create three closures, they are all defined in one function, so they share access to the variable i。那么它们输出神马?事实是,它们都打印3。它们共享变量,当func执行完时,i的值为3,全部的闭包均共享这个值。这个例子说明闭包的一个重要特色:全部闭包在与做用域链关联时是“活动的”,虽然函数必定义完成,做用域链就随着生成,可是全部闭包均不会单独对做用域内的私有成员(如上例中的i、例C中的n)进行复制一份,不会生成一个静态快照,而是共享,当这个成员的值改变时,它们返回的值也跟着变化。
战战兢兢写完,感受仍是要加深理解-_-