this 指向問題

日期:2019 年 9 月 5 日

this 指向問題

介紹

this 指向問題一直是 js 中一個令人頭疼的問題,這幾天得空,復習了一下以前的知識,順便整理了有關 js 中 this 指向的知識點。

this 綁定

很多人對 this 指向一直都存在一個誤區(qū):this 寫在誰里面就指向誰。但其實這是不對的,this 既不指向函數自身,也不指函數的詞法作用域,它實際是在函數被調用時才發(fā)生的綁定,也就是說this具體指向什么,取決于你是怎么調用的函數。


this 存在4種綁定規(guī)則:

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

這 4 種綁定規(guī)則的優(yōu)先級是從低到高的

1、默認綁定
function foo(){
        console.log(this.a);
}
var a = 2;
foo();  // 打印結果?

打印結果:2

因為foo()是直接調用的(獨立函數調用),沒有應用其他的綁定規(guī)則,這里進行了默認綁定,將全局對象( window )綁定this上,所以this.a 就解析成了全局變量中的a,即 2


注意:在嚴格模式下(strict mode),全局對象將無法使用默認綁定( this 指向 undefined ),即執(zhí)行會報 undefined 的錯誤:

function foo(){
        “use strict”;
        console.log(this.a);
}
var a = 2;
foo();  //  Uncaught  TypeError : Cannot read property 'a' of undefined

另外,這幾天正在學 Typescript,剛好也遇到了一些 this 指向的問題,這里也說道說道,看一看下面的幾種情況:

例一:

let a = 233;
let obj1 = {
    a: 666,
    fn: function(){
        console.log(this.a);
    }
}

obj1.fn();    //  輸出: 666

例二:

let a = 233;
let obj2 = {
    a: 888,
    fn: function(){
        console.log("外層:", this.a);
        return function(){
            console.log("內層:", this.a);
        }
    }
}

let ha = obj2.fn();  //  外層: 888
ha();    //  內層: undefined

例三:

var a = 233;
let obj2 = {
    a: 888,
    fn: function(){
        console.log("外層:", this.a);
        return function(){
            console.log("內層:", this.a);
        }
    }
}

let ha = obj2.fn();  //  外層: 888
ha();    //  內層: 233

比較之下我們可以看到,對象的 fn 屬性是一個函數,對象調用這個函數,函數的執(zhí)行上下文就是這個對象,所以外層的 this 指向對象沒有問題;

然后 fn 還返回了一個函數,這個函數里面的 this (內層 this)指向的是 window 而不是對象 obj2;

有人就要問了既然指向 window,為什么例二中內層的輸出不是 233 呢?這個問題問得好,這就涉及到了 ES6 與 ES5 中變量聲明方面的區(qū)別了:

  • ES5聲明變量只有兩種方式:var 和 function
  • ES6有 let、const、import、class 再加上 ES5 的 var、function 共有六種聲明變量的方式
  • 還需要了解頂層對象:瀏覽器環(huán)境中頂層對象是window,Node中是global對象
  • ES5中,頂層對象的屬性等價于全局變量 (敲黑板了啊)
  • ES6中,有所改變:var、function聲明的全局變量,依然是頂層對象的屬性;let、const、class聲明的全局變量不屬于頂層對象的屬性,也就是說ES6開始,全局變量和頂層對象的屬性開始分離、脫鉤

所以ES6非嚴格模式下,與var聲明的全局變量都會成為window的屬性:

a = 1;
console.log(window.a);  // 1
var b = 2;
console.log(window.b);  // 2

而使用let聲明的全局變量,不會成為window的屬性:

let c = 3;
console.log(window.c);  // undefined

關于這方面問題,更多請移步原文:

關于let聲明的變量在window下無法獲取的問題

2、隱式綁定

除了直接對函數進行調用外,有些情況是,函數的調用是在某個對象上觸發(fā)的,即調用位置上存在上下文對象

function foo(){
        console.log(this.a);
}
var a = 2;
var obj = {
        a : 3,
        foo : foo
}
obj.foo();    // ?

輸出結果: 3

這里foo函數被當做引用屬性,被添加到obj對象上。這里的調用過程是這樣的:

獲取obj.foo屬性 -> 根據引用關系找到foo函數,執(zhí)行調用,所以這里對foo的調用存在上下文對象obj,this進行了隱式綁定,即this綁定到了obj上,所以this.a被解析成了obj.a,即 3

當存在多層調用鏈時:

function foo(){
        console.log(this.a);
}
var a = 2;
var obj1 = {
        a : 4,
        foo : foo
}
var obj2 = {
        a : 3,
        obj1 : obj1
}
obj2.obj1.foo();    // ?

輸出結果:4

同樣,我們看下函數的調用過程:

先獲取obj2.obj1 -> 通過引用獲取到obj1對象,再訪問 obj1.foo -> 最后執(zhí)行foo函數調用
這里調用鏈不只一層,存在obj1、obj2兩個對象,那么隱式綁定具體會綁哪個對象。這里原則是獲取最后一層調用的上下文對象,即obj1,所以結果顯然是 4

3、顯示綁定

對于隱式綁定,this值在調用過程中會動態(tài)變化,可是我們就想綁定指定的對象,這時就用到了顯示綁定

顯示綁定主要是通過改變對象的prototype關聯(lián)對象,這里不展開講。具體使用上,可以通過這兩個方法 call() 或 apply() 來實現(xiàn)(大多數函數及自己創(chuàng)建的函數默認都提供這兩個方法)

call 與apply 是同樣的作用,區(qū)別只是其他參數的設置上:

/*apply()方法*/
function.apply(thisObj[, argArray])

/*call()方法*/
function.call(thisObj[, arg1[, arg2[, [,...argN]]]]);

它們各自的定義:

apply:調用一個對象的一個方法,用另一個對象替換當前對象。例如:B.apply(A, arguments);即A對象應用B對象的方法。

call:調用一個對象的一個方法,用另一個對象替換當前對象。例如:B.call(A, args1,args2);即A對象調用B對象的方法。

它們的共同之處:

都“可以用來代替另一個對象調用一個方法,將一個函數的對象上下文從初始的上下文改變?yōu)橛蓆hisObj指定的新對象”。

它們的不同之處:

apply:最多只能有兩個參數——新this對象和一個數組argArray。如果給該方法傳遞多個參數,則把參數都寫進這個數組里面,當然,即使只有一個參數,也要寫進數組里。如果argArray不是一個有效的數組或arguments對象,那么將導致一個TypeError。如果沒有提供argArray和thisObj任何一個參數,那么Global對象將被用作thisObj,并且無法被傳遞任何參數。

call:它可以接受多個參數,第一個參數與apply一樣,后面則是一串參數列表。這個方法主要用在js對象各方法相互調用的時候,使當前this實例指針保持一致,或者在特殊情況下需要改變this指針。如果沒有提供thisObj參數,那么 Global 對象被用作thisObj。 實際上,apply和call的功能是一樣的,只是傳入的參數列表形式不同。

有關 apply 與 call 的比較,詳情請移步原文:

apply() 與 call() 的區(qū)別



OK,回歸正題,我們來看看顯示綁定:

function foo(){
        console.log(this.a);
}
var a = 2;
var obj1 = {
        a : 3,
        foo : foo
}
var obj2 = {
        a : 4,
        obj1 : obj1
}
foo.call(obj1);    // ?
foo.call(obj2);   // ?

結果:

3

4

這里因為顯示的申明了要綁定的對象,所以this就被綁定到了 obj 上,打印的結果自然就是obj1.a 和 obj2.a

4、new 綁定
var a = 2;
function foo(a){
    this.a = a;
}
let bar1 = new foo(3);
console.log(bar1.a); // ?
let bar2 = new foo(4);
console.log(bar2.a); // ?

結果:

3

4

因為每次調用生成的是全新的對象,該對象又會自動綁定到this上,所以答案顯而易見

綁定規(guī)則優(yōu)先級

上面也說過,這里在重復一下。優(yōu)先級是這樣的,以按照下面的順序來進行判斷:

函數是否在new中調用(new綁定)?如果是的話this綁定的是新創(chuàng)建的對象;
函數是否通過call、apply(顯式綁定)或者硬綁定調用?如果是的話,this綁定的是 指定的對象;
函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this綁定的是那個上下文對象;
如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定undefined,否則綁定到全局對象。


規(guī)則例外:

在顯示綁定中,對于 null 和 undefined 的綁定將不會生效

function foo(){
        console.log(this.a);
}
var a = 2;
foo.call(null);  //  2
foo.call(undefined);  //  2

這種情況主要是用在不關心this的具體綁定對象(用來忽略this),而傳入null實際上會進行默認綁定,導致函數中可能會使用到全局變量,與預期不符

所以對于要忽略this的情況,可以傳入一個空對象?,該對象通過Object.create(null)創(chuàng)建。這里不用{}的原因是,?是真正意義上的空對象,它不創(chuàng)建Object.prototype委托,{}和普通對象一樣,有原型鏈委托關系

箭頭函數中的 this

箭頭函數的this指向:

  1. 箭頭函數不會創(chuàng)建自己的this,它只會從自己的作用域鏈的上一層繼承this;
  2. 箭頭函數里的this指向無法被call,apply,bind 改變;
var obj = {
        foo(){
            console.log(this);
        },
        bar : ()=>{
            console.log(this);
        }
}
obj.foo();  //  { foo: foo(), bar: bar() }
obj.bar();  //  window

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容