摘要:刪除對匿名函數(shù)的引用,以便釋放內(nèi)存在匿名函數(shù)從中被返回后,它的作用域鏈被初始化為包含函數(shù)的活動對象和全局變量對象。閉包與變量我們要注意到,閉包只能取到任意變量的最后值,也就是我們保存的是活動對象,而不是確定值。
工作中會遇到很多 this對象 指向不明的問題,你可能不止一次用過 _self = this 的寫法來傳遞this對象,它每每會讓我們覺得困惑和抓狂,我們很可能會好奇其中到底發(fā)生了什么。
一個問題現(xiàn)在先來看一個具體的問題:
var name = "The Window"; var obj = { name: "My obj", getName: function() { return this.name; } }; // 猜測下面的輸出和背后的邏輯(非嚴格模式下) object.getName(); (object.getName)(); (object.getName = object.getName)();
如果上面的三個你都能答對并知道都發(fā)生了什么,那么你對JS的this了解的比我想象的要多,可以跳過這篇文章了,如果沒答對或者不明白,那么這篇文章會告訴你并幫你梳理下相關(guān)的知識。
它們的答案是:
object.getName(); // "My Obj" (object.getName)(); // "My Obj" (object.getName = object.getName)(); // "The Window"函數(shù)的作用域
在函數(shù)被調(diào)用的時候,會創(chuàng)建一個執(zhí)行環(huán)境及相應(yīng)的作用域鏈,然后,使用arguments以及其他命名參數(shù)的值來初始化函數(shù)的活動對象(activation object,簡稱AO)。在作用域上,函數(shù)會逐層復(fù)制自身調(diào)用點的函數(shù)屬性,完成作用域鏈的構(gòu)建,直到全局執(zhí)行環(huán)境。
function compare(value1, value2) { return value1 - value2; } var result = compare(5, 10);
在這段代碼中,result通過var進行了變量聲明提升,compare通過function函數(shù)聲明提升,在代碼執(zhí)行之前我們的全局變量對象中就會有這兩個屬性。
每個執(zhí)行環(huán)境都會有一個變量對象,包含存在的所有變量的對象。全局環(huán)境的變量對象始終存在,而像compare函數(shù)這樣的局部環(huán)境的變量對象,則只在函數(shù)執(zhí)行的過程中存在。當創(chuàng)建compare()函數(shù)時,會創(chuàng)建一個預(yù)先包含全局變量對象的作用域鏈,這個作用域鏈保存在內(nèi)部的[[Scope]]屬性中。
在調(diào)用compare函數(shù)時,會為它創(chuàng)建一個執(zhí)行環(huán)境,然后復(fù)制函數(shù)的[[scope]]屬性中的對象構(gòu)建起執(zhí)行環(huán)境的作用域鏈。此后,又有一個活動對象(變量對象)被創(chuàng)建并被推入執(zhí)行環(huán)境作用域鏈的前端。此時作用域鏈包含兩個變量對象:本地活動對象和全局變量對象。顯然,作用域鏈本質(zhì)上是一個指向變量對象的指針列表,它只引用但不包含實際的變量對象。
當訪問函數(shù)的變量時,就會從作用域鏈中搜索。當函數(shù)執(zhí)行完畢后,局部活動對象就會被銷毀,內(nèi)存中僅保存全局作用域。
閉包但是,閉包的情況有所不同,在一個函數(shù)內(nèi)部定義的函數(shù)會將外部函數(shù)的活動對象添加到它的作用域鏈中去。
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 // 刪除對匿名函數(shù)的引用,以便釋放內(nèi)存 compare = null;
在匿名函數(shù)從create()中被返回后,它的作用域鏈被初始化為包含create()函數(shù)的活動對象和全局變量對象。這樣,該匿名函數(shù)就可以訪問create中定義的所有遍歷,更為重要的是當create()函數(shù)執(zhí)行完畢后,其作用域鏈被銷毀,但是活動對象不會銷毀,因為依然被匿名函數(shù)引用。當匿名函數(shù)別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。但我們可以通過值傳遞的方式創(chuàng)建另外一個匿名函數(shù)來滿足我們的需求。
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對象是基于函數(shù)的執(zhí)行環(huán)境綁定的,在全局的時候,this等于window,而當函數(shù)作為某個對象的方法調(diào)用時,this等于那個對象。不過,匿名函數(shù)的執(zhí)行環(huán)境具有全局性,因此this常常指向window。
var name = "The Window"; var obj = { name: "My obj", getName: function() { return function() { return this.name; }; } }; obj.getName()(); // "The Window"
前面說過,函數(shù)在被調(diào)用時會自動取得兩個特殊變量: this和arguments,內(nèi)部函數(shù)在搜索這兩個變量時,只會搜索到其活動對象,所以永遠不會訪問到外部函數(shù)的這兩個變量。如果我們想滿足需求,可以固定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的說明可以說是非常的淺薄了,現(xiàn)在我們詳細的整理下this關(guān)鍵字,它是函數(shù)作用域的特殊關(guān)鍵字,進入函數(shù)執(zhí)行環(huán)境時會被自動定義,實現(xiàn)原理相當于自動傳遞調(diào)用點的對象:
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,也就是函數(shù)調(diào)用時,執(zhí)行環(huán)境的this值。引擎的這種實現(xiàn)簡化了我們的工作,自動傳遞調(diào)用點的環(huán)境對象作為this對象。
我們要注意的是this只跟調(diào)用點有關(guān),而跟聲明點無關(guān)。這里你需要知道調(diào)用棧,也就是使我們到達當前執(zhí)行位置而被調(diào)用的所有方法的棧,即所有嵌套的函數(shù)棧。
function baz() { // 調(diào)用棧是: `baz` // 我們的調(diào)用點是global scope(全局作用域) console.log( "baz" ); bar(); // <-- `bar`的調(diào)用點 } function bar() { // 調(diào)用棧是: `baz` -> `bar` // 我們的調(diào)用點位于`baz` console.log( "bar" ); foo(); // <-- `foo`的調(diào)用點 } function foo() { // 調(diào)用棧是: `baz` -> `bar` -> `foo` // 我們的調(diào)用點位于`bar` console.log( "foo" ); } baz(); // <-- `baz`的調(diào)用點
我們整理了四種this對象綁定的規(guī)則:
默認綁定function foo() { console.log( this.a, this === window ); } var a = 2; window.a; // 2 foo(); // 2 true
在這種規(guī)則下,函數(shù)調(diào)用為獨立的毫無修飾的函數(shù)引用調(diào)用的,此時foo的調(diào)用環(huán)境就是全局環(huán)境window,所以this就指向window,而在全局下聲明的所有對象都屬于window,導(dǎo)致結(jié)果為2。
但是在嚴格模式下,this不會被默認綁定到全局對象。MDN文檔上寫到:
第一,在嚴格模式下通過this傳遞給一個函數(shù)的值不會被強制轉(zhuǎn)換為一個對象。對一個普通的函數(shù)來說,this總會是一個對象:不管調(diào)用時this它本來就是一個對象;還是用布爾值,字符串或者數(shù)字調(diào)用函數(shù)時函數(shù)里面被封裝成對象的this;還是使用undefined或者null調(diào)用函數(shù)式this代表的全局對象(使用call, apply或者bind方法來指定一個確定的this)。這種自動轉(zhuǎn)化為對象的過程不僅是一種性能上的損耗,同時在瀏覽器中暴露出全局對象也會成為安全隱患,因為全局對象提供了訪問那些所謂安全的JavaScript環(huán)境必須限制的功能的途徑。所以對于一個開啟嚴格模式的函數(shù),指定的this不再被封裝為對象,而且如果沒有指定this的話它值是undefined。
function foo() { "use strict"; console.log( this ); } foo(); // undefined
關(guān)于嚴格模式還需要注意的是,它的作用范圍只有當前的函數(shù)或者