js之函數表達式


函數表達式的特征
函數表達式是js中一種既強大又容易令人困惑的特性。定義函數的方式有兩種:一種是函數聲明,另一種是函數表達式.
            function functionName(arg0,arg1,arg2){
                // 函數體
            }
            console.log(functionName.name); // functionName
            // Firefox,Safari,Chrome和Opera都給函數定義了一個非標准的name屬性,通過這個屬性可以訪問到給函數指定的名字。
            // 這個屬性的值永遠都等於跟在function關鍵詞后面的標識符
    關於函數聲明,它的一個重要特征就是 函數聲明提升 ,意思是在執行代碼之前會讀取函數聲明,這就意味着可以把函數聲明放到調用它的語句后面:

            sayHi();  // Hi
            sayYes(); // 錯誤:函數還不存在
            function sayHi(){
                console.log('Hi');
            }
            var sayYes = function () {
                console.log('Yes');
            }
            // 這種形式看起來好像是常規的變量賦值語句,即創建一個函數並將它賦值給變量sayYes。
            // 這種情況下創建的函數叫做匿名函數。因為function 后面沒有標識符,匿名函數的name屬性是空字符串
    理解函數聲明與函數表達式之間的區別:
            if(true){
                function sayHi(){
                    console.log("Hi");
                }
            }else{
                function sayHi(){
                    console.log("Yo!");
                }
            }
            // 上述代碼在ECMAScript中屬於無效語法,js引擎會嘗試修復錯誤,將其轉換為合理的狀態,但問題是瀏覽器嘗試修正錯誤的做法並不一致。
                // 但是如果使用函數表達式,那就沒什么問題了

            var sayHi;
            if(true){
                sayHi = function (){
                    console.log("Hi");
                }
            }else{
                sayHi = function(){
                    console.log("Yo!");
                }
            }
            sayHi(); // Hi

 


遞歸
遞歸函數是在一個函數通過名字調用自身的情況下構成的。
        // 一個經典的遞歸階乘函數
            function factorial(num){
                if(num <= 1){
                    return 1;
                }else{
                    return num * factorial(num - 1);
                }
            }
            console.log(factorial(10));

            // 看起來沒什么問題,但是下面的代碼卻導致它出錯:
            var antherFactorial = factorial;
            factorial = null;
            console.log(antherFactorial(4)) // 出錯!
            // 在這種情況下,使用arguments.callee可以解決這個問題。我們知道,arguments.callee是一個指向正在執行的函數的指針,因此可以用它來實現對函數的遞歸調用


            -------------隔斷------------------
            function factorial(num){
                if(num <= 1){
                    return 1;
                }else{
                    return num * arguments.callee(num - 1);
                }
            }
            var antherFactorial = factorial;            factorial = null;
            console.log(antherFactorial(4)) // 24
                // 因此,在編寫遞歸函數時,通過使用arguments.callee代替函數名,可以確保無論怎樣調用函數都不會出現問題
                // 但是在嚴格模式下,不能通過腳本訪問arguments.callee,訪問這個屬性會導致出錯。不過,可以通過命名函數表達式來達成相同的效果:


                var ff = (function f(num){
                    if(num <= 1){
                        return 1;
                    }else{
                        return num * f(num - 1);
                    }
                })
            console.log(ff(100));  //9.33262154439441e+157
                // 以上代碼創建了一個名為f() 的命名函數表達式,然后將它賦值給變量ff。即便把函數賦值給另一個變量,函數的名字f依然有效,所以遞歸調用照樣
                //  能正確完成。這種方式在嚴格模式和非嚴格模式下都行得通

 


