JS的閉包與this詳解

工做中會遇到不少 this對象 指向不明的問題,你可能不止一次用過 _self = this 的寫法來傳遞this對象,它往往會讓咱們以爲困惑和抓狂,咱們極可能會好奇其中到底發生了什麼。前端

一個問題

如今先來看一個具體的問題:git

var name = 'The Window';
var obj = {
    name: 'My obj',
    getName: function() {
        return this.name;
    }
};

// 猜想下面的輸出和背後的邏輯(非嚴格模式下)
object.getName();
(object.getName)();
(object.getName = object.getName)();

若是上面的三個你都能答對並知道都發生了什麼,那麼你對JS的this瞭解的比我想象的要多,能夠跳過這篇文章了,若是沒答對或者不明白,那麼這篇文章會告訴你並幫你梳理下相關的知識。
它們的答案是:github

object.getName();    // 'My Obj'
(object.getName)();    // 'My Obj'
(object.getName = object.getName)();    // 'The Window'

函數的做用域

在函數被調用的時候,會建立一個執行環境及相應的做用域鏈,而後,使用arguments以及其餘命名參數的值來初始化函數的活動對象(activation object,簡稱AO)。在做用域上,函數會逐層複製自身調用點的函數屬性,完成做用域鏈的構建,直到全局執行環境。瀏覽器

function compare(value1, value2) {
    return value1 - value2;
}

var result = compare(5, 10);

圖片描述

在這段代碼中,result經過var進行了變量聲明提高,compare經過function函數聲明提高,在代碼執行以前咱們的全局變量對象中就會有這兩個屬性。安全

每一個執行環境都會有一個變量對象,包含存在的全部變量的對象。全局環境的變量對象始終存在,而像compare函數這樣的局部環境的變量對象,則只在函數執行的過程當中存在。當建立compare()函數時,會建立一個預先包含全局變量對象的做用域鏈,這個做用域鏈保存在內部的[[Scope]]屬性中。閉包

在調用compare函數時,會爲它建立一個執行環境,而後複製函數的[[scope]]屬性中的對象構建起執行環境的做用域鏈。此後,又有一個活動對象(變量對象)被建立並被推入執行環境做用域鏈的前端。此時做用域鏈包含兩個變量對象:本地活動對象和全局變量對象。顯然,做用域鏈本質上是一個指向變量對象的指針列表,它只引用但不包含實際的變量對象。app

當訪問函數的變量時,就會從做用域鏈中搜索。當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局做用域。函數

閉包

可是,閉包的狀況有所不一樣,在一個函數內部定義的函數會將外部函數的活動對象添加到它的做用域鏈中去。oop

function create(property) {
    return function(object1, object2) {
        console.log(object1[property], object2[property]);
    };
}

var compare = create('name');
var result = compare({name: 'Nicholas'}, {name: 'Greg'}); // Nicholas Greg

// 刪除對匿名函數的引用,以便釋放內存
compare = null;

在匿名函數從create()中被返回後,它的做用域鏈被初始化爲包含create()函數的活動對象和全局變量對象。這樣,該匿名函數就能夠訪問create中定義的全部遍歷,更爲重要的是當create()函數執行完畢後,其做用域鏈被銷燬,可是活動對象不會銷燬,由於依然被匿名函數引用。當匿名函數別compare()被銷燬後,create()的活動對象纔會被銷燬。性能

圖片描述

閉包與變量

咱們要注意到,閉包只能取到任意變量的最後值,也就是咱們保存的是活動對象,而不是肯定值。

function create() {
    var result = [];
    for (var i = 0; i < 10; i++) {
        result[i] = function() {
            return i;
        };
    }
    return result;
}

create()[3](); // 10

咱們經過閉包,讓每個result的元素都可以返回i的值,可是閉包包含的是同一個活動對象i,而不是固定的1-10的值,因此返回的都是10。但咱們能夠經過值傳遞的方式建立另一個匿名函數來知足咱們的需求。

function create() {
    var result = [];
    for (var i = 0; i < 10; i++) {
        // 經過值傳遞的方式固定i值
        result[i] = function(num) {
            // 這裏閉包固定後的i值,即num值,來知足咱們的需求
            return function() {
                return num;
            };
        }(i);
    }
    return result;
}

create()[3](); // 3

閉包與this

咱們知道this對象是基於函數的執行環境綁定的,在全局的時候,this等於window,而當函數做爲某個對象的方法調用時,this等於那個對象。不過,匿名函數的執行環境具備全局性,所以this經常指向window。

var name = 'The Window';
var obj = {
    name: 'My obj',
    getName: function() {
        return function() {
            return this.name;
        };
    }
};

obj.getName()(); // 'The Window'

