深刻理解JS中this關鍵字

爲何要使用this關鍵字

   看個例子bash

function indetify() {
        retun this.name.toUpperCase()
    }
    
    var obj = {
        name: 'zz'
    }
    indetify.call(obj)  // 'ZZ'
複製代碼

這裏函數identify中的this指向變量obj,若是不使用this的話,想實現這種效果,就須要顯示的傳入上下文對象,例如app

function indetify(context) {
        retun context.name.toUpperCase()
    }
    var obj = {
        name: 'zz'
    }
    indetify(obj)  // 'ZZ'
複製代碼

當模式愈來愈複雜的時候,顯示的傳遞上下文對象就會變得不太好管理,而this提供了一種更優雅的方式,隱式的"傳遞"一個對象的引用,所以能夠將API設計的更加簡潔而且易於複用。ide

對於this的誤解

  1. this指向自身
  2. this指向函數做用域(須要明確的是,this在任何狀況下都不指向函數的詞法做用域)

this究竟是什麼

this是在運行的時候被綁定的,並非編寫的時候,它的上下文取決於函數調用的各類條件。當一個函數被調用時,會建立一個活動記錄(即執行上下文),這個記錄包含函數的調用棧(call Stack),調用方式,傳入參數等信息,this就是這個記錄的一個屬性,在函數執行的過程當中找到。函數

要想找到this的綁定對象,首先得找到函數的調用位置,調用位置就在當前執行函數的前一個調用中。測試

function baz () {
        // 當前調用棧是baz
        // 當前調用位置是全局做用域
        console.log('baz')
        bar()   // bar的調用位置
    }
    
    function bar () {
        // 當前的調用棧是baz -> bar
        // 因此當前的調用位置在baz中
        console.log('bar')
        foo()   // foo的調用位置
    }
    
    function foo () {
        // 當前的調用棧是baz -> bar -> foo
        // 因此當前的調用位置在bar中
        console.log('foo')
    }
    baz()   // baz的調用位置
複製代碼

找到調用位置,接下來就要分析this的綁定規則了。ui

this綁定規則

  1. 默認綁定規則
function foo () {
    console.log(this.a)
}

var a = 1

foo()   // 1
複製代碼

由於foo()是直接不帶任何修飾的函數引用進行調用的,因此只能使用默認綁定。非嚴格模式下指向window,嚴格模式下綁定到undefinedthis

  1. 隱私綁定規則

分析隱式綁定時,必須在一個對象內部包含一個指向函數的屬性,經過這個屬性間接引用函數spa

function foo () {
    console.log(this.a)
}

var obj = {
    a: 2,
    foo: foo
}

obj.foo()   // 2
複製代碼

首先要聲明的是,不管foo函數是先聲明仍是直接在obj對象中定義,這個函數嚴格來講,都不屬於obj對象。prototype

隱式綁定會遇到一個問題,就是會丟失綁定對象,也就是會應用默認綁定。好比設計

function foo () {
    console.log(this.a)
}

var obj = {
    a: 2,
    foo: foo
}

var another = obj.foo   // 函數別名

var a = '隱式丟失'

obj.foo()   // 2
another()   // '隱式丟失'
複製代碼

這裏雖然bar是obj.foo的一個引用,但實際上引用的是foo函數自己,所以bar()是不帶任何修飾的函數調用,因此也是默認綁定。

還有一種更微妙的狀況,發生在傳入回調函數的時候

function foo () {
    console.log(this.a)
}

var obj = {
    a: 2,
    foo: foo
}

function doFun (fn) {
    // fn 實際上是引用foo
    // 至關於var fn = foo
    fn()
}

var a = '又隱式丟失'

obj.foo()   // 2
doFun(obj.foo)   // '又隱式丟失'
複製代碼

實際上傳遞參數,實際上就是一種賦值操做,因此結果和上面同樣

  1. 顯示綁定規則

一般狀況,咱們使用js提供的call和apply方法實現顯示綁定

這倆個方法的第一個參數是一個對象,是給this準備的,在調用函數是將函數綁定到this,所以稱爲顯示綁定。第二個參數就是一個參數列表或參數對象,就是傳遞函數的調用的參數。

function foo (name) {
    console.log(this.a+name)
}

var obj = {
    a: 1
}

foo.call(obj,'顯示綁定')   // 1'顯示綁定'
複製代碼

可是,顯示綁定仍是會出現綁定丟失的狀況,能有辦法解決嗎?固然有

  • 硬綁定
function foo () {
    console.log(this.a)
}

var obj = {
    a: 1
}

var bar = function () {
    foo.call(obj)
}

var a = '會丟失嗎'

bar()   // 1

// 如今不會丟失了

setTimeOut(bar, 1000)   // 1
bar.call(window)        // 1
複製代碼

咱們建立了函數bar(),而且在內部手動調用foo.call(obj),強制將foo的this綁定到了obj,不管後面如何調用函數bar,都會手動在obj上調用foo

  • API調用的「上下文」

js語言和宿主環境中許多新的內置函數,都提供了一個可選參數,一般稱爲「上下文」(context),做用和bind(..)同樣,確保回調函數使用指定的this

function foo (el) {
        console.log(el, this.id)
    }
    
    var obj = {
        id: 'awesome'
    }
    // 調用foo的同時把this綁定到obj上
    [1,2,3].forEach(foo, obj)
    // 1'awesome' 2'awesome' 3'awesome'
複製代碼
  1. new綁定規則

js中的new的機制和麪向類的語言徹底不一樣

咱們習慣把new的函數叫作構造函數,實際上,只是使用new操做符時被調用的函數,不會實例化一個類,由於實例化的類與類之間是複製,是兩位徹底不要緊的,但js不是。只能說這些函數是被new操做符調用的普通函數而已。

首先,咱們看看new操做符會作些什麼

  1. 建立(或者說構造)一個全新的對象

  2. 這個對象會被執行[[prototypt]]鏈(原型鏈)

  3. 新的對象會綁定到函數調用的this上

  4. 若是函數沒有返回對象,那new表達式中的函數會自動返回這個新對象

function foo (a) {
        console.log(a)
    }
    
    var bar = new foo(1)
    bar()   // 1
複製代碼

優先級

既然this有這麼多種綁定方式,確定會存在綁定的優先級

首先,毫無疑問,默認綁定的優先級是最低的

  1. 那隱式綁定和顯示綁定的優先級誰高?
funtion foo (){
        console.log(this.a)
    }
    
    var obj1 = {
        a: 1,
        foo: foo
    }
    
    var obj2 = {
        a: 2,
        foo: foo
    }
    
    obj1.foo()  // 1
    obj2.foo()  // 2
    
    // 顯示綁定改變了this的指向
    obj1.foo.call(obj2) // 2
    obj.foo.call(obj1)  // 1
複製代碼

很顯然,顯示綁定的優先級比隱式綁定的高。

  1. 隱式綁定和new綁定誰的優先級高
function foo (something) {
        this.a =  somethig
    }
    
    var obj1 = {
        foo: foo
    }
    
    var obj2 = {}
    
    obj1.foo(1)
    console.log(obj1.a) // 1
    
    obj1.foo.call(obj2, 2)
    console.log(obj2.a)  // 2
    
    var bar = new obj1.foo(3)
    
    console.log(obj2.a) // 2
    
    console.log(bar.a)  // 3
    
複製代碼

看來new綁定的優先級是比隱式綁定高的,最後咱們看一下new和顯示綁定誰的優先級高,由於new和call/apply沒法一塊兒使用,因此無法經過new foo.call(obj1)來直接測試,咱們選擇用硬綁定來測試。

回憶一下硬綁定是如何工做的,Function.prototype.bind(..)會建立一個新的包裝函數,這個函數會忽略它當前的this綁定,並把提供的對象綁定到this上。

  1. new和顯示綁定誰的優先級高
function foo (something) {
        this.a =  somethig
    }
    
    var obj1 = {}
    
    var bar = foo.bind(obj1)
    bar(1)
    console.log(bar.a)  // 1
    
    var baz = new bar(2)
    
    console.log(obj1.a) // 1
    console.log(baz.a)  // 2
    
複製代碼

bar被硬綁定到obj1上,可是new bar(2),並無將obj1.a修改爲2,相反,new修改了硬綁定(到obj1的)調用bar(..)中的this。這樣看來new調用的函數,新建立的this替換了硬綁定的參數,因此new的優先級是最高的。

那咱們斷定優先級的方法就是從優先級由高往下去斷定。

綁定例外

凡事都有例外,不是全部的綁定都遵循這個規則的。

  1. 若是是call(null)或者apply(undefined),這裏對應的實際上是默認綁定,由於其實,null和undefined只是基礎的數據類型,並非對象。

  2. 軟綁定

硬綁定能夠強制綁定到指定對象,可是這大大下降了函數的靈活性,以後沒法使用隱式或顯示綁定修改this的指向,因此咱們來實現一下軟綁定

// 實現軟綁定
    if (!Function.prototype.softBind) {
        Function.prototype.softBind = function (obj) {
            var fn = this
            // 捕獲全部curried參數
            var curried = [].slice.call(arguments, 1)
            var bound = function () {
                return fn.apply(
                    (!this || this === (window || global)) ?
                        obj : this,
                    curried.concat.apply(curried, arguments)
                )
            }
            bound.prototype = Object.create(fn.prototype)
            return bound
        }
    }
    
    // 驗證
    function foo () {
        console.log("name: " + this.name)
    }
    
    var obj1 = { name: "obj1"}
    var obj2 = { name: "obj2"}
    var obj3 = { name: "obj3"}
    
    var fooOBJ = foo.softBind(obj)
    fooOBJ()    // "obj"
    
    obj2.foo = foo.softBind(obj)
    obj2.foo()  // "obj2"
    
    fooOBJ.call(obj3)   // "obj3"
    
    setTimeOut(obj2.foo, 10)   // "obj"
複製代碼

能夠看到,軟綁定的foo()能夠手動的將this綁定到obj2或者obj3,但若是應用默認綁定則將this綁定到obj1上。

  1. this詞法

此前的4條規則使用與大部分函數,但在ES6中的箭頭函數卻不適用,由於箭頭函數不是function關鍵字定義的,而是使用被稱爲「胖箭頭」的操做符 => 定義的。箭頭函數不使用this的四種標準規則,而是根據外層的做用域決定this的。因此叫作詞法

function foo () {
        // 返回一個箭頭函數
        return (a) => {
            // this繼承自foo()
            console.log(this.a)
        }
    }
    
    var obj1 = {
        a: 2
    }
    
    var obj2 = {
        a: 3
    }
    
    var bar = foo.call(obj1)
    bar.call(obj2)  // 2
複製代碼

foo內部建立的箭頭函數會捕獲調用時foo()的this,因爲foo()的this是綁定到obj1上的,bar(只是引用箭頭函數)的this也會綁定到obj1上,箭頭函數的綁定沒法修改。相似於

function foo () {
        var self = this
        setTimeout(function () {
            console.log(self.a)
        }, 100)
    }
複製代碼

關於this的理解就說這麼多,歡迎指正和交流。

相關文章
相關標籤/搜索