JavaScript作用域与执行上下文

本文最后更新于 2025年10月4日 下午

作用域和执行上下文

作用域

参考:https://github.com/mqyqingfeng/Blog/issues/3

含义

作用域是指程序源代码中定义变量的区域。作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。与多数现代编程语言一样,JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。可以认为 JavaScript 作用域有三种:全局作用域、函数作用域、块级作用域

意义

词法作用域意味着函数作用域在函数定义的时候就确定了,函数作用域基于函数创建的位置。与词法作用域相对的是动态作用域,动态作用域是在被函数调用的时候被确定

执行上下文

参考:https://github.com/mqyqingfeng/Blog/issues/4

含义

JavaScript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”,但 JavaScript 引擎遇到一段怎样的代码时才会做“准备工作”?

JavaScript 的可执行代码(executable code)分三种:全局代码、函数代码、eval 代码。当执行到一个函数时就会进行准备工作,这里的“准备工作”就是执行上下文(execution context)。

执行上下文栈

为了管理创建的执行上下文,JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。

当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候,首先就会向执行上下文栈压入一个全局执行上下文,用 globalContext 表示它,且只有当整个应用程序结束的时候执行上下文栈才会被清空。所以程序结束之前,执行上下文栈最底部永远有 globalContext。

当执行一个函数的时候,就会创建一个执行上下文,并压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。

变量对象

参考:https://github.com/mqyqingfeng/Blog/issues/5

当 JavaScript 代码执行一段可执行代码(executable code)时,都会创建对应的执行上下文。对于每个上下文都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明,不同的执行上下文中的变量对象稍有不同。

全局上下文

ES6 添加了 letconst 声明,使用 var 声明的变量会成为全局对象的属性,但是使用 letconst 声明的变量不会成为全局对象的属性,而会成为变量对象的属性,与全局对象“平级”。因此,使用 ES6 的标准,全局上下文的变量对象就是全局对象;使用 ES6 之前的标准,全局上下文的变量对象包含全局对象和使用 letconst 声明的变量,全局对象是全局上下文的变量对象的子集

但是在初始化时,全局上下文的变量对象只有(等于)全局对象。

全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象可以访问所有其他预定义的对象、函数和属性。

函数上下文

在函数上下文中,我们用活动对象(activation object)来表示变量对象。

活动对象(AO)和变量对象(VO)是一个东西,只是变量对象是规范上的或者说是引擎上实现的,不可在 JavaScript 环境中访问。只有到当进入函数执行上下文中,这个执行上下文的变量对象才会被激活,所以叫 activation object。被激活的变量对象就是活动对象,只有活动对象上的各种属性才能被访问。

活动对象是在进入函数上下文的时刻被创建的,它通过函数的 arguments 属性初始化,arguments 属性值是 Arguments 对象。在初始化时,函数上下文的变量对象初始化只包括 Arguments 对象。

执行过程

执行上下文的代码会分成两个阶段进行处理:分析(进入执行上下文)和执行(代码执行)。

全局上下文

全局上下文当进入执行上下文时,这时候还没有执行代码,变量对象包括两方面:

  • 函数声明
    • 由名称和对应值(函数对象)组成一个变量对象的属性被创建
    • 如果变量对象当中已经存在相同名称的属性,则完全替换这个属性
  • 变量声明
    • 使用var声明的变量:由名称和 undefined 值组成一个变量对象的属性被创建
      (无论声明时是否已经初始化变量,此时值都为 undefined,等执行时再修改值)
      (使用letconst声明的变量都属于未声明的变量,等执行时再添加到变量对象)
    • 如果变量名称跟已经声明的函数相同,则变量声明不会干扰已经存在的属性
    • 如果变量名称跟已经声明的变量相同,则变量声明将会覆盖已经存在的属性
1
2
3
4
5
6
7
8
9
10
11
console.log(a);
// ƒ a() {
// console.log('a');
// }
console.log(b);
// Uncaught ReferenceError: b is not defined

function a() {
console.log("a");
}
let b = 0;
1
2
3
4
5
6
7
8
9
10
11
console.log(a);
// ƒ a() {
// console.log('a');
// }
console.log(b);
// undefined

