嗨,你真的懂this嗎?

this關鍵字是JavaScript中最複雜的機制之一,是一個特別的關鍵字,被自動定義在全部函數的做用域中,可是相信不少JvaScript開發者並非很是清楚它究竟指向的是什麼。據說你很懂this,是真的嗎?javascript

更多文章可戳: github.com/YvetteLau/B…java

請先回答第一個問題:如何準確判斷this指向的是什麼?【面試的高頻問題】node

【圖片來源於網絡,侵刪】git

再看一道題,控制檯打印出來的值是什麼?【瀏覽器運行環境】es6

var number = 5;
var obj = {
    number: 3,
    fn1: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })()
}
var fn1 = obj.fn1;
fn1.call(null);
obj.fn1();
console.log(window.number);
複製代碼

若是你思考出來的結果,與在瀏覽中執行結果相同,而且每一步的依據都很是清楚,那麼,你能夠選擇繼續往下閱讀,或者關閉本網頁,愉快得去玩耍。若是你是連猜帶蒙的,或者對本身的答案並不那麼肯定,那麼請繼續往下閱讀。github

畢竟花一兩個小時的時間,把this完全搞明白,是一件很值得事情,不是嗎?面試

本文將細緻得講解this的綁定規則,並在最後剖析前文兩道題。shell

爲何要學習this?

首先,咱們爲何要學習this?瀏覽器

  1. this使用頻率很高,若是咱們不懂this,那麼在看別人的代碼或者是源碼的時候,就會很吃力。
  2. 工做中,濫用this,卻沒明白this指向的是什麼,而致使出現問題,可是本身殊不知道哪裏出問題了。【在公司,我至少幫10個以上的開發人員處理過這個問題】
  3. 合理的使用this,可讓咱們寫出簡潔且複用性高的代碼。
  4. 面試的高頻問題,回答很差,抱歉,出門右拐,不送。

無論出於什麼目的,咱們都須要把this這個知識點整的明明白白的。網絡

OK,Let's go!

this是什麼?

言歸正傳,this是什麼?首先記住this不是指向自身!this 就是一個指針,指向調用函數的對象。這句話咱們都知道,可是不少時候,咱們未必可以準確判斷出this究竟指向的是什麼?這就好像咱們聽過不少道理 卻依然過很差這一輩子。今天我們不探討如何過好一輩子的問題,可是呢,但願閱讀完下面的內容以後,你可以一眼就看出this指向的是什麼。

爲了可以一眼看出this指向的是什麼,咱們首先須要知道this的綁定規則有哪些?

  1. 默認綁定
  2. 隱式綁定
  3. 硬綁定
  4. new綁定

上面的名詞,你也許聽過,也許沒聽過,可是今天以後,請緊緊記住。咱們將依次來進行解析。

默認綁定

默認綁定,在不能應用其它綁定規則時使用的默認規則,一般是獨立函數調用。

function sayHi(){
    console.log('Hello,', this.name);
}
var name = 'YvetteLau';
sayHi();
複製代碼

在調用Hi()時,應用了默認綁定,this指向全局對象(非嚴格模式下),嚴格模式下,this指向undefined,undefined上沒有this對象,會拋出錯誤。

上面的代碼,若是在瀏覽器環境中運行,那麼結果就是 Hello,YvetteLau

可是若是在node環境中運行,結果就是 Hello,undefined.這是由於node中name並非掛在全局對象上的。

本文中,如不特殊說明,默認爲瀏覽器環境執行結果。

隱式綁定

函數的調用是在某個對象上觸發的,即調用位置上存在上下文對象。典型的形式爲 XXX.fun().咱們來看一段代碼:

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
person.sayHi();
複製代碼

打印的結果是 Hello,YvetteLau.

sayHi函數聲明在外部,嚴格來講並不屬於person,可是在調用sayHi時,調用位置會使用person的上下文來引用函數,隱式綁定會把函數調用中的this(即此例sayHi函數中的this)綁定到這個上下文對象(即此例中的person)

須要注意的是:對象屬性鏈中只有最後一層會影響到調用位置。

function sayHi(){
    console.log('Hello,', this.name);
}
var person2 = {
    name: 'Christina',
    sayHi: sayHi
}
var person1 = {
    name: 'YvetteLau',
    friend: person2
}
person1.friend.sayHi();
複製代碼

結果是:Hello, Christina.

由於只有最後一層會肯定this指向的是什麼,無論有多少層,在判斷this的時候,咱們只關注最後一層,即此處的friend。

