JAVASCRIPT FUNCTIONS

本文是@堂主 對《Pro JavaScript with Mootools》一書的第二章函數部分知識點講解的翻譯。該書的做者 Mark Joseph Obcena 是 Mootools 庫的做者和目前開發團隊的 Leader。雖然本篇文章實際譯於 2012 年初,但我的感受這部分對 Javascript 函數的基本知識、內部機制及 JavaScript 解析器的運行機制講的很是明白,脈絡也清楚,對初學者掌握 JavaScript 函數基礎知識頗有好處。尤爲可貴的是不一樣於其餘 JavaScript書籍講述的都是分散的知識點,這本書的知識講解是有清晰脈絡的,按部就班。換句話說,這本書中的 JavaScript 知識是串起來的。前端

雖然這本《Pro JavaScript with Mootools》國內並未正式引進,但我依然建議有需求的能夠從 Amazon 上自行買來看一下,或者網上搜一下 PDF 的版本(確實有 PDF 全版下載的)。我我的是當初花了近 300 大洋從 Amazon 上買了一本英文原版的,仍是更喜歡紙質版的閱讀體驗。這本書其實能夠理解爲 「基於 MooTools 實踐項目的 JavaScript 指南」,總的脈絡是 「JavaScript 基礎知識 - 高級技巧 - MooTools 對原生 JavaScript 的改進」,很是值得一讀。express

本篇譯文字數較多,近 4 萬字,我不知道能有幾位看官有耐心看完。若是真有,且發現@堂主 一些地方翻譯的不對或有優化建議,歡迎留言指教,共同成長。另外,非本土產技術類書籍,優先建議仍是直接讀英文原版。編程

下面是譯文正式內容:數組


JavaScript 最好的特點之一是其對函數的實現。不一樣於其餘編程語言爲不一樣情景提供不一樣的函數類型,JavaScript 只爲咱們提供了一種涵蓋全部情景(如內嵌函數、匿名函數或是對象方法)的函數類型。 請不要被一個從外表看似簡單的 JavaScript 函數迷惑——這個基本的函數聲明如同一座城堡,隱藏着很是複雜的內部操做。咱們整章都是圍繞着諸如函數結構、做用域、執行上下文以及函數執行等這些在實際操做中須要重點去考慮的問題來說述的。搞明白這些平時會被忽略的細節不但有助你更加了解這門語言,同時也會爲你在解決異常複雜的問題時提供巨大幫助。閉包

關於函數(The Function)

最開始,咱們須要統一一些基本術語。從如今開始,咱們將函數(functions)的概念定義爲「執行一個明確的動做並提供一個返回值的獨立代碼塊」。函數能夠接收做爲值傳遞給它的參數(arguments),函數能夠被用來提供返回值(return value),也能夠經過調用(invoking)被屢次執行。app

// 一個帶有2個參數的基本函數:
function add(one, two) {
    return one + two;
}

// 調用這個函數並給它2個參數:
var result = add(1, 42);
console.log(result); // 43

// 再次調用這個函數,給它另外2個參數
result = add(5, 20);
console.log(result); // 25

JavaScript 是一個將函數做爲一等對象(first-class functions)的語言。一個一等對象的函數意味着函數能夠儲存在變量中,能夠被做爲參數傳遞給其餘函數使用,也能夠做爲其餘函數的返回值。這麼作的合理性是由於在 JavaScript 中隨處可見的函數其實都是對象。這門語言還容許你建立新的函數並在運行時改變函數的定義。編程語言

一種函數,多種形式(One Function, Multiple Forms)

雖然在 JavaScript 中只存在一種函數類型,但卻存在多種函數形式,這意味着能夠經過不一樣的方式去建立一個函數。這些形式中最多見的是下面這種被稱爲函數字面量(function literal)的建立語法:ide

function Identifier(FormalParamters, ...) {
    FunctionBody
}

首先是一個 function 關鍵字後面跟着一個空格,以後是一個自選的標識符(identifier)用以說明你的函數;以後跟着的是以逗號分割的形參(formal parameters)列表,該形參列表處於一對圓括號中,這些形參會在函數內部轉變爲可用的局部變量;最後是一個自選的函數體(funciton body),在這裏面你能夠書寫聲明和表達式。請注意下面的說法是正確的:一個函數有多個可選部分。咱們如今還沒針對這個問題進行詳細的說明,由於對其的解答將貫穿本章。函數

注意:在本書的不少章節咱們都會看到 字面量(literal)這個術語。在JavaScript中,字面量是指在你代碼中明肯定義的值。「mark」、1 或者 true 是字符串、數字和布爾字面量的例子,而 function() 和 [1, 2] 則分別是函數和數組字面量的例子。

在標識符(或後面咱們會見到的針對這個對象自己)後面使用調用操做符(invocation operator) 「()」的被稱爲一個函數。同時調用操做符()也能夠爲函數傳遞實參(actual arguments)學習

注意:一個函數的形參是指在建立函數時圓括號中被聲明的有命名的變量,而實參則是指函數被調用時傳給它的值。

由於函數同時也是對象,因此它也具備方法和屬性。咱們將在本書第三章更多的討論函數的方法和屬性,這裏只須要先記住函數具備兩個基本的屬性:

  1. 名稱(name):保存着函數標識符這個字符串的值
  2. 長度(length):這是一個關於函數形參數量的整數(若是函數沒有形參,其 length 爲 0)
    函數聲明(Function Declaration)

採用基本語法,咱們建立第一種函數形式,稱之爲函數聲明(function declaration)。函數聲明是全部函數形式中最簡單的一種,且絕大部分的開發者都在他們的代碼中使用這種形式。下面的代碼定義了一個新的函數,它的名字是 「add」:

// 一個名爲「add」的函數
function add(a, b) {
    return a + b;
}

console.log(typeof add); // 'function'
console.log(add.name); // 'add'
console.log(add.length); // '2'
console.log(add(20, 5)); // '25'

在函數聲明中須要賦予被聲明的函數一個標識符,這個標識符將在當前做用域中建立一個值爲函數的變量。在咱們的例子中,咱們在全局做用域中建立了一個 add 的變量,這個變量的 name 屬性值爲 add,這等價於這個函數的標識符,且這個函數的 length 爲 2,由於咱們爲其設置了 2 個形參。 由於 JavaScript 是基於詞法做用域(lexically scoped)的,因此標識符被固定在它們被定義的做用域而不是語法上或是其被調用時的做用域。記住這一點很重要,由於 JavaScript 容許咱們在函數中定義函數,這種狀況下關於做用域的規則可能會變得不易理解。

// 外層函數,全局做用域
function outer() {

    // 內層函數,局部做用域
    function inner() {
        // ...
    }

}

// 檢測外層函數
console.log(typeof outer); // 'function'

// 運行外層函數來建立一個新的函數
outer();

// 檢測內層函數
console.log(typeof inner); // 'undefined'

在這個例子中,咱們在全局做用域中建立了一個 outer 變量併爲之賦值爲 outer 函數。當咱們調用它時,它建立了一個名爲 inner 的局部變量,這個局部變量被賦值爲 inner 函數,當咱們使用 typeof 操做符進行檢測的時候,在全局做用域中 outer 函數是能夠被有效訪問的,但 inner 函數卻只能在 outer 函數內部被訪問到 —— 這是由於 inner 函數只存在於一個局部做用域中。 由於函數聲明同時還建立了一個同名的變量做爲他的標識符,因此你必須肯定在當前做用域不存在其餘同名標識符的變量。不然,後面同名變量的值會覆蓋前面的:

// 當前做用域中的一個變量
var items = 1;

// 一個被聲明爲同名的函數
function items() {
    // ...
};

console.log(typeof items); // 'function' 而非 'number'

咱們過一會會討論更多關於函數做用域的細節,如今咱們看一下另一種形式的函數。

函數表達式(Function Expression)

下面要說的函數形式具有必定的優點,這個優點在於函數被儲存在一個變量中,這種形式的函數被稱爲函數表達式(funciton expression)。不一樣於明確的聲明一個函數,這時的函數以一個變量返回值的面貌出現。下面是一個和上面同樣的add函數,但此次咱們使用了函數表達式:

var add = function(a, b) {
    return a + b;
};

console.log(typeof add); // 'function'
console.log(add.name); // '' 或 'anonymous'
console.log(add.length); // '2'
console.log(add(20, 5)); // '25'

這裏咱們建立了一個函數字面量做爲 add 這個變量的值,下面咱們就可使用這個變量來調用這個函數,如最後的那個語句展現的咱們用它來求兩個數的和。你會注意到它的 length 屬性和對應的函數聲明的 length 屬性是同樣,可是 name 屬性卻不同。在一些 JavaScript 解析器中,這個值會是空字符串,而在另外一些中則會是 「anonymous」。發生這種狀況的緣由是咱們並未給一個函數字面量指定一個標識符。在 JavaSrcipt 中,一個未使用明確標識符的函數被稱爲一個匿名函數(anonymous)。 函數表達式的做用域規則不一樣於函數聲明的做用域規則,這是由於其取決於被賦值的那個變量的做用域。記住在 JavaScript 中,由關鍵字 var 聲明的變量是一個局部變量,而忽略了這個關鍵字則會建立一個全局變量。

// 外層函數,全局做用域
var outer = function() {

    // 內層函數,局部做用域
    var localInner = function() {
        // ...
    }; 

    // 內層函數,全局做用域
    globalInner = function() {
        // ...
    };

}

// 檢測外層函數
console.log(typeof outer); // 'function'

// 運行外層函數來建立一個新的函數
outer();

// 檢測新的函數
console.log(typeof localInner); // 'undefined'
console.log(typeof globalInner); // 'function'

outer 函數被定義在全局做用域中,這是由於雖然咱們使用了 var 關鍵字,但其在當前應用中處於最高層級。在這個函數內部有另外的2個函數:localInner 和 globalInner。localInner 函數被賦值給一個局部變量,在 outer 外部沒法訪問它。而 globalIner 則因在定義時缺失 var 關鍵字,其結果是這個變量及其引用的函數都處於全局做用域中。

命名的函數表達式(Named Function Expression)

雖然函數表達式常常被書寫爲採用匿名函數的形式,但你依然能夠爲這個匿名函數賦予一個明確的標識符。這個函數表達式的變種被稱爲一個命名的函數表達式(named function expression)

var add = function add(a, b) {
    return a + b;
};

console.log(typeof add); // 'function'
console.log(add.name); // 'add'
console.log(add.length); // '2'
console.log(add(20, 5)); //'25'

這個例子和採用匿名函數方式的函數表達式是同樣的,但咱們爲函數字面量賦予了一個明確的標識符。和前一個例子不一樣,這時的 name 屬性的值是 「add」,這個值同咱們爲其賦予的那個標識符是一致的。JavaScript 容許咱們爲匿名函數賦予一個明確的標識符,這樣就能夠在這個函數內部引用其自己。你可能會問爲何咱們須要這個特徵,下面讓咱們來看兩個例子:

var myFn = function() {
    // 引用這個函數
    console.log(typeof myFn);
};
myFn(); // 'function'

上面的這個例子,myFn 這個函數能夠輕鬆的經過它的變量名來引用,這是由於它的變量名在其做用域中是有效的。不過,看一下下面的這個例子:

// 全局做用域
var createFn = function() {

    // 返回函數
    return function() {
        console.log(typeof myFn);
    };

};

// 不一樣的做用域
(function() {

    // 將createFn的返回值賦予一個局部變量
    var myFn = createFn();

    // 檢測引用是否可行
    myFn(); // 'undefined'

})();

這個例子可能有點複雜,咱們稍後會討論它的細節。如今,咱們只關心函數自己。在全局做用域中,咱們建立了一個 createFn 函數,它返回一個和前面例子同樣的 log 函數。以後咱們建立了一個匿名的局部做用域,在其中定義了一個變量 myFn,並把 createFn 的返回值賦予這個變量。 這段代碼和前面那個看起來很像,但不一樣的是咱們沒使用一個被明確賦值爲函數字面量的變量,而是使用了一個由其餘函數產生的返回值。並且,變量 myFn 一個不一樣的局部做用域中,在這個做用域中訪問不到上面 createFn 函數做用域中的返回值。所以,在這個例子中,log 函數不會返回 「function」 而是會返回一個 「undefined」。 經過爲匿名函數設置一個明確的標識符,即便咱們經過持有它的變量訪問到它,也能夠去引用這個函數自身。

// 全局做用域
var createFn = function() {

    // 返回函數
    return function myFn() {
        console.log(typeof myFn);
    };

};

// 不一樣的做用域
(function() {

    // 將createFn的返回值賦予一個局部變量
    var myFn = createFn();

    // 檢測引用是否可行
    myFn(); // 'function'

})();

添加一個明確的標識符相似於建立一個新的可訪問該函數內部的變量,使用這個變量就能夠引用這個函數自身。這樣使得函數在其內部調用自身(用於遞歸操做)或在其自己上執行操做成爲可能。 一個命名了的函數聲明同一個採用匿名函數形式的函數聲明具備相同的做用域規則:引用它的變量做用域決定了這個函數是局部的或是全局的。

// 一個有着不一樣標識符的函數
var myFn = function fnID() {
    console.log(typeof fnID);
};

// 對於變量
console.log(typeof myFn); // 'function'

// 對於標識符
console.log(typeof fnID); // 'undefined'

myFn(); // 'function'

這個例子顯示了,經過變量 myFn 能夠成功的引用函數,但經過標識符 fnID 卻沒法從外部訪問到它。可是,經過標識符卻能夠在函數內部引用其自身。

自執行函數(Single-Execution Function)

咱們在前面介紹函數表達式時曾接觸過匿名函數,其還有着更普遍的用處。其中最重要的一項技術就是使用匿名函數建立一個當即執行的函數——且不須要事先把它們先存在變量裏。這種函數形式咱們稱之爲自執行函數(single-execution function)。

// 建立一個函數並當即調用其自身
(function() {

    var msg = 'Hello World';
    console.log(msg);

})();

這裏咱們建立了一個函數字面量並把它包裹在一對圓括號中。以後咱們使用函數調用操做符()來當即執行這個函數。這個函數並未儲存在一個變量裏,或是任何針對它而建立的引用。這是個「一次性運行」的函數:創造它,執行它,以後繼續其餘的操做。

要想理解自執行函數是如何工做的,你要記住函數都是對象,而對象都是值。由於在 JavaScript 中值能夠被當即使用而無需先被儲存在變量裏,因此你能夠在一對圓括號中插入一個匿名函數來當即運行它。

可是,若是咱們像下面這麼作:

// 這麼寫會被認爲是一個語法錯誤
function() {

    var msg = 'Hello World';
    console.log(msg);

}();

當 JavaScript 解析器遇到這行代碼會拋出一個語法錯誤,由於解析器會把這個函數當成一個函數聲明。這看起來是一個沒有標識符的函數聲明,而由於函數聲明的方式必需要在 function 關鍵字以後跟着一個標識符,因此解析器會拋出錯誤。

咱們把函數放在一對圓括號中來告訴解析器這不是一個函數聲明,更準確的說,咱們建立了一個函數並當即運行了它的值。由於咱們沒有一個可用於調用這個函數的標識符,因此咱們須要把函數放在一對圓括號中以即可以建立一個正確的方法來調用到這個函數。這種包圍在外層的圓括號應該出如今咱們沒有一個明確的方式來調用函數的時候,好比咱們如今說的這種自執行函數。

注意:執行操做符()能夠既能夠放在圓括號外面,也能夠放在圓括號裏面,如:(function() {…}())。但通常狀況下你們更習慣於把執行操做符放在外面。

自執行函數的用處不少,其中最重要的一點是爲變量和標識符創造一個受保護的局部做用域,看下面的例子:

// 頂層做用域
var a = 1;

// 一個由自執行函數建立的局部做用域
(function() {

    //局部做用域
    var a = 2;

})();

console.log(a); // 1

這裏,外面先在頂層做用域建立了一個值爲 1 的變量 a,以後建立一個自執行函數並在裏面再次聲明一個 a 變量並賦值爲 2。由於這是一個局部做用域,因此外面的頂層做用域中的變量 a 的值並不會被改變。

這項技術目前很流行,尤爲對於 JavaScript 庫(library)的開發者,由於局部變量進入一個不一樣做用域時須要避免標識符衝突。

另外一種自執行函數的用處是經過一次性的執行來爲你提供它的返回值:

// 把一個自執行函數的返回值保存在一個變量裏
var name = (function(name) {

    return ['Hello', name].join(' ');

})('Mark');

console.log(name); // 'Hello Mark'

