第七章:函數表達式
本章內容:
- 函數表達式的特征
- 使用函數實現遞歸
- 使用閉包定義私有變量
定義函數的方式有兩種,一種是函數聲明,另一種是函數表達式
// 函數聲明
function functionName(arg0){
// 函數體
};
// 函數表達式
var functionName = function(arg0){
// 函數體
}
關于函數聲明,它有個特征是函數聲明提升
。這個在第五章節有講過。意思在執行代碼之前會先讀取聲明函數。
關于函數表達式,就是創建了一個匿名函數(anonymous function)再賦值給一個變量。
7.2 閉包
閉包是值有權訪問另一個函數作用域中的變量的函數。創建閉包的常見方式,就是在函數內部創建另一個函數。來看例子:
function createComparisionFunction(propertyName){
return function(obj1, obj2){
// 下面兩行能獲取createComparisionFunction中的propertyName屬性就是因為閉包
var value1 = obj1[propertyName];
var value2 = obj2[propertyName];
if(value1 < value2){
return -1;
} else if(value1 > value2){
return 1;
} else {
return 0
}
}
}
4、5兩行能夠訪問外部函數變量propertyName。即使這個函數被返回了,而且是在其他地方被調用。之所以能夠訪問變量,是因為內部函數的作用域鏈中包含了createComparisionFunction的變量對象。要理解細節,則從函數被調用時,發生什么開始入手。
當某個函數被調用,會創建一個執行環境(execution context)以及創建相應的作用域鏈,然后,使用arguments和其他命名參數來初始化函數的變量對象。但在作用域鏈中個,外部的變量對象處于第二位,外部函數的外部函數的變量對象處于第三位...直到作為作用域鏈終點的全局變量對象。
function compare(value1, value2){
if(value1 < value2){
return -1;
} else if(value1 > value2) {
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
在上面代碼中,在全局作用域調用compare()函數時,會創建一個包含arguments、value1、value2的活動對象。全局執行環境的變量對象(包含result和compare)在compare()執行環境的作用域鏈中出于第二位。關系如下圖:
后臺的每個執行環境都有一個表示變量的對象-變量對象。全局環境中的變量對象始終存在,而像compare()函數這樣的局部環境的變量對象,則只有在函數執行的過程中存在。在創建compare()函數時,就創建一個預先包含全局變量對象的作用域鏈,這個作用域鏈會保存在內部[[Scope]]中。在調用compare()函數的時候,就會為函數創建一個執行環境,然后復制[[Scope]]屬性中的對象構建起執行環境中的作用域鏈。之后,又有一個活動對象(當前的變量對象即為活動對象)被創建并推入執行環境作用域鏈的前端。
作用域鏈的本質是一個指向變量對象的指針列表
無論在什么時候在函數中訪問一個變量時,都會從作用域鏈中搜索具有相應名字的變量。一般來講,當函數執行完畢后,局部的活動對象就會銷毀,內存僅保存全局的變量對象。 但是閉包的情況又有所不同:
在另一個函數內部定義的函數會將包含函數(即外部函數)的變量對象添加到它的作用域鏈中。因此,在createComparisionFunction內部定義的匿名函數的作用域鏈中,實際上包含外部函數的createComparisionFunction的變量對象。
var compare = createComparisionFunction("name");
var result = compare({name:'Nicholas'},{name:'Greg'});
匿名函數從createComparisionFunction返回后,它的作用域鏈被初始化包含createComparisionFunction()函數的活動對象與全局的變量對象。
這樣匿名函數就可以訪問createComparisionFunction()中定義的變量。更為重要的是,createComparisionFunction函數執行完畢后,其活動對象也不會銷毀,因為匿名函數的作用域鏈仍然引用這個活動對象,但它的活動對象一直保存在內存中;知道匿名函數被銷毀后(compare = null),createComparisionFunction的活動對象才會被銷毀。
7.2.1 閉包與變量
作用域鏈的這種配置機制引出了一個值得注意的副作用,即閉包只能讀取包含函數中任何變量的最后一個值(可能中間變量值發生多次變換)。別忘了閉包保存的事整個變量對象,而不是某個特殊的變量值。
function createFunction(){
var result = [];
for(var i=0; i<10; i++){
result[i] = function(){
return i;
}
}
return result;
}
var selfFunction = createFunction();
console.log(selfFunction[0]()); // 10
console.log(selfFunction[1]()); // 10
這個函數會返回一個函數數組。表面上看,似乎每一個數組都應該返回自己的索引值,即位置0的函數返回0,位置1的函數返回1,以此類推。但事實上,每個函數都返回10。因為每個函數的作用域鏈中都包含了createFunction的變量對象,所以他們都指向了同一個變量i。當createFunction返回之后變量i的值就變成了10。所以每個函數查找的i都是10。 但我們可以創建另外一個匿名函數強制生成一個閉包。
function createFunction(){
var result = [];
for(var i=0; i<10; i++){
result[i] = (function(num){
return function(){
return num
}
})(i)
}
return result;
}
var selfFunction = createFunction();
console.log(selfFunction[0]()); // 0
console.log(selfFunction[1]()); // 1
當調用匿名函數時,我們傳遞了變量i.由于變量是按值傳遞的,所以這回將變量i的當前值賦值給參數num。而在這個匿名函數的內部,又創建了一個返回num的閉包。這樣result每個函數都有自己的一份num變量副本。
延伸閱讀1: 詳細圖解作用域鏈與閉包
閉包是一種特殊的對象。
它由兩部分組成。執行上下文(代號A),以及在該執行上下文中創建的函數(代號B)。
當B執行時,如果訪問了A中變量對象中的值,那么閉包就會產生。
在大多數理解中,包括許多著名的書籍,文章里都以函數B的名字代指這里生成的閉包。而在chrome中,則以執行上下文A的函數名代指閉包。
因此我們只需要知道,一個閉包對象,由A、B共同組成,在以后的篇幅中,我將以chrome的標準來稱呼。
// demo01
function foo() {
var a = 20;
var b = 30;
function bar() {
return a + b;
}
return bar;
}
var bar = foo();
bar();
上面的例子,首先有執行上下文foo,在foo中定義了函數bar,而通過對外返回bar的方式讓bar得以執行。當bar執行時,訪問了foo內部的變量a,b。因此這個時候閉包產生。
JavaScript擁有自動的垃圾回收機制,關于垃圾回收機制,有一個重要的行為,那就是,當一個值,在內存中失去引用時,垃圾回收機制會根據特殊的算法找到它,并將其回收,釋放內存。
而我們知道,函數的執行上下文,在執行完畢之后,生命周期結束,那么該函數的執行環境就會失去引用。其占用的內存空間很快就會被垃圾回收器釋放。可是閉包的存在,會阻止這一過程。
var fn = null;
function foo(){
var a = 2;
function innerFoo(){
console.log(a);
}
fn = innerFoo;
}
function bar(){
fn(); //此處的保留的innerFoo的引用
}
foo();
bar(); //2
在上面的例子中,foo()
執行完畢之后,按照常理,其執行環境生命周期會結束,所占內存被垃圾收集器釋放。但是通過fn = innerFoo
,函數innerFoo的引用被保留了下來,復制給了全局變量fn。這個行為,導致了foo的變量對象,也被保留了下來。于是,函數fn在函數bar內部執行時,依然可以訪問這個被保留下來的變量對象。所以此刻仍然能夠訪問到變量a的值。
這樣,我們就可以稱foo為閉包。
下圖展示了閉包foo的作用域鏈。
我們可以在chrome瀏覽器的開發者工具中查看這段代碼運行時產生的函數調用棧與作用域鏈的生成情況。如下圖。
7.2.2 關于this
作為函數的特殊對象,this對象是運行時基于函數的執行環境板定的:
- 在全局函數中,this等于window
- 而當函數作為某個對象的方法調用時,this等于那個對象。
不過,匿名函數的執行環境具有全局性,因此this對象通常指window。但有時候編寫方式的不同,這一點不那么明顯。
var name = 'window';
var object = {
name: 'my object',
getNameFun: function(){
return function(){
return this.name;
}
}
};
alert(object.getNameFun()()); //window (在全局環境中執行,this即為window)
每個函數在被調用時都會自動獲取兩個特殊變量:this
和argument
。內部函數在搜索這兩個變量時,只會搜索其活動對象為止,因此永遠不可能直接訪問外部函數中的這兩個變量。不過,把外部作用域中的this對象保存在一個閉包能夠訪問的變量中,就可以讓閉包訪問該對象了。
var name = 'window';
var object = {
name: 'my object',
getNameFun: function(){
var that = this;
return function(){
return that.name;
}
}
};
alert(object.getNameFun()()); // my object
在定義匿名函數之前,我們把this對象賦值給了一個叫that的變量。而在定義閉包之后,閉包可以訪問這個變量,
this和arguments也存在同樣的問題,如果訪問作用域中的arguments對象,必須將對該對象的引用保存到另一個閉包能夠訪問的變量中。
延伸閱讀2: 全訪問解讀this
重新回顧一下執行環境
在執行環境的創建階段,會分別生成變量對象,建立作用域鏈,確定this指向。其中變量對象與作用域鏈我們都已經仔細總結過了,而這里的關鍵,就是確定this指向。
首先我們需要得出一個非常重要一定要牢記于心的結論,this的指向,是在函數被調用的時候確定的。也就是執行環境被創建時確定的。因此,一個函數中的this指向,可以是非常靈活的。比如下面的例子中,同一個函數由于調用方式的不同,this指向了不一樣的對象。
var a = 10;
var obj = {
a: 20
}
function fn () {
console.log(this.a);
}
fn(); // 10
fn.call(obj); // 20
除此之外,在函數執行過程中,this一旦被確定,就不可更改了。
var a = 10;
var obj = {
a: 20
}
function fn () {
this = obj; // 這句話試圖修改this,運行后會報錯 ReferenceError: Invalid left-hand side in assignment
console.log(this.a);
}
fn();
1. 全局對象中的this
關 于全局對象的this,我之前在總結變量對象的時候提到過,它是一個比較特殊的存在。全局環境中的this,指向它本身。因此,這也相對簡單,沒有那么多復雜的情況需要考慮。
// 通過this綁定到全局對象
this.a2 = 20;
// 通過聲明綁定到變量對象,但在全局環境中,變量對象就是它自身
var a1 = 10;
// 僅僅只有賦值操作,標識符會隱式綁定到全局對象
a3 = 30;
// 輸出結果會全部符合預期
console.log(a1); // 10
console.log(a2); // 20
console.log(a3); // 30
2. 函數中的this
在總結函數中this指向之前,我想我們有必要通過一些奇怪的例子,來感受一下函數中this的捉摸不定。
// demo01
var a = 20;
function fn(){
console.log(this.a)
}
fun(); //20
// demo02
var a = 20;
function fn(){
var a = 10;
function foo(){
console.log(this.a);
}
foo();
}
fn(); // 20
var a = 20;
var obj = {
a: 10,
c: this.a + 20,
fn: function(){
return this.a;
}
}
console.log(obj.c); // 40
console.log(obj.fn()); // 10
如果你暫時沒想明白怎么回事,也不用著急,我們一點一點來分析。
分析之前,我們先直接了當拋出結論。
在一個函數上下文中,this由調用者提供,由調用函數的方式來決定。如果調用者函數,被某一個對象所擁有,那么該函數在調用時,內部的this指向該對象。如果函數獨立調用,那么該函數內部的this,則指向undefined。但是在非嚴格模式中,當this指向undefined時,它會被自動指向全局對象。
從結論中我們可以看出,想要準確確定this指向,找到函數的調用者以及區分他是否是獨立調用就變得十分關鍵。
// 為了能夠準確判斷,我們在函數內部使用嚴格模式,因為非嚴格模式會自動指向全局
function fn() {
'use strict';
console.log(this);
}
fn(); // fn是調用者,獨立調用
window.fn(); // fn是調用者,被window所擁有
在上面的簡單例子中,fn()
作為獨立調用者,按照定義的理解,它內部的this指向就為undefined。而window.fn()
則因為fn被window所擁有,內部的this就指向了window對象。
但是我們需要特別注意的是demo03。在demo03中,對象obj中的c屬性使用this.a + 20
來計算。這里我們需要明確的一點是,單獨的{}
是不會形成新的作用域的,因此這里的this.a
,由于并沒有作用域的限制,所以它仍然處于全局作用域之中。所以這里的this其實是指向的window對象。
再來看一些容易理解錯誤的例子,加深一下對調用者與是否獨立運行的理解。
var a = 20;
var foo = {
a: 10,
getA: function(){
return this.a
}
}
console.log(foo.getA()); // 10
var test = foo.getA();
console.log(test()); // 20
foo.getA()
中,getA是調用者,他不是獨立調用,被對象foo所擁有,因此它的this指向了foo。而test()
作為調用者,盡管他與foo.getA的引用相同,但是它是獨立調用的,因此this指向undefined,在非嚴格模式,自動轉向全局window。
稍微修改一下代碼,大家自行理解。
var a = 20;
function getA() {
return this.a;
}
var foo = {
a: 10,
getA: getA
}
console.log(foo.getA()); // 10
function foo(){
console.log(this.a);
}
function active(fn){
fn(); //真實調用者
}
var a = 20;
var obj = {
a: 10,
getA: foo
}
active(obj.getA); // 20
7.3 模仿塊級作用域
javascript沒有塊級作用域的概念。
function outputNumber(count){
for(var i=0; i<count; i++){
alert(i)
}
alert(i) // 5
}
outputNumber(5);
可以使用閉包來實現臨時變量
function outputNumber(count){
(function(){
for(var i=0; i<count; i++){
alert(i)
}
})()
alert(i) // 報錯
}
小結:
在JavaScript編程中,函數表達式是一種非常有用的技術。實現函數表達式可以無需對函數命名,從而實現動態編程。匿名函數,也成為拉姆達函數,是一種使用Javascript函數強大的方式。以下總結了函數表達式的特點:
- 函數表達式不同于函數聲明。函數聲明要求有名字,但函數表達式不需要。沒有名字的函數表達式叫做匿名函數;
- 在無法確定如何引用函數的情況下,遞歸函數就會變得很復雜;
- 遞歸函數應該始終使用arguments.callee來遞歸調用自身,不要使用函數名--函數名可能會發生變化;
當在函數內部定義其他函數,其他函數又使用了父函數的變量時,就創建了閉包。閉包有權訪問函數內部的所有變量。原理如下:
- 在后臺的執行環境中,閉包的作用連會包含它自己的變量對象,函數的變量對象,和全局變量對象;
- 通常,函數的作用域以及其所有的變量會在函數執行后被銷毀;
- 但是,當函數返回一個閉包的時候,這個函數的變量對象將會一直在內存直到閉包消失為止;
使用閉包可以在Javascript模仿塊級作用域(javascript只有全局作用域的概念),要點如下:
- 創建并立即調用一個函數,這樣既可以執行其中的代碼,又不會再內存中留下該函數的引用;
- 結果就是函數內部的所有變量都會被立即銷毀--除非將默寫變量賦值給了包含作用域(外部作用域)中的變量;
閉包還可以在對象中創建私有變量,相關概念如下:
- 即使Javascript中沒有正式的私有對象屬性的概念,但可以用閉包來實現公有方法,而使用公有方法可以訪問在包含作用域中的定義變量;
- 有權訪問私有變量的公有方法叫做特權方法;
- 可以使用構造函數模式、原型模式來實現自定義類型的特權方法,也可以使用模塊模式、增強的模塊模式來實現單例的特權方法;