Java程序员学习JavaScript(04)其它函数

1、闭包

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。

首先,我们来实现一个对Array的求和。通常情况下,求和的函数是这样定义的:

在这里插入图片描述

但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:

在这里插入图片描述

当我们调用lazySum()时,返回的并不是求和结果,而是求和函数,请看:

在这里插入图片描述

把返回的函数结果,进行调用,才真正计算求和的结果:

在这里插入图片描述

在这个例子中,我们在函数 lazySum 中又定义了函数sum,并且,内部函数sum可以引用外部函数lazySum的参数和局部变量,当lazySum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
请再注意一点,当我们调用lazySum()时,每次调用都会返回一个新的函数,即使传入相同的参数:

在这里插入图片描述

sum1()和sum2()的调用结果互不影响。

注意到返回的函数在其定义内部引用了局部变量arr(入参),所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。

另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了 sum() 才执行。

我们再来看一个例子:

在这里插入图片描述

在上面的例子中,函数count()返回的是一个数组,通过for循环向数组中push元素,什么元素呢?函数!每次循环,都创建了一个新的函数,然后,把创建的每个函数都添加到一个数组中,最后返回了数组。

你可能认为调用f1(),f2()和f3()结果应该是1,4,9,但实际结果是:

在这里插入图片描述

全部都是16!原因就在于返回的函数引用了变量 i ,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了4,因此最终结果为16。

返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:

在这里插入图片描述

注意这里用了一个“创建一个匿名函数并立刻执行”的语法:

(function (x) {
    return x * x;
})(3); // 9

理论上讲,创建一个匿名函数并立刻执行可以这么写:

function (x) { return x * x } (3);

但是由于JavaScript语法解析的问题,会报SyntaxError错误,

在这里插入图片描述

因此需要用括号把整个函数定义括起来:

(function (x) { return x * x }) (3);

通常,一个立即执行的匿名函数可以把函数体拆开,一般这么写:

(function (x) {
    return x * x;
})(3);

说了这么多,难道闭包就是为了返回一个函数然后延迟执行吗?当然不是!接着看例子。

在下面的例子前,研究两个语句:

var a = b || c; //大致相当于 var a = b? b : c;
var a = b && c; //大致相当于 var a = b? c : b;

另一个要注意的,JavaScript把null、undefined、0、NaN和空字符串’'视为false,其他值一概视为true。

最后就是函数的参数如果不传,默认就是 undefined。

根据上面的理论 ,var x = initial || 0; 如果initial为undefined,则x赋值为0,如果initial为数字,则x=initial。

继续,闭包有非常强大的功能。举个栗子:
在面向对象的程序设计语言里,比如Java,要在对象内部封装一个私有变量,可以用private修饰一个成员变量。
在没有class机制,只有函数的语言里,借助闭包,同样可以封装一个私有变量。我们用JavaScript创建一个计数器:

在这里插入图片描述

上图中,也能看到函数的执行结果,在返回的对象中,实现了一个闭包,该闭包携带了局部变量x,并且,从外部代码根本无法访问到变量x。

换句话说,闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来。

闭包还可以把多参数的函数变成单参数的函数。例如,要计算xy可以用Math.pow(x, y)函数,不过考虑到经常计算 x*xx*x*x,我们可以利用闭包创建新的函数pow2和pow3:

在这里插入图片描述

2、箭头函数

ES6标准新增了一种新的函数:Arrow Function(箭头函数)。有点类似于Java中的Lambda表达式。

为什么叫Arrow Function?因为它的定义用的就是一个箭头:

x => x * x

上面的箭头函数相当于:

function (x) {
    return x * x;
}

定义一个自动执行的箭头函数如下:

在这里插入图片描述

箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像上面的,只包含一个表达式,连{ … }和return都省略掉了。还有一种可以包含多条语句,这时候就不能省略{ … }和return:

x => {
    if (x > 0) {
        return x * x;
    }
    else {
        return - x * x;
    }
}

如果参数不是一个,就需要用括号()括起来:

// 两个参数:
(x, y) => x * x + y * y
// 无参数:
() => 3.14
// 可变参数:
(x, y, ...rest) => {
    var i, sum = x + y;
    for (i=0; i<rest.length; i++) {
        sum += rest[i];
    }
    return sum;
}

如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:

// SyntaxError:
x => { foo: x }

因为和函数体的{ … }有语法冲突,所以要改为:

// ok:
x => ({ foo: x })

把前面的一个例子改为箭头函数:

在这里插入图片描述

箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定。

回顾前面的例子,由于JavaScript函数对this绑定的错误处理,下面的例子无法得到预期结果:

在这里插入图片描述

现在,箭头函数完全修复了this的指向,this总是指向词法作用域,也就是外层调用者obj:

在这里插入图片描述

如果使用箭头函数,以前的那种hack写法:

var that = this;

就不再需要了。

由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略:

var obj = {
birth: 1990,
getAge: function (year) {
var b = this.birth; // 1990
var fn = (y) => y - this.birth; // this.birth仍是1990
return fn.call({birth:2000}, year);
}
};
obj.getAge(2015); // 25

3、generator(生成器)

generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。

ES6定义generator标准的哥们借鉴了Python的generator的概念和语法,如果你对Python的generator很熟悉,那么ES6的generator就是小菜一碟了。

我们先复习函数的概念。一个函数是一段完整的代码,调用一个函数就是传入参数,然后返回结果:

function foo(x) {
    return x + x;
}
var r = foo(1); // 调用foo函数

函数在执行过程中(指的是没有执行完的情况),如果没有遇到return语句(函数末尾如果没有return,就是隐含的return undefined;),控制权无法交回被调用的代码,一直会在函数内执行下去。

generator跟函数很像,定义如下:

function* foo(x) {
    yield x + 1;
    yield x + 2;
    return x + 3;
}

yield关键字用于暂停和恢复生成器功能。

从上面的代码可以分析,generator和函数不同的是,generator由 function* 定义(注意多出的*号),并且,除了return语句,还可以用yield返回多次。

大多数同学立刻就晕了,generator就是能够返回多次的“函数”?返回多次有啥用?还是举个栗子吧。

我们以一个著名的斐波那契数列为例,它由1,1开头:

1 1 2 3 5 8 13 21 34 …

要编写一个产生斐波那契数列的函数,可以这么写:

在这里插入图片描述

函数只能返回一次,所以必须返回一个Array。但是,如果换成generator,就可以一次返回一个数,不断返回多次。用generator改写如下:

在这里插入图片描述

直接调用 var result = fnum(6); ,然后打印 console.log(result); 可以看到,直接返回了生成器对象本身:

Object [Generator] {}

这显然不是我们要的,这里就可以看出区别,调用生成器和调用函数是不一样的,fnum(6) 仅仅是创建了一个generator对象,还没有去执行它。

执行generator对象有两个方法,一是不断地调用generator对象的next()方法,如上图所示,next()方法会执行generator的代码,然后,每次遇到yield a;就返回一个对象{value: x, done: true/false},然后“暂停”。

返回的value就是yield的返回值,done表示这个generator是否已经执行结束了。如果done为true,则value就是return的返回值。
当执行到done为true时,这个generator对象就已经全部执行完毕,不要再继续调用next()了。即使再接着调用,返回的value也只能是undefined。

所以如果要返回所有value,应该这么写:

在这里插入图片描述

第二个方法是直接用for … of循环迭代generator对象,这种方式不需要我们自己判断done:

在这里插入图片描述

generator和普通函数相比,有什么用?

因为generator可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个generator就可以实现需要用面向对象才能实现的功能。例如,用一个对象来保存状态,得这么写:

在这里插入图片描述

如果把max改为4或者其它值,可调用next的有效次数就会跟着改变:

在这里插入图片描述

可见,用对象的属性来保存状态,相当繁琐。

generator还有另一个巨大的好处,就是在AJAX中把异步回调代码变成“同步”代码。

没有generator之前的黑暗时代,用AJAX时需要这么写代码:

ajax('http://url-1', data1, function (err, result) {
    if (err) {
        return handle(err);
    }
    ajax('http://url-2', data2, function (err, result) {
        if (err) {
            return handle(err);
        }
        ajax('http://url-3', data3, function (err, result) {
            if (err) {
                return handle(err);
            }
            return success(result);
        });
    });
});

回调越多,代码越难看。

有了generator的美好时代,用AJAX时可以这么写:

try {
    r1 = yield ajax('http://url-1', data1);
    r2 = yield ajax('http://url-2', data2);
    r3 = yield ajax('http://url-3', data3);
    success(r3);
} catch (err) {
    handle(err);
}

看上去是同步的代码,实际执行是异步的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值