JavaScript中this初識
本文就是綜合網上的文章對this有個初步的認識。
推薦文章,另一個角度理解this:https://www.imooc.com/article/1758#comment
上面文章總結:通過我這篇文章,我希望學會通過把一個函式呼叫替換成funcName.call的形式,從而理解執行時上下文中this到底指向誰。總結來說就是下面兩個等價變形:
- foo() —> foo.call(window)
- obj.foo() --> obj.foo.call(obj)
1.前言
Javascript 是一個文字作用域的語言, 就是說, 一個變數的作用域, 在寫這個變數的時候確定. this 關鍵字是為了在 JS 中加入動態作用域而做的努力. 所謂動態作用域, 就是說變數的作用範圍, 是根據函式呼叫的位置而定的.從這個角度來理解 this, 就簡單的多.
this 是 JS 中的動態作用域機制, 具體來說有四種, 優先順序有低到高分別如下:
1.預設的 this 繫結, 就是說 在一個函式中使用了 this, 但是沒有為 this 繫結物件. 這種情況下, 非嚴格預設, this 就是全域性變數 Node 環境中的 global, 瀏覽器環境中的 window.
2. 隱式繫結: 使用 obj.foo() 這樣的語法來呼叫函式的時候, 函式 foo 中的 this 繫結到 obj 物件.
3. 顯示繫結: foo.call(obj, …), foo.apply(obj,[…]), foo.bind(obj,…)
4. 構造繫結: new foo() , 這種情況, 無論 foo 是否做了繫結, 都要建立一個新的物件, 然後 foo 中的 this 引用這個物件.
一句話:this 總是指向呼叫它所在的函式的那個物件。
「this 是在函式被呼叫時發生的繫結,它指向什麼完全取決於函式在哪裡被呼叫。」
當一個函式被呼叫時,會建立一個「執行上下文」,這個上下文會包含函式在哪裡被呼叫(呼叫棧)、函式的呼叫方式、傳入的引數等資訊。this 就是這個記錄的一個屬性,會在函式執行的過程中用到。
2.從JavaScript記憶體結構理解this
參考文章:http://www.ruanyifeng.com/blog/2018/06/javascript-this.html
本文主要講述了為什麼要有this:
引擎會將函式單獨儲存在記憶體中,然後再將函式的地址賦值給物件的屬性,由於函式可以在不同的執行環境執行,所以需要有一種機制,能夠在函式體內部獲得當前的執行環境(context)。所以,this就出現了,它的設計目的就是在函式體內部,指代函式當前的執行環境。
一、問題來源:
var obj = {
foo: function () { console.log(this.bar) },
bar: 1
};
var foo = obj.foo;
var bar = 2;
obj.foo() // 1
foo() // 2
雖然obj.foo和foo指向同一個函式,但是執行結果可能不一樣。
這種差異的原因,就在於函式體內部使用了this關鍵字。this指的是函式執行時所在的環境。對於obj.foo()來說,foo執行在obj環境,所以this指向obj;對於foo()來說,foo執行在全域性環境,所以this指向全域性環境。所以,兩者的執行結果不一樣。
為什麼會這樣?也就是說,函式的執行環境到底是怎麼決定的?舉例來說,為什麼obj.foo()就是在obj環境執行,而一旦var foo = obj.foo,foo()就變成在全域性環境執行?
二、記憶體的資料結構
var obj = { foo: 5 };
上面的程式碼將一個物件賦值給變數obj。JavaScript 引擎會先在記憶體裡面,生成一個物件{ foo: 5 },然後把這個物件的記憶體地址賦值給變數obj。
也就是說,變數obj是一個地址(reference)。後面如果要讀取obj.foo,引擎先從obj拿到記憶體地址,然後再從該地址讀出原始的物件,返回它的foo屬性。
原始的物件以字典結構儲存,每一個屬性名都對應一個屬性描述物件。舉例來說,上面例子的foo屬性,實際上是以下面的形式儲存的。
{
foo: {
[[value]]: 5
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
注意,foo屬性的值儲存在屬性描述物件的value屬性裡面。
三、函式
這樣的結構是很清晰的,問題在於屬性的值可能是一個函式。
var obj = { foo: function () {} };
這時,引擎會將函式單獨儲存在記憶體中,然後再將函式的地址賦值給foo屬性的value屬性。
{
foo: {
[[value]]: 函式的地址
...
}
}
由於函式是一個單獨的值,所以它可以在不同的環境(上下文)執行。
四、環境變數
現在問題就來了,由於函式可以在不同的執行環境執行,所以需要有一種機制,能夠在函式體內部獲得當前的執行環境(context)。所以,this就出現了,它的設計目的就是在函式體內部,指代函式當前的執行環境。
var f = function () {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
};
// 單獨執行
f() // 1
// obj 環境執行
obj.f() // 2
上面程式碼中,函式f在全域性環境執行,this.x指向全域性環境的x。
在obj環境執行,this.x指向obj.x。
回到本文開頭提出的問題,obj.foo()是通過obj找到foo,所以就是在obj環境執行。一旦var foo = obj.foo,變數foo就直接指向函式本身,所以foo()就變成在全域性環境執行。
總結:由於函式是一個單獨的值,所以它可以在不同的環境(上下文)執行。JavaScript 允許在函式體內部,引用當前環境的其他變數。而為了獲得當前的執行環境,所以this就出現了,因此函式裡面的this函式當前的執行環境。開頭提出的問題,obj.foo()是通過obj找到foo(這時,引擎會將函式單獨儲存在記憶體中,然後再將函式的地址賦值給foo屬性的value屬性),所以就是在obj環境執行。一旦var foo = obj.foo,變數foo就直接指向函式本身,所以foo()就變成在全域性環境執行。
3.不同調用方式this指向詳解
參考文章:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this
3.1全域性上下文
無論是否在嚴格模式下,在全域性執行上下文中(在任何函式體外部)this 都指代全域性物件。
// 在瀏覽器中, window 物件同時也是全域性物件:
console.log(this === window); // true
a = 37;
console.log(window.a); // 37
this.b = "MDN";
console.log(window.b) // "MDN"
console.log(b) // "MDN"
3.2函式上下文
- 簡單呼叫
- bind方法
- 箭頭函式
- 作為物件的方法
- 作為建構函式
- 作為一個DOM事件處理函式
- 作為一個內聯事件處理函式
在函式內部,this的值取決於函式被呼叫的方式。
3.2.1簡單呼叫
預設的 this 繫結, 就是說 在一個函式中使用了 this, 但是沒有為 this 繫結物件. 這種情況下, 非嚴格預設, this 就是全域性變數 Node 環境中的 global, 瀏覽器環境中的 window.
function f1(){
return this;
}
//在瀏覽器中:
f1() === window; //在瀏覽器中,全域性物件是window
//在Node中:
f1() === global;
在嚴格模式下,如果 this 沒有被執行上下文(execution context)定義,那它將保持為 undefined。
function f2(){
"use strict"; // 這裡是嚴格模式
return this;
}
f2() === undefined; // true
如果要想把 this 的值從一個上下文傳到另一個,就要用 call 或者apply 方法。
// 將一個物件作為call和apply的第一個引數,this會被繫結到這個物件。
var obj = {a: 'Custom'};
// 這個屬性是在global物件定義的。
var a = 'Global';
function whatsThis(arg) {
return this.a; // this的值取決於函式的呼叫方式
}
whatsThis(); // 'Global'
whatsThis.call(obj); // 'Custom'
whatsThis.apply(obj); // 'Custom'
當一個函式在其主體中使用 this 關鍵字時,可以通過使用函式繼承自Function.prototype 的 call 或 apply 方法將 this 值繫結到呼叫中的特定物件。
function add(c, d) {
return this.a + this.b + c + d;
}
var o = {a: 1, b: 3};
// 第一個引數是作為‘this’使用的物件
// 後續引數作為引數傳遞給函式呼叫
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
// 第一個引數也是作為‘this’使用的物件
// 第二個引數是一個數組,數組裡的元素用作函式呼叫中的引數
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34
3.2.2bind方法
ECMAScript 5 引入了 Function.prototype.bind。呼叫f.bind(someObject)會建立一個與f具有相同函式體和作用域的函式,但是在這個新函式中,this將永久地被繫結到了bind的第一個引數,無論這個函式是如何被呼叫的。
function f(){
return this.a;
}
var g = f.bind({a:"azerty"});
console.log(g()); // azerty
var h = g.bind({a:'yoo'}); // bind只生效一次!
console.log(h()); // azerty
var o = {a:37, f:f, g:g, h:h};
console.log(o.f(), o.g(), o.h()); // 37, azerty, azerty
3.2.3箭頭函式
3.2.4作為物件的方法
當函式作為物件裡的方法被呼叫時,它們的 this 是呼叫該函式的物件。
下面的例子中,當 o.f()被呼叫時,函式內的this將繫結到o物件。
var o = {
prop: 37,
f: function() {
return this.prop;
}
};
console.log(o.f()); // logs 37
請注意,這樣的行為,根本不受函式定義方式或位置的影響。在前面的例子中,我們在定義物件o的同時,將函式內聯定義為成員 f 。但是,我們也可以先定義函式,然後再將其附屬到o.f。這樣做會導致相同的行為:
var o = {prop: 37};
function independent() {
return this.prop;
}
o.f = independent;
console.log(o.f()); // logs 37
這表明函式是從o的f成員呼叫的才是重點。
3.2.4.1原型鏈中的 this
對於在物件原型鏈上某處定義的方法,同樣的概念也適用。如果該方法存在於一個物件的原型鏈上,那麼this指向的是呼叫這個方法的物件,就像該方法在物件上一樣。
var o = {
f: function() {
return this.a + this.b;
}
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f()); // 5
在這個例子中,物件p沒有屬於它自己的f屬性,它的f屬性繼承自它的原型。雖然在對 f 的查詢過程中,最終是在 o 中找到 f 屬性的,這並沒有關係;查詢過程首先從 p.f 的引用開始,所以函式中的 this 指向p。也就是說,因為f是作為p的方法呼叫的,所以它的this指向了p。這是 JavaScript 的原型繼承中的一個有趣的特性。
3.2.4.2getter 與 setter 中的 this
再次,相同的概念也適用於當函式在一個 getter 或者 setter 中被呼叫。用作 getter 或 setter 的函式都會把 this 繫結到設定或獲取屬性的物件。
function sum() {
return this.a + this.b + this.c;
}
var o = {
a: 1,
b: 2,
c: 3,
get average() {
return (this.a + this.b + this.c) / 3;
}
};
Object.defineProperty(o, 'sum', {
get: sum, enumerable: true, configurable: true});
console.log(o.average, o.sum); // logs 2, 6
3.2.5作為建構函式
當一個函式用作建構函式時(使用new關鍵字),它的this被繫結到正在構造的新物件。
雖然構造器返回的預設值是this所指的那個物件,但它仍可以手動返回其他的物件(如果返回值不是一個物件,則返回this物件)。
function C(){
this.a = 37;
}
var o = new C();
console.log(o.a); // logs 37
function C2(){
this.a = 37;
return {a:38};
}
o = new C2();
console.log(o.a); // logs 38
3.2.6作為一個DOM事件處理函式
當函式被用作事件處理函式時,它的this指向觸發事件的元素(一些瀏覽器在使用非addEventListener的函式動態新增監聽函式時不遵守這個約定)
// 被呼叫時,將關聯的元素變成藍色
function bluify(e){
console.log(this === e.currentTarget); // 總是 true
// 當 currentTarget 和 target 是同一個物件時為 true
console.log(this === e.target);
this.style.backgroundColor = '#A5D9F3';
}
// 獲取文件中的所有元素的列表
var elements = document.getElementsByTagName('*');
// 將bluify作為元素的點選監聽函式,當元素被點選時,就會變成藍色
for(var i=0 ; i<elements.length ; i++){
elements[i].addEventListener('click', bluify, false);
}
3.2.7作為一個內聯事件處理函式
當代碼被內聯on-event 處理函式呼叫時,它的this指向監聽器所在的DOM元素:
<button onclick="alert(this.tagName.toLowerCase());">
Show this
</button>
上面的 alert 會顯示button。注意只有外層程式碼中的this是這樣設定的:
<button onclick="alert((function(){return this})());">
Show inner this
</button>
在這種情況下,沒有設定內部函式的this,所以它指向 global/window 物件(即非嚴格模式下呼叫的函式未設定this時指向的預設物件)。
4.this的四種繫結規則
首先有一句大家都明白的話,我還是要強調一遍:
「this 是在函式被呼叫時發生的繫結,它指向什麼完全取決於函式在哪裡被呼叫。」
這句話很重要,這是理解 this 原理的基礎。
而在講解 this 之前,先要理解一下作用域的相關概念。
4.1詞法作用域
JavaScript 就是採用的詞法作用域,也就是在程式設計階段,作用域就已經明確下來了。
思考下面程式碼:
function foo(){
console.log(a); // 輸出 2
}
function bar(){
let a = 3;
foo();
}
let a = 2;
bar()複製程式碼因為 JavaScript 所用的是詞法作用域,自然 foo() 宣告的階段,就已經確定了變數 a 的作用域了。
4.2this的四種繫結規則
參考文章:https://juejin.im/post/596a28f6f265da6c360a2716
在 JavaScript 中,影響 this 指向的繫結規則有四種:
- 預設繫結
- 隱式繫結
- 顯式繫結
- new 繫結
預設繫結:
這是最直接的一種方式,就是不加任何的修飾符直接呼叫函式,如:
function foo() {
console.log(this.a) // 輸出 a
}
var a = 2; // 變數宣告到全域性物件中
foo();
使用 var 宣告的變數 a,被繫結到全域性物件中,如果是瀏覽器,則是在 window 物件。foo() 呼叫時,引用了預設繫結,this 指向了全域性物件。
隱式繫結:
這種情況會發生在呼叫位置存在「上下文物件」的情況,如:
function foo() {
console.log(this.a);
}
let obj1 = {
a: 1,
foo,
};
let obj2 = {
a: 2,
foo,
}
obj1.foo(); // 輸出 1
obj2.foo(); // 輸出 2
當函式呼叫的時候,擁有上下文物件的時候,this 會被繫結到該上下文物件。正如上面的程式碼,obj1.foo() 被呼叫時,this 繫結到了 obj1,而 obj2.foo() 被呼叫時,this 繫結到了 obj2。
顯式繫結:
這種就是使用 Function.prototype 中的三個方法 call(), apply(), bind() 了。這三個函式,都可以改變函式的 this 指向到指定的物件,不同之處在於,call() 和 apply() 是立即執行函式,並且接受的引數的形式不同:
- call(this, arg1, arg2, …)
- apply(this, [arg1, arg2, …])
而 bind() 則是建立一個新的包裝函式,並且返回,而不是立刻執行。
- bind(this, arg1, arg2, …)
apply() 接收引數的形式,有助於函式巢狀函式的時候,把 arguments 變數傳遞到下一層函式中。
思考下面程式碼:
function foo() {
console.log(this.a); // 輸出 1
bar.apply({a: 2}, arguments);
}
function bar(b) {
console.log(this.a + b); // 輸出 5
}
var a = 1;
foo(3);
上面程式碼中, foo() 內部的 this 遵循預設繫結規則,繫結到全域性變數中。而 bar() 在呼叫的時候,呼叫了 apply() 函式,把 this 繫結到了一個新的物件中 {a: 2},而且原封不動的接收 foo() 接收的函式。
new 繫結:
最後一種,則是使用 new 操作符會產生 this 的繫結。在理解 new 操作符對 this 的影響,首先要理解 new 的原理。在 JavaScript 中,new 操作符並不像其他面向物件的語言一樣,而是一種模擬出來的機制。在 JavaScript 中,所有的函式都可以被 new 呼叫,這時候這個函式一般會被稱為「建構函式」,實際上並不存在所謂「建構函式」,更確切的理解應該是對於函式的「構造呼叫」。
使用 new 來呼叫函式,會自動執行下面操作:
- 建立一個全新的物件。
- 這個新物件會被執行 [[Prototype]] 連線。
- 這個新物件會繫結到函式呼叫的 this。
- 如果函式沒有返回其他物件,那麼 new 表示式中的函式呼叫會自動返回這個新物件。
所以如果 new 是一個函式的話,會是這樣子的:
function New(Constructor, ...args){
let obj = {}; // 建立一個新物件
Object.setPrototypeOf(obj, Constructor.prototype); // 連線新物件與函式的原型
return Constructor.apply(obj, args) || obj; // 執行函式,改變 this 指向新的物件
}
function Foo(a){
this.a = a;
}
New(Foo, 1); // Foo { a: 1 }
所以,在使用 new 來呼叫函式時候,我們會構造一個新物件並把它繫結到函式呼叫中的 this 上。
4.3優先順序
如果一個位置發生了多條改變 this 的規則,那麼優先順序是如何的呢?
看幾段程式碼:
// 顯式繫結 > 隱式繫結
function foo() {
console.log(this.a);
}
let obj1 = {
a: 2,
foo,
}
obj1.foo(); // 輸出 2
obj1.foo.call({a: 1}); // 輸出 1
這說明「顯式繫結」的優先順序大於「隱式繫結」
// new 繫結 > 顯式繫結
function foo(a) {
this.a = a;
}
let obj1 = {};
let bar = foo.bind(obj1);
bar(2);
console.log(obj1); // 輸出 {a:2}
let obj2 = new bar(3);
console.log(obj1); // 輸出 {a:2}
console.log(obj2); // 輸出 foo { a: 3 }
這說明「new 繫結」的優先順序大於「顯式繫結」而「預設繫結」,毫無疑問是優先順序最低的。
所以優先順序順序為:「new 繫結」 > 「顯式繫結」 > 「隱式繫結」 > 「預設繫結。」
4.4 所以,this到底是什麼?
this 並不是在編寫的時候繫結的,而是在執行時繫結的。它的上下文取決於函式呼叫時的各種條件。this 的繫結和函式宣告的位置沒有任何關係,只取決於函式的呼叫方式。當一個函式被呼叫時,會建立一個「執行上下文」,這個上下文會包含函式在哪裡被呼叫(呼叫棧)、函式的呼叫方式、傳入的引數等資訊。this 就是這個記錄的一個屬性,會在函式執行的過程中用到。
還可以參考文章:
https://www.jianshu.com/p/31aec3ab1bb0
https://www.cnblogs.com/snandy/p/4773184.html
https://blog.csdn.net/buddha_itxiong/article/details/79558316