《 JavaScript程序設計》—— 第五章 函數

5.1 黑盒

從概念上講,函數接受輸入,進行計算,而後產生輸出。下圖是一個函數黑盒示意圖,它計算一個帳戶在t年以後的餘額,其初始餘額爲p,年利率爲r,每一年取n次複利。
黑盒函數
要使用這個函數,只需向函數發送四個數值,並在其迴應信息中獲取計算所得的餘額。函數的用戶看不到其「內在工做」,因此咱們把函數想象成黑盒子。javascript

這裏書本給出一個關於什麼是抽象的說明:在平常生活中到處能夠看到相似的狀況。咱們開車,但並不瞭解內燃機或者氫燃料電池;咱們用微波爐加熱事食物,卻不明白深層的物理學知識;咱們發送即便消息、推文、打電話,卻對文字、聲音的編碼與傳輸方式一無所知。咱們把這種只看事物的主體部分而不關心細節的理念叫作抽象。函數則是對計算的抽象。html


5.2 定義和調用函數

在JavaScript中,函數類型值包含一個可執行的代碼塊,成爲函數體,以及零個或多個輸入,成爲形參(parameter)。下面這個函數只有一個參數,它會計算此參數的三次方。前端

function (x) {return x*x*x;}

函數也是值,和數字、真值、字符串、數組及普通對象同樣。所以,能夠把函數類型值賦給變量。java

var cube = function (x) {return x*x*x;}

要運行一個函數(或者說調用一個函數),能夠向它傳遞一個放在小括號中的列表,其中是零個或者多個實參(argument)。在被調用事,函數首先把每一個實參值賦給對應的形參,而後執行函數體。若是存在return語句,它會將計算結果傳回給調用者。

下面的腳本展現一個函數定義以及對它的三次調用。程序員

// 定義函數 —— 這時不會運行函數體
        var cube = function (x) {
            return x*x*x;
        };
        
        // 進三次調用,將函數體運行三次
        alert(cube(-2));
        alert(cube(10));
        alert(("在一個魔方中有" + (cube(3) - 1) + "個立方體 "))

在第一次調用中,咱們向函數cube傳遞了-2,cube會把-2賦值給x,而後計算-2-2-2,並把結果值(-8)返回給調用處。這個值隨後又被傳給alert函數的調用。
函數也能夠有名字。函數有了名字,在調用時,就不必定要將它賦值給變量了。web

function cube (x) {
            return x*x*x;
        };
        alert(cube(-2));    // -8

在JavaScript中,這種定義方式成爲函數聲明,相似於(但又不徹底等同於)把函數賦值給一個同名變量。儘管不少程序員喜歡函數聲明的方式,但咱們更喜歡使用變量聲明方式。咱們會在章尾討論。編程

var diceRoll = function () {
            return 1+Math.floor(6*Math.random());
        };

要運行這個函數,必須寫成diceRoll(),而不能寫成diceRoll。前一個表達式會調用函數,然後一個就是函數自身。

var diceRoll = function () {
            return 1+Math.floor(6*Math.random());
        };
        alert(
            diceRoll()
        );
        alert(
            diceRoll
        );

圖片描述

圖片描述

這裏都沒什麼問題,我想了想試了下以下函數:segmentfault

function test() {
            return "fn-test";
        };
        alert(test);

圖片描述

可見,對函數名進行調用,返回的是函數定義語句,而我在對函數聲明語句進行調用時與匿名函數賦值變量方式調用狀況相同,是否可證實函數聲明語句實際上隱式聲明瞭一個變量(變量名就是函數名),而且將函數引用賦值給函數名(變量名)

若是一個函數完成了某主體的執行,卻沒有執行任何return語句,它會返回undefined值。這個undefined值真的只是一個技術術語,由於在調用一個沒有return語句的函數時,主要是爲了它產生的效果,而不是爲了它產生的任何值。api

我對這就句話理解:若是函數定義時沒有要求返回最終值,則默認返回undefined。調用一個沒有返回值的函數後面還不是太理解...效果?何種效果?數組

var echo = function (message) {
            alert(message + ".");
            alert("I said: "+message+"!");
        };
        echo("Sanibonani");        // 調用這個函數最天然的方式
        var x =echo("Hello");      // 爲x賦值undefined,但在實際中不會發生
        console.log(x);            // undefined

若是沒有爲函數傳遞足夠了實參值,則額外的形參變量會被初始化爲undefined。

var show = function (x,y) {
            alert(x+" "+y);
        };
        show(1);        // "1 undefined"

5.3 示例

5.3.1 簡單的一行函數

// 返回半徑爲r的圓的面積
        var circleArea = function (r) {
            return Math.PI*r*r;
        };
        // 返回y可否被x整除
        var divides = function (x,y) {
            return y % x === 0;
        };

咱們能夠利用本身編寫的函數來構建其餘函數。
...都是基本的例子略過。

須要注意的是:函數語句內隱式類型轉換和優先級與結合性問題!


5.3.2 驗證明參

略.......


5.3.3 將對象引用做爲參數傳送

看一下向函數傳遞對象的狀況

// 返回一個數組中全部元素之和
        var sum = function (a) {
            var result = 0;
            for (var i=0;i<a.length;i+=1) {
                result += a[i];
            }
            return result;
        };
        alert(sum([]));
        alert(sum([10,-3,8]));

再看另外一個例子,它使用了一種徹底不一樣的風格。

// 把一個數組中全部字符串都轉換成大寫
        var uppercaseAll = function (a) {
            for (var i=0;i<a.length;i+=1) {
                a[i]=a[i].toUpperCase();    
            }
        };

區別在於,函數sum返回一個值,而uppercaseAll根本沒有包含return語句!相反,它修改了傳遞給它

// 把一個數組中全部字符串都轉換成大寫
        var uppercaseAll = function (a) {
            for (var i=0;i<a.length;i+=1) {
                a[i]=a[i].toUpperCase();    
            }
        };
        var result = uppercaseAll(["a","b","c"]);
        alert(result);                        // undefined
        alert(uppercaseAll(["a","b","c"]));   // undefined

自我理解:調用函數後,傳參,計算,由於沒有return返回值,因此只是計算而已,則uppercaseAll(["a","b","c"])就會像章開頭說的那樣,默認返回undefined,而後賦值給變量result。(最後alert調用函數證實這一點)。


var uppercaseAll = function (a) {
            for (var i=0;i<a.length;i++) {
                a[i]=a[i].toUpperCase();
            }
        };
        var dogs = ["spike","spot","rex"];
        alert(uppercaseAll(dogs))       // undefined,此值並不表明函數沒有執行,而是執行了未指定返回值,則返回默認值。
        alert(dogs);                    // ["SPIKE","SPOT","REX"],修改了傳入對象的屬性
  • 無返回值函數,按照文中描述,它修改的是傳遞給它的實參對象的屬性。就是修改了實參的屬性

  • 能夠看到,直接向函數傳參,而後被alert調用,結果是undefined由於調用函數並無返回結果,只會返回undefined雖然函數內部的確執行了大寫轉換操做,可是沒有返回值然並卵),因此最後顯示undefined

  • 可是dogs引用的數組對象已經被修改,即以前說的,它修改了傳遞給它的對象的屬性。由於沒有設置返回值,默認返回的undefined被上一個alert函數調用。dogs引用的數組被調用結束後因爲沒有返回值,避免了成爲新數組被返回出去。因此大寫字母保留下來。

另外一個:

var uppercaseAll = function (a) {
            var result = [];
            for (var i=0;i<a.length;i+=1) {
                result.push(a[i].toUpperCase())
            }
            return result;        // 返回的是一個新數組!
        };
        var dogs = ["spike","spot","rex"];
        alert(uppercaseAll(dogs));      // ["SPIKE","SPOT","REX"],這裏alert調用的對象,是函數返回的新數組!不是dogs
        alert(dogs);                    // ["spike","spot","rex"]
  • 有返回值函數,直接調用、傳參,由於有返回值,這個值被alert函數接收,顯示處理結果:["SPIKE","SPOT","REX"]

  • 爲何dogs引用的數組仍是小寫呢?由於調用函數返回的最終值沒有從新賦值給dogs。換句話說,alert函數調用的dogs數組,和uppercaseAll函數沒有關係。uppercaseAll執行結束已經返回了一個新數組

也就是說alert(uppercaseAll(dogs)); 這段語句的結果,是大寫字母仍是undefined,取決因而否對函數設置返回值!!!

確保你理解了最後這兩個函數的區別,第一個函數修改了其實參的屬性第二個函數沒有改動實參,而是返回一個新的數組


5.3.4 先決條件

// 返回數組中的最大元素
        var max = function (a) {
            var largest = a[0];
            for (var i=0;i<a.lengt;ai++) {
                if (a[i]>largest) {
                    largest = a[i];
                }    
            }
            return largest;
        };
        
        max([7,19,-22,0]);
        max(["dog","rat","cat"]);

它能正常工做嗎?
這個函數依靠>操做符一次比較數組中的連續值,跟蹤當前找到的最大值(從第一個元素a[0]開始)。如今>知道如何比較數字與數字、字符串與字符串,但奇怪的是,除非>兩邊的值都是字符串,不然JavaScript會把這兩個值都看做數字(隱式轉換),而後進行相應比較。有時,這種作法是沒問題的。

但若是有一個值被轉換成NaN,那麼狀況就不妙了。若是xyNaN,表達式x>y會得出false3>NaNfalseNaN>3也是false!這就表示:

alert(max([3,"dog"]));            // 3
        alert(max(["dog",3]));            // "dog"

3"dog"實際上是不可比較的,因此計算這種數組的最大值基本上沒有什麼意義。那在這種狀況下難道不該當拋出一個異常嗎?不少語言都會這麼作。其餘語言甚至會拒絕運行包含這種比較的程序!而後,JavaScript很愉快地運行了這種比較,而後給出了沒什麼意義的結果,若是願意的話,能夠嘗試在代碼裏探測這些問題。

