【機制】js中的this指向

木子草明發表於2021-03-02

1.this的使用場景

我們先把this的使用場景分為兩大類:函式外和函式內:
函式外的this
就是在全域性程式碼裡,直接使用this:

"use strict";

let name = "window";
console.log(this);
console.log(this.name);

// Window
// 'window'

從結果看,在函式外的this指向很簡單,就直接指向的是全域性變數Window物件,(瀏覽器環境,以下程式碼都是在瀏覽器環境)
而且嚴格模式或非嚴格模式都是。

函式內部的this
而在函式內部使用的時候,就可以分為以下幾類:

  1. 函式獨立呼叫
  2. 函式作為物件的方法被呼叫
  3. 函式作為建構函式呼叫
  4. 函式通過call,apply,bind呼叫

this指向確定的時間
在分析不同情況this的指向之前,我們確認一個很重要的問題,就是this的指向是什麼時間確定的。
在說這個問題之前,需要簡單說一下執行上下文,如果有看過: js的閉包、執行上下文、作用域鏈 這篇文章,我們就會知道執行上下文包含三個部分:

  • 變數物件
  • 作用域鏈
  • this指向

我們發現this其實執行上下文的一部分,當程式碼需要用到this的時候,也是從這裡取的。所以執行上下文建立的時間,就是this確定的時間。
執行上下文的建立時間是:函式被呼叫,但是還沒執行具體程式碼之前。
所以this的指向確定的時間也就明確了:當函式被呼叫的時候,才開始確定this指向。

2.分場景分析this指向

在瞭解了this被確定的時間後,我們現在來按上面所列出的場景,來具體分析在函式裡面的this:

2.1 函式獨立呼叫時
函式對立呼叫,其實就是我們最常見的以函式名直接呼叫函式:

// code-01-非嚴格模式
var name = "window";
function fun() {
  var name = "function";
  console.log(this);
  console.log(this.name);
}
fun()
// >> Window
// >> 'window'

我們看到,當這樣呼叫函式時,this指向的是全域性物件Window,所以this.name就相當於Window.name:'window',而不是函式的內部變數name='function'
這裡有一點需要說明的是,這是在非嚴格模式下,那如果是在嚴格模式下呢?我們看下面的例子:

// code-01-嚴格模式

"use strict"
var name = "window";
function fun() {
  var name = "function";
  console.log(this);
  console.log(this.name);
}
fun()
// >> undefined
// >> 報錯

從結果來看,在嚴格模式下,獨立呼叫函式時,函式內部的this指向是 undefined
其實應該這麼說:不管是嚴格模式還是非嚴格模式,獨立呼叫函式時,函式內部的this指向都是 undefined,只不過在非嚴格模式下,js會自動把undefined的this預設指向全域性物件:Window

2.2 函式作為物件的方法呼叫
函式作為一個物件的方法呼叫,我們舉例來看:

//code-02 作為物件成員方法呼叫函式
    var name = "window";
    var obj = {
      name: "obj",
      fun: function () {
        console.log(this.name);
      },
      child: {
        name: "child",
        fun: function () {
          console.log(this.name);
        },
      },
    };
    // 作為成員方法呼叫
    obj.fun();
    // 'obj'

    // 多級呼叫
    obj.child.fun();
    // 'child'

    // 賦值後呼叫
    let fun = obj.fun;
    fun();
    // 'window'

我們下面來分析下上面的程式碼結果:

  • obj.fun()
    首先我們從列印的結果來看,這裡的this等於obj物件。
    所以當函式作為某個物件的方法來呼叫的時候,this指向這個方法所屬的物件。

  • obj.child.fun();
    從列印的結果來看,這裡this等於obj.child物件。
    所以不管是多少級的呼叫,this指向最近的所屬物件。

  • var fun = obj.fun; fun();
    從列印的結果來看,這裡this等於全域性物件window。window.name = 'window'
    從程式碼看,這裡先做了一個賦值操作,把函式obj.fun賦值給了變數fun, 上面我們有說到this的確定時間是在函式被呼叫的時候,這時候函式並沒有被呼叫,只是做了賦值操作,所以這一步的時候,this並沒有確定。
    當執行到fun()的時候,函式被呼叫,this在這個時候要確定指向,這時候就相當於是作為獨立函式呼叫,應該指向的是undefined,但是在非嚴格模式下,undefined的this會預設指向全域性變數window。
    所以this.name == window.name == 'window'。如果是嚴格模式,this.name == undefined.name,會報錯。

2.3 函式作為建構函式呼叫
函式作為建構函式的情況,可以分為兩種:

  1. 建構函式無返回
  2. 建構函式有返回值
    a. 返回一個物件
    b. 返回其他非物件的值

下面我們分別來看:

建構函式無返回
這是建構函式最常用的情況,直接來看程式碼:

//code-03 函式作為建構函式(無返回)

let _this;
function User(name, age) {
  this.name = name;
  this.age = age;
  _this = this;
  console.log(this);
  // {name:"xiaoming",age:27}
}

