对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的我是云里雾里,至今仍在苦苦摸索与理解.在一番苦思之后,整理了一下资料,将自己的理解思路记录下来,以 ...
随机推荐
- python中使用queue实现约瑟夫环(约瑟夫问题)求解
约瑟夫问题:是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围. 从编号为k的人开始报数,数到m的那个人出列:他的下一个人又从1开始报数,数到m的那个人又出列: 依 ...
- win10 + gluon + GPU
1. 下载教程 可以用浏览器下载zip格式并解压,在解压目录文件资源管理器的地址栏输入cmd进入命令行模式. 也可以 git pull https://github.com/mli/gluon-tut ...
- [EXP]McAfee ePO 5.9.1 - Registered Executable Local Access Bypass
# Exploit Title: McAfee ePO 5.9.1 Registered Executable Local Access Bypass # Date: 2019-03-07 # Exp ...
- SQL Server性能优化(14)索引碎片
一. 外部碎片和内部碎片的概念 碎片的概念和检测,参考MSDN:https://msdn.microsoft.com/zh-cn/library/ms189858.aspx 二.对于碎片的解决办法 解 ...
- Linux 学习手记(2):Linux文件系统的基本结构
Linux 文件系统概况 Linux文件系统为一个倒置的树状结构,所有文件或文件夹均包含在一个根目录“/”中.如图所示(每个目录的作用可以参考:Linux目录结构说明): Linux系统严格区分大小写 ...
- ContentProvider使用总结
近日来学习ContentProvider相关的知识,做了一个demo,想和网友分享下. 首先说一点相关的知识: 一:作用 ContentProvider是不同应用程序共享数据的接口,跟共享数据的别的方 ...
- 精读JavaScript模式(四),数组,对象与函数的几种创建方式
一.前言 放了个元旦,休息了三天,加上春运抢票一系列事情的冲击,我感觉我的心已经飞了.确实应该收收心,之前计划的学习任务也严重脱节了:我恨不得打死我自己. 在上篇博客中,笔记记录到了关于构造函数方面的 ...
- Postman接口测试_添加断言
1.设置环境变量 postman.setEnvironmentVariable("key", "value"); 例子: postman.setEnviron ...
- 偏流角(Draft Angle)在等距螺旋中的作用
劳动改变人,思维改变世界.我们可以接着聊螺旋线了. 在飞行程序设计中,偏流角(Draft Angle简写为DA)通常指得是受侧风影响航向偏移的最大角度.用速度向量来表示时,是图1中的三角形关系: 图1 ...
- windows7安装flask-mysqldb遇到的坑
最近在windows环境上搭建flask使用环境,遇到过很多坑,这次就记录下安装flask-mysqldb所遇到的坑. 正常逻辑是使用pip install flask-mysqldb进行安装.但是会 ...