黄子涵

6.4 作用域

作用域指的是名称(变量名与函数名)的有效范围。

在 JavaScript 中有以下两种作用域。

  • 全局作用域
  • 函数作用域

全局作用域是函数之外(最外层代码)的作用域。在函数之外进行声明的名称属于全局作用域。这些名称就是所谓的全局变量以及全局函数。

而在函数内进行声明的名称拥有的是函数作用域,它们仅在该函数内部才有效。相对于全局作用域,可以将其称为局部作用域;相对于全局变量,又可以将其称为局部变量。作为函数形参的参数变量也属于函数作用域。

JavaScript 的函数作用域的机制,与 Java(以及其他很多的程序设计语言)中的局部作用域有着微妙的差异。在 Java 中,局部变量所具有的作用域是从方法内对该变量进行声明的那一行开始的;而在JavaScript 中,函数作用域与进行声明的行数没有关系。

请看代码清单6.3的例子。

代码清单6.3 函数作用域的注意事项

var hzh1 = 1;
function hzh() {
    // 对变量 x 进行访问
    console.log('hzh1 = ' + hzh1);
    var hzh2 = 2;
    // 对变量 x 进行访问
    console.log('hzh2 = ' + hzh2);
}
hzh();
[Running] node "e:\HMV\JavaScript\JavaScript.js"
hzh1 = 1
hzh2 = 2

[Done] exited with code=0 in 0.311 seconds

【评】这里和书上的结果不一样,标记一下。

乍一看,会认为函数 hzh 内的第一个 console.log() 显示的是全局变量 hzh1。然而,这里的 hzh1 是在下一行进行声明的局部变量 hzh1。这是因为,局部变量 hzh1 的作用域是整个函数 hzh 内部。由于此时还没有对其进行赋值,因此变量 hzh1 的值为 undefined 值。也就是说,函数 hzh 与下面的代码是等价的。

function HZH() {
    var hzh3;
    console.log('hzh3 = ' + hzh3);
    hzh3 = 3;
    console.log('hzh3 = ' + hzh3);
}
HZH();
[Running] node "e:\HMV\JavaScript\JavaScript.js"
hzh3 = undefined
hzh3 = 3

[Done] exited with code=0 in 0.551 seconds

【评】这里的实验结果和书上的不太一样,标记一下,注释是书上的原文。

代码清单 6.3 中的代码非常不易于理解,常常是发生错误的原因。因此,我们建议在函数的开始处对所有的局部变量进行声明。

Java 等语言建议直到要使用某一变量时才对其进行声明,不过JavaScript 则有所不同,对此请加以注意。

6.4.1 浏览器与作用域

在客户端 JavaScript 中,各个窗口(标签)、框架(包括 iframe)都有其各自的全局作用域。在窗口之间是无法访问各自全局作用域中的名称的,但父辈与其框架之间可以相互访问。

6.4.2 块级作用域

在JavaScript(ECMAScript)中不存在块级作用域的概念,这一点与其他很多的程序设计语言不同。。举例来说,请看代码清单 6.1。如果认为块级作用域存在,就会认为第二个 console.log() 的结果应该是 1,不过实际的输出却是 2。

代码清单6.1 对于块级作用域的误解
var hzh1 = 1;                  // 全局变量
{
    var hzh1 = 2;
    console.log("输出块级作用域的hzh1: ");
    console.log('hzh1 = ' + hzh1);
}
console.log("输出全局变量的hzh1:");
console.log('hzh1 = ' + hzh1); // 认为结果会是1?
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出块级作用域的hzh1: 
hzh1 = 2
输出全局变量的hzh1:
hzh1 = 2

[Done] exited with code=0 in 0.197 seconds

在代码清单 6.1 中,看似是在代码块内重新声明了块级作用域中的变量 hzh1,但实际上,它只是将全局变量 hzh1 赋值为了 2。也就是说,这与下面的代码是等价的。

var hzh3 = 1; // 全局变量
{
    hzh3 = 2;
    console.log("输出块级作用域的hzh3:");
    console.log('hzh3 = ' + hzh3);
}
console.log("输出全局变量的hzh3:");
console.log('hzh3 = ' + hzh3);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出块级作用域的hzh3:
hzh3 = 2
输出全局变量的hzh3:
hzh3 = 2

[Done] exited with code=0 in 0.205 seconds

在函数作用域中也存在这种对块级作用域的错误理解。在 for语句中对循环变量进行声明是一种习惯做法,不过该循环变量的作用域并不局限于 for 语句内。在下面的代码中,其实是对局部变量 i 进行了循环使用。

function hzh() {
    var hzh4 = 4;
    for(var hzh4 = 0; hzh4 < 10; hzh4++) {
        console.log("省略");
    }
    console.log('hzh4 = ' + hzh4);
}
hzh();
[Running] node "e:\HMV\JavaScript\JavaScript.js"
省略
省略
省略
省略
省略
省略
省略
省略
省略
省略
hzh4 = 10