function a() {
console.log("a");
}
var b = 0;

函数上下文

函数上下文当进入执行上下文时,这时候还没有执行代码,变量对象包括三方面:

  • 函数的所有形参
    • 由名称和对应值组成的一个变量对象的属性被创建
      (有实参,属性值为对应值;没有实参,属性值为 undefined)
  • 函数声明
    • 由名称和对应值(函数对象)组成一个变量对象的属性被创建
    • 如果变量对象当中已经存在相同名称的属性,则完全替换这个属性
  • 变量声明
    • 使用var声明的变量:由名称和 undefined 值组成一个变量对象的属性被创建
      (无论声明时是否已经初始化变量,此时值都为 undefined,等执行时再修改值)
      (使用letconst声明的变量都处于暂时性死区,等执行时再添加到变量对象)
    • 如果变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的属性
    • 如果变量名称跟已经声明的变量相同,则变量声明将会覆盖已经存在的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
console.log(a);
console.log(b);
function a() {
console.log("a");
}
let b = 1;
}

foo();
// ƒ a() {
// console.log('a');
// }
// Uncaught ReferenceError: Cannot access 'b' before initialization
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
console.log(a);
console.log(b);
function a() {
console.log("a");
}
var b = 1;
}

foo();
// ƒ a() {
// console.log('a');
// }
// undefined

代码执行

在代码执行阶段,JavaScript 引擎会按顺序执行代码,来修改变量对象的值。

总结

  • 全局上下文的变量对象初始化只包括全局对象
  • 函数上下文的变量对象初始化只包括 Arguments 对象
  • 进入执行上下文,会给变量对象添加形参、函数声明、变量声明等初始的属性值
  • 在代码执行阶段,修改变量对象的属性值

作用域链

参考:https://github.com/mqyqingfeng/Blog/issues/6

当 JavaScript 代码执行一段可执行代码(executable code)时,都会创建对应的执行上下文。对于每个上下文都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

当查找变量的时候,会先从当前块作用域的变量中查找,若没有找到,就会从父级块作用域的变量中查找,一直找到最外层的作用域。这样,由多个作用域的变量构成的链表就叫做作用域链。

函数创建

函数作用域在函数定义的时候就决定了。

这是因为函数有一个内部属性 [[scope]],创建函数时,它就会保存所有父变量对象到其中,可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链。

函数激活

当函数激活时,进入函数上下文,创建变量对象/活动对象后,就会将活动对象添加到作用链的前端。此时,执行上下文的作用域链命名为 Scope:Scope = [AO].concat([[Scope]])。至此,作用域链创建完毕。

总结

以下面的例子为例,来总结一下函数执行上下文中作用域链和变量对象的创建过程。

1
2
3
4
5
6
var scope = "global scope";
function checkscope() {
var scope2 = "local scope";
return scope2;
}
checkscope();
  1. checkscope 函数被创建,保存作用域链到内部属性 [[scope]]。
1
checkscope.[[scope]] = [ globalContext.VO ];
  1. 创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈。
1
ECStack = [globalContext, checkscopeContext];
  1. 不立刻执行 checkscope 函数,开始做准备工作。
  • 第一步:复制函数 [[scope]] 属性创建作用域链。
1
2
3
checkscopeContext = {
Scope: checkscope.[[scope]]
}
  • 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明。
1
2
3
4
5
6
7
8
9
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
  • 第三步:将活动对象压入 checkscope 作用域链顶端。
1
2
3
4
5
6
7
8
9
checkscopeContext = {
AO: {
arguments: {
length: 0,
},
scope2: undefined,
},
Scope: [AO, [[Scope]]],
};
  1. 准备工作做完,开始执行函数,随着函数的执行,修改活动对象的属性值。
1
2
3
4
5
6
7
8
9
checkscopeContext = {
AO: {
arguments: {
length: 0,
},
scope2: "local scope",
},
Scope: [AO, [[Scope]]],
};
  1. 函数执行完毕后,函数上下文从执行上下文栈中弹出。
