JavaScript:作用域链与闭包

578 阅读5分钟

作用域

什么是作用域?

作用域是指针访问某一变量具有访问权限的代码空间,在JS当中作用域是在函数中体现的,表示可访问某一变量的区域,指代了上下文执行环境;
通俗一点说就是,JS只包含两种作用域:全局作用域局部作用域

ps:有的同学问那方法呢,其实方法也是变量,就是声明的语法不一样而已

function fn(){}

var fn = function(){}

全局作用域

在web应用当中,全局作用域就是我们的window对象,node环境中全局作用域为global对象

var a = '123'
console.log(a) // 123
console.log(window.a) // 123
a = {}
window.a === a // true

局部作用域

js当中只有一种局部作用域,就是函数作用域,在当前函数执行上下文,只能访问当前函数内部的变量嵌套作用域的变量(作用域链稍后说),当前函数的作用域链是由函数被声明的时候确定的,而不是调用的时候

var a = 'window'
function fn1(){
    var a = 'fn1'
    console.log(a) // fn1
}
fn1()

块级作用域(ES6)

块级作用域是ES6新增的特性,主要是利用了const和let这样的关键字在声明变量时,会在当前声明的上下文的{}大括号中,创建一个块级作用域

块级作用域解决了什么问题呢?

假设我们有一个需求,我要让函数每次保存一个循环的值,然后最后再依次打印出来

var callbacks = []
for(var i = 0; i < 10; i++) {
  callbacks.push(function(){
    console.log(i)
  })
}
callbacks.forEach((fn)=>{
  fn()
})
// 输出10次10

结果我们发现,因为i是用var声明的,所以是一个全局变量,当10次循环结束后,全局变量已经变为10了,这跟我们的初衷不同,那么我们改用let来试一下

var callbacks = []
for(let i = 0; i < 10; i++) {
  callbacks.push(function(){
    console.log(i)
  })
}
callbacks.forEach((fn)=>{
  fn()
})
// 0 1 2 3 4 5 6 7 8 9

这次就和我们的预期一致了,这是为什么呢?
首先每一次循环的时候,let都会为当前{}内的上下文创建一个新的块级作用域并且声明了变量i再赋值,根据作用域链的就近原则,i每次会找到离自己最近的作用域当中声明的变量i打印 使用var和let的区别:

image.png

变量提升(补充)

这其实是个很大的话题,以后有机会讲可以参考下这篇文章js变量提升

作用域链

js引擎会根据作用域之间的嵌套关系,遵从就近原则,首先在当前作用域当中查找变量的引用,如果没有找到,就像一条锁链逐级向上查询所使用的变量

var a = 'window';
function fn1() {
  console.log('fn1:', a); // a -> window
  function fn2() {
    var a = 'fn2a'
    console.log('fn2:', a); // a -> fn2a
    function fn3() {
      console.log('fn3:', a); // a -> fn2a
    }
    fn3();
  }
  fn2();
}
fn1();
// 执行结果:
// fn1: window
// fn2: fn2a
// fn3: fn2a

作用域链:

image.png

闭包

什么是闭包?

闭包让你可以在一个内层函数中访问到其外层函数的作用域

function init() {
    var name = "window"; // name 是一个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,一个闭包
        alert(name); // 使用了父函数中声明的变量
    }
    displayName();
}
init();

init() 创建了一个局部变量 name 和一个名为 displayName() 的函数。displayName() 是定义在 init() 里的内部函数,并且仅在 init() 函数体内可用。请注意,displayName() 没有自己的局部变量。然而,因为作用域链嵌套的原因它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 init() 中声明的变量 name 。


再看一个例子:

function makeFunc() {
    var name = "window";
    function displayName() {
        alert(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc();

运行这段代码的效果和之前 init() 函数的示例完全一样。其中不同的地方(也是有意思的地方)在于内部函数 displayName() 在执行前,从外部函数返回。

第一眼看上去,也许不能直观地看出这段代码能够正常运行。在一些编程语言中,一个函数中的局部变量仅存在于此函数的执行期间。一旦 makeFunc() 执行完毕,你可能会认为 name 变量将不能再被访问。然而,因为代码仍按预期运行,所以在 JavaScript 中情况显然与此不同。

原因在于,JavaScript中的函数会形成了闭包。 闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用。displayName 的实例维持了一个对它的词法环境(变量 name 存在于其中)的引用。因此,当 myFunc 被调用时,变量 name 仍然可用,其值 window 就被传递到alert中。

闭包的原理

总结一下,由于内部函数引用了外部函数的变量,内部函数又被return出后被全局变量myFunc引用形成了一个引用链,导致js垃圾回收机制无法回收外部函数当中的变量name,于是形成了一个外部函数无法被其他人访问,但可以被内部函数访问的封闭环境,即为闭包

实用的闭包

闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。所以总而言之当你需要一个函数访问一个属于他私有的作用域时,就可以使用闭包

闭包的缺点

由于闭包会使函数中的引用的外部变量无法被垃圾回收机制回收,外部变量仍然占据内存空间,内存消耗很大,所以不能滥用闭包,解决办法是,退出函数之前,将不使用的局部变量删除。