前面說過,函數在被調用時會自動取得兩個特殊變量: this和arguments,內部函數在搜索這兩個變量時,只會搜索到其活動對象,因此永遠不會訪問到外部函數的這兩個變量。若是咱們想知足需求,能夠固定this對象並改名便可。

var name = 'The Window';
var obj = {
    name: 'My obj',
    getName: function() {
        // 固定this對象,造成閉包,防止跟特殊的this重名
        var that = this;
        return function() {
            return that.name;
        };
    }
};

obj.getName()(); // 'My obj'

this的綁定

上面對this的說明能夠說是很是的淺薄了,如今咱們詳細的整理下this關鍵字,它是函數做用域的特殊關鍵字,進入函數執行環境時會被自動定義,實現原理至關於自動傳遞調用點的對象:

var obj = {
    name: 'Nicholas',
    speak() {
        return this.name;
    },
    anotherSpeak(context) {
        console.log(context.name, context === this);
    }
};

obj.name;    //'Nicholas'
obj.speak();    // 'Nicholas'
obj.anotherSpeak(obj);    // 'Nicholas' true

能夠看到,咱們在anotherSpeak()中傳遞的context就是obj,也就是函數調用時,執行環境的this值。引擎的這種實現簡化了咱們的工做,自動傳遞調用點的環境對象做爲this對象。

咱們要注意的是this只跟調用點有關,而跟聲明點無關。這裏你須要知道調用棧,也就是使咱們到達當前執行位置而被調用的全部方法的棧,即全部嵌套的函數棧。

function baz() {
    // 調用棧是: `baz`
    // 咱們的調用點是global scope(全局做用域)

    console.log( "baz" );
    bar(); // <-- `bar`的調用點
}

function bar() {
    // 調用棧是: `baz` -> `bar`
    // 咱們的調用點位於`baz`

    console.log( "bar" );
    foo(); // <-- `foo`的調用點
}

function foo() {
    // 調用棧是: `baz` -> `bar` -> `foo`
    // 咱們的調用點位於`bar`

    console.log( "foo" );
}

baz(); // <-- `baz`的調用點

咱們整理了四種this對象綁定的規則:

默認綁定

function foo() {
    console.log( this.a, this === window );
}
var a = 2;

window.a;    // 2
foo();    // 2 true

在這種規則下,函數調用爲獨立的毫無修飾的函數引用調用的,此時foo的調用環境就是全局環境window,因此this就指向window,而在全局下聲明的全部對象都屬於window,致使結果爲2。

可是在嚴格模式下,this不會被默認綁定到全局對象。MDN文檔上寫到:

第一,在嚴格模式下經過this傳遞給一個函數的值不會被強制轉換爲一個對象。對一個普通的函數來講,this總會是一個對象:無論調用時this它原本就是一個對象;仍是用布爾值,字符串或者數字調用函數時函數裏面被封裝成對象的this;仍是使用undefined或者null調用函數式this表明的全局對象(使用call, apply或者bind方法來指定一個肯定的this)。這種自動轉化爲對象的過程不只是一種性能上的損耗,同時在瀏覽器中暴露出全局對象也會成爲安全隱患,由於全局對象提供了訪問那些所謂安全的JavaScript環境必須限制的功能的途徑。因此對於一個開啓嚴格模式的函數,指定的this再也不被封裝爲對象,並且若是沒有指定this的話它值是undefined。

function foo() {
    "use strict";
    console.log( this );
}

foo();    // undefined

關於嚴格模式還須要注意的是,它的做用範圍只有當前的函數或者<script>標籤內部,而不包括嵌套的函數體:

function foo() {
    console.log( this.a );
}

var a = 2;

(function(){
    "use strict";

    foo(); // 2
})();

隱含綁定

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2

在這個函數調用時,其調用點爲環境對象obj,因此函數執行時,this指向obj。

須要注意多重嵌套的函數引用,在調用時只考慮最後一層:

function foo() {
    console.log( this.a );
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42

若是函數並不直接執行,而是先引用後執行,那麼咱們應該明白,該變量得到的是另外一個指向該函數對象的指針,而脫離了引用的環境,因此天然失去了this的綁定,這被稱爲隱含綁定的丟失

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
// 函數引用!其實獲得的是另外一個指向該函數的指針,脫離了obj環境
var bar = obj.foo;

var a = "oops, global";

bar(); // "oops, global"

明確綁定

咱們除了上面的兩種默認綁定方式,還能夠對其進行明確的綁定,主要經過函數內置的call/apply/bind方法,經過它們能夠指定你想要的this對象是什麼:

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};

foo.call( obj ); // 2

咱們給foo的調用指定了obj做爲它的this對象,因此this.a即obj.a,結果爲2。

call/apply方法須要傳遞一個對象,若是你傳遞的爲簡單原始類型值null,undefined,則this會指向全局對象。若是傳遞的爲基本包裝對象,則this會指向他們的自動包裝對象,即new String(), new Boolean(), new Number(),這個過程稱爲封箱(boxing)。

