亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

深入探討javascript函數(shù)式編程

 更新時(shí)間:2015年10月11日 09:57:15   投稿:hebedich  
在函數(shù)式編程中,函數(shù)被看做是“一等公民”。JavaScript可以通過(guò)巧妙地函數(shù)組合來(lái)構(gòu)建抽象,通過(guò)內(nèi)嵌函數(shù)的方式,在軟件開(kāi)發(fā)的過(guò)程中,我們可以把更多的精力放在“函數(shù)要做什么”上,而不用太關(guān)心“函數(shù)如何做”的問(wèn)題。
有時(shí),優(yōu)雅的實(shí)現(xiàn)是一個(gè)函數(shù)。不是方法。不是類。不是框架。只是函數(shù)。 
                      - John Carmack,游戲《毀滅戰(zhàn)士》首席程序員

函數(shù)式編程全都是關(guān)于如何把一個(gè)問(wèn)題分解為一系列函數(shù)的。通常,函數(shù)會(huì)鏈在一起,互相嵌套, 來(lái)回傳遞,被視作頭等公民。如果你使用過(guò)諸如jQuery或Node.js這樣的框架,你應(yīng)該用過(guò)一些這樣的技術(shù), 只不過(guò)你沒(méi)有意識(shí)到。

我們從Javascript的一個(gè)小尷尬開(kāi)始。

假設(shè)我們需要一個(gè)值的列表,這些值會(huì)賦值給普通的對(duì)象。這些對(duì)象可能包含任何東西:數(shù)據(jù)、HTML對(duì)象等等。

var
  obj1 = {value: 1},
  obj2 = {value: 2},
  obj3 = {value: 3};
var values = [];
function accumulate(obj) {
 values.push(obj.value);
}
accumulate(obj1);
accumulate(obj2);
console.log(values); // Output: [obj1.value, obj2.value]

這個(gè)代碼能用但是不穩(wěn)定。任何代碼都可以不通過(guò)accumulate()函數(shù)改變values對(duì)象。 而且如果我們忘記了給values賦上空數(shù)組[],這個(gè)代碼壓根兒就不會(huì)工作。

但是如果變量聲明在函數(shù)內(nèi)部,他就不會(huì)被任何搗蛋的代碼給更改。

function accumulate2(obj) {
 var values = [];
 values.push(obj.value);
 return values;
}
console.log(accumulate2(obj1)); // Returns: [obj1.value]
console.log(accumulate2(obj2)); // Returns: [obj2.value]
console.log(accumulate2(obj3)); // Returns: [obj3.value]

不行呀!只有最后傳入的那個(gè)對(duì)象的值才被返回。

我們也許可以通過(guò)在第一個(gè)函數(shù)內(nèi)部嵌套一個(gè)函數(shù)來(lái)解決這個(gè)問(wèn)題。

var ValueAccumulator = function(obj) {
 var values = []
 var accumulate = function() {
  values.push(obj.value);
 };
 accumulate();
 return values;
};

可是問(wèn)題依然存在,而且我們現(xiàn)在無(wú)法訪問(wèn)accumulate函數(shù)和values變量了。

我們需要的是一個(gè)自調(diào)用函數(shù)

自調(diào)用函數(shù)和閉包

如果我們能夠返回一個(gè)可以依次返回values數(shù)組的函數(shù)表達(dá)式怎么樣?在函數(shù)內(nèi)聲明的變量可以被函數(shù)內(nèi)的所有代碼訪問(wèn)到, 包括自調(diào)用函數(shù)。

通過(guò)使用自調(diào)用函數(shù),前面的尷尬消失了。

var ValueAccumulator = function() {
 var values = [];
 var accumulate = function(obj) {
  if (obj) {
   values.push(obj.value);
   return values;
  } else {
   return values;
  }
 };
 return accumulate;
};
//This allows us to do this:
var accumulator = ValueAccumulator();
accumulator(obj1);
accumulator(obj2);
console.log(accumulator());
// Output: [obj1.value, obj2.value]
ValueAccumulator = ->
 values = []
 (obj) ->
  values.push obj.value if obj
  values

