函式第三部分,閉包及箭頭函式
閉包
注:閉包在JavaScript中的應用是非常強大的,應當掌握。
一個函式返回的不是具體的值而是一個函式時,這樣的稱謂閉包
函式作為返回值
高階函式除了可以接受函式作為引數外,還可以把函式作為結果值返回。
我們來實現一個對Array
的求和。通常情況下,求和的函式是這樣定義的:
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
首先我們弄懂上面程式碼的執行流程:首先var results = count();
之後,函式count
已經被呼叫了,所以一次執行函式內的各段程式碼:var arr = [];
,for (var i=1; i<=3; i++)
,這個for迴圈尤其值得注意。因為此時迴圈體執行了push方法,將一個個函式function () { return i * i;}
新增到陣列內,但是這個函式並沒有被呼叫,還只是一個變數,所以for迴圈依次執行,直到i = 4
。因為閉包,內部函式function () { return i * i;}
引用的i
就是外部變數,for迴圈中的i = 4
。所以,之後陣列arr
內的函式的i
都是4。
呼叫函式count
後,變數results
已經是陣列arr
了。數組裡面元素依次是function f1() { return i * i;} function f2() { return i * i;} function f3() { return i * i;}
。但是三個函式都沒有被呼叫,直到var f1 = results[0];
,此時function f1() { return i * i;}
開始執行,如上段所寫,此時的i = 4
,所以,返回值就是16了。後面兩個呼叫也是類似情況
返回閉包時牢記的一點就是:返回函式不要引用任何迴圈變數,或者後續會發生變化的變數。
如果一定要引用迴圈變數怎麼辦?方法是再建立一個函式,用該函式的引數繫結迴圈變數當前的值,無論該迴圈變數後續如何更改,已繫結到函式引數的值不變:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));//這裡建立了一個函式將原來都函式包起來了,並且還立即執行了裡面計算的函式,這樣使後來的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) {//函式返回的使inc方法
var x = initial || 0;
return {
inc: function () {//方法inc
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
:
'use strict';
function make_pow(n){
return function(x){
return Math.pow(x,n);
}
}//這裡寫一個閉包,函式make_pow返回的是函式function(x);
var pow2=make_pow(2);
var pow3=make_pow(3);//定義兩個新的函式pow2,pow3.
pow2(5);//25
pow3(6);//36
箭頭函式
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的弊端,那個例子我們在物件裡的方法裡不能通過this直接鎖定到該物件,我們是通過that來解決的
回顧前面的例子,由於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
練習
請使用箭頭函式簡化排序時傳入的函式:
'use strict'
var arr = [10, 20, 1, 2];
arr.sort((x, y) => {
if(x>y){
return 1;
}else if(x<y){
return -1;}
return 0;
});
console.log(arr); // [1, 2, 10, 20]
/*第二種 arr.sort((x, y) => {
return x-y;
});
console.log(arr); // [1, 2, 10, 20]
這是排序的兩種寫法,一般能理解第二種的肯定選擇第二種
這裡的第二種 寫成y-x就是從大到小排序陣列。
generator
函式在執行過程中,如果沒有遇到return
語句(函式末尾如果沒有return
,就是隱含的return undefined;
),控制權無法交回被呼叫的程式碼。
generator跟函式很像,定義如下:
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
generator和函式不同的是,generator由function*
定義(注意多出的*
號),並且,除了return
語句,還可以用yield
返回多次。
要編寫一個產生斐波那契數列的函式,可以這麼寫:
function fib(max) {
var
t,
a = 0,
b = 1,
arr = [0, 1];
while (arr.length < max) {
[a, b] = [b, a + b];
arr.push(b);
}
return arr;
}
// 測試:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
函式只能返回一次,所以必須返回一個Array
。但是,如果換成generator,就可以一次返回一個數,不斷返回多次。用generator改寫如下:
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 0;
while (n < max) {
yield a;
[a, b] = [b, a + b];
n ++;
}
return;
}
直接呼叫試試:
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
直接呼叫一個generator和呼叫函式不一樣,fib(5)
僅僅是建立了一個generator物件,還沒有去執行它。
呼叫generator物件有兩個方法,一是不斷地呼叫generator物件的next()
方法:
var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}
next()
方法會執行generator的程式碼,然後,每次遇到yield x;
就返回一個物件{value: x, done: true/false}
,然後“暫停”。返回的value
就是yield
的返回值,done
表示這個generator是否已經執行結束了。如果done
為true
,則value
就是return
的返回值。
當執行到done
為true
時,這個generator物件就已經全部執行完畢,不要再繼續呼叫next()
了。
第二個方法是直接用for ... of
迴圈迭代generator物件,這種方式不需要我們自己判斷done
:
'use strict'
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 0;
while (n < max) {
yield a;//多次返回a的值
[a, b] = [b, a + b];
n ++;
}
return;
}
for (var x of fib(10)) {
console.log(x); // 依次輸出0, 1, 1, 2, 3, ...
}