首頁 > 軟體

JavaScript中的this例題實戰總結詳析

2022-06-07 18:00:11

前言

是否能夠深刻理解 this,是前端 JavaScript 進階的重要一環。

物件導向語言中 this 表示當前物件的一個參照,但在 JavaScript 中 this 不是固定不變的,它會隨著執行環境的改變而改變。

有一種廣為流傳的說法是:“誰呼叫它,this 就指向誰”。也就是說普通函數在定義的時候無法確定 this 參照取值,因為函數沒有被呼叫,也就沒有執行的上下文環境,因此在函數中 this 的參照取值,是在函數被呼叫的時候確定的。

函數在不同的情況下被呼叫,就會產生多種不同的執行上下文環境,所以 this 的參照取值也就是隨著執 行環境的改變而改變了。

還有一句經典的話:“匿名函數的 this 永遠指向 window”。

還有通過 call、apply、bind 和 new 操作符改變的 this 它們誰的優先順序更高呢?還有它們的實現原理是怎麼樣的呢?最後我們手寫實現 call、apply、bind 和 new 操作符來進一步理解 this。

下面先根據具體環境來逐一分析。

普通函數中的 this

我們來看例題:請給出下面程式碼的執行結果。

例題1

function f1 () {
   console.log(this)
}
f1() // window

普通函數在非嚴格的全域性環境下呼叫時,其中的 this 指向的是 window。

例題2

"use strict"
function f1 () {
   console.log(this)
}
f1() // undefined

用了嚴格模式 "use strict",嚴格模式下無法再意外建立全域性變數,所以 this 不為 window 而為 undefined。

注意:babel 轉成 ES6 的,babel 會自動給 js 檔案上加上嚴格模式。

箭頭函數中的 this

在箭頭函數中,this 的指向是由外層(函數或全域性)作用域來決定的。

例題3

const Animal = {
    getName: function() {
        setTimeout(function() {
            console.log(this)
        })
    }
}
Animal.getName() // window

此時 this 指向 window。這裡也印證了那句經典的話:“匿名函數的 this 永遠指向 window”。

如果要讓 this 指向 Animal 這個物件,則可以巧用箭頭函數來解決。

例題4

const Animal = {
    getName: function() {
        setTimeout(() => {
            console.log(this)
        })
    }
}
Animal.getName() // {getName: ƒ}

嚴格模式對箭頭函數沒有效果

例題5

"use strict";
const f1 = () => {
   console.log(this)
}
f1() // window

我們都知道箭頭函數體內的 this 物件,就是定義時所在的物件,而不是使用時所在的物件。普通函數使用了嚴格模式 this 會指向 undefined 但箭頭函數依然指向了 window。

函數作為物件的方法中的 this

例題6

const obj = {
    name: "coboy", 
    age: 18, 
    add: function() {
        console.log(this, this.name, this.age)
    }
};
obj.add(); // {name: "coboy", age: 18, add: ƒ} "coboy" 18

在物件方法中,作為物件的一個方法被呼叫時,this 指向呼叫它所在方法的物件。也就是開頭我們所說的那句:“誰呼叫了它,它就指向誰”,在這裡很明顯是 obj 呼叫了它。

例題7

const obj = {
    name: "coboy", 
    age: 18, 
    add: function() {
        console.log(this, this.name, this.age)
    }
};
const fn = obj.add
fn() // window

這個時候 this 則仍然指向 window。obj 物件方法 add 賦值給 fn 之後,fn 仍然在 window 的全域性環境中執行,所以 this 仍然指向 window。

例題8

const obj = {
    name: "coboy", 
    age: 18, 
    add: function() {
        function fn() {
            console.log(this)
        }
        fn()
    }
};
obj.add() // window

如果在物件方法內部宣告一個函數,這個函數的 this 在物件方法執行的時候指向就不是這個物件了,而是指向 window 了。

同樣想要 this 指向 obj 可以通過箭頭函數來實現:

const obj = {
    name: "coboy", 
    age: 18, 
    add: function() {
        const fn = () => {
            console.log(this)
        }
        fn()
    }
};
obj.add() // obj

再次說明箭頭函數的 this 是由外層函數作用域或者全域性作用域決定的。

上下文物件呼叫中的 this

例題9

const obj = {
    name: "coboy", 
    age: 18, 
    add: function() {
        return this
    }
};
console.log(obj.add() === obj) // true

參考上文我們很容易知道 this 就是指向 obj 物件本身,所以返回 true。

例題10

