第6章 函数与闭包

绩效

章节 代码量(行)
6.1 0
6.2 14
6.3 44
6.4 154
第6章 212

6.1 函数声明语句与匿名函数表达式

可以通过函数声明语句与匿名函数表达式对函数进行声明。

6.2 函数调用分类

表6.1 函数调用分类

名称 说明
方法调用 通过接收方对象对函数进行调用包括apply 与call 调用
构造函数调用 通过new 表达式对函数进行调用
函数调用 以上两种方式之外的函数调用

将以方法调用的方式使用的函数称为方法,同理,将以构造函数调用方式使用的函数称为构造函数

函数声明语句的后置

通过函数声明语句声明的函数,可以在进行声明的代码行之前就对其调用。虽然这个例子在函数的作
用域内进行,不过对于全局作用域情况也是相同。

function hzh1() {
    hzh2(); // 在声明函数fn之前对其进行调用
    function hzh2() {
        console.log('黄子涵');
    }
}
hzh1();
[Running] node "e:\HMV\JavaScript\JavaScript.js"
黄子涵

[Done] exited with code=0 in 99.724 seconds

通过函数声明语句声明的函数,可以在进行声明的代码行之前就对其调用。在通过匿名函数表达式进行定义的情况下结果将会不同。

function hzh3() {
    hzh4();
    var hzh4 = function() {
        console.log('黄子涵');
    }
}
hzh3();
[Running] node "e:\HMV\JavaScript\JavaScript.js"
e:\HMV\JavaScript\JavaScript.js:2
    hzh4();
    ^

