Skip to content

作用域 & 闭包

作用域

作用域(scope)是指在程序中能够访问变量、函数的区域。JavaScript 中有全局作用域、模块作用域、函数作用域3种。

全局作用域:在整个程序中都可以访问的变量,它们在程序开始时就被创建,在程序结束时才被销毁。

模块作用域:模块模式中运行代码的作用域。

函数作用域:在一个函数内部声明的变量,只能在函数内部访问,而在函数外面是不能访问的。当函数执行完毕后,函数内部的变量会被销毁。

js
function exampleFunction() {
  const x = "declared inside function"; // x 只能在 exampleFunction 函数中使用
  console.log("Inside function");
  console.log(x);
}
console.log(x);  // 报错

块级作用域:用一对花括号(一个代码块)创建出来的作用域。使用 let和const声明的变量是有块级作用域的。块级作用域只对 let 和 const 声明有效,对 var 声明无效。

js
{
  var x = 1;
}
console.log(x); // 1
js
{
  const x = 1;
}
console.log(x); // ReferenceError: x is not defined

词法作用域

词法作用域(lexical scope)是指在编写程序时,变量和函数的作用域是通过它们在代码中声明的位置来确定的。

这意味着,函数可以访问在其外部定义的变量和函数,但在其内部定义的变量和函数不能被外部访问。这是因为 JavaScript 引擎识别变量和函数的作用域是根据它们在代码中的位置来决定的,而不是根据它们被调用的位置。

var 变量提升(hoisting)

在 JavaScript 中,var 关键字会进行预解析(hoisting),这意味着在代码执行之前,所有使用var定义的变量都会被提升(hoist)到当前作用域的顶部,而变量的赋值操作则会留在原处。(⚠️ let 声明的变量可以形成一个块级作用域,不会有提升问题!)

看下面这个例子:

js
function foo() {
  console.log(a); // undefined
  a = 100;
  var a;
  console.log(a); // 100
}

foo();

// 结果: undefined 100

逐行解析:

先看 foo 函数整体内容,发现有一行 var a,它会预解析,这一样会提升到当前作用域顶部,即先定义了 a 变量,但是没有赋值。

所以第一行 console.log(a) 执行,输出 undefined;

a = 100 这一行相当于给变量 a 赋值,foo 函数作用域内,a=100

最后一行 console.log(a);访问的是 foo 函数作用域内的 a 变量,它已经被赋值为 100,因此输出 100;

变量同名时考虑作用域

全局作用域局部作用域的变量同名时:全局变量是不会作用于同名的局部变量的作用域的,也就是说,同名的局部变量所处的作用域访问不到同名的全局变量。 先看一个例子:

js
var a = 10;
var obj = {
  a: 99,
  f: foo,
};

function foo() {
  console.log(a); // undefined
  a = 100;
  console.log(this.a); // 99
  var a;
  console.log(a); // 100
}

obj.f();

输出:undefined 99 100

逐行解析:

先看 foo 函数整体内容,发现有一行 var a,它会预解析,这一样会提升到当前作用域顶部,即先定义了 a 变量,但是没有赋值。

所以第一行 console.log(a) 执行,输出 undefined;

a = 100 这一行相当于给变量 a 赋值,foo 函数作用域内,a=100

console.log(this.a) 这一行考察 this 的指向问题,foo 实际是由 obj 调用,因此 this 指向调用者 obj,那么 this.a == obj.a == 99,因此输出 99;

最后一行 console.log(a);访问的是 foo 函数作用域内的 a 变量,它已经被赋值为 100,因此输出 100;

全局变量与局部变量同名情况对比

js
var a = 10;
function test1() {
  console.log(a); // undefined 输出尚未赋值的局部变量
  a = 100; // 局部变量 a 赋值
  console.log(this.a); // 10 this.a = window.a
  var a; //提升到 test1 函数首行,定义局部变量 a
  console.log(a); // 100 访问局部变量 a
}

test1();

var a = 10;
function test2() {
  console.log(a); // 10 输出全局 a
  a = 100; // 修改全局 a
  console.log(this.a); // 100 this.a = window.a
}

test2();

美团面试题:预解析、作用域

js
var a = 10;
function f1() {
  var b = 2 * a;
  var a = 20;
  var c = a + 1;

  console.log(b); // NaN
  console.log(c); // 21
}

解析:

  • 代码块预解析
js
var a; // 定义全局变量a
function fn1() {} // 整个函数,并未调用
  • f1 函数 预解析 (全局变量和局部变量是否同名)
js
var b;
var a; // 和全局变量a同名,因此在f1函数里只能访问到局部变量a
var c;
  • f1 函数 逐行解析
js
b = 2 * a; // 此时a定义了但并未赋值,a=undefined,2 * undefined = NaN
a = 20; // 开始给a赋值
c = a + 1; // 20 + 1

console.log(b); // NaN
console.log(c); // 21

闭包

闭包(closure)是指函数能够访问其词法作用域之外的变量,即便在函数被调用后仍然可以访问。

  • 特点:既能持久保存局部变量(函数调用后依然能够访问到),又能不造成全局污染;
  • 作用:闭包可以用来创建一些类似于私有变量和方法的功能,以及实现一些高阶函数,如柯里化等;
  • 缺点:过多的闭包使用可能会导致性能问题和内存泄漏的风险,应该谨慎使用。特别是在循环语句中,不能滥用闭包。

先看一个计数器例子:

js
function f1() {
  var a = 0;

  return function () {
    a++;
    console.log(a);
  };
}

var fn = f1(); // 将 f1()执行的结果(函数)赋值给了 fn,fn 就对 a 形成了持续引用,造成 a 无法销毁。

fn(); //1
fn(); //2

f1 返回的结果就是闭包,它是一个函数,并且这个函数内部对父函数的局部变量存在引用,就形成了闭包的包含关系。闭包函数多次调用时,它内部应用的父函数的局部变量不会被销毁。

js
var fn1 = f1();
fn1(); //1
fn1(); //2

var fn2 = f1();
fn2(); //1
fn2(); //2

上面的 fn2 不会影响 fn1 内部引用的局部变量的值。比如 vue2data 的写法

js
new Vue({
    el:'app',
    data:{
    	a:1
    }
})

data(){
    return {
    	a:1
    }
}

以下写法不能形成闭包,未形成引用捆绑关系。

js
function f1() {
  var a = 0;

  return function () {
    a++;
    console.log(a);
  };
}

f1()(); // 1
f1()(); // 1

以下是一个闭包滥用的例子:

js
for (var i = 0; i < 10; i++) {
  setTimeout(function () {
    console.log(i); // 输出的都是 10
  }, 1000);
}

如何解决闭包在循环中的问题?

  • let 声明一个块级作用域,每次循环都创建一个新的 i,避免再在闭包中共享同一个变量;
js
for (let i = 0; i < 10; i++) {
  setTimeout(function () {
    console.log(i); // 输出 0 到 9
  }, 1000);
}
  • 使用一个立即执行函数表达式(IIFE)来创建一个块级作用域
js
for (var i = 0; i < 10; i++) {
  (function (j) {
    setTimeout(function () {
      console.log(j); // 输出 0 到 9
    }, 1000);
  })(i); // 这个函数会立即执行并创建一个新的作用域,把变量 i 的值传递给参数 j,从而避免在闭包中共享同一个变量
}

Released under the MIT License.