1
ECStack = [globalContext];

this

参考:

当 JavaScript 代码执行一段可执行代码(executable code)时,都会创建对应的执行上下文。对于每个上下文都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

在全局上下文中,关键字 this 指向全局对象,而不是全局上下文的变量对象

在标准函数的上下文中,关键字 this 只在函数调用阶段确定,也就是执行上下文创建的阶段进行赋值然后保存下来。这个特性也导致了 this 的多变性:函数在不同的调用方式下都可能导致 this 值不同。

在箭头函数的上下文中,关键字 this 在定义时就被决定,是静态的。

标准函数

在标准函数中,this 引用的是把标准函数当成方法调用的对象,可分为 4 种情况。

直接被调用

当独立调用函数的时候,严格模式下 this 会指向 undefined,非严格模式自动转为指向全局对象。不管是全局函数被直接调用还是父函数里的子函数被父函数直接调用,都指向全局对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = 1;
var obj = {
a: 2,
b: function () {
console.log(this.a);
},
c: function () {
function fun() {
return this.a;
}
console.log(fun());
},
};

obj.b(); //
obj.c(); //
1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1;
var obj = {
a: 2,
};
function b() {
var a = 3;
function fun() {
console.log(this.a);
}
fun();
}

b(); //

作为对象的方法被调用

当函数作为对象的方法被调用时,这时 this 指向调用它的对象。

当对象在全局代码中声明的时候,对象内部属性中的 this 指向全局对象;当对象在一个函数中声明的时候,严格模式下 this 会指向 undefined,非严格模式自动转为指向全局对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var a = 1000;
var obj = {
a: 1,
// 块级有作用域,没有上下文,这里的this指向外部的全局上下文
b: this.a + 1,
};
function fun() {
var obj = {
a: 1,
// 块级有作用域,没有上下文,这里的this就是函数上下文的this,指向调用的全局上下文
c: this.a + 2,
};
return obj.c;
}

console.log(fun()); //
console.log(obj.b); //
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var a = 1;
var obj = {
a: 2,
b: function () {
console.log(this.a);
},
};
var t = obj.b;

obj.b(); //
t(); //

// obj 对象的 b 属性存储的是对该匿名函数的一个引用,可以理解为一个指针
// 当赋值给 t 时并没有单独开辟内存空间存储函数,而是让 t 存储一个指针,该指针指向这个函数
// 相当于执行了下面的伪代码

var a = 1;
function fun() {
return this.a;
} // 此函数存储在堆中
var obj = {
a: 2,
b: fun, // b 指向 fun 函数
};
var t = fun; // 此时的 t 就是一个指向 fun 函数的指针,调用 t,相当于直接调用 fun

console.log(t()); //
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = 0;
function fun() {
console.log(this.a);
}
let obj2 = {
a: 2,
fun: fun,
};
let obj1 = {
a: 1,
fun: obj2,
};

obj1.fun.fun(); //

使用 call 或 apply 被调用

当函数使用 call 或 apply 被调用时,由其传入的参数作为 this 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 0;
function fun() {
console.log(this.a);
}
function b() {
let a = 7;
}
let c = {
a: 9,
};

fun.call(b); //
fun.call(c); //

作为构造函数被调用

当函数作为构造函数被调用时,那么其中的 this 就代表它即将 new 出来的对象。

箭头函数

箭头函数会捕获其所在上下文的 this 值作为自己的 this 值,其 this 由其所在上下文的 this 所决定。对箭头函数使用 applycall 方法只是传入参数,改变不了 this。

1
2
3
4
5
6
var a = 1;
var obj = { a: 2 };
var fun = () => console.log(this.a);

fun(); //
fun.call(obj); //
1
2
3
4
5
6
7
8
9
var a = 1;
var obj = {
a: 2,
b: () => {
console.log(this.a);
},
};

obj.b(); //
1
2
3
4
5
6
7
8
9
10
var a = 1;
var obj = { a: 2 };
function fun() {
var a = 3;
let f = () => console.log(this.a);
f();
}

