深入理解Js中的this


深入理解Js中的this

JavaScript作用域為靜態作用域static scope,但是在Js中的this卻是一個例外,this的指向問題就類似於動態作用域,其並不關心函數和作用域是如何聲明以及在何處聲明的,只關心它們從何處調用,this的指向在函數定義的時候是確定不了的,只有函數執行的時候才能確定this到底指向誰,當然實際上this的最終指向的是那個調用它的對象。

作用域

我們先來了解一下JavaScript的作用域,以便理解為什么說this更類似於動態作用域,通常來說,一段程序代碼中所用到的名字並不總是有效或可用的,而限定這個名字的可用性的代碼范圍就是這個名字的作用域scope,當一個方法或成員被聲明,他就擁有當前的執行上下文context環境,在有具體值的context中,表達式是可見也都能夠被引用,如果一個變量或者其他表達式不在當前的作用域,則將無法使用。作用域也可以根據代碼層次分層,以便子作用域可以訪問父作用域,通常是指沿着鏈式的作用域鏈查找,而不能從父作用域引用子作用域中的變量和引用。
JavaScript作用域為靜態作用域static scope,也可以稱為詞法作用域lexical scope,其主要特征在於,函數作用域中遇到既不是參數也不是函數內部定義的局部變量時,去函數定義時上下文中查,而與之相對應的是動態作用域dynamic scope則不同,其函數作用域中遇到既不是參數也不是函數內部定義的局部變量時,到函數調用時的上下文中去查。

var a = 1;
var s = function(){
    console.log(a);
};

(function(){
    var a = 2;
    s(); // 1
})();

調用s()是打印的a1,此為靜態作用域,也就是聲明時即規定作用域,而假如是動態作用域的話在此處會打印2。現在大部分語言都采用靜態作用域,比如CC++JavaPHPPython等等,具有動態作用域的語言有Emacs LispCommon LispPerl等。

全局作用域

直接聲明在頂層的變量或方法就運行在全局作用域,借用函數的[[Scopes]]屬性來查看作用域,[[Scopes]]是保存函數作用域鏈的對象,是函數的內部屬性無法直接訪問但是可以打印來查看。

function s(){}
console.dir(s);
/*
  ...
  [[Scopes]]: Scopes[1]
    0: Global ...
*/
// 可以看見聲明的s函數運行的上下文環境是全局作用域

函數作用域

當聲明一個函數后,在函數內部聲明的方法或者成員的運行環境就是此函數的函數作用域

(function localContext(){
    var a = 1;
    function s(){ return a; }
    console.dir(s);
})();
/*
  ...
  [[Scopes]]: Scopes[2]
    0: Closure (localContext) {a: 1}
    1: Global ...
*/
// 可以看見聲明的s函數運行的上下文環境是函數localContext的作用域,也可以稱為局部作用域

塊級作用域

代碼塊內如果存在let或者const,代碼塊會對這些命令聲明的變量從塊的開始就形成一個封閉作用域。

{
    let a = 1;
    function s(){return a;}
    console.dir(s);
    /*
      ...
      [[Scopes]]: Scopes[2]
        0: Block {a: 1}
        1: Global ...
    */
}
// 可以看見聲明的s函數運行的上下文環境是Block塊級作用域,也是局部作用域

分析

我們在使用this之前有必要了解為什么在JavaScript中要有this這個設計,在這之前我們先舉個小例子,通常我們使用this時可能會遇到的典型問題就類似於下面這樣,雖然我們運行的都是同一個函數,但是執行的結果可能會不同。

var obj = {
    name: 1,
    say: function() {
        return this.name;
    }
};

window.name = 2;
window.say = obj.say;

console.log(obj.say()); // 1
console.log(window.say()); // 2

