对JavaScript中闭包的理解
在前端开发中闭包是一个很重要的知识点,是面试中一定会被问到的内容。之前我对闭包的理解主要是"通过闭包可以在函数外部能访问到函数内部的变量",对闭包运用的也很少,甚至自己写过闭包自己都不太清楚,只知道这样写可以解决问题。最近在梳理自己的js知识点,发现自己对js闭包理解的很不透彻,于是想全面的分析一下闭包,特别是闭包的形成原因和闭包的使用场景。
闭包的定义
闭包是指有权访问另一个函数作用域中的变量的函数 --《JavaScript高级程序设计》
函数对象可以通过作用域关联起来,函数体内的变量都可以保存在函数作用域内,这在计算机科学文献中称为“闭包”,所有的javascirpt函数都是闭包 --《Javascript权威指南》
看完这些专业的定义是不是感觉一头雾水,没关系,我和一样也没明白这些定义说的是啥,咱接着往下分析。
在认识闭包原理之前我们先必须对作用域、执行上下文、执行上下文堆栈、变量对象、活动对象、作用域链有着全面的认识
作用域 Scope
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)
作用域共有两种主要的工作模型:
- 词法作用域:作用域是在编写代码的时候确定的
- 动态作用域:作用域是在代码运行的时候确定的
我们知道javascript使用的是词法作用域
执行上下文 Execution Contexts
Javascript中代码的执行上下文分为以下三种:
- 全局级别的代码 – 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
- 函数级别的代码 – 当执行一个函数时,运行函数体中的代码。
- Eval的代码 – 在Eval函数内运行的代码。
一个执行的上下文可以抽象的理解为一个对象。每一个执行的上下文都有一系列的属性(变量对象(variable object),this指针(this value),作用域链(scope chain) )
Execution Contexts = {
variable object:变量对象;
this value: this指针;
scope chain:作用域链;
}
执行上下文堆栈 Execution Contexts Stack
活动的执行上下文在逻辑上组成一个堆栈。堆栈底部永远都是全局上下文(globalContext),而顶部就是当前(活动的)执行上下文。
<script>
function add(num){
var sum = 5;
return sum + num;
}
var sum = add(4);
</script>
当add函数被调用时,add函数执行上下文被压入执行上下文堆栈的顶端,此时执行上下文堆栈可表示为:
EC Stack = [
<add> functionContext
globalContext
];
add函数执行完毕后,其执行上下文将会从执行上下文堆栈顶端弹出并被销毁。全局执行上下文只有在浏览器关闭时才会从执行上下文堆栈中销毁
变量对象 Variable Object
如果变量与执行上下文相关,那变量自己应该知道它的数据存储在哪里,并且知道如何访问。这种机制称为变量对象(variable object)。
可以说变量对象是与执行上下文相关的数据作用域(scope of data) 。它是与执行上下文关联的特殊对象,用于存储被定义在执行上下文中的变量(variables)、函数声明(function declarations) 。
当进入全局上下文时,全局上下文的变量对象可表示为:
VO = {
add: <reference to function>,
sum: undefined,
Math: <...>,
String: <...>
...
window: global //引用自身
}
活动对象 Activation Object
当函数被调用者激活时,这个特殊的活动对象(activation object) 就被创建了。它包含普通参数(formal parameters) 与特殊参数(arguments)对象(具有索引属性的参数映射表)。活动对象在函数上下文中作为变量对象使用。
当add函数被调用时,add函数执行上下文被压入执行上下文堆栈的顶端,add函数执行上下文中活动对象可表示为
AO = {
num: 4,
sum :5,
arguments:{0:4}
}
作用域链 Scope Chain
函数上下文的作用域链在函数调用时创建的,包含活动对象AO和这个函数内部的[[scope]]属性。
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
alert(x + y + z);
}
bar();
}
foo();
在这段代码中我们看到变量"y"在函数"foo"中定义(意味着它在foo上下文的AO中)"z"在函数"bar"中定义,但是变量"x"并未在"bar"上下文中定义,相应地,它也不会添加到"bar"的AO中。乍一看,变量"x"相对于函数"bar"根本就不存在;
函数"bar"如何访问到变量"x"?理论上函数应该能访问一个更高一层上下文的变量对象。实际上它正是这样,这种机制是通过函数内部的[[scope]]属性来实现的。
[[scope]]是所有父级变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。
**注意: **[[scope]]在函数创建时被存储是静态的(不变的),直至函数销毁。即:函数可以永不调用,但[[scope]]属性已经写入,并存储在函数对象中。
在这里我们逐步分析下
全局上下文的变量对象是:
globalContext.VO === Global = {
x: 10
foo: <reference to function>
};
在"foo"创建时,"foo"的[[scope]]属性是:
foo.[[Scope]] = [
globalContext.VO
];
在"foo"激活时(进入上下文),"foo"上下文的活动对象是:
fooContext.AO = {
y: 20,
bar: <reference to function>
};
"foo"上下文的作用域链为:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];
内部函数"bar"创建时,其[[scope]]为:
bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];
在"bar"激活时,"bar"上下文的活动对象为:
barContext.AO = {
z: 30
};
"bar"上下文的作用域链为:
bar.Scope= [
barContext.AO,
fooContext.AO,
globalContext.VO
];
介绍了一大堆内容,是不是有点晕忽忽的?坚持一下,下面是重点
闭包的原理
我们通过一个闭包的例子来分析一下闭包的形成原理
function add(){
var sum =5;
var func = function () {
console.log(sum);
}
return func;
}
var addFunc = add();
addFunc(); //5
js执行流进入全局执行上下文环境时,全局执行上下文可表示为:
globalContext = {
VO: {
add: <reference to function>,
addFunc: undefined
},
this: window,
scope chain: window
}
当add函数被调用时,add函数执行上下文可表示为:
addContext = {
AO: {
sum: undefined //代码进入执行阶段时此处被赋值为5
func: undefined //代码进入执行阶段时此处被赋值为function (){console.log(sum);}
},
this: window,
scope chain: addContext.AO + globalContext.VO
}
add函数执行完毕后,js执行流回到全局上下文环境中,将add函数的返回值赋值给addFunc。
由于addFunc仍保存着func函数的引用,所以add函数执行上下文从执行上下文堆栈顶端弹出后并未被销毁而是保存在内存中。
当addFunc()执行时,func函数被调用,此时func函数执行上下文可表示为:
funcContext = {
this: window,
scope chain: addContext.AO + globalContext.VO
}
当要访问变量sum时,func的活动对象中未能找到,则会沿着作用域链查找,由于js遵循词法作用域,作用域在函数创建阶段就被确定,在add函数的活动对象中找到sum = 5;
介绍到这里你明白形成闭包的原因了吗?
Javascript允许使用内部函数---即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。
闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
1. 保护变量的安全实现JS私有属性和私有方法
利用闭包可以读取函数内部的变量,变量在函数外部不能直接读取到,从而达到保护变量安全的作用。因为私有方法在函数内部都能被访问到,从而实现了私有属性和方法的共享。
常见的模块模式就是利用闭包的这种特性建立的
var Counter = (function() {
//私有属性
var privateCounter = 0;
//私有方法
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(privateCounter); //privateCounter is not defined
console.log(Counter.value()); // 0
Counter.increment();
Counter.increment();
console.log(Counter.value()); // 2
Counter.decrement();
console.log(Counter.value()); // 1
在jQuery框架的私有方法和变量也是这么设计的
var $ = jQuery = function(){
return jQuery.fn.init();
}
jQuery.fn = jQuery.prototype = {
init:function(){
return this; //this指向jQuery.prototype
},
length: 1,
size: function(){
return this.length;
}
}
console.log($().size()); // 1
2. 将处理结果缓存
var mult = (function(){
var cache = {};
var calculate = function(){
var a = 1;
for(vari=0,l=arguments.length;i<l;i++){
a = a*arguments[i];
}
return a;
};
return function(){
var args = Array.prototype.join.call(arguments,',');
if(args in cache){
return cache[args];
}
return cache[args] = calculate.apply(null,arguments);
}
})();
这样我们在第二次调用的时候,就会从缓存中读取到该对象。
理解了闭包的原理我们发现闭包的这些用途都是利用了闭包保存了当前函数的活动对象的特点,这样闭包函数在作用域之外被调用时依然能够访问其创建时的作用域
闭包的缺点
- 闭包将函数的活动对象维持在内存中,过度使用闭包会导致内存占用过多,所以在使用完后需要将保存在内存中的活动对象解除引用;
- 闭包只能取得外部函数中任何变量的最后一个值,在使用循环且返回的函数中带有循环变量时会得到错误结果;
- 当返回的函数为匿名函数时,注意匿名函数中的this指的是window对象。
这里仅仅是我对闭包的一些见解,若有错误的地方,还望大家提出,一起交流共同进步!
参考文献
- 《你不知道的JavaScript》上卷
- 深入理解JavaScript系列
对JavaScript中闭包的理解的更多相关文章
- 关于javascript中闭包的理解
闭包就是能够读取其他函数内部变量的函数. 在javascript中,只有函数内部的子函数可以读取局部变量,因此,我理解闭包就是定义在一个函数内部的函数. 例子: var f1 = function() ...
- 转:对JavaScript中闭包的理解
关于 const let var 总结: 建议使用 let ,而不使用var,如果要声明常量,则用const. ES6(ES2015)出现之前,JavaScript中声明变量只有 ...
- 第二话:javascript中闭包的理解
闭包是什么? 通过闭包,子函数得以访问父函数的上下文环境,即使父函数已经结束执行. OK,我来简单叙述下,先上图. 都知道函数是javascript整个世界,对象是函数,方法是函数,并且js中实质性的 ...
- JavaScript中闭包的理解
1.什么是闭包 我个人理解闭包就是函数中嵌套函数,但是嵌套的那个函数必须是返回值,才构成闭包: <!DOCTYPE html> <html> <head> < ...
- 在Javascript中闭包(Closure)
在Javascript中闭包(Closure) 什么是闭包 “官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. ...
- javascript中 (function(){})();如何理解?
javascript中 (function(){})();如何理解? javascript中: (function(){})()是匿名函数,主要利用函数内的变量作用域,避免产生全局变量,影响整体页面环 ...
- javascript中闭包最简单的简绍
javascript中闭包是什么 JavaScript 变量可以是局部变量或全局变量.私有变量可以用到闭包.闭包就是将函数内部和函数外部连接起来的一座桥梁. 函数的闭包使用场景:比如我们想要一个函数来 ...
- 在JavaScript中闭包的作用和简单的用法
在JavaScript中闭包的作用和简单的用法 一.闭包的简介 作用域链:在js中只有函数有作用域的概念,由于函数内能访问函数外部的数据,而函数外部不能访问函数内部的数据,由上述形成一种作用域访问的链 ...
- Javascript中闭包的个人理解
Javascript的一个特殊点就在于它的闭包和回调特性,这两个特性让初学Javascript的我是云里雾里,至今仍在苦苦摸索与理解.在一番苦思之后,整理了一下资料,将自己的理解思路记录下来,以 ...
随机推荐
- Win10手记-取色器ColorPicker的实现
最近个人项目需要用到ColorPicker,但是适用于WinRT和Win10的基本没用,所以只能自己造轮子了. 平台环境 Windows 10 Visual Studio 2015 思路 确定需求后, ...
- Python又把GUI界面攻下了,今天就告诉你怎么玩
0.引言 学Python这么久了,一直想做个界面出来,最近发现Python有个内置库tkinter,利用它可以很轻松做出一些简易的UI界面,首先来看Python官方对Tkinter的说明: The t ...
- 从零开始学 Web 之 CSS3(二)颜色模式,文字阴影,盒模型,边框圆角,边框阴影
大家好,这里是「 从零开始学 Web 系列教程 」,并在下列地址同步更新...... github:https://github.com/Daotin/Web 微信公众号:Web前端之巅 博客园:ht ...
- java学习-消息队列rabbitmq的组成
rabbitMQ组成部分 rabbitmq有以下组成部分,分别为: 1. Server(broker)接受客户端连接,实现AMQP消息队列和路由功能的进程 2.虚拟主机virtual host虚拟主机 ...
- linux下xdebug的安装和配置方法
xdebug简介 Xdebug是一个开放源代码的PHP程序调试器(即一个Debug工具),可以用来跟踪,调试和分析PHP程序的运行状况. xdebug安装 首先让php错误显示,只需要修改php.in ...
- array与xml转换实现(转)
<?php function xml_encode($data, $charset = 'utf-8', $root = 'so') { $xml = '<?xml version=&qu ...
- win7下安装mongodb
1.下载mongodb,解压2.新建路径,如D:\mongodb,将解压出来的bin目录复制到该目录下3.在D:\mongodb目录下在新建data目录,在data目录下新建两个目录:db和log.4 ...
- JS判断是否是数组的四种做法
一.前言 如何判断一个对象或一个值是否是一个数组,在面试或工作中我们常常会遇到这个问题,既然出现频率高,想着还是做个整理,那么本文主要基于几种判断方式,以及方式判断的原理,是否存在问题展开讨论. 二. ...
- kali linux安装中文输入法
1.先安装VMware虚拟机,再安装kali linux ------------------------------------------------------------------ 2.安装 ...
- netty源码解解析(4.0)-13 Channel NIO实现: 关闭和清理
Channel提供了3个方法用来实现关闭清理功能:disconnect,close,deregister.本章重点分析这个3个方法的功能的NIO实现. disconnect实现: 断开连接 disco ...