javascript高級程序設(shè)計(第7章) -- 函數(shù)表達式

第七章:函數(shù)表達式

本章內(nèi)容:

  • 函數(shù)表達式的特征
  • 使用函數(shù)實現(xiàn)遞歸
  • 使用閉包定義私有變量

定義函數(shù)的方式有兩種,一種是函數(shù)聲明,另一種是函數(shù)表達式

// 函數(shù)聲明
function functionName(arg0){
    // 函數(shù)體
};
    
// 函數(shù)表達式
var functionName = function(arg0){
    // 函數(shù)體
}

關(guān)于函數(shù)聲明,它有個特征是函數(shù)聲明提升。這個在第五章節(jié)有講過。意思在執(zhí)行代碼之前會先讀取聲明函數(shù)。

關(guān)于函數(shù)表達式,就是創(chuàng)建了一個匿名函數(shù)(anonymous function)再賦值給一個變量。

7.2 閉包

閉包是值有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。創(chuàng)建閉包的常見方式,就是在函數(shù)內(nèi)部創(chuàng)建另一個函數(shù)。來看例子:

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兩行能夠訪問外部函數(shù)變量propertyName。即使這個函數(shù)被返回了,而且是在其他地方被調(diào)用。之所以能夠訪問變量,是因為內(nèi)部函數(shù)的作用域鏈中包含了createComparisionFunction的變量對象。要理解細節(jié),則從函數(shù)被調(diào)用時,發(fā)生什么開始入手。

當某個函數(shù)被調(diào)用,會創(chuàng)建一個執(zhí)行環(huán)境(execution context)以及創(chuàng)建相應的作用域鏈,然后,使用arguments和其他命名參數(shù)來初始化函數(shù)的變量對象。但在作用域鏈中個,外部的變量對象處于第二位,外部函數(shù)的外部函數(shù)的變量對象處于第三位...直到作為作用域鏈終點的全局變量對象。

