通过阅读《JS高级程序设计》这本书,对js中的作用域和作用域链知识有了初步的了解和认识,准备成笔记供大家参考,笔记中字数比较多,但个人认为叙述的挺详细的,所以希望读者耐心看。再者,本人了解的比较基础,不足的地方希望大家一起交流,共同学习。

1.执行环境(execution context)

  执行环境定义了变量和函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有与之对应的变量对象(variable object),保存着该环境中定义的所有变量和函数。我们无法通过代码来访问变量对象,但是解析器在处理数据时会在后台使用到它。

  执行环境有全局执行环境(也称全局环境)和函数执行环境之分。执行环境如其名是在运行和执行代码的时候才存在的,所以我们运行浏览器的时候会创建全局的执行环境,在调用函数时,会创建函数执行环境。

1.1 全局执行环境

  全局执行环境是最外围的一个执行环境,在web浏览器中,我们可以认为他是window对象,因此所有的全局变量和函数都是作为window对象的属性和方法创建的。代码载入浏览器时,全局环境被创建,关闭网页或者关闭浏览时全局环境被销毁。

1.2 函数执行环境

  每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就被推入一个环境栈中,当函数执行完毕后,栈将其环境弹出,把控制权返回给之前的执行环境。

2 作用域、作用域链

2.1 全局作用域(globe scope)和局部作用域(local scope)

  全局作用域可以在代码中的任何地方都能被访问,例如:

  var name1="haha";
function changName(){
var name2="xixi";
console.log(name1); // haha
console.log(name2);// xixi
}
changName();
console.log(name1);//haha
console.log(name2);//Uncaught ReferenceError: name2 is not defined

  其中,name1具有全局作用域,因此在第4行和第8行都会在控制台上输出 haha。name2定义在changName()函数内部,具有局部作用域,因此在第9行,解析器找不到变量name2,抛出错误。
  另外,在函数中声明变量时,如果省略 var 操作符,那么声明的变量就是全局变量,拥有全局作用域,但是不推荐这种做法,因为在局部作用域中很难维护定义的全局变量。

  再者,window对象的内置属性都拥有全局作用域。

  局部作用域一般只在固定的代码片段内可以访问得到,例如上述代码中的name2,只有在函数内部可以访问得到。

 2.2 作用域链(scope chain)

  全局作用域和局部作用域中变量的访问权限,其实是由作用域链决定的。

  每次进入一个新的执行环境,都会创建一个用于搜索变量和函数的作用域链。作用域链是函数被创建的作用域中对象的集合。作用域链可以保证对执行环境有权访问的所有变量和函数的有序访问。

  作用域链的最前端始终是当前执行的代码所在环境的变量对象(如果该环境是函数,则将其活动对象作为变量对象),下一个变量对象来自包含环境(包含当前还行环境的环境),下一个变量对象来自包含环境的包含环境,依次往上,直到全局执行环境的变量对象。全局执行环境的变量对象始终是作用域链中的最后一个对象。

  标识符解析是沿着作用域一级一级的向上搜索标识符的过程。搜索过程始终是从作用域的前端逐地向后回溯,直到找到标识符(找不到,就会导致错误发生)。

例如:

 var name1 = "haha";
function changeName(){
var name2="xixi";
function swapName(){
console.log(name1);//haha
console.log(name2);//xixi
var tempName=name2;
name2=name1;
name1=tempName;
console.log(name1);//xixi
console.log(name2);//haha
console.log(tempName);//xixi
}
swapName();
console.log(name1);//haha
console.log(name2);//xixi
//console.log(tempName);抛出错误:Uncaught ReferenceError: tempName is not defined
}
changName();
console.log(name1);
//console.log(name2); 抛出错误:Uncaught ReferenceError: name2 is not defined
//console.log(tempName);抛出错误:Uncaught ReferenceError: tempName is not defined

运行结果如下:

上述代码中,一共有三个执行环境:全局环境、changeName()的局部环境和 swapName() 的局部环境。所以,

 1.函数 swapName()的作用域链包含三个对象:自己的变量对象----->changeName()局部环境的变量对象 ----->全局环境的变量对象

 2.函数changeName()的作用域包含两个对象:自己的变量对象----->全局环境的变量对象。

