JavaScript基础-作用域与闭包

140 阅读3分钟

什么是作用域?

我所理解的作用域主要分为全局作用域和局部作用域:在浏览器当中,全局作用域就是我们常说的window对象里面的变量可在任何地方都能访问;局部作用域主要是函数作用域和块级作用域只能在其内部读取变量外部是读取不到内部的变量的;

作用域中的代码在执行的时候,会创建变量对象的一个作用域链;作为内部函数可以通过作用域链访问外部函数作用域中的一切变量,也可以访问到最外层作用域(window对象)的变量,但是外部函数无法访问内部的任何东西。

var a = 1;
function fun() {
  var b = 2;
  var c = d;
  console.log(a); // 1
  console.log(c); // 报错 因为fun是拿不到fun2作用域的变量
  function fun2() {
    var d = 3
    console.log(a); // 1
    console.log(b); // 2
    console.log(d); // 3 
    // fun2 函数的作用域作为最内层的作用域
    // 可以拿到外层函数fun的作用域变量b以及window对象的全局作用域下的变量a。
  }
  fun2()
};
fun()

全局作用域

在浏览器当中,全局作用域中会有一个全局对象window,我们创建的每个变量都会在window对象的属性中保存下来,创建的每个函数都会作为window对象的方法保持下来。

在全局作用域下的变量是全局变量,我们可以在任何地方都可以访问到全局变量。

var a = 1;
function fun() {
  console.log(a); // 1
};
fun();
console.log(a);// 1
console.log(window.a); // 1

局部作用域

局部作用域一般包括函数作用域和块级作用域。

函数作用域

函数作用域指在这个函数内定义的全部变量都可以在整个函数的作用域当中反复使用(例1)。当然,在这个函数内嵌套的函数下,也可以使用(例2-闭包),但是这样可能会造成一些意想不到的问题,关于闭包带来什么问题以及如何解决我们在闭包当中来解释。

例1

function fun() {
  var a = 1;
  console.log(a);
}
fun(); // 1
console.log(a); // ReferenceError: a is not defined

例2

function fun() {
  var a = 1;
  var b = 2;
  console.log(a); // 1
  return function fun2() {
    console.log(b);  // 2
  }
}
var info = fun();
info ();

块级作用域

我们可能常见的局部作用域是函数作用域,但是其他作用域单元的也是存在的,“并且可以通过使用其他类型的作用域单元甚至可以实现维护起来更加优秀、简洁的代码”。在之前,JavaScript是不支持块级作用域的。

但是ES6带来了let和const关键字,给我们提供了更好的声明变量的方式。

首先,let和const可以用来在{ }创建块级作用域,声明的变量只能在{ } 内可以被拿到(例3),并且let和const不会存在变量提升(例4),还有需要注意的是let声明的量是可以修改的,而const不可以(例5)。

例3

{
 const a = 1;
 let b = 2;
 console.log(a);  // 1 
 console.log(b);  // 2
}
console.log(a);  // ReferenceError: a is not defined
console.log(b); // ReferenceError: b is not defined

例4

console.log(a); // underfined
var  a = 3;

console.log(b); // ReferenceError: Cannot access 'b' before initialization
const b = 3;

console.log(c); // ReferenceError: Cannot access 'c' before initialization
let c = 3;

// 变量提升 我所理解的变量提升 即var关键字声明会被提升到当前作用域或者全局作用域的顶部,
// 所以 打印 a 会出现underfined;

例5

let num = 1;
num = 2;
console.log(num); // 2 
const total = 10;
total = 20; // 运行之后报 TypeError
console.log(total);

有关块级作用域的好处,我们可能对下面的很熟悉

for (var i = 0; i < 10; i++) {
    console.log(i);
}
console.log(i); // 10
// 我们可能只想在for循环里面来定义i 但是 var 关键字会将i绑定到外部的作用域当中。
// 如果i在其他地方也被使用了,这会非常的混乱,甚至可能会导致非常严重的错误。

// 如果我们使用let来声明,相当于用let关键字来劫持for循环的块,i只能在for{ } 内拿到,这样我们可以保证我们的变量不会被
复用到其他的地方,提升我们代码的可维护性。
for (let i = 0; i < 10; i++) {
    console.log(i);
}
console.log(i); // ReferenceError: i is not defined

闭包

什么是闭包?

闭包是指那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的(例6)。

在例6当中fun2 函数应用了外部函数的变量b,在这个内部函数被返回之后,在其他的地方使用,变量b仍然被引用。至于为什么还可能被引用,因为fun2函数的作用域链当中包含这fun函数的作用域。

例6

function fun() {
  var a = 1;
  var b = 2;
  console.log(a); // 1
  return function fun2() {
    var c = b
    console.log(c);  // 2
  }
}
var info = fun();
info ();

闭包也会给我们带来一些问题,因为闭包会保留函数的作用域,比较占用内存。在返回的函数变量b一直被使用,所以fun()的作用域就不会被销毁,作用域链也一直存在,这样的话是非常占用内存的。如果我们在使用完变量之后将变量设置为null,从而解除对变量的使用让垃圾回收程序可以将内存释放掉。

我们常见的闭包例7

例7

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

这样会输出5个5,因为执行setTimeout的时候,会把我们的代码块放到宏任务队列,当for循环执行完毕之后,由于for循环是几乎瞬间执行完毕,我们用var 声明的i 是一个全局变量,所以此时打印的就会是5个5;这样不是我们想要的结果0,1,2,3,4

所以我们需要一个闭包作用域,我们将使用let声明i就会解决这个问题,在每次迭代之后将这个块作用域关闭,从而实现我们想要的结果

例8

for (let i =0; i < 5; i++) {
    setTimeout(function timer() {
        console.log(i);// 0 1 2 3 4
    },i*1000)
}

闭包虽然可以帮助我们实现私有化变量或者方法,但是滥用闭包,会占用大量内存,会让我们的网站性能变得很差,我还是不推荐使用闭包。在es6之后 ,在开发过程中,我们还是更多的使用const来声明,除非我们需要一个将来需要修改的变量。因为用const可以从根本上保证提前发现因为重新赋值导致的错误。

参考书籍

  • 《你不知道的JavaScript》
  • 《JavaScript高级程序设计》

作者: plus