您的位置:首页 > Web前端 > JavaScript

JS(二)-闭包和箭头函数

2016-12-27 11:15 302 查看
(一)闭包

一、函数作为返回值

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

求和的函数定义:

function sum(arr) {

    return arr.reduce(function (x, y) {

        return x + y;

    });

}

sum([1, 2, 3, 4, 5]); // 15

可以不返回求和的结果,而是返回求和的函数!

function lazy_sum(arr) {

    var sum = function () {

        return arr.reduce(function (x, y) {

            return x + y;

        });

    }

    return sum;

}

当调用lazy_sum()时,返回的并不是求和结果,而是求和函数:

var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()

调用函数f时,才真正计算求和的结果:

f(); // 15

在这个例子中,在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。

请再注意一点,当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:

var f1 = lazy_sum([1, 2, 3, 4, 5]);

var f2 = lazy_sum([1, 2, 3, 4, 5]);

f1 === f2; // false

f1()和f2()的调用结果互不影响。

二、闭包

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

另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()才执行。我们来看一个例子:

function count() {

    var arr = [];

    for (var i=1; i<=3; i++) {

        arr.push(function () {

            return i * i;

        });

    }

    return arr;

}

var results = count();

var f1 = results[0];

var f2 = results[1];

var f3 = results[2];

在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都添加到一个Array中返回了。

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

f1(); // 16

f2(); // 16

f3(); // 16

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

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

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

function count() {

    var arr = [];

    for (var i=1; i<=3; i++) {

        arr.push((function (n) {

            return function () {

                return n * n;

            }

        })(i));

    }

    return arr;

}

var results = count();

var f1 = results[0];

var f2 = results[1];

var f3 = results[2];

f1(); // 1

f2(); // 4

f3(); // 9

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

(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);

说了这么多,难道闭包就是为了返回一个函数然后延迟执行吗?

在面向对象的程序设计语言里,比如Java和C++,要在对象内部封装一个私有变量,可以用private修饰一个成员变量。

在没有class机制,只有函数的语言里,借助闭包,同样可以封装一个私有变量。我们用JavaScript创建一个计数器:

'use strict';

function create_counter(initial) {

    var x = initial || 0;

    return {

        inc: function () {

            x += 1;

            return x;

        }

    }

}

它用起来像这样:

var c1 = create_counter();

c1.inc(); // 1

c1.inc(); // 2

c1.inc(); // 3

var c2 = create_counter(10);

c2.inc(); // 11

c2.inc(); // 12

c2.inc(); // 13

在返回的对象中,实现了一个闭包,该闭包携带了局部变量x,并且,从外部代码根本无法访问到变量x。换句话说,闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来。

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

function make_pow(n) {

    return function (x) {

        return Math.pow(x, n);

    }

}

// 创建两个新函数:

var pow2 = make_pow(2);

var pow3 = make_pow(3);

pow2(5); // 25

pow3(7); // 343
脑洞大开

很久很久以前,有个叫阿隆佐·邱奇的帅哥,发现只需要用函数,就可以用计算机实现运算,而不需要0、1、2、3这些数字和+、-、*、/这些符号。

JavaScript支持函数,所以可以用JavaScript用函数来写这些计算。来试试:

'use strict';

// 定义数字0:

var zero = function (f) {

    return function (x) {

        return x;

    }

};

// 定义数字1:

var one = function (f) {

    return function (x) {

        return f(x);

    }

};

// 定义加法:

function add(n, m) {

    return function (f) {

        return function (x) {

            return m(f)(n(f)(x));

        }

    }

}

// 计算数字2 = 1 + 1:

var two = add(one, one);

// 计算数字3 = 1 + 2:

var three = add(one, two);

// 计算数字5 = 2 + 3:

var five = add(two, three);

// 你说它是3就是3,你说它是5就是5,你怎么证明?

// 呵呵,看这里:

// 给3传一个函数,会打印3次:

(three(function () {

    console.log('print 3 times');

}))();

// 给5传一个函数,会打印5次:

(five(function () {

    console.log('print 5 times');
}))();

(二)箭头函数

ES6标准新增了一种新的函数:Arrow Function(箭头函数)。为什么叫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

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

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

var obj = {

    birth: 1990,

    getAge: function () {

        var b = this.birth; // 1990

        var fn = function () {

            return new Date().getFullYear() - this.birth; // this指向window或undefined

        };

        return fn();

    }

};

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

var obj = {

    birth: 1990,

    getAge: function () {

        var b = this.birth; // 1990

        var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象

        return fn();

    }

};

obj.getAge(); // 25

如果使用箭头函数,以前的那种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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  闭包 javascript