這些都是關(guān)于作用域的。變量values在內(nèi)部函數(shù)accumulate()中可見(jiàn),即便是在外部的代碼在調(diào)用這個(gè)函數(shù)時(shí)。 這叫做閉包。

Javascript中的閉包就是函數(shù)可以訪問(wèn)父作用域,哪怕父函數(shù)已經(jīng)執(zhí)行完畢。

閉包是所有函數(shù)式語(yǔ)言都具有的特征。傳統(tǒng)的命令式語(yǔ)言沒(méi)有閉包。

高階函數(shù)

自調(diào)用函數(shù)實(shí)際上是高階函數(shù)的一種形式。高階函數(shù)就是以其它函數(shù)為輸入,或者返回一個(gè)函數(shù)為輸出的函數(shù)。

高階函數(shù)在傳統(tǒng)的編程中并不常見(jiàn)。當(dāng)命令式程序員使用循環(huán)來(lái)迭代數(shù)組的時(shí)候,函數(shù)式程序員會(huì)采用完全不同的一種實(shí)現(xiàn)方式。 通過(guò)高階函數(shù),數(shù)組中的每一個(gè)元素可以被應(yīng)用到一個(gè)函數(shù)上,并返回新的數(shù)組。

這是函數(shù)式編程的中心思想。高階函數(shù)具有把邏輯像對(duì)象一樣傳遞給函數(shù)的能力。

在Javascript中,函數(shù)被作為頭等公民對(duì)待,這和Scheme、Haskell等經(jīng)典函數(shù)是語(yǔ)言一樣的。 這話聽(tīng)起來(lái)可能有點(diǎn)古怪,其實(shí)實(shí)際意思就是函數(shù)被當(dāng)做基本類型,就像數(shù)字和對(duì)象一樣。 如果數(shù)字和對(duì)象可以被來(lái)回傳遞,那么函數(shù)也可以。

來(lái)實(shí)際看看?,F(xiàn)在把上一節(jié)的ValueAccumulator()函數(shù)配合高階函數(shù)使用:
// 使用forEach()來(lái)遍歷一個(gè)數(shù)組,并對(duì)其每個(gè)元素調(diào)用回調(diào)函數(shù)accumulator2
var accumulator2 = ValueAccumulator();
var objects = [obj1, obj2, obj3]; // 這個(gè)數(shù)組可以很大
objects.forEach(accumulator2);
console.log(accumulator2());

純函數(shù)

純函數(shù)返回的計(jì)算結(jié)果僅與傳入的參數(shù)相關(guān)。這里不會(huì)使用外部的變量和全局狀態(tài),并且沒(méi)有副作用。 換句話說(shuō)就是不能改變作為輸入傳入的變量。所以,程序里只能使用純函數(shù)返回的值。

用數(shù)學(xué)函數(shù)來(lái)舉一個(gè)簡(jiǎn)單的例子。Math.sqrt(4)將總是返回2,不使用任何隱藏的信息,如設(shè)置或狀態(tài), 而且不會(huì)帶來(lái)任何副作用。

純函數(shù)是對(duì)數(shù)學(xué)上的“函數(shù)”的真實(shí)演繹,就是輸入和輸出的關(guān)系。它們思路簡(jiǎn)單也便于重用。 由于純函數(shù)是完全獨(dú)立的,它們更適合被一次又一次地使用。

舉例說(shuō)明來(lái)對(duì)比一下非純函數(shù)和純函數(shù)。

// 把信息打印到屏幕中央的函數(shù)
var printCenter = function(str) {
 var elem = document.createElement("div");
 elem.textContent = str;
 elem.style.position = 'absolute';
 elem.style.top = window.innerHeight / 2 + "px";
 elem.style.left = window.innerWidth / 2 + "px";
 document.body.appendChild(elem);
};
printCenter('hello world');
// 純函數(shù)完成相同的事情
var printSomewhere = function(str, height, width) {
 var elem = document.createElement("div");
 elem.textContent = str;
 elem.style.position = 'absolute';
 elem.style.top = height;
 elem.style.left = width;
 return elem;
};
document.body.appendChild(
printSomewhere('hello world',
window.innerHeight / 2) + 10 + "px",
window.innerWidth / 2) + 10 + "px"));

