作用域、上下文、执行期上下文、作用域链、闭包是JavaScript中关键概念之一,是JavaScript难点之一,在应聘面试时必定会问到的问题,作为前端工程师必须理解和掌握。相信大家已经阅读了很多关于这方面的文章,但是看完之后似懂非懂。在我阅读了《JavaScript高级程序设计》、《高性能的JavaScript》这两本书后,我才完全理解这些概念。


一、作用域(Scope)

  作用域概念是理解JavaScript的关键所在,不仅仅从性能角度,还包括从功能角度。作用域就是变量和函数的可访问范围,控制着变量和函数的可见性与生命周期,换句话说,作用域决定了代码区块中变量和其他资源的可见性。在JavaScript中变量的作用域有全局作用域和局部作用域。JavaScript采用词法作用域(lexical scoping),也就是静态作用域。

静态作用域与动态作用域

  • 词法作用域:词法作用域是指在词法分析阶段就确定了,不会改变。变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。

  • 动态作用域:动态作用域是在运行时根据程序的流程信息来动态确定的,而不是在写代码时进行静态确定的。 动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们在何处调用。

JavaScript的词法作用域

如果一个文档流中包含多个script代码段(用script标签分隔的js代码或引入的js文件),它们的运行顺序是:

  • 1 、读入第一个代码段(js执行引擎并非一行一行地分析程序,而是一段一段地分析执行的)

  • 2 、做词法分析,有错则报语法错误(比如括号不匹配等),并跳转到步骤5

  • 3、对var变量和function定义做“预解析“(永远不会报错的,因为只解析正确的声明)

  • 4、执行代码段,有错则报错(比如变量未定义)

  • 5、如果还有下一个代码段,则读入下一个代码段,重复步骤2

  • 6 、完成

下面看一个例子就能明白JavaScript的词法作用域:

 var value = 1;function foo() {console.log(value);}function bar() {var value = 2;foo();}bar();// 结果是 ???

假设JavaScript采用静态作用域,让我们分析下执行过程:

  执行 foo 函数,先从 foo 函数局部作用域中查找是否有变量 value,如果没有,就从全局作用域中查找变量value的值,所以结果会打印 1。

假设JavaScript采用动态作用域,让我们分析下执行过程:

  执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。

前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 1。

全局作用域、局部作用域和块级作用域

  在ECMAScript 5(包括ECMAScript 5)之前的版本中,作用域只有全局作用域和局部作用域,不存在块级作用域;ECMAScript 6引入了let和const关键字,利用let和const可以形成块级作用域。

1、全局作用域

  在代码中任何地方都能访问到的对象拥有全局作用域。全局作用域的变量是全局对象的属性,不论在什么函数中都可以直接访问,而不需要通过全局对象,但加上全局对象,可以提供搜索效率。

a、没有用var声明的变量(除去函数的参数)都具有全局作用域,成为全局变量,所以声明局部变量必须要用var。

b、window的所有属性都具有全局作用域

c、最外层函数体外声明的变量也具有全局作用域

2、局部作用域

局部变量的优先级高于全局变量。

a、函数体内用var声明的变量具有局部作用域,成为局部变量

b、函数的参数也具有局部作用域

var a=3; // 全局变量
function fn(b){ // 局部变量  c=2; // 全局变量  var d=5; // 局部变量  function subFn(){  var e=d; // 父函数的局部变量对子函数可见  for(var i=0;i<3;i++){  console.write(i);  }  alert(i);// 3, 在for循环内声明,循环外function内仍然可见,没有块作用域  }
}
alert(c); // 在function内声明但不带var修饰,仍然是全局变量  

3、块级作用域

使用let和const关键字声明的变量,会在形成块级作用域。

if (true) {// 'if' 条件语句块不会创建一个新的作用域// name 在全局作用域中,因为通过 'var' 关键字定义var name = 'Hammad';// likes 在局部(本地)作用域中,因为通过 'let' 关键字定义let likes = 'Coding';// skills 在局部(本地)作用域中,因为通过 'const' 关键字定义const skills = 'JavaScript and PHP';
}
console.log(name); // logs 'Hammad'
console.log(likes); // Uncaught ReferenceError: likes is not defined
console.log(skills); // Uncaught ReferenceError: skills is not defined

二、上下文(context)

  许多开发人员经常混淆作用域(scope)和上下文(context),很多误解为它们是相同的概念。但事实并非如此。作用域(scope)我们上面已经讨论过了,而上下文(context)是用来指定代码某些特定部分中this的值。作用域(scope) 是指变量的可访问性,上下文(context)是指this在同一作用域内的值。我们也可以使用call()、apply()、bind()、箭头函数等改变上下文。在浏览器中在全局作用域(scope)中上下文中始终是Window对象。在Node.js中在全局作用域(scope)中上下文中始终是Global 对象。

