对于Javascript 执行上下文的全面了解


Posted in Javascript onSeptember 05, 2017

在这篇文章中,将比较深入地阐述下执行上下文 ? JavaScript中最基础也是最重要的一个概念。相信读完这篇文章后,你就会明白javascript引擎内部在执行代码以前到底做了些什么,为什么某些函数以及变量在没有被声明以前就可以被使用,以及它们的最终的值是怎样被定义的。

什么是执行上下文

Javascript中代码的运行环境分为以下三种:

全局级别的代码 ? 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。

函数级别的代码 ? 当执行一个函数时,运行函数体中的代码。

Eval的代码 ? 在Eval函数内运行的代码。

在网上可以找到很多阐述作用域的资源,为了使该文便于大家理解,我们可以将“执行上下文”看做当前代码的运行环境或者作用域。下面我们来看一个示例,其中包括了全局以及函数级别的执行上下文:

对于Javascript 执行上下文的全面了解

上图中,一共用4个执行上下文。紫色的代表全局的上下文;绿色代表person函数内的上下文;蓝色以及橙色代表person函数内的另外两个函数的上下文。注意,不管什么情况下,只存在一个全局的上下文,该上下文能被任何其它的上下文所访问到。也就是说,我们可以在person的上下文中访问到全局上下文中的sayHello变量,当然在函数firstName或者lastName中同样可以访问到该变量。

至于函数上下文的个数是没有任何限制的,每到调用执行一个函数时,引擎就会自动新建出一个函数上下文,换句话说,就是新建一个局部作用域,可以在该局部作用域中声明私有变量等,在外部的上下文中是无法直接访问到该局部作用域内的元素的。在上述例子的,内部的函数可以访问到外部上下文中的声明的变量,反之则行不通。那么,这到底是什么原因呢?引擎内部是如何处理的呢?

执行上下文堆栈

在浏览器中,javascript引擎的工作方式是单线程的。也就是说,某一时刻只有唯一的一个事件是被激活处理的,其它的事件被放入队列中,等待被处理。下面的示例图描述了这样的一个堆栈:

对于Javascript 执行上下文的全面了解

我们已经知道,当javascript代码文件被浏览器载入后,默认最先进入的是一个全局的执行上下文。当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创建一个新的执行上下文,并且将其压入到执行上下文堆栈的顶部。浏览器总是执行当前在堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,然后,进入其下的上下文执行代码。这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到回到全局的上下文。请看下面一个例子:

(function foo(i) {
   if (i === 3) {
    return;
   }
   else {
    foo(++i);
   }
  }(0));

上述foo被声明后,通过()运算符强制直接运行了。函数代码就是调用了其自身3次,每次是局部变量i增加1。每次foo函数被自身调用时,就会有一个新的执行上下文被创建。每当一个上下文执行完毕,该上上下文就被弹出堆栈,回到上一个上下文,直到再次回到全局上下文。真个过程抽象如下图:

对于Javascript 执行上下文的全面了解

由此可见 ,对于执行上下文这个抽象的概念,可以归纳为以下几点:

单线程

同步执行

唯一的一个全局上下文

函数的执行上下文的个数没有限制

每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如此。

执行上下文的建立过程

我们现在已经知道,每当调用一个函数时,一个新的执行上下文就会被创建出来。然而,在javascript引擎内部,这个上下文的创建过程具体分为两个阶段:

建立阶段(发生在当调用一个函数时,但是在执行函数体内的具体代码以前)

建立变量,函数,arguments对象,参数

建立作用域链

确定this的值

代码执行阶段:

变量赋值,函数引用,执行其它代码

实际上,可以把执行上下文看做一个对象,其下包含了以上3个属性:

(executionContextObj = {
   variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ },
   scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ },
   this: {}
   }

建立阶段以及代码执行阶段的详细分析

