JavaScript中this的繫結規則

MomentYY發表於2022-02-13

JavaScript中this的繫結規則

前言

我們知道瀏覽器執行環境下在全域性作用域下的this是指向window的,但是開發中卻很少在全域性作用域下去使用this,通常都是在函式中進行使用,而函式使用不同方式進行呼叫,其this的指向是不一樣的。JavaScript中函式在被呼叫時,會先建立一個函式執行上下文(FEC),而這個上下文中記錄著函式的呼叫棧、活動物件(AO)以及this等等。那麼this到底指向什麼呢?下面一起來看看this的四個繫結規則。

1.this四個繫結規則

1.1.預設繫結

在進行獨立函式呼叫時,this就會使用預設繫結,而預設繫結的值就是全域性的window。獨立函式呼叫可以簡單認為函式沒有被繫結到某個物件上進行呼叫。

滿足預設繫結的函式呼叫情況:

  • 全域性定義的函式,直接在全域性進行呼叫;

    function foo() {
      console.log(this)
    }
    
    foo() // window
    
  • 多個函式進行巢狀呼叫,雖然以下函式巢狀了一層又一層,原則上還是獨立函式呼叫;

    function foo1() {
      console.log(this) // window
    }
    function foo2() {
      console.log(this) // window
      foo1()
    }
    function foo3() {
      console.log(this) // window
      foo2()
    }
    
    foo3()
    
  • 函式的返回值為一個函式,通過呼叫該函式將返回值賦值給某個變數,最後呼叫這個變數(最後fn的呼叫也是獨立函式呼叫,與函式的定義位置無關);

    function foo() {
      return function() {
        console.log(this)
      }
    }
    
    const fn = foo()
    fn() // window
    
  • 將物件中的方法賦值給別人,別人再進行呼叫,雖然是物件中的方法再次賦值,最後被賦值的物件都是進行獨立函式呼叫的;

    • obj中的bar方法作為引數傳遞到foo函式中,然後進行呼叫(最後被傳遞的引數進行了獨立呼叫)

      function foo(func) {
        func()
      }
      
      const obj = {
        bar: function() {
          console.log(this)
        }
      }
      
      foo(obj.bar) // window
      
    • obj中的bar方法賦值給fn變數,然後進行呼叫(最後被賦值的fn進行了獨立呼叫)

      const obj = {
        bar: function() {
          console.log(this)
        }
      }
      
      const fn = obj.bar
      fn() // window
      

1.2.隱式繫結

隱式繫結是開發中比較常見的,通過某個物件對函式進行呼叫,也就是說函式的呼叫是通過物件發起的,常見為物件中方法的呼叫(這個物件會被js引擎繫結到函式中的this裡面)。

滿足隱式繫結的函式呼叫情況:

  • 直接呼叫物件中的方法;

    const obj = {
      foo: function() {
        console.log(this)
      }
    }
    
    obj.foo() // obj物件
    
  • 將函式定義在全域性,然後再賦值給物件屬性,再通過物件呼叫;

    function bar() {
      console.log(this)
    }
    
    const obj = {
      foo: bar
    }
    
    obj.foo() // obj物件
    
  • 呼叫物件中屬性值為物件中的方法(最後foo函式的呼叫發起者為obj2);

    const obj1 = {
      name: 'obj1',
      obj2: {
        name: 'obj2',
        foo: function() {
          console.log(this)
        }
      }
    }
    
    obj1.obj2.foo() // obj2物件
    

1.3.顯示繫結

如果我們不希望使用隱式繫結的物件,而想在呼叫函式時給this繫結上自己想繫結的東西,那麼這樣的行為就稱為顯示繫結,顯示繫結主要藉助於三個方法,分別為call、apply和bind。

1.3.1.簡介
  • call 方法:使用一個指定的this值和單獨給出的一個或多個引數來呼叫一個函式。
  • apply方法:呼叫一個具有給定this值的函式,以及以一個陣列(或類陣列物件)的形式提供的引數。
  • bind方法:建立一個新的函式,在 bind 被呼叫時,這個新函式的this被指定為bind的第一個引數,而其餘引數將作為新函式的引數,供呼叫時使用。
1.3.2.call、apply繫結this