const animal = {
    name: "coboy", 
    age: 18, 
    dog: {
        name: 'cobyte',
        getName: function() {
            console.log(this.name)
        }
    }
};
animal.dog.getName() // 'cobyte'

如果函數中的 this 是被上一級的物件所呼叫的,那麼 this 指向的就是上一級的物件,也就是開頭所說的:“誰呼叫了它,它就指向誰”。

例題11

const obj1 = {
    txt: 'coboy1',
    getName: function() {
        return this.txt
    }
}
const obj2 = {
    txt: 'coboy2',
    getName: function() {
        return obj1.getName()
    }
}
const obj3 = {
    txt: 'coboy3',
    getName: function() {
        return obj2.getName()
    }
}
console.log(obj1.getName()) 
console.log(obj2.getName())
console.log(obj3.getName())

三個最終都列印了coboy1。

執行 obj3.getName 裡面返回的是 obj2.getName 裡面返回的結果,obj2.getName 裡面返回的是 obj1.getName 的結果,obj1.getName 返回的結果就是 'coboy1'。

如果上面的題改一下:

例題12

const obj1 = {
    txt: 'coboy1',
    getName: function() {
        return this.txt
    }
}
const obj2 = {
    txt: 'coboy2',
    getName: function() {
        return obj1.getName()
    }
}
const obj3 = {
    txt: 'coboy3',
    getName: function() {
        const fn = obj1.getName
        return fn()
    }
}
console.log(obj1.getName()) 
console.log(obj2.getName())
console.log(obj3.getName())

這個時候輸出了 coboy1, coboy1, undefined。

最後一個其實在上面例題5中已經有說明了。通過 const fn = obj1.getName 的賦值進行了“裸奔”呼叫,因此這裡的 this 指向了 window,執行結果當然是 undefined。

例題13

上述的例題10中的 obj2.getName() 如果要它輸出‘coboy2’,如果不使用 bind、call、apply 方法該怎麼做?

const obj1 = {
    txt: 'coboy1',
    getName: function() {
        return this.txt
    }
}
const obj2 = {
    txt: 'coboy2',
    getName: obj1.getName
}

console.log(obj1.getName()) 
console.log(obj2.getName())

上述方法同樣說明了那個重要的結論:this 指向最後呼叫它的物件。

我們將函數 obj1 的 getName 函數掛載到了 obj2 的物件上,getName 最終作為 obj2 物件的方法被呼叫。

在建構函式中的 this

通過 new 操作符來構建一個建構函式的範例物件,這個建構函式中的 this 就指向這個新的範例物件。同時建構函式 prototype 屬性下面方法中的 this 也指向這個新的範例物件。

例題14

function Animal(){
    console.log(this) // Animal {}
}
const a1 = new Animal();
console.log(a1) // Animal {}
function Animal(){
    this.txt = 'coboy';
    this.age = 100;
}
Animal.prototype.getNum = function(){
    return this.txt;
}
const a1 = new Animal();
console.log(a1.age) // 100
console.log(a1.getNum()) // 'coboy'

在建構函式中出現顯式 return 的情況。

例題15

function Animal(){
    this.txt = 'coboy'
    const obj = {txt: 'cobyte'}
    return obj
}

const a1 = new Animal();
console.log(a1.txt) // cobyte

此時 a1 返回的是空物件 obj。

例題16

function Animal(){
    this.txt = 'coboy'
    return 1
}

const a1 = new Animal();
console.log(a1.txt) // 'coboy'

由此可以看出,如果建構函式中顯式返回一個值,且返回的是一個物件,那麼 this 就指向返回的物件,如果返回的不是一個物件,而是基本型別,那麼 this 仍然指向範例。

call,apply,bind 顯式修改 this 指向

call方法

例題17

const obj = {
    txt: "coboy", 
    age: 18, 
    getName: function() {
        console.log(this, this.txt)
    }
};
const obj1 = {
    txt: 'cobyte'
}
obj.getName(); // this指向obj
obj.getName.call(obj1); // this指向obj1
obj.getName.call(); // this指向window

apply方法

例題18

const obj = {
    txt: "coboy", 
    age: 18, 
    getName: function() {
        console.log(this, this.txt)
    }
};
const obj1 = {
    txt: 'cobyte'
}

obj.getName.apply(obj1) // this指向obj1
obj.getName.apply() // this指向window

call 方法和 apply 方法的區別

例題19

const obj = {
    txt: "coboy", 
    age: 18, 
    getName: function(name1, name2) {
        console.log(this, name1, name2)
    }
};
const obj1 = {
    txt: 'cobyte'
}