var name = "windowsName";
function a() {var name = "Cherry";console.log(this.name);  // windowsNameconsole.log("inner:" + this);// inner: Window
}
a();
console.log("outer:" + this) // outer: Window

  上下文始终坚持一个原理:this 永远指向最后调用它的那个对象,上例中调用a函数的是window,所以a函数中的this指向window对象。关于this以及改变this的指向,可以参考this、apply、call、bind

三、作用域链(Scope Chain)

  JavaScript 中每个函数都都表示为一个函数对象(函数实例),函数对象有一个仅供 JavaScript 引擎使用的[[scope]] 属性。通过语法分析和预解析,将[[scope]] 属性指向函数定义时作用域中的所有对象集合。这个集合被称为函数的作用域链(scope chain),包含函数定义时作用域中所有可访问的数据。

function add(num1, num2) {var sum = num1 + num2;return sum;
}

  当定义 add 函数后,其作用域链就创建了。函数所在的全局作用域的全局对象被放置到 add 函数作用域链([[scope]] 属性)中。我们可以从下图中看到作用域链的第一个对象保存的是全局对象,全局对象中保存了诸如 this , window , document 以及全局对象中的 add 函数,也就是他自己。这也就是我们可以在全局作用域下的函数中访问 window(this),访问全局变量,访问函数自身的原因。全局上下文中的变量对象(Variable object,VO)就是全局对象。

四、执行期上下文(Execution Context)

  执行具体的某个函数时,JS引擎在执行每个函数实例时,都会创建一个执行期上下文(Execution Context)和激活对象(active Object)(它们属于宿主对象,与函数实例执行的生命周期保持一致,也就是函数执行完成,这些对象也就被销毁了,闭包例外。)

假设我们运行以下代码:

var total = add(5, 10);

  执行该函数创建一个内部对象,称为 Execution Context(执行期上下文)。执行期上下文定义了一个函数正在执行时的作用域环境。特别注意,执行期上下文和我们平常说的上下文不同,执行期上下文指的是作用域。平常说的上下文是this的取值指向。执行期上下文和函数创建时的作用域链对象 [[scope]] 区分,这是两个不同的作用域链对象。分开的原因很简单,函数定义时的作用域链对象 [[scope]] 是固定的,而 执行期上下文 会根据不同的运行时环境变化。而且该函数每执行一次,都会创建单独的 执行期上下文,因此对同一函数调用多次,会导致创建多个执行期上下文。一旦函数执行完成,执行期上下文将被销毁。

  执行期上下文对象有自己的作用域链,当创建执期行上下文时,其作用域链将使用执行函数[[scope]]属性所包含的对象(即,函数定义时的作用域链对象)进行初始化。这些值按照它们在函数中出现的顺序复制到执行期上下文作用域链中。

  无论有多少个函数上下文,但是全局上下文只有一个。执行期上下文有创建和代码执行的两个阶段。

第一阶段:创建阶段

当一个函数被调用但是其代码还没有被执行的时。在创建阶段主要做的三件事情是:

  • 创建变量(激活)对象(VO == AO)
  • 创建作用域链
  • 设置上下文(context)的值( this

激活对象(Activation Object,AO)

当一个函数被调用但是其代码还没有被执行的时,在执行其上下文中创建一个名为 Activation Object(激活对象)的新对象。这个激活对象保存了函数中的所有形参,实参,局部变量,this 指针等函数执行时函数内部的数据情况。然后将这个激活对象推送到执行其上下文作用域链的顶部。

1).函数参数(若未传入,初始化该参数值为undefined)
  
2).函数声明(若发生命名冲突,会覆盖)
  
3).变量声明(初始化变量值为undefined,若发生命名冲突,会忽略。)

add函数被调用,但是还未执行时的VO(变量对象)==AO(激活对象)是:

AO(add) = { arguments: {0: 5,1: 10length: 2}, num1: 5,  num2: 10,  sum: undefined
}; 

  激活对象是一个可变对象,里面的数据随着函数执行时的数据的变化而变化,当函数执行结束之后,执行期上下文将被销毁。也就会销毁Execution Context的作用域链,激活对象也同样被销毁。但如果存在闭包,激活对象就会以另外一种方式存在,这也是闭包产生的真正原因,具体的我们稍后讨论。下图显示了执行上下文及其作用域链:

  从左往右看,第一部分是函数执行时创建的执行期上下文,它有自己的作用域链,第二部分是作用域链中的对象,索引为1的对象是从[[scope]]作用域链中复制过来的,索引为0的对象是在函数执行时创建的激活对象,第三部分是作用域链中的对象的内容Activation Object(激活对象)和Global Object(全局对象)。

  函数在执行时,每遇到一个变量,都会去执行期上下文的作用域链的顶部,执行函数的激活对象开始向下搜索,如果在第一个作用域链(即,Activation Object 激活对象)中找到了,那么就返回这个变量。如果没有找到,那么继续向下查找,直到找到为止。如果在整个执行期上下文中都没有找到这个变量,在这种情况下,该变量被认为是未定义的。这也就是为什么函数可以访问全局变量,当局部变量和全局变量同名时,会使用局部变量而不使用全局变量,以及 JavaScript 中各种看似怪异的、有趣的作用域问题的答案。