就上述程序中出现的变量和函数来讲(不考虑隐形变量):

 1.swapName() 局部环境的变量对象中存放变量 tempName;

 2.changeName() 局部环境的变量对象中存放变量 name2 和 函数swapName();

 3.全局环境的变量对象中存放变量 name1 、函数changeName();

  在swapName()的执行环境中,在执行第5句代码时,解析器沿着函数 swapName()的作用域链一级级向后回溯查找变量 name1,直到在全局环境中找到变量 name1.并输出在控制台上。同样,在执行第6句代码时,解析器沿着函数 swapName()的作用域链一级级向后回溯,在函数changeName()的变量对象中发现变量 name2.通过代码对 name1 和 name2进行交换,并输出在控制台上,根据结果我们发现,这两个变量的值确实交换了。因此我们可以得出结论,函数的局部环境可以访问函数作用域中的变量,也可以访问和操作父环境(包含环境)乃至全局环境中的变量。

  在changeName() 的执行环境中,执行第15行和第16行代码时,可以正确地输出  name1 和 name2 和两个变量的值(调用了函数swapName(),所以俩变量的值已相互交换),那是因为 name1 在changName()的父环境(全局环境)中, name2 在他自己的局部环境中,即 name1 和 name2 都在其作用域链上。但当执行第17行代码是发生错误 tempName is not defined。因为解析器沿着 函数changeName()的作用域链一级级的查找 变量 tempName时,并不能找到该变量的存在(变量 tempName不在其作用域链上),所以抛出错误。因此,我们可以得出结论:父环境只能访问其包含环境和自己环境中的变量和函数,不能访问其子环境中的变量和函数。

  同理,在全局环境中,其变量对象中只存放变量 name1 、函数changeName();  解析器只能访问变量 name1 和函数 changeName(), 而不能访问和操作 函数 changeName() 和函数 swapName() 中定义的变量或者函数。因此,在执行第21行和第22行代码时抛出变量没有定义的错误。所以说,全局环境只能访问全局环境中的变量和函数,不能直接访问局部环境中的任何数据。

  其实,我们可以把作用域链想象成这样(里面的能访问外面的,外面的不能访问里面的,图为参考):

  

作用域链相关知识的总结:

1.执行环境决定了变量的生命周期,以及哪部分代码可以访问其中变量

2,执行环境有全局执行环境(全局环境)和局部执行环境之分。

3.每次进入一个新的执行环境,都会创建一个用于搜索变量和函数的作用域链

4.函数的局部环境可以访问函数作用域中的变量和函数,也可以访问其父环境,乃至全局环境中的变量和环境。

5.全局环境只能访问全局环境中定义的变量和函数,不能直接访问局部环境中的任何数据。

6.变量的执行环境有助于确定应该合适释放内存。

3.提升(hoisting)

提升有变量提升和函数提升之分,下面我们依次介绍他们。

3.1 变量提升(variable hoisting)

请看一下代码:

  var name="haha";
function changeName(){
console.log(name);
var name="xixi";
}
changeName();
console.log(name);

大家认为第6行和第7行代码输出的结果应该是什么?好了,答案是:输出结果结果分别是 undefined  haha。为什么是undefined? 按照作用域链的思路思考,输出的结果应该是 haha或者xixi啊? (当然大家都知道 xixi是不可能的,因为解析器在解析第3行代码时,还不知道第4行中的赋值内容)。

  那我们先来分析一下代码 函数changeName() 的作用域链: 自己的变量对象 -----> 全局变量对象。解析器在函数执行环境中发现变量 name,因此不会再向全局环境的变量对象中寻找。但是大家要注意的是,解析器在解析第3句代码时,还不知道变量name的值,也就是说只知道有变量name,但是不知道它具体的值(因为还没有执行第4句代码),因此输出是 undefined,第7行输出haha大家应该都理解把(作用域问题)。所以上述代码可以写成下面的形式:

1   var name="haha";
2 function changeName(){
3 var name;
4 console.log(name);
5 name="xixi";
6 }
7 changeName();
8 console.log(name);

这个现象就是变量提升

  变量提升,就是把变量提升到函数的顶部,需要注意的是,变量提升只是提升变量的声明,不会吧变量的值也提升上来!见上述代码,最常见的代码如下,函数example1()和函数example2()是等价的:

 function example1(){
var a="haha";
var b="xixi";
var c="heihei";
} function example2(){
var a,b,c;
a="haha";
b="xixi";
c="heihei";
}

3.2 函数提升()

  函数提升就是把函数提升到前面。

  在JavaScript中函数的创建方式有三种:函数声明(静态的,像函数example1()的形式)、函数表达式(函数字面量)、函数构造法(动态的,匿名的)。函数表达式的形式如下:

 var func1 = function(n1,n2){
//function body;
};

函数构造法构造函数的形式如下:

 var func2 = new Function("para1","para2",...,"function body");    

在这里需要说明的是:只有函数声明形式才能被提升!例如:

//函数声明
function myTest1(){
func();
function func(){
console.log("我可以被提升");
}
}
myTest1(); //函数表达式
function myTest2(){
func();
var func = function(){
console.log("我不能被提升");
}
}
myTest2();

控制台显示结果如下:

以上就是该文章所讲述的东西,欢迎大家批评指正!

