1. 程式人生 > >JavaScript中this初識

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