function compare(value1, value2){
    if(value1 < value2){
        return -1;
    } else if(value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}

var result = compare(5, 10);

在上面代碼中,在全局作用域調(diào)用compare()函數(shù)時,會創(chuàng)建一個包含arguments、value1、value2的活動對象。全局執(zhí)行環(huán)境的變量對象(包含result和compare)在compare()執(zhí)行環(huán)境的作用域鏈中出于第二位。關(guān)系如下圖:

compare函數(shù)運行圖

后臺的每個執(zhí)行環(huán)境都有一個表示變量的對象-變量對象。全局環(huán)境中的變量對象始終存在,而像compare()函數(shù)這樣的局部環(huán)境的變量對象,則只有在函數(shù)執(zhí)行的過程中存在。在創(chuàng)建compare()函數(shù)時,就創(chuàng)建一個預先包含全局變量對象的作用域鏈,這個作用域鏈會保存在內(nèi)部[[Scope]]中。在調(diào)用compare()函數(shù)的時候,就會為函數(shù)創(chuàng)建一個執(zhí)行環(huán)境,然后復制[[Scope]]屬性中的對象構(gòu)建起執(zhí)行環(huán)境中的作用域鏈。之后,又有一個活動對象(當前的變量對象即為活動對象)被創(chuàng)建并推入執(zhí)行環(huán)境作用域鏈的前端。

創(chuàng)建compare函數(shù)的時候的scopes屬性

作用域鏈的本質(zhì)是一個指向變量對象的指針列表

無論在什么時候在函數(shù)中訪問一個變量時,都會從作用域鏈中搜索具有相應名字的變量。一般來講,當函數(shù)執(zhí)行完畢后,局部的活動對象就會銷毀,內(nèi)存僅保存全局的變量對象。 但是閉包的情況又有所不同:

在另一個函數(shù)內(nèi)部定義的函數(shù)會將包含函數(shù)(即外部函數(shù))的變量對象添加到它的作用域鏈中。因此,在createComparisionFunction內(nèi)部定義的匿名函數(shù)的作用域鏈中,實際上包含外部函數(shù)的createComparisionFunction的變量對象。

var compare = createComparisionFunction("name");
var result = compare({name:'Nicholas'},{name:'Greg'});

匿名函數(shù)從createComparisionFunction返回后,它的作用域鏈被初始化包含createComparisionFunction()函數(shù)的活動對象與全局的變量對象。

mark

這樣匿名函數(shù)就可以訪問createComparisionFunction()中定義的變量。更為重要的是,createComparisionFunction函數(shù)執(zhí)行完畢后,其活動對象也不會銷毀,因為匿名函數(shù)的作用域鏈仍然引用這個活動對象,但它的活動對象一直保存在內(nèi)存中;知道匿名函數(shù)被銷毀后(compare = null),createComparisionFunction的活動對象才會被銷毀。

mark

7.2.1 閉包與變量

作用域鏈的這種配置機制引出了一個值得注意的副作用,即閉包只能讀取包含函數(shù)中任何變量的最后一個值(可能中間變量值發(fā)生多次變換)。別忘了閉包保存的事整個變量對象,而不是某個特殊的變量值。

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
mark

這個函數(shù)會返回一個函數(shù)數(shù)組。表面上看,似乎每一個數(shù)組都應該返回自己的索引值,即位置0的函數(shù)返回0,位置1的函數(shù)返回1,以此類推。但事實上,每個函數(shù)都返回10。因為每個函數(shù)的作用域鏈中都包含了createFunction的變量對象,所以他們都指向了同一個變量i。當createFunction返回之后變量i的值就變成了10。所以每個函數(shù)查找的i都是10。 但我們可以創(chuàng)建另外一個匿名函數(shù)強制生成一個閉包。

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

當調(diào)用匿名函數(shù)時,我們傳遞了變量i.由于變量是按值傳遞的,所以這回將變量i的當前值賦值給參數(shù)num。而在這個匿名函數(shù)的內(nèi)部,又創(chuàng)建了一個返回num的閉包。這樣result每個函數(shù)都有自己的一份num變量副本。

延伸閱讀1: 詳細圖解作用域鏈與閉包

閉包是一種特殊的對象。

它由兩部分組成。執(zhí)行上下文(代號A),以及在該執(zhí)行上下文中創(chuàng)建的函數(shù)(代號B)。

當B執(zhí)行時,如果訪問了A中變量對象中的值,那么閉包就會產(chǎn)生。

在大多數(shù)理解中,包括許多著名的書籍,文章里都以函數(shù)B的名字代指這里生成的閉包。而在chrome中,則以執(zhí)行上下文A的函數(shù)名代指閉包。

因此我們只需要知道,一個閉包對象,由A、B共同組成,在以后的篇幅中,我將以chrome的標準來稱呼。

// demo01
function foo() {
    var a = 20;
    var b = 30;

    function bar() {
        return a + b;
    }

    return bar;
}

var bar = foo();
bar();

上面的例子,首先有執(zhí)行上下文foo,在foo中定義了函數(shù)bar,而通過對外返回bar的方式讓bar得以執(zhí)行。當bar執(zhí)行時,訪問了foo內(nèi)部的變量a,b。因此這個時候閉包產(chǎn)生。

JavaScript擁有自動的垃圾回收機制,關(guān)于垃圾回收機制,有一個重要的行為,那就是,當一個值,在內(nèi)存中失去引用時,垃圾回收機制會根據(jù)特殊的算法找到它,并將其回收,釋放內(nèi)存。

而我們知道,函數(shù)的執(zhí)行上下文,在執(zhí)行完畢之后,生命周期結(jié)束,那么該函數(shù)的執(zhí)行環(huán)境就會失去引用。其占用的內(nèi)存空間很快就會被垃圾回收器釋放??墒情]包的存在,會阻止這一過程。

var fn = null;
function foo(){
    var a = 2;
    function innerFoo(){
        console.log(a);
    }
    fn = innerFoo;
}

function bar(){
    fn(); //此處的保留的innerFoo的引用
}

foo();
bar(); //2

在上面的例子中,foo()執(zhí)行完畢之后,按照常理,其執(zhí)行環(huán)境生命周期會結(jié)束,所占內(nèi)存被垃圾收集器釋放。但是通過fn = innerFoo,函數(shù)innerFoo的引用被保留了下來,復制給了全局變量fn。這個行為,導致了foo的變量對象,也被保留了下來。于是,函數(shù)fn在函數(shù)bar內(nèi)部執(zhí)行時,依然可以訪問這個被保留下來的變量對象。所以此刻仍然能夠訪問到變量a的值。

這樣,我們就可以稱foo為閉包。

下圖展示了閉包foo的作用域鏈。

mark

我們可以在chrome瀏覽器的開發(fā)者工具中查看這段代碼運行時產(chǎn)生的函數(shù)調(diào)用棧與作用域鏈的生成情況。如下圖。

mark

7.2.2 關(guān)于this

作為函數(shù)的特殊對象,this對象是運行時基于函數(shù)的執(zhí)行環(huán)境板定的:

  • 在全局函數(shù)中,this等于window
  • 而當函數(shù)作為某個對象的方法調(diào)用時,this等于那個對象。

不過,匿名函數(shù)的執(zhí)行環(huán)境具有全局性,因此this對象通常指window。但有時候編寫方式的不同,這一點不那么明顯。

