最近在重新复习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