关于JavaScript闭包的理解

之前对闭包的概念和理解都不是很清晰,所以写下这一篇博客来加深自己对闭包的理解,如果有什么不对的地方欢迎指正!

作用域和作用域链

说到闭包就得提一下 JavaScript 的作用域和作用域链,JavaScript 只有全局作用域函数作用域没有块级作用域,不过 ES6 中出现了 let,补上了 JavaScript 没有块级作用域的短板。

// 全局作用域(定义在全局的变量,函数内部可以访问的到)
var global = 'global'
function f () {
  console.log(global)
}
f() // => global

// 函数作用域(定义在函数内部的变量,函数外部访问不到)
function f2 () {
  var inner = 'inner'
}
console.log(inner) // => Uncaught ReferenceError: inner is not defined

// 如果是用 var 声明的变量是没有块级作用域的,但是用 let 声明的变量是块级作用域(这里暂时不讨论let)
{
  var foo = 'bar'
  let foo2 = 'bar2' // 顺便举个 let 的栗子
}
console.log(foo) // => bar
console.log(foo2) // => Uncaught ReferenceError: foo2 is not defined

总结:如果在当前作用域中找不到想要的变量,则通过作用域链向在父作用域中继续查找,直到找到第一个同名的变量为止,若果找不到,抛出 ReferenceError 错误,这就是 JavaScript 中作用域链的概念。那么问题来了,子作用域可以根据作用域链访问父作用域中的变量,那如果父作用域想访问子作用域中的变量呢?这时候就需要通过闭包来实现。

什么是闭包?

闭包就是能够读取其他函数内部变量的函数,由于在 JavaScript 语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成 “定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

在 JavaScript 高级程序设计(第3版)中是这样描述的:

闭包是指有权访问另一个函数作用域中的变量的函数。

闭包有什么作用?

  • 可以在函数的外部访问到函数内部的局部变量。
  • 让这些变量始终保存在内存中,不会随着函数的结束而自动销毁。

一些关于闭包的栗子?

例1:即使函数已经运行结束,导致创建变量的环境销毁,也依然会存在,直到访问变量的那个函数被销毁。

function fn () {
  var count = 0
  return function () {
    return ++count
  }
}

// 函数已经运行结束,创建变量的环境销毁
var fns = fn()
// 依然能够访问到函数内部的 count 变量,始终活跃在内存中
console.log(fns()) // => 1
console.log(fns()) // => 2
// 直到访问变量的那个函数被销毁,即释放对闭包的引用
fns = null
console.log(fns()) // => Uncaught TypeError: fns is not a function
// 重新创建一个,就又可以访问到了
fns = fn()
console.log(fns()) // => 1

例2:闭包只能取得包含函数中任何变量的最后一个值。因为闭包所保存的是整个变量对象,而不是某个特殊的变量。

var arr = []
for (var i = 0; i < 3; i++) {
  arr[i] = function () {
    console.log(i)
  }
}
console.log(arr[0]()) // => 3
console.log(arr[1]()) // => 3
console.log(arr[2]()) // => 3
/*
  解释:arr 数组中有三个函数,每个函数都是打印变量 i 的值,
  但是 function 的作用域中没有变量 i,i 为 undefined,
  则解析引擎会寻找父级作用域,发现父级作用域中有 i,且 for 循环绑定事件结束后,
  i 的值为 3,所以每个函数打印的都是 3,这是作用域的问题。
*/

我们把每次的 i 都保存到一个变量中,匿名闭包就可以实现想要的效果,改写后就能够正常输出0,1,2。

var arr = []
for (var i = 0; i < 3; i++) {
  arr[i] = (function (j) {
  	return function () {
  	  console.log(j)
  	}
  })(i)
}
/*
  解释:这样就使用了闭包,这里面的闭包指的是 function () { console.log(j) }
  第二个 function 里面打印的 j 是第一个 function 的参数,
  通过 (i) 执行了这里面的第一个函数,同时 i 的值被保存到 j 中,
  这样子每个点击事件中都有一个局部变量 j,j 保存的是相应的传入的 i 的值。
*/

例3:老生常谈的 for 循环中定时器的问题

console.log('start')
for(var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i)
  }, 0)
}
console.log('end')
// => start
// => end
// => 3
// => 3
// => 3
/*
  注意:setTimeout 的第二个参数不管是 1000 还是 0 都是一样的,
  这是因为 JavaScript 是单线程的,异步任务总是在同步任务执行完之后才执行,就算设置成 0,
  也要等到 for 循环执行完之后才执行 setTimeout。
*/

使用闭包稍作修改之后就能够正常输出啦。

console.log('start')
for(var i = 0; i < 3; i++) {
  (function (i) {
	setTimeout(function () {
      console.log(i)
  	}, 0)
  })(i)
  /*
  // 或者这样写
  setTimeout((function (j) {
    return function () {
	  console.log(j)
	}
  })(i), 0)
  */
}
console.log('end')
// => start
// => end
// => 0
// => 1
// => 2

闭包中 this 的指向问题

var name = "The Window"
var object = {
  name: "My Object",
  getNameFunc: function () {
    return function () {
      return this.name
    }
  }
}
console.log(object.getNameFunc()()) // => The Window

在上面这段代码中,object.getNameFunc()() 实际上是在全局作用域中调用了匿名函数,this指向了 Window。这里要理解函数名与函数功能是分割开的,不要认为函数在哪里,其内部的this 就指向哪里。Window 才是匿名函数功能执行的环境。如果想使 this 指向外部函数的执行环境,可以这样改写:

var name = "The Window"
var object = {
  name: "My Object",
  getNameFunc: function () {
    var that = this
    return function () {
      return that.name
    }
  }
}
console.log(object.getNameFunc()()) // => My Object

总结

闭包的优点:

  • 能够在减少全局变量污染的情况下,长久保存局部变量。
  • 减少了全局变量的使用,增强了网页的安全性。

闭包的缺点:

  • 常驻内存会增大内存使用量,并且使用不当很容易造成内存泄露。
  • 如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值