第二阶段:代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值,并最终执行代码。当代码执行完后,这时候的 AO 是:

AO(add) = { arguments: {0: 5,1: 10length: 2}, num1: 5,  num2: 10,  sum: 15
}; 

五、闭包(Closure)

  闭包(Closure)是 JavaScript 最强大的特性之一,它允许函数访问局部作用域之外的数据。闭包在日常编码工作中非常常见。但是,它会对性能造成影响。了解闭包我们使用以下示例代码:

function assignEvents(){var id = "xdi9592";document.getElementById("save-btn").onclick = function(event) {saveDocument(id);};
}

  assignEvents 函数为DOM元素分配一个事件处理程序。这个处理函数就是一个闭包。为了使该闭包访问id变量,必须创建一个特定的作用域链。

我们一起来从作用域的角度分析一下闭包的形成过程:

  assignEvents 函数创建并且词法解析后,函数对象assignEvents的[[scope]]属性被初始化,作用域链形成,作用域链中包含了全局对象的所有属性和方法(注意,此时因为 assignEvents 函数还未被执行,所以闭包函数并没有被解析)。

  assignEvents 开始执行时,创建 Execution Context(执行期上下文),在执行期上下文的作用域链中创建 Activation Object(激活对象),并将 Activation Object(激活对象) 推送到作用域链顶部,在其中保存了函数执行时所有可访问函数内部的数据。激活对象包含 id 变量。

  当执行到闭包时,JavaScript 引擎发现了闭包函数的存在,按照通常的手法,将闭包函数解析,为闭包函数对象创建 [[scope]] 属性,初始化作用域链。特别注意的是,这个时候,闭包函数对象的作用域链中有两个对象,一个是 assignEvents 函数执行时的 Activation Object(激活对象) ,还有一个是全局对象,如下图:

  我们看到图中闭包函数对象的作用域链和 assignEvents 函数的执行期上下文的作用域链是相同的。为什么相同呢?我们来分析一下,闭包函数是在 assignEvents 函数执行的过程中被定义并且解析的,而函数执行时的作用域是 Activation Object(激活对象) ,闭包函数被解析的时候它的作用域正是 assignEvents 作用域链中的第一个作用域对象 Activation Object(激活对象) ,当然,由于作用域链的关系,全局对象作用域也被引入到闭包函数的作用域链中。

  在词法分析的时候闭包函数的 [[scope]] 属性 就已经在作用域链中保存了对 assignEvents 函数的 Activation Object(激活对象) 的引用,所以当 assignEvents 函数执行完毕之后,闭包函数虽然还没有开始执行,但依然可以访问 assignEvents 的局部数据,并不是因为闭包函数要访问 assignEvents 的局部变量id,所以当 assignEvents 函数执行完毕之后依然保持了对局部变量id的引用。而是不管是否存在变量引用,都会保存对 assignEvents 的 Activation Object(激活对象)作用域对象的引用。因为在词法分析时,闭包函数没有执行,函数内部根本就不知道是否要对 assignEvents 的局部变量进行访问和操作,所以只能先把 assignEvents 的 Activation Object(激活对象) 作用域对象保存起来,当闭包函数执行时,如果需要访问 assignEvents 的局部变量,那么再去作用域链中查找。

  也正是因为这种引用,造成了一个副作用。通常,当执行期上下文被销毁时,函数的激活对象也就被销毁了。当有闭包引用时,激活对象就不会被销毁,因为他仍然被引用。这意味着闭包比非隔离的函数需要更多的内存。

  闭包函数执行时创建了自己的 Execution Context(执行期上下文),其作用域链使用了 [[scope]] 属性,其引用了 assignEvents 函数的 Activation Object(激活对象) 和 全局对象。然后为闭包本身创建一个新的 Activation Object(激活对象)。 所以在闭包函数的执行期上下文的作用域链中保存了自己的 Activation Object(激活对象),外层函数 assignEvents Execution Context(执行期上下文)的 Activation Object(激活对象),以及 Global Object(全局对象),如图:

参考链接:

实例分析 JavaScript 作用域

深入理解JavaScript中的作用域和上下文

JavaScript 核心概念之作用域和闭包

