对作用域链的理解
课本上的解释是:在 JavaScript 里面,函数、块、模块都可以形成作用域(一个存放变量的独立空间),他们之间可以相互嵌套,作用域之间会形成引用关系,这条链叫做作用域链。
那作用域链到底具体是什么样的呢?让我们一起来看看吧
首先呢,我们要插入一个预编译的环节
预编译详解
(一) JavaScript编译过程的三个步骤: 1. 语法分析 2. 预编译 3. 解释执行
(二) 预编译概述
JavaScript预编译发生在代码片段执行前的很短时间内(时间可以忽略不计但又存在),预编译分为两种,一种是函数内部预编译,另一种是全局预编译,全局预编译发生在页面加载完成时执行,函数预编译发生在函数执行的前一刻。预编译会创建当前环境的执行上下文。(后文的作用域链会提及 执行上下文 注意注意哟!!)
(三) 函数的预编译执行四部曲
- 创建AO(Activation Object)对象;
- 找形参和变量声明,将变量声明和形参作为AO的属性名,值为underfined;
- 将实参和形参值统一;
- 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体。
代码示例
//请问下面的console.log()输出什么?
function fn(a) {
console.log(a); // 1
var a = 123 //变量赋值
console.log(a); // 123
function a() { } //函数声明
console.log(a); // 123
var b = function () { }//变量赋值(函数表达式)
console.log(b); // function (){}
function d() { } //函数声明
}
fn(1) //函数调用
接下来,就让我们根据 函数的预编译执行四部曲 通过AO的变化 一起来解答四个console.log()的输出值吧!
1.创建AO对象
AO{
// 空对象
}
2.找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined;
AO{
a: undefined //obj不能同时出现相同的key值 否则下面会覆盖上面的
b: undefined
}
3.将实参和形参值统一;
AO{
a: 1,
b: undefined
}
4.在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体。
AO{
a: function(){}
b: undefined
d: function(){}
}
下面是完整的预编译过程
AO:{
a:undefined 1 function a(){}
b:undefined function (){}
d:function d(){}
}
5.预编译结束,最后,代码开始执行,执行结果 在代码示例里哦
(四) 全局的预编译执行三部曲
- 创建 GO 对象
- 找全局变量声明,将变量声明作为AO的属性名,值为undefined
- 在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体
代码示例
global = 100;
function fn() {
console.log(global); //
global = 200;
console.log(global); //
var global = 300;
}
fn();
根据全局预编译三部曲我们同样可以知道他的GO变化过程
1.创建GO对象
GO{
// 空对象
}
- 找形参和变量声明,将变量声明和形参作为GO的属性名,值为underfined
GO: {
global: undefined
}
- 在全局里找函数声明,将函数名作为GO对象的属性名,值赋予函数体
GO: {
global: undefined
fn: function() { }
}
只要存在函数声明就会带来函数自己的AO,预编译过程(多层嵌套情况同样适用)继续套用四部曲即可
作用域链
接下来便是主场show了,首先来了解几个概念名词吧,它们对作用域链的理解起重要作用哦
执行期上下文
当函数执行的时候,会创建一个称为执行期上下文的内部对象(AO对象)。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行期上下文会被销毁
注意哦:函数在执行时会创建一个内部对象(AO对象),并且在执行完毕后会被销毁
查找变量
从作用域的顶端依次往下查找。当找到了第一个就会停止查找(遮蔽效应)
[[scope]]
函数的作用域,是不可访问的,其中存储了运行期上下文的集合
作用域链
[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式连接,我们把这种链式连接叫做作用域链
当然,单纯看概念是很抽象的,那我们就来举举栗子吧
//函数是对象,是对象就可以具备属性
function test(){
}
//test.name
//test.prototype // 原型(原型属于后面es6的内容,就是个概念,这里就不深究啦)
//test.[[scope]] // 作用域属性 隐式属性
test() ---> AO:{} // 函数执行之后,那么这个函数的AO对象就会被回收
test() ---> AO:{} // 再次创建,执行之后会被再次回收
接着再看看下面的代码和拆分详谈,思路会更加清晰
function a(){
function b(){
var b= 222
}
var a = 111
b()
console.log(a)
}
var glob = 100
a()
function a(){}
a的定义给我们带来了a.[[scope]]
var global = 100
全局代码的执行会创建 GO{} 此时a.[[scope]] --->0:GO{}
此时a可以访问GO{}
a()
a的执行会创建了AO{} 此时a.[[scope]] --->0:AO{} 1:GO{}
注意:后创建的会放在前面,在作用域里面的排列方式,此时在a的作用域里既可以访问到AO还可以访问到GO
为了理解的更透彻,我们可以采用画图+文字的方法来描述
function a(){
function b(){
var b= 222
}
var a = 111
b()
console.log(a)
}
var glob = 100
a()
a函数的定义,带来了a自己的作用域,在a函数作用域里,a函数的执行过程如上表
再次强调哦,谁后创建,谁就放在前面 a的执行带来了b的定义
// a定义 a.[[scope]] --->0:GO{}
// a执行 a.[[scope]] --->0:AO{} 1:GO{} // a的作用域链
// a的执行带来了b的定义
// b定义 b.[[scope]] --->0:bAO{} 1:aAO{} 2:GO{} // b的作用域链
为什么函数体外的作用域访问不到函数体内的作用域,而函数体内的作用域可以访问到函数体外的作用域的本质原因
答:因为函数的AO对象会在函数的执行完毕回收。函数b在调用结束之后,函数b的AO回收了,但是此时函数a未必执行完毕,如果函数a想要访问b的数据,极大可能会出现BUG,所以JavaScript这门语言一般不允许外层函数访问内层数据