js對象系列【二】深刻理解js函數,詳解做用域與做用域鏈。

 此次說一下對象具體的一個實例:函數,以及其對應的做用域與做用域鏈。簡單的東西你們查下API就好了,這裏我更多的是分享本身的理解與技巧。對於做用域和做用域鏈,相信絕大多數朋友看了個人分享都能基本理解,少數人看完以後再努力思考思考,基本也就懂了。最後說一下,不合理的地方,歡迎批評指正。html

函數調用es6

 跳過基本的函數定義,直接說函數調用,js中的函數調用有如下四種方式:數組

1.直接調用數據結構

2.做爲對象的方法調用閉包

看成爲對象調用時,這裏的this指向調用方法的對象,而咱們所說的鏈式調用便是在函數內部做用域的最後return this。當函數不須要明確的返回值時,咱們經常將this上下文返回,養成這種習慣有助於後期使用鏈式調用提升工做效率。app

3.經過構造函數調用,值得注意的是,若是構造函數沒有形參,圓括號是能夠省略的,以下:函數

new Array();
new Array;

構造函數的返回值固定爲實例對象,沒法修改。this

另外,當構造函數做爲對象的方法調用時,構造函數中的this仍指向實例對象es5

        var obj = {
            a: function (name, sex) {
                this.name = name;
                this.sex = sex;
                console.log(this)
            }
        }
        var stu = new obj.a('kevin',18) // a {name: "kevin", sex: 18}

4.函數經過call()和apply() 間接調用方法,call和appy的第一個參數爲this上下文指向的母對象.spa

當以對象a的方法來調用fn(x,y)時

fn.call(a, x, y);
fn.apply(a, [x, y]);

當使用 call() 和 apply() 方法時(  以對象a的方法來調用fn() )

fn.call(a)

等價於以下代碼:

a.m = fn;
a.m();
delete a.m;

可選形參

當咱們在定義函數時,常常須要考慮形參的排列順序。傳入實參的個數能夠小於形參,但必須是一一對應,而且可選的形參必須放在形參列表的最後。

使用/*optional*/b 表示b爲可選參數

function test (a, /*optional*/b) {

// .....


}

與可選形參相比咱們則經常使用對象形參來代替形參列表,此方法經常應用於編寫插件的配置項

 做用域

 說道函數,就不得不說做用域,咱們在工做中,常常會遇到這樣的錯誤

1. XXX is not defined

2. can't find property 'XXX'

常見的緣由就是:調用方法的對象未被找的(未成功獲取)或變量在此做用域內未被找到。

而在聲明變量時咱們則應該聲明在函數做用域的最頂端,此習慣可讓開發者對變量所處的做用域一目瞭然。

js中的做用域爲函數做用域,即每一個函數內部爲一個做用域(做用域也可稱爲執行環境)。當系統在執行js代碼時,會建立一個做用域鏈,此做用域鏈的數據結構爲類棧(注意不徹底等同於棧,這裏多說無益)。

每一個做用域的代碼在執行時都會建立一個變量對象(VO),此變量對象(VO)中包括了在該做用域中定義的全部變量和函數。由外層向內層的變量對象(VO)會被依次push進做用域鏈中,全局做用域的變量對象(VO)始終會在做用域鏈的最底端(es5的聲明提早所決定),而當前執行代碼的做用域對象(VO)始終在做用域鏈的最頂端。當咱們在內部做用域修改全局做用域的變量的值時,因爲全局變量對象在棧的最底部,棧的指針須要依次尋找至棧的最底部,並修改全局做用域變量對象的相應值。若是全部變量都定義在全局變量中,內部變量確實是能夠訪問獲得,可是,執行效率會大大下降。這就是咱們爲何須要減小沒必要要的全局聲明的緣由,只調用一次的變量或者函數,儘量的在其執行環境所對應的做用域中聲明。

執行環境 = {
    VO:{/*函數中的arguments對象、參數、內部變量以及函數聲明*/}
    this:{},
    Scope:{/*當前做用域的VO以及全部父執行上下文中的VO(與prototype相似)*/}
}

注意:在當前做用域的代碼真正執行時,變量對象的值(變量和函數)纔會初始化完成。(下文會舉例說明)