JavaScript作用域、上下文、执行期上下文、作用域链、闭包相关推荐

  1. 25、搞懂闭包、作用域、执行期上下文(VO、AO)、作用域链

    25.1 闭包 闭包是指有权访问另一个函数作用域中的变量的函数--js高程 简单来说闭包就是函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包. 25.2 作 ...

  2. 深入理解javascript原型和闭包(18)——补充:上下文环境和作用域的关系

    本系列用了大量的篇幅讲解了上下文环境和作用域,有些人反映这两个是一回儿事.本文就用一个小例子来说明一下,作用域和上下文环境绝对不是一回事儿. 再说明之前,咱们先用简单的语言来概括一下这两个的区别. 0 ...

  3. JS基础篇之作用域、执行上下文、this、闭包

    前言:JS 的作用域.执行上下文.this.闭包是老生常谈的话题,也是新手比较懵懂的知识点.当然即便你作为老手,也未必真的能理解透彻这些概念. 一.作用域和执行上下文 作用域: js中的作用域是词法作 ...

  4. js笔记---作用域(执行上下文[execution context],活动对象) 闭包

    作用域: 首先,在javascript中的每个函数都是对象,是Funtion对象的一个实例,而Funtion中有一系列仅供javascript引擎存取的内部属性,其中一个便是[[scope]],它包含 ...

  5. JavaScript 作用域与执行上下文

    JavaScript的作用域(scope)和执行上下文(execution context)总是纠缠不清,以至于网上出现了大量文章来区分这两个概念. MDN中是这样描述scope的: Scope Th ...

  6. php函数嵌套 作用域,javascript 嵌套的函数(作用域链)_javascript技巧

    嵌套的函数(作用域链) 当你进行函数的嵌套时,要注意实际上作用域链是发生变化的,这点可能看起来不太直观.你可把下面的代码置入firebug监视值的变化. var testvar = 'window属性 ...

  7. 【疑难杂症】JavaScript执行期上下文

    什么是执行期上下文(execution context) 总结一句话:执行上下文就是JS代码的环境,它记录着当前环境所定义的所有变量 执行期上下文的类型 全局执行期上下文(GO) 全局代码执行之前,产 ...

  8. JS作用域、立即执行函数、闭包

    作用域 首先先介绍一下作用域等一些基础概念. 每个JavaScript函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供JavaScript引擎存取,[[scope]]就是其中一 ...

  9. 以及其任何超类对此上下文都是未知的_web前端入门到实战:Javascript 中的「上下文」你只需要看这一篇

    正文 上下文 是Javascript 中的一个比较重要的概念, 可能很多朋友对这个概念并不是很熟悉, 那换成「作用域」 和 「闭包」呢?是不是就很亲切了. 「作用域」和「闭包」 都是和「执行上下文」密 ...

最新文章

  1. 使用SQLServer配置管理器配置SQLServer数据库引擎实例,以便侦听特定的固定1433端口。...
  2. 马斯克要往火星轨道送跑车,在深空待10亿年,静候外星人
  3. php中curl的详解
  4. notepad 没有plugin manager_如何在没有反光镜的情况下在户外拍摄人物?
  5. 数据分析 python 用途-想做好数据分析,不用Python怎么行?
  6. XML注入介绍--XXE,XEE,xpath等
  7. python的仿真效果好吗_Python SimPy 仿真系列 (1)
  8. List.addAll方法的入参不能为null
  9. (软件工程复习核心重点)第十二章软件项目管理-第一节:软件项目管理综述、估算软件规模和工作量估算
  10. 宕机了,Redis数据丢了怎么办?
  11. python pytest
  12. python制作安装包_如何制作python安装模块(setup.py)
  13. i219v微星 驱动_适用于WinPE的I219V英特尔驱动程序
  14. 使用visualSVN做版本管理
  15. HTML5线性图表 图表数据区域可着色
  16. java美图秀秀,SpringMvc整合美图秀秀M4(头像编辑器)
  17. python 前端开发_python和前端开发怎么抉择?
  18. 龙芯3A3000上实现BLFS的轻量级桌面LXDE
  19. 2020.11.23Junit详解
  20. 【深度学习概念】感受野

热门文章

  1. 第8.27节 Python中__getattribute__与property的fget、@property装饰器getter关系深入解析
  2. M.2 Nvme硬盘和SDD固态(RAID5下) 速率对比及对 SQL SERVER 影响
  3. Android Studio报错Android resource linking failed mergeDebugResources-24:/values-v31/val...的解决方案
  4. Linux||环境变量
  5. 英雄联盟总结之客户端综述3(笔记分享)
  6. 初中学历学计算机专业,初中毕业学计算机专业可不可以
  7. matlab 光镊,光镊工具包对于中间尺寸粒子光捕获的计算
  8. 4-3-2 扫描技术(Web漏洞扫描)
  9. 通过Kalman滤波对方程进行参数估计matlab
  10. 构建springclound项目(详情)