博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
正确理解Javascript Closures -- 闭包
阅读量:5997 次
发布时间:2019-06-20

本文共 8730 字,大约阅读时间需要 29 分钟。

hot3.png

闭包是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 parameter​mjName ("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(); // 999​mjID.setID(567); // Changes the outer function's variable​mjID.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); // 100​​​var 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/

转载于:https://my.oschina.net/u/2510955/blog/1551513

你可能感兴趣的文章
设计模式——2抽象工厂模式(Abstract Factory)
查看>>
为什么程序员都反感笔试?
查看>>
为什么编程这么难?!
查看>>
阿里云服务器如何购买,以及阿里云服务器的购买操作流程
查看>>
[20160302]绑定变量的分配长度2.txt
查看>>
ElasticSearch搜索实例含高亮显示及搜索的特殊字符过滤
查看>>
log4j与commons-logging,slf4j的关系(转)
查看>>
Spark..........WordCount
查看>>
看着自己有什么样的资源,利用好这些资源就好了。不要看着别人的资源流口水(转)...
查看>>
springboot + devtools(热部署)
查看>>
HDOJ 1215 七夕节
查看>>
js如何控制css伪元素内容(before,after)
查看>>
IntelliJ&#160;IDEA导出Java&#160;可执行Jar包
查看>>
奇怪的报错---待解决
查看>>
arcgis10.2.2桌面版具体的安装步骤过程
查看>>
头部——MimeHeaders
查看>>
.NET获取当前程序所在电脑的CPU和内存使用率
查看>>
dbcp连接池不合理的锁导致连接耗尽
查看>>
SQL Server 2012故障转移的looksalive check和is alive check
查看>>
已被否弃的结构,留个念想
查看>>