全面了解JavaScript的作用域链


Posted in Javascript onApril 03, 2019
JavaScript的作用域链

这是一个非常重要的知识点了,了解了JavaScript的作用域链的话,能帮助我们理解很多‘异常'问题。

下面我们来看一个小例子,前面我说过的声明提前的例子。

var name = 'Skylor.min';
 function echo() {
 alert(name);
 var name = 'mm';
 alert(name);
 alert(age);
 }

 echo();

 对于这个例子,没有接触过这方面的时候,第一反应是会纠结下,这第一个的name,到底调用全局变量的name,还是函数内部的name呢,如果调用全局的,可是函数内部也用定义和赋值啊, 如果调用函数内部的局部变量的话,那么他的值是mm吗?还是引用全局的'Skylor.min'呢?

于是这个小例子就会有这样的错误答案:

Skylor.min
mm
[脚本出错]
 

其实不然,知道函数内的提前说明,就知道这是不正确的。

    undefined
    mm
    [脚本出错]
 

应该是这样的,那到底为什么是这个答案呢,提前声明这又是什么呢?一切的一切,涉及到JavaScript的作用域链。

原理

首先来说说,JavaScript的作用域的原理:

在JavaScript权威指南中有一句很精辟的描述: JavaScript中的函数运行在它们被定义的作用域里,而不是它们被运行的作用域里。

另外在JavaScript中有个很重要的概念,那就是: 在JavaScript中,一切皆对象,函数也是。

在JS中,作用域的概念和其他语言差不多, 在每次调用一个函数的时候 ,就会进入一个函数内的作用域,当从函数返回以后,就返回调用前的作用域

JS的语法风格和C/C++类似, 但作用域的实现却和C/C++不同,并非用“堆栈”方式,而是使用列表,具体过程如下(ECMA262中所述):

  • 任何执行上下文时刻的作用域, 都是由作用域链(scope chain, 后面介绍)来实现
  • 在一个函数被定义的时候, 会将它定义时刻的scope chain链接到这个函数对象的[[scope]]属性
  • 在一个函数对象被调用的时候,会创建一个活动对象(也就是一个对象), 然后对于每一个函数的形参,都命名为该活动对象的命名属性, 然后将这个活动对象做为此时的作用域链(scope chain)最前端, 并将这个函数对象的[[scope]]加入到scope chain中.

看个例子吧:

var func = function(lps, rps){
        var name = 'Skylor.min';
        ........
    }
    func();
 

在执行func的定义语句的时候, 会创建一个这个函数对象的[[scope]]属性(内部属性,只有JS引擎可以访问, 但FireFox的几个引擎(SpiderMonkey和Rhino)提供了私有属性__parent__来访问它), 并将这个[[scope]]属性, 链接到定义它的作用域链上(后面会详细介绍), 此时因为func定义在全局环境, 所以此时的[[scope]]只是指向全局活动对象window active object.

在调用func的时候, 会创建一个活动对象(假设为aObj, 由JS引擎预编译时刻创建, 后面会介绍),并创建arguments属性, 然后会给这个对象添加俩个命名属性aObj.lps, aObj.rps; 对于每一个在这个函数中申明的局部变量和函数定义, 都作为该活动对象的同名命名属性.

然后将调用参数赋值给形参数,对于缺少的调用参数,赋值为undefined。

然后将这个活动对象做为scope chain的最前端, 并将func的[[scope]]属性所指向的,定义func时候的顶级活动对象, 加入到scope chain.

有了上面的作用域链, 在发生标识符解析的时候, 就会逆向查询当前scope chain列表的每一个活动对象的属性,如果找到同名的就返回。找不到,那就是这个标识符没有被定义。

注意到, 因为函数对象的[[scope]]属性是在定义一个函数的时候决定的, 而非调用的时候, 所以如下面的例子:

var name = 'Skylor.min';
 function echo() {
 alert(name);
 }

 function env() {
 var name = 'mm';
 echo();
 }

 env();

他的运行结果是:Skylor.min

结合上面的知识, 我们来看看下面这个例子,还记得那句JavaScript权威指南中的经典,JavaScript中的函数运行在它们被定义的作用域里,而不是它们被运行的作用域里。

