Javascript中的闭包详解(closure)和相关面试题

闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数”。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

1.变量的作用域

要理解闭包,首先必须理解Javascript特殊的变量作用域:全局变量和局部变量。Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。

	var n=999;

  function f1(){
    alert(n);
  }

  f1(); // 999

但在函数外部自然无法读取函数内的局部变量。

	function f1(){
    var n=999;
  }

  alert(n); // error

注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

	function f1(){
    n=999;
  }

  f1();

  alert(n); // 999
2.在函数外读取内部变量

看下面一个例子:

function userFunc() {
    var name = "账单"; // name 是一个被 userFunc 创建的局部变量
    var age = 24;
    function addAge() { 	// addAge() 是内部函数,一个闭包
    	agg++;
        console.log(age); // 使用了父函数中声明的变量
    }
    addAge();		// 调用函数
}
var myFunc = userFunc();

userFunc() 创建了两个局部变量 nameage和一个名为 addAge() 的函数。addAge() 是定义在 userFunc() 里的内部函数,并且仅在 userFunc() 函数体内可用。addAge()没有自己的局部变量。然而,因为它可以访问到外部函数的变量,所以 addAge() 可以使用父函数 userFunc() 中声明的变量 age
运行之后,addAge()中的console.log() 语句成功打印出了变量 age 的值(该变量在其父函数中声明)。既然addAge可以读取userFunc中的局部变量,那么只要把addAge作为返回值,我们不就可以在userFunc外部读取它的内部变量了吗!
现在来考虑以下例子 :

	function userFunc() {
      var name = '张三'
      var age = 24
      
      function addAge() {
        console.log(age)
        age++
      }

      return addAge
    }

    var user = userFunc()
    user()

运行这段代码的效果和之前 userFunc() 函数的示例完全一样。其中不同的地方在于内部函数 addAge() 在执行前,从外部函数返回。
第一眼看上去,也许不能直观地看出这段代码能够正常运行。在一些编程语言中,一个函数中的局部变量仅存在于此函数的执行期间。一旦 userFunc() 执行完毕,你可能会认为 age 变量将不能再被访问。然而,因为代码仍按预期运行,所以在 JavaScript 中情况显然与此不同。

原因在于,JavaScript中的函数会形成了闭包。 闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例子中,user 是执行 userFunc 时创建的 addAge 函数实例的引用。addAge 的实例维持了一个对它的词法环境(变量 age 存在于其中)的引用。因此,当 user 被调用时,变量 age 仍然可用,其值 24 就被传递到console.log()中,并且自增1。
让我们看一下更直观的描述,在Google Chrome中打开closure.html,打开 检查-sources进行断点调试。

// closure.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script>
    function userFunc() {
      var name = '张三'
      var age = '24'

      function displayName() {
        alert(name)
      }

      function addAge() {
        age++
        console.log(age)
      }

      return addAge
    }

    var user = userFunc()
    user()
    //    user()
    //    user()
  </script>
</head>
<body>

</body>
</html>

下图中的三个断点执行顺序为3,2,1,执行到断点2时,user已被实例化,可以看到右侧红框中的Local有两个变量和一个addAge方法,Local是被浏览器划分出来的临时内存区域,在函数执行完毕后会被销毁;
断点2
继续执行下一个断点,上面提到过,user 是执行 userFunc 时创建的 addAge 函数实例的引用,因此user()语句执行时就会使得addAge()执行,我们可以看到右侧Local中已无变量和函数,说明userFunc()实例化之后变量被销毁了,但是下方红框中出现了Closure模块,这就是我们所说的闭包函数了,addAge 的实例维持了一个对它的词法环境(变量 age 存在于其中)的引用。因此,当 user 被调用时,变量 age 仍然可用。
断点
再执行两次user()

	user()
    user()

可以看到Closure中的age值一直在增加,控制台中也打印出了递增的age,这说明age变量常驻内存没有被销毁,为什么呢?因为userFuncaddAge的父函数,而var user = userFunc()使得addAge被赋给了一个全局变量,这导致addAge始终在内存中,而addAge的存在依赖于userFunc,因此userFunc也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
age增加
打印age

