解析this關鍵字

解析this關鍵字

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


請先回答第一個問題,如何準確地判斷this指向的是什麼?面試


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

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)
        }
    })()
}

爲何要學習this

  1. this使用頻率很高
  2. 工做中,濫用this
  3. 合理的使用this
  4. 面試高頻問題

this是什麼

<br> this不是指向自身!this就是一個指針,指向調用函數的對象。 閉包

<br><br> 爲了可以一眼看出this指向的是什麼,首先須要知道this的綁定規則有哪些:app

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

默認綁定

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

function sayHi () {
    console.log('Hello,',this.name);
}

var name = 'make'

sayHi();

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

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

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


隱式綁定

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

function sayHi () {
    console.log('Hello,',this.name);
}

var person = {
    name: 'make',
    sayHi: sayHi
}

var name = 'kang';

person.sayHi();

打印的結果是Hello,make

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

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

function sayHi () {
    console.log('Hello,',this.name);
}

var person2 = {
    name: 'name2',
    sayHi: sayHi
}

var person1 = {
    name: 'name1',
    friend: person2
}

person1.friend.sayHi();

結果是:Hello,name2。

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

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

function sayHi () {
    console.log('Hello,',this.name);
}

var person = {
    name: 'name1',
    sayHi: sayHi
}

var name = 'name2';
var Hi = person.sayHi;
Hi();

結果是:Hello,name2

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

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

function sayHi () {
    console.log('Hello,',this.name);
}

var person1 = {
    name: 'name1',
    sayHi: function () {
        setimeout(function(){
            console.log('Hello,',this.name);
        }
    }
}

var person2 = {
    name: 'name2',
    sayHi:sayHi
}

var name = 'name3';
person1.sayHi();
setTimeout(person2.sayHi,100);
setTimeout(function(){
    person2.sayHi();
},200)

結果爲:

Hello,name3
Hello.name3
Hello,name2
  • 第一條輸出:setTimeout的回調函數中,this使用的是默認綁定,非嚴格模式下,執行的是全局對象。
  • 第二條輸出:setTimeout(fn.delay){ fn(); },至關因而將person2.sayHi賦值給了一個變量,最後執行了變量,這個時候,sayHi中的this顯然和person2就沒有關係了
  • 第三條輸出:雖然也是在setTimeout回調中,可是能夠看出,這是執行的person2.sayHi(),使用的是隱式綁定,所以此次this指向的是person2,跟當前做用域沒有關係。

顯式綁定

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

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

function sayHi () {
    console.log('Hello',this.name);
}

var person = {
    name: 'name1',
    sayHi: sayHi
}

var name = 'name'
var Hi = person.sayHi;
Hi.call(person);    // Hi.apply(person)

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

那麼,使用了硬綁定,是否是意味着不會出現隱式綁定所遇到的綁定丟失呢?答案是:並非!!!

function sayHi () {
    console.log('Hello',this.name);
}

var person = {
    name: 'name1',
    sayHi: sayHi
}

var name = 'name';
var Hi = function(fn){
    fn();
}
Hi.call(person,person.sayHi);

輸出的結果是Hello,name.緣由很簡單,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 = 'name1',
    sayHi: sayHi
}

var name = 'name';
var Hi = function(fn) {
    fn.call(this);
}
Hi.call(person,person.sayHi);

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


new綁定

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


使用 new 來調用函數,會自動執行下面的操做:
  1. 建立一個新對象;
  2. 將構造函數的做用域賦值給新對象,即this指向的這個新對象;
  3. 執行構造函數中的代碼;
  4. 返回新對象。

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

function sayHi(name) {
    this.name = name;
}

var Hi = new sayHi('make');
console.log('Hello,',Hi.name);

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


綁定優先級

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

顯然,須要瞭解那一種綁定方式的優先級更高嗎,這四種綁定的優先級爲:

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


綁定例外

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

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

function sayHi() {
    console.log('Hello,',this.name);
}

var person = {
    name: 'name',
    sayhi: sayHi
}

var name1 = 'name1';
var Hi = function(fn) {
    fn();
}
Hi.call(null,parson.sayHi);

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


箭頭函數

箭頭函數是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: function() {
        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

總結

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關鍵字),隱式綁定有沒有?很顯然沒有。若是沒有XXX.fun(),那麼確定沒有應用隱式綁定,因此這裏應用的就是默認綁定了,非嚴格模式下this綁定到了window上(瀏覽器執行環境)。【這裏很容易被迷惑的就是覺得this指向的是obj,必定要注意,除非是箭頭函數,不然this跟詞法做用域是兩回事,必定要牢記在心】
window.number *= 2;     //window.nuumber 的值是 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
thia.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
相關文章
相關標籤/搜索