【学习笔记】JS基础-作用域和闭包

作用域

概念

变量在程序中定义的可见范围。

详细解释

JavaScript采用的是词法作用域,即变量的作用域在定义变量时根据定义的位置决定。

JavaScript中有三种作用域:全局作用域、函数作用域和块级作用域。

全局作用域中定义的变量在所有地方都可见;函数中定义的变量只在函数内部可见;块,也就是大括号定义的变量只在块内可见。其中块级作用域是ES6新增的,搭配使用的是let和const声明方式,主要为了解决var声明对更大范围作用域的污染的问题。

作用域之间可以存在嵌套关系,如全局作用域内部包含函数作用域,内部又包含块级作用域。编译器在查找变量定义的时候会从内到外沿着作用域链查找,直到找到最近的定义,如果达到全局作用域仍然找不到,则会抛出ReferenceError。

总结

作用域是变量能够被访问到的范围,JS中包含三种作用域以及三种声明变量的方式,let 和const的作用域能够达到块级,var只有全局和函数级。

闭包

概念

内部函数持有对定义时的作用域内的变量的引用。即一个函数能够访问到它定义时所在的作用域的变量。

详细解释

将内部函数传递到所定义的词法作用域之外的地方去调用,此时该函数持有对定义时的词法作用域的引用,形成了闭包。当函数执行完毕后,该函数内部定义的变量和函数仍然存在于内存中,不会被自动回收,因此可以被其他函数继续访问和使用。这个机制称为闭包。

闭包的应用非常广泛,特别是在异步编程和模块化开发中。以下是一些常见的使用场景:

  1. 保存变量状态和私有化变量和函数。
  2. 用于事件处理和回调函数。
  3. 用于封装模块。
  4. 用于解决循环中异步问题。
  5. 用于实现缓存和记忆化等功能。

举例

循环错觉

来自《你不知道的JavaScript·上》

以下代码会输出5个6,而不是1,2,3,4,5

得到这个结果的原因是var声明的变量在全局作用域中,而timer在执行的时候取到的值都是全局作用域的这一份,并不取得到每轮循环时的副本值。

for (var i=1; i<=5; i++) {
  setTimeout( function timer() {
    console.log( i );
  }, i*1000 );
}

如何修正,让代码输出1-5呢,有若干种办法,核心都是创造非全局作用域。

方法一:创建函数作用域,并使用闭包保存副本。函数作用域使用立即执行函数实现

for (var i=1; i<=5; i++) { 
  (function() {
    var j = i;
    setTimeout( function timer() {
        console.log( j );
    }, j*1000 );
  })(); 
}

上面的写法在每轮循环内部创建了timer的闭包,把j作为词法作用域中的变量放了进来,使得i在那一轮的值能够被保存下来。

下面的写法也是能达到同样效果的,这里有个隐式的LHS,把i当做参数传进来。

for (var i=1; i<=5; i++) {
  (function(j) {
    setTimeout( function timer() {
      console.log( j );
    }, j*1000 );
  })( i );
}

方法二:使用let创建块作用域,并使用闭包保存副本

let声明在每轮循环都会执行一次(这个有点反直觉),且这个变量是块级作用域的,每一轮循环会被作为闭包保存下副本。

for (let i=1; i<=5; i++) {
  setTimeout( function timer() {
     console.log( i );
  }, i*1000 );
}
模块管理

来自《你不知道的JavaScript·上》

构成闭包的地方是define和get方法对modules的引用。

var MyModules = (function Manager() {
    var modules = {};
    function define(name, deps, impl) {
        for (var i = 0; i < deps.length; i++) {
            const depName = deps[i];
            // 把名称替换为实例。此处引用了modules,构成了闭包
            deps[i] = modules[depName];
        }
        // 把deps传入impl,则为包装好了依赖的模块实例
        modules[name] = impl.apply(impl, deps);
    }
    function get(name) {
        // 此处构成了闭包
        return modules[name];
    }
    return {
        define: define,
        get: get
    };
})();
// 定义没有依赖的模块
MyModules.define("bar", [], function () {
    function hello(who) {
        return "Let me introduce: " + who;
    }
    return {
        hello: hello
    };
});
MyModules.define("bar2", [], function () {
    function hello(who) {
        return "Let me introduce: " + who + " again";
    }
    return {
        hello: hello,
    }
});
// 定义一个有两个依赖的模块。注意复习一下apply的第二个参数
MyModules.define("foo", ["bar", "bar2"], function (bar, bar2) {
    function awesome() {
        console.log(bar.hello("hippo").toUpperCase());
        console.log(bar2.hello("hippo").toUpperCase());
    }
    return {
        awesome: awesome
    };
});
var bar = MyModules.get("bar");
var foo = MyModules.get("foo");

// Let me introduce: hippo
console.log(bar.hello("hippo"));

// LET ME INTRODUCE: HIPPO
// LET ME INTRODUCE: HIPPO AGAIN
foo.awesome();

扩展--内存泄漏问题

为什么会出现内存泄漏:

内部函数对变量的引用一直存在,如果该变量很大,则占用的内存空间一直不释放,造成内存泄漏。

如何避免:

  1. 在不需要用闭包之后及时将引用闭包的变量置为null,使得垃圾回收机制可以将它回收
  2. 在闭包内使用弱引用的数据结构,如WeakSet, WeakMap,同样可以触发垃圾回收机制
  3. 减少对DOM的引用,因为DOM占用的内存空间较大,一旦无法回收很可能造成内存泄漏

总结

当一个函数可以记住并访问所在的词法作用域中的变量时,即使该函数是在其他词法作用域被调用的。这时就形成了闭包。闭包具有封装变量的功能,因此被广泛应用于模块化等场景。使用闭包时需要注意内存泄漏的问题,在不需要时及时清理闭包。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值