縱觀JavaScript中全部必須須要掌握的重點知識中,函數是咱們在初學的時候最容易忽視的一個知識點。在學習的過程當中,可能會有不少人、不少文章告訴你面向對象很重要,原型很重要,但是卻不多有人告訴你,面向對象中全部的重點難點,幾乎都與函數息息相關。javascript
包括我以前幾篇文章介紹的執行上下文,變量對象,閉包,this等,都是圍繞函數的細節來展開。css
我知道不少人在學習中,很急切的但願本身快一點開始學習面向對象,學習模塊,學習流行框架,而後迅速成爲高手。可是我能夠很負責的告訴你,關於函數的這些基礎東西沒理解到必定程度,那麼你的學習進展必定是舉步維艱的。前端
因此,你們必定要重視函數!java
固然,關於函數的重點,難點在前面幾篇文章都已經說得差很少了,這篇文章主要總結一下函數的基礎知識,並初步學習函數式編程的思惟。react
關於函數在實際開發中的應用,大致能夠總結爲函數聲明、函數表達式、匿名函數、自執行函數。express
函數聲明編程
咱們知道,JavaScript中,有兩種聲明方式,一個是使用var
的變量聲明,另外一個是使用function
的函數聲明。redux
在前端基礎進階(三):變量對象詳解中我有提到過,變量對象的建立過程當中,函數聲明比變量聲明具備更爲優先的執行順序,即咱們經常提到的函數聲明提早。所以咱們在執行上下文中,不管在什麼位置聲明瞭函數,咱們均可以在同一個執行上下文中直接使用該函數。小程序
fn(); // function function fn() { console.log('function'); }
函數表達式 segmentfault
與函數聲明不一樣,函數表達式使用了var進行聲明,那麼咱們在確認他是否能夠正確使用的時候就必須依照var的規則進行判斷,即變量聲明。咱們知道使用var進行變量聲明,實際上是進行了兩步操做。
// 變量聲明 var a = 20; // 實際執行順序 var a = undefined; // 變量聲明,初始值undefined,變量提高,提高順序次於function聲明 a = 20; // 變量賦值,該操做不會提高
一樣的道理,當咱們使用變量聲明的方式來聲明函數時,就是咱們經常說的函數表達式。函數表達的提高方式與變量聲明一致。
fn(); // 報錯 var fn = function() { console.log('function'); }
上例子的執行順序爲:
var fn = undefined; // 變量聲明提高 fn(); // 執行報錯 fn = function() { // 賦值操做,此時將後邊函數的引用賦值給fn console.log('function'); }
所以,因爲聲明方式的不一樣,致使了函數聲明與函數表達式在使用上的一些差別須要咱們注意,除此以外,這兩種形式的函數在使用上並沒有不一樣。
關於上面例子中,函數表達式中的賦值操做,在其餘一些地方也會被常用,咱們清楚其中的關係便可。
在構造函數中添加方法 function Person(name) { this.name = name; this.age = age; // 在構造函數內部中添加方法 this.getAge = function() { return this.age; } this. } // 給原型添加方法 Person.prototype.getName = function() { return this.name; } // 在對象中添加方法 var a = { m: 20, getM: function() { return this.m; } }
匿名函數
在上面咱們大概講述了函數表達式中的賦值操做。而匿名函數,顧名思義,就是指的沒有被顯示進行賦值操做的函數。它的使用場景,多做爲一個參數傳入另外一個函數中。
var a = 10; var fn = function(bar, num) { return bar() + num; } fn(function() { return a; }, 20)
在上面的例子中,fn的第一個參數傳入了一個匿名函數。雖然該匿名函數沒有顯示的進行賦值操做,咱們沒有辦法在外部執行上下文中引用到它,可是在fn函數內部,咱們將該匿名函數賦值給了變量bar,保存在了fn變量對象的arguments對象中。
// 變量對象在fn上下文執行過程當中的建立階段 VO(fn) = { arguments: { bar: undefined, num: undefined, length: 2 } } // 變量對象在fn上下文執行過程當中的執行階段 // 變量對象變爲活動對象,並完成賦值操做與執行可執行代碼 VO -> AO AO(fn) = { arguments: { bar: function() { return a }, num: 20, length: 2 } }
因爲匿名函數傳入另外一個函數以後,最終會在另外一個函數中執行,所以咱們也經常稱這個匿名函數爲回調函數。關於匿名函數更多的內容,我會在下一篇深刻探討柯里化的文章中進行更加詳細講解。
匿名函數的這個應用場景幾乎承擔了函數的全部難以理解的知識點,所以咱們必定要對它的這些細節瞭解的足夠清楚,若是對於變量對象的演變過程你還看不太明白,必定要回過頭去看這篇文章:前端基礎進階(三):變量對象詳解
函數自執行與塊級做用域
在ES5中,沒有塊級做用域,所以咱們經常使用函數自執行的方式來模仿塊級做用域,這樣就提供了一個獨立的執行上下文,結合閉包,就爲模塊化提供了基礎。而函數自執行,實際上是匿名函數的一種應用。
(function() { // ... })();
一個模塊每每能夠包括:私有變量、私有方法、公有變量、公有方法。
根據做用域鏈的單向訪問,外面可能很容易知道在這個獨立的模塊中,外部執行環境是沒法訪問內部的任何變量與方法的,所以咱們能夠很容易的建立屬於這個模塊的私有變量與私有方法。
(function() { // 私有變量 var age = 20; var name = 'Tom'; // 私有方法 function getName() { return `your name is ` + name; } })();
可是共有方法和變量應該怎麼辦?你們還記得咱們前面講到過的閉包的特性嗎?沒錯,利用閉包,咱們能夠訪問到執行上下文內部的變量和方法,所以,咱們只須要根據閉包的定義,建立一個閉包,將你認爲須要公開的變量和方法開放出來便可。
若是你對閉包瞭解不夠, 前端基礎進階(四):詳細圖解做用域鏈與閉包應該能夠幫到你。
(function() { // 私有變量 var age = 20; var name = 'Tom'; // 私有方法 function getName() { return `your name is ` + name; } // 共有方法 function getAge() { return age; } // 將引用保存在外部執行環境的變量中,造成閉包,防止該執行環境被垃圾回收 window.getAge = getAge; })();
固然,閉包在模塊中的重要做用,咱們也在講解閉包的時候已經強調過,可是這個知識點真的過重要,須要咱們反覆理解而且完全掌握,所以爲了幫助你們進一步理解閉包,咱們來看看jQuery中,是如何利用咱們模塊與閉包的。
// 使用函數自執行的方式建立模塊 (function(window, undefined) { // 聲明jQuery構造函數 var jQuery = function(name) { // 主動在構造函數中,返回一個jQuery實例 return new jQuery.fn.init(name); } // 添加原型方法 jQuery.prototype = jQuery.fn = { constructor: jQuery, init:function() { ... }, css: function() { ... } } jQuery.fn.init.prototype = jQuery.fn; // 將jQuery更名爲$,並將引用保存在window上,造成閉包,對外開發jQuery構造函數,這樣咱們就能夠訪問全部掛載在jQuery原型上的方法了 window.jQuery = window.$ = jQuery; })(window); // 在使用時,咱們直接執行了構造函數,由於在jQuery的構造函數中經過一些手段,返回的是jQuery的實例,因此咱們就不用再每次用的時候在本身new了 $('#div1');
在這裏,咱們只須要看懂閉包與模塊的部分就好了,至於內部的原型鏈是如何繞的,爲何會這樣寫,我在講面向對象的時候會爲你們慢慢分析。舉這個例子的目的所在,就是但願你們可以重視函數,由於在實際開發中,它無處不在。
接下來我要分享一個高級的,很是有用的模塊的應用。當咱們的項目愈來愈大,那麼須要保存的數據與狀態就愈來愈多,所以,咱們須要一個專門的模塊來維護這些數據,這個時候,有一個叫作狀態管理器的東西就應運而生。對於狀態管理器,最出名的,我想非redux莫屬了。雖然對於還在學習中的你們來講,redux是一個有點高深莫測的東西,可是在咱們學習以前,能夠先經過簡單的方式,讓你們大體瞭解狀態管理器的實現原理,爲咱們將來的學習奠基堅實的基礎。
先來直接看代碼。
// 自執行建立模塊 (function() { // states 結構預覽 // states = { // a: 1, // b: 2, // m: 30, // o: {} // } var states = {}; // 私有變量,用來存儲狀態與數據 // 判斷數據類型 function type(elem) { if(elem == null) { return elem + ''; } return toString.call(elem).replace(/[\[\]]/g, '').split(' ')[1].toLowerCase(); } /** * @Param name 屬性名 * @Description 經過屬性名獲取保存在states中的值 */ function get(name) { return states[name] ? states[name] : ''; } function getStates() { return states; } /* * @param options {object} 鍵值對 * @param target {object} 屬性值爲對象的屬性,只在函數實現時遞歸中傳入 * @desc 經過傳入鍵值對的方式修改state樹,使用方式與小程序的data或者react中的setStates相似 */ function set(options, target) { var keys = Object.keys(options); var o = target ? target : states; keys.map(function(item) { if(typeof o[item] == 'undefined') { o[item] = options[item]; } else { type(o[item]) == 'object' ? set(options[item], o[item]) : o[item] = options[item]; } return item; }) } // 對外提供接口 window.get = get; window.set = set; window.getStates = getStates; })() // 具體使用以下 set({ a: 20 }); // 保存 屬性a set({ b: 100 }); // 保存屬性b set({ c: 10 }); // 保存屬性c // 保存屬性o, 它的值爲一個對象 set({ o: { m: 10, n: 20 } }) // 修改對象o 的m值 set({ o: { m: 1000 } }) // 給對象o中增長一個c屬性 set({ o: { c: 100 } }) console.log(getStates())
我之因此說這是一個高級應用,是由於在單頁應用中,咱們極可能會用到這樣的思路。根據咱們提到過的知識,理解這個例子其實很簡單,其中的難點估計就在於set方法的處理上,由於爲了具備更多的適用性,所以作了不少適配,用到了遞歸等知識。若是你暫時看不懂,沒有關係,知道如何使用就好了,上面的代碼能夠直接運用於實際開發。記住,當你須要保存的狀態太多的時候,你就想到這一段代碼就好了。
函數自執行的方式另外還有其餘幾種寫法,諸如!function(){}()
,+function(){}()
還記得基本數據類型與引用數據類型在複製上的差別嗎?基本數據類型複製,是直接值發生了複製,所以改變後,各自相互不影響。可是引用數據類型的複製,是保存在變量對象中的引用發生了複製,所以複製以後的這兩個引用實際訪問的實際是同一個堆內存中的值。當改變其中一個時,另一個天然也被改變。以下例。
var a = 20; var b = a; b = 10; console.log(a); // 20 var m = { a: 1, b: 2 } var n = m; n.a = 5; console.log(m.a) // 5
當值做爲函數的參數傳遞進入函數內部時,也有一樣的差別。咱們知道,函數的參數在進入函數後,實際是被保存在了函數的變量對象中,所以,這個時候至關於發生了一次複製。以下例。
var a = 20; function fn(a) { a = a + 10; return a; } fn(a); console.log(a); // 20
var a = { m: 10, n: 20 } function fn(a) { a.m = 20; return a; } fn(a); console.log(a); // { m: 20, n: 20 }
正是因爲這樣的不一樣,致使了許多人在理解函數參數的傳遞方式時,就有許多困惑。究竟是按值傳遞仍是按引用傳遞?實際上結論仍然是按值傳遞,只不過當咱們指望傳遞一個引用類型時,真正傳遞的,只是這個引用類型保存在變量對象中的引用而已。爲了說明這個問題,咱們看看下面這個例子。
var person = { name: 'Nicholas', age: 20 } function setName(obj) { // 傳入一個引用 obj = {}; // 將傳入的引用指向另外的值 obj.name = 'Greg'; // 修改引用的name屬性 } setName(person); console.log(person.name); // Nicholas 未被改變
在上面的例子中,若是person是按引用傳遞,那麼person就會自動被修改成指向其name屬性值爲Gerg的新對象。可是咱們從結果中看到,person對象並未發生任何改變,所以只是在函數內部引用被修改而已。
雖然JavaScript並非一門純函數式編程的語言,可是它使用了許多函數式編程的特性。所以瞭解這些特性可讓咱們更加了解本身寫的代碼。
當咱們想要使用一個函數時,一般狀況下其實就是想要將一些功能,邏輯等封裝起來。相信你們對於封裝這個概念並不陌生。
咱們一般經過函數封裝來完成一件事情。例如,我想要計算任意三個數的和,咱們就能夠將這三個數做爲參數,封裝一個簡單的函數。
function add(a, b, c) { return a + b + c; }
當咱們想要計算三個數的和時,直接調用該方法便可。
add(1, 2, 3); // 6
固然,當咱們想要作的事情比較簡單的時候,可能還看不出來封裝成爲函數以後帶來的便利。若是咱們想要作的事情稍微複雜一點呢。例如我想要計算一個數組中的全部子項目的和。
function mergeArr(arr) { var result = 0; for(var i = 0; i < arr.length; i++) { result += arr[i] } return result; }
若是咱們不經過函數封裝的方式,那麼再每次想要實現這個功能時,就不得不從新使用一次for循環,這樣的後果就是咱們的代碼中充斥着愈來愈多的重複代碼。而封裝以後,當咱們想要再次作這件事情的時候,只須要一句話就能夠了。
mergeArr([1, 2, 3, 4, 5]);
固然,我相信你們對於函數封裝的意義都應該有很是明確的認知,可是咱們要面臨的問題是,當咱們想要去封裝一個函數時,如何作纔是最佳實踐呢?
函數式編程能給咱們答案。
咱們在初學時,每每會情不自禁的使用命令式編程的風格來完成咱們想要乾的事情。由於命令式編程更加的簡單,直白。例如咱們如今有一個數組,array = [1, 3, 'h', 5, 'm', '4']
,如今想要找出這個數組中的全部類型爲number的子項。當咱們使用命令式編程思惟時,可能就會直接這樣作。
var array = [1, 3, 'h', 5, 'm', '4']; var res = []; for(var i = 0; i < array.length; i ++) { if (typeof array[i] === 'number') { res.push(array[i]); } }
在這種實現方式中,咱們平鋪直敘的實現了咱們的目的。這樣作的問題在於,當咱們在另外的時刻,想要找出另一個數組中全部的子項時,咱們不得不把一樣的邏輯再寫一次。當出現次數變多時,咱們的代碼也變得更加糟糕且難以維護。
而函數式編程的思惟則建議咱們將這種會屢次出現的功能封裝起來以備調用。
function getNumbers(array) { var res = []; array.forEach(function(item) { if (typeof item === 'number') { res.push(item); } }) return res; } // 以上是咱們的封裝,如下是功能實現 var array = [1, 3, 'h', 5, 'm', '4']; var res = getNumbers(array);
所以當咱們將功能封裝以後,咱們實現一樣的功能時,只須要寫一行代碼。而若是將來需求變更,或者稍做修改,咱們只須要對getNumbers方法進行調整就能夠了。並且咱們在使用時,只須要關心這個方法能作什麼,而不用關心他具體是怎麼實現的。這也是函數式編程思惟與命令式不一樣的地方之一。
函數式編程思惟還具備如下幾個特徵。
函數是第一等公民
所謂"第一等公民"(first class),指的是函數與其餘數據類型同樣,處於平等地位,能夠賦值給其餘變量,也能夠做爲參數,傳入另外一個函數,或者做爲別的函數的返回值。這些場景,咱們應該見過不少。
var a = function foo() {} // 賦值 function fn(function() {}, num) {} // 函數做爲參數 // 函數做爲返回值 function var() { return function() { ... ... } }
固然,這都是JavaScript的基本概念。可是我想不少人,甚至包括正在閱讀的你本身均可能會無視這些概念。能夠用一個簡單的例子來驗證一下。
咱們先自定義這樣一個函數。
function delay() { console.log('5000ms以後執行該方法.'); }
如今要作的是,若是要求你結合setTimeout方法,讓delay方法延遲5000ms執行,應該怎麼作?
其實很簡單,對不對,直接這樣就能夠了。
var timer = setTimeout(function() { delay(); }, 5000);
那麼如今問題來了,若是你對函數是一等公民有一個深入的認知,我想你會發現上面這種寫法實際上是有一些問題的。因此思考一下,問題出在哪裏?
函數既然可以做爲一個參數傳入另一個函數,那麼咱們是否是能夠直接將delay做爲setTimeout的第一個參數,而不用額外的多加一層匿名函數呢?
所以,其實最正確的解法應該這樣寫。
var timer = setTimeout(delay, 5000);
固然,若是你已經提早想到這樣作了,那麼恭喜你,說明你在JavaScript上比普通人更有天賦。其實第一種糟糕的方式不少人都在用,包括有多年工做經驗的人也沒有徹底避免。而他們甚至還不知道本身問題出在什麼地方。
在將來的實踐中,你還會遇到更多相似的場景。爲了驗證讀者朋友們的理解,咱們不妨來思考一下如何優化下面的代碼。
function getUser(path, callback) { return $.get(path, function(info) { return callback(info); }) } getUser('/api/user', function(resp) { // resp爲成功請求以後返回的數據 console.log(resp); })
優化的原理和setTimeout的例子如出一轍,我這裏賣個關子,不打算告訴你們結論,僅提示一句,getUser優化以後,僅有一句代碼。考驗你們學習成果的時候到了 ^ ^。
只用"表達式",不用"語句"
"表達式"(expression)是一個單純的運算過程,老是有返回值;"語句"(statement)是執行某種操做,沒有返回值。函數式編程要求,只使用表達式,不使用語句。也就是說,每一步都是單純的運算,並且都有返回值。
假如咱們的項目中,多處須要改變某個元素的背景色。所以咱們能夠這樣封裝一下。
var ele = document.querySelector('.test'); function setBackgroundColor(color) { ele.style.backgroundColor = color; } // 多處使用 setBackgroundColor('red'); setBackgroundColor('#ccc');
咱們能夠很明顯的感覺到,setBackgroundColor封裝的僅僅只是一條語句。這並非理想的效果。函數式編程指望一個函數有輸入,也有輸出。所以良好的習慣應該以下作。
function setBackgroundColor(ele, color) { ele.style.backgroundColor = color; return color; } // 多處使用 var ele = document.querySelector('.test'); setBackgroundColor(ele, 'red'); setBackgroundColor(ele, '#ccc');
瞭解這一點,可讓咱們本身在封裝函數的時候養成良好的習慣。
純函數
相同的輸入總會獲得相同的輸出,而且不會產生反作用的函數,就是純函數。
所謂"反作用"(side effect),指的是函數內部與外部互動(最典型的狀況,就是修改全局變量的值),產生運算之外的其餘結果。
函數式編程強調沒有"反作用",意味着函數要保持獨立,全部功能就是返回一個新的值,沒有其餘行爲,尤爲是不得修改外部變量的值。
即所謂的只要是一樣的參數傳入,返回的結果必定是相等的。
例如咱們指望封裝一個函數,可以獲得傳入數組的最後一項。那麼能夠經過下面兩種方式來實現。
function getLast(arr) { return arr[arr.length]; } function getLast_(arr) { return arr.pop(); } var source = [1, 2, 3, 4]; var last = getLast(source); // 返回結果4 原數組不變 var last_ = getLast_(source); // 返回結果4 原數據最後一項被刪除
getLast與getLast_雖然一樣可以得到數組的最後一項值,可是getLast_改變了原數組。而當原始數組被改變,那麼當咱們再次調用該方法時,獲得的結果就會變得不同。這樣不可預測的封裝方式,在咱們看來是很是糟糕的。它會把咱們的數據搞得很是混亂。在JavaScript原生支持的數據方法中,也有許多不純的方法,咱們在使用時須要很是警戒,咱們要清晰的知道原始數據的改變是否會留下隱患。
var source = [1, 2, 3, 4, 5]; source.slice(1, 3); // 純函數 返回[2, 3] source不變 source.splice(1, 3); // 不純的 返回[2, 3, 4] source被改變 source.pop(); // 不純的 source.push(6); // 不純的 source.shift(); // 不純的 source.unshift(1); // 不純的 source.reverse(); // 不純的 // 我也不能短期知道如今source被改變成了什麼樣子,乾脆從新約定一下 source = [1, 2, 3, 4, 5]; source.concat([6, 7]); // 純函數 返回[1, 2, 3, 4, 5, 6, 7] source不變 source.join('-'); // 純函數 返回1-2-3-4-5 source不變
閉包
閉包是函數式編程語言的重要特性,我也在前面幾篇文章中說了不少關於閉包的內容。這裏再也不贅述。
柯里化
下一章。