fun(); //
fun.call(obj); //
1
2
3
4
5
6
7
8
9
10
11
12
var a = 1;
var obj = { a: 2 };
function fun() {
var a = 3;
function f() {
console.log(this.a);
}
f();
}

fun(); //
fun.call(obj); //

总结

  • 函数的 color 变量值与 其所在的上下文的作用域链 有关
  • 箭头函数的 this.color 值与 其所在的上下文的 this 有关
  • 标准函数的 this.color 值与 把其当成方法调用的对象 有关

注意:全局上下文的变量对象包含着全局对象和使用 letconst 声明的变量和对象。

练习:

  • 1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var color = "red"; // var 声明的变量是函数作用域
const o = {
color: "blue",
a: function () {
console.log(color); // o 只是全局上下文的一个变量,不是块级作用域
console.log(this.color); // 函数作为对象的方法被调用,this 指向调用它的对象
},
b: () => {
console.log(color); // o 只是全局上下文的一个变量,不是块级作用域
console.log(this.color); // 此箭头函数被定义在全局上下文
},
};

o.a(); //
o.b(); //
  • 2.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let color = "red"; // let 声明不会成为全局变量的属性
const o = {
color: "blue",
a: function () {
console.log(color); // o 只是全局上下文的一个变量,不是块级作用域
console.log(this.color); // 函数作为对象的方法被调用,this 指向调用它的对象
},
b: () => {
console.log(color); // o 只是全局上下文的一个变量,不是块级作用域
console.log(this.color); // 全局变量没有 color 属性
},
};

o.a(); //
o.b(); //
  • 3.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 可略过
var color = "red"; // var 声明的变量是函数作用域
{
let color = "blue"; // let 声明不会成为全局变量的属性
function a() {
console.log(color);
console.log(this.color);
}
let b = () => {
console.log(color);
console.log(this.color);
};

a(); //
b(); //
}
  • 4.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 可略过
var color = "red"; // var 声明的变量是函数作用域
{
var color = "blue"; // var 声明的变量是函数作用域,会覆盖
function a() {
console.log(color);
console.log(this.color);
}
let b = () => {
console.log(color);
console.log(this.color);
};

a(); //
b(); //
}
  • 5.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 可略过
var color = "red"; // var 声明的变量是函数作用域
{
let color = "blue";
function a() {
console.log(color);
console.log(this.color);
}
let b = () => {
console.log(color);
console.log(this.color);
};

a(); //
b(); //
}
  • 6.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 可略过
var color = "red"; // var 声明的变量是函数作用域
{
let color = "blue";
function a() {
let color = "black";
console.log(color);
console.log(this.color);
}
let b = () => {
let color = "black";
console.log(color);
console.log(this.color);
};

a(); //
b(); //
}
  • 7.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 可略过
let color = "red"; // let 声明不会成为全局变量的属性
{
let color = "blue";
function a() {
let color = "black";
console.log(color);
console.log(this.color); // 全局变量没有 color 属性
}
let b = () => {
let color = "black";
console.log(color);
console.log(this.color);
};

a(); //
b(); //
}
  • 8.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var color = "red"; // var 声明的变量是函数作用域
const o = {
color: "blue",
};
function a() {
console.log(color); // o 只是全局上下文的一个变量,不是块级作用域
console.log(this.color); // 函数作为对象的方法被调用,this 指向调用它的对象
}
const b = () => {
console.log(color); // o 只是全局上下文的一个变量,不是块级作用域
console.log(this.color);
};
o.a = a;
o.b = b;

a(); //
b(); //
o.a(); //
o.b(); //
  • 9.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var aaa = "123";
(function () {
console.log(aaa); //
})();

// ------------------------

var aaa = "123";
(function () {
console.log(aaa); //
var aaa = "345";
})();

// ------------------------

var aaa = "123";
(function () {
console.log(aaa); //
let aaa = "345";
})();
  • 10.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function test() {
return {
m: function () {
console.log("m---", this);
},
n: () => {
console.log("n---", this);
},
};
}
const obj1 = { obj1: "obj1" };
const obj2 = test.call(obj1);