別被這段代碼迷惑到:咱們這裏不是建立了一個函數表達式,而是建立了一個自執行函數並當即執行它,把它的返回值賦予變量 name。

自執行函數另外一個特點是能夠爲它配置標識符,相似一個函數聲明的作法:

(function myFn() {

    console.log(typeof myFn); // 'function'

})();

console.log(myFn); // 'undefined'

雖然這看起來像是一個函數聲明,但這倒是一個自執行函數。雖然咱們爲它設置了一個標識符,但它並不會像函數聲明那樣在當前做用域建立一個變量。這個標識符使得你能夠在函數內部引用其自身,而沒必要另外在當前做用域再新建一個變量。這對於避免覆蓋當前做用域中已存在的變量尤爲有好處。

同其餘的函數形式同樣,自執行函數也能夠經過執行操做符來傳遞參數。經過在函數內部把函數的標誌符做爲一個變量並把該函數的返回值儲存在該變量中,咱們能夠建立一個遞歸的函數。

var number = 12;

var numberFactorial = (function factorial(number) {
     return (number === 0) ? 1 : number * factorial(number - 1);
})(number);

console.log(numberFactorial); //479001600

函數對象(Function Object)

最後一種函數形式,就是函數對象(funciton object),它不一樣於上面幾種採用函數字面量的方式,這種函數形式的語法以下:

// 一個函數對象
new Function('FormalArgument1', 'FormalArgument2',..., 'FunctionBody');

這裏,咱們使用 Function 的構造函數建立了一個新的函數並把字符串做爲參數傳遞給它。前面的已經命名的參數爲新建函數對象的參數,最後一個參數爲這個函數的函數體。

注意:雖然這裏咱們把這種形式成爲函數對象,但請記住其實全部的函數都是對象。咱們在這裏採用這個術語的目的是爲了和函數字面量的方式進行區分。

下面咱們採用這種形式建立一個函數:

var add = new Function('a', 'b', 'return a + b;');

console.log(typeof add); // 'function'
console.log(add.name); // '' 或 'anonymous'
console.log(add.length); // '2'
console.log(add(20, 5)); // '25'

你可能會發現這種方式比採用函數字面量方式建立一個匿名函數要更簡單。和匿名函數同樣,對其檢測 name 屬性會獲得一個空的字符串或 anonymous。在第一行,咱們使用 Function 的構造函數建立了一個新的函數,並賦值給變量 add。這個函數接收 2 個參數 a 和 b,會在運行時將 a 和 b 相加並把相加結果作做爲函數返回值。

使用這種函數形式相似於使用 eval:最後的一個字符串參數會在函數運行時做爲函數體裏的代碼被執行。

注意:你不是必須將命名的參數做爲分開的字符串傳遞,Function 構造函數也容許一個字符串裏包含多個以逗號分隔的項這種傳參方式。好比:new Function(‘a, b’, ‘return a + b;’);

雖然這種函數形式有它的用處,但其相比函數字面量的方式存在一個顯著的劣勢,就是它是處在全局做用域中的:

// 全局變量
var x = 1;

// 局部做用域
(function() {

    // 局部變量
    var x = 5;
    var myFn = new Function('console.log(x)');
    myFn(); // 1, not 5

})();

雖然咱們在獨立的做用域中定義了一個局部變量,但輸出結果倒是 1 而非 5,這是由於 Function 構造函數是運行在全局做用域中。

參數(Arguments)

全部函數都能從內部訪問到它們的實參。這些實參會在函數內部變爲一個個局部變量,其值是函數在調用時傳進來的那個值。另外,若是函數在調用時實際使用的參數少於它在定義時肯定的形參,那麼那些多餘的未用到的參數的值就會是 undefined。

var myFn = function(frist, second) {
    console.log('frist : ' + frist);
    console.log('second : ' + second);
};

myFn(1, 2);
// first : 1
// second : 2

myFn('a', 'b', 'c');
// first : a
// second : b

myFn('test');
// first : test
// second : undefined

由於 JavaScript 容許向函數傳遞任意個數的參數,這也同時爲咱們提供了一個方式來判斷函數在調用時使用的實參和函數定義時的形參的數量是否相同。這個檢測的方式經過 arguments 這個對象來實現,這個對象相似於數組,儲存着該函數的實參:

var myFn = function(frist, second) {
    console.log('length : ' + arguments.length);
    console.log('frist : ' + arguments[0]);
};

myFn(1, 2);
// length : 2
// frist : 1

myFn('a', 'b', 'c');
// length : 3
// frist : a

myFn('test');
// length : 2
// frist : test

arguments 對象的 length 屬性能夠顯示咱們傳遞函數的實參個數。對實參的調用能夠對 arguments 對象使用相似數組的下標法:arguments[0] 表示傳遞的第一個實參,arguments[1] 表示第二個實參。

使用 arguments 對象取代有名字的參數,你能夠建立一個能夠對不一樣數量參數進行處理的函數。好比可使用這種技巧來幫助咱們改進前面的那個 add 函數,使得其能夠對任意數量的參數進行累加,最後返回累加的值:

var add = function(){
    var result = 0,
        len = arguments.length;

    while(len--) result += arguments[len];
    console.log(result);
}; 

add(15); // 15
add(31, 32, 92); // 135
add(19, 53, 27, 41, 101); // 241

arguments 對象有一個很大的問題須要引發你的注意:它是一個可變的對象,你能夠改變其內部的參數值甚至是把它整個變成另外一個對象:

var rewriteArgs = function() {
    arguments[0] = 'no';
    console.log(arguments[0]);
};

rewriteArgs('yes'); // 'no'

var replaceArgs = function() {
    arguments = null;
    console.log(arguments === null);
};

replaceArgs(); // 'true'

上面第一個函數向咱們展現了若是重置一個參數的值;後面的函數向咱們展現瞭如何總體更改一個 arguments 對象。對於 arguments 對象來講,惟一的固定屬性就是 length 了,即便你在函數內部動態的增長了 arguments 對象裏的參數,length 依然只顯示函數調用時賦予的實參的數量。

var appendArgs = function() {
    arguments[2] = 'three';
    console.log(arguments.length);
};

appendArgs('one', 'two'); // 2

當你寫代碼的時候,請確保沒有更改 arguments 內的參數值或覆蓋這個對象。

對於 arguments 對象還有另外一個屬性值:callee,這是一個針對該函數自身的引用。在前面的代碼中咱們使用函數的標識符來實如今函數內部引用其自身,如今咱們換一種方式,使用 arguments.callee:

var number = 12;

var numberFactorial = (function(number) {
    return (number === 0) ? 1 : number * arguments.callee(number - 1);
})(number);

console.log(numberFactorial); //479001600

注意這裏咱們建立的是一個匿名函數,雖然咱們沒有函數標識符,但依然能夠經過 arguments.callee 來準確的引用其自身。建立這個屬性的意圖就是爲了能在沒有標識符可供使用的時候(或者就算是有一個標識符時也可使用 callee)來提供一個有效方式在函數內部引用其自身。

雖然這是一個頗有用的屬性,但在新的 ECMAScript 5 的規範中,arguments.callee 屬性卻被廢棄了。若是使用 ES5 的嚴格模式,該屬性會引發一個報錯。因此,除非真的是有必要,不然輕易不要使用這個屬性,而是用咱們前面說過的方法使用標識符來達到一樣的目的。

雖然 JavaScript 容許給函數傳遞不少參數,但卻並未提供一個設置參數默認值的方法,不過咱們能夠經過判斷參數值是不是 undefined 來模擬配置默認值的操做:

var greet = function(name, greeting) {

    // 檢測參數是不是定義了的
    // 若是不是,就提供一個默認值
    name = name || 'Mark';
    greeting = greeting || 'Hello';

    console.log([greeting, name]).join(' ');

};

greet('Tim', 'Hi'); // 'Hi Tim'
greet('Tim');       // 'Hello Tim'
greet();            // 'Hello Mark'

由於未在函數調用時賦值的參數其值爲 undefined,而 undefined 在布爾判斷時返回的是 false,因此咱們可使用邏輯或運算符 || 來爲參數設置一個默認值。

另一點須要特別注意的是,原生類型的參數(如字符串和整數)是以值的方式來傳遞的,這意味着這些值的改變不會對外層做用域引發反射。不過,做爲參數使用的函數和對象,則是以他們的引用來傳遞,在函數做用域中的對參數的任何改動都會引發外層的反射:

var obj = {name : 'Mark'};

var changeNative = function(name) {
    name = 'Joseph';
    console.log(name);
};

changeNative(obj.name); // 'Joseph'

console.log(obj.name); // 'Mark'

var changeObj = function(obj) {
    obj.name = 'joseph';
    console.log(obj.name);
};

changeObj(obj); // 'Joseph'

console.log(obj.name); // 'Joseph'

第一步咱們將 obj.name 做爲參數傳給函數,由於其爲一個原生的字符串類型,其傳遞的是它值的拷貝(儲存在棧上),因此在函數內部對其進行改變不會對外層做用域中的 obj 產生影響。而接下來咱們把 obj 對象自己做爲一個參數傳遞,由於函數和對象等在做爲參數進行傳遞時其傳遞的是對自身的引用(儲存在堆上),因此局部做用域中對其屬性值的任何更改都會當即反射到外層做用域中的 obj 對象。

最後,你可能會說以前我曾提到過 arguments 對象是類數組的。這意味着雖然 arguments 對象看起來像數組(能夠經過下標來用於),但它沒有數組的那些方法。若是你喜歡,你能夠用數組的 Array.prototype.slice 方法把 arguments 對象轉變爲一個真正的數組:

var argsToArray = function() {
    console.log(typeof arguments.callee); // 'function'
    var args = Array.prototype.slice.call(arguments);
    console.log(typeof arguments.callee); // 'undefined'
    console.log(typeof arguments.slice); // 'function'
};

argsToArray();

返回值(Return Values)

Return 關鍵字用來爲函數提供一個明確的返回值,JavaScript 容許在函數內部書寫多個 return 關鍵字,函數會再其中一個執行後當即退出。

var isOne = function(number) {
   if (number === 1) return true;

   console.log('Not one ..');
   return false;
};

var one = isOne(1);
console.log(one); // true

var two = isOne(2); // Not one ..
console.log(two); // false

在這個函數第一次被引用時,咱們傳進去一個參數 1,由於咱們在函數內部先作了一個條件判斷,當前傳入的參數1使得該條件判斷語句返回 true,因而 return true 代碼會被執行,函數同時當即中止。在第二次引用時咱們傳進去的參數 2 不符合前面的條件判斷語句要求,因而函數會一直執行到最後的 return false代碼。

在函數內部設置多個 return 語句對於函數分層執行是頗有好處的。這同時也被廣泛應用於在函數運行最開始對必須的變量進行檢測,若有不符合的狀況則當即退出函數執行,這既能節省時間又能爲咱們提供一個錯誤提示。下面的這個例子就是一段從 DOM 元素中獲取其自定義屬性值的代碼片斷:

var getData = function(id) {
    if (!id) return null;
    var element = $(id);
    if (!element) return null;
    return element.get('data-name');
};

console.log(getData()); // null
console.log(getData('non existent id')); // null
console.log(getData('main')); // 'Tim'

組後關於函數返回值要提醒各位的一點是:不論你但願與否,函數老是會提供一個返回值。若是未顯示地設置 return 關鍵字或設置的 return 未有機會執行,則函數會返回一個 undefined。

函數內部(Function Internals)

咱們前面討論過了函數形式、參數以及函數的返回值等與函數有關的核心話題,下面咱們要討論一些代碼之下的東西。在下面的章節裏,咱們會討論一些函數內部的幕後事務,讓咱們一塊兒來偷窺下當 JavaScript 解析器進入一個函數時會作些什麼。咱們不會陷入針對細節的討論,而是關注那些有利於咱們更好的理解函數概念的那些重要的點。

有些人可能會以爲在最開始接觸 JavaScript 的時候,這門語言在某些時候會顯得不那麼嚴謹,並且它的規則也不那麼好理解。瞭解一些內部機制有助於咱們更好的理解那些看起來隨意的規則,同時在後面的章節裏會看到,瞭解 JavaScript 的內部工做機制會對你書寫出可靠的、健壯的代碼有着巨大的幫助。

注意:JavaScript 解析器在現實中的工做方式會因其製造廠商不一樣而不相一致,因此咱們下面要討論的一些解析器的細節可能不全是準確的。不過 ECMAScript 規範對解析器應該如何執行函數提供了基本的規則描述,因此對於函數內部發生的事,咱們是有着一套官方指南的。

可執行代碼和執行上下文(Executable Code and Execution Contexts)

JavaScript 區分三種可執行代碼:

  • 全局代碼(Global code)是指出如今應用代碼中頂層的代碼。
  • 函數代碼(Function code)是指在函數內部的代碼或是在函數體以前被調用的代碼。
  • Eval 代碼(Eval code)是指被傳進 eval 方法中並被其執行的代碼。

下面的例子展現了這三種不一樣的可執行代碼:

// 這是全局代碼
var name = 'John';
var age = 20;

function add(a, b) {
    // 這是函數代碼
    var result = a + b;
    return result;
}

(function() {
    // 這是函數代碼
    var day = 'Tuesday';
    var time = function() {
        // 這仍是函數代碼
        // 不過和上面的代碼在做用域上是分開的
        return day;
    };
})();

// 這是eval代碼
eval('alert("yay!");');

上面咱們建立的 name、age 以及大部分的函數都在頂層代碼中,這意味着它們是全局代碼。不過,處於函數中的代碼是函數代碼,它被視爲同全局代碼是相分隔的。函數中內嵌的函數,其內部代碼同外部的函數代碼也被視爲是相分隔的。

那麼爲何咱們須要對 JavaScript 中的代碼進行分類呢?這是爲了在解析器解析代碼時可以追蹤到其當前所處的位置,JavaScript 解析器採用了一個被稱爲執行上下文(execution context)的內部機制。在處理一段腳本的過程當中,JavaScript 會建立並進入不一樣的執行上下文,這個行爲自己不只保存着它運行到這個函數當前位置所通過的軌跡,同時還儲存着函數正常運行所須要的數據。

每一個 JavaScript 程序都至少有一個執行上下文,一般咱們稱之爲全局執行上下文(global execution context),當一個 JavaScript 解析器開始解析你的程序的時候,它首先「進入」全局執行上下文並在這個執行上下文環境中處理代碼。當它遇到一個函數,它會建立一個新的執行上下文並進入這個上下文利用這個環境來執行函數代碼。當函數執行完畢或者遇到一個 return 結束以後,解析器會退出當先的執行上下文並回到以前所處的那個執行上下文環境。

這個看起來不是很好理解,咱們下面用一個簡單的例子來把它理清:

var a = 1;

var add = function(a, b) {
    return a + b;
};

var callAdd = function(a, b) {
    return add(a, b);
};

add(a, 2);

call(1, 2);

這段簡單的代碼不單足夠幫助咱們來理解上面說的事情,同時仍是一個很好的例子來展現 JavaScript 是如何建立、進入並離開一個執行上下文的。讓咱們一步一步來分析:

當程序開始執行,Javascript 解析器首先進入全局執行上下文並在這裏解析代碼。它會先建立三個變量 a、add、callAdd,並分別爲它們賦值爲數字 一、一個函數和另外一個函數。

解析器遇到了一個針對 add 函數的調用。因而解析器建立了一個新的執行上下文,進入這個上下文,計算 a + b 表達式的值,以後返回這個表達式的值。當這個值被返回後,解析器離開了這個它新建立的執行上下文,把它銷燬掉,從新回到全局執行上下文。

接下來解析器遇到了另外一個函數調用,此次是對 callAdd 的調用。像第二步同樣,解析器會新建立一個執行上下文,並在它解析 callAdd 函數體中的代碼以前先進入這個執行上下文。當它對函數體內的代碼進行處理的時候,遇到了一個新的函數調用——此次是對 add 的調用,因而解析器會再新建一個執行上下文並進入這裏。此時,咱們已有了三個執行上下文:一個全局執行上下文、一個針對 callAdd 的執行上下文,一個針對 add 函數的執行上下文。最後一個是當前被激活的執行上下文。當 add 函數調用執行完畢後,當前的執行上下文會被銷燬並回到 callAdd 的執行上下文中,callAdd 的執行上下文中的運行結果也是返回一個值,這通知解析器退出並銷燬當前的執行上下文,從新回到全局執行上下文中。

執行上下文的概念對於一個在代碼中不會直接面對它的前端新人來講,多是會有一點複雜,這是能夠理解的。你此時可能會問,那既然咱們在編程中不會直接面對執行上下文,那咱們又爲何要討論它呢?

