JS基础学习——作用域

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] 什么是脚本语言?什么是解释性语言?什么是编译性语言?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: JavaScript函数是一种可重复使用的代码块,它可以接受输入参数并返回值。函数可以用来组织代码、减少重复代码、提高代码的可读性和可维护性。在JavaScript中,函数可以通过函数声明、函数表达式和箭头函数来定义。函数可以有任意数量的参数,也可以没有参数。函数可以返回任何类型的值,包括数字、字符串、布尔值、对象和函数等。函数可以在全局作用域或局部作用域中定义,也可以作为对象的方法来定义。在JavaScript中,函数是一等公民,可以作为参数传递给其他函数,也可以作为返回值返回给其他函数。 ### 回答2: JavaScript函数是一种非常重要的概念,它允许我们定义一段可重复使用的代码片段,同时还可以让我们更好的组织和管理程序的结构。在JavaScript中,函数可以作为一个值进行处理,可以使用函数表达式或函数声明定义。 函数声明是定义函数的最简单方式,它使用“function”关键字和函数名称来声明一个函数,例如: ``` function greeting() { console.log("Hello World"); } ``` 这里定义了一个简单的函数,功能是输出“Hello World”到控制台。我们可以通过使用函数名来调用它: ``` greeting(); ``` 输出结果为“Hello World”。 除了函数声明外,还有一种常见的函数定义方式——函数表达式。函数表达式是定义一个函数并将其赋值给一个变量,例如: ``` var greeting = function() { console.log("Hello World"); }; ``` 这里定义了一个变量greeting,并将一个匿名函数赋值给它。我们可以通过使用变量名来调用该函数: ``` greeting(); ``` 输出结果为“Hello World”。 JavaScript函数支持一个特殊的参数对象——arguments对象,它可以让我们在函数内部访问到所有传递给该函数的参数。例如: ``` function sum() { var result = 0; for (var i = 0; i < arguments.length; i++) { result += arguments[i]; } return result; } ``` 这里定义了一个函数sum,它可以计算任意数量的参数的和。我们可以使用以下代码来调用该函数并输出结果: ``` console.log(sum(1, 2, 3, 4)); //输出10 console.log(sum(10, 20)); //输出30 ``` 总的来说,JavaScript函数是一项非常重要的技能,它可以帮助我们更好的组织和管理我们的程序,并且可以帮助我们构建可重复使用的代码片段。掌握JavaScript函数,是掌握JavaScript编程的重要一步。 ### 回答3: JavaScript是目前世界上最流行的动态编程语言之一,已成为很多网站和Web应用程序的必选之一。JavaScript的函数是编写高度可重复使用的代码和组织逻辑的基本组成部分,也是JavaScript编程的核心。随着Web应用场景的复杂化,函数在JavaScript编程中的重要性越来越得到了认识和应用,学习和掌握JavaScript的函数是很有必要的。 JavaScript函数与其他编程语言中的函数类似,是一个代码块,可以接受参数,返回值,并可重复使用。JavaScript函数有两种常用的定义方式,分别是函数声明和函数表达式。函数声明是指在代码中提前声明一个函数,函数表达式是指将一个函数赋值给一个变量或常量。 在JavaScript函数中,参数和返回值都是可以选择性的。一个函数可以不需要参数也不需要返回值,也可以仅需要参数或仅需要返回值。使用参数可以让函数更加灵活和通用,参数可以是任何类型,包括字符串、数字、函数和对象等。返回值可以是任何JavaScript支持的类型,包括布尔类型、数字类型、字符串类型和数组等。 函数的作用域JavaScript函数编程中的一个关键概念,它指的是一段代码能够访问和操作的变量和函数的范围。JavaScript函数有词法作用域,即变量的作用域取决于它们在代码中的位置,作用范围被限定在函数内部和它所嵌套的函数中。 常用的JavaScript函数应用包括数组和对象的处理、事件处理和DOM操作等。在Web开发中,JavaScript函数和jQuery库的结合应用很广泛,它可以极大地提高Web应用的交互性和用户体验。 总之,JavaScript函数是一个必须掌握的编程概念,它可以让我们的代码更加灵活、通用、数据安全、易使用和易维护。了解JavaScript函数的语法和应用场景,能够有效地提高JavaScript编程的效率和质量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值