this 其實很簡單

此文主要總結於《你不知道的JavaScript 上卷》,雖然講解this的文章已經爛大街了,可是依舊但願,這篇文章能夠幫助到那些仍是對this有些疑惑的哥們javascript

前言

this關鍵字是JavaScript中最複雜的機制之一,它不是一個特殊的關鍵字,被自動定義在全部的函數做用域中。php

老規矩,咱們直接看例子:前端

function identify(){
    console.log(this.name)
    return this.name.toUpperCase();
}

function speak() {
  var gretting = 'Hello I am '+identify.call(this)
  console.log(gretting);
}

var me = {
    name:'Neal'
}

var you = {
    name:'Nealyang'
}
identify.call(me);
identify.call(you);

speak.call(me);
speak.call(you);複製代碼

關於運行結果你們能夠自行運行查看,若是對於this是如何工做的這裏咱們仍是存在疑惑,那麼別急,咱們後面固然會繼續深刻探討下,這裏,先說下關於this
的一些誤解的地方java

關於this的誤解

this 值得是它本身

一般新手都會認爲this就是指向函數自己,至於爲何在函數中引用他本身呢,可能就是由於遞歸這種狀況的存在吧。可是這裏,我想說,this並非指向函數自己的es6

function foo(num) {
  console.log("foo:"+num);
  this.count++;
}

foo.count = 0;

for(var i = 0;i<10;i++){
    foo(i);
}

console.log(foo.count);複製代碼

經過運行上面的代碼咱們能夠看到,foo函數的確是被調用了十次,可是this.count彷佛並無加到foo.count上。也就是說,函數中的this.count並非foo.count。面試

因此,這裏咱們必定要記住一個,就是函數中的this並非指向函數自己的。上面的代碼修改以下:bash

function foo(num) {
  console.log("foo:"+num);
  this.count++;
}

foo.count = 0;

for(var i = 0;i<10;i++){
    foo.call(foo,i);
}

console.log(foo.count);複製代碼

運行如上代碼,此時咱們就能夠看到foo函數中的count的確已經變成10了微信

this值得是他的做用域

另外一種對this的誤解是它不知怎麼的指向函數的做用域,其實從某種意義上來講他是正確的,可是從另外一種意義上來講,這的確是一種誤解。app

明確的說,this不會以任何方式指向函數的詞法做用域,做用域好像是一個將全部可用標識符做爲屬性的對象,這從內部來講他是對的,可是JavaScript代碼不能訪問這個做用域「對象」,由於它是引擎內部的實現。ide

function foo() {
    var a = 2;
    this.bar();
}

function bar() {
    console.log( this.a );
}

foo(); //undefined複製代碼

上面的代碼不止一處錯誤,這裏不作討論,僅僅用於看代碼,首先,視圖this.bar()來視圖訪問bar函數,的確他作到了。雖然只是碰巧而已。然而,寫下這段代碼的開發者視圖使用this在foo和bar的詞法做用域中創建一座橋,是的bar能夠訪問foo內部變量做用域a。固然,這是不可能的,不可能使用this引用在詞法做用域中查找東西。

什麼是this

因此說了這麼coder對this的誤解,那麼究竟什麼是this呢。記住,this不是在編寫時候綁定的,而是在運行時候綁定的上下文執行環境。this綁定和函數申明無關,反而和函數被調用的方式有關係。

當一個函數被調用的時候,會創建一個活動記錄,也成爲執行環境。這個記錄包含函數是從何處(call-stack)被調用的,函數是 如何 被調用的,被傳遞了什麼參數等信息。這個記錄的屬性之一,就是在函數執行期間將被使用的this引用。

完全明白this到底值得什麼鬼

調用點

爲了完全弄明白this的指向問題,咱們還必須明白什麼是調用點,即一個函數被調用的位置。考慮調用棧(即便咱們到達當前執行位置而被d調用的全部方法堆棧)是很是重要的,咱們關心的調用點就是當前執行函數的以前的調用

function baz() {
    // 調用棧是: `baz`
    // 咱們的調用點是global scope(全局做用域)

    console.log( "baz" );
    bar(); // <-- `bar`的調用點
}

function bar() {
    // 調用棧是: `baz` -> `bar`
    // 咱們的調用點位於`baz`

    console.log( "bar" );
    foo(); // <-- `foo`的call-site
}

