【javaScript】- 作用域[[scope]]

最近在重新复习js底层的内容,这里简单把作用域的内容做个整理,并附上几道题及其思路(很可能会有笔误哈哈哈哈哈,如有错误恳请指出!),欢迎大家一起来探究。

一、必备知识

1.js运行三部曲(语法分析 - 预编译 - 解释执行)

  • ①语法分析:会通篇扫描一遍代码,看是否有低级语法错误(如:写了中文),若有错误,该代码块中的代码一行都不会执行,无错则进入下一步
  • ②预编译:就是创建全局的执行期上下文,也就是GO对象(如:进行变量函数声明提升等操作)
  • ③解释执行:开始执行代码,解释一行执行一行

2.作用域:即[[scope]]存储了运行期上下文的集合(就是变量和函数生效的区域)

3.作用域链:[[scope]]中存储着执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。

4.执行期上下文:当函数在执行的前一刻,会创建一个称为执行期上下文的内部对象(即预编译环节)。一个执行期上下问定义了一个函数执行时的环境,函数每次执行时对应的上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下问,当函数执行完毕,执行上下文被销毁(剪断链接)

5.查找变量:在哪个函数里查找变量,就从哪个函数作用域链的顶端依次向下查找

6.全局预编译三部曲(发生在语法分析后,解释执行前)

  • ①创建GO对象(即全局执行期上下文) 【此时全局的[[scope]][0] = GO:{}】
  • ②找变量声明,并将其作为GO的属性名,值为undefined
  • ③找到函数声明,并将其作为GO的属性名,值为该函数 【此时函数的 [[scope]][0] = GO:{}】

7.函数预编译四部曲(发生在函数执行前一刻)

  • ①创建AO对象(即该函数的执行期上下文)【此时函数的[[scope]][0] = AO:{}; [[scope]][1] = GO:{}】
  • ②找形参和变量声明,并将其作为AO的属性名,值为undefined
  • ③将实参值和形参相统一(即把实参的值传到形参里)
  • ④找到函数声明,并将其作为AO的属性名,值为该函数

二、案例

先来段代码,帮助理解

js 复制代码
function a() {
   function b() {
      function c() { }
         c();
      }
   b();
}
a();
// 以下是代码理解过程:
// a被定义:a.[[scope]] -> 0:GO:{}
// a被执行:a.[[scope]] -> 0:aAO:{} -> 1:GO:{}
// b被定义:b.[[scope]] -> 0:aAO:{} -> 1:GO:{}
// b被执行:b.[[scope]] -> 0:bAO:{} -> 1:aAO:{} -> 2:GO:{}
// c被定义:c.[[scope]] -> 0:bAO:{} -> 1:aAO:{} -> 2:GO:{}
// c被执行:c.[[scope]] -> 0:cAO:{} -> 1:bAO:{} -> 2:aAO:{} -> 3:GO:{}
// c执行完毕:c.[[scope]] -> 0:bAO:{} -> 1:aAO:{} -> 2:GO:{}
// b执行完毕:b.[[scope]] -> 0:aAO:{} -> 1:GO:{}
// a执行完毕:a.[[scope]] -> 0:GO:{}

案例1

js 复制代码
console.log(a);//function a(){}
var a = 123;
function a() { }
// 以下是代码理解过程:
// 1.开始运行代码前,先进行全局的预编译
// ①生成GO,也就是window对象
// ②找全局的变量声明,并将变量名作为属性名存到AO中,值为undefined,此时GO:{a:undefined}
// ③找全局的函数声明,并将其作为属性存到GO中,值为该函数,此时GO:{a:function a(){}},且此时a函数的[[scope]][0]=GO
// 2.开始执行全局代码
// 3.执行console.log(a)。到GO中去找a,为function a(){}
// 4.执行var a = 123。var a在预编译已经完成了,略过,看赋值a = 123,此时GO:{a:123}
// 5.执行function a(){}。函数声明在预编译已经完成了,略过。

案例2