obj2.m(); //
obj2.n(); //

闭包

参考:https://github.com/mqyqingfeng/Blog/issues/9

定义

MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

所以,可以得出:闭包 = 函数 + 函数能够访问的自由变量。

理论角度

所有的 JavaScript 函数都是闭包。因为在创建函数的时候,会将外层上下文的数据保存起来。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候就是在用最外层的作用域。

实践角度

从实践角度,符合以下条件的函数才算是闭包:

  • 创建闭包的函数已经被销毁,但闭包仍然存在(比如,内部函数从父函数中返回)
  • 闭包的代码中引用了自由变量

用途

优点

  • 私有化变量,可以做到隔离作用域,保证数据不被污染
  • 持久化变量,闭包及其变量长期在内存中,可长时间访问
  • 可以创建高级函数,柯里化等

缺点

  • 内存消耗很大,不能滥用闭包,容易造成内存泄漏
  • 性能开销大
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let count = (function () {
let num = 0;
function aaa() {
return ++num;
}
aaa.renew = function () {
num = 0;
};
return aaa;
})();
count(); // 1
count(); // 2
count(); // 3
count.renew();
count(); // 1

场景

本质就是:闭包让函数记住它定义时的环境,从而能长期持有某些状态。

私有变量

实现数据封装,创建私有属性和方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 计数器模块
function createCounter() {
let count = 0; // 私有变量

return {
increment: function () {
count++;
return count;
},
decrement: function () {
count--;
return count;
},
getValue: function () {
return count;
},
};
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getValue()); // 2
console.log(counter.count); // undefined - 无法直接访问私有变量

React Hooks 的实现原理:

1
2
3
4
5
6
7
8
9
10
11
// useState 的简化实现就是闭包
function useState(initialValue) {
let state = initialValue;

const setState = (newValue) => {
state = newValue;
// 触发重新渲染...
};

return [state, setState];
}

模块化(IIFE)

在 ES6 模块之前,闭包是模拟模块的主要方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Module = (function () {
const privateVar = "secret";
function privateFn() {
console.log(privateVar);
}

return {
publicFn() {
privateFn();
},
};
})();

Module.publicFn(); // "secret"

函数作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 柯里化函数 - 创建特定功能的函数
function createMultiplier(multiplier) {
return function (number) {
return number * multiplier;
};
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

// 更实际的例子:API请求函数
function createAPIRequest(baseURL) {
return function (endpoint) {
return function (params) {
return fetch(`${baseURL}${endpoint}`, {
method: "POST",
body: JSON.stringify(params),
});
};
};
}

const api = createAPIRequest("https://api.example.com");
const login = api("/login");
const getUser = api("/user");

// 使用
login({ username: "john", password: "123" });

循环中的异步操作

解决经典循环变量捕获问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 错误的方式 - 所有回调都引用同一个 i
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 输出 3, 3, 3
}, 100);
}

// 正确的方式 - 使用闭包创建独立作用域
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(function () {
console.log(j); // 输出 0, 1, 2
}, 100);
})(i);
}

// 现代方式 - 使用 let (本质也是闭包)
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 输出 0, 1, 2
}, 100);
}

数据缓存与记忆化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 记忆化函数 - 缓存计算结果
function memoize(fn) {
const cache = new Map();

return function (...args) {
const key = JSON.stringify(args);

if (cache.has(key)) {
console.log("从缓存获取");
return cache.get(key);
}

console.log("重新计算");
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}

// 昂贵的计算函数
function expensiveCalculation(n) {
console.log("执行复杂计算...");
return n * n;
}

const memoizedCalc = memoize(expensiveCalculation);

console.log(memoizedCalc(5)); // 执行复杂计算... 25
console.log(memoizedCalc(5)); // 从缓存获取 25 (避免重复计算)

事件处理与回调函数

在事件处理中保持状态:防抖(记录状态)、节流(记录时间)


JavaScript作用域与执行上下文
https://xuekeven.github.io/2021/08/19/JavaScript作用域与执行上下文/
作者
Keven
发布于
2021年8月19日
更新于
2025年10月4日
许可协议