縱觀JavaScript中全部必須須要掌握的重點知識中,函數是咱們在初學的時候最容易忽視的一個知識點。在學習的過程當中,可能會有不少人、不少文章告訴你面向對象很重要,原型很重要,但是卻不多有人告訴你,面向對象中全部的重點難點,幾乎都與函數息息相關。css
包括我以前幾篇文章介紹的執行上下文,變量對象,閉包,this等,都是圍繞函數來展開。react
我知道不少人在學習中,很急切的但願本身快一點開始學習面向對象,學習模塊,學習流行框架,而後迅速成爲高手。可是我能夠很負責的告訴你,關於函數的這些基礎東西沒理解到必定程度,那麼你的學習進展必定是舉步維艱的。git
因此,你們必定要重視函數!github
關於函數在實際開發中的應用,大致能夠總結爲函數聲明、函數表達式、匿名函數、自執行函數。面試
函數聲明express
咱們知道,JavaScript中,有兩種聲明方式,一個是使用var
的變量聲明,另外一個是使用function
的函數聲明。編程
變量對象的建立過程當中,函數聲明比變量聲明具備更爲優先的執行順序,即咱們經常提到的函數聲明提早。所以咱們在執行上下文中,不管在什麼位置聲明瞭函數,咱們均可以在同一個執行上下文中直接使用該函數。redux
1
2
3
4
5
|
fn(); // function
function fn() {
console.log('function');
}
|
函數表達式小程序
與函數聲明不一樣,函數表達式使用了var進行聲明,那麼咱們在確認他是否能夠正確使用的時候就必須依照var的規則進行判斷,即變量聲明。咱們知道使用var進行變量聲明,實際上是進行了兩步操做。數組
1
2
3
4
5
6
|
// 變量聲明
var a = 20;
// 實際執行順序
var a = undefined; // 變量聲明,初始值undefined,變量提高,提高順序次於function聲明
a = 20; // 變量賦值,該操做不會提高
|
一樣的道理,當咱們使用變量聲明的方式來聲明函數時,就是咱們經常說的函數表達式。函數表達的提高方式與變量聲明一致。
1
2
3
4
|
fn(); // 報錯
var fn = function() {
console.log('function');
}
|
上例子的執行順序爲:
1
2
3
4
5
|
var fn = undefined; // 變量聲明提高
fn(); // 執行報錯
fn = function() { // 賦值操做,此時將後邊函數的引用賦值給fn
console.log('function');
}
|
所以,因爲聲明方式的不一樣,致使了函數聲明與函數表達式在使用上的一些差別須要咱們注意,除此以外,這兩種形式的函數在使用上並沒有不一樣。
關於上面例子中,函數表達式中的賦值操做,在其餘一些地方也會被常用,咱們清楚其中的關係便可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
在構造函數中添加方法
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;
}
}
|
匿名函數
在上面咱們大概講述了函數表達式中的賦值操做。而匿名函數,顧名思義,就是指的沒有被顯示進行賦值操做的函數。它的使用場景,多做爲一個參數傳入另外一個函數中。
1
2
3
4
5
6
7
8
|
var a = 10;
var fn = function(bar, num) {
return bar() + num;
}
fn(function() {
return a;
}, 20)
|
在上面的例子中,fn的第一個參數傳入了一個匿名函數。雖然該匿名函數沒有顯示的進行賦值操做,咱們沒有辦法在外部執行上下文中引用到它,可是在fn函數內部,咱們將該匿名函數賦值給了變量bar,保存在了fn變量對象的arguments對象中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 變量對象在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中,沒有塊級做用域,所以咱們經常使用函數自執行的方式來模仿塊級做用域,這樣就提供了一個獨立的執行上下文,結合閉包,就爲模塊化提供了基礎。
1
2
3
|
(function() {
// ...
})();
|
一個模塊每每能夠包括:私有變量、私有方法、公有變量、公有方法。
根據做用域鏈的單向訪問,外面可能很容易知道在這個獨立的模塊中,外部執行環境是沒法訪問內部的任何變量與方法的,所以咱們能夠很容易的建立屬於這個模塊的私有變量與私有方法。
1
2
3
4
5
6
7
8
9
10
|
(function() {
// 私有變量
var age = 20;
var name = 'Tom';
// 私有方法
function getName() {
return `your name is ` + name;
}
})();
|
可是共有方法和變量應該怎麼辦?你們還記得咱們前面講到過的閉包的特性嗎?沒錯,利用閉包,咱們能夠訪問到執行上下文內部的變量和方法,所以,咱們只須要根據閉包的定義,建立一個閉包,將你認爲須要公開的變量和方法開放出來便可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
(function() {
// 私有變量
var age = 20;
var name = 'Tom';
// 私有方法
function getName() {
return `your name is ` + name;
}
// 共有方法
function getAge() {
return age;
}
// 將引用保存在外部執行環境的變量中,造成閉包,防止該執行環境被垃圾回收
window.getAge = getAge;
})();
|
固然,閉包在模塊中的重要做用,咱們也在講解閉包的時候已經強調過,可是這個知識點真的過重要,須要咱們反覆理解而且完全掌握,所以爲了幫助你們進一步理解閉包,咱們來看看jQuery中,是如何利用咱們模塊與閉包的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// 使用函數自執行的方式建立模塊
(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是一個有點高深莫測的東西,可是在咱們學習以前,能夠先經過簡單的方式,讓你們大體瞭解狀態管理器的實現原理,爲咱們將來的學習奠基堅實的基礎。
先來直接看代碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
// 自執行建立模塊
(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(){}()
還記得基本數據類型與引用數據類型在複製上的差別嗎?基本數據類型複製,是直接值發生了複製,所以改變後,各自相互不影響。可是引用數據類型的複製,是保存在變量對象中的引用發生了複製,所以複製以後的這兩個引用實際訪問的實際是同一個堆內存中的值。當改變其中一個時,另一個天然也被改變。以下例。
1
2
3
4
5
6
7
8
9
|
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
|
當值做爲函數的參數傳遞進入函數內部時,也有一樣的差別。咱們知道,函數的參數在進入函數後,實際是被保存在了函數的變量對象中,所以,這個時候至關於發生了一次複製。以下例。
1
2
3
4
5
6
7
8
|
var a = 20;
function fn(a) {
a = a + 10;
return a;
}
console.log(a); // 20
|
1
2
3
4
5
6
7
8
|
var a = { m: 10, n: 20 }
function fn(a) {
a.m = 20;
return a;
}
fn(a);
console.log(a); // { m: 20, n: 20 }
|
正是因爲這樣的不一樣,致使了許多人在理解函數參數的傳遞方式時,就有許多困惑。究竟是按值傳遞仍是按引用傳遞?實際上結論仍然是按值傳遞,只不過當咱們指望傳遞一個引用類型時,真正傳遞的,只是這個引用類型保存在變量對象中的引用而已。爲了說明這個問題,咱們看看下面這個例子。
1
2
3
4
5
6
7
8
9
10
11
12
|
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並非一門純函數式編程的語言,可是它使用了許多函數式編程的特性。所以瞭解這些特性可讓咱們更加了解本身寫的代碼。
函數是第一等公民
所謂」第一等公民」(first class),指的是函數與其餘數據類型同樣,處於平等地位,能夠賦值給其餘變量,也能夠做爲參數,傳入另外一個函數,或者做爲別的函數的返回值。這些場景,咱們應該見過不少。
1
2
3
4
5
6
7
8
9
|
var a = function foo() {} // 賦值
function fn(function() {}, num) {} // 函數做爲參數
// 函數做爲返回值
function var() {
return function() {
... ...
}
}
|
只用」表達式」,不用」語句」
「表達式」(expression)是一個單純的運算過程,老是有返回值;」語句」(statement)是執行某種操做,沒有返回值。函數式編程要求,只使用表達式,不使用語句。也就是說,每一步都是單純的運算,並且都有返回值。
瞭解這一點,可讓咱們本身在封裝函數的時候養成良好的習慣。藉助這個特性,咱們在學習其餘API的時候,瞭解函數的返回值也是一個十分重要的習慣。
沒有」反作用」
所謂」反作用」(side effect),指的是函數內部與外部互動(最典型的狀況,就是修改全局變量的值),產生運算之外的其餘結果。
函數式編程強調沒有」反作用」,意味着函數要保持獨立,全部功能就是返回一個新的值,沒有其餘行爲,尤爲是不得修改外部變量的值。
即所謂的只要是一樣的參數傳入,返回的結果必定是相等的。
閉包
閉包是函數式編程語言的重要特性,我也在前面幾篇文章中說了不少關於閉包的內容。這裏再也不贅述。
柯里化
理解柯里化稍微有點難,我在下一篇文章裏專門單獨來深刻分析。
在咱們本身封裝函數時,最好儘可能根據函數式編程的特色來編寫。固然在許多狀況下並不能徹底作到,好比函數中咱們經常會利用模塊中的私有變量等。
普通封裝
1
2
3
4
5
|
function add(num1, num2) {
return num1 + num2;
}
add(20, 10); // 30
|
掛載在對象上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
if(typeof Array.prototype.add !== 'function') {
Array.prototype.add = function() {
var i = 0,
len = this.length,
result = 0;
for( ; i < len; i++) {
result += this[i]
}
return result;
}
}
[1, 2, 3, 4].add() // 10
|
修改數組對象的例子,常在面試中被問到相似的,可是並不建議在實際開發中擴展原生對象。與普通封裝不同的是,由於掛載在對象的原型上咱們能夠經過this來訪問對象的屬性和方法,因此這種封裝在實際使用時會有許多的難點,所以咱們必定要掌握好this。