JavaScript 中 this 是如何工作的 ?


先來看看這個題目:

var x = 0;
var foo = {
    x:1,
    bar:{
    x:2,
    baz: function () { console.log(this.x) } } } var a = foo.bar.baz foo.bar.baz() // 2 a() //0 
  • this 永遠指向函數運行時所在的對象,而不是函數創建時所在的對象
  • 匿名函數和不處於任何對象中的函數,This指向window
  • call, apply, with指的This是誰就是誰。
  • 普通函數調用,函數被誰調用,This就指向誰

上面的例子中,baz被bar調用所以指向的指bar. a 運行時所在的對象是 window,所以指向的是window。

作用域鏈?

理解執行環境和上下文

函數調用都有與之相關的作用域和上下文。從根本上說,作用域是基於函數(function-based)而上下文是基於對象(object-based)。換句話說,作用域是和每次函數調用時變量的訪問有關,並且每次調用都是獨立的。上下文總是關鍵字 this 的值,是調用當前可執行代碼的對象的引用。

執行上下文分有globalfunctioneval,一個函數可以產生無數個執行上下文,一系列的執行上下文從邏輯上形成了 執行上下文棧,棧底總是全局上下文,棧頂是當前(活動的)執行上下文。

執行上下文三屬性:this指針,變量對象(數據作用域),作用域鏈

作用域鏈 即:一變量在自己的作用域中沒有,那么它會尋找父級的,直到最頂層。過程如下:

  • 任何在執行上下文時刻的作用域都由作用域鏈來實現
  • 在一個函數被定義的時候, 會將它定義時刻的scope chain鏈接到這個函數對象的[[scope]]屬性
  • 在一個函數對象被調用的時候,會創建一個活動對象(也就是一個對象), 然后對於每一個函數的形參,都命名為該活動對象的命名屬性, 然后將這個活動對象做為此時的作用域鏈(scope chain)最前端, 並將這個函數對象的[[scope]]加入到scope chain中.

上面的文字大家可以好好琢磨一下,可以更好的理解函數作用域。

函數聲明提升和變量聲明提升(Hoisting) ?

我們先來了解js編譯器在執行代碼的過程:
以執行一段function代碼為例:
第一步:創建可執行上下文(以下簡稱為EC),壓入當前的EC棧中。EC中包括了以下信息:

  • 詞法環境(=環境記錄項(保存變量、函數聲明和形參)+ 外部詞法環境(function的[[scope]]屬性,作用域鏈的本質))
  • this的指針
  • 變量環境(與環境記錄項的值相同,但不再發生變動。)

第二步:收集函數聲明變量聲明形參,保存在環境記錄項內。這個收集的過程,就是一般所謂的聲明提升現象的本質。如果發現了重復的標識符,則優先級函數聲明形參變量聲明(優先級低的會被無視)。

第三步:開始執行代碼,環境記錄項內沒有的標識符會根據作用域鏈查找標識符對應的值,環境記錄項亦有可能因賦值語句而被修改。

第四步:函數執行完畢,EC棧被彈出、銷毀。

好了,第二步說的很清楚了 聲明提升(Hoisting)現象就是在收集函數、變量聲明和形參的過程會根據函數聲明、形參、變量聲明的順序優先級來收集。

例子:

var a = 1;  
function b() { a = 10; return; function a() {} } b(); console.log(a); // 輸出1 由於函數聲明提升,b內的實際是這樣: // function b() { // function a() {}; 這里是函數聲明提升 // a = 10; // return; // function a() {} // } 
理解了嗎?
勘誤:謝謝github上有同學的指正 關於博客中的一個問題 · Issue #1 · stephenzhao/hexo-theme-damon,上面的正確執行應該為先進行預編譯,所以先執行function a(){},然后會進行對a的賦值操作。
//正確的順序應該為:
// function b() { // function a() {} // a = 10; // return; // } 

什么是閉包,如何使用它,為什么要使用它?

還是上面的題目,做個變形。

var x = 0;
var foo = {
    x:1,
    bar:function () { console.log(this.x); var that = this; return function () { console.log(this.x) console.log(that.x) } } } foo.bar() // 1 foo.bar()() // this: 0, that: 1 

上面的例子中ba'r里面返回了一個匿名函數,這個匿名函數可以在外部被調用即:foo.bar()() 讀取到了bar的執行上下文的變量對象 that,這個函數就形成了一個閉包。

好了,我們理解了上面的套路,下面來解釋閉包就好理解了。

閉包就是能夠讀取其它函數內部變量的函數

在Javascript語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成“定義在一個函數內部的函數”

var x = 0;
var bar:function () { var n = 999; return function () { return n; } } var outer = bar(); outer() // 999 

用途:

  1. 讀取函數內部的變量
  2. 讓這些變量的值始終保持在內存中

我們修改一下上面的代碼

var add;
var bar = function () { var n = 999; add = function () { n += 1; } return function () { return n; } } var outer = bar(); outer() // 999 add(); outer(); // 1000 

說明,n一直保存在內存當中,而沒有在bar()執行完成之后被銷毀;
原因:
bar里面的匿名函數被賦值給了outer,這個導致在outer沒有被銷毀的時候,該匿名函數一直存在內存當中,而匿名函數的存在依賴於bar,所以bar需要使用都在內存當中,所以bar並不會在調用結束后唄垃圾回收機制給收回。

而上面的add接受的也是一個匿名函數,該匿名函數本身也是閉包,所以也可以在外部操作里面的變量。

注意點

  1. 會導致內存泄漏,慎用
  2. 閉包會修改內部變量的值,所以在使用閉包作為對象的公用方法時要謹慎。
    閉包的一個應用,單例模式

單例模式的定義是產生一個類的唯一實例

單例模式在js中經常會遇到,比如 var a = {}; 其實就是一個單例子。

但是我們寫一個更有意義的單例:

var singleton = function( fn ){ var result; return function(){ return result || ( result = fn .apply( this, arguments ) ); } } 

更簡潔一點的:

var singleton = (function () { var instance; return function (object) { if(!instance){ instance = new object(); } return instance; } })();

我的前端博客http://www.gaoyang521.top


免責聲明!

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



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