閉包
有不少開發人員總是搞不清匿名函數和閉包這兩個概念,因此經常混用。閉包是指有權訪問另一個函數作用域中的變量的函數。創建閉包的常見方式
就是在一個函數內部創建另一個函數:
            function createCF(propertyName){
                return function(obj1,obj2){
                    var val1 = obj1[propertyName];
                    var val2 = obj2[propertyName];
                    // 上面兩行代碼是內部函數(一個匿名函數)中的代碼,這兩行代碼訪問了外部函數中的變量propertyName。即使這個內部函數被返回了,而且是在其他
                    // 地方被調用了,但它依然可訪問變量propertyName。之所以還能訪問這個變量,是因為內部函數的作用域鏈中包含createCF()的作用域
                    if(val1 < val2){
                        return -1;
                    }else{
                        return 0;
                    }
                }

            }
            var o1 = {age : 14};
            var o2 = {age : 13};
            var CF = createCF('age');
                /*
                 有匿名函數從createCF()被返回后,它的作用域鏈被初始化為包含createCF()函數的活動對象和全局變量對象。這樣,匿名函數
                 就可以訪問在createCF()中定義的所以變量,更為重要的是,createCF()函數在執行完畢時,其活動對象也不會被銷毀,因為匿名函數的
                 作用域鏈仍然在引用這個活動對象。換句話說,當createCF()函數返回后,其執行環境的作用域鏈會被銷毀,但它的活動對象仍然會留在內存中
                 。直到匿名含函數被銷毀后,createCF()函數的活動對象才會被銷毀。
                 */
            var res = CF(o1,o2);
            CF = null;
            //解除對匿名函數的引用(以便釋放內存
                /*
                首先,創建的比較函數被保存在變量CF中。通過將CF設置為null解除該函數的引用,就等於通知垃圾回收例程將其清除。
                隨着匿名函數的作用域鏈被銷毀,其他作用域(除了全局作用域)也都可以安全的銷毀了。
                 */

 


當函數被調用時,會創建一個執行環境及相應的作用域鏈。然后使用arguments 和 其他命名參數和初始化函數的活動對象。
但在作用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位。。。直至作為作用域終點的全局執行環境。
無論什么時候在函數中訪問一個變量時,就會從作用域鏈中搜索具有相應名字的變量。一般來講,當函數執行完畢后,局部活動對象就會被銷毀,內存
中僅保存全局作用域(全局執行環境的變量對象)。但是,閉包的情況又有所不同:
在另一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中。因此,在createCF()函數內部定義的匿名函數的作用域鏈中
實際上會包含外部函數createCF()的活動對象
注意:由於閉包會攜帶包含它的函數的作用域,因此會比其他和函數占用更多的內存。過度使用閉包可能會導致內存占用過多,我們建議讀者只在必要時再
考慮使用閉包


別忘了閉包所保存的是整個變量對象,而不是某個特殊的變量。 作用鏈的這種配置機制引出了一個值得注意的副作用,即閉包只能取得包含函數中任何變量的最后一個值。
   function createFunctions(){
        var result = [];
        for(var i = 0; i < 10; i++){
            result[i] = function () {
                return i;
            };
        }
        return result;
    }
    var res = createFunctions();
    for( var i in res){
        console.log(res[i]());   // 每個函數返回的都是10
    }
    //當createFunctions()函數返回后,變量i的值是10,此時每個函數都引用着保存着變量i的同一個變量對象。所以在每個函數內部i的值都是10.
        // 但是,我們可以創建另一個匿名函數強制讓閉包的行為符合預期。



    function _createFunctions(){
        var result = [];
        for(var i = 0; i < 10; i++){
            result[i] = function (num){
                return function () {
                    return num;
                };
            }(i);
        }
        return result;
    }
    var _res = _createFunctions();
    for( var i in _res){
        console.log(_res[i]());   // 返回 0 - 9
    }
    // 這個版本,我們沒有直接把閉包賦值給數組,而是定義了一個匿名函數,並將立即執行該匿名函數的結果賦值給數組。
        // 這里的匿名函數有一個參數num,也就是最終的函數要返回的值。在調用每個匿名函數時,我們傳入了變量i。
        // 由於函數的參數是按值傳遞的,所以就會將變量i的值賦值給參數num。而在這個匿名函數內部,又創建了一個訪問num的閉包。
        // 這樣一來,result數組中的每個函數都有自己num變量的一個副本,因此就可以返回各自不同的數值了。

 


關於this對象
在閉包中使用this對象也可能會導致一些問題。我們直到this對象是在運行時基於函數的執行環境綁定的:
在全局函數中,this等於window
而當函數與被作為某個對象的方法調用時,this等於那個對象
不過,匿名函數的執行環境具有全局性,因此其this對象通常指向window。但有時候由於編寫閉包的方式不同,這一點可能不會那么明顯。
        var name = "the window";
            var object = {
                name : "my object",
                getNameFunc : function () {
                    return function () {
                        return this.name;
                    }
                }
            };
            console.log(object.getNameFunc()());  // the window
            // 為什么匿名函數沒有取得其包含作用域(或外部作用域)的this對象呢?
                /*
                前面已經提到過,每個函數被調用時,都會自動取得兩個特殊變量:this 和 arguments 。內部函數在搜索這兩個變量時,只會搜索到其活動對象為止
                因此永遠不可能直接訪問外部函數中的這兩個變量。不過,把外部作用域的this對象保存到在一個閉包能夠訪問到的變量,就可以讓閉包訪問該對象了。
                 */


            var name = "the window";
            var object = {
                name : "my object",
                getNameFunc : function () {
                    var that = this;
                    return function () {
                        return that.name;
                    }
                }
            };
            console.log(object.getNameFunc()());   //my object

 