确切地说,执行上下文对象(上述的executionContextObj)是在函数被调用时,但是在函数体被真正执行以前所创建的。函数被调用时,就是我上述所描述的两个阶段中的第一个阶段 ? 建立阶段。这个时刻,引擎会检查函数中的参数,声明的变量以及内部函数,然后基于这些信息建立执行上下文对象(executionContextObj)。在这个阶段,variableObject对象,作用域链,以及this所指向的对象都会被确定。

上述第一个阶段的具体过程如下:

找到当前上下文中的调用函数的代码

在执行被调用的函数体中的代码以前,开始创建执行上下文

进入第一个阶段-建立阶段:

建立variableObject对象:

建立arguments对象,检查当前上下文中的参数,建立该对象下的属性以及属性值

检查当前上下文中的函数声明:

每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用

如果上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖。

初始化作用域链

确定上下文中this的指向对象

代码执行阶段:

执行函数体中的代码,一行一行地运行代码,给variableObject中的变量属性赋值。

下面来看个具体的代码示例:

function foo(i) {
   var a = 'hello';
   var b = function privateB() {
  
   };
   function c() {
  
   }
  }
  
  foo(22);

在调用foo(22)的时候,建立阶段如下:

fooExecutionContext = {
   variableObject: {
    arguments: {
     0: 22,
     length: 1
    },
    i: 22,
    c: pointer to function c()
    a: undefined,
    b: undefined
   },
   scopeChain: { ... },
   this: { ... }
  }

由此可见,在建立阶段,除了arguments,函数的声明,以及参数被赋予了具体的属性值,其它的变量属性默认的都是undefined。一旦上述建立阶段结束,引擎就会进入代码执行阶段,这个阶段完成后,上述执行上下文对象如下:

fooExecutionContext = {
   variableObject: {
    arguments: {
     0: 22,
     length: 1
    },
    i: 22,
    c: pointer to function c()
    a: 'hello',
    b: pointer to function privateB()
   },
   scopeChain: { ... },
   this: { ... }
  }

我们看到,只有在代码执行阶段,变量属性才会被赋予具体的值。

局部变量作用域提升的缘由

在网上一直看到这样的总结: 在函数中声明的变量以及函数,其作用域提升到函数顶部,换句话说,就是一进入函数体,就可以访问到其中声明的变量以及函数。这是对的,但是知道其中的缘由吗?相信你通过上述的解释应该也有所明白了。不过在这边再分析一下。

看下面一段代码:

(function() {
   console.log(typeof foo); // function pointer
   console.log(typeof bar); // undefined
  
   var foo = 'hello',
    bar = function() {
     return 'world';
    };
  
   function foo() {
    return 'hello';
   }
  
  }());​

上述代码定义了一个匿名函数,并且通过()运算符强制理解执行。那么我们知道这个时候就会有个执行上下文被创建,我们看到例子中马上可以访问foo以及bar变量,并且通过typeof输出foo为一个函数引用,bar为undefined。

为什么我们可以在声明foo变量以前就可以访问到foo呢?

因为在上下文的建立阶段,先是处理arguments, 参数,接着是函数的声明,最后是变量的声明。那么,发现foo函数的声明后,就会在variableObject下面建立一个foo属性,其值是一个指向函数的引用。当处理变量声明的时候,发现有var foo的声明,但是variableObject已经具有了foo属性,所以直接跳过。当进入代码执行阶段的时候,就可以通过访问到foo属性了,因为它已经就存在,并且是一个函数引用。

为什么bar是undefined呢?

因为bar是变量的声明,在建立阶段的时候,被赋予的默认的值为undefined。由于它只要在代码执行阶段才会被赋予具体的值,所以,当调用typeof(bar)的时候输出的值为undefined。

好了,到此为止,相信你应该对执行上下文有所理解了,这个执行上下文的概念非常重要,务必好好搞懂之!