隱式綁定有一個大陷阱,綁定很容易丟失(或者說容易給咱們形成誤導,咱們覺得this指向的是什麼,可是實際上並不是如此).

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
var Hi = person.sayHi;
Hi();
複製代碼

結果是: Hello,Wiliam.

這是爲何呢,Hi直接指向了sayHi的引用,在調用的時候,跟person沒有半毛錢的關係,針對此類問題,我建議你們只需緊緊記住這個格式:XXX.fn();fn()前若是什麼都沒有,那麼確定不是隱式綁定。

除了上面這種丟失以外,隱式綁定的丟失是發生在回調函數中(事件回調也是其中一種),咱們來看下面一個例子:

function sayHi(){
    console.log('Hello,', this.name);
}
var person1 = {
    name: 'YvetteLau',
    sayHi: function(){
        setTimeout(function(){
            console.log('Hello,',this.name);
        })
    }
}
var person2 = {
    name: 'Christina',
    sayHi: sayHi
}
var name='Wiliam';
person1.sayHi();
setTimeout(person2.sayHi,100);
setTimeout(function(){
    person2.sayHi();
},200);

複製代碼

結果爲:

Hello, Wiliam
Hello, Wiliam
Hello, Christina
複製代碼
  • 第一條輸出很容易理解,setTimeout的回調函數中,this使用的是默認綁定,非嚴格模式下,執行的是全局對象

  • 第二條輸出是否是有點迷惑了?說好XXX.fun()的時候,fun中的this指向的是XXX呢,爲何此次卻不是這樣了!Why?

    其實這裏咱們能夠這樣理解: setTimeout(fn,delay){ fn(); },至關因而將person2.sayHi賦值給了一個變量,最後執行了變量,這個時候,sayHi中的this顯然和person2就沒有關係了。

  • 第三條雖然也是在setTimeout的回調中,可是咱們能夠看出,這是執行的是person2.sayHi()使用的是隱式綁定,所以這是this指向的是person2,跟當前的做用域沒有任何關係。

讀到這裏,也許你已經有點疲倦了,可是答應我,別放棄,好嗎?再堅持一下,就能夠掌握這個知識點了。

顯式綁定

顯式綁定比較好理解,就是經過call,apply,bind的方式,顯式的指定this所指向的對象。(注意:《你不知道的Javascript》中將bind單獨做爲了硬綁定講解了)

call,apply和bind的第一個參數,就是對應函數的this所指向的對象。call和apply的做用同樣,只是傳參方式不一樣。call和apply都會執行對應的函數,而bind方法不會。

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
var Hi = person.sayHi;
Hi.call(person); //Hi.apply(person)
複製代碼

輸出的結果爲: Hello, YvetteLau. 由於使用硬綁定明確將this綁定在了person上。

那麼,使用了硬綁定,是否是意味着不會出現隱式綁定所遇到的綁定丟失呢?顯然不是這樣的,不信,繼續往下看。

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
    fn();
}
Hi.call(person, person.sayHi); 
複製代碼

輸出的結果是 Hello, Wiliam. 緣由很簡單,Hi.call(person, person.sayHi)的確是將this綁定到Hi中的this了。可是在執行fn的時候,至關於直接調用了sayHi方法(記住: person.sayHi已經被賦值給fn了,隱式綁定也丟了),沒有指定this的值,對應的是默認綁定。

如今,咱們但願綁定不會丟失,要怎麼作?很簡單,調用fn的時候,也給它硬綁定。

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
    fn.call(this);
}
Hi.call(person, person.sayHi);
複製代碼

此時,輸出的結果爲: Hello, YvetteLau,由於person被綁定到Hi函數中的this上,fn又將這個對象綁定給了sayHi的函數。這時,sayHi中的this指向的就是person對象。

至此,革命已經快勝利了,咱們來看最後一種綁定規則: new 綁定。

new 綁定

javaScript和C++不同,並無類,在javaScript中,構造函數只是使用new操做符時被調用的函數,這些函數和普通的函數並無什麼不一樣,它不屬於某個類,也不可能實例化出一個類。任何一個函數均可以使用new來調用,所以其實並不存在構造函數,而只有對於函數的「構造調用」。

使用new來調用函數,會自動執行下面的操做:

  1. 建立一個空對象,構造函數中的this指向這個空對象
  2. 這個新對象被執行 [[原型]] 鏈接
  3. 執行構造函數方法,屬性和方法被添加到this引用的對象中
  4. 若是構造函數中沒有返回其它對象,那麼返回this,即建立的這個的新對象,不然,返回構造函數中返回的對象。