這裏咱們應該清楚call/apply方法都只在最後一層嵌套生效,因此咱們稱呼它爲明確綁定:

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};

var bar = function() {
    foo.call( obj );
};

// `bar`將`foo`的`this`硬綁定到`obj`, 因此它不能夠被覆蓋
bar.call( window ); // 2

但若是咱們想複用並返回一個新函數,並固定this值時,能夠這樣作:

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}

// 簡單的`bind`幫助函數
function bind(fn, obj) {
    return function() {
        return fn.apply( obj, arguments );
    };
}

var obj = {
    a: 2
};

var bar = bind( foo, obj );

var b = bar( 3 ); // 2 3
console.log( b ); // 5

這種方式被稱爲硬綁定,也是明確綁定的一種,這個函數在被建立時就已經明確的聲明瞭做用域,也就是該對象被放置在了[[Scope]]屬性裏。這種方式有時很經常使用,因此被內置在ES5後的版本里,其內部實現(Polyfill低版本補丁)爲:

if (!Function.prototype.bind) {
    Function.prototype.bind = function(oThis) {
        if (typeof this !== "function") {
            // 可能的與 ECMAScript 5 內部的 IsCallable 函數最接近的東西
            throw new TypeError( "Function.prototype.bind - what " +
                "is trying to be bound is not callable"
            );
        }

        var aArgs = Array.prototype.slice.call( arguments, 1 ),
            fToBind = this,
            fNOP = function(){},
            fBound = function(){
                return fToBind.apply(
                    (
                        this instanceof fNOP &&
                        oThis ? this : oThis
                    ),
                    aArgs.concat( Array.prototype.slice.call( arguments ) )
                );
            }
        ;

        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();

        return fBound;
    };
}

在ES6裏,bind()生成的硬綁定函數擁有一個name屬性,源自於目標函數,此時顯示爲bound foo

在一些語言內置的函數裏,提供了可選參數做爲函數執行時的this對象,這些函數的內部實現方式和bind()相似,也是經過apply/call來明確綁定了你傳遞的參數做爲this對象,如:

var obj = {
    name: 'Nicholas'
};
[1,2,3].forEach(function(item) {
    console.log(item, this.name);
}, obj);
// 1 "Nicholas"
// 2 "Nicholas"
// 3 "Nicholas"

new綁定

new操做符會調用對象的構造器函數來初始化類成爲一個實例。它的執行過程爲:

  1. 一個全新的對象被憑空建立
  2. 這個新構造的對象被接入原型鏈(__proto__指向該構造函數的prototype)
  3. 這個新構造的對象被綁定爲函數調用的this對象
  4. 除非函數返回一個其它對象,這個被new調用的函數將返回這個新構建的對象。

實質上加new關鍵字和()只不過是該函數的不一樣調用方式而已,前者爲構造器調用,後者爲執行調用,在調用過程當中,this指向不一樣,返回值不一樣。

在new綁定的規則中,this指向新建立的對象。

箭頭函數綁定

如今咱們看一個十分特別的this綁定,ES6中加入的箭頭函數,前面的四種都是函數執行時經過調用點確認this對象,而箭頭函數是在詞法做用域肯定this對象,即在詞法解析到該箭頭時爲該函數綁定this對象爲當前對象:

function foo() {
    setTimeout(() => {
        // 這裏的`this`是詞法上從`foo()`採用
        console.log( this.a );
    },100);
}

var obj = {
    a: 2
};

foo.call( obj ); // 2

綁定的優先級

經過一些具體的實例對比,咱們能夠得出不一樣綁定方式的優先級:
new綁定 > 明確綁定 > 隱含綁定 > 默認綁定

箭頭函數屬於詞法做用域綁定,因此其優先級更高,可是跟上面的不衝突。

最初的問題

如今咱們再來看下最初的問題:

var name = 'The Window';
var obj = {
    name: 'My obj',
    getName: function() {
        return this.name;
    }
};

// 猜想下面的輸出和背後的邏輯(非嚴格模式下)
obj.getName();    // 'My obj'
(obj.getName)();    // 'My obj'
(obj.getName = obj.getName)();    // 'The Window'

咱們能夠看出第一個直接綁定this對象爲obj,第二個加上括號好像是引用了一個函數,但object.getName(object.getName)定義一致,因此this依然指向obj;第三個賦值語句會返回函數自己,因此做爲匿名函數來執行,就會返回'The Window'。

參考資料

  1. 簡書 - this與對象原型: http://www.jianshu.com/p/11d8...
  2. MDN - bind: https://developer.mozilla.org...
  3. Github - 深刻變量對象:https://github.com/mqyqingfen...
  4. JS高級程序設計:第五章(引用類型),第七章(函數表達式)
相關文章
相關標籤/搜索