/*返回數組中的最大元素。若是數組包含了不可比較的元素,函數會返回一個不肯定的任意值*/

函數在這個註釋中承諾:只要調用者僅傳遞有意義的參數,那它就返回最大值;不然契約失效。函數對實參提出了這些約數條件稱爲先決條件函數自身不會檢查先決條件,沒有知足先決條件只是會致使未指明的行爲。先決條件是編程圈子很是熟悉並且深入理解的一個術語,因此咱們將爲引入先決條件的註釋採用一種約定。

// 返回數組中的最大元素。先決條件:數組中的全部元素必須是能夠互相比較的

5.3.5 關注點分離

下面要舉的例子幾乎會出如今全部介紹編程的書中 ———— 一個質數判斷函數。該函數接受一個輸入值,而後返回它是否爲質數。4.5.1節給出了一份完整的質數腳本,可是如今要學習的內容要多得多了,因此下面將對該腳本進行重構重構重構就是對代碼作結構性的調整,讓其變得更好,通常(但不必定)是將大而混亂的代碼分解成較小的組成部分。在這個案例中咱們要將用戶交互與主要計算區分開來,將主要計算部分包裝成一個漂亮的函數。

//  返回n是否爲質數。先決條件:n是一個大於或等於2的整數,在JavaScript可表示的整數範圍以內。
        var isPrime = function (n) {
            for (var k=2,last=Math.sqrt(n);k<=last;k+=1) {
                if (n%k===0) {
                    return false;
                }
            }
            return true;
        };

請務必注意:這個函數只會返回它的實參是否是質數,並不會彈出一條說明判斷結果的消息!其他腳本負責提示輸入、檢查錯誤、報告結果。

var SMALLEST = 2,BIGGEST = 9E15;
        var n = prompt("輸入一個數組,我會檢查它是否是質數");
        if (isNaN(n)) {
            alert("這不是個數字");
        } else if (n<SMALLEST) {
            alert("我不能檢測這麼小的數字");
        } else if (n>BIGGEST) {
            alert("這個數字對我來講太大了,沒法檢測");
        } else if (n%1!==0) {
            alert("我只能測試整數");
        } else {
            alert(n+"是"+(isPrime(n)? "質數" : "合數"));
            // 注意這裏若是去掉三目運算符的括號,則會先計算字符串鏈接符,永遠彈出:"質數"
        }

重構後的代碼體現了關注點的分離,這是一種很優秀的編程作法,它主要有兩點好處。

  • 分離關注點可讓複雜系統變得容易理解。對於像航天飛機或金融服務系統這樣額大型系統,要理解或診斷其中的某個問題,必須可以肯定一些具備明確行爲的子系統。若是隻是把一個大型系統當作一系列語句的集合,那就永遠沒法真正理解它。

  • 將質數計算放到它本身的函數中,就能生成一段能夠重複使用的代碼,能夠將它放到咱們未來編寫的任意腳本中。咱們已經體驗過函數的複用性了:咱們已經調用過alertMath.sqrt,卻不須要本身去編寫其中的細節。

但咱們這個質數函數的複用性到底如何呢?調用這個函數的腳本作了不少錯誤檢查。若是真的但願這個函數只需編寫一次,卻能被數百個、數千個腳本調用,那期待這些「調用者」來作一樣的錯誤檢查是否公平呢?固然不公平了。咱們能夠在函數中檢查錯誤。

// 返回實參是否爲2到9e15之間的質數。
        // 若是實參不是整數或者超出2到9e15的範圍,則會拋出異常。
        var isPrime = function (n) {
            if (n%1!==0 || n<2 || n>9e15) {
                throw "這個數組不是整數或者超出範圍";
            };
            for (var k=2,last=Math.sqrt(n);k<last;k++) {
                if (n%k===0) {
                    return true;
                }
            }
            return false;
        };

注意,這個函數在遇到問題時會拋出異常,而不是彈出錯誤提示!這是很關鍵的。要使函數真正實現可複用,它永遠都不該接管用戶交流的責任。我理解爲錯誤不與交互模塊混用
函數的不一樣用戶對錯誤報告可能會有不一樣的要求。有些人會把錯誤寫到網頁的某個位置,有些人可能會把錯誤收集到一個數組中,有些人可能想用別的某種語言博報告錯誤,預測用戶可能使用的每種語言不是這個函數的任務。

當編寫爲調用者計算數值的函數時,應當經過拋出異常來指示錯誤。


5.3.6 斐波那契數列

本節最後一個例子是一個生成斐波那契數列的函數。斐波那契數列是一個很是值得注意的數列,在天然、音和金融市場中都會出現它的屬性。這個數列的開頭以下:

0,1,1,2,3,5,8,13,21,34,55,89,144,...

數列中每一個值(前兩個值除外)都是前兩個值之和。

f(n)=f(n-1)+f(n-2)