非純函數(shù)依賴window對(duì)象的狀態(tài)來(lái)計(jì)算寬度和高度,自給自足的純函數(shù)則要求這些值作為參數(shù)傳入。 實(shí)際上它就允許了信息打印到任何地方,這也讓這個(gè)函數(shù)有了更多用途。

非純函數(shù)看起來(lái)是一個(gè)更容易的選擇,因?yàn)樗谧约簝?nèi)部實(shí)現(xiàn)了追加元素,而不是返回元素。 返回了值的純函數(shù)printSomewhere()則會(huì)在跟其他函數(shù)式編程技術(shù)的配合下有更好的表現(xiàn)。

var messages = ['Hi', 'Hello', 'Sup', 'Hey', 'Hola'];
messages.map(function(s, i) {
 return printSomewhere(s, 100 * i * 10, 100 * i * 10);
}).forEach(function(element) {
 document.body.appendChild(element);
});

當(dāng)一個(gè)函數(shù)是純的,也就是不依賴于狀態(tài)和環(huán)境,我們就不用管它實(shí)際是什么時(shí)候被計(jì)算出來(lái)。 后面的惰性求值將講到這個(gè)。

匿名函數(shù)

把函數(shù)作為頭等對(duì)象的另一個(gè)好處是匿名函數(shù)。

就像名字暗示的那樣,匿名函數(shù)就是沒(méi)有名字的函數(shù)。實(shí)際不止這些。它允許了在現(xiàn)場(chǎng)定義臨時(shí)邏輯的能力。 通常這帶來(lái)的好處就是方便:如果一個(gè)函數(shù)只用一次,沒(méi)有必要給它浪費(fèi)一個(gè)變量名。

下面是一些匿名函數(shù)的例子:

// 寫匿名函數(shù)的標(biāo)準(zhǔn)方式
function() {
 return "hello world"
};

// 匿名函數(shù)可以賦值給變量
var anon = function(x, y) {
 return x + y
};

// 匿名函數(shù)用于代替具名回調(diào)函數(shù),這是匿名函數(shù)的一個(gè)更常見(jiàn)的用處
setInterval(function() {
 console.log(new Date().getTime())
}, 1000);
// Output: 1413249010672, 1413249010673, 1413249010674, ...

// 如果沒(méi)有把它包含在一個(gè)匿名函數(shù)中,他將立刻被執(zhí)行,
// 并且返回一個(gè)undefined作為回調(diào)函數(shù):
setInterval(console.log(new Date().getTime()), 1000)
// Output: 1413249010671

下面是匿名函數(shù)和高階函數(shù)配合使用的例子

function powersOf(x) {
 return function(y) {
  // this is an anonymous function!
  return Math.pow(x, y);
 };
}
powerOfTwo = powersOf(2);
console.log(powerOfTwo(1)); // 2
console.log(powerOfTwo(2)); // 4
console.log(powerOfTwo(3)); // 8
powerOfThree = powersOf(3);
console.log(powerOfThree(3)); // 9
console.log(powerOfThree(10)); // 59049

這里返回的那個(gè)函數(shù)不需要命名,它可以在powersOf()函數(shù)外的任何地方使用,這就是匿名函數(shù)。

還記得累加器的那個(gè)函數(shù)嗎?它可以用匿名函數(shù)重寫

var
 obj1 = { value: 1 },
 obj2 = { value: 2 },
 obj3 = { value: 3 };
 
var values = (function() {
 // 匿名函數(shù)
 var values = [];
 return function(obj) {
  // 有一個(gè)匿名函數(shù)!
  if (obj) {
   values.push(obj.value);
   return values;
  } else {
   return values;
  }
 }
})(); // 讓它自執(zhí)行
console.log(values(obj1)); // Returns: [obj.value]
console.log(values(obj2)); // Returns: [obj.value, obj2.value]
obj1 = { value: 1 }
obj2 = { value: 2 }
obj3 = { value: 3 }

values = do ->
 valueList = []
 (obj) ->
  valueList.push obj.value if obj
  valueList
console.log(values(obj1)); # Returns: [obj.value]
console.log(values(obj2)); # Returns: [obj.value, obj2.value]

