闭包是Javascript中最常见的语法和形式之一,闭包可以避免全局污染,模块化编程。
要理解闭包,先要熟悉scope, Javascript的基本概念
0 准备
Scope - 作用域
Javacript中的作用域有两种
- Global scope
- Local scope
定义在函数之内的变量处于local scope, 定义在函数之外的变量处于global scope, 函数每调用一次,就新生成一个scope
- Global scope的生存期为整个应用application
- Local scope的生存期为所在函数被调用和执行中
Context - 上下文
context和scope不同,scope决定变量的可见性,context则决定了this的值,在同样的scope里
context是可以通过function methods修改的, .apply(), .bind(), .call()
Execution Context - 执行上下文
execution context就是execution scope,里面的context和上面讲到context不一样?
因为Javascript是一个单线程语言,所以同一时间只能执行一个任务,其余的任务会在execution context排队等候。
当Javascript interperter开始执行代码,scope首先会被设为global, global context会被添加到execution context,在那之后,每一次函数调用都会生成scope, 被添加到execution context中,需要注意的是,内部函数调用后,同样会将新的scope添加到外部函数的execution context中,也就是说,每个函数会生成它自己的execution context。
一旦当前context里面的代码执行完毕(浏览器执行),这个context会从execution context中popped off(出栈),当前context的状态就会转换成parent context
浏览器总是执行栈顶部的execution context,其实就是最内部的scope, 代码的执行是从内而外
function parent () { child()}function child () { console.log('child')}parent()
图1 execution context
Phase - 执行阶段
execution context执行分为两个阶段:creation phase, code execution phase
Creation Phase: 函数被调用但是还没有执行,这个阶段主要做三件事
- Creation of Variable (Activation) Object 变量对象生成
- Creation of Scope Chain 作用域链生成
- Setting of the value of context(this) 设置context(this)的值
'variableObject': { // contains function arguments, inner variable and function declarations}
Scope Chain: 在variable object生成之后就会生成scope chain, scope chain 包含variable object,scope chain是用来解析变量的,当浏览器开始解析变量时,Javascript会从最里层的代码开始向外找,其实scope chain就是包含自己的execution context和父的execution context
'scopeChain': { // contains its own variable object and other variable objects of the parent execution contexts}
executionContextObject = { 'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts 'variableObject': {}, // contains function arguments, inner variable and function declarations 'this': valueOfThis}
Code Execution Phase: 函数执行阶段
1 什么是闭包?
闭包就是内部函数访问外部函数的变量
A closure is an inner function that has access to the outer (enclosing) function’s variables—scope chain.
当一个函数被创建后,它就可以访问创建它的scope,如果函数innerFunc是在另一个函数outerFunc内部创建的,那么innerFunc就可以访问创建它的outerFunc的scope, 即使outerFunc 执行结束了returns
示例:
function fnGenerator(str) { var stringToLog = 'The string I was given is "' + str + '"'; return function() { console.log(stringToLog); }}var fnReturned = fnGenerator('Bat-Man');fnReturned(); // -> The string I was given is "Bat-Man"
即使上面的fnGenerator执行完了,它的scope仍然在内存中,它返回的函数依旧可以访问fnGenerator的scope
闭包有三种scope chains:
- own scope (variables defined between its curly brackets)
- outer function's variables (cannot call outer function's arguments object)
- global variables
例子:
function showName (firstName, lastName) { var nameIntro = "Your name is "; // this inner function has access to the outer function's variables, including the parameter function makeFullName () { return nameIntro + firstName + " " + lastName; } return makeFullName (); } showName ("Michael", "Jackson"); // Your name is Michael Jackson
jquery的例子:
$(function() { var selections = []; $(".niners").click(function() { // this closure has access to the selections variable selections.push (this.prop("name")); // update the selections variable in the outer function's scope });});
2 闭包多用在哪些场景?
2.1 减少重复代码
这样一个需求,给传入的参数加10, 或者20, 30...
function add10(num) { return num + 10;}function add20() { return num + 20;}function add30() { return num + 30;}...
代码看来有重复,怎么解决呢?看看下面使用闭包来减少重复代码
function addFactory(storedNum) { return function(num2) { return storedNum + num2; }}var add10 = addFactory(10);var add20 = addFactory(20);var add30 = addFactory(30);console.log(add10(5)); // -> 15console.log(add20(6)); // -> 26console.log(add30(7)); // -> 37
addFactory 接收一个参数storedNum, 返回了一个函数,这个内部函数永久地保留了访问storedNum的权限,而且内部函数接收一个参数,加在storedNum上
每一次调用addFactory,会生成一个scope, 里面包含对传入的参数storedNum的访问权限,返回的函数可以访问这个scope,并且保留了对这个scope的访问权限,即使addFactory执行完毕
小结:如果我们需要的函数绝大部分都相同,闭包常常是一个技巧
2.2 隐藏数据(封装)
将内部的实现细节封装起来,只暴露接口给外部调用,更新代码,接口并不变化
示例:一个计数函数,每次调用都会+1
function counterGenerator() { var counter = 1; return function() { return counter++; }}var incrementCounter = counterGenerator();console.log(incrementCounter()); // -> 1console.log(incrementCounter()); // -> 2counter = 100; // <- sets a new global variable 'counter'; // the one inside counterGenerator is unchangedconsole.log(incrementCounter()); // -> 3
上面的代码给调用者incrementCounter函数,隐藏了counterGenerator函数,incrementCounter是唯一操作counter变量的方法
3 闭包的特点
3.1 side effects - 边界效应
闭包可以访问外部函数的变量,即使外部函数已经return
这是因为函数的执行使用的是同一个scope chain, 闭包内访问了外部函数的变量,当函数返回时,闭包的context并没有出栈,从而该函数的context也无法出栈,这个scope chain一直存在
function celebrityName (firstName) { var nameIntro = "This celebrity is "; // this inner function has access to the outer function's variables, including the parameter function lastName (theLastName) { return nameIntro + firstName + " " + theLastName; } return lastName;}var mjName = celebrityName ("Michael"); // At this juncture, the celebrityName outer function has returned.// The closure (lastName) is called here after the outer function has returned above// Yet, the closure still has access to the outer function's variables and parametermjName ("Jackson"); // This celebrity is Michael Jackson
3.2 闭包存储的是外部函数变量的引用
function celebrityID () { var celebrityID = 999; // We are returning an object with some inner functions // All the inner functions have access to the outer function's variables return { getID: function () { // This inner function will return the UPDATED celebrityID variable // It will return the current value of celebrityID, even after the changeTheID function changes it return celebrityID; }, setID: function (theNewID) { // This inner function will change the outer function's variable anytime celebrityID = theNewID; } }}var mjID = celebrityID (); // At this juncture, the celebrityID outer function has returned.mjID.getID(); // 999mjID.setID(567); // Changes the outer function's variablemjID.getID(); // 567: It returns the updated celebrityId variable
2.3 循环更新外部函数的变量易出错
// This example is explained in detail below (just after this code box).function celebrityIDCreator (theCelebrities) { var i; var uniqueID = 100; for (i = 0; i < theCelebrities.length; i++) { theCelebrities[i]["id"] = function () { return uniqueID + i; } } return theCelebrities;}var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}];var createIdForActionCelebs = celebrityIDCreator (actionCelebs);var stalloneID = createIdForActionCelebs [0]; console.log(stalloneID.id()); // 103
在上面函数的循环体中,闭包访问了外部函数循环更新后的变量i,在stalloneID.id()执行前,i = 3,所以,结果为103,要解决这个问题,可以使用 Immediately Invoked Function Expression (IIFE)
function celebrityIDCreator (theCelebrities) { var i; var uniqueID = 100; for (i = 0; i < theCelebrities.length; i++) { theCelebrities[i]["id"] = function (j) { // the j parametric variable is the i passed in on invocation of this IIFE return uniqueID + j; // each iteration of the for loop passes the current value of i into this IIFE and it saves the correct value to the array // returning just the value of uniqueID + j, instead of returning a function. } (i); // immediately invoke the function passing the i variable as a parameter } return theCelebrities;}var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}];var createIdForActionCelebs = celebrityIDCreator (actionCelebs);var stalloneID = createIdForActionCelebs [0]; console.log(stalloneID.id); // 100var cruiseID = createIdForActionCelebs [1]; console.log(cruiseID.id); // 101
一般情况下,如果闭包访问了外部循环变量,会和立即执行函数(immediately invoked function expression)结合使用,再看一个例子
for (var i = 0; i < 5; i++) { setTimeout( function() {console.log(i);}, i * 1000 );}
结果是0,1,2,3,4秒后都log的是5,因为当i log的时候,循环已经执行完了,全局变量i变成了5
那么怎么让每秒log的是0,1,2,3,4呢?可以在IIFE里使用闭包,将变量循环的值传给立即执行函数
for (var i = 0; i < 5; i++) { setTimeout( (function(num) { return function() { console.log(num); } })(i), i * 1000 );}// -> 0// -> 1// -> 2// -> 3// -> 4
我们在setTimeout里立即执行了匿名函数,传递了i给num, 闭包返回的函数将log num, 返回的函数将在setTimeout 0,1,2,3,4秒 后执行
参考资料:https://scotch.io/tutorials/understanding-scope-in-javascript#toc-scope-in-javascript
http://javascriptissexy.com/understand-javascript-closures-with-ease/