JS中所有的函式都有call和apply方法屬性(主要是其原型鏈上實現了這兩個方法),可以說這兩個函式就是給this準備的。簡單介紹一下這兩個函式的區別:在進行函式呼叫時,都可以使用call和apply進行呼叫,其傳入的第一個引數都是一樣的,也就是我們希望給this繫結的值,後面的引數apply對應為包含多個引數的陣列,call對應為引數列表

  • foo函式不僅可以直接進行呼叫,還可以通過call和apply進行呼叫,並且在執行foo函式時,可以明確繫結this;

    function foo() {
      console.log(this)
    }
    
    const obj = {
      name: 'curry',
      age: 30
    }
    
    foo() // window
    foo.call(obj) // obj物件
    foo.apply(obj) // obj物件
    
  • 當被呼叫函式有引數需要傳入時,就可以在call和apply後面傳入對應的引數;

    function sum(x, y) {
      console.log(this, x + y)
    }
    
    const obj = {
      name: 'curry',
      age: 30
    }
    
    sum(10, 20) // window 30
    sum.call(obj, 10, 20) // obj物件 30
    sum.apply(obj, [10, 20]) // obj物件 30
    
  • 上面的示例都是給this繫結一個物件,其實還可以給this繫結不同資料型別;

    function foo() {
      console.log(this)
    }
    
    // 1.繫結字串
    foo.call('aaa')
    foo.apply('bbb')
    
    // 2.繫結字串
    foo.call(111)
    foo.apply(222)
    
    // 3.繫結陣列
    foo.call([1, 2, 3])
    foo.apply(['a', 'b', 'c'])
    

1.3.3.bind繫結this

call和apply可以幫助我們呼叫函式明確this的繫結物件,而bind與這兩種的用法不太一樣。如果希望一個函式的this可以一直繫結到某個物件上,那麼bind就可以派出用場了。

  • 上面提到了使用bind對this進行繫結時,會給我們返回一個新函式,而這個新函式的this就繫結為我們指定的物件;

    function foo() {
      console.log(this)
    }
    
    const obj = {
      name: 'curry',
      age: 30
    }
    
    const newFoo = foo.bind(obj)
    newFoo() // obj物件
    
  • 使用bind繫結的this是不會被我們上面提到的繫結規則所改變的,比如,使用bind繫結的物件為obj1,不管是使用call、apply還是物件方法呼叫(隱式繫結),都不會改變this的指向了;

    function foo() {
      console.log(this)
    }
    
    const obj1 = {
      name: 'curry',
      age: 30
    }
    const obj2 = {
      name: 'kobe',
      age: 24
    }
    
    const newFoo = foo.bind(obj1)
    newFoo() // obj1物件
    
    newFoo.call(obj2) // obj1物件
    newFoo.apply(obj2) // obj1物件
    
    const obj3 = {
      bar: newFoo
    }
    obj3.bar() // obj1物件
    

1.4.new繫結

JavaScript中的函式不僅可以通過上面的方法進行呼叫,還可以使用new關鍵字來呼叫函式,當使用new來呼叫的函式,一般稱為建構函式(ES6中的類),以這樣的方式來呼叫的函式this所繫結的值,就叫做new繫結。

建構函式一般用於建立物件,通過new呼叫建構函式可以返回一個例項物件,而this繫結就是這個例項物件。那麼使用new呼叫時,函式內部會進行哪些操作呢?如下:

  • 1.函式內部建立一個全新的物件(空物件)

  • 2.這個新物件內部的[[prototype]]屬性(也就是物件原型)會被賦值為該建構函式的prototype屬性

  • 3.建構函式內部的this,會指向這個建立出來的新物件

  • 4.執行建構函式中的程式碼(函式體程式碼);

  • 5.如果該建構函式沒有返回其它物件,則返回建立出來的新物件;

    function Student(sno, name, age) {
      this.sno = sno
      this.name = name
      this.age = age
    
      console.log('this: ', this)
    }
    
    const stu1 = new Student(1, 'curry', 30) // this: {sno: 1, name: "curry", age: 30}
    console.log(stu1) // Student {sno: 1, name: "curry", age: 30}
    
    const stu2 = new Student(2, 'kobe', 24) // this: {sno: 2, name: "kobe", age: 24}
    console.log(stu2) // Student {sno: 2, name: "kobe", age: 24}
    
    console.log(stu1.__proto__ === Student.prototype) // true
    console.log(stu2.__proto__ === Student.prototype) // true
    

2.JS內建函式的this繫結

  • setTimeout:第一個引數在內部是進行獨立函式呼叫的;

    setTimeout(function() {
      console.log(this) // window
    }, 1000)
    
    // ------相當於------
    function mySetTimeout(callback, delay) {
      callback() // 獨立函式呼叫
    }
    
  • DOM事件監聽:事件觸發後的回撥函式中的this是指向當前DOM物件的;

    const boxDiv = document.querySelector('.box')
    boxDiv.onclick = function() {
      console.log(this) // boxDiv(DOM物件)
    }
    boxDiv.addEventListener('click', function() {
      console.log(this) // boxDiv(DOM物件)
    })
    
  • 陣列內建方法:陣列中的forEach、map、filter、find等這些方法都可以傳入第二個引數,這個引數即為內部函式的this指向。

    const arr = [1, 2, 3, 4, 5]
    
    const obj = {
      name: 'curry',
      age: 30
    }
    
    arr.forEach(function() {
      console.log(this) // obj物件
    }, obj)
    
    arr.map(function() {
      console.log(this) // obj物件
    }, obj)
    
    arr.filter(function() {
      console.log(this) // obj物件
    }, obj)
    