obj.getName.call(obj1, 'coboy1', 'coboy2')
obj.getName.apply(obj1, ['coboy1', 'coboy2'])

可見 call 和 apply 主要區別是在傳參上。apply 方法與 call 方法用法基本相同,不同點主要是 call() 方法的第二個引數和之後的引數可以是任意資料型別,而 apply 的第二個引數是陣列型別或者 arguments 引數集合。

bind 方法

例題20

const obj = {
    txt: "coboy", 
    age: 18, 
    getName: function() {
        console.log(this.txt)
    }
};
const obj2 = {
    txt: "cobyte"
}
const newGetName = obj.getName.bind(obj2)
newGetName() // this指向obj2
obj.getName() // this仍然指向obj

bind() 方法也能修改 this 指向,不過呼叫 bind() 方法不會執行 getName()函數,也不會改變 getName() 函數本身,只會返回一個已經修改了 this 指向的新函數,這個新函數可以賦值給一個變數,呼叫這個變數新函數才能執行 getName()。

call() 方法和 bind() 方法的區別在於

  • bind 的返回值是函數,並且不會自動呼叫執行。
  • 兩者後面的引數的使用也不同。call 是 把第二個及以後的引數作為原函數的實參傳進去, 而 bind 實參在其傳入引數的基礎上往後獲取引數執行。

例題21

function fn(a, b, c){ 
    console.log(a, b, c); 
}
const fn1 = fn.bind({abc : 123},600);
fn(100,200,300) // 100,200,300 
fn1(100,200,300) // 600,100,200 
fn1(200,300) // 600,200,300 
fn.call({abc : 123},600) // 600,undefined,undefined
fn.call({abc : 123},600,100,200) // 600,100,200

this 優先順序

我們通常把通過 call、apply、bind、new 對 this 進行繫結的情況稱為顯式繫結,而把根據呼叫關係確定 this 指向的情況稱為隱式繫結。那麼顯示繫結和隱式繫結誰的優先順序更高呢?

例題22

function getName() {
    console.log(this.txt)
}

const obj1 = {
    txt: 'coboy1',
    getName: getName
}

const obj2 = {
    txt: 'coboy2',
    getName: getName
}

obj1.getName.call(obj2) // 'coboy2'
obj2.getName.apply(obj1) // 'coboy1'

可以看出 call、apply 的顯示繫結比隱式繫結優先順序更高些。

例題23

function getName(name) {
   this.txt = name
}

const obj1 = {}

const newGetName = getName.bind(obj1)
newGetName('coboy')
console.log(obj1) // {txt: "coboy"}

當再使用 newGetName 作為建構函式時。

const obj2 = new newGetName('cobyte')
console.log(obj2.txt) // 'cobyte'

這個時候新物件中的 txt 屬性值為 'cobyte'。

newGetName 函數本身是通過 bind 方法構造的函數,其內部已經將this繫結為 obj1,當它再次作為建構函式通過 new 被呼叫時,返回的範例就已經和 obj1 解綁了。也就是說,new 繫結修改了 bind 繫結中的 this 指向,所以 new 繫結的優先順序比顯式 bind 繫結的更高。

例題24

function getName() {
   return name => {
      return this.txt
   }
}

const obj1 = { txt: 'coboy1' }
const obj2 = { txt: 'coboy2' }

const newGetName = getName.call(obj1)
console.log(newGetName.call(obj2)) // 'coboy1'

由於 getName 中的 this 繫結到了 obj1 上,所以 newGetName(參照箭頭函數) 中的 this 也會綁到 obj1 上,箭頭函數的繫結無法被修改。

例題25

var txt = 'good boy'
const getName = () => name => {
    return this.txt
}

const obj1 = { txt: 'coboy1' }
const obj2 = { txt: 'coboy2' }

const newGetName = getName.call(obj1)
console.log(newGetName.call(obj2)) // 'good boy'

例題26

const txt = 'good boy'
const getName = () => name => {
    return this.txt
}

const obj1 = { txt: 'coboy1' }
const obj2 = { txt: 'coboy2' }

const newGetName = getName.call(obj1)
console.log(newGetName.call(obj2)) // undefined

const 宣告的變數不會掛到 window 全域性物件上,所以 this 指向 window 時,自然也找不到 txt 變數了。

箭頭函數的 this 繫結無法修改

例題27

function Fn() {
    return txt => {
        return this.txt
    }
}

const obj1 = {
    txt: 'coboy'
}
const obj2 = {
    txt: 'cobyte'
}

const f = Fn.call(obj1)
console.log(f.call(obj2)) // 'coboy'

