全面了解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 相关文章推荐
用javascript实现给出的盒子的序列是否可连为一矩型
Aug 30 Javascript
JQuery 表格操作(交替显示、拖动表格行、选择行等)
Jul 29 Javascript
JQuery onload、ready概念介绍及使用方法
Apr 27 Javascript
js添加事件的通用方法推荐
May 15 Javascript
Vue表单实例代码
Sep 05 Javascript
javascript数据结构中栈的应用之符号平衡问题
Apr 11 Javascript
基于vue 开发中出现警告问题去除方法
Jan 25 Javascript
微信小程序ibeacon三点定位详解
Oct 31 Javascript
VUE+Element UI实现简单的表格行内编辑效果的示例的代码
Oct 31 Javascript
vue路由守卫及路由守卫无限循环问题详析
Sep 05 Javascript
vue+webpack 更换主题N种方案优劣分析
Oct 28 Javascript
JavaScript中作用域链的概念及用途讲解
Aug 06 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
PHP使用者状态管理功能的应用
2006/10/09 PHP
基于mysql的论坛(3)
2006/10/09 PHP
PHP 5.3新特性命名空间规则解析及高级功能
2010/03/11 PHP
解析array splice的移除数组中指定键的值,返回一个新的数组
2013/07/02 PHP
PHP error_log()将错误信息写入一个文件(定义和用法)
2013/10/25 PHP
php输出1000以内质数(素数)示例
2014/02/16 PHP
学习php设计模式 php实现适配器模式
2015/12/07 PHP
php官方微信接口大全(微信支付、微信红包、微信摇一摇、微信小店)
2015/12/21 PHP
PHP实现的下载远程文件类定义与用法示例
2017/07/05 PHP
js获得地址栏?问号后参数的方法
2013/08/08 Javascript
javascript数组去重的六种方法汇总
2015/08/16 Javascript
JavaScript学习小结(7)之JS RegExp
2015/11/29 Javascript
require、backbone等重构手机图片查看器
2016/11/17 Javascript
angular2中router路由跳转navigate的使用与刷新页面问题详解
2017/05/07 Javascript
ES6中javascript实现函数绑定及类的事件绑定功能详解
2017/11/08 Javascript
总结JavaScript在IE9之前版本中内存泄露问题
2018/04/28 Javascript
javascript显示动态时间的方法汇总
2018/07/06 Javascript
微信小程序公用参数与公用方法用法示例
2019/01/09 Javascript
JS中getElementsByClassName与classList兼容性问题解决方案分析
2019/08/07 Javascript
JS严格模式原理与用法实例分析
2020/04/27 Javascript
[00:36]我的中国心——Serenity vs Fnatic
2018/08/21 DOTA
Python中的集合类型知识讲解
2015/08/19 Python
python结合API实现即时天气信息
2016/01/19 Python
Python 实现一个颜色色值转换的小工具
2016/12/06 Python
用yum安装MySQLdb模块的步骤方法
2016/12/15 Python
python中pathlib模块的基本用法与总结
2020/08/17 Python
Python爬虫自动化获取华图和粉笔网站的错题(推荐)
2021/01/08 Python
Python环境搭建过程从安装到Hello World
2021/02/05 Python
突袭HTML5之Javascript API扩展3—本地存储全新体验
2013/01/31 HTML / CSS
Lowe’s加拿大:家居装修、翻新和五金店
2019/12/06 全球购物
机电一体化应届生求职信范文
2014/01/24 职场文书
村干部四风问题整改措施
2014/09/30 职场文书
党员查摆问题及整改措施
2014/10/10 职场文书
​(迎国庆)作文之我爱我的祖国
2019/09/19 职场文书
Go语言 详解net的tcp服务
2022/04/14 Golang
Win10玩csgo闪退如何解决?Win10玩csgo闪退的解决方法
2022/07/23 数码科技