咱們的函數會構造一個數組f,從[0,1]開始,而後不停地把最後一個元素(f[f.length-1])和倒數第二個元素(f[f.length-2])相加。由於函數只能處理整數,因此咱們必須確保結果只不會超過JavaScript能夠連續表達的整數範圍,大約是9e15。就目前來講,咱們先作個弊,只生成其中的前75的數字,由於我知道這些數字是安全的。

// 返回一個數組,其中包含斐波那契數列的前75個數字。即f.length = 75
        
        var fibonacciSequence = function () {
            var f = [0,1];
            
            for (var i=0;i<=75;i++) {
                f.push(f[f.length-1]+f[f.length-2]);
            }
            alert(f);
        };
        fibonacciSequence();

練習

  • 改寫這個斐波那契數列,使其接受一個實參,代表要產生多少個斐波那契數。若是傳入的參數不是介於0到75之間(包含)的整數,則拋出一個異常。

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>     
    <body>
        <script type="text/javascript">
            /*計算*/
            function fbnqFn(n) {
                var fbnqArr = [0,1];
    
                for (var i=0;i<n;i++) {
                    fbnqArr.push(fbnqArr[fbnqArr.length-2]+fbnqArr[fbnqArr.length-1]);
                }
                
                fbnqArr.length = n;
                return fbnqArr;
            };
            /*交互*/
            function client() {
                var Max = prompt("須要生成多少個斐波那契數?(不能超過150)");
                if (isNaN(Max)===true || Max%1!==0 || Max<0 || Max>150) {
                    throw "輸入數字不能是非數字、整數、負數且不能超過150";
                }
                var test = fbnqFn(Max);
                console.log(test+" | "+test.length);    
            };
            // start
            client();
        </script>
    </body>
</html>

5.4 做用域

利用函數,能夠將其任意的計算進行打包,在調用者看來,就是一條單獨地簡單命令。請看如下計算階乘的函數:

// 返回n的階乘。先決條件:n是一個介於0到21之間的整數(包含0和21)。超過21,返回近似值
        var factorial = function (n) {
            var result = 1;
            for (var i=1;i<=n;i++) {
                result *= i;
            }
            return result;
        };

這個函數聲明瞭一個形參n,以及它本身的兩個變量:i和result。在函數內部聲明的變量成爲局部變量,和形參同樣,屬於函數本身,與腳本其餘位置的同名變量徹底無關。這點很是好,請看:

var result = 100;
        alert(factorial(5));          // 120
        alert(result);                // 100

咱們不會希全局變量result僅僅由於咱們計算了一次階乘就發生改變。腳本的不一樣部分每每是由不一樣人編寫的。編寫函數調用部分的做者徹底不知道在函數中會用到哪些變量。若是你調用了alert函數,而它改變了你的某些變量,你確定會不高興。
在JavaScript中,在函數內部聲明的變量以及函數的形參均擁有函數做用域,而在函數以外聲明的變量則擁有全局做用域,成爲全局變量擁有函數做用域的變量只在聲明它們的函數中可見,與外部世界隔離,就像咱們前面看到的那樣。下面這段很是簡短的腳本更清地代表了這一點。

var message = "冥王星只是一個矮行星";            
        var warn = function () {
            var message = "你立刻要看到一些爭議性的東西";
            alert(message);
        };
        warn();            // "你立刻要看到一些爭議性的東西"
        alert(message);    // "冥王星只是一個矮行星"

這裏有兩個恰巧同名的不一樣變量。全局變量的做用域開始於它的聲明位置,一直延伸到腳本結束,而局部變量的做用域則是聲明它的函數體內部。在這種狀況下,局部變量和全局變量的名字相同(message),其做用域重疊。在重疊區域中,最內層的聲明優先。

局部變量對外部是隱藏的,沒法從外部引用,而全局變量則能在函數中看到,除非你特意隱藏它們。

var warning = "不要雙擊提交按鈕"; 
        var warn = function () {
            alert(warning);        // 這裏能夠看到全局變量
        };    
        warn();                    // "不要雙擊提交按鈕"
        alert(warning);            // "不要雙擊提交按鈕"

能在函數訪問全局變量並無什麼使人驚訝的。實際上,咱們已經用過了不少全局變量:alertpromptisNaNMath等等。若是不容許在函數中用它們,要完成任何事情都會面臨巨大的阻礙。可是,這也意味着一個潛在的問題。

var message = "新遊戲的時間";
        var play = function () {
            message = "正在玩";        // 沒有聲明
            alert(message);
        };        
        alert(message);
        play();
        alert(message);
        play();

上面腳本定義了一個message變量,它的值由函數更新。在函數中修改全局變量幾乎總被認爲是很是差的編程實踐:腳本中的函數進行"相互交流"的正確作法是經過函數實參和返回值,而不是經過全局變量。程序應當儘可能少的使用全局變量:

儘可能減小全局變量的使用。具體來講,函數應該經過參數和返回值進行"交流",而不是經過更新全局變量。

JavaScript中局部變量的做用域包含了聲明它們的整個函數體,這一事實又會致使另外一種可能狀況:全局變量是在聲明以後纔會出現,而局部變量則是在其函數開始執行時就立刻存在的,即使變量是在函數體中間聲明的。考慮如下代碼:

var x = 1;
        // 在此處,全局變量x已經存在,而全局變量y則還沒有存在
        // 在此處使用y則會拋出一個ReferenceError引用錯誤
        var y = 2;
        // 此時全局變量y已經存在
        var f = function () {
            alert(z);            // 沒有錯誤,顯示undefined
            var z = 3;
            alert(y+3);            // 5
        };
        f();

其實上面例子有一個變量提高的問題,根據變量提高機制,var會提高到當前做用域的頂端,z的做用域是f所包含的區塊,因此你的代碼等價於

var x = 1;
        var y = 2;
        var f = function () {
            var z; 
        // 會把 var 聲明提高到最高的位置 這種特性叫作 變量提高  此時聲明瞭 z 可是爲定義值 因此z的值是 undefined     
            alert(z);            
            z = 3;
            alert(y+3);        
        };
        f();

當調用函數時,JavaScript引擎會在該處建立一個對象,用以保存函數的形參和局部變量。形參會被當即初始化,得到調用時所傳實參值的副本,全部局部變量會被馬上初始化爲undefined(這裏不是先初始化再賦值的?)。上面例子裏,在z聲明前就引用了它,但並無拋出ReferenceError,其緣由就在於此。

可是,儘管你知道局部變量在聲明以前便可調用,但這並不意味着就應該使用處於未定義狀態的局部變量。事實上,故意在定義變量以前就使用它們,幾乎可讓全部閱讀你代碼的人產生混淆,因此這被認爲是很是差的風格。不少JavaScript風格指南甚至直接認定這是一種錯誤;JSLint甚至包含了一項設置,專門用於檢查這一狀況。


練習(包含變量聲明提高和函數聲明提高問題)

  • 請定義術語做用域

    1. 函數內部定義的變量只對函數內部可見,對外部不可見

    2. 或,函數內部定義的變量、對象。使其能被外部發現,使用的範圍。


  • 下面的腳本中,彈出什麼提示內容?

var x = 1;
        var f = function (y) {
            alert(x+y);
        };
        f(2);        // 3

那麼按照變量提高,其實是:

var x = 1;
        var f = function (y) {
            var y;        // 聲明提高
            y = 2;        // 得到調用函數傳入的實參
            alert(x+y);   // 這裏的y引用的是全局變量y
        };
        f(2);

若是把形參y更名爲x,腳本會提示什麼?

var x = 1;
        var f = function (x) {
            alert(x+y);
        };
        f(2);

實際上會報錯,由於變量y沒有定義

var x = 1;
        var f = function (x) {
            var x;
            x = 2;
            alert(x+y);
        };

關於變量提高和塊級做用域問題能夠看幾位大神的文章:



5.5 做爲對象的函數

JavaScript是每個值,只要它不是undefined、null、布爾值、數字和字符串,那它就是一個對象。所以,函數值也是對象,並且跟全部對象同樣,也能夠有屬性。它們還能夠像其餘值同樣,其自己是其餘對象的屬性。

5.5.1 函數的屬性

知道函數是對象以後,天然會問,函數有那些屬性?
函數屬性的其餘用途包括:計算生成特定結果的次數、記住函數在給定實參下的返回值,以及定義與特定對象集合相關的數據。

當建立了函數對象以後,JavaScript會其初始化兩個屬性。第一個是length,初始值爲函數的形參個數

var average = function (x,y) {
            return (x+y)/2;
        };
        alert(average.length);    // 2,一個用於x,一個用於y

第二個預約義屬性是prototype,以後在討論


5.5.2 做爲屬性的函數

因爲函數也是值,因此能夠做爲對象的屬性。把函數放在對象內部有兩個主要理由,第一個理由是把許多相關函數放在一組。例如:

var geometry = {
            circleArea:function (radius) {
                return Math.PI*radius*radius;
            },
            circleCircumference:function (radius) {
                return 2*Math.PI*radius;
            },
            sphereSurfaceArea:function (radius) {
                return 4*Math.PI*radius*radius;
            },
            boxVolume:function (length,width,depth) {
                return length*width*depth;
            }
        };

把許多函數組合到單個對象中,有助於組織和理解大型程序。人類不但願去嘗試理解一個擁有數百個甚至數千個函數的系統,若是一個系統只有數十個軟件組成部分,那咱們理解起來會容易不少。例如,在一個遊戲程序中,咱們會很天然地爲玩家、地貌、物理屬性、消息傳遞、裝備、圖像等分別建立出子系統,每一個都是一個很大的對象。

將函數做爲屬性的第二個理由是讓程序從面向過程轉向面向對象。例如,咱們不必定要將函數看做對形狀執行操做,將函數存儲爲形狀的屬性。將函數放在對象的內部,可讓人們專一於這些函數,讓函數扮演對象行爲的角色。

var circle = {
            radius:5,
            area:function () {
                return Math.PI*this.radius*this.radius;
            },
            circumference:function () {
                return 2*Math.PI*this.radius;
            }
        };
        alert(circle.area());            // 78.53981633974483
        circle.radius = 1.5;
        alert(circle.circumference());    // 9.42477796076938

