前言: 在学习李兵老师的线上课程《浏览器工作原理与实践》后,收获颇丰。故作此篇以记录、整理和总结所学。本篇将从闭包在JS中的使用方法开始,从上往下深挖其原理,这一过程中将对Javascript的执行机制有所了解。
一、闭包的定义与用法
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
在这一定义中,说明了闭包的内容、意义以及内存创建方式。关于内存部分,后续详谈,我们先来了解下定义中的“词法环境”为何物。
- 词法环境又可以理解为“词法作用域”。词法作用域就是指作用域是由代码中函数声明的位置来决定的,接下来我们看一张图便可以理解:
可以看出,顶部作用域便是全局作用域,而每一个函数声明创建后就会建立一个独立的作用域。因此,闭包作为携带词法环境的组合,可以通过定义方法来搭建起外部访问内部数据的桥梁。
二、闭包的应用
闭包在形式上可以简单理解为函数嵌套函数,并且返回该被嵌套函数或者被嵌套函数的对象组合。
接下来通过几个栗子理解闭包。
1、将方法与数据关联
例如,我们需要绑定onclick方法,使其点击后可以修改组件的样式,这时候可以采用闭包。
样式代码如下:
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
闭包函数定义如下:
//函数闭包
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
将其绑定在onclick方法上:
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
//不用闭包的等价定义形式:
//document.getElementById('size-12').onclick = ()=>{document.body.style.fontSize = 12 + 'px'};
//document.getElementById('size-14').onclick = ()=>{document.body.style.fontSize = 14 + 'px'};
//document.getElementById('size-16').onclick = ()=>{document.body.style.fontSize = 16 + 'px'};
通过以上比较可见,闭包函数既满足了onclick需要绑定函数方法的规定,又通过构造外层函数,传递size参数,提高核心代码的复用性。
2、模拟面向对象时访问私有数据方法
当一个普通函数被调用、执行完后,便会被回收销毁。但是闭包函数的返回值(即被返回的函数),会保留在内存中,外部可以通过其访问函数内部的数据,类似于通过公有方法访问类对象中的私有数据。
举个例子:
function count() {
//内部数据
var privateCounter = 0;
//内部方法
function changeBy(val) {
privateCounter += val;
}
//返回方法对象
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
}
//接收返回的方法对象
var Counter = count();
//调用返回的方法,操作内部属性
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
该闭包函数的定义和使用还借助匿名闭包简化:
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(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
说到匿名闭包,可以理解为匿名函数+闭包,由于是回调一般的连续调用,所以常常有两个括号。
接下来,我们比较下直接回调和保存后再调用函数的区别:
function add(){
var num= 100; // 这里改为局部变量;
return function(){
num++;
alert(num);
}
};
//使用形式一:直接回调,多次重复
add()(); // num :100
add()(); // num :100
add()(); // num :100 ,每次add()又将num重新初始化
//使用形式二:调用一次add ,再执行返回的函数
var fn=add()//只在这里初始化一次,后边调用的时候执行的是里边的匿名函数
fn(); // num :101
fn(); // num :102
fn(); // num :103 ,累加
fn=null //应及时解除引用,否则会占用更多存
三、闭包与作用域链
了解完闭包的定义,我们看看JS底层对于闭包的执行机制流程。首先要了解的是作用域链。
1、作用域链
我们先来看一段代码:
function bar() {
console.log(myName)
}
function foo() {
var myName = "极客邦"
bar()
}
var myName = "极客时间"
foo()
这段代码中,myName的打印结果是什么?
如果按照JS栈的调用原理,应该如下图所示,从上至下查找myName变量的声明,首先查到的应是foo作用域中的“极客邦”,但是其最终打印结果却是"极客时间"。
这说明,它并没有严格按照栈的顺序从上往下查找,而是本作用域查找完毕后直接去往了全局作用域中查找。这就涉及到了作用域链的概念 ,原来每个作用域中还有一个指向全局作用域的指针outer,查找会沿着outer先前往全局作用域中查找。我们把这个查找的链条就称为作用域链。如下图:
2、闭包产生的过程
基于作用域链,我们就可以理解闭包的产生过程。思考下述代码的执行结果:
function foo() {
var myName = "极客时间"
let test1 = 1
const test2 = 2
var innerBar = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())
首先我们看看当执行到foo函数内部的return innerBar
这行代码时调用栈的情况,你可以参考下图:
从上面的代码可以看出,innerBar是一个对象,包含了getName和setName的两个方法(通常我们把对象内部的函数称为方法)。你可以看到,这两个方法都是在foo函数内部定义的,并且这两个方法内部都使用了myName和test1两个变量。
根据词法作用域的规则,内部函数getName和setName总是可以访问它们的外部函数foo中的变量,所以当innerBar对象返回给全局变量bar时,虽然foo函数已经执行结束,但是getName和setName函数依然可以使用foo函数中的变量myName和test1。所以当foo函数执行完成之后,其整个调用栈的状态如下图所示:
从上图可以看出,foo函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的setName和getName方法中使用了foo函数内部的变量myName和test1,所以这两个变量依然保存在内存中。这像极了setName和getName方法背的一个专属背包,无论在哪里调用了setName和getName方法,它们都会背着这个foo函数的专属背包。
之所以是专属背包,是因为除了setName和getName函数之外,其他任何地方都是无法访问该背包的,我们就可以把这个背包称为foo函数的闭包。
bar调用foo返回的方法后,栈内容如下:
开发者工具中的闭包展示:
*对于闭包返回的方法,其查找变量的顺序沿着作用域链,便为“当前执行上下文–>foo函数闭包–>全局执行上下文”也就是“Local–>Closure(foo)–>Global”,这一顺序和开头提到的例子不同,它是先找了foo函数作用域,再去找全局作用域,可见闭包函数与周围的词法环境是一体的。这也就可以大致区分,普通嵌套函数和闭包函数在作用域关系上的区别