JavaScript学习笔记(十一) 闭包

简介: JavaScript学习笔记(十一) 闭包

前言


什么是闭包?我们先来看看《JavaScript 权威指南》中的定义:

函数对象可以通过作用域链关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性称为闭包

哈哈哈看完是不是一脸懵呢?没关系,下面我们从最简单的作用域、作用域链开始,一步步探索究竟什么是闭包


正文


1、作用域


(1)函数作用域


什么是作用域?一个变量的作用域就是源代码中定义这个变量的区域,在 JavaScript 中采用的是函数作用域

也就是说,变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的

function scope() {
    if (true) {
        var i = 0
        for (var j = 1; j <= 5; j++) {
            i = i + j
        }
        console.log(j)
    }
    console.log(i)
    ~function() { // 立即执行函数
        console.log(i)
      console.log(j)
    }()
}
scope()
/*
 * 执行结果:
 * 6
 * 15
 * 15
 * 6
**/


(2)声明提前


JavaScript 采用函数作用域,这意味着在函数内声明的所有变量在该函数体内都是可见的

这导致了变量在声明前就已可用,在 JavaScript 中这种现象称为声明提前,但要注意的是赋值不会提前

function hoisting() {
    console.log(i) // 声明提前,但赋值并不会提前
    var i = 'hello'
    console.log(i)
}
hoisting()
/*
 * 执行结果:
 * undefined
 * hello
**/


2、作用域链


(1)声明上下文对象


我们知道,在 JavaScript 中全局变量是全局对象的属性,但是很多人不知道局部变量也是某个对象的属性


这个对象叫做声明上下文对象,它是与函数调用相关的一个对象,作为一种内部实现,我们无法直接引用


(2)作用域链


每段 JavaScript 代码(全局代码或函数)都有一个与之关联的作用域链


所谓的作用域链其实就是一个对象列表,这组对象定义这段代码作用域中的变量,称为变量对象


一般来说,变量对象包括定义全局变量的全局对象和定义局部变量的声明上下文对象


在全局代码中(就是不包含在函数定义中的代码),作用域链上只有一个对象,就是全局对象


对于没嵌套的函数,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象


对于有嵌套的函数,它的作用域链上至少存在三个对象(想一想这三个对象分别是什么)


(3)变量解析


当 JavaScript 需要查找一个变量 x 的值时,它会从作用域链上的第一个对象开始查找


如果这个对象上有名为 x 的属性,则直接使用该属性的值作为变量 x 的值


如果这个对象没有名为 x 的属性,就会查找作用域链上的下一个对象,直至到达作用域链的最后


如果作用域链上没有任何一个对象存在名为 x 的属性,那么抛出一个引用异常


(4)函数的作用域链


当定义一个函数时,就会保存一个作用域链;当调用这个函数时,就会创建一个新的对象用于保存局部变量


然后将这个对象添加到保存的作用域链上,并且创建一个新的作用域链表示函数调用的作用域


当函数返回的时候,就会从保存的作用域链上删除这个对象,但是这并不意味着对象马上就被当作垃圾回收


因为按照 JavaScript 的垃圾回收机制,只有当对象没有被引用的时候,才会被 JavaScript 回收


3、闭包


(1)闭包的理解


如果一个函数内部包含嵌套函数(但是没有返回嵌套函数),那么外部函数返回后,嵌套函数也被回收


闭包的核心在于一个函数包含嵌套函数,并且返回这个嵌套函数,这时就有一个外部的引用指向这个嵌套的函数


这个嵌套函数就不会被当作垃圾回收,它所对应的作用域链也被保存下来


var global_scope = 'global'
function outer_function(outer_params) {
    var outer_scope = 'outer'
    console.log(outer_scope)
    // console.log(inner_scope) -> inner_scope is not defined
    var inner_function = function(inner_params) {
        var inner_scope = 'inner'
        console.log(outer_scope)
      console.log(inner_scope)
    }
    return inner_function
}
outer_function('outer')('inner')
/*
 * 执行结果:
 * outer
 * outer
 * inner
**/


在调用 outer_function 的时候,作用域链如下:


  • outer_function{outer_params:'outer',outer_scope:'outer',inner_function:function}
  • window{global_scope:'global'}


故打印 outer_scope 时,首先查找 outer_function 对象,里面能够找到 outer_scope 属性,直接返回


若要打印 inner_scope,查找 outer_function 对象和 window 对象都没有找到 outer_scope 属性,抛出异常


在调用 inner_function 的时候,作用域链如下:


  • inner_function{inner_params:'inner',inner_scope:'inner'}
  • outer_function{outer_params:'outer',outer_scope:'outer',inner_function:function}
  • window{global_scope:'global',outer_function:function}

打印 outer_scope 时,首先查找 inner_function 对象,没找到,然后查找 outer_function 对象,能找到


打印 inner_scope 时,首先查找 inner_function 对象,能找到


(2)闭包的应用


  • 定义私有属性
var counter = (function() { // 立即执行函数,返回一个对象
    var value = 0 // 私有属性,无法直接访问
    var changeBy = function(val) { value += val } // 私有方法,无法直接访问
    // 以下多个嵌套函数共享一个作用域链
    return {
        getValue: function() { return value },
        increase: function() { changeBy(+1) },
        decrease: function() { changeBy(-1) }
    }
})()
console.log(counter.getValue())
counter.increase()
console.log(counter.getValue())
counter.decrease()
console.log(counter.getValue())
/*
 * 执行结果:
 * 0
 * 1
 * 0
**/


  • 缓存处理结果
function memory(f) {
    // 缓存处理结果
    var cache = {}
    return function() {
        // 将传入的参数作为键
        var key = arguments.length + Array.prototype.join.call(arguments, ',')
        if (key in cache) { // 如果值在缓存,直接读取返回
            return cache[key]
        } else { // 否则执行计算,并把结果放到缓存
            return cache[key] = f.apply(this, arguments)
        }
    }
}
var factorial = function(n) { return (n <= 1) ? 1 : n * factorial(n - 1) }
var factorialWithMemory = memory(factorial)


(3)使用闭包的注意事项


  • 外部变量的值是否改变
function test() {
    var array = []
    for(var count = 0; count < 5; count++) {
        array[count] = function() { console.log(count) }
    }
    return array
}
var result = test()
result[0]()
result[1]()
result[2]()
result[3]()
result[4]()
/*
 * 执行结果:
 * 5
 * 5
 * 5
 * 5
 * 5
**/
/*
 * 结果分析:
 * 在调用数组中的函数时,由于需要打印变量 count 的值,所以沿着这些函数的作用域链往上查找
 * 最终在外层函数 test 的变量对象中找到,但是此时局部变量 count 的值已经变成 5
**/



解决办法

function test() {
    var array = []
    for(var count = 0; count < 5; count++) {
        array[count] = function(value) { // 立即执行函数
            return function() { console.log(value) }
        }(count)
    }
    return array
}
var result = test()
result[0]()
result[1]()
result[2]()
result[3]()
result[4]()
/*
 * 执行结果:
 * 0
 * 1
 * 2
 * 3
 * 4
**/
/*
 * 结果分析:
 * 同样道理,由于需要打印变量 value 的值,所以在调用数组中的函数时需要往上查找作用域链
 * 但是此时在上层函数对应的变量对象中即可找到 value,并且 value 的值等于当时传入的 count 的值
**/


  • this 的指向是否符合预期
var test = {
  value: 0,
    getValue: function() {
        return function() { console.log(this.value) }
    }
}
var result = test.getValue()
result()
/*
 * 执行结果:
 * undefined
**/
/*
 * 结果分析:
 * 在调用闭包函数时,是在全局作用域的环境下执行的,因此 this 指向全局对象
**/


解决方法