function _new() {
    let target = {}; //建立的新對象
    //第一個參數是構造函數
    let [constructor, ...args] = [...arguments];
    //執行[[原型]]鏈接;target 是 constructor 的實例
    target.__proto__ = constructor.prototype;
    //執行構造函數,將屬性或方法添加到建立的空對象上
    let result = constructor.apply(target, args);
    if (result && (typeof (result) == "object" || typeof (result) == "function")) {
        //若是構造函數執行的結構返回的是一個對象,那麼返回這個對象
        return result;
    }
    //若是構造函數返回的不是一個對象,返回建立的新對象
    return target;
}
複製代碼

所以,咱們使用new來調用函數的時候,就會新對象綁定到這個函數的this上。

function sayHi(name){
    this.name = name;
	
}
var Hi = new sayHi('Yevtte');
console.log('Hello,', Hi.name);
複製代碼

輸出結果爲 Hello, Yevtte, 緣由是由於在var Hi = new sayHi('Yevtte');這一步,會將sayHi中的this綁定到Hi對象上。

綁定優先級

咱們知道了this有四種綁定規則,可是若是同時應用了多種規則,怎麼辦?

顯然,咱們須要瞭解哪種綁定方式的優先級更高,這四種綁定的優先級爲:

new綁定 > 顯式綁定 > 隱式綁定 > 默認綁定

這個規則時如何獲得的,你們若是有興趣,能夠本身寫個demo去測試,或者記住上面的結論便可。

綁定例外

凡事都有例外,this的規則也是這樣。

若是咱們將null或者是undefined做爲this的綁定對象傳入call、apply或者是bind,這些值在調用時會被忽略,實際應用的是默認綁定規則。

var foo = {
    name: 'Selina'
}
var name = 'Chirs';
function bar() {
    console.log(this.name);
}
bar.call(null); //Chirs 
複製代碼

輸出的結果是 Chirs,由於這時實際應用的是默認綁定規則。

箭頭函數

箭頭函數是ES6中新增的,它和普通函數有一些區別,箭頭函數沒有本身的this,它的this繼承於外層代碼庫中的this。箭頭函數在使用時,須要注意如下幾點:

(1)函數體內的this對象,繼承的是外層代碼塊的this。

(2)不能夠看成構造函數,也就是說,不可使用new命令,不然會拋出一個錯誤。

(3)不可使用arguments對象,該對象在函數體內不存在。若是要用,能夠用 rest 參數代替。

(4)不可使用yield命令,所以箭頭函數不能用做 Generator 函數。

(5)箭頭函數沒有本身的this,因此不能用call()、apply()、bind()這些方法去改變this的指向.

OK,咱們來看看箭頭函數的this是什麼?

var obj = {
    hi: function(){
        console.log(this);
        return ()=>{
            console.log(this);
        }
    },
    sayHi: function(){
        return function() {
            console.log(this);
            return ()=>{
                console.log(this);
            }
        }
    },
    say: ()=>{
        console.log(this);
    }
}
let hi = obj.hi();  //輸出obj對象
hi();               //輸出obj對象
let sayHi = obj.sayHi();
let fun1 = sayHi(); //輸出window
fun1();             //輸出window
obj.say();          //輸出window
複製代碼

那麼這是爲何呢?若是你們說箭頭函數中的this是定義時所在的對象,這樣的結果顯示不是你們預期的,按照這個定義,say中的this應該是obj纔對。

咱們來分析一下上面的執行結果:

  1. obj.hi(); 對應了this的隱式綁定規則,this綁定在obj上,因此輸出obj,很好理解。
  2. hi(); 這一步執行的就是箭頭函數,箭頭函數繼承上一個代碼庫的this,剛剛咱們得出上一層的this是obj,顯然這裏的this就是obj.
  3. 執行sayHi();這一步也很好理解,咱們前面說過這種隱式綁定丟失的狀況,這個時候this執行的是默認綁定,this指向的是全局對象window.
  4. fun1(); 這一步執行的是箭頭函數,若是按照以前的理解,this指向的是箭頭函數定義時所在的對象,那麼這兒顯然是說不通。OK,按照箭頭函數的this是繼承於外層代碼庫的this就很好理解了。外層代碼庫咱們剛剛分析了,this指向的是window,所以這兒的輸出結果是window.
  5. obj.say(); 執行的是箭頭函數,當前的代碼塊obj中是不存在this的,只能往上找,就找到了全局的this,指向的是window.

你說箭頭函數的this是靜態的?

依舊是前面的代碼。咱們來看看箭頭函數中的this真的是靜態的嗎?

我要說:非也

