一天一个知识点 - 浅谈 JavaScript 闭包

前言

前些日子,在掘金上看到一片热门文章《在酷家乐做面试官的日子》。该文作者以面试官的角度,详细阐述了作为一名 web 应聘者应该具有哪些技能,才会更让人青睐。

在对比自身的过程中,发现有一些问题,或许了解,但不全面,这也是本系列文章诞生的缘由。

什么是闭包

闭包是函数和声明该函数的词法环境的组合。(源自 MDN)

官方解释通常来讲都非常拗口,通俗点来讲:

闭包就是创建一个了上下文环境,这个环境包含了创建时所能访问的所有局部变量。并且不受 GC 影响。

最简单的闭包

我们先来看一个最简单闭包声明:


function func1() {
    // func1 函数的局部变量
    var msg = 'hello world!'
    
    
    // 依据变量作用域的定义, 在函数内部创建的函数, 根据函数的作用域链,使其可以访问其父函数的变量
    // 这段代码中,func2 就是一个闭包。
    function func2() {
        console.log(msg)
    }
    
    func2()
}

func1() => hello world

复制代码

看到这,很多同学有疑惑,这不就是一个普通的方法调用吗?在实际的代码中,经常这样写,并没发现特殊的地方啊。

实际上,函数 func2 就是一个闭包,它遵循闭包的定义和内部概念,在 func2 的上下文环境中中,包含了它创建时所有访问的所有局部变量 - msg,并且 msg 不受 GC 影响。

闭包的进阶表现

我们看另外一段进阶代码,来验证为什么说 func2 是一个闭包:


function func1() {
    var msg = 'hello world!'
    
    function func2() {
        console.log(msg)
    }
    
    return func2
}

var func = new func1()

func()

复制代码

相较于上一个例子,仅仅是将 func2() => return func

想想看,func() 会输出什么?实际也是 "hello world!"。

我们回到闭包的定义中:

闭包就是创建一个了上下文环境,这个环境包含了创建时所能访问的所有局部变量。并且不受 GC 影响。

我们尝试理解,func2是一个闭包,在 func2 创建时,这个环境可以访问当时创建的局部变量,当 func1 执行时,返回了这个封闭环境,并且这个封闭环境不受 GC 影响,仍然可以访问 msg 变量。

再稍微进阶一点


function func1(p1) {
    return function func2(p2) {
        return p1 + p2
    }
}

var f1 = new func1(1)
var f2 = new func1(10)

f1(1) => 2
f2(10) => 20

复制代码

上述代码中,f1、f2 各自为闭包,虽然拥有相同的函数定义,但实际上拥有各自不同的上下文环境:

函数 f1 => 1 + 1 => 2

函数 f2 => 10 + 10 => 20

闭包的作用

通过上面的例子,我们知道了闭包的概念和定义。那么闭包究竟有什么实际作用呢?

在我的理解中,闭包是为了解决函数作用域链上的变量值问题。

有几个经典应用:

函数柯里化


/*
 * 函数柯里化可以有效对代码解耦
 * 得益于闭包的封闭的上下文环境
 * 在 f1、f2、f3 实例化对象中,都拥有不同的词法环境
 */
function f (x) {
  return function (y) {
    return function (z) {
      return function (a) {
        return function (b) {
          return function (c) {
              console.log(x  + y + z + a + b + c);
          };
        };
      };
    };
  };
}

var f1 = f(1)
var f2 = f(2)
var f3 = f(10)(10)

复制代码

模拟私有方法

/*
 * 得益于闭包的封闭的上下文环境,我们在每个 f1、f2 实例化对象中,都拥有不同的词法环境
 * 不仅如此,该函数还扩展了私有方法
 * changeIndex 则是私有方法,它对函数内部公开,对函数外部隐藏
 */
var func1 = function() {
  var index = 0
  
  // changeIndex 则是私有方法
  function changeIndex(val) {
    index = index + val
    
    return index
  }
  return {
    increment: function() {
      return changeIndex(1);
    },
    decrement: function() {
      return changeIndex(-1);
    },
    value: function() {
      return index;
    }
  }  
};

var f1 = new func1()
var f2 = new func1()

f1.value() => 0
f1.increment() => 1
f1.increment() => 2
f1.decrement() => 1

// f1 对比 f2
f1.value() => 1
f2.value() => 0

复制代码

经典问题

闭包的循环引用问题


/*
 * 期望循环输出 0 - 9
 * 实际循环输出 10
 */
function f1(){
    console.log('begin')
    
    for (var index = 0; index < 10; index++) {
        setTimeout(() => {
            console.log(index)
        }, 1000);
    }
    
    console.log('end')
}

f1()

复制代码

在这个闭包循环的经典问题中,考察了我们对闭包、函数作用域、事件循环的理解。

我们尝试先通过事件循环理解一下该函数的运行:

  1. 主线程执行函数 f1 的定义并执行函数 f1。
  2. 主线程执行常规任务 => console.log('begin')
  3. 主线程执行 for 循环与 setTimeout 函数本身。
  4. 将 setTimeout 的回调函数的 Task 添加到 Task Queue。
  5. 主线程执行常规任务 => console.log('end')。
  6. 主线程的执行栈为空
  7. 定时器触发,主线程取出 setTimeout 的 Task 并执行相应的回调函数(此时,for 循环早已结束,index 值变成10)。
  8. 输出 10 次 10

那么如何改进呢?

使用自执行函数解决

/*
 * 实际循环输出 0 - 9
 */
function f1() {
    console.log('begin')

    for (var index = 0; index < 10; index++) {
        ;(function(index) {
            setTimeout(() => {
                console.log(index)
            }, 1000)
        })(index)
    }

    console.log('end')
}

f1()

复制代码
使用 ES6 的 Let 解决

/*
 * 实际循环输出 0 - 9
 */
function f1() {
    console.log('begin')

    for (let index = 0; index < 10; index++) {
        setTimeout(() => {
            console.log(index)
        }, 1000)
    }

    console.log('end')
}

f1()

复制代码

参考

系列文章

转载于:https://juejin.im/post/5c7d0335f265da2ddf78a8a9

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值