答案就在於執行上下文的其餘那些用途。我在前面提到過 JavaScript 解析器依靠執行上下文來保存它運行到當前位置所通過的軌跡,此外一些程序內部相互關聯的對象也要依靠執行上下文來正確處理你的程序。

變量和變量初始化(Variables and Variable Instantition)

這些內部的對象之一就是變量對象(variable object)。每個執行上下文都擁有它本身的變量對象用來記錄在當前上下文環境中定義的變量。

在 JavaScript 中建立變量的過程被稱爲變量初始化(variable instantition)。由於 JavaScript 是基於詞法做用域的,這意味着一個變量所處的做用域由其在代碼中被實例化的位置所決定。惟一的例外是不採用關鍵字 var 建立的變量是全局變量。

var fruit = 'banana';

var add = function(a, b) {
    var localResult = a + b;
    globalResult = localResult;
    return localResult;
};

add(1, 2);

在這個代碼片斷中,變量 fruit 和函數 add 處於全局做用域中,在整個腳本中都能被訪問到。而對於變量 localResult、a、b 則是局部變量,只能在函數內部被訪問到。而變量 globalResult 由於在聲明時缺乏關鍵字 var,因此它會成爲一個全局變量。

當 JavaScript 解析器進入一個執行上下文中,首先要作的就是變量初始化操做。解析器首先會在當前的執行上下文中建立一個 variable 對象,以後在當前上下文環境中搜索 var 聲明,建立這些變量並添加進以前建立的 variable 對象中,此時這些變量的值都被設置爲 undefined。讓咱們審視一下咱們的演示代碼,咱們能夠說變量 fruit 和 add 經過 variable 對象在當前執行上下文中被初始化,而變量 localResult、a、b 則經過 variable 對象在 add 函數的上下文空間中被初始化。而 globalResult 則是一個須要被特別注意的變量,這個咱們一會再來討論它。

關於變量初始化有很重要的一點須要咱們去記住,就是它同執行上下文是緊密結合的。回憶一下,前面咱們對 JavaScript 劃分了三種不一樣的執行代碼:全局代碼、函數代碼和 eval 代碼。同理,咱們也能夠說存在着三種不一樣的執行上下文:全局執行上下文、函數執行上下文、eval 執行上下文。由於變量初始化是經過處於執行上下文中的 variable 對象實現的,進而能夠說也存在着三種類型的變量:全局變量、處於函數做用域中的變量以及來自 eval 代碼中的變量。

這爲咱們引出了不少人對這門語言感受困惑的那些問題中一個:JavaScript 沒有塊級做用域。在其餘的類 C 語言中,一對花括號中的代碼被稱爲一個塊(block),塊有着本身獨立的做用域。由於變量初始化發生在執行上下文這一層級中,因此在當前執行上下文中任意位置被初始化的變量,在這整個上下文空間中(包括其內部的其餘子上下文空間)都是可見的:

var x = 1;

if (false) {
    var y =2;
}

console.log(x); // 1
console.log(y); // undefined

在擁有塊級做用域的語言中,console.log(y) 會拋出一個錯誤,由於條件判斷語句中的代碼是不會被執行的,那麼變量 y 天然也不會被初始化。但在 JavaScript 中這並不會拋出一個錯誤,而是告訴咱們 y 的值是 undefined,這個值是一個變量已經被初始化但還未被賦值時所具備的默認值。這個行爲看起來挺有意思,不是麼?

不過,若是咱們還記得變量初始化是發生在執行上下文這一層級中,咱們就會明白這種行爲其實正是咱們所指望的。當 JavaScript 開始解析上面的代碼塊的時候,它首先會進入全局執行上下文,以後在整個上下文環境中尋找變量聲明並初始化它們,以後把他們加入 variable 對象中去。因此咱們的代碼其實是像下面這樣被解析的:

var x;
var y;

x = 1;

if (false) {
    y = 2;
}

console.log(x); // 1
console.log(y); // undefined

一樣的在上下文環境中的初始化也適用於函數:

function test() {
    console.log(value); // undefined
    var value = 1;
    console.log(value); // 1
}

test();

雖然咱們對變量的賦值操做是在第一行 log 語句以後才進行的,但第一行的 log 仍是會給咱們返回一個 undefined 而非一個報錯。這是由於變量初始化是先於函數內其餘任何執行代碼以前進行的。咱們的變量會在第一時間被初始化並被暫時設置爲 undefined,其到了第二行代碼被執行時才被正式賦值爲 1。因此說將變量初始化的操做放在代碼或函數的最前面是一個好習慣,這樣能夠保證在當前做用域的任何位置,變量都是可用的。

就像你見到的,建立變量的過程(初始化)和給變量賦值的過程(聲明)是被 JavaScript 解析器分開執行的。咱們回到上一個例子:

var add = function(a, b) {
    var localResult = a + b;
    globalResult = localResult;
    return localResult;
};

add(1, 2);

在這個代碼片斷中,變量 localResult 是函數的一個局部變量,可是 globalResult 倒是一個全局變量。對於這個現象最多見的解釋是由於在建立變量時缺乏關鍵字 var 因而變量成了全局的,但這並非一個靠譜的解釋。如今咱們已經知道了變量的初始化和聲明是分開進行的,因此咱們能夠從一個解析器的視角把上面的代碼重寫:

var add = function(a, b) {
    var localResult;
    localResult = a + b;
    globalResult = localResult;
    return localResult;
};

add(1, 2);

變量 localResult 會被初始化並會在當前執行上下文的 variable 對象中建立一個針對它的引用。當解析器看到 「localResult = a + b;」 這一行時,它會在當前執行上下文環境的 variable 對象中檢查是否存在一個 localResult 對象,由於如今存在這麼一個變量,因而這個值(a + b)被賦給了它。然而,當解析器遇到 「globalResult = localResult;」 這一行代碼時,它不論在當前環境的 variable 對象中仍是在更上一級的執行上下文環境(對本例來講是全局執行上下文)的 variable 對象中都沒找到一個名爲 globalResult 的對象引用。由於解析器始終找不到這麼一個引用,因而它認爲這是一個新的變量,並會在它所尋找的最後一層執行上下文環境——總會是全局執行上下文——中建立這麼一個新的變量。因而, globalResult 最後成了一個全局變量。

做用域和做用域鏈(Scoping and Scope Chain)

在執行上下文的做用域中查找變量的過程被稱爲標識符解析(indentifier resolution),這個過程的實現依賴於函數內部另外一個同執行上下文相關聯的對象——做用域鏈(scope chain)。就像它的名字所蘊含的那樣,做用域鏈是一個有序鏈表,其包含着用以告訴 JavaScript 解析器一個標識符到底關聯着哪個變量的對象。

每個執行上下文都有其本身的做用域鏈,該做用域鏈在解析器進入該執行上下文以前就已經被建立好了。一個做用域鏈能夠包含數個對象,其中的一個即是當前執行上下文的 variable 對象。咱們看一下下面的簡單代碼:

var fruit = 'banana';
var animal = 'cat';

console.log(fruit); // 'banana'
console.log(animal); // 'cat'

這段代碼運行在全局執行上下文中,因此變量 fruit 和 animal 儲存在全局執行上下文的 variable 對象中。當解析器遇到 「console.log(fruit);」 這段代碼,它看到了標識符 fruit 並在當前的做用域鏈(目前只包含了一個對象,就是當前全局執行上下文的 variable 對象)中尋找這個標識符的值,因而接下來解析器發現這個變量有一個內容爲 「banana」 的值。下一行的 log 語句的執行過程同這個是同樣的。

同時,全局執行上下文中的 variable 對象還有另一個用途,就是被用作 global 對象。解析器對 global 對象有其自身的內部實現方式,但依然能夠經過 JavaScript 在當前窗口中自身的window對象或當前 JavaScript 解析器的 global 對象來訪問到。全部的全局對象實際上都是 global 對象中的成員:在上面的例子中,你能夠經過 window.fruit、global.fruit 或 window.animal、global.animal 來引用變量 fruit 和 animal。global 對象對全部的做用域鏈和執行上下文均可用。在咱們這個只是全局代碼的例子裏,global 對象是這個做用域鏈中僅有的一個對象。

好吧,這使得函數變得更加不易理解了。除了 global 對象以外,一個函數的做用域鏈還包含擁有其自身執行上下文環境的變量對象。

