作用域和闭包

本文详细介绍了JavaScript中的作用域,包括词法作用域、函数作用域与块级作用域,强调了变量提升和闭包的概念。闭包是函数能够记住并访问其所在词法作用域,即使在该作用域之外执行,它依然持有对原始作用域的引用。通过闭包,可以解决变量共享和模块化的实现问题。
摘要由CSDN通过智能技术生成

目录

什么是作用域?

作用域是一套规则,用于确定在何处以及如何查找变量。——《你不知道的JavaScript上卷》

当一个块或者函数嵌套在另一个块或者函数中时,就发生了作用域嵌套

查找变量的规则是:从当前作用域开始查找变量,如果找不到,继续向上级查找,直到全局作用域停止。由当前作用域到全局作用域中发生的嵌套就形成了作用域链。

JS的词法作用域

作用域分词法作用域和动态作用域

JS采用的就是词法作用域!

词法作用域意味着作用域由写代码时函数所声明的位置来决定的,也就说,我们不用管函数在哪里调用或者怎样调用,它的作用域在声明的时候就决定了。

函数作用域和块作用域

JS中的作用域单元包含函数作用域和块级作用域。

函数作用域

将一段代码用一个函数包裹起来,就形成了一个新的作用域———函数作用域

函数作用域隐藏内部实现,规避了命名冲突,也是ES6之前模块管理的一种解决方案。

理想情况下,我们希望的是,函数不需要函数名(或者至少函数名可以不污染所在作用域),并且能够自动运行。

答案是:立即执行函数表达式(IIFE)

(function foo(){
    
})()

// 或者

(function foo(){
    
}())

函数表达式中,foo被绑定在自身函数中,而不是所在作用域中,foo变量名不会污染外部作用域。

块级作用域

ES6之前js不支持块级作用域,但是,ES3规范中规定try/catch的catch分句会创建一个块级作用域,声明的变量仅在catch内部有效

ES6中的关键字let和const,使用这两个关键字声明的变量,会被绑定在当前块级({…})作用域中

变量提升

提升:所有声明(变量和函数)都会被移动到各自作用域的最顶端

如:

bar() // 1
console.log(a) // undefined 而不是报错

var a = 2
var bar;

function bar(){
    console.log(1)
}

bar = function(){
    console.log(2)
}

var a=2 可以分为两步var aa=2

当重复声明的代码,函数会首先被提升,这里的var bar会被忽略掉,被function bar覆盖。

闭包

当函数可以记住并访问所在词法作用域,即使函数是在当前作用域之外执行,这时就产生了闭包。 ———《你不知道的JavaScript上卷》

funciton foo(){
    var a = 2
    function bar(){
        console.log(a)
    }
    return bar;
}

var baz = foo()

bar(); // 2 这里就是闭包的效果!

前面我们了解了作用域,知道函数bar只能在其所在的词法作用域(foo)中被调用,在foo函数之外调用会报错

在执行var baz = foo()后,原本根据垃圾回收机制会释放不再使用的内存空间,拜bar()所声明的位置所赐,无论通过何种方式将内部函数传递到所在词法作用域以外(这里我们通过return将函数bar传递出去了), 它依然持有对原始作用域的引用,无论在何处执行这个函数都会使用闭包。

由此,闭包需要必备的两个条件是:

  1. 函数能够记住并访问所在词法作用域,将函数当做值进行传递(比如return、赋值到外部作用域等)
  2. 函数必须在它本身的词法作用域之外执行
闭包无处不在

看下面一个例子:

    for(var i=0; i<5; i++){
          setTimeout(function(){
            console.log(i)
          }, 1000)
      }
      // 想要的结果为:0 1 2 3 4 
      // 控制台输出 5个5

这是由于每次循环延时函数都被封闭在共享的全局作用域中,实际上共享一个i值,setTimeout会在循环结束后才执行,而此时i的值为5。

解决方案:

  1. 通过一个立即执行函数形成的闭包
    for (var i = 0; i < 5; i++) {
        (function (i) {
            var j = i
            setTimeout(function () {
                console.log(j)
            }, 1000)
        })(i)
    }
    
    // 0 1 2 3 4

通过IIFE,每次循环都会创建各自的词法作用域并捕获一个i的副本,setTimeout通过引用了所在词法作用域中的变量j形成了一个闭包。循环结束后,执行setTimeout都会根据自己的作用域找到i的值。

  1. 使用块作用域
     for (let i = 0; i < 5; i++) {
        setTimeout(function () {
            console.log(i)
        }, 1000)
    }
    // 0 1 2 3 4

let 声明有个特殊的行为:每次迭代都会重新声明,将上一次迭代结束的值用来初始化变量。

以上两种解决方案,本质上就是将一个块转换成一个可以被关闭的作用域。

用闭包来实现模块

模块模式需要具备的两个必要条件:

  1. 必须要有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建新的模块实例)
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有状态。

一个简单的模块例子:

    var foo = (function moudle() {
            var a = 1
            var b = [1, 2, 3];
            function foo() {
                console.log("foo");
            }
            function bar() {
                console.log(b.join(" ! "));
            }
            return {
                foo,
                bar
            };
    })();
        foo.foo(); // foo
        foo.bar(); // 1 ! 2 ! 3

模块有两个主要特征:

  1. 为创建内部作用域调用了一个包装函数
  2. 包装函数的返回值必须至少包括一个对内部函数的引用,这样就创建涵盖整个包装函数内部作用域的闭包

现有的模块工具库,如define、require等都是将这种模块定义封装。
ES6将文件当作独立的模块来处理。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值