真棒!一個(gè)高階匿名純函數(shù)。我們?cè)趺催@么幸運(yùn)?實(shí)際上還不止這些,這里面還有個(gè)自執(zhí)行的結(jié)構(gòu), (function(){...})();。函數(shù)后面跟的那個(gè)括號(hào)可以讓函數(shù)立即執(zhí)行。在上面的例子里, 給外面values賦的值是函數(shù)執(zhí)行的結(jié)果。

匿名函數(shù)不僅僅是語(yǔ)法糖,他們是lambda演算的化身。請(qǐng)聽(tīng)我說(shuō)下去…… lambda演算早在計(jì)算機(jī)和計(jì)算機(jī)語(yǔ)言被發(fā)明的很久以前就出現(xiàn)了。它只是個(gè)研究函數(shù)的數(shù)學(xué)概念。 非同尋常的是,盡管它只定義了三種表達(dá)式:變量引用,函數(shù)調(diào)用和匿名函數(shù),但它被發(fā)現(xiàn)是圖靈完整的。 如今,lambda演算處于所有函數(shù)式語(yǔ)言的核心,包括javascript。
 由于這個(gè)原因,匿名函數(shù)往往被稱作lambda表達(dá)式。

匿名函數(shù)也有一個(gè)缺點(diǎn),那就是他們?cè)谡{(diào)用棧中難以被識(shí)別,這會(huì)對(duì)調(diào)試造成一些困難。要小心使用匿名函數(shù)。

方法鏈

在Javascript中,把方法鏈在一起很常見(jiàn)。如果你使用過(guò)jQuery,你應(yīng)該用過(guò)這種技巧。它有時(shí)也被叫做“建造者模式”。

這種技術(shù)用于簡(jiǎn)化多個(gè)函數(shù)依次應(yīng)用于一個(gè)對(duì)象的代碼。

// 每個(gè)函數(shù)占用一行來(lái)調(diào)用,不如……
arr = [1, 2, 3, 4];
arr1 = arr.reverse();
arr2 = arr1.concat([5, 6]);
arr3 = arr2.map(Math.sqrt);

// ……把它們串到一起放在一行里面
console.log([1, 2, 3, 4].reverse().concat([5, 6]).map(Math.sqrt));
// 括號(hào)也許可以說(shuō)明是怎么回事
console.log(((([1, 2, 3, 4]).reverse()).concat([5, 6])).map(Math.sqrt));

這只有在函數(shù)是目標(biāo)對(duì)象所擁有的方法時(shí)才有效。如果你要?jiǎng)?chuàng)建自己的函數(shù),比如要把兩個(gè)數(shù)組zip到一起, 你必須把它聲明為Array.prototype對(duì)象的成員.看一下下面的代碼片段:
Array.prototype.zip = function(arr2) {
  // ...
}

這樣我們就可以寫成下面的樣子
arr.zip([11,12,13,14).map(function(n){return n*2});
// Output: 2, 22, 4, 24, 6, 26, 8, 28

遞歸

遞歸應(yīng)該是最著名的函數(shù)式編程技術(shù)。就是一個(gè)函數(shù)調(diào)用它自己。

當(dāng)函數(shù)調(diào)用自己,有時(shí)奇怪的事情就發(fā)生了。它的表現(xiàn)即是一個(gè)循環(huán),多次執(zhí)行同樣的代碼,也是一個(gè)函數(shù)棧。

使用遞歸函數(shù)時(shí)必須十分小心地避免無(wú)限循環(huán)(這里應(yīng)該說(shuō)是無(wú)限遞歸)。就像循環(huán)一樣,必須有個(gè)停止條件。 這叫做基準(zhǔn)情形(base case)。

下面有個(gè)例子

var foo = function(n) {
 if (n < 0) {
  // 基準(zhǔn)情形
  return 'hello';
 } else {
  // 遞歸情形
  return foo(n - 1);
 }
}
console.log(foo(5));

譯注:原文中的代碼有誤,遞歸情形的函數(shù)調(diào)用缺少return,導(dǎo)致函數(shù)執(zhí)行得最后沒(méi)有結(jié)果。這里已經(jīng)糾正。

遞歸和循環(huán)可以相互轉(zhuǎn)換。但是遞歸算法往往更合適,甚至是必要的,因?yàn)橛行┣樾斡醚h(huán)很費(fèi)勁。

