JavaScript高级程序设计——闭包
前言
有很多人搞不清匿名函数和闭包这两个概念,经常混用。闭包是指有权访问另一个函数作用域中的变量的函数。匿名函数就是没有实际名字的函数。
闭包
概念
闭包,其实是一种语言特性,它是指的是程序设计语言中,允许将函数看作对象,然后能像在对象中的操作搬在函数中定义实例(局部)变量,而这些变量能在函数中保存到函数的实例对象销毁为止,其它代码块能通过某种方式获取这些实例(局部)变量的值并进行应用扩展。
条件
闭包是允许函数访问局部作用域之外的数据。即使外部函数已经退出,外部函数的变量仍可以被内部函数访问到。
因此闭包的实现需要三个条件:
内部函数实用了外部函数的变量
外部函数已经退出
内部函数可以访问
function a() { var x = 0; return function(y) { x = x + y; // return x; console.log(x); } } var b = a(); b(1); // b(1); //
上述代码在执行的时候,b得到的是闭包对象的引用,虽然a执行完毕后,但是a的活动对象由于闭包的存在并没有被销毁,在执行b(1)的时候,仍然访问到了x变量,并将其加1,若再执行b(1),则x是2,因为闭包的引用b并没有消除。(后面会解释,闭包返回了函数,函数可以创建独立的作用域)
闭包,其实就是指程序语言中能让代码调用已运行的函数中所定义的局部变量。
但是你只需要知道应用的两种情况即可——函数作为返回值,函数作为参数传递。
function fn() { var max = 10; return function bar(x) { if (x > max) { console.log(x); } }; } var f1 = fn(); f1(15);
如上代码,bar函数作为返回值,赋值给f1变量。执行f1(15)时,用到了fn作用域下的max变量的值。至于如何跨作用域取值,可以参考上一篇文章。
var max = 10, fn = function(x) { if (x > max) { console.log(x); // } }; (function(f) { var max = 100; f(15); })(fn);
如上代码中,fn函数作为一个参数被传递进入另一个函数,赋值给f参数。执行f(15)时,max变量的取值是10,而不是100。
上一篇讲到自由变量跨作用域取值时,曾经强调过:要去创建这个函数的作用域取值,而不是“父作用域”。理解了这一点,以上两端代码中,自由变量如何取值应该比较简单.
另外,讲到闭包,除了结合着作用域之外,还需要结合着执行上下文栈来说一下。
在前面讲执行上下文栈时,我们提到当一个函数被调用完成之后,其执行上下文环境将被销毁,其中的变量也会被同时销毁。
有些情况下,函数调用完成之后,其执行上下文环境不会接着被销毁。这就是需要理解闭包的核心内容。
可以拿本文的之前代码(只做注释修改)来分析一下。
//全局作用域 function fn() { var max = 10; // fn作用域 return function bar(x) { if (x > max) { console.log(x); } }; //bar作用域 } var f1 = fn(); f1(15);
全局作用域为:代码1-12行;fn作用域为:代码2-10行;bar作用域为:代码5-9行。
举例
第一步,代码执行前生成全局上下文环境,并在执行时对其中的变量进行赋值。此时全局上下文环境是活动状态。
第二步,执行第17行代码时,调用fn(),产生fn()执行上下文环境,压栈,并设置为活动状态。
第三步,执行完第17行,fn()调用完成。按理说应该销毁掉fn()的执行上下文环境,但是这里不能这么做。注意,重点来了:
因为执行fn()时,返回的是一个函数。函数的特别之处在于可以创建一个独立的作用域。而正巧合的是,返回的这个函数体中,还有一个自由变量max要引用fn作用域下的fn()上下文环境中的max。因此,这个max不能被销毁,销毁了之后bar函数中的max就找不到值了。
因此,这里的fn()上下文环境不能被销毁,还依然存在与执行上下文栈中。
——即,执行到第18行时,全局上下文环境将变为活动状态,但是fn()上下文环境依然会在执行上下文栈中。另外,执行完第18行,全局上下文环境中的max被赋值为100。如下图:
第四步,执行到第20行,执行f1(15),即执行bar(15),创建bar(15)上下文环境,并将其设置为活动状态。
执行bar(15)时,max是自由变量,需要向创建bar函数的作用域中查找,找到了max的值为10。这个过程在作用域链一节已经讲过。
这里的重点就在于,创建bar函数是在执行fn()时创建的。fn()早就执行结束了,但是fn()执行上下文环境还存在与栈中,因此bar(15)时,max可以查找到。如果fn()上下文环境销毁了,那么max就找不到了。
总结:使用闭包会增加内容开销
第五步,执行完20行就是上下文环境的销毁过程,这里就不再赘述了。
闭包与变量
概念
闭包只能取得包含函数中任何变量的最后一个值,闭包所保存的是整个变量对象,而不是某个特殊变量。
例子
function createFunctions() { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function() { return i; }; } return result; } var funcs = createFunctions(); //每个函数都输出10 for (var i = 0; i < funcs.length; i++) { document.write(funcs[i]() + "<br />"); }
总结:每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i。当createFunctions()函数返回后,变量i的值为10。
我们可以通过创建另一个匿名函数强制让闭包的行为符合预期。
function createFunctions() { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function(x) { return function() { return x; }; }(i); } return result; } var funcs = createFunctions(); //循环输出0-10 for (var i = 0; i < funcs.length; i++) { document.write(funcs[i]() + "<br />"); }
总结:没有直接把闭包赋值给数组,而是定义了一个匿名函数,并通过立即执行该匿名函数的结果赋值给数组,并带了for循环的参数i进去,让x能找到传入的参数值为0-10,这就解释了函数参数是按值传递的,所以会将变量i的当前值复制给参数x。而这个匿名函数内部又创建并返回了一个访问x的闭包。这样以来result数组中的每个函数都有自己x变量的一个副本,所以会符合我们的预期输出不同的值。
函数按值传递
函数传参就两个类型,基本类型和引用类型,大家纠结的都是引用类型的传递。
引用类型作为参数传入函数,传的是个地址值,或者指针值,不是那个引用类型本身,它还好好的呆在堆内存呢。赋值给argument的同样是地址值或者指针。所以说是value值传递一点没错,传的是个地址值。通过两个例子看懂就行了。
例子1:
function setName(obj) { obj.name = 'aaa'; var obj = new Object(); // 如果是按引用传递的,此处传参进来obj应该被重新引用新的内存单元 obj.name = 'ccc'; return obj; } var person = new Object(); person.name = 'bbb'; var newPerson = setName(person); console.log(person.name + ' | ' + newPerson.name); // aaa | ccc
从结果看,并没有显示两个’ccc’。这里是函数内部重写了obj,重写的obj是一个局部对象。当函数执行完后,立即被销毁。
引用值:对象变量它里面的值是这个对象在堆内存中的内存地址。因此如果按引用传递,它传递的值也就是这个内存地址。那么var obj = new Object();会重新给obj分配一个地址,比如是0x321了,那么它就不在指向有name = ‘aaa’;属性的内存单元了。相当于把实参obj和形参obj的地址都改了,那么最终就是输出两个ccc了。
例子2
var a = { num:'1' }; var b = { num:'2' }; function change(obj){ obj.num = '3'; obj = b; return obj.num; } var result = change(a); console.log(result + ' | ' + a.num); // 2 | 3
首先把a的值传到change函数内,obj.num = ‘3’;后a.name被修改为3;
a的地址被换成b的地址;
返回此时的a中a.num。
闭包中使用this对象
概念
this对象是在运行时基于函数的执行环境绑定的:全局函数中,this等于window;当函数被作用某个对象的方法调用时,this等于那个对象。
但在匿名函数中,由于匿名函数的执行环境具有全局性,因此this对象通常指向window(在通过call或apply函数改变函数执行环境的情况下,会指向其他对象)。
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ return function(){ return this.name; }; } }; alert(object.getNameFunc()()); //"The Window"
通过修改把作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。如下代码:
var name = "The Window";
var object = { name : "My Object", getNameFunc : function(){ var that = this; return function(){ return that.name; }; } }; alert(object.getNameFunc()()); //"MyObject"
变量声明提前
var scope="global"; function scopeTest() { console.log(scope); var scope="local"; } scopeTest(); //undefined
此处的输出是undefined,并没有报错,这是因为在前面我们提到的函数内的声明在函数体内始终可见,上面的函数等效于:
var scope="global"; function scopeTest() { var scope; console.log(scope); scope="local"; } scopeTest(); //undefined
注意,如果忘记var,那么变量就被声明为全局变量了。结果就是global
没有块级作用域
和其他我们常用的语言不同,在Javascript中没有块级作用域:
function scopeTest() { var scope = {}; if (scope instanceof Object) { var j = 1; for (var i = 0; i < 10; i++) { console.log(i); //输出0-9 } console.log(i); //输出10 } console.log(j); //输出1 } scopeTest();
在javascript中变量的作用范围是函数级的,即在函数中所有的变量在整个函数中都有定义,这也带来了一些我们稍不注意就会碰到的“潜规则”:
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了。
原文链接:http://www.cduyzh.com/js-closure/
JavaScript高级程序设计——闭包的更多相关文章
- 《JavaScript高级程序设计(第3版)》阅读总结记录第一章之JavaScript简介
前言: 为什么会想到把<JavaScript 高级程序设计(第 3 版)>总结记录呢,之前写过一篇博客,研究的轮播效果,后来又去看了<JavaScript 高级程序设计(第3版)&g ...
- 读javascript高级程序设计00-目录
javascript高级编程读书笔记系列,也是本砖头书.感觉js是一种很好上手的语言,不过本书细细读来发现了很多之前不了解的细节,受益良多.<br/>本笔记是为了方便日后查阅,仅作学习交流 ...
- 读javascript高级程序设计-目录
javascript高级编程读书笔记系列,也是本砖头书.感觉js是一种很好上手的语言,不过本书细细读来发现了很多之前不了解的细节,受益良多.<br/>本笔记是为了方便日后查阅,仅作学习交流 ...
- 读书笔记(03) - 性能 - JavaScript高级程序设计
作用域链查找 作用域链的查找是逐层向上查找.查找的层次越多,速度越慢.随着硬件性能的提升和浏览器引擎的优化,这个慢我们基本可以忽略. 除了层级查找损耗的问题,变量的修改应只在局部环境进行,尽量避免在局 ...
- 《Javascript高级程序设计》阅读记录(七):第七章
<Javascript高级程序设计>中,2-7章中已经涵盖了大部分精华内容,所以摘录到博客中,方便随时回忆.本系列基本完成,之后的章节,可能看情况进行摘录. 这个系列以往文字地址: < ...
- JavaScript高级程序设计第三版.CHM【带实例】
从驱动全球商业.贸易及管理领域不计其数的复杂应用程序的角度来看,说 JavaScript 已经成为当今世界上最流行的编程语言一点儿都不为过. JavaScript 是一种非常松散的面向对象语言,也是 ...
- javascript高级程序设计学习笔记
javascript高级程序设计,当枕头书已经好久了~zz 现在觉得自己在js的开发上遇到了一些瓶颈,归根究底还是基础太薄弱,所以重新刷一遍js高程希望有更新的认识. 一.javascript简介 ...
- JavaScript高级程序设计(读书笔记)之函数表达式
定义函数的方式有两种:一种是函数声明,另一种就是函数表达式. 函数声明的一个重要特征就是函数声明提升(function declaration hoisting),意思是在执行代码前会先读取函数声明. ...
- 《JavaScript高级程序设计(第3版)》笔记-序
很少看书,不喜欢看书,主要是上学时总坐不住,没有多大定性,一本书可以两天看完,随便翻翻,也可以丢在角落里几个月不去动一下. 上次碰到了<JavaScript高级程序设计(第3版)>感觉真的 ...
随机推荐
- Codeforces 534B - Covered Path
534B - Covered Path 思路:贪心,每一秒取尽可能大并且可以达到的速度. 画张图吧,不解释了: 代码: #include<bits/stdc++.h> using name ...
- git/ssh备查文档
配置多个ssh key: 待更新 git速查表: git remote set-url origin(远程仓库名称) https://xxxxx/ProjectName.git 从ssh切换至htt ...
- C#一套简单的单例系统
单例基类 public class CSingletonBase<TYPE> { public static TYPE Singleton { get { return m_singlet ...
- UltraDropDown
private void FruitInit() { //Create some fruit fruits.Add(-1,"apple"); fruits.Add(-2," ...
- Prefix Product Sequence CodeForces - 487C (数论,构造)
大意: 构造一个[1,2,...n]的排列, 使得前缀积模n为[0,1,...,n-1]的排列 这种构造都好巧妙啊, 大概翻一下官方题解好了 对于所有>=6的合数$n$, 有$(n-1)! \e ...
- bzoj1833: [ZJOI2010]count 数字计数 数位dp
bzoj1833 Description 给定两个正整数a和b,求在[a,b]中的所有整数中,每个数码(digit)各出现了多少次. Input 输入文件中仅包含一行两个整数a.b,含义如上所述. O ...
- CoderForce 140C-New Year Snowmen(贪心)
题目大意:有n个已知半径的雪球.堆一个雪人需要三个尺寸不同的雪球,问用这些雪球最多能堆多少个雪人? 题目分析:先统计一下每种尺寸的球的个数,从三种最多的种类中各取出一个堆成雪人,这样贪心能保证的到的数 ...
- 使用kubernetes的deployment进行RollingUpdate
rolling update,可以使得服务近乎无缝地平滑升级,即在不停止对外服务的前提下完成应用的更新. replication controller与deployment的区别 replicatio ...
- Grep console 设置
Grep console DEBUG 9961B8 INFO 4B5E76 WARN 8A8A00 ERROR 9F6B00 8A7674
- js运动例子1
<!doctype html> <html> <head> <meta charset="utf-8"> <meta name ...