全面了解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实现在页面加载完毕后获取图片高度或宽度
Jun 16 Javascript
js实现拉幕效果的广告代码
Sep 02 Javascript
jQuery遍历json的方法(推荐)
Jun 12 Javascript
详解vue.js的事件处理器v-on:click
Jun 27 Javascript
详解Chart.js轻量级图表库的使用经验
May 22 Javascript
Angular 利用路由跳转到指定页面的指定位置方法
Aug 31 Javascript
vue中利用simplemde实现markdown编辑器(增加图片上传功能)
Apr 29 Javascript
微信小程序tabBar 返回tabBar不刷新页面
Jul 25 Javascript
JS使用H5实现图片预览功能
Sep 30 Javascript
微信小程序实现时间进度条功能
Nov 17 Javascript
javascript中的with语句学习笔记及用法
Feb 17 Javascript
JavaScript, select标签元素左右移动功能实现
May 14 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
ThinkPHP中的常用查询语言汇总
2014/08/22 PHP
深入理解PHP之源码目录结构与功能说明
2016/06/01 PHP
PHP判断数组是否为空的常用方法(五种方法)
2017/02/08 PHP
载入进度条 效果
2006/07/08 Javascript
js 通用javascript函数库整理
2011/08/14 Javascript
Javascript四舍五入Math.round()与Math.pow()使用介绍
2013/12/27 Javascript
用Jquery.load载入页面后样式没了页面混乱的解决方法
2014/10/20 Javascript
jQuery插件imgPreviewQs实现上传图片预览
2016/01/15 Javascript
利用JS屏蔽页面中的Enter按键提交表单的方法
2016/11/25 Javascript
bootstrap3使用bootstrap datetimepicker日期插件
2017/05/24 Javascript
详解如何在vue中使用sass
2017/06/21 Javascript
基于JS脚本语言的基础语法详解
2017/07/22 Javascript
JavaScript实现简单的图片切换功能(实例代码)
2020/04/10 Javascript
js实现滑动进度条效果
2020/08/21 Javascript
Python实现合并字典的方法
2015/07/07 Python
Python解析json之ValueError: Expecting property name enclosed in double quotes: line 1 column 2(char 1)
2017/07/06 Python
Python 实现选择排序的算法步骤
2018/04/22 Python
python 对key为时间的dict排序方法
2018/10/17 Python
python requests 库请求带有文件参数的接口实例
2019/01/03 Python
Python面向对象之继承和多态用法分析
2019/06/08 Python
Python进阶之使用selenium爬取淘宝商品信息功能示例
2019/09/16 Python
Python 依赖库太多了该如何管理
2019/11/08 Python
Python语言异常处理测试过程解析
2020/01/08 Python
pytorch实现mnist分类的示例讲解
2020/01/10 Python
Python多线程正确用法实例解析
2020/05/30 Python
使用Python实现微信拍一拍功能的思路代码
2020/07/09 Python
杭州联环马网络笔试题面试题
2013/08/04 面试题
什么是托管函数?托管函数有什么用?
2014/06/15 面试题
售后服务科岗位职责范文
2013/11/13 职场文书
新闻学专业大学生职业生涯规划范文
2014/03/02 职场文书
工作决心书范文
2014/03/11 职场文书
信息工作经验交流材料
2014/05/28 职场文书
先进单位申报材料
2014/12/25 职场文书
win10+anaconda安装yolov5的方法及问题解决方案
2021/04/29 Python
Python中的datetime包与time包包和模块详情
2022/02/28 Python
CentOS7安装GlusterFS集群以及相关配置
2022/04/12 Servers