function foo() {
    // 調用棧是: `baz` -> `bar` -> `foo`
    // 咱們的調用點位於`bar`

    console.log( "foo" );
}

baz(); // <-- `baz`的調用點複製代碼

上面代碼你們簡單感覺下什麼是調用棧和調用點,比較簡單的東西。

來點規則,有規可尋

咱們必須考察調用點,來判斷下面即將要說的四中規則哪種適用。先獨立解釋下四中規則的每一種,而後再來講明下若是多種規則適用調用點時他們的優先級。

默認綁定

所謂的默認綁定,就是獨立函數的調用形式。

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

var a = 2;

foo(); // 2複製代碼

爲何會是2呢,由於在調用foo的時候,JavaScript對this實施了默認綁定,因此this就指向了全局對象。

咱們怎麼知道這裏適用 默認綁定 ?咱們考察調用點來看看foo()是如何被調用的。在咱們的代碼段中,foo()是被一個直白的,毫無修飾的函數引用調用的。沒有其餘的咱們將要展現的規則適用於這裏,因此 默認綁定 在這裏適用。

須要注意的是,對於嚴格模式來講,默認綁定全局對象是不合法的,this被置爲undefined。可是一個很微妙的事情是,即使是全部的this綁定規則都是基於調用點的,若是foo的內容沒有嚴格模式下,默認綁定也是合法的。

隱含綁定

調用點是否有一個環境對象,也成爲擁有者和容器對象。

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

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

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

foo被申明,而後被obj添加到其屬性上,不管foo()是否一開始就在obj上被聲明,仍是後來做爲引用添加(如上面代碼所示),都是這個 函數 被obj所「擁有」或「包含」。

這裏須要注意的是,只有對象屬性引用鏈的最後一層才影響調用點

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

var obj2 = {
    a: 42,
    foo: foo
};

var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42複製代碼
隱含綁定丟死

this綁定最讓人頭疼的地方就是隱含綁定丟失了他的綁定,其實明確了調用位置,這個也不是難點。直接看代碼

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

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

var bar = obj.foo; // 函數引用!

var a = "oops, global"; // `a`也是一個全局對象的屬性

bar(); // "oops, global"複製代碼

因此如上的調用模式,咱們又退回到了默認綁定模式。

還能hold住,那麼接着看代碼:

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

function doFoo(fn) {
    // `fn` 只不過`foo`的另外一個引用

    fn(); // <-- 調用點!
}

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

var a = "oops, global"; // `a`也是一個全局對象的屬性

doFoo( obj.foo ); // "oops, global"複製代碼

參數傳遞,僅僅是一種隱含的賦值,並且由於咱們是傳遞一個函數,他是一個隱含的引用賦值,因此最終結果和咱們前一段代碼同樣。

因此,在回調函數中丟失this綁定是一件很常見的事情,可是還有另外一種狀況,接受咱們回調的函數故意改變this的值。那些很受歡迎的事件處理JavaScript包就十分喜歡強制你的回調的this指向觸發事件的DOM元素。

無論哪種意外改變this的方式,你都不能真正地控制你的回調函數引用將如何被執行,因此你(還)沒有辦法控制調用點給你一個故意的綁定。咱們很快就會看到一個方法,經過 固定 this來解決這個問題。

如上,咱們必定要清除的是引用和調用。記住,找this,咱們只看調用,別被引用所迷惑

明確綁定

在JavaScript中,咱們能夠強制制定一個函數在運行時候的this值。是的,call和apply,他們的做用就是擴充函數賴以生存的做用域。

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

var obj = {
    a: 2
};

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

上面代碼,咱們使用foo,強制將foo的this指定爲obj

若是你傳遞一個簡單原始類型值(string,boolean,或 number類型)做爲this綁定,那麼這個原始類型值會被包裝在它的對象類型中(分別是new String(..),new Boolean(..),或new Number(..))。這一般稱爲「boxing(封箱)」。

可是,單獨的依靠明確綁定仍然不能爲咱們先前提到的問題,提供很好的解決方案,也就是函數丟失本身本來的this綁定。

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

var obj = {
    a: 2
};

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

bar(); // 2
setTimeout( bar, 100 ); // 2

// `bar`將`foo`的`this`硬綁定到`obj`
// 因此它不能夠被覆蓋
bar.call( window ); // 2複製代碼

