不一样的Javascript(10)——函数嵌套

1. 在Javascript中,函数中可以嵌套其他函数。例如:

function distance(x1, y1, x2, y2) {
    function square (x) {
        return x * x;
    }
    
    return Math.sqrt(square(x1 - x2) + square(y1 - y2));
}

var result = distance(0, 0, 3, 4);
console.log(result); // 5

在上述代码中,函数square中嵌套定义在函数distance中。square只能在distance中被调用。如果试图在distance的外面调用square,将会出错。

2. 如果嵌套函数使用了外面函数的变量或者参数,那么这个嵌套函数就形成了一个闭包(Closure)。

2.1 闭包能在它外面的函数执行完之后仍然能够访问他外面函数的参数和变量。例如:

function sayGreeting(greeting) {
    var prefix = "my friend ";
    
    return function sayGreetingTo(name) {
        return greeting + ", " + prefix + name;
    }
}

var sayHi = sayGreeting("Hi");
var sayHiToHarry = sayHi("Harry"); 
console.log(sayHiToHarry); // Hi, my friend Harry

var sayHello = sayGreeting("Hello");
var sayHelloToPeter = sayHello("Peter"); 
console.log(sayHelloToPeter); // Hello, my friend Peter

在上述代码中,我们用参数"Hi"或者"Hello"调用函数sayGreeting,在函数sayGreeting结束之后,在闭包sayGreetingTo中仍然能够访问参数greeting和变量prefix。

2.2 闭包中包含指向外面函数的引用。例如:

function counter (initialValue) {
    var privateValue = initialValue;
    
    function changeBy(delta) {
        privateValue += delta;
    }
    
    return {
        increament: function() {
            changeBy(1);
        },
        decreament: function() {
            changeBy(-1);
        },
        getValue: function() {
            return privateValue;
        }
    }
}

var counter1 = counter(5);
var counter2 = counter(5);

counter1.increament();
counter1.increament();
console.log(counter1.getValue()); // 7

counter1.decreament();
console.log(counter1.getValue()); // 6

console.log(counter2.getValue()); // 5
在上面的代码中,3个闭包函数increament、decreament和getValue都嵌套在函数counter里面,都能访问定义在counter里的其他函数以及变量。定义在外面函数中的变量(privateValue)的值能在调用之后得以保存。因此两次调用counter1.increament之后再调用getValue的结果是7。

两次调用counter得到的两个不同对象,它们之间的运行环境(例如counter函数中的变量privateValue等)没有共享。因此在counter1上调用increament或者decreament对counter2没有影响。

上述代码还演示了闭包的一个作用:模拟私有变量和函数。在上述代码中,函数counter中的变量privateValue以及函数changeBy在函数counter之外都不能访问。

2.3 当多个闭包在一个循环中创建时,很多人都会犯错误。例如如下代码:

function getNumberFuncs(length) {
    var numberFuncs = [];
    for(var i = 0; i < length; ++i) {
        numberFuncs[i] = function() {
            return i;
        };
    }
    
    return numberFuncs;
}
var numberFuncs = getNumberFuncs(3);
for(var i = 0; i < numberFuncs.length; ++i) {
    var number = numberFuncs[i]();
    console.log(number);
}
上述代码期待的输出可能是3行,分别为0、1和2。然而,上述代码的实际输出的3行都是3。这是因为三个闭包函数都通过引用使用了printNumber函数中的变量i。当函数printNumber执行结束之后,i的值是3。因此三个闭包函数再试图输出i时,输出的数值都是3。
我们可以使用立即调用函数机制(Immediately Invoked Function Expression, IIFE)来解决这个问题。修改之后的代码为:

function getNumberFuncs(length) {
    var numberFuncs = [];
    for(var i = 0; i < length; ++i) {
        numberFuncs[i] = function(j) {
            return j;
        }(i);
    }
    
    return numberFuncs;
}
var numberFuncs = getNumberFuncs(3);
for(var i = 0; i < numberFuncs.length; ++i) {
    var number = numberFuncs[i];
    console.log(number);
}

