【JavaScript教程】js三座大山之函数闭包

零 JavaScript教程评论147字数 5140阅读17分8秒阅读模式

什么是闭包

wiki定义:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。

简化下: 闭包是由 函数 和 创建该函数的 静态词法作用域 组合而成的。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html

在这个定义下 一个函数只要有能力访问外部作用域的变量 即形成闭包。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html

所以任何js函数都可以成为闭包 因为js是静态词法作用域。 函数的作用域链在解析阶段就已经确定了。任何函数都具备访问外层数据的能力。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html

mdn定义:

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html

在mdn定义下需要满足几个条件:

  1. 需要 静态词法作用域
  2. 内部函数和外部函数
  3. 内部函数访问了外部函数的作用域(不是有能力访问而是实际访问了)

我们这里以mdn的定义为标准,同时这也是浏览器判断是否为闭包的标准...文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html

闭包判断

  • case1: 普通函数

arduino

复制代码
function test() {
    let text = 'Hello'
    console.log('test', text);
}
  • case2: 普通函数

arduino

复制代码
const text = 'Hello'
function test2() {
    console.log('test2', text);
}

test2 访问了外部作用域的变量 但是没有外层函数 所以不是一个闭包文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html

截屏2024-05-30 下午5.50.05.png debug调试也可以证明 浏览器并没有识别这个函数是一个闭包。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html

  • case3: 这也是一个普通函数

kotlin

复制代码
        function outer() {
            const outObj = {
                name: 'outer',
            };
            function inner() {
                const innerObj = {
                    name: 'foo',
                };
                return innerObj;
            }
            return inner;
        }
        const inner = outer();
        inner();

这个函数比较让人迷惑 大家可能会认为inner是一个闭包函数 仔细看我们会发现 无论是outer还是inner 都没有访问外部作用域的变量 inner函数只是生命周期被延长了 但这并不是构成闭包的条件。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html

我们通过chrome调试也可以看到,并没有形成闭包。 截屏2024-05-30 上午10.32.40.png文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html

  • case4: 在inner函数上做些改动

kotlin

复制代码
        function outer() {
            const outObj = {
                name: 'outer',
            };
            function inner() {
                const innerObj = {
                    name: 'foo',
                };
                console.log('test outObj: ', outObj)
                return innerObj;
            }
            return inner;
        }
        const inner = outer();
        inner();

在inner函数内部访问outer函数作用域中的对象outObj. 截屏2024-05-30 上午10.33.34.png 可以看到 此时chrome显示有一个闭包 并且被访问的外部变量是outObj.文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/15455.html

  • case5:在case3的基础上 直接访问全局变量

kotlin

复制代码
        var gObj = {
            name: 'global'
        }
        function outer() {
            const outObj = {
                name: 'outer',
            };
            function inner() {
                const innerObj = {
                    name: 'foo',
                };
                console.log(gObj)
                return innerObj;
            }
            return inner;
        }
        const inner = outer();
        inner();

这个例子满足静态词法作用域 满足内层函数&外层函数 但是内层函数没有访问外层函数数据 而是直接访问了全局的gObj对象 这情况chrome会认为是闭包嘛?

截屏2024-05-30 下午5.57.26.png

可以看到浏览器认为这并不形成闭包。

  • case6:这次返回一个对象

kotlin

复制代码
        function outer() {
            const outObj = {
                name: 'outer',
            };
            function inner() {
                const innerObj = {
                    name: 'foo',
                };
                console.log(outObj)
                return innerObj;
            }
            return {
                inner
            };
        }
        const out = outer();
        out.inner();

这里outer执行后返回一个新对象 对象内部引用了inner函数。 截屏2024-05-30 下午7.10.01.png 同样形成了闭包。

  • case7: 没有返回值

ini

复制代码
        let Global_inner;
        function outer() {
            const outObj = {
                name: 'outer',
            };
            function inner() {
                const innerObj = {
                    name: 'foo',
                };
                console.log(outObj)
            }
            Global_inner = inner;
        }
        outer();
        Global_inner();

这里通过直接赋值给Global_inner,看下是否可以形成闭包 截屏2024-05-30 下午7.19.32.png 这里如图 浏览器认为是一个闭包。

关于闭包定义的总结

  1. 闭包必须要有内层函数外层函数嘛?
    答:根据mdn定义和浏览器的实测表现 必须要有内外函数
  2. 闭包必须要返回一个函数嘛?
    答: 非必须,只是比较常见的使用方式而已
  3. 闭包必须要访问函数中的变量嘛?
    答:根据浏览器的表现 必须要访问另一个函数的数据 才能形成闭包。
  4. 函数访问全局中的变量会形成闭包嘛?
    答:根据浏览器的表现 不会形成闭包 参考case5。

所以通过实践我们再次确认了满足闭包的三个条件是缺一不可的
一句话概括:在一个函数中访问另一个函数的数据 即形成闭包。

函数的其他形式

从上面的例子中我们可以知道 闭包是函数的一种表现形式

在js中函数有多种表现形式,例如作为普通函数对象的方法构造函数立即执行的iife函数箭头函数作为函数的入参作为函数的返回值作为一个函数对象生成器函数闭包函数

函数对于js非常重要。我们知道js中可以有多种编程范式或者编程风格,例如 面向对象编程,元编程, 异步编程 ,但是和js最为契合的一定是函数式编程, 因为js中函数是第一公民

如何理解闭包

闭包是函数的一种表现形式 那么他和普通函数在执行上会有什么差异?我们可以通过执行栈和堆内存的引用情况分析下。