3.闭包用途

由此可以看出闭包的两大用途:

  1. 保护:保护私有变量不受外界干扰,与外界没有必然关系,可以读取另一个函数内部的变量;
  2. 保存:形成一个不销毁的私有栈内存,让这些变量的值始终保持在内存中;
    以jQuery为例:
    jQuery(JQ)前端非常经典的类库:提供了大量的方法公开发人员使用
    为了防止全局变量污染(解释:导入JQ后,它里面有大量的方法,如果这些方法不保护起来,用户编写的方法很容易和JQ方法名字相同的产生冲突,产生冲突可以理解为全局变量污染),JQ中的方法和变量需要用闭包保护起来
/*==JQ源码解析==*/
(function(global, factory){
  //...
  // typeof window!=="undefined" ? window : this 验证当前所处环境的全局对象是window还是global等
  //factory => function qxj(window, noGlobal){}
  factory(global) //=> qxj(window)
})(window, function qxj(window, noGlobal){
  //...
  var jQuery = function(selector, context){
    //...
  }
  
  // => 通过给全局对象增加属性:jQuery和$,把私有的jQuery方法暴露到全局作用域下,供外面使用(等价于return jQuery)(外界需要使用函数中的私有内容,我们可以基于window.xxx和return xxx两种方式实现需求)
  window.jQuery = window.$ = jQuery
})

// => 开始使用JQ
jQuery()		// => window.jQuery()
$()

在真实的项目中,我们一般都要把自己写的内容放到一个闭包中,这样可以有效防止自己的代码和别人的代码产生冲突(全局变量污染:真实项目中是要尽可能减少对全局变量的使用的);如果需要把自己的东西给别人用,基于return xxx和window.xxx等方式暴露给别人即可。

// => 原生JS
var xxx = (function({
  //...A自己写的代码
  return xxx
}))();
           
(function({
  //...B自己写的代码
  window.xxx = xxx
}))()

// => JQ
$(function(){
  //...这样写在某些程度上也是为了减少全局变量
})        
4.使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

5.闭包相关面试题

eg1:

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));
console.log(add10(2));

输出:7 12
在这个示例中,我们定义了 makeAdder(x) 函数,它接受一个参数 x ,并返回一个新的函数。返回的函数接受一个参数 y,并返回x+y的值。

从本质上讲,makeAdder 是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。

add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。
eg2:

let x = 1
function A(y) {
  let x = 2
  function B(z) {
    console.log(x + y + z)
  }
  return B
}
let C = A(2)
C(3)

输出:7
图解:
eg2图解
eg3:

let x = 5
function fn(x) {
  return function (y) {
    console.log(y + (++x))
  }
}
let f = fn(6)
f(7)
fn(8)(9)
f(10)
console.log(x)

输出:14 18 18 5
图解:
eg3图解
eg4:

let a = 0,
  b = 0
function A(a) {
  A = function (b) {
    console.log(a + b++)
  }
  console.log(a++)
}
A(1)
A(2)

输出:1 4
图解:
eg4
【涉及知识点】:函数重构、函数上级作用域时查找

注意点:函数找上级作用域时,有一个准则:它的亲爹妈是谁,上级作用域就是谁,即它在哪儿创建的,上级作用域就是谁。

eg4:

var x = 3,
  obj = {x: 5}
obj.fn = (function () {
  this.x *= ++x
  return function (y) {
    this.x *= (++x) + y
    console.log(x)
  }

})()
var fn = obj.fn
obj.fn(6)
fn(4)
console.log(obj.x, x)

输出:13 234 95 234
图解:
eg4
【涉及知识点】:函数执行this指向、函数上级作用域时查找,运算符优先级
this.x *= (++x) + y => this.x = this.x * ((++x) + y)

总结
通俗讲,函数执行会形成一个全新的私有作用域,保护里面的变量不受外界的干扰,这种保护机制就叫做闭包。
外面所说的闭包:函数执行形成的私有作用域,而且栈内存不销毁,那么里面的私有变量和外面的不冲突,并且还可以保存下来。

参考资料:
学习Javascript闭包(Closure)
JavaScript闭包

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值