0、开场白

  在平时编写JavaScript代码时,我们并不会和执行上下文直接接触,但是想要彻底搞懂JavaScript函数的话,执行上下文是我们绕不过去的一个知识点。

1、执行上下文栈

  JavaScript在对一个函数的每次调用,都会创建一个执行上下文,然后基于这个执行上下文运行函数体内的代码。一个函数可能会创建无数的执行上下文,因为对函数的每次调用(即使在函数内部调用自己)都会创建一个具有新状态的上下文。

  当函数a执行的时候,会创建一个函数a的执行上下文,然后执行函数a中的代码,在函数a中调用另一个函数b的时候,当前a函数的执行会暂停下来,它的执行上下文也不会消失,然后创建一个函数b的执行上下文,执行函数b中的代码。从这个例子可以看出,JavaScript需要对函数的执行上下文进行有序的管理,有一种数据结构特别符合这种场景,它就是栈,所以JavaScript中管理执行上下文的就是执行上下文栈,有时也叫它调用栈。

2、执行上下文

  为什么调用函数的时候要创建一个执行上下文?因为JavaScript要保存一些函数调用时的信息,比如传入的参数、谁调用的。

  执行上下文是由JavaScript的一个内部实现,类似于一个简单的对象,这个对象拥有三个属性:变量对象,作用域链,this。下图展示了一个执行上下文的结构

  通俗易懂的来讲讲js的函数执行上下文-LMLPHP

3、变量对象

  变量对象是与执行上下文相关的数据作用域。它是一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明。变量对象是一个抽象概念。对于不同的上下文类型,使用不同的对象。比如,在全局上下文中变量对象就是全局对象本身;当函数被调用的时候,则会创建一个活动对象来作为变量对象使用。函数调用创建的活动对象,会比全局对象多一些属性,例如:形参、arguments。

4、作用域链  

  作用域链的作用就是用来查找自由变量的,如果一个变量在函数自身的作用域(活动对象)中没有找到,那么将会查找它外层函数的作用域(活动对象),以此类推。在函数体内没有定义,需要搜索作用域链的变量叫做自由变量。

  关于作用域链最关键的一条信息就是,在创建函数的时候会保存外层函数的作用域链,这个保存下来的作用域链会在将来函数调用时用来查找变量。在函数调用时,将当前函数的活动对象拼接在创建函数时保存的外层函数作用域链开头,然后保存在执行上下文的作用域链属性中,方便函数体内的自由变量进行变量查找。

5、闭包

  在计算机科学中,闭包,又称词法闭包或函数闭包,是引用了自由变量的函数。

  在JavaScript中,函数是第一级,就是说函数可以作为函数的参数、可以作为函数的返回值、可以赋值给变量。这是一个很强大的语言特性,不过它会引起两个问题:

  1. 当函数作为返回值时,这个函数内部的自由变量的解析问题。
  2. 当函数作为参数传递时,这个函数内部的自由变量的解析问题。

  接下来通过两段代码来说清楚这两个问题,先来看第一段代码,在全局环境中声明一个foo函数,foo函数里声明一个变量x等于10,然后返回一个函数,这个函数内部打印了x,执行foo函数,把返回值赋值给了back,在全局环境中声明一个变量x=20,此时执行back函数,会打印出来多少?

function foo() {
  var x = 10;
  return function() {
    console.log(x);
  };
}
var back = foo();
var x = 20;
back(); 

   要想知道打印出来的x等于多少,只需要知道变量x的查找顺序就行。back函数是定义在foo函数内部的,在定义的时候,会保存foo函数的作用域链,然后在back函数执行的时候,会把自己的活动对象拼接在foo函数作用域链的开头,所以变量x的查找顺序是back函数的活动对象,这是个空对象,没找到x,然后接着查找第二级,也就是foo函数的活动对象,里面有x,等于10,所以最后打印出来的结果是10。foo函数已经执行完了,它的执行上下文应该消失了,为什么还能找到foo函数执行时的活动对象呢?因为在JavaScript中,如果有外部引用使用了活动对象里的属性,这个活动对象并不会被回收掉。我画了张图帮助大家理解一下这个执行过程:

 通俗易懂的来讲讲js的函数执行上下文-LMLPHP

  第二段代码描述的是函数作为参数传递时的场景:

var x = 10;
function foo() {
  console.log(x);
}
var b = function (foo) {
  var x = 20;
  foo();
}
b(foo);

   原理和上面一样,我也画了张图帮助大家理解一下:

通俗易懂的来讲讲js的函数执行上下文-LMLPHP

  在JavaScript中,所有函数的执行都是基于这两种基础的模型的,无非就是变量多一些,嵌套深一些,查找的过程长一点,希望大家以后能看到代码时就能在脑海中呈现出代码的执行过程,变量的查找过程。

6、this

  执行上下文中还有最后一个this属性没讲,很多JavaScript初学者被this搞的晕头转向,这里我会帮大家理清楚this的所有情况,以后不用再怕this了。

  在JavaScript中,this是关键字,它不是变量,所以它不会参与到变量的解析过程,也就是说不会去查找作用域链。当JavaScript在执行代码时遇到this时,它会直接从执行上下文中拿到this的值,不用做任何查找。

  this的值只在创建执行上下文的时候进行确定,确定之后不会更改。可能有人会反驳,this指向可以修改的呀,那是在进入函数体执行代码之前修改的,进入函数体之后就不能修改了。

  在全局环境中,this的值就是全局对象。

  在函数中,this的值一共有四种情况:

  1. 作为构造函数调用时,this指向构造函数创建的对象
  2. 作为对象的方法调用时,指向该对象
  3. 作为函数调用时,指向全局对象,严格模式下为undefined
  4. call、apply、bind调用时,指向传入的第一个参数

  到此为止,和JavaScript函数执行上下文有关的知识点我都介绍完了,希望这篇文章能够帮你更深刻的认识JavaScript的函数。

  

  参考资料:

  https://www.ecma-international.org/publications/standards/Ecma-262.htm

  中文版ECMA规范

03-09 20:37