JavaScript中作用域和作用域链的简单理解(变量提升)的更多相关文章

  1. Javascript中闭包的作用域链

    作用域定义了在当前上下文中能够被访问到的成员,在Javascript中分为全局作用域和函数作用域,通过函数嵌套可以实现嵌套作用域. 闭包一般发生在嵌套作用域中.闭包是JavaScript最强大的特性之 ...

  2. javascript中的this作用域详解

    javascript中的this作用域详解 Javascript中this的指向一直是困扰我很久的问题,在使用中出错的机率也非常大.在面向对象语言中,它代表了当前对象的一个引用,而在js中却经常让我觉 ...

  3. 从问题入手,深入了解JavaScript中原型与原型链

    从问题入手,深入了解JavaScript中原型与原型链 前言 开篇之前,我想提出3个问题: 新建一个不添加任何属性的对象为何能调用toString方法? 如何让拥有相同构造函数的不同对象都具备相同的行 ...

  4. javascript中的函数作用域和声明提前

    在一些类C的编程语言中,花括号内的每一段代码都具有各自作用域,并且变量在声明他们的代码段之外是不可见的,这个概念叫做块级作用域. javascript中没有块级作用域的概念,有的是函数作用域的概念:变 ...

  5. javascript中闭包与作用域的理解

    很多js的框架与插件编写都用到了闭包,所以,阅读和掌握闭包很有必要.最近学习vue框架时,经常会猜想很多功能的native js实现,很多都应用到了闭包,闭包除了目前已知的一些特性,如:可以保持局部变 ...

  6. javascript 中的 let 作用域

    let 声明了一个块级域的局部变量,并且可以给它一个初始化值. 语法EDIT let var1 [= value1] [, var2 [= value2]] [, ..., varN [= value ...

  7. Javascript中的词法作用域、动态作用域、函数作用域和块作用域(四)

    一.js中的词法作用域和动态作用域      词法作用域也就是在词法阶段定义的作用域,也就是说词法作用域在代码书写时就已经确定了.       js中其实只有词法作用域,并没有动态作用域,this的执 ...

  8. javascript中继承(一)-----原型链继承的个人理解

    [寒暄]好久没有更新博客了,说来话长,因为我下定决心要从一个后台程序员转为Front End,其间走过了一段漫长而艰辛的时光,今天跟大家分享下自己对javascript中原型链继承的理解. 总的说来, ...

  9. 对JavaScript中局部变量、全局变量和闭包的理解

    对js中局部变量.全局变量和闭包的理解 局部变量 对于局部变量,js给出的定义是这样的:在 JavaScript函数内部声明的变量(使用 var)是局部变量,所以只能在函数内部访问它.(该变量的作用域 ...

随机推荐

  1. grafana-Admin密码重置

    1)查看Grafana配置文件,确定grafana.db的路径 [paths] ;data = /var/lib/grafana [database] # For "sqlite3" ...

  2. JAVA传递带有空格的参数

    String s="b2 + b1"; Process child = Runtime.getRuntime().exec("C:\\eclipse-workspace\ ...

  3. MySQL学习4 - 数据类型一

    介绍 一.数值类型 二.浮点型 验证三种类型建表 验证三种类型的精度 三.日期类型 综合练习: 介绍 存储引擎决定了表的类型,而表内存放的数据也要有不同的类型,每种数据类型都有自己的宽度,但宽度是可选 ...

  4. 401 experience

    AM: 块元素与内联元素  : div与span的区别 span只能设置水平的margin(左右内外边距) 在span里面加 display:block; 内联转块(相当于给span加了上下的边距)反 ...

  5. Spring系列(三) Bean装配的高级技术

    profile 不同于maven的profile, spring的profile不需要重新打包, 同一个版本的包文件可以部署在不同环境的服务器上, 只需要激活对应的profile就可以切换到对应的环境 ...

  6. Windows下的wget,命令行下载url

    1.进命令行(Win + R,输入"cmd") 2.输入:start powershell 3.等待PowerShell窗口启动 4.PowerShell窗口依次输入: $clie ...

  7. 可持久化 trie 的简单入门

    可持久化 $trie$  ....又是一个表里不一的东西..... 可持久化 $trie$  的介绍: 和主席树类似的,其实可持久化就是体现在前缀信息的维护上(搞不懂这怎么就叫做可持久化了...) $ ...

  8. mysql join on and

    2018-6-4 10:28:50 星期一 开发中一直在用 left join, 心中只有一丝丝的了解, 还都是学校里学的, 今天看了几遍文章这里记录一下 sql的left join .right j ...

  9. Python--可迭代对象,迭代器,生成器

    记得在刚开始学Python的时候,看到可迭代对象(iterable).迭代器(iterator)和生成器(generator)这三个名词时,完全懵逼了,根本就不知道是啥意识.现在以自己的理解来详解下这 ...

  10. svn忽略不需要同步的文件夹或文件

    如果某个文件已经提交到了svn,这个时候需要通过svn来把服务器上的改文件删除,然后再在本地,点击该文件 选择把该文件删除,recursively表示递归删除(文件下->下级文件夹->下级 ...