1. 程式人生 > >深入理解Js中的this

深入理解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`則不同,其函式作用域中遇到既不是引數也不是函式內部定義的區域性變數時,到函式呼叫時的上下文中去查。 ```javascript var a = 1; var s = function(){ console.log(a); }; (function(){ var a = 2; s(); // 1 })(); ``` 呼叫`s()`是列印的`a`為`1`,此為靜態作用域,也就是宣告時即規定作用域,而假如是動態作用域的話在此處會列印`2`。現在大部分語言都採用靜態作用域,比如`C`、`C++`、`Java`、`PHP`、`Python`等等,具有動態作用域的語言有`Emacs Lisp`、`Common Lisp`、`Perl`等。 ### 全域性作用域 直接宣告在頂層的變數或方法就執行在全域性作用域,借用函式的`[[Scopes]]`屬性來檢視作用域,`[[Scopes]]`是儲存函式作用域鏈的物件,是函式的內部屬性無法直接訪問但是可以列印來檢視。 ```javascript function s(){} console.dir(s); /* ... [[Scopes]]: Scopes[1] 0: Global ... */ // 可以看見宣告的s函式執行的上下文環境是全域性作用域 ``` ### 函式作用域 當宣告一個函式後,在函式內部宣告的方法或者成員的執行環境就是此函式的函式作用域 ```javascript (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`,程式碼塊會對這些命令宣告的變數從塊的開始就形成一個封閉作用域。 ```javascript { 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`時可能會遇到的典型問題就類似於下面這樣,雖然我們執行的都是同一個函式,但是執行的結果可能會不同。 ```javascript 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`中基本資料型別`String`、`Number`、`Boolean`、`Null`、`Undefined`、`Symbol`,佔用空間小且大小固定,值直接儲存在棧記憶體中,是按值訪問,對於`Object`引用型別,其指標放置於棧記憶體中,指向堆記憶體的實際地址,是通過引用訪問。 那麼此時我們來看一下上邊的示例,在記憶體中對於`obj`物件是存放在堆記憶體的,如果在物件中的屬性值是個基本資料型別,那麼其會跟這個物件儲存在同一塊記憶體區域,但是這個屬性值同樣可能是一個引用型別,那麼對於`say`這個函式也是存在於堆記憶體中的,實際上在此處我們可以將其理解為這個函式的實際定義在一個記憶體區域(以一個匿名函式的形式存在),而`obj`這個物件同樣在其他的一個記憶體區域,`obj`通過`say`這個屬性指向了這個匿名函式的記憶體地址,`obj --say--> funtion`,那麼此時問題來了,由於這種記憶體結構,我們可以使任何變數物件等指向這個函式,所以在`JavaScript`的函式中是需要允許我們取得執行環境的值以供使用的,我們必須要有一種機制,能夠在函式體內部獲得當前的執行環境`context`,所以`this`就出現了,它的設計目的就是在函式體內部,指代函式當前的執行環境。 ## 使用 我們需要記住,`this`是在執行時進行繫結的,並不是在定義時繫結,它的`context`取決於函式呼叫時的各種條件,簡單來說`this`的繫結和函式宣告的位置沒有任何關係,只取決於函式的呼叫方式,再簡單來說`this`永遠指向呼叫者,但箭頭函式除外,接下來我們介紹一下五種`this`的使用情況。 ### 預設繫結 最常用的函式呼叫型別即獨立函式呼叫,這個也是優先順序最低的一個,此時`this`指向全域性物件,注意如果使用嚴格模式`strict mode`,那麼全域性物件將無法使用預設繫結,因此`this`會變為`undefined`。 ```javascript 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`指向呼叫者,在上文分析那部分使用的示例就屬於間接引用的情況。 ```javascript function f() { console.log(this.a); } var obj1 = { a: 1, f: f }; var obj2 = { a: 11, obj1: obj1 }; obj2.obj1.f(); // 1 // 最後一層呼叫者即obj1 ``` ```javascript function f() { console.log(this.a); } var obj1 = { a: 1, f: f }; var obj2 = { a: 11, }; obj2.f = obj1.f; // 間接引用 obj2.f(); // 11 // 呼叫者即為obj2 ``` ### 顯示繫結 如果我們想把某個函式強制在某個環境即物件上,那麼就可以使用`apply`、`call`、`bind`強制繫結`this`去執行即可,每個`Function`物件都存在`apply()`、`call()`、`bind()`方法,其作用都是可以在特定的作用域中呼叫函式,等於設定函式體內`this`物件的值,以擴充函式賴以執行的作用域,此外需要注意使用`bind`繫結`this`的優先順序是大於`apply`和`call`的,即使用`bind`繫結`this`後的函式使用`apply`和`call`是無法改變`this`指向的。 ```javascript 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繫結 在`JavaScript`中`new`是一個語法糖,可以簡化程式碼的編寫,可以批量建立物件例項,在`new`的過程實際上進行了以下操作。 1. 建立一個空的簡單`JavaScript`物件即`{}`。 2. 連結該物件(即設定該物件的建構函式)到另一個物件。 3. 將步驟`1`新建立的物件作為`this`的上下文`context`。 4. 如果該函式沒有返回物件,則返回步驟`1`建立的物件。 ```javascript 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`指標,使用`apply`、`call`、`bind`僅能傳遞引數而不能動態改變箭頭函式的`this`指向,另外箭頭函式不能用作構造器,使用`new`例項化時會丟擲異常。 ```javascript 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物件 ``` ## 示例 ```javascript 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.