var obj = {
    hi: function(){
        console.log(this);
        return ()=>{
            console.log(this);
        }
    },
    sayHi: function(){
        return function() {
            console.log(this);
            return ()=>{
                console.log(this);
            }
        }
    },
    say: ()=>{
        console.log(this);
    }
}
let sayHi = obj.sayHi();
let fun1 = sayHi(); //輸出window
fun1();             //輸出window

let fun2 = sayHi.bind(obj)();//輸出obj
fun2();                      //輸出obj
複製代碼

能夠看出,fun1和fun2對應的是一樣的箭頭函數,可是this的輸出結果是不同的。

因此,請你們緊緊記住一點: 箭頭函數沒有本身的this,箭頭函數中的this繼承於外層代碼庫中的this.

總結

關於this的規則,至此,就告一段落了,可是想要一眼就能看出this所綁定的對象,還須要不斷的訓練。

咱們來回顧一下,最初的問題。

1. 如何準確判斷this指向的是什麼?

  1. 函數是否在new中調用(new綁定),若是是,那麼this綁定的是新建立的對象。
  2. 函數是否經過call,apply調用,或者使用了bind(即硬綁定),若是是,那麼this綁定的就是指定的對象。
  3. 函數是否在某個上下文對象中調用(隱式綁定),若是是的話,this綁定的是那個上下文對象。通常是obj.foo()
  4. 若是以上都不是,那麼使用默認綁定。若是在嚴格模式下,則綁定到undefined,不然綁定到全局對象。
  5. 若是把null或者undefined做爲this的綁定對象傳入call、apply或者bind,這些值在調用時會被忽略,實際應用的是默認綁定規則。
  6. 若是是箭頭函數,箭頭函數的this繼承的是外層代碼塊的this。

2. 執行過程解析

var number = 5;
var obj = {
    number: 3,
    fn: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);
複製代碼

咱們來分析一下,這段代碼的執行過程。

  1. 在定義obj的時候,fn對應的閉包就執行了,返回其中的函數,執行閉包中代碼時,顯然應用不了new綁定(沒有出現new 關鍵字),硬綁定也沒有(沒有出現call,apply,bind關鍵字),隱式綁定有沒有?很顯然沒有,若是沒有XX.fn(),那麼能夠確定沒有應用隱式綁定,因此這裏應用的就是默認綁定了,非嚴格模式下this綁定到了window上(瀏覽器執行環境)。【這裏很容易被迷惑的就是覺得this指向的是obj,必定要注意,除非是箭頭函數,不然this跟詞法做用域是兩回事,必定要牢記在心】
window.number * = 2; //window.number的值是10(var number定義的全局變量是掛在window上的)

number = number * 2; //number的值是NaN;注意咱們這邊定義了一個number,可是沒有賦值,number的值是undefined;Number(undefined)->NaN

number = 3;          //number的值爲3
複製代碼
  1. myFun.call(null);咱們前面說了,call的第一個參數傳null,調用的是默認綁定;
fn: function(){
    var num = this.number;
    this.number *= 2;
    console.log(num);
    number *= 3;
    console.log(number);
}
複製代碼

執行時:

var num = this.number; //num=10; 此時this指向的是window
this.number * = 2;     //window.number = 20
console.log(num);      //輸出結果爲10
number *= 3;           //number=9; 這個number對應的閉包中的number;閉包中的number的是3
console.log(number);   //輸出的結果是9
複製代碼
  1. obj.fn();應用了隱式綁定,fn中的this對應的是obj.
var num = this.number;//num = 3;此時this指向的是obj
this.number *= 2;     //obj.number = 6;
console.log(num);     //輸出結果爲3;
number *= 3;          //number=27;這個number對應的閉包中的number;閉包中的number的此時是9
console.log(number);  //輸出的結果是27
複製代碼
  1. 最後一步console.log(window.number);輸出的結果是20

所以組中結果爲:

10
9
3
27
20
複製代碼

嚴格模式下結果,你們根據今天所學,本身分析,鞏固一下知識點。

最後,恭喜堅持讀完的小夥伴們,大家成功get到了this這個知識點,可是想要徹底掌握,仍是要多回顧和練習。若是你有不錯的this練習題,歡迎在評論區留言哦,你們一塊兒進步!

謝謝您花費寶貴的時間閱讀本文,若是本文給了您一點幫助或者是啓發,那麼不要吝嗇你的贊和Star哦,您的確定是我前進的最大動力。github.com/YvetteLau/B…

感謝指出,增長參考連接以下:

關注公衆號,加入技術交流羣

相關文章
相關標籤/搜索