這個例子引入了JavaScript的this表達式,這是一個至關強大的表達式,能夠根據上下文表達出不一樣含義。當一個調用中引入了包含函數的對象時(就如上面的circle.area()),this指的就是這個包含函數的對象。

使用this表達式的函數屬性稱爲方法。所以,咱們說circle有一個area方法和一個circumference方法。

練習

  • 將函數值用做對象屬性的兩個主要理由是什麼?

    談談本身理解,有大神有別的建議歡迎評論

    1. 更好的運用面向對象編程思惟

    2. 對象在建立時自帶操做函數(屬性),存儲在對象內部,做爲對象的一部分存在。


  • 下面的腳本會提示什麼?爲何?

var x = 2;
        var p = {
            x:1,
            y:1,
            z:function () {
                return x + this.x;    // x引用的是全局變量,this.x指向的是p.x
            },
        };
        alert(p.z());    // 3

5.5.3 構造器

在上一節,咱們僅定義了一個circle圓對象。但若是須要不少個圓,怎麼辦?

// 錯誤的示範
        var Circle = function (r) {
            return {
                radius:r,
                area:function () {
                    return Math.PI*this.radius*this.radius;
                },
                circumference:function () {
                    return 2*Math.PI*this.radius;
                }
            };
        };
        var c1 = Circle(2);        // 建立一個半徑爲2的圓
        var c2 = Circle(10);    // 建立一個半徑爲10的圓
        alert(c1.area())        // "314.1592653589793"

這段代碼表面上看沒問題,但有一個缺陷。每次建立一個圓,也另行建立了額外的面積和周長方法。
圖片描述
在建立多個圓時,會浪費大量的內存來保存面積和周長函數的冗餘副本——這是很糟糕的事情,由於內存資源是有限的。當腳本耗盡內存就會崩潰。型號,JavaScript的原型prototype提供了一種解決方案。

// 一個圓的原型,其設計目的是做爲下面用Circle函數建立的全部圓的原型
        var protoCircle = {
            radius:1,
            area:function () {return Math.PI*this.radius*this.radius;},
            circumference:function () {return 2*Math.PI*this.radius;}
        };
        // 建立具備給定半徑的圓
        var Circle = function (r) {
            var c= Object.create(protoCircle);    // 將protoCircle原型建立到變量c中
            c.radius = r;                         // c的_proto_指向protoCircle對象
            return c;
        };

每一個經過調用Circle建立的圓都有本身的radius屬性和一個隱藏連接,指向一個惟一的共享原型,其中包含了areacircumference函數(分別只有一個)。這是極好的,不過還只是有小小缺陷。咱們使用了兩個全局變量CircleprotoCircle。若是隻有一個就更好了,這樣可讓咱們的原型圓做爲Circle函數的一個屬性。咱們如今就有了一模式,用於很方便的定義一系列同種"類型"的對象。

/* 一個圓數據類型。概要:
         * 
         * var c = Circle(5);
         * c.radius => 5
         * c.area() => 25pi
         * c.circumference() => 10pi
         */
        
        var Circle = function (r) {
            var circle = Object.create(Circle.prototype);
            circle.radius = r;
            return circle;
        };
        Circle.prototype = {
            area:function () {return Math.PI*this.radius*this.radius},
            circumference:function () {return 2*Math.PI*this.radius},
        };

咱們能夠應用這一模式,生成一個用於建立矩形的函數。

/* 矩形數據類型。概要:
         * 
         * var r = Rectangle(5,4);
         * r.width => 5
         * r.height => 4
         * r.area() => 20
         * r.perimeter() => 18
         */
        
        var Rectangle = function (w,h) {
            var rectangle = Object.create(Rectangle.prototype);
            rectangle.width = w;
            rectangle.height = h;
            return rectangle;
        };
        Rectangle.prototype = {
            area:function () {return this.width*this.height};
            perimeter:function () {return 2*(this.width+this.height)}
        };

全新方式:JavaScript中的每一個函數對象都自動包含一個prototype屬性,prototype是函數兩個預約義屬性中的第二個,第一個length。只要函數一經定義,它的prototype屬性就會被初始化爲一個全新對象。(這個全新對象有本身的一個屬性,叫作constructor)。
下圖展現了一個新鮮出爐的函數,用於算兩個值的平均值。
圖片描述

其次在使用函數建立對象時,只要是用來魔法操做符new,就無需明確鏈接原型,也無需返回新建立對象。當你在函數調用以前加上了new時,會發生三件事情。

  • JavaScript會建立一個全新的空對象,而後使用引用這個新對象的表達式this來調用此函數。

  • 該構造對象的原型被設定爲函數的prototype屬性。

  • 該函數會自動返回新的對象(除非你明確要求函數返回其餘東西)
    這些規則看上去很複雜,但看一個例子就清楚了。

產生一個圓的函數,如何使用new操做符來調用該函數,建立的圓的實例