var fruit = 'banana';
var animal = 'cat';

function sayFruit() {
    var fruit = 'apple';

    console.log(fruit); // 'apple'
    console.log(animal); // 'cat'
}

console.log(fruit); // 'banana'
console.log(animal); // 'cat'

sayFruit();

對於全局執行上下文中的代碼,fruit 和 animal 標識符分別指向 「banana」 和 「cat」 值,由於它們的引用是被存儲在執行上下文的 variable 對象中(也就是 global 對象中)的。不過,在 sayFruit 函數裏標識符 fruit 對應的倒是另外一個值 —— 「apple」。由於在這個函數內部,聲明並初始化了另外一個變量 fruit。由於當前執行上下文中的 variable 對象在做用域鏈中處在更靠前的位置(相比全局執行上下文中的 variable 對象而言),因此 JavaScript 解析器會知道如今處理的應該是一個局部變量而非全局變量。

由於 JavaScript 是基於詞法做用域的,因此標識符解析還依賴於函數在代碼中的位置。一個嵌在函數中的函數,能夠訪問到其外層函數中的變量:

var fruit = 'banana';

function outer() {
    var fruit = 'orange';

    function inner() {
        console.log(fruit); // 'orange'
    }

    inner();
}

outer();

inner 函數中的變量 fruit 具備一個 「orange」 的值是由於這個函數的做用域鏈不僅僅包含了它本身的 variable 對象,同時還包含了它被聲明時所處的那個函數(這裏指 outer 函數)的 variable 對象。當解析器遇到 inner 函數中的標識符 fruit,它首先會在做用域鏈最前面的 inner 函數的 variable 對象中尋找與之同名的標識符,若是沒有,則去下一個 variable 對象(outer 函數的)中去找。當解析器找到了它須要的標識符,它就會停在那並把 fruit 的值設置爲 「orange」。

不過要注意的是,這種方式只適用於採用函數字面量建立的函數。而採用構造函數方式建立的函數則不會這樣:

var fruit = 'banana';

function outer() {
    var fruit = 'orange';

    var inner = new Function('console.log(fruit);');

    inner(); // 'banana'
}

outer();

在這個例子裏,咱們的 inner 函數不能訪問 outer 函數裏的局部變量 fruit,因此 log 語句的輸出結果是 「banana」 而非 「orange」。發生這種狀況的緣由是由於採用 new Function() 建立的函數其做用域鏈僅含有它本身的 variable 對象和 global 對象,而其外圍函數的 variable 對象都不會被加入到它的做用域鏈中。由於在這個採用構造函數方式新建的函數自身的 variable 對象中沒有找到標識符 fruit,因而解析器去後面一層的 global 對象中查找,在這裏面找到了一個 fruit 標識符,其值爲 「banana」,因而被 log 了出來。

做用域鏈的建立發生在解析器建立執行上下文以後、變量初始化以前。在全局代碼中,解析器首先會建立一個全局執行上下文,以後建立做用域鏈,以後繼續建立全局執行上下文的 variable 對象(這個對象同時也成爲 global 對象),再以後解析器會進行變量初始化,以後把儲存了這些初始化了的變量的 variable 對象加入到前面建立的做用域鏈中。在函數代碼中,發生的狀況也是同樣的,惟一不一樣的是 global 對象會首先被加入到函數的做用域鏈,以後把其外圍函數的的 variable 對象加入做用域鏈,最後加入做用域鏈的是該函數本身的 variable 對象。由於做用域鏈在技術角度來說屬於邏輯上的一個棧,因此解析器的查找操做所遵循的是從棧上第一個元素開始向下順序查找。這就是爲何咱們絕大部分的局部變量是最後才被加入到做用域鏈卻在解析時最早被找到的緣由。

閉包(Closures)

JavaScript 中函數是一等對象以及函數能夠引用到其外圍函數的變量使得 JavaScript 相比其餘語言具有了一個很是強大的功能:閉包(closures)。雖然增長這個概念會使對 JavaScript 這部分的學習和理解變得更加困難,但必須認可這個特點使函數的用途變得很是強大。在前面咱們已經討論過了 JavaScript 函數的內在工做機制,這正好能幫助咱們瞭解閉包是如何工做的,以及咱們應該如何在代碼中使用閉包。

通常狀況下,JavaScript 變量的生命週期被限定在聲明其的函數內。全局變量在整個程序未結束以前一直存在,局部變量則在函數未結束以前一直存在。當一個函數執行完畢,其內部的局部變量會被 JavaScript 解析器的垃圾回收機制銷燬從而再也不是一個變量。當一個內嵌函數保存了其外層函數一個變量的引用,即便外層函數執行完畢,這個引用也繼續被保存着。當這種狀況發生,咱們說建立了一個閉包。

很差理解?讓咱們看幾個例子:

var fruit = 'banana';

(function() {
    var fruit = 'apple';
    console.log(fruit); // 'apple'
})();

console.log(fruit); // 'banana'

這裏,咱們有一個建立了一個 fruit 變量的自執行函數。在這個函數內部,變量 fruit 的值是 apple。當這個函數執行完畢,值爲 apple 的變量 fruit 便被銷燬。因而只剩下了值爲 banana 的全局變量 fruit。此種狀況下咱們並未建立一個閉包。再看看另外一種狀況:

var fruit = 'banana';

(function() {
    var fruit = 'apple';

    function inner() {
        console.log(fruit); // 'apple'
    }

    inner();
})();

console.log(fruit); // 'banana'

這段代碼和上一個很相似,自執行函數建立了一個 fruit 變量和一個 inner 函數。當 inner 函數被調用時,它引用了外層函數中的變量 fruit,因而咱們的獲得了一個 apple 而不是 banana。不幸的是,對於自執行函數來講,這個 inner 函數是一個局部對象,因此在自執行函數結束後,inner 函數也會被銷燬掉。咱們仍是沒建立一個閉包,再來看一個例子:

var fruit = 'banana';
var inner;

(function() {
    var fruit = 'apple';

    inner = function() {
        console.log(fruit);
    }

})();

console.log(fruit); // 'banana'
inner(); // 'apple'

如今開始變得有趣了。在全局做用域中咱們聲明瞭一個名爲 inner 的變量,在自執行函數中咱們把一個 log 出 fruit 變量值的函數做爲值賦給全局變量 inner。正常狀況下,當自執行函數結束後,其內部的局部變量 fruit 應該被銷燬,就像咱們前面 2 個例子那樣。可是由於在 inner 函數中依然保持着對局部變量 fruit 的引用,因此最後咱們在調用 inner 時會 log 出 apple。這時能夠說咱們建立了一個閉包。

一個閉包會在這種狀況下被建立:一個內層函數嵌套在一個外層函數裏,這個內層函數被儲存在其外層函數做用域以外的做用域的 variable 對象中,同時還保存着對其外層函數局部變量的引用。雖然外層函數中的這個 inner 函數不會再被運行,但其對外層函數變量的引用卻依然保留着,這是由於在函數內部的做用域鏈中依然保存着該變量的引用,即便外層的函數此時已經不存在了。

要記住一個函數的做用域鏈同它的執行上下文是綁定的,同其餘那些與執行上下文關聯緊密的對象同樣,做用域鏈在函數執行上下文被建立以後建立,並隨着函數執行上下文的銷燬而銷燬。解析器只有在函數被調用時纔會建立該函數的執行上下文。在上面的例子中,inner 函數是在最後一行代碼被執行時調用的,而此時,原匿名函數的執行上下文(連同它的做用域鏈和 variable 對象)都已經被銷燬了。那麼 inner 函數是如何引用到已經被銷燬的保存在局部做用域中的局部變量的呢?

這個問題的答案引出了函數內部對象中一個被稱爲 scope 屬性(scope property)的對象。全部的 JavaScript 函數都有其自身的內在 scope 屬性,該對象中儲存着用來建立該函數做用域鏈的那些對象。當解析器要爲一個函數建立做用域鏈,它會去查看 scope 屬性看看哪些項是須要被加進做用域鏈中的。由於相比執行上下文,scope 屬性同函數自己的聯繫更爲緊密,因此在函數被完全銷燬以前,它都會一直存在——這樣苦於保證不了函數被調用多少次,它都是可用的。

一個在全局做用域中被建立的函數擁有一個包含了 global 對象的 scope 對象,因此它的做用域鏈僅包含了 global 對象和和它本身的 variable 對象。一個建立在其餘函數中的函數,它的 scope 對象包含了封裝它的那個函數的 scope 對象中的全部對象和它本身的 variable 對象。

