【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
相关推荐
来杯三花豆奶1 小时前
Vue3 Pinia 从入门到精通
前端·javascript·vue.js
syt_10131 小时前
设计模式之-工厂模式
javascript·单例模式·设计模式
卡布叻_星星2 小时前
Docker之Nginx前端部署(Windows版-x86_64(AMD64)-离线)
前端·windows·nginx
LYFlied2 小时前
【算法解题模板】-解二叉树相关算法题的技巧
前端·数据结构·算法·leetcode
weibkreuz2 小时前
React的基本使用@2
前端·javascript·react.js
于是我说2 小时前
前端JavaScript 项目中 获取当前页面滚动位置
开发语言·前端·javascript
小肖爱笑不爱笑2 小时前
JavaScript
java·javascript·json·web
GISer_Jing2 小时前
AI在前端开发&营销领域应用
前端·aigc·音视频
凯小默2 小时前
02.内存管理和内存泄漏
javascript