產生這樣的結果的原因就是因為使用了this關鍵字,前文已經提到了this必須要在運行時才能確定,在這里,對於obj.say()來說,say()運行的環境是obj對象,對於window.say()來說,say()運行的環境是window對象,所以兩者運行的結果不同。
此時我們就來了解一下,為什么JavaScript會有this這樣一個設計,我們首先來了解一下JavaScript的內存結構中的堆棧,堆heap是動態分配的內存,大小不定也不會自動釋放,棧stack為自動分配的內存空間,在代碼執行過程中自動釋放。JavaScript在棧內存中提供一個供Js代碼執行的環境,關於作用域以及函數的調用都是棧內存中執行的。Js中基本數據類型StringNumberBooleanNullUndefinedSymbol,占用空間小且大小固定,值直接保存在棧內存中,是按值訪問,對於Object引用類型,其指針放置於棧內存中,指向堆內存的實際地址,是通過引用訪問。
那么此時我們來看一下上邊的示例,在內存中對於obj對象是存放在堆內存的,如果在對象中的屬性值是個基本數據類型,那么其會跟這個對象存儲在同一塊內存區域,但是這個屬性值同樣可能是一個引用類型,那么對於say這個函數也是存在於堆內存中的,實際上在此處我們可以將其理解為這個函數的實際定義在一個內存區域(以一個匿名函數的形式存在),而obj這個對象同樣在其他的一個內存區域,obj通過say這個屬性指向了這個匿名函數的內存地址,obj --say--> funtion,那么此時問題來了,由於這種內存結構,我們可以使任何變量對象等指向這個函數,所以在JavaScript的函數中是需要允許我們取得運行環境的值以供使用的,我們必須要有一種機制,能夠在函數體內部獲得當前的運行環境context,所以this就出現了,它的設計目的就是在函數體內部,指代函數當前的運行環境。

使用

我們需要記住,this是在運行時進行綁定的,並不是在定義時綁定,它的context取決於函數調用時的各種條件,簡單來說this的綁定和函數聲明的位置沒有任何關系,只取決於函數的調用方式,再簡單來說this永遠指向調用者,但箭頭函數除外,接下來我們介紹一下五種this的使用情況。

默認綁定

最常用的函數調用類型即獨立函數調用,這個也是優先級最低的一個,此時this指向全局對象,注意如果使用嚴格模式strict mode,那么全局對象將無法使用默認綁定,因此this會變為undefined

var a = 1; //  變量聲明到全局對象中
function f1() {
    return this.a;
}

function f2() {
    "use strict";
    return  this;
}

console.log(f1()); // 1 // 實際上是調用window.f1()而this永遠指向調用者即window
console.log(f2()); // undefined // 實際上是調用 window.f2() 此時由於嚴格模式use strict所以在函數內部this為undefined

隱式綁定

對象屬性引用鏈中只有最頂層或者說最后一層會影響this,同樣也是this永遠指向調用者,具體點說應該是指向最近的調用者,當然箭頭函數除外,另外我們可能有意無意地創建間接引用地情況,這個情況下同樣也適用於this指向調用者,在上文分析那部分使用的示例就屬於間接引用的情況。

function f() {
    console.log(this.a);
}
var obj1 = {
    a: 1,
    f: f
};
var obj2 = {
    a: 11,
    obj1: obj1
};
obj2.obj1.f(); // 1 // 最后一層調用者即obj1
function f() {
    console.log(this.a);
}
var obj1 = {
    a: 1,
    f: f
};
var obj2 = {
    a: 11,
};
obj2.f = obj1.f; // 間接引用
obj2.f(); // 11 // 調用者即為obj2

顯示綁定

如果我們想把某個函數強制在某個環境即對象上,那么就可以使用applycallbind強制綁定this去執行即可,每個Function對象都存在apply()call()bind()方法,其作用都是可以在特定的作用域中調用函數,等於設置函數體內this對象的值,以擴充函數賴以運行的作用域,此外需要注意使用bind綁定this的優先級是大於applycall的,即使用bind綁定this后的函數使用applycall是無法改變this指向的。

window.name = "A"; // 掛載到window對象的name
document.name = "B"; // 掛載到document對象的name
var s = { // 自定義一個對象s
    name: "C"
}

var rollCall = {
    name: "Teacher",
    sayName: function(){
        console.log(this.name);
    }
}
rollCall.sayName(); // Teacher