[Done] exited with code=0 in 0.188 seconds

6.4.3 let和块级作用域

虽然在 ECMAScript 第 5 版中没有块级作用域,不过 JavaScript 自带有 let 这一增强功能,可以实现块级作用域的效果。可以通过 let 定义(let 声明)、let 语句,以及 let 表达式三种方式来使用 let 功能。虽然语法结构不同,但是原理是一样的。

let 定义(let 声明)与 var 声明的用法相同。可以通过下面这样的语法结构对变量进行声明。

let var1 [= value1] [, var2 [= value2]] [, ..., varN [= valueN]];

通过 let 声明进行声明的变量具有块级作用域。除了作用域之外,它的其他方面与通过 var 进行声明的变量没有区别。代码清单 6.4 中是个简单的例子。

代码清单 6.4 let 声明
function hzh() {
    let hzh1 = 1;
    console.log("在函数作用域输出hzh1:")
    console.log('hzh1 = ' + hzh1);     // 输出1
    {
        let hzh1 = 2;
        console.log("在块级作用域输出hzh1:");
        console.log('hzh1 = ' + hzh1); // 输出2
    }                                  // let hzh1 = 2 的作用域到此为止
    console.log("在函数作用域输出hzh1:");
    console.log('hzh1 = ' + hzh1);     // 输出1
} 
hzh();
[Running] node "e:\HMV\JavaScript\JavaScript.js"
在函数作用域输出hzh1:
hzh1 = 1
在块级作用域输出hzh1:
hzh1 = 2
在函数作用域输出hzh1:
hzh1 = 1

[Done] exited with code=0 in 0.18 seconds

如果不考虑作用域的不同,let 变量(通过 let 声明进行声明的变量)与 var 变量的执行方式非常相似。请参见代码清单 6.5 中的注释部分。

代码清单 6.5 let变量的执行方式的具体示例
// 名称的查找
function HZH1() {
    let hzh1 = 1;
    {
        console.log("在块级作用域中输出hzh1:");
        console.log('hzh1 = ' + hzh1); // 输出 1。将对代码块由内至外进行名称查找
    }
}
HZH1();
console.log("****************************************************");
// 该名称在进行 let 声明之前也是有效的
function HZH2() {
    let hzh2 = 2;
    {
        // 这里的 let hzh2 = 2的作用域。
        //不过由于还未对其进行赋值,所以 let 变量 hzh2 的值为undefined
        console.log("在块级作用域中第一次输出hzh2:");
        console.log('hzh2 = ' + hzh2);

        let hzh2 = 3;
        console.log("在块级作用域中第二次输出hzh2:");
        console.log('hzh2 = ' + hzh2); // 输出2
    } 
}
HZH2();
[Running] node "e:\HMV\JavaScript\JavaScript.js"
在块级作用域中输出hzh1:
hzh1 = 1
****************************************************
在块级作用域中第一次输出hzh2:
e:\HMV\JavaScript\JavaScript.js:18
        console.log('hzh2 = ' + hzh2);
                                ^

