完整梳理this指向,看完保證秒懂

前言

this做爲JS語言中的關鍵字,其複雜的指向每每是初學者混淆的地方,同時也是面試官常常詢問的考點之一,理解好JS中的this指向,才能算是邁入了JS這門語言的門檻,本文將梳理出this指向的不一樣情景,以及如何更好、更準確的判斷出this指向,讓面試的時候再也不爲this指向頭疼。git

理解好this指向的關鍵要素

在ES5中,判斷this指向的關鍵是在於這句話,this永遠指向調用它的那個對象,想要理解本句話就只有一個重要的點,是誰調用了this,只要從這個點出發,找到這個,那麼就能輕車熟路的判斷this的指向。es6

this的調用

This 被分爲三種狀況:全局對象、當前對象或者任意對象;判斷處於哪一種狀況,徹底取決於函數的調用方式github

從這句話能夠看出,this調用的指向,其實取決於函數的調用方式,也就是在運行時,哪一個函數內部使用了this,那麼this就指向調用此函數的對象。這樣就很容易理解this指向了,看到判斷this指向問題的第一反應,去找使用了this這個函數被誰調用就完事了。如,A函數中使用了this,那麼就去找哪一個對象調用了A函數。面試

麻煩就麻煩在,如何去找到哪一個對象調用了使用this的函數,由於函數的調用在JS中是能夠經過多種方式的。數組

  1. 做爲普通函數調用
  2. 做爲對象方法調用
  3. 使用 applycall 調用
  4. 做爲構造函數調用

那麼問題就變成了只要判斷函數被哪一個對象調用就好了。由此分析下四種不一樣的函數調用狀況(針對ES5)瀏覽器

做爲普通函數調用

var a = 'arzh';
function thisName() {
    console.log(this.a);
}
thisName(); //'arzh'
console.log(window);
複製代碼

咱們拿這個thisName函數分析一下,做爲普通函數調用時,掛載在全局,可輕易判斷出函數被window調用了,那麼就能夠判斷出this指向了window,this.a就至關於window.a,由於在ES5中定義的全局變量會變成window上的屬性 bash

因此 window.a === this.a === arzh;

若是把函數改爲這樣app

var a = 'arzh';
function thisName() {
    var a = 'arzh1'
    console.log(this.a);
}
thisName(); //'arzh'
複製代碼

這個也好理解,由於this始終都是指向window,那麼windowa變量一直都是'arzh',由於thisName函數中的a變量不會覆蓋了window上的a變量。函數

咱們再把函數變一下oop

var a = 'arzh';
function thisName() {
    a = 'arzh1'
    console.log(this.a);
}
thisName(); //'arzh1'
複製代碼

這個如何理解呢,同理這個時候是window調用了thisName函數,this仍是始終指向window,可是thisName函數中a變量沒有加var,就被默認爲全局的變量,覆蓋了上一次的a變量,因此this.a就變成了最後一次定義a變量的字符串,也就是'arzh1';

做爲對象方法調用

做爲對象方法調用,顧名思義,就是對象調用了這個函數(方法),那麼根據上面的分析,誰調用了使用this的函數,this就指向誰,可知this確定就指向了此對象。用代碼看一下

var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : function() {
        console.log(this.a)
    }
}
b.fn(); //'arzh1'
複製代碼

這個就很容易理解,fn做爲b對象的方法調用,即b對象調用了fn函數,那麼this指向了b對象,因此this.a === b.a === 'arzh1'

修改一下函數,以下:

var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : function() {
        console.log(this.a)
    }
}
window.b.fn(); //'arzh1'
複製代碼

window調用了b對象,b對象調用了fn方法,本質上,調用fn方法的仍是b對象,因此this一直指向b對象,也即輸出了'arzh1',若是將b對象中的a屬性去掉,那麼理論上就應該輸出undefined,由於this仍是在b對象上,b中並無a屬性。代碼以下:

var a = 'arzh';
var b = {
    //a : 'arzh1',
    fn : function() {
        console.log(this.a)
    }
}
window.b.fn(); //undefined
複製代碼

咱們繼續往下看

var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : function() {
        console.log(this.a)
    }
}
var fn1 = b.fn; //fn並無被調用
fn1(); //'arzh'
複製代碼

這種狀況也是同樣的,b對象將fn方法賦值給fn1變量,那麼此時fn方法並無被任何人調用,只是單純的賦值。執行fn1方法以後,fn方法纔算真正被調用,那麼此時fn1掛載在全局window上,也就是window對象去調用了fn1方法,因此fn1的指向就變成了window,也就輸出了'arzh';

那麼若是是這種狀況呢

var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : function(){
        setTimeout(function() {
            console.log(this.a)
        },100)
    }
}
b.fn(); //arzh
複製代碼

