JS基础学习——作用域

什么是作用域

变量的作用域就是变量能被访问到的代码范围,比如在下面的这个js代码中,变量a的作用域就是函数foo,因此在全局作用域内的console.log(a)语句不能访问到变量a,报ReferenceError错误。

function foo()
{
var a =3;
console.log(a);
}
foo();/*3*/
console.log(a);/*ReferenceError: a is not defined*/

作用域可以分为词法作用域和动态作用域两种类型。词法作用域也叫静态作用域,变量的作用域是在词法分析阶段确定的,由变量写在代码上的位置决定,与调用其的上下文无关。动态作用域中变量的作用域是由调用代码的堆栈决定的, 它在代码运行时确定。

比如下面这个例子,如果是词法作用域,bar1()和bar2()都会返回2,因为foo函数中变量a的作用域链为foo函数>全局;但如果是动态作用域的话,bar1()会返回3,bar2()会返回4,即foo函数中的a变量的值与执行环境上下文有关,由调用堆栈动态决定。

var a = 2;
function foo()
{
return a;
} function bar1()
{
var a = 3;
var b1 = foo();
return b1;
} function bar2()
{
var a = 4;
var b2 = foo();
return b2;
} bar1();/*return 2 or 3?*/
bar2();/*return 2 or 4?*/

JS的作用域

JS的作用域属于词法作用域,JS的作用域按作用域范围区分,可以分为全局作用域、函数作用域,且默认不包含块作用域,比如对于下面这段JS代码,因为不存在块作用域,if代码块内部申明的变量a是可以被语句console.log(a)访问到。但在某些特殊情况下,变量可以有块作用域,比如可以通过let、const、catch等关键词设置。

/*-----------code 1----------*/
if(true){
var a = 10;
}
console.log(a);/*10*/

全局作用域

不在函数内部申明的变量或是没有申明就直接使用的变量(非严格模式下),都会成为全局变量,拥有全局作用域,即网页上所有的语句都能访问到它。全局变量会自动成为浏览器全局对象window的属性,所以可以以“window.变量”的形式来引用变量,全局变量在页面关闭后才会消亡。

函数作用域

在函数内部用var声明的变量拥有函数作用域,变量只能被也在该函数内部的语句访问,且它在函数运行完之后立刻消亡。

函数作用域的作用(个人理解)

  1. 保证代码正确执行,若函数内部的变量能被外部语句访问到,变量的值容易被修改,导致运行结果出错;
  2. 允许在不同函数内部的函数变量重名,方便代码编写;
  3. 减少内存空间的占用;
  4. 隐藏函数内部实现,保证函数私有;

延伸阅读:全局变量和局部变量在内存中的区别

块作用域

有时候我们希望函数内部的部分代码块也能拥有自己的作用域,一方面可以提高代码的可读性更高,同时利用块作用域可以实现最小化变量的作用域,从而更加灵活的控制变量内存的占有和释放。生成块作用的方式如下。

  1. 立即执行函数表达IIFE(Immediately Invoking Function Expressions)

    立即执行函数表达IIFE形如(function (){ .. })(),第一个括号包裹函数表达,第二个括号表示立即执行函数,第二个括号里可以传递函数所需的参数。

    我们知道JS中函数定义的方式之一是函数表达式定义,包括匿名函数表达式定义和内联函数表达式定义,那么IIFE也有两种写法,匿名函数表达如下面code 2所示,内联函数表达如code 3所示,推荐使用第二种,它的好处是可以方便实现函数调用自己,同时代码的可读性比较高。

     /*-----------code 2----------*/
    var a = 2;
    (function (){
    var a = 3;
    console.log( a ); // 3
    })();
    console.log( a ); // 2 /*-----------code 3----------*/
    var a = 2;
    (function IIFE(){
    var a = 3;
    console.log( a ); // 3
    })();
    console.log( a ); // 2
  2. with关键词

    with关键词申明的块的最初目的是为了减少对同一对象的重复引用,方便代码的编写,但是with关键词还有一个特性就是with的包含块是一个独立的作用域,但是这个关键词已经被弃用了。

     /*-----------code 4----------*/
    var obj = {
    a: 1,
    b: 2,
    c: 3
    }; // more "tedious" to repeat "obj"
    obj.a = 2;
    obj.b = 3;
    obj.c = 4; // "easier" short-hand
    with (obj) {
    a = 3;
    b = 4;
    c = 5;
    }
  3. catch关键词

    从ES3开始定义catch的包含块是一个独立的块作用域,所以在code 4中,catch外面的console.log( a );是访问不到变量a的。

     /*-----------code 4----------*/
    try{
    throw 2;
    }catch(a){
    console.log( a ); // 2
    }
    console.log( a ); //ReferenceError: a is not defined
  4. let关键词

    ES6引入了let关键字用来为变量创建块作用域,它是代替var关键词的的一种新的变量申明方式。let关键词申明的变量的作用域为包含它的最小{...}内部,如code 5所示。

     /*-----------code 5----------*/
    {
    let i = 1;
    };
    console.log(i);//ReferenceError: i is not defined

    利用let关键字为变量指定现有块为作用域是为变量添加块作用域的隐式写法,这种写法容易混淆变量的作用域,在移动复制代码时容易发生错误,因此建议使用显式写法为变量增加块作用域,即用新的{}给出变量作用域,如code 6所示,这样写更容易进行代码的重构,保证代码语义正确。

     /*-----------code 6----------*/
    var foo = true;
    if (foo) {
    { // <-- explicit block
    let bar = foo * 2;
    bar = something( bar );
    console.log( bar );
    }
    }
    console.log( bar ); // ReferenceError

    for循环里用let声明循环变量时,循环变量的作用域不是整个for循环过程,而是每一次迭代都会重新生成一个新的循环变量,一次迭代结束之后,这个变量就会消亡,下一次迭代又会有一个新的同名循环变量生成。如code 7所示例子,code 8是它的等价代码。

     /*-----------code 7----------*/
    for (let i=0; i<10; i++) {
    console.log( i );
    }
    console.log( i ); // ReferenceError /*-----------code 8----------*/
    {
    let j;
    for (j=0; j<10; j++) {
    let i = j; // re-bound for each iteration!
    console.log( i );
    }
    console.log( i ); // ReferenceError
    }
  5. const关键词

    const也是ES6引入了一个新的关键字,const声明的变量会和let声明的变量一样拥有块作用域,除此之外,const声明的变量的值是不可改变的,任何想要修改const变量值的操作都会报错。

JS作用域延伸——变量、函数提升

JS代码中有一个神奇的现象,函数作用域或全局作用域中后申明的var变量和函数可以提前被调用,如code 9所示,这段代码等价于code 10,这种现象称为变量、函数提升,需要注意变量是只有其申明被提升,赋值以及其他可执行逻辑还是按照原先的顺序执行,函数是整体被提升,包括函数声明和函数内容。

/*-----------code 9----------*/
foo();
function foo() {
console.log( a ); // Undefined
var a = 2;
} /*-----------code 10----------*/
function foo() {//函数提升
var a; //变量提升
console.log( a ); // Undefined
a = 2;
}
foo();

变量、函数提升现象时由JS代码运行的内部原理决定的。JS是一种解释型语言,依靠JS引擎对代码进行实时解释执行,JS自上而下的执行过程包括两个阶段:编译和执行,在编译阶段进行形参分析、变量函数申明,在执行阶段再顺序执行脚本语言。因为JS引擎是先进行变量函数申明再执行脚本,因此所有变量就好像是提到了它所在范围的最前端进行执行了一样。