TypeError: hzh4 is not a function
    at hzh3 (e:\HMV\JavaScript\JavaScript.js:2:5)
    at Object.<anonymous> (e:\HMV\JavaScript\JavaScript.js:7: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 3.35 seconds

6.3 参数与局部变量

6.3.1 arguments 对象

可以通过在函数内使用 arguments 对象来访问实参。使用方式如代码清单 6.1 所示。

代码清单 6.1 使用 arguments 对象的例子
function hzh1() {
    console.log(arguments.length);
    console.log(arguments[0], arguments[1], arguments[2]);
}
console.log("arguments.length 为实参的数量,值为1");
console.log("arguments[0]的值为7");
console.log(hzh1(7));
console.log("*********************************************");
console.log("arguments.length 为实参的数量,值为2");
console.log("arguments[0] 的值为7,arguments[1] 的值为8");
console.log(hzh1(7, 8));
console.log("*********************************************");
console.log("arguments.length为实参的数量,值为1");
console.log("arguments[0]的值为7");
console.log("arguments[1]的值为8");
console.log("arguments[2] 的值为9");
console.log(hzh1(7, 8, 9));
[Running] node "e:\HMV\JavaScript\JavaScript.js"
arguments.length 为实参的数量,值为1
arguments[0]的值为7
1
7 undefined undefined
undefined
*********************************************
arguments.length 为实参的数量,值为2
arguments[0] 的值为7,arguments[1] 的值为8
2
7 8 undefined
undefined
*********************************************
arguments.length为实参的数量,值为1
arguments[0]的值为7
arguments[1]的值为8
arguments[2] 的值为9
3
7 8 9
undefined

[Done] exited with code=0 in 0.506 seconds

没有相对应的形参的实参也可以通过 arguments 访问。由于能够通过arguments.length 获知实参的数量,因此可以写出所谓的可变长参数函数。而形参的数量则可以通过 Function 对象自身的 length 属性来获得。

虽然 arguments 可以以数组的方式使用,不过它本身并不是数组对象。因此,无法对其使用数组类中的方法

6.3.2 递归函数

递归函数是一种在函数内对自身进行调用函数。这种方式被称为递归执行或递归调用

代码清单 6.2 n 的阶乘(递归函数的例子)
function hzh1(hzh2) {
    if(hzh2 <= 1) {
        return 1;
    } else {
        return hzh2 * hzh1(hzh2 - 1); 
    }
}
console.log("调用函数hzh1:");
console.log(hzh1(5));
[Running] node "e:\HMV\JavaScript\JavaScript.js"
调用函数hzh1:
120

[Done] exited with code=0 in 1.416 seconds

如果递归函数不停地调用自身,运行将不会终止(这与无限循环的情况是一样的,因此俗称为无限递归)。JavaScript 发生无限递归之后的反应取决于实际的运行环境。如果是 SpiderMonkey 的壳层,则会像下面这样发生 InternalError。而在 Java6 附带的 Rhino 中,发生无限递归后则会产生java.lang.OutOfMemoryError 而使 Rhino 停止运行。

// SpiderMonkey 中的无限递归

function hzh() {
    hzh();
}
console.log("调用hzh函数:");
console.log(hzh());
[Running] node "e:\HMV\JavaScript\JavaScript.js"
调用hzh函数:
e:\HMV\JavaScript\JavaScript.js:3
function hzh() {
            ^

RangeError: Maximum call stack size exceeded
    at hzh (e:\HMV\JavaScript\JavaScript.js:3:13)
    at hzh (e:\HMV\JavaScript\JavaScript.js:4:5)
    at hzh (e:\HMV\JavaScript\JavaScript.js:4:5)
    at hzh (e:\HMV\JavaScript\JavaScript.js:4:5)
    at hzh (e:\HMV\JavaScript\JavaScript.js:4:5)
    at hzh (e:\HMV\JavaScript\JavaScript.js:4:5)
    at hzh (e:\HMV\JavaScript\JavaScript.js:4:5)
    at hzh (e:\HMV\JavaScript\JavaScript.js:4:5)
    at hzh (e:\HMV\JavaScript\JavaScript.js:4:5)
    at hzh (e:\HMV\JavaScript\JavaScript.js:4:5)

[Done] exited with code=1 in 0.225 seconds

必须在递归函数内部设置递归执行的停止条件判断,这称为终止条件。对于代码清单 6.2 中的情况,在函数开始处会对参数 n 的值是否小于等于 1 进行判断。终止条件的代码并不一定非要写在递归函数的头部,不过一般来说,写在头部更便于阅读。

可以通过循环实现的处理一定也能够通过递归处理来实现,反之也成立。这是因为,递归调用和循环处理两者的本质说到底都是反复执行某一操作。大多数情况下,通过循环来实现的代码会更为简洁明了。而且,在 JavaScript 中递归处理的执行效率并不一定很高。因此,一般情况下最好避免在 JavaScript中使用递归。

能够通过 arguments.callee 来获取正在执行的 Function 对象的引用。这一引用可以在通过没有名字的函数(所谓的匿名函数)来实现递归函数时使用。下面是一个计算 n 的阶乘的具体示例(请注意,在 ECMAScript 第 5 版的静态模式中,arguments.callee 被禁止使用)。

// n 的阶乘(利用arguments.callee)
var hzh = (function(n) {
    if (n <= 1) { 
        return 1; 
    }
    else {
        return n*arguments.callee(n - 1);
    }
})(5);
console.log("输出hzh的值:");
console.log(hzh);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出hzh的值:
120

[Done] exited with code=0 in 0.177 seconds

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 那样的函数作用域以及块级作用域所构成的隐藏并不会引发什么问题。不过,当代码变得更为复杂时,问题就不容易发现了,因此仍需多加注意。

6.5 函数是一种对象

6.6 Function类

6.7 嵌套函数声明与闭包

6.8 回调函数设计模式

相关文章

前言 做过web项目开发的人对layer弹层组件肯定不陌生,作为l...
前言 前端表单校验是过滤无效数据、假数据、有毒数据的第一步...
前言 图片上传是web项目常见的需求,我基于之前的博客的代码...
前言 导出Excel文件这个功能,通常都是在后端实现返回前端一...
前言 众所周知,js是单线程的,从上往下,从左往右依次执行,...
前言 项目开发中,我们可能会碰到这样的需求:select标签,禁...