一個(gè)明顯的例子就是遍歷樹(shù)。

var getLeafs = function(node) {
 if (node.childNodes.length == 0) {
  // base case
  return node.innerText;
 } else {
  // recursive case:
  return node.childNodes.map(getLeafs);
 }
}

分而治之

遞歸不只是代替for和while循環(huán)的有趣的方式。有個(gè)叫分而治之的算法,它遞歸地把問(wèn)題拆分成更小的情形, 直到小到可以解決。

歷史上有個(gè)歐幾里得算法用于找出兩個(gè)數(shù)的最大公分母

function gcd(a, b) {
 if (b == 0) {
  // 基準(zhǔn)情形 (治)
  return a;
 } else {
  // 遞歸情形 (分)
  return gcd(b, a % b);
 }
}

console.log(gcd(12,8));
console.log(gcd(100,20));

gcb = (a, b) -> if b is 0 then a else gcb(b, a % b)

理論上來(lái)說(shuō),分而治之很牛逼,但是現(xiàn)實(shí)中有用嗎?當(dāng)然!用Javascript的函數(shù)對(duì)數(shù)組排序不是很好, 它不但替換了原數(shù)組,也就是說(shuō)數(shù)據(jù)不是不變的,并且它還不夠可靠、靈活。通過(guò)分而治之,我們可以做得更好。

全部的實(shí)現(xiàn)代碼大概要40行,這里只展示偽代碼:

var mergeSort = function(arr) {
 if (arr.length < 2) {
  // 基準(zhǔn)情形: 只有0或1個(gè)元素的數(shù)組是不用排序的
  return items;
 } else {
  // 遞歸情形: 把數(shù)組拆分、排序、合并
  var middle = Math.floor(arr.length / 2);
  // 分
  var left = mergeSort(arr.slice(0, middle));
  var right = mergeSort(arr.slice(middle));
  // 治
  // merge是一個(gè)輔助函數(shù),返回一個(gè)新數(shù)組,它將兩個(gè)數(shù)組合并到一起
  return merge(left, right);
 }
}

譯注:關(guān)于用分而治之的思路進(jìn)行排序的一個(gè)更好的例子是快排,使用Javascript也只有13行代碼。 具體請(qǐng)參考我以前的博文 《優(yōu)雅的函數(shù)式編程語(yǔ)言》

惰性求值

惰性求值,也叫做非嚴(yán)格求值,它會(huì)按需調(diào)用并推遲執(zhí)行,它是一種直到需要時(shí)才計(jì)算函數(shù)結(jié)果的求值策略, 這對(duì)函數(shù)式編程特別有用。比如有行代碼是 x = func(),調(diào)用這個(gè)func()函數(shù)得到的返回值會(huì)賦值給x。 但是x等于什么一開(kāi)始并不重要,直到需要用到x的時(shí)候。等到需要用x的時(shí)候才調(diào)用func()就是惰性求值。

這一策略可以讓性能明顯增強(qiáng),特別是當(dāng)使用方法鏈和數(shù)組這些函數(shù)式程序員最喜愛(ài)的程序流技術(shù)的時(shí)候。 惰性求值讓人興奮的一個(gè)優(yōu)點(diǎn)是讓無(wú)限序列成為可能。因?yàn)樵谒鼘?shí)在無(wú)法繼續(xù)延遲之前,什么都不需要被真正計(jì)算出來(lái)。 它可以是這個(gè)樣子:

// 理想化的JavaScript偽代碼:
var infinateNums = range(1 to infinity);
var tenPrimes = infinateNums.getPrimeNumbers().first(10);

這為很多可能性敞開(kāi)了大門,比如異步執(zhí)行、并行計(jì)算、組合,這只列舉了一點(diǎn)。

然而,還有個(gè)問(wèn)題,Javascript本身并不支持惰性求值,也就是說(shuō)存在讓Javascript模擬惰性求值的函數(shù)庫(kù), 這是第三章的主題。

相關(guān)文章

最新評(píng)論