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 对象
**/
目录
相关文章
|
16天前
|
前端开发 JavaScript Java
JavaScript闭包深入剖析:性能剖析与优化技巧
JavaScript 闭包是强大而灵活的特性,广泛应用于数据封装、函数柯里化和事件处理等场景。闭包通过保存外部作用域的变量,实现了私有变量和方法的创建,提升了代码的安全性和可维护性。然而,闭包也可能带来性能问题,如内存泄漏和执行效率下降。为优化闭包性能,建议采取以下策略:及时解除对不再使用的闭包变量的引用,减少闭包的创建次数,使用 WeakMap 管理弱引用,以及优化闭包结构以减少作用域链查找的开销。在实际开发中,无论是 Web 前端还是 Node.js 后端,这些优化措施都能显著提升程序的性能和稳定性。
110 70
|
1天前
|
自然语言处理 JavaScript 前端开发
当面试官再问我JS闭包时,我能答出来的都在这里了。
闭包(Closure)是前端面试中的高频考点,广泛应用于函数式编程中。它不仅指函数内部定义的函数,还涉及内存管理、作用域链和垃圾回收机制。闭包可以让函数访问其外部作用域的变量,但也可能引发内存泄漏等问题。通过合理使用闭包,可以实现模块化、高阶函数和回调函数等应用场景。然而,滥用闭包可能导致代码复杂度增加、调试困难以及潜在的性能问题。为了避免这些问题,开发时应谨慎处理闭包,避免不必要的嵌套,并及时清理不再使用的变量和监听器。
当面试官再问我JS闭包时,我能答出来的都在这里了。
|
4月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
3月前
|
JavaScript 前端开发
js 闭包的优点和缺点
【10月更文挑战第27天】JavaScript闭包是一把双刃剑,在合理使用的情况下,它可以带来很多好处,如实现数据封装、记忆功能和模块化等;但如果不注意其缺点,如内存泄漏、变量共享和性能开销等问题,可能会导致代码出现难以调试的错误和性能问题。因此,在使用闭包时,需要谨慎权衡其优缺点,根据具体的应用场景合理地运用闭包。
148 58
|
3月前
|
缓存 JavaScript 前端开发
js 闭包
【10月更文挑战第27天】JavaScript闭包是一种强大的特性,它可以用于实现数据隐藏、记忆和缓存等功能,但在使用时也需要注意内存泄漏和变量共享等问题,以确保代码的质量和性能。
50 7
|
3月前
|
自然语言处理 JavaScript 前端开发
JavaScript闭包:解锁编程潜能,释放你的创造力
【10月更文挑战第25天】本文深入探讨了JavaScript中的闭包,包括其基本概念、创建方法和实践应用。闭包允许函数访问其定义时的作用域链,常用于数据封装、函数柯里化和模块化编程。文章还提供了闭包的最佳实践,帮助读者更好地理解和使用这一强大特性。
35 2
|
3月前
|
存储 缓存 自然语言处理
掌握JavaScript闭包,提升代码质量与性能
掌握JavaScript闭包,提升代码质量与性能
|
3月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包(Closures)
深入理解JavaScript中的闭包(Closures)
|
3月前
|
存储 自然语言处理 JavaScript
深入理解JavaScript的闭包(Closures)
深入理解JavaScript的闭包(Closures)
53 0
|
4月前
|
设计模式 JavaScript 前端开发
探索JavaScript中的闭包:从基础概念到实际应用
在本文中,我们将深入探讨JavaScript中的一个重要概念——闭包。闭包是一种强大的编程工具,它允许函数记住并访问其所在作用域的变量,即使该函数在其作用域之外被调用。通过详细解析闭包的定义、创建方法以及实际应用场景,本文旨在帮助读者不仅理解闭包的理论概念,还能在实际开发中灵活运用这一技巧。

热门文章

最新文章