由於 Fn 中的 this 繫結到了 obj1 上,所以 f 中的 this 也會繫結到 obj1 上, 箭頭函數的繫結無法被修改。

例題28

var txt = '意外不'
const Fn = () => txt => {
   return this.txt
}

const obj1 = {
    txt: 'coboy'
}
const obj2 = {
    txt: 'cobyte'
}

const f = Fn.call(obj1)
console.log(f.call(obj2)) // '意外不'

如果將 var 宣告方式改成 const 或 let 則最後輸出為 undefined,原因是使用 const 或 let 宣告的變數不會掛載到 window 全域性物件上。因此,this 指向 window 時,自然也找不到 txt 變數了。

從手寫 new 操作符中去理解 this

有一道經典的面試題,JS 的 new 操作符到底做了什麼?

  • 建立一個新的空物件
  • 把這個新的空物件的隱式原型(__proto__)指向建構函式的原型物件(prototype
  • 把建構函式中的 this 指向新建立的空物件並且執行建構函式返回執行結果
  • 判斷返回的執行結果是否是參照型別,如果是參照型別則返回執行結果,new 操作失敗,否則返回建立的新物件
/*
  create函數要接受不定量的引數,第一個引數是建構函式(也就是new操作符的目標函數),其餘引數被建構函式使用。
  new Create() 是一種js語法糖。我們可以用函數呼叫的方式模擬實現
*/
function create(Fn,...args){
    // 1、建立一個空的物件
    let obj = {}; // let obj = Object.create({});
    // 2、將空物件的原型prototype指向建構函式的原型
    Object.setPrototypeOf(obj,Fn.prototype); // obj.__proto__ = Fn.prototype 
    // 以上 1、2步還可以通過 const obj = Object.create(Fn.prototype) 實現
    // 3、改變建構函式的上下文(this),並將引數傳入
    let result = Fn.apply(obj,args);
    // 4、如果建構函式執行後,返回的結果是物件型別,則直接將該結果返回,否則返回 obj 物件
    return result instanceof Object ? result : obj;
    // return typeof result === 'object' && result != null ? result : obj
}

一般情況下建構函式沒有返回值,但是作為函數,是可以有返回值的,這就解析了上面例題15和例題16的原因了。 在 new 的時候,會對建構函式的返回值做一些判斷:如果返回值是基礎資料型別,則忽略返回值,如果返回值是參照資料型別,則使用 return 的返回,也就是 new 操作符無效。

從手寫 call、apply、bind 中去理解 this

手寫 call 的實現

Function.prototype.myCall = function (context, ...args) {
    context = context || window
    // 建立唯一的屬性防止汙染
    const key = Symbol()
    // this 就是繫結的那個函數
    context[key] = this
    const result = context[key](...args)
    delete context[key]
    return result
}
  • myCall 中的 this 指向誰?

myCall 已經設定在 Function 建構函式的原型物件(prototype)上了,所以每個函數都可以呼叫 myCall 方法,比如函數 Fn.myCall(),根據 this 的確定規律:“誰呼叫它,this 就指向誰”,所以myCall方法內的 this 就指向了呼叫的函數,也可以說是要繫結的那個函數。

  • Fn.myCall(obj) 本質就是把函數 Fn 賦值到 物件 obj 上,然後通過物件 obj.Fn() 來執行函數 Fn,那麼最終又回到那個 this 的確定規律:“誰呼叫它,this 就指向誰”,因為物件 obj 呼叫了 Fn 所以 Fn 內部的 this 就指向了物件 obj。

手寫 apply 的實現

apply 的實現跟 call 的實現基本是一樣的,因為他們的使用方式也基本一樣,只是傳參的方式不一樣。apply 的引數必須以陣列的形式傳參。

Function.prototype.myApply = function (context, args) {
    if(!Array.isArray(args)) {
       new Error('引數必須是陣列')
    }
    context = context || window
    // 建立唯一的屬性防止汙染
    const key = Symbol()
    // this 就是繫結的那個函數
    context[key] = this
    const result = context[key](...args)
    delete context[key]
    return result
}

手寫 bind 的實現

bind 和 call、apply 方法的區別是它不會立即執行,它是返回一個改變了 this 指向的函數,在繫結的時候可以傳參,之後執行的時候,還可以繼續傳引數數。這個就是一個典型的閉包行為了,是不是。

我們先來實現一個簡單版的:

Function.prototype.myBind = function (ctx, ...args) {
    // 根據上文我們可以知道 this 就是呼叫的那個函數
    const self = this
    return function bound(...newArgs) {
        // 在再次執行的的時候去改變 this 的指向
        return self.apply(ctx, [...args, ...newArgs])
    }
}

但是,就如之前在 this 優先順序分析那裡所展示的規則:bind 返回的函數如果作為建構函式通過 new 操作符進行範例化操作的話,繫結的 this 就會實效。

為了實現這樣的規則,我們就需要區分這兩種情況的呼叫方式,那麼怎麼區分呢?首先返回出去的是 bound 函數,那麼 new 操作符範例化的就是 bound 函數。通過上文 “從手寫 new 操作符中去理解 this” 中我們可以知道當函數被 new 進行範例化的時候, 建構函式內部的 this 就是指向範例化的物件,那麼判斷一個函數是否是一個範例化的物件的建構函式時可以通過 intanceof 操作符判斷。

知識點: instanceof 運運算元用於檢測建構函式的 prototype 屬性是否出現在某個範例物件的原型鏈上。

Function.prototype.myBind = function (ctx, ...args) {
    // 根據上文我們可以知道 this 就是呼叫的那個函數
    const self = this
    return function bound(...newArgs) {
        // new Fn 的時候, this 是 Fn 的範例物件
        if(this instanceof Fn) {
            return new self(...args, ...newArgs)
        }
        // 在再次執行的的時候去改變 this 的指向
        return self.apply(ctx, [...args, ...newArgs])
    }
}

另外我們也可以通過上文的實現 new 操作符的程式碼來實現 bind 裡面的 new 操作。

完整的複雜版:

Function.prototype.myBind = function (ctx) {
  const self = this
  if (!Object.prototype.toString.call(self) === '[object Function]') {
    throw TypeError('myBind must be called on a function');
  }
  // 對 context 進行深拷貝,防止 bind 執行後返回函數未執行期間,context 被修改
  ctx = JSON.parse(JSON.stringify(ctx)) || window;

  const args = Array.prototype.slice.call(arguments, 1);

  /**
   * 建構函式生成物件範例
   * @returns {Object|*}
   */
  const create = function (conFn) {
    const obj = {};

    /* 設定原型指向,確定繼承關係 */
    obj.__proto__ = conFn.prototype;

    /**
     * 1、執行目標函數,繫結函數內部的屬性
     * 2、如果目標函數有物件型別的返回值則取返回值,符合js new關鍵字的規範
     */
    const res = conFn.apply(obj, Array.prototype.slice.call(arguments,1));
    return typeof res === 'object' && res != null ? res : obj;
  };

  const bound = function () {
    // new 操作符操作的時候
    if (this instanceof bound) {
      return create(self, args.concat(Array.prototype.slice.call(arguments)));
    }
    return self.apply(ctx, args.concat(Array.prototype.slice.call(arguments)));
  };

  return bound;
};

為什麼顯式繫結的 this 要比隱式繫結的 this 優先順序要高

通過上面的實現原理,我們就可以理解為什麼上面的 this 優先順序中通過 call、apply、bind 和 new 操作符的顯式繫結的 this 要比隱式繫結的 this 優先順序要高了。例如上面的 obj1.getName.call(obj2) 中的 getName 方法本來是通過 obj1 來呼叫的,但通過 call 方法之後,實際 getName 方法變成了 obj2.getName() 來執行了。

總結

通過本篇內容的學習,我們看到 this 的用法紛繁多樣,確實不容易掌握。但總的來說可以總結為以下幾條規則:

  • 在函數體中,非顯式或隱式地簡單呼叫函數時,在嚴格模式下,函數內的 this 會繫結到 undefined 上,在非嚴格模式下則會被繫結到全域性物件 window/global 上。
  • 一般使用 new 方法呼叫建構函式時,建構函式內的 this 會被繫結到新建立的物件上,且優先順序要比 bind 的高。
  • 一般通過 call、apply、bind 方法顯式呼叫函數時,函數體內的 this 會被繫結到指定引數的物件上,顯式繫結的 this 要比隱式繫結的 this 優先順序要高。
  • 一般通過上下文物件呼叫函數時,函數體內的 this 會被繫結到該物件上。
  • 在箭頭函數中,this 的指向是由外層(函數或全域性)作用域來決定的。

最後推薦一個學習 vue3 原始碼的庫,它是基於崔效瑞老師的開源庫 mini-vue 而來,在 mini-vue 的基礎上實現更多的 vue3 核心功能,用於深入學習 vue3, 讓你更輕鬆地理解 vue3 的核心邏輯。

Github 地址:mini-vue3-plus

到此這篇關於JavaScript中this例題實戰的文章就介紹到這了,更多相關js this例題內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com