可执行上下文

知识梳理

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时,变量对象包括:

  1. 函数的所有形参(针对函数上下文)
    • 由名称和对应值组成的一个变量对象的属性被创建
    • 没有实参,属性值是undefined
  2. 函数声明
    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建;
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性;
  3. 变量声明
    • 由名称和对应值(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()

理解🚩

🦝面试中必问闭包:

  1. 官方定义
  2. 执行上下文两大概念:作用域链、变量对象🚩
  3. 优点:获取方式便捷
  4. 缺点:太便捷

🚩针对第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

始终指向调用它的人