/*一個圓數據類型。概要:
         * var c = new Circle(5);
         * c.radius => 5
         * c.area() => 25pi
         * c.circumference() => 10pi
         */
        var Circle = function (r) {
            this.radius = r;
        };
        Circle.prototype.area = function () {
            return Math.PI*this.radius*this.radius;
        };
        Circle.prototype.circumference = function () {
            return 2*Math.PI*this.radius;
        };
        var c1 = new Circle(2);        // 建立半徑爲2的圓
        var c2 = new Circle(10);    // 建立半徑爲10的圓
        alert(c2.area());            // "314.1592653589793"

此腳本先建立一個函數對象,咱們將用變量Circle引用它。和全部函數同樣,建立它時,擁有一個第二對象,這個對象被prototye屬性引用。隨後,咱們向這個原型對象添加areacircumference函數。接下來咱們調用new Circle建立一對圓對象。操做符new建立新的對象,這個對象其原型爲Circle.prototype
圖片描述

根據設計,諸如Circle這樣的函數就是要用new調用的,這種函數稱爲構造器。根據約定,咱們用大寫首字母命名,並省略return語句,優先使用JavaScript的自動功能返回新建立的對象。之因此要約定使用大寫首字母,緣由在下一節給出。

沒有return語句的構造器調用將返回對象,而不是返回一般的undefined,新建立對象的原型將被神奇地指定給一個歷來不會顯式建立的對象。

這種方法不夠直接,這多是JavaScript語言中要添加Object.create的緣由之一。一些JavaScript程序員建議對於新腳本僅使用Object.create,由於這樣可讓對象與其原型之間的連接更爲明確。明確的代碼更易讀易懂易於處理。堅持使用Object.create的另外一個緣由多是出於哲學考慮:咱們能夠直接用對象來考慮問題,而不用另行引用「類型」的概念。

可是,咱們不能放棄構造器和操做符new。JavaScript從一開始就在使用它們,數以千計的現有腳本中都使用了它們,JavaScript的許多內置對象都是經過這些方式構建的,因此咱們須要真正理解它們。經過一些練習能夠熟悉它們,對目前來講,請複習如下步驟。

使用操做符new建立和使用一種自定義數據類型,好比圓:

  • 編寫一個構造器函數,經過體會this.radius = r這樣的賦值語句,爲每一個圓初始化一個獨有的屬性;

  • 將全部圓共享的方法指定給Circle.prototype

  • 經過調用new Circle()來建立特定圓。對於如此建立的每一個圓,其原型將自動變爲Circle.prototype