以上这篇对于Javascript 执行上下文的全面了解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JQuery 构建客户/服务分离的链接模型中Table中的排序分析
Jan 22 Javascript
jQuery Ajax使用 全解析
Dec 15 Javascript
jQuery AJAX实现调用页面后台方法和web服务定义的方法分享
Mar 01 Javascript
JS定时器实例
Apr 17 Javascript
js操作checkbox遇到的问题解决
Jun 29 Javascript
js的hasownproperty使用示例
Mar 02 Javascript
jQuery层级选择器用法分析
Feb 10 Javascript
浅析ES6的八进制与二进制整数字面量
Aug 30 Javascript
浅谈vue2 单页面如何设置网页title
Nov 08 Javascript
node使用request请求的方法
Dec 20 Javascript
Vue.js中的高级面试题及答案
Jan 13 Javascript
js滚轮事件 js自定义滚动条的实现
Jan 18 Javascript
JS一个简单的注册页面实例
Sep 05 #Javascript
基于react组件之间的参数传递(详解)
Sep 05 #Javascript
zTree获取当前节点的下一级子节点数实例
Sep 05 #Javascript
JavaScript基础进阶之数组方法总结(推荐)
Sep 04 #Javascript
JS沙箱模式实例分析
Sep 04 #Javascript
详解基于 axios 的 Vue 项目 http 请求优化
Sep 04 #Javascript
JS实现颜色的10进制转化成rgba格式的方法
Sep 04 #Javascript
You might like
《破坏领主》销量已超100万 未来将继续开发新内容
2020/03/08 其他游戏
杏林同学录(一)
2006/10/09 PHP
php新建文件自动编号的思路与实现
2011/06/27 PHP
PHP读取数据库并按照中文名称进行排序实现代码
2013/01/29 PHP
php中使用PHPExcel读写excel(xls)文件的方法
2014/09/15 PHP
php json_encode()函数返回json数据实例代码
2014/10/10 PHP
laravel5.6框架操作数据curd写法(查询构建器)实例分析
2020/01/26 PHP
IE不出现Flash激活框的小发现的js实现方法
2007/09/07 Javascript
Javascript 文件夹选择框的两种解决方案
2009/07/01 Javascript
js 完美图片新闻轮转效果,腾讯大粤网首页图片轮转改造而来
2011/11/21 Javascript
js动态在form上插入enctype=multipart/form-data的问题
2012/05/24 Javascript
用js实现trim()的解决办法
2013/04/16 Javascript
自制的文件上传JS控件可支持IE、chrome、firefox etc
2014/04/18 Javascript
JS实现下拉菜单赋值到文本框的方法
2015/08/18 Javascript
详解基于angular路由的requireJs按需加载js
2017/01/20 Javascript
Vue.js 单页面多路由区域操作的实例详解
2017/07/17 Javascript
js自定义trim函数实现删除两端空格功能
2018/02/09 Javascript
通过nodejs 服务器读取HTML文件渲染到页面的方法
2018/05/17 NodeJs
vue打包之后生成一个配置文件修改接口的方法
2018/12/09 Javascript
nodejs微信开发之授权登录+获取用户信息
2019/03/17 NodeJs
vue+vant实现购物车全选和反选功能
2020/11/17 Vue.js
Python实现的径向基(RBF)神经网络示例
2018/02/06 Python
在Python中关于使用os模块遍历目录的实现方法
2019/01/03 Python
python爬虫请求头的使用
2020/12/01 Python
浅谈HTML5 defer和async的区别
2016/06/07 HTML / CSS
俄罗斯最大的香水和化妆品网上商店:Randewoo
2020/11/05 全球购物
大学生学期自我鉴定
2014/03/19 职场文书
党的群众路线对照检查材料思想汇报(学校)
2014/10/04 职场文书
中学后勤工作总结2015
2015/07/22 职场文书
重阳节主题班会
2015/08/17 职场文书
小学生优秀作文范文(六篇)
2019/07/10 职场文书
nginx location优先级的深入讲解
2021/03/31 Servers
redis 限制内存使用大小的实现
2021/05/08 Redis
Java8中接口的新特性使用指南
2021/11/01 Java/Android
Tomcat执行startup.bat出现闪退的原因及解决办法
2022/04/20 Servers
Python日志模块logging用法
2022/06/05 Python