從上述中能夠看到b.fn()裏面執行了setTimeout函數,setTimeout內的this是指向了window對象,這是因爲setTimeout()調用的代碼運行在與所在函數徹底分離的執行環境上。熟知EventLoop的人員應該明白,setTimeout函數其實調用的是瀏覽器提供的其餘線程,當JS主線程走完以後,會調用任務隊列的函數,也便是setTimeout函數,此時setTimeout是在window上調用的,那麼this天然指向了window

使用 apply 或 call 調用

applycall函數均可以用來更改this指向,具體的用法能夠參考applycall.由於applycall函數在使用上,基本只有傳入參數不一樣的區別(apply第二個參數是數組,call第二個參數不爲數組),本文拿call來說解。咱們直接看代碼

var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : function() {
        console.log(this.a)
    }
}
var c = {
    a : 'arzh2'
}
b.fn(); //arzh1
b.fn.call(window); //arzh
b.fn.call(c); //arzh2
複製代碼

從代碼能夠看出,call能夠直接把this指向爲傳入的第一個參數。咱們這裏只是經過結果來得出call能夠改變this指向,那麼問題來了,call函數是怎麼改變this指向的呢,咱們能夠看一下call的簡單實現

Function.prototype.call = function(context) {
    // 這裏的this是指調用call的函數
    // b.fn.call => this就是fn,上述已經有相似的例子
    context.fn = this;
    context.fn();
    delete context.fn;
}
複製代碼

看到這裏,應該就能明白call是如何改變this指向了吧,也是借用了做爲對象方法調用的原理,咱們只要把傳入call的第一個參數當成一個對象, 而後直接調用方法,那麼this天然就指向了這個對象了。咱們拿上述b.fn.call(window)來講明一下

  1. context就是傳入的window
  2. window.fn = this = window.fn
  3. 執行context.fn => window.fn() => window.fn()

做爲構造函數調用

JavaScript 中的構造函數也很特殊,構造函數,其實就是經過這個函數生成一個新對象(object),這時候的 this 就會指向這個新對象;若是不使用 new 調用,則和普通函數同樣

咱們知道在JS中普通函數能夠直接當構造函數使用,使用new來生成新的對象

function Name(b) {
    this.b = b;
    console.log(this)
}
var o = new Name('arzh');
//Name {b: "arzh"}

function Name(b) {
    this.b = b;
    console.log(this)
}
Name('arzh')
//Window
複製代碼

能夠看出若是是經過new生成的對象,this是指向已經生成的對象的。

咱們來分析下具體緣由:這裏咱們引用一下冴羽大大的new實現,具體的實現過程能夠看這裏

function objectFactory() {
    //新建立一個對象
    var obj = new Object(),
    //獲取函數參數的第一個
    Constructor = [].shift.call(arguments);
    //原型關聯
    obj.__proto__ = Constructor.prototype;
    //執行第一個參數的函數
    Constructor.apply(obj, arguments);
    //返回建立成功的對象
    return obj;
};
複製代碼

這裏主要關注一下第四步,new內部構造對象的時候,使用了apply函數,apply函數的一個參數就是咱們須要生成的對象obj,根據上述的使用 apply 或 call 調用,咱們也就不難理解使用new爲何this會指向新生成對象了。

箭頭函數的this指向(ES6)

es5中,this的指向能夠簡單的歸結爲一句話:this指向函數的調用者。那麼在ES6中其實這句話在箭頭函數下是不成立的。咱們來看看這個例子

var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : function() {
        console.log(this.a)
    }
}
b.fn(); //'arzh1'
複製代碼
var a = 'arzh';
var b = {
    a : 'arzh1',
    fn : () => {
        console.log(this.a)
    }
}
b.fn(); //'arzh'
複製代碼

經過這兩個例子,咱們能夠很明顯的發現,在箭頭函數中的this指向,跟在普通函數中的this指向是不一樣的。

函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象

因此能夠這麼理解,在ES6this的指向是固定的,this綁定定義時所在的做用域,而不是指向運行時所在的做用域

上述例子中,箭頭函數輸出 'arzh' 是由於對象不構成單獨的做用域,致使fn箭頭函數定義時的做用域就是全局做用域

this指向的固定化,並非由於箭頭函數內部有綁定this的機制,實際緣由是箭頭函數根本沒有本身的this,致使內部的this就是外層代碼塊的this。正是由於它沒有this,因此也就不能用做構造函數,因此固然也就不能用call()、apply()、bind()這些方法去改變this的指向

結語

其實this指向不難,比較麻煩的場景,只要用心去肯定哪一個對象調用了函數,就能很快肯定this的指向,但願看了這篇文章,對你之後判斷this指向有必定的用處

參考文章

[1] 阮一峯博客es6教程
[2] 冴羽博客new模擬實現

相關文章
相關標籤/搜索