注意: 閉包會引用包含函數的整個活動對象,即使閉包不直接引用,包含函數的活動對象也仍然會保存一個引用,設為null可以解決這個問題
模仿塊級作用域
 (function () {
       //在這里面的是塊級作用域
        console.log(123);
    })()

 


這種技術經常被用在函數外部,從而限制向全局作用域中添加過多的變量和函數。一般來說在一個由很多開發人員參與的大型運用程序中,過多的全局變量很容易
導致命名沖突。而創建私有作用域,每個開發人員可以使用自己的變量,又不用擔心搞亂全局作用域

私有變量
嚴格來講,js中沒有私有成員的概念;所以對象屬性都是公有的,不過,倒是有一個私有變量的概念。任何在函數中定義的變量,都可以認為是私有變量。
因此,不能在函數的外部訪問這些變量。私有變量包含函數的參數,局部變量和再函數內部定義的其他函數
      function Person(name){
             this.getName = function () {
                 return name;
             }
             this.setName = function (value) {
                name = value;
             }
         }
         var person = new Person('tom');
         console.log(person.getName()); // tom
         person.setName('bob');
         console.log(person.getName()); // bob


            /*
            以上代碼的構造函數中定義了兩個特權方法:getName()和 setName()。這兩個方法都可以在構造函數外部使用,而且都有權訪問私有變量name。
            私有變量name在Person中的每個實例中都不相同,因此每次調用構造函數都會重新創建這兩個方法。不過在構造函數中定義特權方法也有一個缺點,
            那就是你必須使用構造函數模式來達成這個目的。構造函數的缺點就是針對每個實例都會創建同樣一組新方法,而使用靜態私有變量來實現特權方法
            就可以避免這個問題。
             */
靜態私有變量
        (function () {
            var name = '';
            Person = function (value) {
                name = value;
            }
            Person.prototype.getName = function () {
                return name;
            }
            Person.prototype.setName = function (value) {
                name = value;
            }
        })();
        var p1  = new Person('ni');
        console.log(p1.getName());
        /*
            以這種方式創建靜態私有變量會因為使用原型而增進代碼復用,但每個實例都沒有自己的私有變量。
            多查找作用域鏈中的一個層次,就會在一定程度上影響查找速度,而這正是使用閉包和私有變量的一個顯明的不足之處
         */

 


模塊模式
前面的模式是用於為自定義類型創建私有變量和特權方法的。而道格拉斯所說的模塊模式,則是為單例創建私有變量和方法的。所謂單例,指的就是只有一個實例的
對象。按照慣例,js是以對象字面量的方式來創建單例對象的。
     var singleton = function () {
            // 私有變量和函數
            var privateVariable = 10;
            function privateFunction () {
                return false;
            }
            // 特權 / 公有方法和屬性
            return {
                publicProperty : true,
                publicMethod : function () {
                    privateVariable++;
                    return privateFunction();
                }
            }
        }();
            console.log(singleton);  // {publicProperty: true, publicMethod: ƒ}
            /*
                這個模塊模式使用了一個返回對象的匿名函數。在這個匿名函數內部,首先定義了私有變量和函數。
                然后,將一個對象字面量作為函數的值返回。由於這個對象是在匿名函數內部定義的,因此它的公有方法有權訪問私有變量和函數。從本質上來講,
                這個對象字面量定義的是單例的公共接口。這種模式在需要對單例進行某些初始化,同時又需要維護其私有變量時非常有用
             */

            var app = function (){
                var data = [];
                return {
                    joinData : function (val) {
                        data[data.length] = val;
                    },
                    getData : function(){  // !!這里有閉包,私有變量的原理還是閉包!
                        console.log(data);
                    }
                }
            }();
            app.joinData('demo');
            app.getData();
            /*
            簡言之,如果必須創建一個對象並以某些數據對其進行初始化,同時還要公開一些能夠訪問這些私有數據的方法,那就可以使用模塊模式
             */

 



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM