目录
1.执行环境与作用域链
2. 立即执行函数
3. 闭包知识点
3.1 什么是闭包
3.2 使用闭包的意义与注意点
3.3 闭包的具体应用
4. 小结
这是JavaScript基本语法的函数部分的第2篇文章,主要讲述了JavaScript中比较重要的知识点闭包;
在讲闭包之前,在上一篇《JavaScript函数(二)》的基础上,进一步深化执行环境和作用域链的知识点,并补充立即执行函数方面的知识;
最后重点探讨了有关闭包的相关方面;
1. 执行环境与作用域链
讲闭包之前,首先要清晰了解函数的执行环境和作用域链的原理;
1.1 执行环境
执行环境指的是变量在执行阶段所在的作用域,执行环境定义了变量或函数有权访问的其他数据,每一个执行环境都有一个与之关联的变量对象,环境中所有的变量都保存在这个对象中;
var a = 1;
function fn (args){
console.log(a+args)
}
fn(1)//2
上述代码的存在两个执行环境,每个执行环境都有与之关联的变量对象,变量a和函数fn保存在全局变量对象window当中,保存函数的参数的agruments对象保存在局部变量对象fn当中;
值得注意的是,如果这个执行环境是函数,则将其活动对象作为变量对象,即函数调用时所生成的对象,因为函数未调用定义在里面的变量是不存在的;
1.2 作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链,作用域的用途是能够保证执行环境有权、有序访问当前作用域及其外部的变量;
var a =1;
function fn(b,c){
var d = 4;
console.log(a+b+c+d)
}
fn(2,3)//10
以上述代码为例,去探讨执行环境和作用域链的相关知识点;
- js引擎在解析阶段将变量a和函数fn保存在window变量对象上,此时变量a和函数fn的执行环境是window对象;
- 在调用函数fn时,函数fn创建创建一个活动对象fn(),函数内部的变量b、c(保存在函数的arguments对象中)和变量d的执行环境是活动对象fn();
- 此时,函数内部的代码在执行过程中会创建活动对象的作用域链,它可访问到的变量处理有保存在arguments对象的b和c,直接定义在内部的d,同时可以访问到定义在外部执行环境——window对象的a;
- 因此,这就是一条简单的作用域链,同时,这条作用域链式单向(由内向外可访问)、有序的;
1.3 关于JavaScript的块级作用域
JavaScript是不存在块级作用域的,只用函数才能单独开辟一个作用域来;
上一段比较经典代码,来为块级作用域和接下来的闭包预预热;
//html
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
//js
var lists = document.querySelectorAll('li')
console.log(lists)
for(var i=0;i<lists.length;i++){
lists[i].onclick = function(){
console.log(i)
}
}
- 原本的需求是点击第几个
li
,控制台弹出第几个li
的下标,但实际弹出的都是5; - 出现这个问题的原因是,for这个流程控制语句是无法创建块级作用域的,如果能够创建块级作用域的话,那么每循环一次,function里面的
i
都是保存当时的i
; - 而实际上由于for不存在款及作用域,所以循环遍历后,
i=5
,当触发点击事件时,调用函数console.log(i)
就变成5; - 如果想用实现原本的需求,那么我们就需要在每次循环时都能够创建一个能够保存当时
i
的块级作用域来; - 具体如何实现需要用到接下来的知识点,所以答案在后头;
2. 立即执行函数
所谓立即执行函数(Immediately-Invoked Function Expression,IIFE),就是函数声明后立即调用该函数,具体的写法为:
(function(){
console.log(1)
})();
//1
可以传参
(function(a){
console.log(a)
})(5);
//5
初学者可能对立即执行函数的写法感到奇怪,其实只要了解js背后的解析原理就很容易掌握其规则;
立即执行函数由一个匿名函数和两个圆括号构成,将匿名函数放在第一个圆括号内;
1.()();
2.function(){};
3.(function(){})()
之所以出现这样的写法,是js默认出现在行首的函数解析为语句(声明式)
//声明式
function(){};
//表达式
var f = function(){}
如果语句后面直接出现圆括号会报错,通过圆括号将语句括起来从而让js识别为表达式,然后再加一个圆括号立即调用,从而实现一个立即执行函数;
function(){}();//报错
(function(){})()//不报错
立即执行函数的优点在于:
- 具有函数独立开辟一个作用域的功能,实现私有变量封装;
- 定义好后即可执行内部的代码;
- 匿名特性不必担心污染同级或上级的变量;
3. 闭包知识点
3.1什么是闭包
前面已经讲到执行环境和作用域链,我们知道执行环境定义了变量有权访问其他数据,函数能够创建一个作用域。如果我们现在有这么一个需求:想要在外部环境下也能访问内部环境的变量,那么究竟如何实现呢?
这里接需要引出闭包的概念:闭包(closure)指的是能够访问另一个函数内部变量的函数;
function outer(){
var a = 1;
function inner(){
return a
}
return inner
}
var result = outer();
result();//1
- 上述代码中,变量a是在函数outer内声明,所以只有函数outer内部才可访问,外部全局环境无法访问;
- 通过在outer内部定义一个函数inner,因为作用域链的作用,这个inner可以访问变量a;
- 最后暴露一个inner接口,使得在调用outer时,获得inner这个接口,在调用这个接口,从而达到访问函数内部变量的目的;
- 通过上述分析,我们可以知道,函数inner就是闭包,即能够访问另一个函数内部变量的函数;
3.2 使用闭包的意义和注意点
使用闭包的意义
闭包的意义在于:实现函数的封装,将变量全部封装在函数内部而不必担心污染全局环境,只暴露出接口,而不必关心内部的代码逻辑,例如将对象的私有属性和方法进行封装:
function Animal(name){
var _age;
function setAge(age){
_age = age
};
function getAge(){
return _age
}
return {
name:name,
setAge:setAge,
getAge:getAge
}
}
var cat = Animal('cat');
cat.setAge(12)
cat.getAge()//12
- 上述代码封装了对象的私有属性
_age
和私有方法setAge
、getAge
,暴露出一个对象作为接口; - 函数Animal内部定义的变量外部是直接无法访问,只能通过对象接口间接访问;
- 我们只需要调用对象相关的属性和方法,而不必关心方法具体是如何实现的,也不必担心内部定义的变量会对全局变量有污染;
使用闭包的注意点
使用闭包会产生内存泄露的问题。通常函数在调用完后,js内部的垃圾回收机制会自动销毁局部活动对象(函数调用时生成的对象),但是以上述代码为例: -
var cat = Animal('cat')
这段代码执行完后,按道理会销毁函数Animal
这个活动对象,实际上这个活动对象仍存在内存中; - 原因是
return {...}
的这个对象的私有方法的作用域链仍在引用这个Animal
活动对象; - 只有
return {...}
的这个对象被销毁后,Animal
活动对象才能被销毁,内存才能释放;
可通过添加以下代码释放内存:
var cat = Animal('cat');
cat.setAge(12)
cat.getAge()//12
cat = null
3.3 闭包的具体应用
回到前面的代码中来:
//html
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
//js
var lists = document.querySelectorAll('li')
console.log(lists)
for(var i=0;i<lists.length;i++){
lists[i].onclick = function(){
console.log(i)
}
}
- 实现点击哪个
list
,就在控制台输出i
; - 通过立即执行函数形成独立的作用域;
- 传递参数进IIFE,从而每循环一次就形成函数内部的变量;
- 最后return出一个函数,这个函数的返回值就是当时的i;
var lists = document.querySelectorAll('li')
console.log(lists)
for(var i=0;i<lists.length;i++){
lists[i].onclick = (function(num){
return function(){
return num+1
}
})(i);
}
【demo】
4.小结
通过整篇文章,我们知道了:
- 执行环境指的是变量在执行阶段所在的作用域,定义了变量有权访问的其他数据,每个执行环境都有一个与之关联的变量对象,所有的变量都保存在其中,函数在执行阶段会创建一个活动对象,这活动对象可以包含以下变量:
arguments
,函数内部使用var
定义的变量和外部变量; - 变量在执行阶段会创建变量对象的一个作用域链,作用域链的用途是保证执行环境有权有序访问当前作用域和其他外部变量;
- JavaScript不存在块级作用域;
- 闭包是能够访问函数内部变量的函数,闭包的意义在于能够封装变量在函数内部而实现间接访问函数内部变量;使用闭包要注意内存泄露问题,解决办法是在调用完接口后,可以使用赋值
null
进行销毁;
参考资料
- 《JavaScript高级程序设计(第3版)》
- 《JavaScript标准参考教程》——阮一峰