萬水千山老是情,看看this行不行

this = ?

在JS中,當一個函數執行時,都會建立一個執行上下文用來確認當前函數的執行環境,執行上下文分爲 全局執行上下文函數執行上下文。而 this 就是指向這個執行上下文的對象。因此,this是在運行時決定的,能夠簡單的理解爲 誰調用,this指向誰。javascript

分四種狀況來看:java

  • 普通函數調用
  • 對象方法調用
  • 構造函數調用
  • call、apply、bind

普通函數調用

當函數做爲函數獨立調用的時候,則是在全局環境中運行,this 則指向全局對象 window數據結構

一個簡單的例子app

function demo() {
    console.log(this);  // window
}

demo();

demo 函數獨立調用,因此 this 指向全局對象 window函數

接着this

function outer() {
    function inner() {
        console.log(this); // window
    }

    inner();
}

outer();

雖然在 outer 函數內部聲明瞭一個 inner 函數,但實際上 inner 函數是獨立調用的,因此依然是在全局環境,this仍然是指向了 windowprototype

function demo(func) {
        func();
    }
    demo(function () {
        console.log(this); // window
    });

demo函數傳入一個匿名函數,執行匿名函數func的時候,依然是做爲函數獨立調用,因此this仍然指向window設計

理解一下什麼是做爲函數獨立調用:
當定義一個函數,例如var demo = function () {} 等號右邊的函數是獨立放在內存中的,而後賦予demo變量的指向爲函數所在的內存地址,當直接調用 demo(),至關於直接找到函數自己執行,因此函數內部建立的上下文爲全局上下文,this 則指向了全局對象 windowcode

對象方法調用

當調用一個對象方法時,this 表明了對象自己。對象

let obj = {
        name: 'invoker',
        getName: function () {
            console.log(this);   // obj
            console.log(this.name);  // "invoker"
        }
    }

    obj.getName();

定義了一個 obj 對象,調用其內部的getNamethis 則指向了 obj 對象。

稍微修改一下

var name = 'windowName';
    let obj = {
        name: 'invoker',
        getName: function () {
            console.log(this);  // window
            console.log(this.name); // windowName
        }
    }
 
    var getName = obj.getName;
    getName();

當用一個變量 getName 接收 obj 對象的 getName方法, 再執行 getName,發現 this 指向了 window,由於此時變量 getName 直接指向了函數自己,而不是經過 obj 去調用,此時就變成了函數獨立調用的狀況了。

再看個例子

let obj = {
        test: function() {
            function fn() {
                console.log(this); // window
            }
            fn();
        },
        test1: function (fn) {
            fn()
        }
    }

    obj.test();
    obj.test1(function () {
        console.log(this) // window
    });

雖然在 obj 對象的 test 方法內定義了 fn ,但執行時一樣屬於函數獨立調用,因此 this 指向 window
將函數做爲參數傳入 objtest1 方法,也屬於函數獨立調用,this 一樣指向 window

構造函數調用

使用 new 關鍵字調用函數,則是構造函數調用,this 指向了該構造函數新建立的對象。

function person(name) {
        this.name = name
    }

    let p = new person('invoker')
    console.log(p.name) // 'invoker'

回顧一下 new 關鍵詞的過程:

  • 建立一個新的對象 obj
  • 使得 obj__proto__ 指向 構造函數的原型對象
  • 執行構造函數中的 constructor,改變this的指向爲 obj
  • 若是結果是對象類型,則返回結果,不然返回obj
function myNew(Fn) {
        let obj = {}
        obj.__proto__ = Fn.prototype

        const res = Fn.prototype.constructor.call(obj)
        if (typeof res === 'object') {
            obj = res
        }

        return obj
    }

call、apply、bind

this 指向的是 callapplybind 調用時傳遞的第一個參數。

let obj = {
        name: 'invoker'
    }

    function demo() {
        console.log(this.name) // 'invoker'
    }

    demo.call(obj)
    demo.apply(obj) 
    demo.bind(obj)()

箭頭函數

箭頭函數在執行時並不會建立自身的上下文,它的 this 取決於自身被定義的所在執行上下文。

例子:

let obj = {
        fn: () => {
            console.log(this) // window
        }
    }

    obj.fn()

objfn 指向一個箭頭函數,因爲只有函數能夠建立執行上下文,而箭頭函數外部並無包裹函數,因此箭頭函數所在的執行上下文爲全局的執行上下文,this 指向 window

包裹一個函數看看唄?

let obj = {
        fn: function () {
            console.log('箭頭函數所在執行上下文', this) // '箭頭函數所在執行上下文' obj
            
            var arrow = () => {
                console.log(this) //obj
            }
            arrow()
        }
    }

    obj.fn()

箭頭函數 arrow 被定義在 obj.fn 內,因此 fn 中的 this 就是 arrow 中的 this

箭頭函數一次綁定上下文後便不可更改:

let obj = { name: 'invoker' }

    var demo = () => {
        console.log(this) // window
    }

    demo.call(obj)

雖然使用了 call 函數間接修改 this 的指向,但並不起做用。

爲何會有this的設計

javascript中存在 this 的設計,跟其內存中的數據結構有關係。

假設定義 let obj = { name: 'invoker' }

  1. 此時會先生成一個對象 { name: 'invoker' } 並放在內存當中
  2. { name: 'invoker } 所在的內存地址賦予 obj

因此 obj 其實就是個指向某個對象的地址,若是要讀取 obj.name,則先要找到 obj 所在地址,而後從地址中拿到原始對象,讀取 name 屬性。

對象中每一個屬性都有一個屬性描述對象:可經過 Object.getOwnPropertyDescriptor(obj, key) 來讀取。
也就是說上面所說的 objname 屬性實際是下面這樣的

{
  name: {
    [[value]]: 'invoker',
    [[configurable]]: true,
    [[enumerable]]: true,
    [[writable]]: true
  }
}

value 就是得到的值。

如今假設對象的屬性是一個函數:

let name = 'windowName'
    let obj = {
        name: 'invoker',
        sayHello: function () {
            console.log('my name is ' + this.name)
        }
    }

    let descriptor = Object.getOwnPropertyDescriptor(obj, 'sayHello')
    console.log(descriptor)
    
    //這個sayHello的屬性描述對象爲:
      sayHello: {
        [[value]]: ƒ (),
        [[configurable]]: true,
        [[enumerable]]: true,
        [[writable]]: true
      }

sayHellovalue 值是一個函數,這個時候,引擎會單獨將這個函數放在 內存 當中,而後將函數的內存地址賦予 value

所以能夠得知,這個函數在 內存 中是單獨的,並不被誰擁有,因此它能夠在不一樣的上下文執行。

因爲函數能夠在不一樣上下文執行,因此須要一種機制去獲取當前函數內部的執行上下文。因此,就有了 this,它指向了當前函數執行的上下文。

// 接着上面代碼

    obj.sayHello() // my name is invoker
    
    let sayHello = obj.sayHello
    sayHello() // my name is windowName

obj.sayHello() 是經過 obj 找到 sayHello,也就是對象方法調用,因此就是在 obj 環境執行。
let sayHello = obj.sayHello,變量 sayHello 就直接指向函數自己,因此 sayHello() 也就是函數獨立調用,因此是全局環境執行。

總結

this 的出現,跟JS引擎內存中的數據結構有關係。
當發現一個函數被執行時,經過上面的多種狀況。

  • 分析函數怎麼調用(單獨調用、對象方法、構造方法)
  • 是否有使用 call、apply 等間接調用
  • 是否有箭頭函數
  • 甚至還可能分析是否爲嚴格模式

這樣就能很好的確認 this 的指向。

相關文章
相關標籤/搜索