深刻理解javascript函數

函數是javascript中最重要的內容,也是其相對其餘語言來講在設計上比較有意思的地方。javascript許多高級特性也或多或少和函數相關。本文將以函數爲中心,對函數的各個關鍵知識點作簡要介紹。javascript

函數是對象

理解函數是對象,是準確理解函數的第一步。下面的代碼就建立了一個函數對象。java

var sum = new Function("num1", "num2", "return num1 + num2;");

每一個函數都是Function類型的實例。Function構造函數能夠接受多個參數,最後一個參數是函數體,其餘參數均爲函數的形參。因爲其書寫的不優雅和兩次解析致使的性能問題,這種方式不常常被採用,可是這種寫法對於理解函數就是對象是很是有幫助的。通常地,咱們都用字面的方式來建立函數。數組

var sum = function(num1, num2){
    return num1 + num2;
}
//或者
function sum(num1, num2){
    return num1 + num2;
}

以上兩種定義函數的方法分別叫作函數表達式和函數聲明,二者的效果是等價的,區別在於解析器向執行環境加載數據時對二者的處理不同。解析器會率先讀取函數聲明來建立函數對象,保證其在任何代碼執行以前可用;對於函數表達式,則必須等到解析器執行到對應的代碼行,函數對象才被建立。瀏覽器

在javascript中,函數對象和其餘對象同樣,均被視爲一等公民。因此函數能夠被引用、能夠做爲參數被傳遞或做爲返回值返回,這使得函數的使用很是的靈活。閉包

函數的執行

函數對象表明了一個過程,和大多數語言同樣經過函數調用表達式能夠調用這個過程。可是javascript的函數對象還提供了另外兩種調用方式,call和apply方法。call和apply方法的第一個參數用於指定執行環境中this的綁定,後面的參數用於指定函數的實際參數。call和apply的惟一區別是實參的形式不同,call是用逗號分割,apply則是以數組傳遞。例如:app

//函數調用表達式
sum(1, 2);
//call方法
sum.call(this, 1, 2);
//apply方法
sum.apply(this, [1, 2]);

無論用哪一種調用方式,最終都是經過函數對象的[[Call]]方法實際調用這個過程。[[Call]]方法是javascript引擎內部使用的一個方法,程序不能直接訪問它。[[Call]]方法接受兩個參數,第一個參數指定this的綁定值,第二個參數指定函數的參數列表。爲了表達方便,後面咱們將[[Call]]方法的第一個參數稱做thisArg。函數對象的call方法和apply方法能夠顯示指定thisArg,函數表達式則是隱式指定這個參數的。例如:函數

var foo = function(){
    console.log(this);
};
var obj = {name:'object'};
foo();
obj.foo = foo;
obj.foo();

代碼在瀏覽器的執行結果以下:性能

Window {top: Window, window: Window, location: Location...}
Object {name: "object", foo: function}

從執行結果能夠看出,obj.foo()這種調用方法,隱式將調用它的對象obj做爲了thisArg。可是爲何foo()這種調用方式this的綁定值是window這個全局對象?難道foo()這種調用方式將全局對象默認指定爲thisArg?其實不是這樣的。thisArg並非和this關鍵字的綁定一一對應的,其中有一個轉換過程。以下:
1.若是thisArg爲undefined或者null,則this的綁定爲全局對象。
2.若是thisArg不是Object類型,則將thisArg強制轉型爲Object類型並綁定到this。
3.不然this的綁定就爲thisArg。
其實foo()這種調用方式thisArg的值爲undefined,經過以上的轉換過程將this綁定爲全局對象。this

執行環境與閉包

前面提到過執行環境(Execution Context)這個概念,簡單來講執行環境就是函數在執行時所依賴的一個數據環境,它決定了函數的行爲。程序執行流每次進入函數代碼時都會建立一個新的執行環境。活動的執行環境在邏輯上造成了一個棧的結構。當函數執行完畢,其執行環境從棧中彈出並銷燬。prototype