function A() {
    function B() {
        function C() {
        }
    }
}

在這個代碼片斷中,函數 A 的 scope 屬性中僅保存了 global 對象。由於函數嵌套在函數 A 中,全部函數 B 的 scope 屬性會繼承函數 A 的 scope 屬性的內容並附加上函數 A 的 variable 對象。最後,函數 C 的 scope 屬性會繼承函數 B 的 scope 屬性中的全部內容。

另外,採用函數對象方式(使用 new Function() 方法)建立的函數,在它們的 scope 屬性中只有一個項,就是 global 對象。這意味着它們不能訪問其外圍函數(若是有的話)的局部變量,也就不能用來建立閉包。

This 關鍵字(The 「this」 Keyword)

上面咱們討論了一些函數的內部機制,最後咱們還有一個項目要討論:this 關鍵字。若是你對其餘的面向對象的編程語言有使用經驗,你應該會對一些關鍵字感到熟悉,好比 this 或者 self,用以指代當前的實例。不過在 JavaScript 中 this 關鍵字會便得有些複雜,由於它的值取決於執行上下文和函數的調用者。同時 this 仍是動態的,這意味着它的值能夠在程序運行時被更改。

this 的值老是一個對象,而且有些一系列規則來明確在當前代碼塊中哪個對象會成爲 this。其中最簡單的規則就是,在全局環境中,this 指向全局對象。

var fruit = 'banana';

console.log(fruit); // 'banana'
console.log(this.fruit); // 'banana'

回憶一下,全局上下文中聲明的變量都會成爲全局 global 對象的屬性。這裏咱們會看到 this.fruit 會正確的指向 fruit 變量,這向咱們展現在這段代碼中 this 關鍵字是指向 global 對象的。對於全局上下文中聲明的函數,在其函數體中 this 關鍵字也是指向 global 對象的。

var fruit = 'banana';

function sayFruit() {
    console.log(this.fruit);
}

sayFruit(); // 'banana'

(function() {
    console.log(this.fruit); // 'banana'
})();

var tellFruit = new Function('console.log(this.fruit);');

tellFruit(); // 'banana'

對於做爲一個對象的屬性(或方法)的函數,this 關鍵字指向的是這個對象自己而非 global 對象:

var fruit = {

    name : 'banana',

    say : function() {
        console.log(this.name);
    }

};

fruit.say(); // 'banana'

在第三章咱們會深刻討論關於對象的話題,可是如今,咱們要關注 this.name 屬性是如何指向 fruit 對象的 name 屬性的。在本質上,這和前面的例子是同樣的:由於上面例子中的函數是 global 對象的屬性,因此函數體內的 this 關鍵字會指向 global 對象。因此對於做爲某個對象屬性的函數而言,其函數體內的 this 關鍵字指向的就是這個對象。

對於嵌套的函數而言,遵循第一條規則:不論它們出如今哪裏,它們老是將 global 對象做爲其函數體中 this 關鍵字的默認值。

var fruit = 'banana';

(function() {
    (function() {
        console.log(this.fruit); // 'banana'
    })();
})();

var object = {

    fruit : 'orange',

    say : function() {
        var sayFruit =  function() {
            console.log(this.fruit); // 'banana'
        };
        sayFruit();
    }

};

object.say();

這裏,咱們看處處在兩層套嵌的子執行函數中的標識符 this.fruit 指向的是 global 對象中的 fruit 變量。在 say 函數中有一個內嵌函數的例子中,即便 say 函數自身的 this 指向的是 object 對象,但內嵌的 sayFruit 函數中的 this.fruit 指向的仍是 banana。這意味着外層函數並不會對內嵌函數代碼體中 this 關鍵字的值產生任何影響。

我在前面提到過 this 關鍵字的值是可變的,且在 JavaScript 中可以對 this 的值進行改變是頗有用的。有兩種方法能夠應用於更改函數 this 關鍵字的值:apply 方法和 call 方法。這兩種方法實際上都是應用於無需使用調用操做符 () 來調用函數,雖然沒有了調用操做符,但你仍是能夠經過 apply 和 call 方法給函數傳遞參數。

apply 方法接收 2 個參數:thisValue 被用於指明函數體中 this 關鍵字所指向的對象;另外一個參數是 params,它以數組的形式向函數傳遞參數。當使用一個無參數或第一個參數爲 null 的 apply 方法去調用一個函數的時候,那麼被調用的函數內部 this 指向的就會是 global 對象而且也意味着沒有參數傳遞給它:

var fruit = 'banana'

var object = {

    fruit : 'orange',

    say : function() {
        console.log(this.fruit);
    }

};

object.say(); // 'banana'
object.say.apply(); // 'banana'

若是要將一個函數內部的 this 關鍵字指向另外一個對象,簡單的作法就是使用 apply 方法並把那個對象的引用做爲參數傳進去:

function add() {
    console.log(this.a + this.b);
}

var a = 12;
var b = 13;

var values = {
    a : 50,
    b : 23
};

add.apply(values); // 73

apply 方法的第二個參數是以一個數組的形式向被調用的函數傳遞參數,數組中的項要和被調用函數的形參保持一致。

function add(a, b) {
    console.log(a); // 20
    console.log(b); // 50
    console.log(a + b); // 70
}

add.apply(null, [20, 50]);

上面說到的另外一個方法 call,和 apply 方法的工做機制是同樣的,所不一樣的是在 thisValue 參數以後跟着的是自選數量的參數,而不是一個數組:

function add(a, b) {
    console.log(a); // 20
    console.log(b); // 50
    console.log(a + b); // 70
}

add.call(null, 20, 50);

高級的函數技巧(Advanced Function Techniques)

前面的內容主要是關於咱們對函數的基礎知識的一些討論。不過,要想完整的展示出 JavaScript 函數的魅力,咱們還必須可以應用前面學到的這些分散的知識。

在下面的章節中,咱們會討論一些高級的函數技巧,並探索目前所掌握的技能其更普遍的應用範圍。我想說,本書不會是 JavaScript 學習的終點,咱們不可能把關於這門語言的全部信息都寫出來,而應該是開啓你探索之路的一個起點。

限制做用域(Limiting Scope)

如今,我在維護一個用戶的姓名和年齡這個事情上遇到了問題。

// user對象保存了一些信息
var user = {
    name : 'Mark',
    age : 23
};

function setName(name) {
    // 首先確保name是一個字符串
    if (typeof name === 'string') user.name = name;
}

function getName() {
   return user.name;
}

function setAge(age) {
    // 首先確保age是一個數字
    if (typeof age === 'number') user.age = age;
}

function getAge() {
    return user.age;
}

// 設置一個新的名字
setName('Joseph');
console.log(getName()); // 'Joseph'

// 設置一個新的年齡
setAge(22);
console.log(getAge()); // 22

目前爲止,一切都正常。setName 和 setAge 函數確保咱們要設置的值是正確的類型。但咱們要注意到,user 變量是出在全局做用域中的,能夠在該做用域內的任何地方被訪問到,這回致使你能夠不適應咱們的設置函數也可以設置 name 和 age 的值:

user.name = 22;
user.age = 'Joseph';

console.log(getName()); // 22
console.log(getAge()); // Joseph

很明顯這樣很差,由於咱們但願這些值可以保持其數據類型的正確性。

那麼咱們該怎麼作呢?如何你回憶一下,你會記起一個建立在函數內部的變量會成爲一個局部變量,在該函數外部是不能被訪問到的,另外閉包卻能夠爲一個函數可以保存其外層函數局部變量的引用提供途徑。結合這些知識點,咱們能夠把 user 變成一個受限制的局部變量,再利用閉包來使得獲取、設置等函數能夠對其進行操做。

// 建立一個自執行函數
// 包圍咱們的代碼使得user變成局部變量
(function() {

    // user對象保存了一些信息
    var user = {
        name : 'Mark',
        age : 23
    };

    setName = function(name) {
        // 首先確保name是一個字符串
        if (typeof name === 'string') user.name = name;
    };

    getName = function() {
        return user.name;
    };

    setAge = function(age) {
        // 首先確保age是一個數字
        if (typeof age === 'number') user.age = age;
    };

    getAge = function() {
        return user.age;
    }

})();

// 設置一個新的名字
setName('Joseph');
console.log(getName()); // 'Joseph'

