深入理解Js中的this
JavaScript
作用域為靜態作用域static scope
,但是在Js
中的this
卻是一個例外,this
的指向問題就類似于動態作用域,其并不關心函數和作用域是如何聲明以及在何處聲明的,只關心它們從何處調用,this
的指向在函數定義的時候是確定不了的,只有函數執行的時候才能確定this
到底指向誰,當然實際上this
的最終指向的是那個調用它的對象。
作用域
我們先來了解一下JavaScript
的作用域,以便理解為什么說this
更類似于動態作用域,通常來說,一段程序代碼中所用到的名字并不總是有效或可用的,而限定這個名字的可用性的代碼范圍就是這個名字的作用域scope
,當一個方法或成員被聲明,他就擁有當前的執行上下文context
環境,在有具體值的context
中,表達式是可見也都能夠被引用,如果一個變量或者其他表達式不在當前的作用域,則將無法使用。作用域也可以根據代碼層次分層,以便子作用域可以訪問父作用域,通常是指沿著鏈式的作用域鏈查找,而不能從父作用域引用子作用域中的變量和引用。
JavaScript
作用域為靜態作用域static scope
,也可以稱為詞法作用域lexical scope
,其主要特征在于,函數作用域中遇到既不是參數也不是函數內部定義的局部變量時,去函數定義時上下文中查,而與之相對應的是動態作用域dynamic scope
則不同,其函數作用域中遇到既不是參數也不是函數內部定義的局部變量時,到函數調用時的上下文中去查。
1
2
3
4
5
6
7
8
|
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]]
是保存函數作用域鏈的對象,是函數的內部屬性無法直接訪問但是可以打印來查看。
1
2
3
4
5
6
7
8
|
function s(){} console.dir(s); /* ... [[Scopes]]: Scopes[1] 0: Global ... */ // 可以看見聲明的s函數運行的上下文環境是全局作用域 |
函數作用域
當聲明一個函數后,在函數內部聲明的方法或者成員的運行環境就是此函數的函數作用域
1
2
3
4
5
6
7
8
9
10
11
12
|
( 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
,代碼塊會對這些命令聲明的變量從塊的開始就形成一個封閉作用域。
1
2
3
4
5
6
7
8
9
10
11
12
|
{ 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
時可能會遇到的典型問題就類似于下面這樣,雖然我們運行的都是同一個函數,但是執行的結果可能會不同。
1
2
3
4
5
6
7
8
9
10
11
|
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
。
1
2
3
4
5
6
7
8
9
10
11
12
|
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
指向調用者,在上文分析那部分使用的示例就屬于間接引用的情況。
1
2
3
4
5
6
7
8
9
10
11
12
|
function f() { console.log( this .a); } var obj1 = { a: 1, f: f }; var obj2 = { a: 11, obj1: obj1 }; obj2.obj1.f(); // 1 // 最后一層調用者即obj1 |
1
2
3
4
5
6
7
8
9
10
11
12
|
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
指向的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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
的過程實際上進行了以下操作。
創建一個空的簡單JavaScript
對象即{}
。鏈接該對象(即設置該對象的構造函數)到另一個對象。將步驟1
新創建的對象作為this
的上下文context
。如果該函數沒有返回對象,則返回步驟1
創建的對象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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
實例化時會拋出異常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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對象 |
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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對象 |
到此這篇關于深入理解Js中的this的文章就介紹到這了,更多相關深入理解Js中的this內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://www.cnblogs.com/WindrunnerMax/p/14379232.html