function factory() {
 var name = 'Skylor.min';
 var intro = function(){
  alert('I am ' + name);
 }
 return intro;
 }

 function app(para){
 var name = para;
 var func = factory();
 func();
 }

 app('mm');

当调用app的时候, scope chain是由: {window活动对象(全局)}->{app的活动对象} 组成.

在刚进入app函数体时, app的活动对象有一个arguments属性, 其他俩个值为undefined的属性: name和func. 和一个值为'mm'的属性para;

此时的scope chain如下:

[[scope chain]] = [
 {
  para : 'mm',
  name : undefined,
  func : undefined,
  arguments : []
 }, {
  window call object
 }
 ]

 当调用进入factory的函数体的时候, 此时的factory的scope chain为:

[[scope chain]] = [
 {
  name : undefined,
  intor : undefined
 }, {
  window call object
 }
 ]

注意到, 此时的作用域链中, 并不包含app的活动对象.

在定义intro函数的时候, intro函数的[[scope]]为:

[[scope chain]] = [
 {
  name : 'Skylor.min',
  intor : undefined
 }, {
  window call object
 }
 ]

从factory函数返回以后,在app体内调用intor的时候, 发生了标识符解析, 而此时的sope chain是:

[[scope chain]] = [
 {
  intro call object
 }, {
  name : 'Skylor.min',
  intor : undefined
 }, {
  window call object
 }
 ]

 因为scope chain中,并不包含factory活动对象. 所以, name标识符解析的结果应该是factory活动对象中的name属性, 也就是'Skylor.min'.

所以运行结果是: I am Skylor.min

至此,完整的一个运行流程,很清晰的能读懂“JavaScript中的函数运行在它们被定义的作用域里,而不是它们被运行的作用域里。”这句话讲的是什么了。

为了解释上面的一些问题,还得说说JavaScript的预编译。

JavaScriptの预编译

预编译,学过C等的我们都知道,可是问题来了,JavaScript是脚本语言,JavaScript的执行过程是一种翻译执行的过程,那在JavaScript的执行中,有没有类似编译的过程呢?

如果不是很确定,先通过一个例子:

alert(typeof fun); //function
    function fun() {
        alert('I am Skylor.min');
    };
 

这时候弹出来的是?-----我去,是“I am Skylor.min”然而这时为什么呢,为啥不是undefined呢。

恩, 对, 在JS中, 是有预编译的过程的, JS在执行每一段JS代码之前, 都会首先处理var关键字和function定义式(函数定义式和函数表达式).

如上文所说, 在调用函数执行之前, 会首先创建一个活动对象, 然后搜寻这个函数中的局部变量定义,和函数定义, 将变量名和函数名都做为这个活动对象的同名属性, 对于局部变量定义,变量的值会在真正执行的时候才计算, 此时只是简单的赋为undefined.

而对于函数的定义,是一个要注意的地方:

alert(typeof fun); //结果:function
 alert(typeof fn); //结果:undefined
 function fun() { //函数定义式
 alert('I am Skylor.min');
 };
 var fn = function() { //函数表达式
 }
 alert(typeof fn); //结果:function

这就是函数定义式和函数表达式的不同, 对于函数定义式, 会将函数定义提前. 而函数表达式, 会在执行过程中才计算.

说到这里, 顺便说一个问题 :

    var name = 'Skylor.min';
    age = 25;
 

我们都知道不使用var关键字定义的变量, 相当于是全局变量, 联系到我们刚才的知识:

在对age做标识符解析的时候, 因为是写操作, 所以当找到到全局的window活动对象的时候都没有找到这个标识符的时候, 会在window活动对象的基础上, 返回一个值为undefined的age属性.

也就是说, age会被定义在顶级作用域中.

现在, 也许你注意到了我刚才说的: JS在执行每一段JS代码之前, 都会首先处理var关键字和function定义式(函数定义式和函数表达式).

对, 让我们看看下面的例子:

<script >
 alert(typeof mm); //结果:undefined
 </script >
 <script >
 function mm() {
  alert('I am Skylor.min');
 }
 </script >

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
jquery创建一个ajax关键词数据搜索实现思路
Feb 26 Javascript
js中的前绑定和后绑定详解
Aug 01 Javascript
详解jQuery插件开发中的extend方法
Nov 19 Javascript
JS中把字符转成ASCII值的函数示例代码
Nov 21 Javascript
IE7浏览器窗口大小改变事件执行多次bug及IE6/IE7/IE8下resize问题
Aug 21 Javascript
jquery捕捉回车键及获取checkbox值与异步请求的方法
Dec 24 Javascript
浅谈jQuery 选择器和dom操作
Jun 07 Javascript
理解javascript中的Function.prototype.bind的方法
Feb 03 Javascript
基于VUE选择上传图片并页面显示(图片可删除)
May 25 Javascript
基于datepicker定义自己的angular时间组件的示例
Mar 14 Javascript
Node绑定全局TraceID的实现方法
Nov 14 Javascript
ant design vue导航菜单与路由配置操作
Oct 28 Javascript
从理论角度讨论JavaScript闭包
Apr 03 #Javascript
Node.js+Express+Mysql 实现增删改查
Apr 03 #Javascript
微信小程序配置服务器提示验证token失败的解决方法
Apr 03 #Javascript
js前端面试之同步与异步问题详解
Apr 03 #Javascript
详解JavaScript 为什么要有 Symbol 类型?
Apr 03 #Javascript
es6 filter() 数组过滤方法总结
Apr 03 #Javascript
基于Vue 实现一个中规中矩loading组件
Apr 03 #Javascript
You might like
《Pokemon Sword·Shield》系列WEB动画《薄明之翼》第2话声优阵容公开!
2020/03/06 日漫
php curl登陆qq后获取用户信息时证书错误
2015/02/03 PHP
PHP判断一个字符串是否是回文字符串的方法
2015/03/23 PHP
Linux环境下php实现给网站截图的方法
2016/05/03 PHP
JavaScript 动态添加表格行 使用模板、标记
2009/10/24 Javascript
jQuery prev ~ siblings选择器使用介绍
2013/08/09 Javascript
JS和Jquery获取和修改label的值的示例代码
2014/01/15 Javascript
node.js中的fs.rename方法使用说明
2014/12/16 Javascript
分享2个jQuery插件--jquery.fileupload与artdialog
2014/12/26 Javascript
javascript实现全角与半角字符的转换
2015/01/07 Javascript
JavaScript实现模仿桌面窗口的方法
2015/07/18 Javascript
一步一步封装自己的HtmlHelper组件BootstrapHelper(二)
2016/09/14 Javascript
jQuery插件MovingBoxes实现左右滑动中间放大图片效果
2017/02/28 Javascript
nodejs multer实现文件上传与下载
2017/05/10 NodeJs
Vue实战之vue登录验证的实现代码
2017/10/31 Javascript
在 Linux/Unix 中不重启 Vim 而重新加载 .vimrc 文件的流程
2018/03/21 Javascript
详解如何构建一个Angular6的第三方npm包
2018/09/07 Javascript
js动态获取时间的方法分析
2019/08/02 Javascript
vue+elementUI实现图片上传功能
2019/08/20 Javascript
Vue父子之间值传递的实例教程
2020/07/02 Javascript
Vue中ref和$refs的介绍以及使用方法示例
2021/01/11 Vue.js
使用Python装饰器在Django框架下去除冗余代码的教程
2015/04/16 Python
ubuntu17.4下为python和python3装上pip的方法
2018/06/12 Python
python paramiko利用sftp上传目录到远程的实例
2019/01/03 Python
Python正则表达式实现简易计算器功能示例
2019/05/07 Python
TensorFlow索引与切片的实现方法
2019/11/20 Python
Python matplotlib读取excel数据并用for循环画多个子图subplot操作
2020/07/14 Python
阿迪达斯意大利在线商店:adidas意大利
2016/09/19 全球购物
一些高难度的SQL面试题
2016/11/29 面试题
kfc实习自我鉴定
2013/12/14 职场文书
会计专业自我鉴定范文
2013/12/29 职场文书
项目考察欢迎辞
2014/01/17 职场文书
3分钟英语演讲稿
2014/04/29 职场文书
三好学生个人先进事迹材料
2014/05/17 职场文书
就业协议书
2014/09/12 职场文书
党建工作目标管理责任书
2015/01/29 职场文书