// 設置一個新的年齡
setAge(22);
console.log(getAge()); // 22

如今,若是有什麼人想不經過咱們的 setName 和 setAge 方法來設置 user.name 和 user.age 的值,他就會獲得一個報錯。

柯里化(Currying)

函數做爲一等對象最大的好處就是能夠在程序運行時建立它們並將之儲存在變量裏。以下面的這段代碼:

function add(a, b) {
  return a + b;
}

add(5, 2);
add(5, 5);
add(5, 200);

這裏咱們每次都使用 add 函數將數字 5 和其餘三個數字進行相加,若是能把數字 5 內置在函數中而不用每次調用時都做爲參數傳進去是個不錯的主意。咱們能夠將 add 函數的內部實現機制變爲 5 + b 的方式,但這會致使咱們代碼中其餘已經使用了舊版 add 函數的部分發生錯誤。那有沒有什麼方法能夠實現不修改原有 add 函數的優化方式?

固然咱們能夠,這種技術被稱爲柯里化(partial application 或 currying),其實現涉及到一個可爲其提早「提供」一些參數的函數:

var add= function(a, b) {
    return a + b;
};

function add5() {
    return add(5, b);
}

add5(2);
add5(5);
add5(200);

如今,咱們建立了一個調用 add 函數並預置了一個參數值(這裏是5)的 add5 函數,add5 函數本質上來說其實就是預置了一個參數(柯里化)的 add 函數。不過,上面的例子並沒展現出這門技術動態的一面,若是咱們提供的默認值是另一個應該怎麼作?按照上面的例子,咱們必需要再次新建一個函數來提供一個新的預置參數。

函數做爲一等對象早晚都會派上用處,看,下面應用場景來了。不一樣於明確的建立一個新的 add5 函數,咱們能夠像下面這樣來作。

function add(a, b) {
    return a + b;
}

function curryAdd(a) {
    return function(b) {
        return add(a, b);
    }
}

var add5 = curryAdd(5);

add5(2);
add5(5);
add5(200);

如今來介紹一下這個新的函數 curryAdd,它接收一個參數,這個參數會做爲 add 函數的參數 a,同時返回一個新的匿名函數,這個匿名函數接收一個參數 b 來做爲 add 函數的另外一個參數。當咱們經過 curryAdd(5) 來調用這個函數時,它返回一個已經儲存了咱們一個明確參數值的函數,這個參數值此時被當作是這個匿名函數的一個局部變量。由於咱們建立了一個閉包,因此即便這個匿名函數已經執行完畢,但咱們仍是能夠經過它來最終求出咱們須要的 a + b 的值。

咱們這裏指介紹了一種柯里化函數一個極爲簡單常見的應用場景,但這能夠很好的說明柯里化函數是如何工做的。瞭解並掌握這種技巧會對你平常的編程工做帶來不少便利的。

裝飾(Decoration)

另外一項綜合使用函數動態賦值和閉包的技術被稱爲裝飾(decoration)。這裏的關鍵詞是「裝飾」(decorate),函數的裝飾是指可以動態的爲一個函數增長新的功能特性。

如今咱們有一個函數,它把一個對象做爲參數,它的工做是把這個參數對象內的名值對儲存到另外一個對象裏去:

(function() {

    var storage = {};

    store = function(obj) {
        for (var i in obj) storage[i] = obj[i];
    };

    retrieve = function(key) {
        return storage[key];
    };

})();

console.log(retrieve('name')); // undefined

store({
    name : 'Mark',
    age : '23'
});
console.log(retrieve('name')); // 'Mark'

看起來彷佛不錯,但若是咱們的需求變成不單能夠給 store 函數傳由名值對組成的對象作參數,還能夠直接傳名值對,就是相似 store(‘name’, ‘Mark’); 這種形式的,那咱們目前的函數就不能起做用了,咱們須要對函數進行改進。

咱們能夠經過爲 store 函數套上一層裝飾者函數來實現想要的改進:

var decoratePair = function(fn) {
    return function(key, value) {
        if (typeof key === 'string') {
            var _temp = {};
            _temp[key] = value;
            key = _temp;
        }
        return fn(key);
    }
};

(function() {

    var storage = {};

    store = decoratePair(function(obj) {
        for (var i in obj) storage[i] = obj[i];
    });

    retrieve = function(key) {
        return storage[key];
    };

})();

console.log(retrieve('name')); // undefined

store('name', 'Mark');
console.log(retrieve('name')); // 'Mark'

這應該是目前爲止咱們看過的比較複雜的例子了,讓咱們一步一步的來分析下這段代碼。首先,咱們聲明瞭一個名爲 decoratePair 的函數,這個函數只接收一個參數 fn,這個函數會被咱們進行裝飾。以後 decoratePair 會返回一個新的被裝飾過的函數,這個函數接收兩個參數,key 和 value。咱們原先的 store 函數只接收一個對象類型的參數,如今經過裝飾者函數能夠判斷第一個參數是對象仍是字符串。若是第一個參數不是字符串,則fn函數會當即被執行;若是第一個參數是字符串,則 decoratePair 的返回值函數會先把傳進去的參數 key 和 value 以名值對的方式存進一個私有變量 _temp 裏,以後把 _temp 賦值給一個變量 key,這時變量 key 引用的是一個符合 fn 函數參數要求的對象,以後再來調用 fn 函數。

咱們上面的裝飾者函數能夠確保在調用被包裝的 fn 函數時傳輸的是類型正確的參數,可是修飾着函數也能夠用在函數被調用後爲其增長特性。下面有一個簡單的裝飾者函數,它調用 add 函數的 2 個參數,並返回這 2 個參數的和與第二個參數的積。

var add = function(a, b) {
    return a + b;
};

var decorateMultiply = function(fn) {
    return function(a, b) {
        var result = fn(a, b);
        return result * b;
    }
};

var addThenMultiply = decorateMultiply(add);

console.log(add(2, 3)); // 5
console.log(addThenMultiply(2, 3)); // 15

裝飾者函數的用途很廣,它能夠幫助你在無需直接修改函數的狀況下爲其增長功能。它尤爲適用於那些你不能直接修改的內建函數和第三方代碼。

組合(Combination)

組合(combination)是一項和裝飾者函數類似的技術,它的用途是使用兩個(或數個)函數來創造一個新的函數。這和聲明一個新的函數不一樣,組合者函數只是將一個函數的返回值做爲參數傳給下一個函數。

**var add = function(a, b) {
    return a + b;
};

var square = function(a) {
    return a * a;
};

var result = square(add(3, 5));

console.log(result); // 64**

square(add(3, 5)) 這段代碼顯示了組合者函數是如何工做的,但這還不能算一個正確的組合者函數。這裏,add(3, 5) 的返回值 8,做爲參數傳給了 square 函數,以後 square 函數返回了 64。要把它變成一個組合者函數,咱們要將加工過程自動化,省得每次都要去敲 square(add(3, 5))。

var add = function(a, b) {
    return a + b;
};

var square = function(a) {
    return a * a;
};

var combine = function(fnA, fnB) {
    return function() {
        var args = Array.prototype.slice.call(arguments);
        var result = fnA.apply(null, args);
        return fnB.call(null, result);
    }
};

var addThenSquare = combine(add, square);

var result = addThenSquare(3, 5);
   
console.log(result); // 64

在這個代碼片斷中咱們先建立了兩個具有單一功能的函數 add 和 square。以後建立了一個組合者函數 combine,combine 函數接收 add 和 square 爲參數,在返回的匿名函數裏,先將傳給匿名調用函數的參數 a 和 b 轉爲一個數組 args,以後用 apply 方法調用 add 函數,將 a 與 b 的和賦值給變量 result,最後用 call 方法調用 square 方法,計算出最終的結果。

注意在使用組合者函數時,函數的順序和參數的數量是須要被重點注意的。在咱們的例子中,由於 square 函數只須要一個參數,而 add 函數須要的則是兩個,因此咱們不能獲得一個 squareTheAdd(先乘後加,先傳一個參數後傳 2 個參數)函數。由於 JavaScript 只容許函數返回一個值,因此組合者函數的使用場景每每是被限制在那些只採用單個參數的函數中。

招賢納士(Recruitment)

招人,前端,隸屬政採雲前端大團隊(ZooTeam),50 餘個小夥伴正等你加入一塊兒浪~ 若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5年工做時間3年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手參與一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章
相關標籤/搜索