本文深入解析 JavaScript 中的作用域、作用域链及执行环境栈机制。详细阐述函数级作用域特性、标识符解析规则、执行上下文的生命周期管理,以及 this 对象的判定逻辑。通过代码示例说明变量查找顺序、遮蔽效应及性能优化策略,帮助开发者理清概念混淆。
引言与核心概念梳理
前言:这是笔者学习之后自己的理解与整理。如果有错误或者疑问的地方,请大家指正,我会持续更新!
在 JavaScript 中,作用域、作用域链、执行环境、执行环境栈以及 this 的概念至关重要,笔者曾经常将这些概念混淆,因此在此进行梳理和澄清。
- 局部作用域指的是函数内部的区域,而全局作用域则对应于 window 对象。
- 作用域链由函数声明时的位置决定。在解析标识符时,引擎会先查找当前作用域,若未找到则向外层查找,直至全局作用域。这一查找顺序是固定的,与函数实际调用的位置无关。
- 执行环境是指函数可访问的数据和变量的集合,即该函数作用域链上包含的所有数据和变量。
- 执行环境栈依据代码执行顺序,将各执行环境以栈的形式逐层访问。当某个执行环境使用完毕后会被移除并丢弃。需要注意的是,如果当前执行环境(包含当前作用域链的数据和变量)中找不到变量,则直接判定为找不到,不会回溯到之前的执行环境查找,这与作用域链的机制不同。
- this 是一个对象,其值取决于执行者是谁,即由谁执行就指向谁。关于 this 的概念理解尚不完全透彻,此处描述较为宽泛,后续可能会进行修正。
JavaScript 作用域机制
作用域
JavaScript 没有块级作用域的概念,仅具备函数级作用域:变量在声明它们的函数体及其子函数内可见。
作用域定义了变量和函数的可访问范围,控制着它们的可见性与生命周期。在 JavaScript 中,变量的作用域主要分为全局作用域和局部作用域。
未在函数内部声明的变量,或声明时未使用 var 关键字的变量,均为全局变量,拥有全局作用域。
<script type="text/javascript">
function test1(){
a = 1;//全局变量,只有在当前函数运行时,才有效
}
test1();
console.log(a);//1 注意test1函数必须运行,不然找不到a
</script>
全局变量可以作为 window 对象的属性来访问,两者在本质上是相同的。
<script type="text/javascript">
var b = 1;//全局变量
console.log(b === window.b);//true 全局变量可以当做window对象的属性用,他们是一样的;
</script>
window 对象的所有属性都拥有全局作用域,可以在代码的任何位置进行访问。
在函数内部声明的变量称为局部变量,仅能在函数体内使用。即使函数的参数未使用 var 声明,它们仍然是局部变量。
<script type="text/javascript">
var c = 1;//全局变量
// console.log(d);//ReferenceError: d is not defined 引用错误,当前作用域就是最外层作用域,依然找不到d
function test2(d){
console.log(c);//1 全局变量,哪都可以访问;(先找当前作用域,找不到,就向外层作用域找,直到window最外层,找到了)
console.log(d);//3 形参是局部变量,只有当前作用域下可以访问
}
test2(3);
</script>
作用域链与变量查找
作用域链
作用域链取决于函数被声明时的位置。解析标识符时,引擎先从当前作用域开始查找;若在当前作用域中未找到,则会在外层嵌套的作用域中继续查找,直到找到该变量或抵达最外层(即全局作用域)。这一查找路径在函数声明时已确定,与函数运行时的位置无关。
<script type="text/javascript">
var a = 1;
var b = 2;
var c = 3;
var d = 4;
function inner(d) {//它的作用域链是inner---全局
var c = 8;
console.log(a);//1 当前作用域找不到a,去全局作用域找到了a=1
console.log(b);//2 当前作用域找不到b,去全局作用域找到了b=2
console.log(c);//8 当前作用域找到了c=8
console.log(d);//7 当前作用域找到了d=7,形参也是局部作用域
// console.log(e);//ReferenceError: e is not defined 引用错误,找不到e, 它的作用域链是inner---全局
console.log(a+b+c+d);//18
}
function outter(e) {
var a = 5;//inner()的作用域链是inner---全局,所以这个a相当于无效
var b = 6;//inner()的作用域链是inner---全局,所以这个a相当于无效
inner(7);
}
outter(999);//这个999无效,里面的e根本找不到
</script>
在多层嵌套作用域中可以定义同名的标识符,这被称为“遮蔽效应”,即内部标识符会遮蔽外部同名标识符。
通过 window.a 这种技术可以访问那些被同名变量遮蔽的全局变量。但如果非全局变量被遮蔽,则无论如何都无法被访问到。
<script type="text/javascript">
var a = 'Lily';
var b = 'Lucy';
function outer() {
var b = 'Jesica';
var c = 'Susan';
function inner(c) {
console.log(a);//Lily
console.log(window.b);//Lucy
console.log(b);//Jesica
console.log(c);//Jenifer
}
inner('Jenifer');
}
outer();
</script>
执行环境与执行环境栈
执行环境
执行环境(execution context),也称为执行上下文。每个执行环境都关联一个变量对象(variable object),用于保存函数可访问的所有变量和数据(即函数作用域链上的所有数据和变量)。代码无法直接访问该对象,它是供引擎内部使用的。
执行环境栈:当执行进入一个函数时,该函数的执行环境会被推入栈中。函数执行完毕后,栈会将其执行环境移除,其中的变量和数据会被标记清除并等待垃圾回收,随后将控制权交还给之前的执行环境。JavaScript 程序的执行正是由这一机制控制的。
需要注意的是,如果当前执行环境(存放当前作用域链里的数据和变量)找不到变量,那就是找不到了,不会往之前的那个执行环境查找,和作用域链是不一样的。
代码的执行顺序并非完全按行执行,而是与函数的调用顺序密切相关。
- 代码进入全局执行环境,全局执行环境被放入环境栈。
- 当执行到一个函数时,该函数的执行环境会被推入到环境栈的顶端,之前的执行环境则向后移。
- 全局执行环境最先进入,因此一直位于栈底,这与栈的概念类似。
- 函数执行完毕后,其执行环境会从作用域链顶端移除,其中保存的数据和函数会被标记清除,等待垃圾回收。
- 控制权交还给之前的执行环境,继续往下执行。
- 当页面关闭时,全局执行环境才会被销毁。
<script type="text/javascript">
var a = 1;
var b = 2;
var c = 3;
var d = 4;
function inner(d) {//它的作用域链是inner---全局
var c = 8;
console.log(a);//1 当前作用域找不到a,去全局作用域找到了a=1
console.log(b);//2 当前作用域找不到b,去全局作用域找到了b=2
console.log(c);//8 当前作用域找到了c=8
console.log(d);//7 当前作用域找到了d=7,形参也是局部作用域
// console.log(e);//ReferenceError: e is not defined 引用错误,找不到e, 它的作用域链是inner---全局
console.log(a+b+c+d);//18
}
function outter(e) {
var a = 5;//inner()的作用域链是inner---全局,所以这个a相当于无效
var b = 6;//inner()的作用域链是inner---全局,所以这个a相当于无效
inner(7);
}
outter(999);//这个999无效,里面的e根本找不到
</script>
以上代码的执行顺序:
代码执行进入全局执行环境,并对全局执行环境中的代码进入声明提升; 执行第2行,赋值a=1; 然后第3行赋值b=2; 然后第4行赋值c=3; 然后第5行赋值d=4; 执行第20行,调用outer(999)函数,然后进入outer(999)函数执行环境,声明提升,并将实参999传给形参e;现在环境栈中有两个执行环境,outer(999)是当前执行环境; 执行第16行,赋值a=5; 然后第17行赋值b=6; 执行第18行,调用inner(7)函数,然后进入inner(7)函数执行环境,声明提升,并将实参7传给形参d; 执行第7行,赋值c=8; 然后运算并输出;
性能优化建议
代码优化
由于在作用域链上查找变量需要消耗性能,我们应该尽快找到变量。因此在函数多层嵌套时,应尽可能使用函数内部的局部变量。
在函数内部使用全局变量属于跨作用域操作。如果某个跨作用域的值在函数内部被多次使用,应将其存储到局部变量中,这样可以提高性能。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。