ReferenceError: Cannot access 'hzh2' before initialization
    at HZH2 (e:\HMV\JavaScript\JavaScript.js:18:33)
    at Object.<anonymous> (e:\HMV\JavaScript\JavaScript.js:25:1)
    at Module._compile (internal/modules/cjs/loader.js:999:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
    at Module.load (internal/modules/cjs/loader.js:863:32)
    at Function.Module._load (internal/modules/cjs/loader.js:708:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
    at internal/main/run_main_module.js:17:47

[Done] exited with code=1 in 0.203 seconds

【评】这里的实验结果和书上的不一样,标记一下。

像下面这样,将 for 语句的初始化表达式中的 var 声明改为 let 变量之后,作用域就将被限制于 for 语句之内。这样的做法更符合通常的思维方式。for in 语句以及 for each in 语句也是同理。

var arr = ["h", "z", "h"];
for(let hzh = 0, len = arr.length; hzh < len; hzh++) {
    console.log("arr[" + hzh + "] = " + arr[hzh]);
}
// 这里已是let变量hzh的作用域之外
[Running] node "e:\HMV\JavaScript\JavaScript.js"
arr[0] = h
arr[1] = z
arr[2] = h

[Done] exited with code=0 in 0.259 seconds

let 语句的语法结构如下。let 变量的作用域被限制于语句内部。

let (var1 [= value1] [, var2 [= value2] [, ..., varN [= valueN]]]) 语句;

下面是 let 语句的具体示例。

let hzh = 1;
{                     // 代码块
    console.log("在代码块中输出hzh:");
    console.log("hzh = " + hzh); // 输出1
}                     // let变量的作用域到此为止
[Running] node "e:\HMV\JavaScript\JavaScript.js"
在代码块中输出hzh:
hzh = 1

[Done] exited with code=0 in 0.185 seconds

代码清单 6.6 是一个混用 var 声明与 let 语句的具体示例。

代码清单 6.6 var 声明与 let 语句
function huangzihan() {
    var hzh1 = 1;
    let hzh2 = 2;
    {
        console.log("输出hzh2:");
        console.log("hzh2 = " + hzh2);     // 输出2
        console.log("");
        hzh3 = 3;
        console.log("输出hzh3:");
        console.log("hzh3 = " + hzh3);     // 输出3
        console.log("");
    }
    console.log("输出hzh1:");  // 输出1
    console.log("hzh1 = " + hzh1);
}

huangzihan();
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出hzh2:
hzh2 = 2

输出hzh3:
hzh3 = 3

输出hzh1:
hzh1 = 1

[Done] exited with code=0 in 0.264 seconds

在 let 语句内部,声明与 let 变量同名的变量会引起 TypeError 问题。下面是一个例子。

// 不能通过let声明同名变量

let (hzh1 = 1) {
    let hzh1 = 2;
    console.log(hzh1); 
}

// 也不能通过 var 声明同名变量

let (hzh2 = 1) {
    var hzh2 = 2;
    console.log(hzh2); 
}

结果和书上说的不一样,标记一下。

let 表达式的语法结构如下所示。let 变量的作用域被限制于表达式内部。

let (var1 [= value1] [, var2 [= value2]] [, ..., varN [= valueN]]) 表达式;

下面是let表达式的具体示例。

var hzh1 = 1;
var hzh2 = let(hzh1 = 2) hzh1 + 1 ; // 在表达式 hzh1 + 1 中使用了 let变量(值为2)
console.log(hzh1, hzh2);            // 对 var 变量 hzh1 没有影响

这里的也是和上的不一样。

6.4.4 嵌套函数与作用域

在 JavaScript 中我们可以对函数进行嵌套声明。也就是说,可以在一个函数中声明另一个函数。这时,可以在内部的函数中访问其外部函数的作用域。从形式上来说,名称的查找是由内向外的。在最后将会查找全局作用域中的名称。

代码清单 6.7 是个具体例子。在代码清单 6.7 中写的是函数声明语句,如果使用的是匿名函数表达式,效果是相同的。

代码清单 6.7 嵌套函数及其作用域
function huangzihan1 () {
    var hzh1 = 1; // 函数huangzihan1的局部变量

    // 嵌套函数的声明
    function huangzihan2 () {
        var hzh2 = 2; // 函数huangzihan2的局部变量
        console.log("对函数huangzihan2的局部变量进行访问:");
        console.log(hzh1); 
        console.log("");
        console.log("对函数huangzihan2的局部变量进行访问:");
        console.log(hzh2);
    }

    function huangzihan3() {
        console.log(hzh2); // 如果不存在全局变量hzh2,则会发生ReferenceError
    }

    // 嵌套函数的调用
    huangzihan2();
    huangzihan3();
}

huangzihan1();
[Running] node "e:\HMV\Babel\hzh.js"
对函数huangzihan2的局部变量进行访问:
1

对函数huangzihan2的局部变量进行访问:
2
e:\HMV\Babel\hzh.js:15
        console.log(hzh2); // 如果不存在全局变量hzh2,则会发生ReferenceError
                    ^

ReferenceError: hzh2 is not defined
    at huangzihan3 (e:\HMV\Babel\hzh.js:15:21)
    at huangzihan1 (e:\HMV\Babel\hzh.js:20:5)
    at Object.<anonymous> (e:\HMV\Babel\hzh.js:23:1)
    at Module._compile (internal/modules/cjs/loader.js:999:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
    at Module.load (internal/modules/cjs/loader.js:863:32)
    at Function.Module._load (internal/modules/cjs/loader.js:708:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
    at internal/main/run_main_module.js:17:47

[Done] exited with code=1 in 0.371 seconds

6.4.5 变量隐藏

在这里使用了隐藏这一比较专业的术语,它指的是,通过作用域较小的变量(或函数),来隐藏作用域较大的同名变量(或函数)。这种情况常常会在无意中发生,从而造成错误。例如,在下面的代码中,全局变量 n 被局部变量 n 所隐藏。

var hzh1 = 1; // 全局变量

function huangzihan() { // 局部变量隐藏了全局变量
    var hzh1 = 2;
    console.log("检测一下局部变量有没有隐藏了全局变量:");
    console.log(hzh1);
}

// 函数调用
huangzihan();
[Running] node "e:\HMV\Babel\hzh.js"
检测一下局部变量有没有隐藏了全局变量:
2

[Done] exited with code=0 in 0.24 seconds

这段代码的功能显而易见。乍一看,类似于代码清单 6.3 或代码清单 6.1 那样的函数作用域以及块级作用域所构成的隐藏并不会引发什么问题。不过,当代码变得更为复杂时,问题就不容易发现了,因此仍需多加注意。

posted @ 2022-05-28 17:04  黄子涵  阅读(64)  评论(0编辑  收藏  举报