摘要:刪除對匿名函數的引用,以便釋放內存在匿名函數從中被返回后,它的作用域鏈被初始化為包含函數的活動對象和全局變量對象。閉包與變量我們要注意到,閉包只能取到任意變量的最后值,也就是我們保存的是活動對象,而不是確定值。
工作中會遇到很多 this對象 指向不明的問題,你可能不止一次用過 _self = this 的寫法來傳遞this對象,它每每會讓我們覺得困惑和抓狂,我們很可能會好奇其中到底發生了什么。
一個問題現在先來看一個具體的問題:
var name = "The Window"; var obj = { name: "My obj", getName: function() { return this.name; } }; // 猜測下面的輸出和背后的邏輯(非嚴格模式下) object.getName(); (object.getName)(); (object.getName = object.getName)();
如果上面的三個你都能答對并知道都發生了什么,那么你對JS的this了解的比我想象的要多,可以跳過這篇文章了,如果沒答對或者不明白,那么這篇文章會告訴你并幫你梳理下相關的知識。
它們的答案是:
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]]屬性中的對象構建起執行環境的作用域鏈。此后,又有一個活動對象(變量對象)被創建并被推入執行環境作用域鏈的前端。此時作用域鏈包含兩個變量對象:本地活動對象和全局變量對象。顯然,作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不包含實際的變量對象。
當訪問函數的變量時,就會從作用域鏈中搜索。當函數執行完畢后,局部活動對象就會被銷毀,內存中僅保存全局作用域。
閉包但是,閉包的情況有所不同,在一個函數內部定義的函數會將外部函數的活動對象添加到它的作用域鏈中去。
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
關于嚴格模式還需要注意的是,它的作用范圍只有當前的函數或者標簽內部,而不包括嵌套的函數體:
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操作符會調用對象的構造器函數來初始化類成為一個實例。它的執行過程為:
一個全新的對象被憑空創建
這個新構造的對象被接入原型鏈(__proto__指向該構造函數的prototype)
這個新構造的對象被綁定為函數調用的this對象
除非函數返回一個其它對象,這個被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"。
參考資料簡書 - this與對象原型: http://www.jianshu.com/p/11d8...
MDN - bind: https://developer.mozilla.org...
Github - 深入變量對象:https://github.com/mqyqingfen...
JS高級程序設計:第五章(引用類型),第七章(函數表達式)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/88967.html
摘要:理解的函數基礎要搞好深入淺出原型使用原型模型,雖然這經常被當作缺點提及,但是只要善于運用,其實基于原型的繼承模型比傳統的類繼承還要強大。中文指南基本操作指南二繼續熟悉的幾對方法,包括,,。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。 怎樣使用 this 因為本人屬于偽前端,因此文中只看懂了 8 成左右,希望能夠給大家帶來幫助....(據說是阿里的前端妹子寫的) this 的值到底...
摘要:跨域請求詳解從繁至簡前端掘金什么是為什么要用是的一種使用模式,可用于解決主流瀏覽器的跨域數據訪問的問題。異步編程入門道典型的面試題前端掘金在界中,開發人員的需求量一直居高不下。 jsonp 跨域請求詳解——從繁至簡 - 前端 - 掘金什么是jsonp?為什么要用jsonp?JSONP(JSON with Padding)是JSON的一種使用模式,可用于解決主流瀏覽器的跨域數據訪問的問題...
摘要:不是引用類型,無法輸出簡而言之,堆內存存放引用值,棧內存存放固定類型值。變量的查詢在變量的查詢中,訪問局部變量要比全局變量來得快,因此不需要向上搜索作用域鏈。 贊助我以寫出更好的文章,give me a cup of coffee? 2017最新最全前端面試題 基本類型值有:undefined,NUll,Boolean,Number和String,這些類型分別在內存中占有固定的大小空...
摘要:個人前端文章整理從最開始萌生寫文章的想法,到著手開始寫,再到現在已經一年的時間了,由于工作比較忙,更新緩慢,后面還是會繼更新,現將已經寫好的文章整理一個目錄,方便更多的小伙伴去學習。 showImg(https://segmentfault.com/img/remote/1460000017490740?w=1920&h=1080); 個人前端文章整理 從最開始萌生寫文章的想法,到著手...
閱讀 2659·2021-09-13 10:26
閱讀 1919·2021-09-03 10:28
閱讀 1993·2019-08-30 15:44
閱讀 810·2019-08-29 14:07
閱讀 398·2019-08-29 13:12
閱讀 2154·2019-08-26 11:44
閱讀 2346·2019-08-26 11:36
閱讀 2015·2019-08-26 10:19