【JS执行机制】从表面到深入,详解闭包及其背后的作用域链

  前言: 在学习李兵老师的线上课程《浏览器工作原理与实践》后,收获颇丰。故作此篇以记录、整理和总结所学。本篇将从闭包在JS中的使用方法开始,从上往下深挖其原理,这一过程中将对Javascript的执行机制有所了解。

一、闭包的定义与用法

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

在这一定义中,说明了闭包的内容、意义以及内存创建方式。关于内存部分,后续详谈,我们先来了解下定义中的“词法环境”为何物。

  • 词法环境又可以理解为“词法作用域”。词法作用域就是指作用域是由代码中函数声明的位置来决定的,接下来我们看一张图便可以理解:

2f2f0446a7174dfb9f0f37249a7b608a.png

可以看出,顶部作用域便是全局作用域,而每一个函数声明创建后就会建立一个独立的作用域。因此,闭包作为携带词法环境的组合,可以通过定义方法来搭建起外部访问内部数据的桥梁。

二、闭包的应用

闭包在形式上可以简单理解为函数嵌套函数,并且返回该被嵌套函数或者被嵌套函数的对象组合。

接下来通过几个栗子理解闭包。

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参数,提高核心代码的复用性。

扫描二维码关注公众号,回复: 15256039 查看本文章

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作用域中的“极客邦”,但是其最终打印结果却是"极客时间"。

bc5e02733bd34972b9af2625a1c737ea.png

这说明,它并没有严格按照栈的顺序从上往下查找,而是本作用域查找完毕后直接去往了全局作用域中查找。这就涉及到了作用域链的概念 ,原来每个作用域中还有一个指向全局作用域的指针outer,查找会沿着outer先前往全局作用域中查找。我们把这个查找的链条就称为作用域链。如下图:

e06dfeaf8e774a45bb7419b420e10dbe.png

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这行代码时调用栈的情况,你可以参考下图:

0df3645d4e6542a09c2c1db6c66465bb.png

从上面的代码可以看出,innerBar是一个对象,包含了getName和setName的两个方法(通常我们把对象内部的函数称为方法)。你可以看到,这两个方法都是在foo函数内部定义的,并且这两个方法内部都使用了myName和test1两个变量。

根据词法作用域的规则,内部函数getName和setName总是可以访问它们的外部函数foo中的变量,所以当innerBar对象返回给全局变量bar时,虽然foo函数已经执行结束,但是getName和setName函数依然可以使用foo函数中的变量myName和test1。所以当foo函数执行完成之后,其整个调用栈的状态如下图所示:

9d01460635c8477d825c09994b9247db.png

从上图可以看出,foo函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的setName和getName方法中使用了foo函数内部的变量myName和test1,所以这两个变量依然保存在内存中。这像极了setName和getName方法背的一个专属背包,无论在哪里调用了setName和getName方法,它们都会背着这个foo函数的专属背包。

之所以是专属背包,是因为除了setName和getName函数之外,其他任何地方都是无法访问该背包的,我们就可以把这个背包称为foo函数的闭包

bar调用foo返回的方法后,栈内容如下:

f58d61de305144a3afdb0bac71a4798b.png

   开发者工具中的闭包展示:

84a332ef7f9b493682c7292c27165a08.png

*对于闭包返回的方法,其查找变量的顺序沿着作用域链,便为“当前执行上下文–>foo函数闭包–>全局执行上下文”也就是“Local–>Closure(foo)–>Global”,这一顺序和开头提到的例子不同,它是先找了foo函数作用域,再去找全局作用域,可见闭包函数与周围的词法环境是一体的。这也就可以大致区分,普通嵌套函数和闭包函数在作用域关系上的区别

猜你喜欢

转载自blog.csdn.net/weixin_57208584/article/details/126665155
今日推荐