咱們建立了一個函數bar(),在它的內部手動調用foo.call(obj),由此強制this綁定到obj並調用foo。不管你事後怎樣調用函數bar,它老是手動使用obj調用foo。這種綁定即明確又堅決,因此咱們稱之爲 硬綁定(hard binding)

new 綁定

這個比較簡單,當函數前面加入new關鍵字調用的時候,其實就是當作構造函數調用的。其內部其實完成了以下事情:

  • 一個新的對象會被建立
  • 這個新建立的對象會被接入原型鏈
  • 這個新建立的對象會被設置爲函數調用的this綁定
  • 除非函數返回一個他本身的其餘對象,這個被new調用的函數將自動返回一個新建立的對象

總結性來一波

  • 函數是否在new中調用,若是是的話this綁定的是新建立的對象
    var bar = new Foo();複製代碼
  • 函數是否經過call、apply或者其餘硬性調用,若是是的話,this綁定的是指定的對象
    var bar = foo.call(obj);複製代碼
  • 函數是否在某一個上下文對象中調用,若是是的話,this綁定的是那個上下文對象
    var bar = obj.foo();複製代碼
  • 若是都不是的話,使用默認綁定,若是在嚴格模式下,就綁定到undefined,注意這裏是方法裏面的嚴格聲明。不然綁定到全局對象
    var bar = foo();複製代碼

綁定例外

第一種狀況就是將null和undefined傳給call、apply、bind等函數,而後此時this採用的綁定規則是默認綁定

第二種狀況這裏舉個例子,也是面試中經常會出現的例子

function foo() {
  console.log(this.a);
}
var a = 2;
var o = {
    a:3,
    foo:foo
}
var p = {a:4};
(p.foo = o.foo)();複製代碼

如上調用,其實foo採用的也是默認綁定,這裏咱們須要知道的是,p.foo = o.foo的返回值是目標函數的引用,因此最後一句其實就是foo()

es6中的箭頭函數

es6中的箭頭函數比較簡單,因爲箭頭函數並非function關鍵字定義的,因此箭頭函數不適用this的這四中規則,而是根據外層函數或者全局做用域來決定this

function foo() {
  // 返回一個arrow function
    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, 不是3!複製代碼

這裏foo內部建立的箭頭函數會自動獲取foo的this。

來一道經典面試題吧

  • 第一題
    var a=10;
    var foo={
    a:20,
    bar:function(){
        var a=30;
        console.log(this)
        return this.a;
      }
    };
    foo.bar()
    (foo.bar)()
    (foo.bar=foo.bar)()
    (foo.bar,foo.bar)()複製代碼
  • 第二題

    function t(){
    this.x=2;
    }
    t();
    console.log(window.x);複製代碼
  • 第三題
    ```javascript
    var obj = {
    x: 1,
    y: 2,
    t: function() {
    console.log(this.x)
    }
    }
    obj.t();

var dog={x:11};
dog.t=obj.t;
dog.t();

show=function(){
console.log('show'+this.x);

}

dog.t=show;
dog.t();

- 第四題
```javascript
name = 'this is window';
var obj1 = {
name: 'php',
t: function() {
console.log(this.name)
}
};
var dog1 = {
name: 'huzi'
};

obj1.t();

dog1.t = obj1.t;

var tmp = dog1.t;
tmp(); //this原本指向window

(dog1.t = obj1.t)();
dog1.t.call(obj1);複製代碼
  • 第五題
var number=2;
var obj={
number:4,
/*匿名函數自調*/
fn1:(function(){
var number;
this.number*=2;//4

number=number*2;//NaN
number=3;
return function(){
var num=this.number;
this.number*=2;//6
console.log(num);
number*=3;//9
alert(number);
}
})(),

db2:function(){
this.number*=2;
}
}

var fn1=obj.fn1;

alert(number);

fn1();

obj.fn1();

alert(window.number);

alert(obj.number);複製代碼

交流

掃碼關注個人我的微信公衆號,分享更多原創文章。點擊交流學習加我微信、qq羣。一塊兒學習,一塊兒進步。共同交流上面的題目吧

歡迎兄弟們加入:

Node.js技術交流羣:209530601

React技術棧:398240621

前端技術雜談:604953717 (新建)

相關文章
相關標籤/搜索