var name = 'window';
var object = {
    name: 'my object',
    getNameFun: function(){
        return function(){
            return this.name;
        }
    }
};
alert(object.getNameFun()()); //window  (在全局環(huán)境中執(zhí)行,this即為window)

每個函數(shù)在被調(diào)用時都會自動獲取兩個特殊變量:thisargument。內(nèi)部函數(shù)在搜索這兩個變量時,只會搜索其活動對象為止,因此永遠不可能直接訪問外部函數(shù)中的這兩個變量。不過,把外部作用域中的this對象保存在一個閉包能夠訪問的變量中,就可以讓閉包訪問該對象了。

var name = 'window';
var object = {
    name: 'my object',
    getNameFun: function(){
        var that = this;
        return function(){
            return that.name;
        }
    }
};
alert(object.getNameFun()());  // my object
mark

在定義匿名函數(shù)之前,我們把this對象賦值給了一個叫that的變量。而在定義閉包之后,閉包可以訪問這個變量,

this和arguments也存在同樣的問題,如果訪問作用域中的arguments對象,必須將對該對象的引用保存到另一個閉包能夠訪問的變量中。

延伸閱讀2: 全訪問解讀this

重新回顧一下執(zhí)行環(huán)境

mark

在執(zhí)行環(huán)境的創(chuàng)建階段,會分別生成變量對象,建立作用域鏈,確定this指向。其中變量對象與作用域鏈我們都已經(jīng)仔細總結(jié)過了,而這里的關(guān)鍵,就是確定this指向。

首先我們需要得出一個非常重要一定要牢記于心的結(jié)論,this的指向,是在函數(shù)被調(diào)用的時候確定的。也就是執(zhí)行環(huán)境被創(chuàng)建時確定的。因此,一個函數(shù)中的this指向,可以是非常靈活的。比如下面的例子中,同一個函數(shù)由于調(diào)用方式的不同,this指向了不一樣的對象。

var a = 10;
var obj = {
    a: 20
}

function fn () {
    console.log(this.a);
}

fn(); // 10
fn.call(obj); // 20

除此之外,在函數(shù)執(zhí)行過程中,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

關(guān) 于全局對象的this,我之前在總結(jié)變量對象的時候提到過,它是一個比較特殊的存在。全局環(huán)境中的this,指向它本身。因此,這也相對簡單,沒有那么多復雜的情況需要考慮。

// 通過this綁定到全局對象
this.a2 = 20;

// 通過聲明綁定到變量對象,但在全局環(huán)境中,變量對象就是它自身
var a1 = 10;

// 僅僅只有賦值操作,標識符會隱式綁定到全局對象
a3 = 30;

// 輸出結(jié)果會全部符合預期
console.log(a1); // 10
console.log(a2); // 20
console.log(a3); // 30
2. 函數(shù)中的this

在總結(jié)函數(shù)中this指向之前,我想我們有必要通過一些奇怪的例子,來感受一下函數(shù)中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

如果你暫時沒想明白怎么回事,也不用著急,我們一點一點來分析。

分析之前,我們先直接了當拋出結(jié)論。

在一個函數(shù)上下文中,this由調(diào)用者提供,由調(diào)用函數(shù)的方式來決定。如果調(diào)用者函數(shù),被某一個對象所擁有,那么該函數(shù)在調(diào)用時,內(nèi)部的this指向該對象。如果函數(shù)獨立調(diào)用,那么該函數(shù)內(nèi)部的this,則指向undefined。但是在非嚴格模式中,當this指向undefined時,它會被自動指向全局對象。

從結(jié)論中我們可以看出,想要準確確定this指向,找到函數(shù)的調(diào)用者以及區(qū)分他是否是獨立調(diào)用就變得十分關(guān)鍵。

// 為了能夠準確判斷,我們在函數(shù)內(nèi)部使用嚴格模式,因為非嚴格模式會自動指向全局
function fn() {
    'use strict';
    console.log(this);
}

fn();  // fn是調(diào)用者,獨立調(diào)用
window.fn();  // fn是調(diào)用者,被window所擁有

在上面的簡單例子中,fn()作為獨立調(diào)用者,按照定義的理解,它內(nèi)部的this指向就為undefined。而window.fn()則因為fn被window所擁有,內(nèi)部的this就指向了window對象。

但是我們需要特別注意的是demo03。在demo03中,對象obj中的c屬性使用this.a + 20來計算。這里我們需要明確的一點是,單獨的{}是不會形成新的作用域的,因此這里的this.a,由于并沒有作用域的限制,所以它仍然處于全局作用域之中。所以這里的this其實是指向的window對象。

再來看一些容易理解錯誤的例子,加深一下對調(diào)用者與是否獨立運行的理解。

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是調(diào)用者,他不是獨立調(diào)用,被對象foo所擁有,因此它的this指向了foo。而test()作為調(diào)用者,盡管他與foo.getA的引用相同,但是它是獨立調(diào)用的,因此this指向undefined,在非嚴格模式,自動轉(zhuǎn)向全局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(); //真實調(diào)用者
}
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);  

可以使用閉包來實現(xiàn)臨時變量

function outputNumber(count){
    (function(){
        for(var i=0; i<count; i++){
        alert(i)
    }   
    })()
 
    alert(i)   // 報錯
}

小結(jié):

在JavaScript編程中,函數(shù)表達式是一種非常有用的技術(shù)。實現(xiàn)函數(shù)表達式可以無需對函數(shù)命名,從而實現(xiàn)動態(tài)編程。匿名函數(shù),也成為拉姆達函數(shù),是一種使用Javascript函數(shù)強大的方式。以下總結(jié)了函數(shù)表達式的特點:

  • 函數(shù)表達式不同于函數(shù)聲明。函數(shù)聲明要求有名字,但函數(shù)表達式不需要。沒有名字的函數(shù)表達式叫做匿名函數(shù);
  • 在無法確定如何引用函數(shù)的情況下,遞歸函數(shù)就會變得很復雜;
  • 遞歸函數(shù)應該始終使用arguments.callee來遞歸調(diào)用自身,不要使用函數(shù)名--函數(shù)名可能會發(fā)生變化;

當在函數(shù)內(nèi)部定義其他函數(shù),其他函數(shù)又使用了父函數(shù)的變量時,就創(chuàng)建了閉包。閉包有權(quán)訪問函數(shù)內(nèi)部的所有變量。原理如下:

  • 在后臺的執(zhí)行環(huán)境中,閉包的作用連會包含它自己的變量對象,函數(shù)的變量對象,和全局變量對象;
  • 通常,函數(shù)的作用域以及其所有的變量會在函數(shù)執(zhí)行后被銷毀;
  • 但是,當函數(shù)返回一個閉包的時候,這個函數(shù)的變量對象將會一直在內(nèi)存直到閉包消失為止;

使用閉包可以在Javascript模仿塊級作用域(javascript只有全局作用域的概念),要點如下:

  • 創(chuàng)建并立即調(diào)用一個函數(shù),這樣既可以執(zhí)行其中的代碼,又不會再內(nèi)存中留下該函數(shù)的引用;
  • 結(jié)果就是函數(shù)內(nèi)部的所有變量都會被立即銷毀--除非將默寫變量賦值給了包含作用域(外部作用域)中的變量;

閉包還可以在對象中創(chuàng)建私有變量,相關(guān)概念如下:

  • 即使Javascript中沒有正式的私有對象屬性的概念,但可以用閉包來實現(xiàn)公有方法,而使用公有方法可以訪問在包含作用域中的定義變量;
  • 有權(quán)訪問私有變量的公有方法叫做特權(quán)方法;
  • 可以使用構(gòu)造函數(shù)模式、原型模式來實現(xiàn)自定義類型的特權(quán)方法,也可以使用模塊模式、增強的模塊模式來實現(xiàn)單例的特權(quán)方法;

參考:

前端基礎(chǔ)進階(四):詳細圖解作用域鏈與閉包

前端基礎(chǔ)進階(五):全方位解讀this

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 第3章 基本概念 3.1 語法 3.2 關(guān)鍵字和保留字 3.3 變量 3.4 數(shù)據(jù)類型 5種簡單數(shù)據(jù)類型:Unde...
    RickCole閱讀 5,489評論 0 21
  • 第2章 基本語法 2.1 概述 基本句法和變量 語句 JavaScript程序的執(zhí)行單位為行(line),也就是一...
    悟名先生閱讀 4,502評論 0 13
  • ??函數(shù)表達式是 JavaScript 中的一個既強大有容易令人困惑的特性。定義函數(shù)的的方式有兩種: 函數(shù)聲明; ...
    霜天曉閱讀 887評論 0 1
  • 在暑假的一個下午我和小伙伴們,約好了去德克士吃下午茶。 我和小伙伴說好了,“下午一點半”見面、等我到了地...
    賈博杰閱讀 304評論 0 0
  • 前緣——席慕蓉 人若真能轉(zhuǎn)世 世間若真有輪回 那么 我的愛 我們前世曾經(jīng)是什么 你 若曾是江南采蓮的女子 我 必是...
    海燈仁成閱讀 255評論 0 1

友情鏈接更多精彩內(nèi)容