正确理解Javascript Closures — 闭包

Java基础

浏览数:55

2020-7-3

要理解闭包,先要熟悉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)); // -> 15
console.log(add20(6)); // -> 26
console.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()); // -> 1
console.log(incrementCounter()); // -> 2
counter = 100; // <- sets a new global variable 'counter';
               // the one inside counterGenerator is unchanged
console.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/

作者:pengqinmm