let xiaoming = new User("xiaoming", 27);
console.log(_this === xiaoming);

// true

從結果來看,我們知道當函式作為建構函式的時候,該函式裡面的this等於這個建構函式new的例項物件,就是這裡的物件xiaoming。從【機制】JavaScript的原型、原型鏈、繼承這篇可以知道操作符new實際上做了什麼事情。

建構函式有返回
如果返回的是非物件,則返回值會被忽略,情況等同於無返回。
下面就只討論返回值為一個物件的情況:

//code-03 函式作為建構函式(返回物件)

let _this;
function User(name, age) {
  this.name = name;
  this.age = age;

  _this = this;
  console.log(this);
// {name:'xiaoming',age:27}

  let obj = {
    name: "obj",
  };
  return obj;
}
let xiaoming = new User("xiaoming", 27);

console.log(xiaoming);
// {name:'obj'}

console.log(_this === xiaoming);
// false

從結果來看,當建構函式返回一個物件時,它new出來的例項就等於它返回的物件(xiaoming === obj),而建構函式的內部this並沒有起到任何作用。

2.4 函式通過call,apply,bind呼叫
call,apply,bind都是可以指定this的值。

// code-04 指定this

function fun(name, age) {
  console.log(name, age, this);
}
let obj = {
  name: "obj",
};

fun.call(obj, "obj", 27);
fun.apply(obj, ["obj", 27]);
let funBind = fun.bind(obj, "obj", 27);
funBind();

// 結果返回都一樣
// 'obj' 27 {name:obj}

call,apply,bind:
相同點:都可以指定函式內部的this值,引數的第一個即為this的值。
不同點:

  • call:fun引數(name,age),由call函式的第2,3..引數依次賦值。
  • apply:fun引數(name,age),由apply函式的第2個引數賦值,第二個引數是一個陣列,所存的值依次賦值給fun引數。
  • bind:fun引數(name,age)賦值方式同call,但bind返回的是一個函式,而不是直接執行fun。

3.幾種特殊情況

在說明了上面常用情景後,我們來分析幾種特殊的情況:

陣列成員
當函式作為陣列的成員時:

// code-05 函式作為陣列成員

function arrFun() {
  console.log(this.length);
  console.log(this === arr);
}
let arr = [1, 2, arrFun];
arr[2]();

// 3
// true

從結果看,我們知道當函式作為陣列的成員的時候,此函式內部的this指向的是當前陣列。
可以這樣理解:arr[2] == arr["2"], 類似於物件的成員方法。

事件繫結
函式作為繫結事件時:

// code-06 事件繫結

<button id="btn">點選</button>

document.getElementById("btn").addEventListener("click", function () {
  console.log(this);
});

// <button id="btn">點選</button>

從結果看,我們知道當函式作為事件被繫結時,此函式內部的this指向的是繫結了該事件的dom元素。

非同步函式:promise,setTimeout
非同步執行函式的時候分為promise和setTimeout情況(關於非同步機制可以參看 【機制】 JavaScript的事件迴圈機制總結 eventLoop):

// code-07 非同步函式

"use strict";

setTimeout(function () {
  console.log("setTimeout:", this);
});

new Promise(function (resolve) {
  console.log("start");
  resolve();
}).then(function () {
  console.log("promise:", this);
});

// start
// promise: undefined
// setTimeout: Window

從結果來看,我們知道其實 setTimeout執行的函式下的this,相當於是在全域性環境下的this:執行全域性變數 Window物件,嚴格模式和非嚴格模式都一樣。
promise下執行的函式其實相當於函式獨立執行的情況:嚴格模式this等於undefined,非嚴格模式下會預設把undefined的this指向Window。

箭頭函式
其實箭頭函式本身沒有this,它裡面的this指向的是外部作用域中的this:

// code-08 箭頭函式

"use strict";

let Obj = {
  name: "obj",
  fun_1: () => {
    console.log(this);
  },
  fun_2() {
    let fun = () => {
      console.log(this);
    };
    fun();
  },
};
Obj.fun_1();
// Window

Obj.fun_2();
// Obj

function foo() {
  setTimeout(() => {
    console.log(this);
  });
}

foo.call({ id: 42 });
// {id:42}

Obj.fun_1()
fun_1是箭頭函式,本身沒有this。它的外層作用域就是全域性作用域,所以箭頭函式的this指向的是全域性作用域下的this:Window

Obj.fun_2()
fun_2函式內部的fun是箭頭函式,本身沒有this。它的外層作用域就是fun_2,而fun_2的this是呼叫它的物件Obj,所以箭頭函式的this指向的也是Obj。

foo.call({ id: 42 })
foo函式用call呼叫,於是foo的this為{id:42}。本來setTimeout內部的函式this指向的是Widow,但是因為它是箭頭本身沒有this,箭頭函式的this指向的是外部作用域的this,在這裡就是foo的this:{id:42}。

-- 完 --

相關文章