关于变量函数提升还有几点需要注意。

  1. 表达式中的函数定义是不会被提升的,它只遵循变量提升的原则,如code 11所示,它等价于code 12;

     /*-----------code 11----------*/
    foo(); // not ReferenceError, but TypeError!
    var foo = function bar() {
    // ...
    }; /*-----------code 12----------*/
    var foo;
    foo(); // not ReferenceError, but TypeError!
    foo = function bar() {
    // ...
    };
  2. 函数提升的优先级高于变量,即如果同时定义了两个同名的变量和函数,函数会覆盖变量,如code 13,最终会打印出1而不是显示TypeError,code 14是它的等价代码。

     /*-----------code 13----------*/
    foo(); // 1
    var foo;
    function foo() {
    console.log( 1 );
    }
    foo = function() {
    console.log( 2 );
    }; /*-----------code 14----------*/
    function foo() {
    console.log( 1 );
    }
    foo(); // 1
    foo = function() {
    console.log( 2 );
    };
  3. 两个同名的函数定义,后出现的函数定义会覆盖前面的函数定义。如code 15等价于code 16。

     /*-----------code 15----------*/
    foo(); // 3
    function foo() {
    console.log( 1 );
    }
    var foo = function() {
    console.log( 2 );
    };
    function foo() {
    console.log( 3 );
    } /*-----------code 16----------*/
    function foo() {
    console.log( 1 );
    }
    function foo() {
    console.log( 3 );
    }
    foo(); // 3
    foo = function() {
    console.log( 2 );
    };
  4. JS默认只有函数作用域,因此代码块(if、for等)里面的函数定义也会被提升,如code 18等价于code 17。

     /*-----------code 17----------*/
    
     foo(); // "b"
    var a = true;
    if (a) {
    function foo() { console.log("a"); }
    }
    else {
    function foo() { console.log("b"); }
    } /*-----------code 17----------*/ function foo() { console.log("a"); }
    function foo() { console.log("b"); }
    foo(); // "b"
    var a = true;
    if (a) {
    }
    else {
    }
  5. let、const关键词声明的变量不存在变量提升现象。

延伸阅读:解释型语言和编译型语言的区别

由于计算机无法直接执行高级语言,它只能识别二进制的机器码,因此高级语言一定要翻译成机器语言计算机才能识别运行。语言翻译的方式有两种:编译和解释,两者的主要区别是翻译语言的时间不同。按照语言翻译方式的不同,高级语言被分为编译型语言和解释型语言。

编译型语言需要在执行代码之前通过编译器将代码翻译成机器语言,生成.exe可执行二进制代码,依次编译可多次执行,因此执行效率比较高。比如:C/C++、Delphi、Pascal、Fortran。

解释型语言不需要提前编译,在执行的时候再通过解释器进行语言的翻译,因为每次执行都需要重新尽心语言的翻译,因此解释型语言的效率比较慢;但解释型语言的跨平台性比较好,只要在特定的平台安装对应的解释型就能运行代码。如JAVA、Basic、javascrip、Python。

JS作用域延伸——变量查询

上面提到,JS自上而下的执行过程包括两个阶段:编译和执行,在执行阶段大部分的语句都涉及变量查询,因此了解变量查询的概念是重要的。

根据查找的内容不同变量查询可分为LHS(left-Hand-Side)查询和RHS(right-Hand-Side)查询。根据字面理解,LHS表示要查找的变量处于“=”的左边,RHS表示要查找的变量处于“=”的右边。更加准确的说,LHS要查找的是存在变量的内存地址,RHS要查找的是变量的值。

变量查询的范围是变量的作用域链,作用域链由包含变量的从内到外的多个作用域组成(函数\块作用域>函数\块作用域>...>全局域),按顺序依次查找从内到外每个作用域,知道找到变量。

参考资料:

[1] You don't know js -- Scope & Closures

[2] 深入理解javascript作用域系列第一篇——内部原理

[3] 深入理解javascript作用域系列第二篇——词法作用域和动态作用域

[4] 深入理解javascript作用域系列第三篇——声明提升(hoisting)

[5] 深入理解javascript作用域系列第四篇——块作用域

[6] 脚本语言、编译性语言和解释性语言的区别

[7] 什么是脚本语言?什么是解释性语言?什么是编译性语言?

JS基础学习——作用域的更多相关文章

  1. JS基础学习——闭包

    JS基础学习--闭包 什么是闭包 闭包的定义如下,它的意思是闭包使得函数可以记住和访问它的词法范围,即使函数是在它声明的词法范围外执行.更简单来讲,函数为了自己能够正确执行,它对自己的词法范围产生闭包 ...

  2. JS基础学习——对象

    JS基础学习--对象 什么是对象 对象object是JS的一种基本数据类型,除此之外还包括的基本数据类型有string.number.boolean.null.undefined.与其他数据类型不同的 ...

  3. JS 基础学习随想

    2012年就已经接触过了js,给我的印象:这是一门谈不上复杂的语言.大概这就是所谓的学的越浅,用的越少,觉得自己会的东西好像得更多吧!开始做基础练习题的时候觉得好像都十分简单.可是后来在做到对象数组的 ...

  4. handlebars.js基础学习笔记

    最近在帮学校做个课程网站,就有人推荐用jquery+ajax+handlebars做网站前端,刚接触发现挺高大上的,于是就把一些基础学习笔记记录下来啦. 1.引用文件: jquery.js文件下载:h ...

  5. JS基础学习1

    1 JS 概述 一个完整的javascript实现是由以下3个不同部分组成的: (1)     核心(ECMAscript) (2)     文档对象模型(DOM)  Document object ...

  6. JS基础学习篇(一)

    近来一直在学习js和jquery.刚刚进入前端工作还没有多久,虽然大学里学习的是编程自认为也学的还可以,但前端接触的不多,一直认为前端十分简单.其实不然,特别是工作的时候要自己设计一个完整的项目前端, ...

  7. 两万字Vue.js基础学习笔记

    Vue.js学习笔记 目录 Vue.js学习笔记 ES6语法 1.不一样的变量声明:const和let 2.模板字符串 3.箭头函数(Arrow Functions) 4. 函数的参数默认值 5.Sp ...

  8. Node.js基础学习四之注册功能

    前言:在Node.js学习(二)和(三)中介绍了如何在Node.js 中获取登录的用户名和密码与数据库进行验证并返回数据给客户端 需求:实现注册功能 为了区分登录和注册是两个不同的请求,在端口后面加上 ...

  9. JS基础学习第五天

    作用域 作用域简单来说就是一个变量的作用范围.在JS中作用域分成两种: 1.全局作用域 直接在script标签中编写的代码都运行在全局作用域中全局作用域在打开页面时创建,在页面关闭时销毁.全局作用域中 ...

随机推荐

  1. 181114socke编程

    一.Socket Families 地址簇 socket.AF_UNIX socket.AF_INET socket.AF_INET6 二.Socker Types socket.SOCK_STREA ...

  2. Foremost恢复Linux中已删除的文件

    Foremost 我们只能在Linux中恢复已删除的文件,只要这些扇区在硬盘上没有被覆盖. 首先安装 要在CentOS上安装Foremost,我们将从官方网页下载并安装最前面的rpm.打开终端并执行以 ...

  3. Mysql Update更新错误 Error Code:1175

    Mysql 5.7,默认执行 update 语句时遇到错误提示: Error Code: 1175. You are using safe update mode and you tried to u ...

  4. 【算法笔记】B1042 字符统计

    1042 字符统计 (20 分) 请编写程序,找出一段给定文字中出现最频繁的那个英文字母. 输入格式: 输入在一行中给出一个长度不超过 1000 的字符串.字符串由 ASCII 码表中任意可见字符及空 ...

  5. 【算法笔记】B1048 数字加密

    1048 数字加密 (20 分) 本题要求实现一种数字加密方法.首先固定一个加密用正整数 A,对任一正整数 B,将其每 1 位数字与 A 的对应位置上的数字进行以下运算:对奇数位,对应位的数字相加后对 ...

  6. 前端页面 script 事件总结

    1. input  失去焦点是触发事件 $("#sn").blur(function(){ alert("sdfasdf"); }); 2.  获取文本框中的 ...

  7. JedisPool

    package redis; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis. ...

  8. nodejs下载器,通过chrome代理下载http资源

    var config={ //不想访问的东西,节约流量 "404":[ "http://qidian.qpic.cn/qdbimg" ], //奇数为需要下载的 ...

  9. java中的线程(4):常用同步类 CountDownLatch、CyclicBarrier和Semaphore

    转自: http://www.cnblogs.com/dolphin0520/p/3920397.html 1.简介 CountDownLatch和CyclicBarrier都能够实现线程之间的等待, ...

  10. Linux mmap 要主动释放共享内存

    #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/sta ...