定義函數javascript
在 JS 中定義函數的方式有兩種:函數聲明
和 函數表達式
。java
函數聲明
的語法爲:數組
function functionName(arg0, arg1, arg2) {
// 函數體;
}
複製代碼
函數聲明的一個重要特徵就是 函數聲明提高 ,即在執行代碼以前會先讀取函數聲明,這意味着能夠把函數聲明放在調用它的語句後面:瀏覽器
sayHi(); // 'hi';
sayHi() {
alert('hi');
}
複製代碼
第二種建立函數的形式是 函數表達式
。函數表達式有建立幾種不一樣的語法形式,下面是最多見的一種:閉包
var functionName = function(arg0, arg1, arg2) {
// 函數體;
}
複製代碼
這看來好像是常規的變量複製語句,即建立一個函數並將它賦值給變量 functionName,在這種狀況下建立的函數叫作 匿名函數 ( 也稱爲 Lambda 拉姆達函數 ),在使用前必須先賦值。以下面的代碼會報錯:app
sayHi(); // 錯誤,函數還不存在;
var sayHi = function(){
alert('hi');
}
複製代碼
理解函數提高的關鍵,就是理解函數聲明與函數表達式之間的區別。例如,執行如下代碼的結果可能會讓人意想不到:函數
if(condition) {
function sayHi() {
alert('hi');
}
} else {
function sayHi() {
alert('Yo~');
}
}
複製代碼
表面上看,上述代碼會在 condition 爲 true 時使用一個 sayHi() 的定義,不然就使用另外一個定義。實際上,這在 ECMAScript 中屬於無效語法,JavaScript 引擎會嘗試修正錯誤,將其轉換爲合理的狀態,大多數瀏覽器會返回第二個聲明,忽略 condition。ui
不過,若是是使用函數表達式,那就沒什麼問題:this
var sayHi;
if(condition) {
sayHi = function() {
alert('hi');
}
} else {
sayHi = function() {
alert('Yo~');
}
}
複製代碼
這樣不一樣的函數將根據不一樣的 condition 被賦值給 sayHi。spa
遞歸
遞歸函數是在一個函數經過名字調用自身的狀況下構成的:
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
複製代碼
這是一個經典的遞歸階乘函數。雖然這個函數表面上看起來沒有問題,但下面的代碼卻可能致使它出錯:
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); // error!
複製代碼
在調用 anotherFactorial() 時,因爲必須執行 factorial(),而此時 factorial 已再也不是函數,因此會致使錯誤。
在這種狀況下,可使用 arguments.callee
解決問題。
所以能夠用它來實現對函數的遞歸調用:
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
複製代碼
經過使用 arguments.callee 代替函數名,能夠確保不管怎樣調用函數都不會出問題。所以,在編寫遞歸函數時,使用 arguments.callee 總比使用函數名更保險。
不過在嚴格模式下,不能經過腳本訪問 arguments.callee,訪問這個屬性會致使錯誤。but~ 咱們可使用命名一個函數表達式來完成一樣的效果:
var factorial = (function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1);
}
});
複製代碼
這樣遞歸調用仍能正常完成。
閉包
很多童鞋老是會混淆 匿名函數
和 閉包
這兩個概念。 匿名函數
是沒有實際名字的函數;而 閉包
是指有權訪問另外一個函數做用域中的變量的函數。
而建立閉包的常見方式,就是在一個函數內部建立另外一個函數:
function compare(name) {
return function(obj1, obj2) {
var value1 = obj1[name]; // 能夠訪問到外部函數中的變量 name;
var value2 = obj2[name];
if (value1 < value2) {
return -1;
} else {
return 1
}
};
}
複製代碼
之因此還可以訪問這個變量,是由於這個內部函數的做用域鏈中包含 compare() 的做用域。
而瞭解做用域的細節,對完全理解閉包相當重要:
當某個函數被調用時,會建立一個執行環境及相應的做用域鏈。而後,使用 arguments 和其它命名參數的值來初始化函數的活動對象。在做用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,......直至做爲做用域鏈終點的全局執行環境。
在函數執行過程當中,爲讀取和寫入變量的值,就須要在做用域鏈中查找變量,來看下面的例子:
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else {
return 1;
}
}
var result = compare(5, 10);
複製代碼
上述代碼首先定義了 compare() 函數。而後又在全局做用域中調用了它。
當第一次調用 compare() 時,會建立一個包含 this、arguments、value1 和 value2 的活動對象。全局執行環境的變量對象(包含 this、result 和 compare)在 compare() 執行環境的做用域中則處於第二位。
下圖展現了包含上述關係的 compare() 函數執行時的做用域鏈:
全局環境的變量對象始終存在,而像 compare() 函數這樣的局部環境的變量對象,則只在函數執行的過程當中存在。
顯然,做用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。
不管何時在函數中訪問一個變量時,都會從做用域鏈中搜索具備相應名字的變量。通常來說,當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局做用域。
但,閉包的狀況又有所不一樣:
function compare(name) {
return function(obj1, obj2) {
var value1 = obj1[name]; // 能夠訪問到外部函數中的變量 name;
var value2 = obj2[name];
if (value1 < value2) {
return -1;
} else {
return 1
}
};
}
複製代碼
在匿名函數從 compare() 中被返回後,它的做用域鏈被初始化爲包含 compare() 函數的活動對象和全局對象。
更爲重要的是,compare() 函數在執行完畢後,其活動對象也不會被銷燬,由於匿名函數的做用域鏈仍在引用這個活動對象,直到匿名函數被銷燬後,compare() 的活動對象纔會被銷燬。
// 建立函數;
var compareNames = compare('name');
// 調用函數;
var result = compareNames({name: 'Fly_001'}, { name: 'juejin' });
// 解除對匿名函數的引用,以便釋放內存;
compareNmaes = null;
複製代碼
Tips: 因爲閉包會攜帶包含它的函數的做用域,所以會比其它函數佔用更多的內存,因此過分使用閉包可能會致使內存佔用過多。
閉包與變量
做用域鏈的這種配置機制引出了一個值得注意的反作用,即閉包只能取得包含函數中任何變量的最後一個值。
別忘了閉包保存的是整個變量對象,而不是某個特殊的變量。
下面是一個經典的例子:
function createFunctions() {
var result = [];
for (var i = 0; i < 10; i ++) {
result[i] = function() {
return i;
};
}
return result;
}
複製代碼
表面上看,彷佛每一個函數都應該返回本身的索引值,但實際上,每一個函數都返回 10。
由於每一個函數的做用域鏈中都保存着 createFunctions() 函數的活動對象,因此它們引用的都是同一個變量 i。
當 createFunctions() 函數返回時,變量 i 的值是 10,此時每一個函數都引用着變量 i 的同一個變量對象,因此在每一個函數內部 i 的值都是 10。
不過,咱們能夠經過建立另外一個匿名函數強制讓閉包的行爲符合預期:
function createFunctions() {
var result = [];
for (var i = 0; i < 10; i ++) {
result[i] = function(num) {
return function() {
return num;
};
}(i);
}
return result;
}
複製代碼
在這個版本中,咱們沒有直接把閉包賦值給數組,而是定義了一個匿名函數,並將當即執行該匿名函數的結果賦給數組。
這裏的匿名函數有一個參數 num,也就是最終要返回的值。
在調用每一個匿名函數時,咱們傳入了變量 i,並會將變量 i 的當前值複製給參數 num,而在這個匿名函數內部,又建立並返回了一個訪問 num 的閉包。
因此 result 數組中的每一個函數都有本身 num 變量的一個副本,所以就能夠返回各自不一樣的數值了。
另外,咱們如今能夠用 ES6 中的 let
命令實現上述效果:
function createFunctions() {
var result = [];
for (let i = 0; i < 10; i ++) {
result[i] = function() {
return i;
};
}
return result;
}
複製代碼
Tips: 由於 let
聲明的變量只在所在的塊級做用域有效,因此每一次循環的變量 i 都是一個新的變量。
閉包中的 this 對象
咱們知道,this 對象是在運行時基於函數的執行環境綁定的:在全局函數中,this 等於 window;而當函數被做爲某個對象的方法調用時,this 等於那個對象。
不過, 匿名函數的執行環境具備全局性, 所以其 this 對象一般指向 window (在經過 call() 或 apply() 改變函數執行環境的狀況下,this 就會指向其它對象)。
但有時因爲編寫閉包的方式不一樣,這一點可能不會那麼明顯:
var name = 'The Window';
var object = {
name: 'My Object',
getName: function() {
return function() {
return this.name;
};
}
};
alert(object.getName()()); // 'The Window', ( 在非嚴格模式下 )
複製代碼
不過,把外部做用域中的 this 對象保存在一個閉包可以訪問到的變量裏,就可讓閉包訪問該對象了:
var name = 'The Window';
var object = {
name: 'My Object',
getName: function() {
var that = this;
return function() {
return that.name;
};
}
};
alert(object.getName()()); // 'My Object';
複製代碼
在定義匿名函數以前,咱們把 this 對象賦值給 that 變量,且閉包也能夠訪問這個變量,即便在函數返回後,that 也仍然引用着 object,因此調用 object.getName()() 就返回了 'My Object'。
JavaScript 中的函數表達式和閉包都是極其有用的特性,利用它們能夠實現不少功能。
不過,由於建立閉包必須維護額外的做用域,過分使用它們可能會佔用大量內存,因此不要爲了閉包而閉包~
關於函數和閉包的淺薄知識就先講到這裏,若有不正確的地方,歡迎各位指正。【比心】