5.6 上下文(applycall

前面的在JavaScript——this、全局變量和局部變量混談中已經給出前兩種規則(全局做用域和函數做用域下的this引用),接下來要說一個注意點。

規則3:當用一個以new操做符調用的函數中時,this引用指的是新建立的對象。

var Point = function (x,y ) {
            this.x = x;
            this.y = y;
        };
        var p = new Point(4,-5);        // 新的實例
        var q = Point(3,8);                // 這裏修改了全局變量x和y!

上面的最後一行代表,咱們必定要很是注意,老是以new來調用構造器,以避免修改了已有的全局變量,致使腳本運行失控。爲減小發生這種意外的可能性,JavaScript程序員用大寫字母書寫構造器的名字。可使用一些工具(JSLint)來掃描代碼,不要調用函數而不使用new前綴,很危險!


規則4:利用函數方法applycall,能夠專門定義一個但願用做this值的對象。

var f = function (a,b,c) {
            this.x += a+b+c;            
        };
        var a = {x:1,y:2};
        
        f.apply(a,[10,20,5]);            // 調用f(10,20,5),以"a"爲this            
        f.call(a,3,4,15);                // 調用f(3,4,15),以"a"爲this
        alert(a.x);                        // 58
        
        var Point = function (x,y) {
            this.x = x;
            this.y = y;
        };
        var p = {z:3};
        Point.apply(p,[2,9]);            // 如今p爲{x:2,y:9,z:3}
        Point.call(p,10,4);                // 如今p爲{x:10,y:4,z:3}

這些方法容許借用(或劫持)現有的方法和構造器,將它們用於一些原本沒打算爲其使用的對象。這些方法稍有不一樣:call會傳送其實參,而apply會將實參打包放在一個數組中。


練習:

  • this引用有哪四種應用?

分別對應不一樣的做用域和上下文中,全局做用域、被當作方法調用、new構造函數調用、applycall


  • 如下腳本會輸出什麼?

var p = {
            x:1,
            f:function (y) {
                this.x += y;
                return this.x;
            }
        };            
        var q = {x:5};
        
        alert(p.f(1));
        alert(p.f.call(q,3));

先分析第一個alert:既然是p.f,接收方對象是p,則this引用指向p。且本來的p對象中,局部變量x值爲1,執行函數傳參後,1 += 1;因此最後p.x的值爲2。

第二個alert一樣是p.f,只是此次用了call方法,這個方法能夠借用現有的方法和構造器,也就是說,p.f這個方法被借用了,給誰呢?對了括號內的q對象,並傳參3,此時this引用指向了q對象(注意當調用的一瞬間,this已經指向了q對象),且q對象已經有q.x=5,傳參相加,最終結果q.x值爲8。


5.7高階函數

考慮下面兩個函數:

var squareAll = function (a) {
            var result = [];
            for (var i=0;i<a.length;i+=1) {
                result[i]=a[i]*a[i];
            }
            return result;
        };
        
        var capitalizeAll = function (a) {
            var result = [];
            for (var i=0;i<a.length;i+=1) {
                result[i]=a[i].toUpperCase();
            }
            return result;
        };

這兩個函數只有很小的一點不一樣,他們都是向一個數組中的每一個元素應用一個函數,並收集結果;可是,第一個函數是計算這些元素的平方,而第二個函數則是將這些元素變爲大寫。咱們能不能僅爲共同結構編寫一次代碼,而後用參數來實現它們之間的小小區別?

var collect = function (a,f) {
            var result = [];
            for (var i=0;i<a.length;i+=1) {
                result[i]=f(a[i]);
            }
            return result;
        };

對每一個數組元素實際執行的函數(好比求平方或轉換爲大寫)如今做爲實參傳送。

var square = function (x) {return x*x};
        var capitalize = function (x) {return x.toUpperCase();};
        
        var squareAll = function (a) {return collect(a,square);};
        var capitalizeAll = function (a) {return collect(a,capitalize)};

對於這些小小的square和capitalize函數,咱們甚至能夠不爲其聲明變量。

var squareAll = function (a) {
            return collect(a,function (x) {return x*x};)
        };
        var capitalizeAll = function (a) {
            return collect(a,function (x) {return x.toUpperCase();});
        };

好,來看看它們如何工做的。

var arr1 = [-2,5,0];        
        var arr2 = ["hi","ho"];
        
        alert(squareAll(arr1));
        alert(capitalizeAll(arr2));

函數f接受一個另外一個函數g做爲其實參(並在本身體內調用g),這種函數f稱爲高階函數。函數collect稱爲高階函數,內置的sort也是如此。咱們能夠向sort傳送一個比較函數,使它採用不一樣的排序方式。比比較函數就是咱們本身編寫的一個兩實參函數,當第一個實參小於第二個時返回一個負值,當兩個實參相等時返回0,當第一個實參較大時則返回一個正值。

var a = [3,6,10,1,40,25,8,73];            
        alert(a.sort());                                // 按字母排序
        alert(a.sort(function (x,y) {return x-y;}));    // 按數值遞增排序
        alert(a.sort(function (x,y) {return y-x;}));    // 按數值遞減排序

由於咱們能夠告訴sort函數,按照咱們喜歡的任意方式來比較元素,因此能夠編寫一些代碼,用幾種不一樣方式對一組對象進行排序。


在web頁上設置定時器、與用戶操做進行交流時,常常會傳送函數。它也是人工智能編程中最爲重要的程序設計範例之一。且有助於構建很是大的分佈式應用程序。

高階函數一詞不只適用於以函數爲實參的函數,還適用於返回函數的函數。

var withParentheses = function (s) {return "("+s+")";};
        var withBrackets = function (s) {return "["+s+"]";};
        var withBraces = function (s) {return "{"+s+"}";};

這三個函數很是相似。能夠怎樣進行重構呢?這三個函數中的每個均可以由另外一函數構造而成,只需告訴構造者要使用那種分隔符便可。

var delimitWith = function (prefix,suffix) {
            return function (s) {return prefix+s+suffix;}
        };
        var withParentheses = delimitWith("("+s+")");
        var withBrackets = delimitWith("[","]");
        var withBraces = delimitWith("{","}");

withParentheses、withBrackets、withBraces這三個函數都成爲閉包。粗略的說,JavaScript閉包是一種函數,它的函數體使用了來自外圍(enclosing)函數的變量。閉包在一些很是高級複雜的java結構中扮演着不可或缺的角色。


5.8函數聲明與函數表達式

function circleArea(x) {
            return Math.PI*Math.pow(x,2)
        };

這種形式的函數官方名稱爲函數聲明,它的工做方式與前者很是類似,可是這兩種定義形式是不一樣的。

  • 函數聲明不能出如今代碼中的某些地方。

  • 經過函數聲明引入的變量遵循不一樣於普通變量的做用域規則。

具體來講,函數聲明只能出如今腳本中的全局位置,或者出如今一個函數體的"頂級",不容許只出如今語句內部。根據官方的EA規範,下面代碼出現移一處語法錯誤:

if (true) {
            function successor() {return x+1;}        // 不容許
        }

這裏的戒律是:即使瀏覽器容許,也絕對不要將函數聲明放在一條語句內。不管是否選擇使用函數聲明,它們的存在都會影響咱們編寫特定表達式的方式,由於函數聲明一以單詞function開頭,因此JavaScript的設計者決定任何語句都不能以這個單詞開頭,以避免讀者混淆。

相關文章
相關標籤/搜索