// apply
rollCall.sayName.apply(); // A // 不傳參默認綁定window
rollCall.sayName.apply(window); // A // 綁定window對象
rollCall.sayName.apply(document); // B // 綁定document對象
rollCall.sayName.apply(s); // C // 綁定自定義對象

// call
rollCall.sayName.call(); // A // 不傳參默認綁定window
rollCall.sayName.call(window); // A // 綁定window對象
rollCall.sayName.call(document); // B // 綁定document對象
rollCall.sayName.call(s); // C // 綁定自定義對象

// bind // 最后一個()是為讓其執行
rollCall.sayName.bind()(); //A // 不傳參默認綁定window
rollCall.sayName.bind(window)(); //A // 綁定window對象
rollCall.sayName.bind(document)(); //B // 綁定document對象
rollCall.sayName.bind(s)(); // C // 綁定自定義對象

new綁定

JavaScriptnew是一個語法糖,可以簡化代碼的編寫,可以批量創建對象實例,在new的過程實際上進行了以下操作。

  1. 創建一個空的簡單JavaScript對象即{}
  2. 鏈接該對象(即設置該對象的構造函數)到另一個對象。
  3. 將步驟1新創建的對象作為this的上下文context
  4. 如果該函數沒有返回對象,則返回步驟1創建的對象。
function _new(base,...args){
    var obj = {};
    obj.__proto__ = base.prototype;
    base.apply(obj, args);
    return obj;
}

function Funct(a) {
    this.a = a;
}
var f1 = new Funct(1);
console.log(f1.a); // 1

var f2 = _new(Funct, 1);
console.log(f2.a); // 1

箭頭函數

箭頭函數沒有單獨的this,在箭頭函數的函數體中使用this時,會取得其上下文context環境中的this。箭頭函數調用時並不會生成自身作用域下的this,它只會從自己的作用域鏈的上一層繼承this。由於箭頭函數沒有自己的this指針,使用applycallbind僅能傳遞參數而不能動態改變箭頭函數的this指向,另外箭頭函數不能用作構造器,使用new實例化時會拋出異常。

window.name = 1;
var obj = {
    name: 11,
    say: function(){
        const f1 = () => {
            return this.name;
        }
        console.log(f1()); // 11 // 直接調用者為window 但是由於箭頭函數不綁定this所以取得context中的this即obj對象
        const f2 = function(){
            return this.name;
        }
        console.log(f2()); // 1 // 直接調用者為window 普通函數所以
        return this.name;
    }
}

console.log(obj.say()); // 11 // 直接調用者為obj 執行過程中的函數內context的this為obj對象

示例

function s(){
    console.log(this);
}

// window中直接調用 // 非 use strict
s(); // Window // 等同於window.s(),調用者為window
// window是Window的一個實例 // window instanceof Window //true

// 新建對象s1
var s1 = {
    t1: function(){ // 測試this指向調用者
        console.log(this); // s1
        s(); // Window // 此次調用仍然相當 window.s(),調用者為window
    },
    t2: () => { // 測試箭頭函數,this並未指向調用者
        console.log(this);
    },
    t3: { // 測試對象中的對象
      tt1: function() {
           console.log(this);
      }  
    },
    t4: { // 測試箭頭函數以及非函數調用this並未指向調用者
      tt1: () => {
           console.log(this);
      }  
    },
    t5: function(){ // 測試函數調用時箭頭函數的this的指向,其指向了上一層對象的調用者
        return {
            tt1: () => {
                console.log(this);
            }
        }
    }
}
s1.t1(); // s1對象 // 此處的調用者為 s1 所以打印對象為 s1
s1.t2(); // Window
s1.t3.tt1(); // s1.t3對象
s1.t4.tt1(); // Window
s1.t5().tt1(); // s1對象

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://juejin.cn/post/6882527259584888845
https://www.cnblogs.com/raind/p/10767622.html
http://www.ruanyifeng.com/blog/2018/06/javascript-this.html


免責聲明!

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



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