js 复制代码
function test() {
    var a = b = 123;
    console.log(window.b);//123
    console.log(b);//123
}
test();
// 以下是代码理解过程:
// 1.开始运行代码前,先进行全局的预编译
// ①生成GO,也就是window对象
// ②找全局的变量声明,无,下一步
// ③找全局的函数声明,并将其作为属性存到GO中,值为该函数,此时GO:{test:function test(){}},且此时test函数的[[scope]][0]=GO
// 2.开始执行全局代码
// 3.执行function test(){...}。函数声明在预编译已经完成了,略过。
// 4.执行test()。为函数调用,开始test函数的预编译
// ①会创建AO{}对象,并将其放到[[scope]]最顶端,即第0位,此时fn的[[scope]][0]=AO[[scope]][1]=GO
// ②找形参和变量声明,并将变量名作为属性名存到AO中,值为undefined,此时AO:{a:undefined}
// ③将形参和实参一一对应,无,下一步
// ④找函数声明无,下一步
// 5.执行var a = b = 123;。var a在预编译已经完成了,略过,赋值符号是最后执行的,所以先看b = 123,b未声明就赋值,所以将其放到GO上
//   此时GO:{test:function test(){},b:123},然后再将b的值123赋值给a,则此时AO:{a:123}
// 6.执行console.log(window.b)。这里标明了找window上的b,也就是直接上GO找,也就是123
// 7.执行console.log(b)。作用域链最顶端,也就是第0位的AO上找b,发现没有b,沿着作用域链往下找,找到下一位GO,有b,为123

案例3

js 复制代码
function fn(a) {
    console.log(a);//function a(){}
    var a = 123
    console.log(a);//123
    function a() { }
    console.log(a)//123
    var b = function () { }
    console.log(b)//function (){}
    function d() { }
}
fn(1)
// 以下是代码理解过程:
// 1.开始运行代码前,先进行全局的预编译
// ①生成GO,也就是window对象
// ②找全局的变量声明,无,下一步
// ③找全局的函数声明,并将其作为属性存到GO中,值为该函数,此时GO:{fn:function fn(){...}},且此时fn函数的[[scope]][0]=GO
// 2.开始执行全局代码
// 3.执行fn(1),为函数调用,开始fn函数的预编译
// ①会创建AO{}对象,并将其放到[[scope]]最顶端,即第0位,此时fn的[[scope]][0]=AO[[scope]][1]=GO
// ②找形参和变量声明,并将变量名作为属性名存到AO中,值为undefined,此时AO:{a:undefined,b:undefined}
// ③将形参和实参一一对应,形参a对应的实参是1,则此时AO:{a:1,b:undefined}
// ④找函数声明,并将其函数名作为属性名存到AO中,值为该函数体,此时AO:{a:function a(){},b:undefined,d:function d(){}}
// 4.执行console.log(a)。从作用域链最顶端,也就是第0位的AO上找a,为function a(){}
// 5.执行var a = 123。var a在预编译已经完成了,略过,看赋值a = 123,此时AO:{a:123,b:undefined,d:function d(){}}
// 6.执行console.log(a)。从作用域链最顶端,也就是第0位的AO上找a,为123
// 7.执行function a(){}。 函数声明在预编译已经完成了,略过。
// 8.执行console.log(a)。从作用域链最顶端,也就是第0位的AO上找a,为123
// 9.执行var b = function (){}。var b在预编译已经完成了,略过,看赋值b = function (){},此时AO:{a:123,b:function (){},d:function d(){}}
// 10.执行console.log(b)。从作用域链最顶端,也就是第0位的AO上找b,为function (){}
// 11.执行function d(){}。函数声明在预编译已经完成了,略过。
// 12.fn函数执行完毕,此时会将fn函数的执行期上下文销毁(即剪断链接)此时fn的[[scope]][0]=GO

案例4

js 复制代码
function test(a, b) {
    console.log(a);//function a(){}
    console.log(b);//undefined
    var b = 234;
    console.log(b);//234
    a = 123;
    console.log(a);//123
    function a() { }
    var a;
    b = 234;
    var b = function () { }
    console.log(a);//123
    console.log(b);//function (){}
}
test(1)
// 以下是代码理解过程:
// 1.开始运行代码前,先进行全局的预编译
// ①生成GO,也就是window对象
// ②找全局的变量声明,无,下一步
// ③找全局的函数声明,并将其作为属性存到GO中,值为该函数,此时GO:{test:function test(){...}},且此时test函数的[[scope]][0]=GO
// 2.开始执行全局代码
// 3.执行test(1),为函数调用,开始test函数的预编译
// ①会创建AO{}对象,并将其放到[[scope]]最顶端,即第0位,此时fn的[[scope]][0]=AO[[scope]][1]=GO
// ②找形参和变量声明,并将变量名作为属性名存到AO中,值为undefined,此时AO:{a:undefined,b:undefined}
// ③将形参和实参一一对应,形参a对应的实参是1,则此时AO:{a:1,b:undefined}
// ④找函数声明,并将其函数名作为属性名存到AO中,值为该函数体,此时AO:{a:function a(){},b:undefined}
// 4.执行console.log(a)。从作用域链最顶端,也就是第0位的AO上找a,为function a(){}
// 5.执行console.log(b)。从作用域链最顶端,也就是第0位的AO上找b,为undefined
// 6.执行var b = 234。var b在预编译已经完成了,略过,看赋值b = 234,此时AO:{a:function a(){},b:234}
// 7.执行console.log(b)。从作用域链最顶端,也就是第0位的AO上找b,为234
// 8.执行a = 123。此时AO:{a:123,b:234}
// 9.执行console.log(a)。从作用域链最顶端,也就是第0位的AO上找a,为123
// 10.执行function a(){}。函数声明在预编译已经完成了,略过。
// 11.执行var a。变量声明在预编译已经完成了,略过。
// 12.执行b = 234。此时AO:{a:123,b:234}
// 13.执行var b = function (){}。var b在预编译已经完成了,略过,看赋值b = function (){},此时AO:{a:123,b:function (){}}
// 14.执行console.log(a)。从作用域链最顶端,也就是第0位的AO上找a,为123
// 15.执行console.log(b)。从作用域链最顶端,也就是第0位的AO上找b,为function (){}
// 16.test函数执行完毕,此时会将test函数的执行期上下文销毁(即剪断链接)此时test的[[scope]][0]=GO

案例5

js 复制代码
console.log(a);//function a(){}
var a = 123;
function a() { }
// 以下是代码理解过程:
// 1.开始运行代码前,先进行全局的预编译
// ①生成GO,也就是window对象
// ②找全局的变量声明,并将变量名作为属性名存到AO中,值为undefined,此时GO:{a:undefined}
// ③找全局的函数声明,并将其作为属性存到GO中,值为该函数,此时GO:{a:function a(){}},且此时a函数的[[scope]][0]=GO
// 2.开始执行全局代码
// 3.执行console.log(a)。到GO中去找a,为function a(){}
// 4.执行var a = 123。var a在预编译已经完成了,略过,看赋值a = 123,此时GO:{a:123}
// 5.执行function a(){}。函数声明在预编译已经完成了,略过。

案例6

js 复制代码
console.log(test);//function test(){...}
function test(test) {
    console.log(test);//function test(){}
    var test = 234;
    console.log(test);//234
    function test() { }
}
test(1);
var test = 123;
console.log(test);//123
// 以下是代码理解过程:
// 1.开始运行代码前,先进行全局的预编译
// ①生成GO,也就是window对象
// ②找全局的变量声明,并将变量名作为属性名存到GO中,值为undefined,此时GO:{test:undefined}
// ③找全局的函数声明,并将其作为属性存到GO中,值为该函数,此时GO:{test:function test(){...}},且此时test函数的[[scope]][0]=GO
// 2.开始执行全局代码
// 3.执行console.log(test)。找GO中的test,为function test(){...}
// 4.执行test(1)。为函数调用,开始test函数的预编译
// ①会创建AO{}对象,并将其放到[[scope]]最顶端,即第0位,此时test的[[scope]][0]=AO[[scope]][1]=GO
// ②找形参和变量声明,并将变量名作为属性名存到AO中,值为undefined,此时AO:{test:undefined}
// ③将形参和实参一一对应,形参test对应的实参是1,则此时AO:{test:1}
// ④找函数声明,并将其函数名作为属性名存到AO中,值为该函数体,此时AO:{test:function test(){}}
// 4.执行console.log(test)。从作用域链最顶端,也就是第0位的AO上找a,为function test(){}
// 5.执行var test = 234。var test在预编译已经完成了,略过,看赋值test = 234,此时AO:{test:234}
// 6.执行console.log(test)。从作用域链最顶端,也就是第0位的AO上找a,为234
// 7.执行function test(){}。变量声明在预编译已经完成了,略过。
// 8.test函数执行完毕,此时会将test函数的执行期上下文销毁(即剪断链接)此时test的[[scope]][0]=GO
// 9.执行var test = 123。var test在预编译已经完成了,略过,看赋值test = 123,此时GO:{test:123}
// 10.执行console.log(test)。找GO中的test,为123

案例7

js 复制代码
var global = 100;
function fn() {
    console.log(global);//100
}
fn();
// 以下是代码理解过程:
// 1.开始运行代码前,先进行全局的预编译
// ①生成GO,也就是window对象
// ②找全局的变量声明,并将变量名作为属性名存到GO中,值为undefined,此时GO:{global:undefined}
// ③找全局的函数声明,并将其作为属性存到GO中,值为该函数,此时GO:{global:undefined,fn:function fn(){...}},且此时fn函数的[[scope]][0]=GO
// 2.开始执行全局代码
// 3.执行var global = 100。var global在预编译已经完成了,略过,看赋值global = 100,此时GO:{global:100,fn:function fn(){...}}
// 4.fn()。为函数调用,开始fn函数的预编译
// ①会创建AO{}对象,并将其放到[[scope]]最顶端,即第0位,此时fn的[[scope]][0]=AO[[scope]][1]=GO
// ②找形参和变量声明,无,下一步
// ③将形参和实参一一对应,无,下一步
// ④找函数声明,无,下一步
// 5.执行console.log(global)。从作用域链最顶端,也就是第0位的AO上找global,发现没有,沿着作用域链往下找,找到下一位GO,有global,为100
// 6.fn函数执行完毕,此时会将fn函数的执行期上下文销毁(即剪断链接)此时fn的[[scope]][0]=GO

案例8

js 复制代码
global = 100;
function fn() {
    console.log(global); // undefined
    global = 200;
    console.log(global); // 200
    var global = 300;
}
fn();
var global;
// 以下是代码理解过程:
// 1.开始运行代码前,先进行全局的预编译
// ①生成GO,也就是window对象
// ②找全局的变量声明,并将变量名作为属性名存到GO中,值为undefined,此时GO:{global:undefined}
// ③找全局的函数声明,并将其作为属性存到GO中,值为该函数,此时GO:{global:undefined,fn:function fn(){...}},且此时fn函数的[[scope]][0]=GO
// 2.开始执行全局代码
// 3.执行global = 100。此时GO:{global:100,fn:function fn(){...}}
// 4.fn()。为函数调用,开始fn函数的预编译
// ①会创建AO{}对象,并将其放到[[scope]]最顶端,即第0位,此时fn的[[scope]][0]=AO[[scope]][1]=GO
// ②找形参和变量声明,并将变量名作为属性名存到AO中,值为undefined,此时AO:{global:undefined}
// ③将形参和实参一一对应,无,下一步
// ④找函数声明,无,下一步
// 5.执行console.log(global)。从作用域链最顶端,也就是第0位的AO上找global,为undefined
// 6.执行global = 200。此时AO:{global:200}
// 7.执行console.log(global)。从作用域链最顶端,也就是第0位的AO上找global,为200
// 8.执行global = 300。此时AO:{global:300}
// 9.fn函数执行完毕,此时会将fn函数的执行期上下文销毁(即剪断链接)此时fn的[[scope]][0]=GO
// 10.执行var global。var global在预编译已经完成了,略过

案例9

js 复制代码
function test() {
    console.log(b);//undefined
    if (a) {
        var b = 100;
    }
    c = 234;
    console.log(c);//234
}
var a;
test();
a = 10;
console.log(c);//234
// 以下是代码理解过程:
// 1.开始运行代码前,先进行全局的预编译
// ①生成GO,也就是window对象
// ②找全局的变量声明,并将变量名作为属性名存到GO中,值为undefined,此时GO:{a:undefined}
// ③找全局的函数声明,并将其作为属性存到GO中,值为该函数,此时GO:{a:undefined,test:function test(){...}},且此时test函数的[[scope]][0]=GO
// 2.开始执行全局代码
// 3.执行var a。var a在预编译已经完成了,略过
// 4.test()。为函数调用,开始fn函数的预编译
// ①会创建AO{}对象,并将其放到[[scope]]最顶端,即第0位,此时fn的[[scope]][0]=AO[[scope]][1]=GO
// ②找形参和变量声明,并将变量名作为属性名存到AO中,值为undefined,此时AO:{b:undefined}(注意:在if中的变量声明依然会被找到并进行预编译环节)
// ③将形参和实参一一对应,无,下一步
// ④找函数声明,无,下一步
// 5.执行console.log(b)。从作用域链最顶端,也就是第0位的AO上找b,为undefined
// 6.执行if (a) {var b = 100}。从作用域链最顶端,也就是第0位的AO上找a,发现没有,沿着作用域链往下找,找到下一位GO,有a,为undefined,所以判断条件为false,不进入
// 7.执行c = 234。c是未经声明的变量直接使用,所以会挂在全局GO上,此时GO:{a:undefined,test:function test(){...},c:234}
// 8.执行console.log(c)。从作用域链最顶端,也就是第0位的AO上找a,发现没有,沿着作用域链往下找,找到下一位GO,有c,为234
// 9.fn函数执行完毕,此时会将fn函数的执行期上下文销毁(即剪断链接)此时fn的[[scope]][0]=GO
// 10.执行var global。var global在预编译已经完成了,略过
// 11.test函数执行完毕,此时会将test函数的执行期上下文销毁(即剪断链接)此时test的[[scope]][0]=GO
// 12.执行a = 10。此时GO{a:10,test:function test(){...},c:234}
// 12.执行console.log(c)。找GO中的c,为234

案例10

js 复制代码
a = 100;
function test(e) {
    function e() { }
    arguments[0] = 2;
    console.log(e);//2
    if (a) {
        var b = 123;
        function c() { }
    }
    var c;
    a = 10;
    var a;
    console.log(b);//undefined
    f = 123;
    console.log(c);//undefined
    console.log(a);//10
}
var a;
test(1);
console.log(a);//100
console.log(f);//123
// 以下是代码理解过程:
// 1.开始运行代码前,先进行全局的预编译
// ①生成GO,也就是window对象
// ②找全局的变量声明,并将变量名作为属性名存到GO中,值为undefined,此时GO:{a:undefined}
// ③找全局的函数声明,并将其作为属性存到GO中,值为该函数,此时GO:{a:undefined,test:function test(){...}},且此时test函数的[[scope]][0]=GO
// 2.开始执行全局代码
// 3.执行a = 100。此时GO:{a:100,test:function test(){...}}
// 4.执行var a。var a在预编译已经完成了,略过
// 5.test(1)。为函数调用,开始fn函数的预编译
// ①会创建AO{}对象,并将其放到[[scope]]最顶端,即第0位,此时fn的[[scope]][0]=AO[[scope]][1]=GO
// ②找形参和变量声明,并将变量名作为属性名存到AO中,值为undefined,此时AO:{e:undefined,b:undefined,a:undefined,c:undefined}(注意:在if中的变量声明依然会被找到并进行预编译环节)
// ③将形参和实参一一对应,形参e对应的实参是1,则此时AO:{e:1,b:undefined,a:undefined,c:undefined}
// ④找函数声明,并将其函数名作为属性名存到AO中,值为该函数体,此时AO:{e:function e(){},b:undefined,a:undefined,c:undefined}
// (注意:之前在if中的函数声明是会被找到并进行预编译的,但现在是不允许在if里面定义函数声明的)
// 6.执行function e(){}。函数声明在预编译已经完成了,略过
// 7.执行arguments[0] = 2。因为实参列表和形参是对应关系,一方改另一方也会改,始终保持相同。也就是将形参e的值修改成2,此时AO:{e:2,b:undefined,a:undefined,c:undefined}
// 8.执行console.log(e)。从作用域链最顶端,也就是第0位的AO上找e,为2
// 9.执行if (a){...}。要找a的值,从作用域链最顶端,也就是第0位的AO上找a,为undefined,所以判断条件为false,不进入
// 10.执行var c。var c在预编译已经完成了,略过
// 11.执行a = 10。此时AO:{e:2,b:undefined,a:10,c:undefined}
// 12.执行console.log(b)。从作用域链最顶端,也就是第0位的AO上找b,为undefined
// 13.执行f = 123。f是未经声明的变量直接使用,所以会挂在全局GO上,此时GO:{a:100,test:function test(){...},f:123}
// 14.执行console.log(c)。从作用域链最顶端,也就是第0位的AO上找c,为undefined
// 15.执行console.log(a)。从作用域链最顶端,也就是第0位的AO上找a,为10
// 16.test函数执行完毕,此时会将test函数的执行期上下文销毁(即剪断链接)此时test的[[scope]][0]=GO
// 17.执行console.log(a)。找GO中的a,为100
// 18.执行console.log(f)。找GO中的f,为123
相关推荐
恋猫de小郭10 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端