普通函数执行机制

scss

复制代码
function outer() {
    const outObj = {
        name: 'outer',
    };
    function inner() {
        const innerObj = {
            name: 'foo',
        };
        return innerObj;
    }
    inner();
}
outer();

执行栈和内存情况

  • 运行前解析阶段:
  1. 为全局上下文开辟内存空间
  2. 为outer开辟内存空间 初始化outer作用域链【scope】: 全局对象
  3. 为inner开辟内存空间 初始化inner作用域链【scope】: outer地址
  4. 在outer内部声明inner函数引用指向inner内存地址, 声明outObj变量指向outObj对象的内存地址

截屏2024-05-30 上午11.38.55.png

  • 执行阶段
  1. 执行outer函数 outer函数进入执行栈
  2. outer函数内部 执行了inner函数
  3. inner函数入栈

截屏2024-05-30 上午11.41.16.png

  1. inner执行完成出栈
  2. outer函数执行完成出栈
  3. 清空内存对象

截屏2024-05-30 上午11.31.52.png

闭包函数执行机制

kotlin

复制代码
  function outer() {
            const outObj = {
                name: 'outer',
            };
            function inner() {
                const innerObj = {
                    name: 'foo',
                };
                console.log('test outObj: ', outObj)
                return innerObj;
            }
            return inner;
        }
        const inner = outer();
        inner();
  • 运行前解析阶段和普通函数相同。
  • 执行阶段
  1. 执行outer函数 outer函数进入执行栈
  2. outer执行完成 返回值被全局属性inner引用了 outer出栈。
  3. inner函数入栈继续执行 截屏2024-05-30 上午11.50.53.png

截屏2024-05-30 上午11.51.19.png

  1. inner函数执行完成
  2. 因为inner被全局引用 所以全局有指针指向inner 而inner又可以访问到outer

截屏2024-05-30 上午11.55.28.png

  1. 所以垃圾回收机制(GC)后内存依然存在 不会被释放

截屏2024-05-30 上午11.56.29.png

区别

普通函数执行完成 会出栈 内部数据会被清空。

闭包函数 外层函数执行完成 内层函数持有外层函数的作用域链 因此外层函数中的数据不会被清空 依然可以被内层函数访问到。只有清空了内层函数的引用 才能释放内存。

应用场景

控制数据访问权限

js实现私有属性的六种方式 文章中我们通过闭包函数,实现了数据的私有化,达到了控制变量访问权限的作用。

javascript

复制代码
function createObject() {
    var privateProperty = 'I am private';

    return {
        getPrivateProperty() {
            return privateProperty;
        },
    };
}

var obj = createObject();
console.log(obj.getPrivateProperty()); // "I am private"
console.log(obj.privateProperty); // undefined

延长变量生命周期 缓存数据

  1. 防抖

javascript

复制代码
function debounce(fn, delay) {
    let timer;
    return function (...arg) {
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(fn.bind(this, ...arg), delay);
    };
}

function test(...args){
    console.log('test args', args, this)
    return args;
}

const dTest = debounce(test, 200);
dTest.call({name: 'call'}, 1,2,3,4,5);

缓存计时器字段,每次调用判断run函数 可以获取到外层的timer是否存在 存在清空计时器。可以实现防抖功能。

  1. 节流

javascript

复制代码
function throttle(fn, delay) {
    let lastCallTime = 0 ;
    return function (...arg) {
        if (Date.now() - lastCallTime < delay) {
            return;
        }
        lastCallTime = Date.now();
        fn.call(this, ...arg);
    };
}

缓存上一次执行时间。

  1. react中的闭包

scss

复制代码
function MyComp = () => {
  const [count, setCount] = useState(0)
  useEffect(() => {
    setInterval(() => {
      console.log('test timer count', count)
    }, 1000)
  }, [])
  return <div onClick={() => setCount((pre) => pre + 1)}>{count}</div>
}

相信每个刚刚接触react + hooks的同学都有这样的疑问 点击按钮视图正常更新,为什么定时器里获取到的count一直是0。

  • 首先计时器传入的回调函数访问了外部的count数据 因此形成了闭包 即使MyComp执行完成,依然可以访问到作用域的数据
  • 因为react的useEffect hooks的依赖列表为【】。因此多次执行MyComp函数这个hook回调 只会执行一次。读取到的一直是之前的旧数据。

截屏2024-05-30 下午5.02.12.png

闭包与内存泄漏

内存泄漏的本质:不被使用的变量没有被及时释放,这通常是因为变量的生命周期大于其作用域导致的。

闭包可以延迟变量的生命周期,这是闭包的主要用途之一,因此闭包容易引起内存泄漏。

闭包为什么对js这么重要

闭包贯穿了多个核心基础概念。

首先,闭包的本质是函数,捕获了其静态词法作用域,从而形成了作用域链。这使得函数能够跨越其定义环境访问外部变量,是对作用域链和内存引用管理的深刻理解。

其次,闭包揭示了 JavaScript 函数作为一等公民的重要特征。函数不仅可以作为值传递,还能保留其定义时的上下文,这为函数式编程提供了强大支持。

通过闭包,我们能够创建私有变量、实现数据封装和模块化,提升代码的安全性和可维护性。因此,掌握闭包是全面理解和高效使用 JavaScript 的关键。

零
  • 转载请务必保留本文链接:https://www.0s52.com/bcjc/javascriptjc/15455.html
    本社区资源仅供用于学习和交流,请勿用于商业用途
    未经允许不得进行转载/复制/分享

发表评论