var test = {
  value: 0,
    getValue: function() {
        return function() { console.log(this.value) }.bind(this)
    }
}
var result = test.getValue()
result()
/*
 * 执行结果:
 * 0
**/
/*
 * 结果分析:
 * 使用 bind 函数将 this 的值绑定为 test 对象
**/
目录
相关文章
|
8天前
|
自然语言处理 JavaScript 前端开发
JavaScript中闭包:概念、用途与潜在问题
【4月更文挑战第22天】JavaScript中的闭包是函数及其相关词法环境的组合,允许访问外部作用域,常用于数据封装、回调函数和装饰器。然而,不恰当使用可能导致内存泄漏和性能下降。为避免问题,需及时解除引用,减少不必要的闭包,以及优化闭包使用。理解并慎用闭包是关键。
|
8天前
|
JavaScript
闭包(js的问题)
闭包(js的问题)
13 0
|
8天前
|
JavaScript 前端开发
解释JavaScript闭包的工作原理,并举例说明其在游戏开发中的应用。
JavaScript闭包允许内部函数访问并保持对外部函数变量的引用,即使外部函数执行结束。当函数返回内部函数时,形成闭包,继承父函数作用域链。在游戏开发中,闭包用于创建具有独立状态和行为的角色实例。例如,`createCharacter`函数创建角色并返回包含属性和方法的对象,内部函数如`getHealth`、`setHealth`和`attack`通过闭包访问并操作角色的变量。这种方式确保了每个角色的状态在不同实例间独立,是实现游戏逻辑的强大工具。
14 2
|
6天前
|
JavaScript 前端开发
JavaScript 闭包:让你更深入了解函数和作用域
JavaScript 闭包:让你更深入了解函数和作用域
|
6天前
|
自然语言处理 JavaScript 前端开发
JavaScript闭包基础
JavaScript闭包基础
|
8天前
|
缓存 自然语言处理 JavaScript
JavaScript内存泄漏导致应用性能下降,常见于闭包使用不当
【5月更文挑战第14天】JavaScript内存泄漏导致应用性能下降,常见于闭包使用不当。闭包能记住并访问词法作用域,若函数返回后,其引用的对象未被释放,就会引发泄漏。例如,`createLeakyFunction`创建的闭包保留了对大型对象`someLargeObject`的引用,即使函数执行完毕,对象也无法被垃圾回收。避免泄漏的方法包括及时解除引用、清除事件监听器、使用WeakMap和WeakSet以及定期清理缓存。使用性能分析工具可检测和修复内存泄漏问题。
18 3
|
8天前
|
JavaScript 前端开发
JavaScript闭包允许内部函数访问并保留外部函数的变量,即使外部函数执行结束
【5月更文挑战第13天】JavaScript闭包允许内部函数访问并保留外部函数的变量,即使外部函数执行结束。在游戏开发中,闭包常用于创建独立状态的角色实例。例如,`createCharacter`函数生成角色,内部函数(如`getHealth`、`setHealth`)形成闭包,保存角色的属性(如生命值)。这样,每个角色实例都有自己的变量副本,不互相影响,从而实现角色系统的独立性。
21 0
|
8天前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包机制
闭包是JavaScript中一个重要且常被误解的概念。本文将深入探讨闭包的本质、工作原理以及在实际开发中的应用。通过详细解析闭包的定义、作用域链、内存管理等方面,读者将对闭包有更清晰的理解,并能够运用闭包解决实际开发中的问题。
|
8天前
|
前端开发 JavaScript
闭包在JavaScript中有许多应用场景
【5月更文挑战第7天】闭包在JavaScript中发挥关键作用,如封装私有变量和函数提升安全性,维护变量生命周期,实现高阶函数,模拟块级作用域,支持回调函数以处理异步操作,以及促进模块化编程,增强代码组织和管理。闭包是理解和掌握JavaScript高级特性的重要一环。
30 7
|
8天前
|
自然语言处理 JavaScript 前端开发
【JavaScript技术专栏】深入理解JavaScript作用域与闭包
【4月更文挑战第30天】了解JavaScript的关键在于掌握作用域和闭包。作用域决定变量和函数的可访问范围,分为全局(在`window`或`global`对象中)和局部(函数内部)。闭包则允许函数访问其创建时的作用域,即使在其他地方调用。它通过作用域链保存对外部变量的引用,常用于实现私有变量、模块化和柯里化。然而,不当使用闭包可能导致内存泄漏和性能下降。理解这些概念能提升代码质量,但也需谨慎处理潜在问题。