3.1 闭包的一个作用是给函数以参数之外的形式传入数据。有些API的参数是其他函数,但是没有预留位置给函数传入参数。例如,函数setTimeout、setInterval有两个参数,第一个参数是一个函数,第二个是以毫秒为单位的时间。当传给setTimeout、setInterval的函数也有自己的参数的时候,就有可能有问题。比如如下代码:

function printNumber(number) {
    console.log(number);
}

setInterval(printNumber, 1000); // undefined
由于没有给printNumber传入参数,因此每隔1秒钟之后实际输出的都是undefined。

我们可以用闭包解决这种类型的问题。修改之后的代码如下所示:

function printNumberFunc(number) {
    return function() {
        console.log(number);
    }
}

var func = printNumberFunc(20);
setInterval(func, 1000); // 20

很多JavaScript的新手会试图直接给setInterval中printNumber以如下形式传入参数:

function printNumber(number) {
    console.log(number);
}

setInterval(printNumber(20), 1000); 
这样调用setInterval的结果是最终只输出一行20。这是因为上述代码只是调用了一次printNumber(20)。由于printNumber没有返回值,因此并没有给函数setInterval传入一个函数。

3.2 闭包的另一个作用是封装数据,这些数据只有闭包函数才能访问。

假如我们需要一个函数把一个人的名字和邮件地址生成HTML表格中的一行。例如输入"Harry"和"harry@abcd.com",则输出"<tr><td>Harry</td><td>harry@abcd.com</td></tr>"。下面这个函数可以完成这一功能:

function getRowData(name, email) {
    var rowTemplate = ["<tr><td>",
                       "", // placeholder for name
                       "</td><td>",
                       "", // placeholder for email
                       "</td></tr>"];
    
    rowTemplate[1] = name;
    rowTemplate[3] = email;
    
    return rowTemplate.join("");
}

var row = getRowData("Harry", "harry@abcd.com");
console.log(row);
上面的函数有一个缺点:每一次调用函数getRowData的时候都会生成一个rowTemplate对象。虽然这些对象会被GC释放掉,但生成和释放对象都是需要时间开销。我们需要尽量减少不必要的开销。

一个办法把rowTemplate从函数getRowData中移出来,变成:

var rowTemplate = ["<tr><td>",
                   "", // placeholder for name
                   "</td><td>",
                   "", // placeholder for email
                   "</td></tr>"];

function getRowData(name, email) {
    rowTemplate[1] = name;
    rowTemplate[3] = email;
    
    return rowTemplate.join("");
}

var row = getRowData("Harry", "harry@abcd.com");
console.log(row);
这样多次调用函数getRowData使用的都是同一个rowTemplate对象。不必要的开销是消除了,但我们引入了一个新的全局变量。每引入一个全局变量,就增加了变量名字冲突的可能。因此我们需要尽量减少全局变量的数目。

如果我们使用闭包,则既可以避免重复生成rowTemplate对象,又不引入新的全局变量。下面是利用闭包实现同样的功能:

var getRowData = (function() {
    var rowTemplate = ["<tr><td>",
                       "", // placeholder for name
                       "</td><td>",
                       "", // placeholder for email
                       "</td></tr>"];
    
    return function(name, email) {
        rowTemplate[1] = name;
        rowTemplate[3] = email;
    
        return rowTemplate.join("");
    };
})();

var row = getRowData("Harry", "harry@abcd.com");
console.log(row);
我们定义了一个匿名函数,该匿名函数是一个立即调用函数,函数的返回值是另一个函数。getRowData指向这个返回的函数。我们仍然可以通过getRowData调用函数根据人名和邮件地址生成HTML表格中的一行数据。由于立即调用函数只会调用一次,因此只会生成一个rowTemplate对象。




  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值