在js中每一個變量都有其自身的歸屬,咱們能夠把用戶建立的全部變量類比爲一個你們庭中的成員。而全局做用域的變量對象可看作是「祖宗」,而做用域鏈就可看作是家庭的「血緣」,每一個函數做用域則可看做是一輩(一代)家庭成員,每一個家庭的目標與關注度都放在當前(最新)一輩人【當前執行環境對應的做用域】,而每一個家庭也都不能忘本,都要謹記祖宗或長輩的教誨【父做用域的變量】。

一說變量的做用域鏈,就離不開一個老生常談的例子:

// aBtn 爲五個按鈕的類數組
// 起初,咱們都很渴望打印出  0 1 2 3 4
      var aBtn = document.querySelectorAll('button');
        for (var i=0; i<aBtn.length; i++) {
             aBtn[i].onclick = function () {
                 console.log(i)
             }
       }
// 事實上,所有都是 5

這就用到上文所說的,for循環中的匿名函數在調用以前,i的值已經所有爲5,也就是for循環已經執行完。

從我的理解來說,不外呼如下四種方法:

        // 1 .
        // 爲btn增長加自定義屬性index,使其在匿名函數中可經過this上下文獲取
        var aBtn = document.querySelectorAll('button');
        for (var i=0; i<aBtn.length; i++) {
            aBtn[i].index = i;
             aBtn[i].onclick = function () {
                 console.log(this.index)
             }
        }
        // 2.
        // 在onclik的事件處理函數上級強行增長一個做用域(一代人),並在此做用域內初始化相應的i值
        var aBtn = document.querySelectorAll('button');
        for (var i=0; i<aBtn.length; i++) {
             clickFn(aBtn[i], i);
        }
        function clickFn (btn, index) {
            btn.onclick = function () {
                console.log(index)
            }
        }
        // 同第二種方法相似,只不過函數改成了匿名函數
        var aBtn = document.querySelectorAll('button');
        for (var i=0; i<aBtn.length; i++) {
            (function (i) {
                aBtn[i].onclick = function () {
                     console.log(i)
                 }
            })(i)
        }
        // 4.
        // 使用es6的let聲明變量,則在for循環的{}內也可看作是一個做用域
        var aBtn = document.querySelectorAll('button');
        for (let i=0; i<aBtn.length; i++) {
             aBtn[i].onclick = function () {
                 console.log(i)
             }
        }

閉包

閉包時做用域鏈的特殊應用的產物,特殊就特殊在閉包所指向的做用域與函數在定義時對應的做用域不一樣

用一句話歸納閉包的形式即:函數b嵌套在函數a內部,函數a返回函數b

        function a () {
            var x = 0;
            function b () {
                x++
                return x
            }
            return b()
        }
        console.log(a()) // 1

出現閉包的緣由是,有時候根據邏輯須要,咱們要在父級做用域中使用局部變量,而閉包就剛好解決了這個問題。另外一方面使用閉包得到的局部變量不會在局部做用域失效後就被清除。而是被保留下來。這是把雙刃劍,而它的缺點就是濫用閉包很容易形成「循環使用」以致於致使內存泄漏。

下面咱們看一個特殊的例子:

       function counter () {
            var n = 0;
            return {
                count (num) {
                    n = n + num;
                    return n
                },
                reeset () {
                    n = 0
                }
            }
        }
        var a = counter();
        var b = counter ();
        console.log(a.count(1)) // 1
        console.log(b.count(2)) // 2

這個例子就說明每次調用counter()都會出現一個新的做用域鏈分支和一個新的私有變量n,兩個私有變量互不影響。

像上面的特殊閉包,可使用對象的存取器屬性實現:

詳細瞭解Object的屬性,可見上一篇文章:http://www.cnblogs.com/pomelott/p/8082951.html

       var obj = {
            n: 0,
            get count () {
                return this.n
            },
            set count (val) {
                this.n = this.n + val;
                return this.n
            }
        }
        obj.count = 5;
        console.log(obj.n)  // 5
        console.log(obj.count) // 5

函數的其餘可挖掘內容和技巧還不少,這期暫時先分享到這。後續請繼續關注。

相關文章
相關標籤/搜索