每一個執行環境都包含一個重要的組件:詞法環境(Lexical Environment)。詞法環境定義了javascript程序標識符到變量或函數的關聯關係。詞法環境包含了環境記錄(Environment Record)和一個到外層詞法環境的引用(若是有的話,不然爲null)。環境記錄記錄了當前做用域下的變量或函數的綁定狀況。有兩種類型的環境記錄,聲明式環境記錄(Declarative Environment Records)和對象環境記錄(Object Environment Records)。聲明式環境記錄包含了當前做用域下標識符到變量聲明和函數聲明的綁定。對象環境記錄是一個和特定對象綁定的環境記錄,用於臨時改變標識符的解析狀況,好比在with子句中。

函數對象都有一個[[Scope]]屬性,函數對象在建立時會將當前執行環境的詞法環境的值賦予給[[Scope]]屬性。這個屬性是引擎的內部屬性,程序沒法訪問到它。當程序流進入到函數時,javascript引擎會建立新的執行環境,同時也建立對應的詞法環境。引擎會將當前做用域聲明的變量和函數綁定到詞法環境,同時將[[Scope]]屬性的引用也添加到詞法環境。程序在進行標識符解析的時候,會優先從當前的詞法環境中搜索,搜索失敗則向外層詞法環境搜索,若是到最外層的全局環境還沒搜索到則會拋出異常。

嵌套定義的函數會造成javascript中一個有趣的特性:閉包。閉包的造成是因爲內層函數引用了外層函數在建立它時的詞法環境。即便外層函數已經返回,執行環境已經銷燬,可是內層函數依然可以經過詞法環境的引用訪問外層函數中定義的變量或函數。

with和catch子句

with子句和catch子句都能臨時改變當前的詞法環境。他們的方式是有些區別的。先看with子句。

function foo(){
    var background = '#ccc';
    with(document){
        body.style.background = background;
    }
}

當執行流進入foo時,這時會建立一個聲明式詞法環境。執行流進入with子句的時候,引擎會建立一個對象環境記錄。此時with子句中的標識符解析都會先從document這個對象中查找。當with子句執行完以後,對象環境記錄銷燬。

try{
//do something
}catch(e){
//handel error
}

catch子句也能臨時改變當前的詞法環境。和with子句不同的是,它會建立一個聲明式詞法環境,將catch子句中的參數綁定到這個詞法環境。

構造器與原型繼承

函數對象還有個很是重要的內部方法[[Construct]],當咱們將new操做符應用到函數對象時就調用了[[Construct]]方法。此時的函數充當構造器的角色。下面的代碼就經過[[Construct]]建立了一個對象。

var Dog = function(){
}
var dog = new Dog();

[[Construct]]方法的執行過程以下。
1.建立一個空對象obj。
2.設置obj的內部屬性[[Class]]爲Object。
3.設置obj的內部屬性[[Extensible]]爲true。
4.設置obj的[[Prototype]]屬性:若是函數對象prototype的值爲對象則直接賦給obj,不然賦予Object的prototype值。
5.調用函數對象的[[Call]]方法並將結果賦給result。
6.若是result爲對象則返回result,不然返回obj。

每一個javascript對象都有一個[[Prototype]]的內部屬性,[[Prototype]]的值爲一個對象,叫作原型對象。當程序在訪問javascript對象的某個屬性時,首先會在當前對象中搜索,搜索失敗則到原型鏈中搜索,直到搜索到相應值,不然就爲undefined。javascript的這種特性叫作原型繼承。[[Construct]]方法的第四步是實現原型繼承的關鍵,它指定了javascript對象的[[Prototype]]屬性。

var Dog = function(){
}
var animal = {};
Dog.prototype = animal;
var dog = new Dog();

上面代碼建立出來的dog對象的原型就爲animal,它「繼承」了animal對象的屬性。原型繼承是另一種面向對象的模型,相對於「類」的繼承模型來講,原型繼承更加符合咱們的現實世界的模型。原型繼承在javascript也是有很是廣的用途。

結語

函數這條線將javascript許多核心內容串起來了,我的以爲這也是javascript最有意思的地方。本文主要是根據Ecma-262第五版規範中相關內容進行的總結和整理,因爲能力有限,若有理解上的錯誤,望批評指出。

相關文章
相關標籤/搜索