可执行上下文
知识梳理
INFO
当JS代码执行一段 excutable code(可执行代码),会创建 excution context(可执行上下文)。对每一个EC,都包含三个重要属性:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
执行上下文(EC)
变量提升
- 函数和变量提升,函数优于变量,Javascript中函数是一等公民;
- 函数声明
foo(){}
有提升,函数表达式var foo = funtion(){}
没有; - 为什么有变量提升的概念?和JS代码执行有关。JS如何执行?一段一段去执行可执行的代码,每执行一段可执行代码就会创建一个可执行上下文。
var foo = function () {
console.log('foo1');
}
foo();
var foo = function () {
console.log('foo2');
}
foo();
function foo() {
console.log('foo1');
}
foo();
function foo() {
console.log('foo2');
}
foo();
可执行代码和上下执行文栈
- 可执行代码(excutable code):类型包括全局代码、函数代码、eval代码(JS的内置函数,用于计算字符串表达式的值)
- 执行上下文栈(excution context stack,ECS):管理执行上下文,执行过程:
//1.上下文栈为一个数组 ECStack = [] //2.JS开始执行可执行代码时,最先遇到的全局代码,初始化时压入全局上下执行文 //只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext ECStack = [globalContext] //3.JS遇到函数代码: function fun3() { console.log('fun3') } function fun2() { fun3(); } function fun1() { fun2(); } fun1(); //4.当执行一个代码才生效(fn1()),创建一个执行上下文,且压入ECStack,执行完毕弹出。 ECStack.push(<fun1> functionContext);// fun1() ECStack.push(<fun2> functionContext);// fun1中竟然调用了fun2,还要创建fun2的执行上下文 ECStack.push(<fun3> functionContext);// fun2还调用了fun3! ECStack.pop();// fun3执行完毕 ECStack.pop();// fun2执行完毕 ECStack.pop();// fun1执行完毕
思考
虽然两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?
// case 1
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
// case 2
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
执行栈变化不同:
// 第一段代码:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
// 第二段代码:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
变量对象(VO)
定义
VO是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。因不同可执行上下文的变量对象不同,以下对全局上下文的变量对象
和函数上下文的变量对象
进行描述。
全局上下文的变量对象
对JS而言,全局上下文的变量对象就是全局对象。
// 1.客户端JS中,通过this可以访问全局对象window对象
console.log(this);
// 2.全局对象是由Object构造函数实例化的一个对象
console.log(this instanceof Object);
// 3.预定义的属性是否可用
console.log(Math.random());
console.log(this.Math.random());
// 4.作为全局变量的宿主
var a = 1;
console.log(this.a);
// 5.客户端 JavaScript 中,全局对象有 window 属性指向自身
var a = 1;
console.log(window.a);
this.window.b = 2;
console.log(this.b);
函数上下文的变量对象(AO)
函数上下文中,用活动对象activation object(AO)来表示变量对象。
活动对象和变量对象是同一个东西,变量对象是引擎是线上的,不可在JS环境中访问,当进入一个ECS中,ESC的变量才会激活。而只有被激活的变量对象,活动对象上的属性才能被访问。
活动对象(AO)是进入函数上下文被创建的,通过函数的arguments属性初始化,arguments属性值是Arguments对象。
EC的执行过程
EC的代码执行过程:进入EC => 代码执行
1.进入EC
当进入EC时,变量对象包括:
- 函数的所有形参(针对函数上下文)
- 由名称和对应值组成的一个变量对象的属性被创建
- 没有实参,属性值是undefined
- 函数声明
- 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建;
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性;
- 变量声明
- 由名称和对应值(undefined)组成一个变量对象的属性被创建;
- 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性;
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
// 以上代码进入EC,AO为
AO = {
arguments: {//1.函数上下文的变量对象初始化只包括 Arguments 对象;类似全局上下文的变量对象初始化是全局对象🚩
0: 1,
length: 1
},
a: 1,//1.实参
b: undefined,//3.变量
c: reference to function c(){},//2.函数声明
d: undefined//3.函数表达式视为变量
}
2.代码执行
顺序执行代码,根据代码修改变量的值,上方1的例子,当代码执行后,AO为:
AO = {
arguments: {//1.arguments对象
0: 1,
length: 1
},
a: 1,//1.实参
b: 3,//3.变量
c: reference to function c(){},//2.函数声明
d: reference to FunctionExpression "d"//3.函数表达式视为变量
}
思考
function foo() {
console.log(a);
a = 1;
}
foo(); // ???
function bar() {
a = 1;
console.log(a);
}
bar(); // ???
第一段foo函数a没有通过var声明,所以不会存放在AO中。
AO = {
arguments: {
length: 0
}
}
没有 a 的值,然后就会到全局去找,全局也没有,所以会报错。
当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1。
作用域(scope)
作用域在许多程序设计语言中非常重要。值程序源代码中定义变量的区域,规定了如何查找变量,确定当前执行代码对变量的访问权限。
静态作用域(词法作用域 Lexical scoping)
静态作用域(词法作用域)是函数的作用域在函数定义时确认,相关语言:Javascript、C/C++、Python、Java……
动态作用域是函数的作用域在调用时确认,相关语言:Pascal、Emacs Lisp、bash脚本……
var value = 1;
function foo(){//此写法是函数声明,var foo = function fn(){}是函数表达式,函数声明才会变量提升
console.log(value)
};
function bar(){
value = 2
foo()
}
bar()
函数创建
静态(词法)作用域是函数在定义时就已经决定。
原因是函数有一个内部属性[[scope]]
,当函数创建时,会保存所有父变量对象到其中,[[scope]]
是所有父级变量对象的层级链,但并不代表完整的作用域链。
function foo(){
function bar(){...}
}
// 此时,各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
函数激活
当函数激活,进入函数上下文,创建VO/AO后,会将活动对象添加到作用域链的前端。此时EC的作用域链命名为Scope:
Scope = [AO].concat([[Scope]]);
至此,作用域链创建完毕。
闭包
概念
重点在于变量对象的引用。
官方定义:能够访问到自由变量的函数
自由变量:能够在函数中使用,但不是函数的参数,也不是函数内部的局部变量
沿着作用域链 访问别人的变量对象
var a = 1;
function foo(){
console.log(a)
}
foo()
理解🚩
🦝面试中必问闭包:
- 官方定义
- 执行上下文两大概念:作用域链、变量对象🚩
- 优点:获取方式便捷
- 缺点:太便捷
🚩针对第2点,扩展说明 函数执行上下文中的作用域链和变量对象的创建过程:
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
//1.checkscope 函数被创建,保存作用域链到内部属性`[[scope]]`
checkscope.[[scope]] = [
globalContext.VO
];
//2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
checkscopeContext,
globalContext
];
//3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
Scope: checkscope.[[scope]],
}
//4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
AO:{
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
//5.第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
AO:{
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
//6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = {
AO:{
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
//7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];
this
始终指向调用它的人