3.this繫結規則優先順序

上面提到了this的四種繫結規則,如果在函式呼叫時,使用到了多種繫結規則,最終函式的this指向什麼呢?那麼這裡就涉及到this繫結的優先順序,優先順序高的規則決定this的最終繫結。

this四種繫結規則優先順序如下:

  • 預設繫結優先順序最低:函式呼叫存在其它規則時,就會遵循其它規則來繫結其this;

  • 顯示繫結優先順序高於隱式繫結

    const obj1 = {
      name: 'obj1',
      foo: function() {
        console.log(this)
      }
    }
    const obj2 = {
      name: 'obj2'
    }
    
    obj1.foo.call(obj2) // obj2物件
    obj1.foo.apply(obj2) // obj2物件
    
    const newFoo = obj1.foo.bind(obj2)
    newFoo() // obj2物件
    
  • 顯示繫結中bind高於call和apply

    function foo() {
      console.log(this)
    }
    
    const obj1 = {
      name: 'curry',
      age: 30
    }
    const obj2 = {
      name: 'kobe',
      age: 24
    }
    
    const newFoo = foo.bind(obj1)
    newFoo() // obj1物件
    newFoo.call(obj2) // obj1物件
    newFoo.apply(obj2) // obj1物件
    
  • new繫結優先順序高於隱式繫結和顯示繫結

    // 1.new繫結高於隱式繫結
    const obj1 = {
      foo: function() {
        console.log(this)
      }
    }
    
    const f = new obj1.foo() // foo函式物件
    
    // 2.new繫結高於顯示繫結
    function foo(name) {
      console.log(this)
    }
    
    const obj2 = {
      name: 'obj2'
    }
    
    const newFoo = foo.bind(obj2)
    const nf = new newFoo(123) // foo函式物件
    

總結

  • new繫結 > 顯示繫結(apply/call/bind) > 隱式繫結 > 預設繫結
  • 注意new關鍵字不能和apply、call一起使用,所以不太好進行比較,預設為new繫結是優先順序最高的;

4.特殊情況下的this繫結

在特殊情況下,this的繫結不一定滿足上面的繫結規則,主要有以下特殊情況:

  • 顯示繫結的忽略:在顯示繫結中,如果給call、apply和bind第一個引數傳入null或者undefined,那麼這樣的顯示繫結會被忽略,最終使用預設繫結,也就是全域性的window;

    function foo() {
      console.log(this)
    }
    
    foo.call(null) // window
    foo.call(undefined) // window
    
    foo.apply(null) // window
    foo.apply(undefined) // window
    
    const newFoo1 = foo.bind(null)
    const newFoo2 = foo.bind(undefined)
    newFoo1() // window
    newFoo2() // window
    
  • 間接函式的引用:建立一個函式的間接引用,該情況也使用預設繫結。如下程式碼中給obj2建立一個bar屬性並賦值為obj1中foo函式時直接進行呼叫;

    const obj1 = {
      name: 'obj1',
      foo: function() {
        console.log(this)
      }
    }
    const obj2 = {
      name: 'obj2'
    }
    
    // 被認為是獨立函式呼叫
    ;(obj2.bar = obj1.foo)() // window
    
  • ES6中的箭頭函式:箭頭函式不會使用上面的四種繫結規則,也就是說不繫結this,箭頭函式的this是根據它外層作用域中的this繫結來決定的;

    • 陣列內建方法中回撥使用箭頭函式:

      const names = ['curry', 'kobe', 'klay']
      const obj = {
        name: 'obj'
      }
      
      names.map(() => {
        console.log(this) // window
      }, obj)
      
    • 多層物件中的方法屬性使用箭頭函式:

      const obj1 = {
        name: 'obj1',
        obj2: {
          name: 'obj2',
          foo: () => {
            console.log(this)
          }
        }
      }
      
      obj1.obj2.foo() // window
      
    • 函式的返回值為箭頭函式:

      function foo() {
        return () => {
          console.log(this)
        }
      }
      
      const obj = {
        name: 'curry',
        age: 30
      }
      
      const bar = foo.call()
      bar() // obj物件
      

5.node環境下全域性this的指向

以上提到的內容都是在瀏覽器環境中進行測試的,在瀏覽器環境下的this是指向全域性window的,那麼在node環境中全域性this指向什麼呢?

  • 在node環境下列印一下全域性this:

  • 為什麼全域性this列印為一個空物件?

    • 當我們js檔案被node執行時,該js檔案會被視為一個模組;

    • node載入編譯這個模組,將模組中的所有程式碼放入到一個函式中;

    • 然後會執行這個函式,在執行這個函式時會通過apply繫結一個this,而繫結的this就為{}

  • 那為什麼node中還是可以使用想window中的全域性方法呢?像setTimeout、setInterval等。

    • 因為node環境中也是存在全域性物件的,通過列印globalThis就可以進行檢視;

相關文章