前端基礎進階(五):全方位解讀this

~

咱們在學習JavaScript的過程當中,因爲對一些概念理解得不是很清楚,可是又想要經過一些方式把它記下來,因而就很容易草率的給這些概念定下一些方便本身記憶的有誤差的結論。javascript

危害比較大的是,有的不許確的結論在網上還廣爲流傳。前端

好比對於this指向的理解中,有這樣一種說法:誰調用它,this就指向誰。在我剛開始學習this的時候,我是很是相信這句話的。由於在一些狀況下,這樣理解也還算說得通。但是我經常會在開發中遇到一些不同的狀況,一個因爲this的錯誤調用,可讓我懵逼一成天。那個時候我也查資料,在羣裏問大神,但是我仍然搞不清楚「我特麼到底錯哪裏了」。其實只是由於我心中有一個不太準確的結論。java

因此,我認爲須要有這樣一篇文章,來幫助你們全方位的解讀this。讓你們對this,有一個正確的,全面的認知。node

在這以前,咱們須要來回顧一下執行上下文。segmentfault

在前面幾篇文章中,我有好幾個地方都提到執行上下文的生命週期,爲了防止你們沒有記住,再次來回顧一下,以下圖。
執行上下文生命週期數組

在執行上下文的建立階段,會分別生成變量對象,創建做用域鏈,肯定this指向。其中變量對象與做用域鏈咱們都已經仔細總結過了,而這裏的關鍵,就是肯定this指向。閉包

首先咱們須要得出一個很是重要必定要牢記於心的結論,this的指向,是在函數被調用的時候肯定的。也就是執行上下文被建立時肯定的。所以,一個函數中的this指向,能夠是很是靈活的。好比下面的例子中,同一個函數因爲調用方式的不一樣,this指向了不同的對象。app

var a = 10;
var obj = {
    a: 20
}

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

fn(); // 10
fn.call(obj); // 20

除此以外,在函數執行過程當中,this一旦被肯定,就不可更改了。函數

var a = 10;
var obj = {
    a: 20
}

function fn () {
    this = obj; // 這句話試圖修改this,運行後會報錯
    console.log(this.a);
}

fn();

1、全局對象中的this 學習

關於全局對象的this,我以前在總結變量對象的時候提到過,它是一個比較特殊的存在。全局環境中的this,指向它自己。所以,這也相對簡單,沒有那麼多複雜的狀況須要考慮。

// 經過this綁定到全局對象
this.a2 = 20;

// 經過聲明綁定到變量對象,但在全局環境中,變量對象就是它自身
var a1 = 10;

// 僅僅只有賦值操做,標識符會隱式綁定到全局對象
a3 = 30;

// 輸出結果會所有符合預期
console.log(a1);
console.log(a2);
console.log(a3);
2、函數中的this

在總結函數中this指向以前,我想咱們有必要經過一些奇怪的例子,來感覺一下函數中this的捉摸不定。

// demo01
var a = 20;
function fn() {
    console.log(this.a);
}
fn();
// demo02
var a = 20;
function fn() {
    function foo() {
        console.log(this.a);
    }
    foo();
}
fn();
// demo03
var a = 20;
var obj = {
    a: 10,
    c: this.a + 20,
    fn: function () {
        return this.a;
    }
}

console.log(obj.c);
console.log(obj.fn());

這幾個例子須要花點時間仔細感覺一下,若是你暫時沒想明白怎麼回事,也不用着急,咱們一點一點來分析。

分析以前,咱們先直接了當拋出結論。

在一個函數上下文中,this由調用者提供,由調用函數的方式來決定。若是調用者函數,被某一個對象所擁有,那麼該函數在調用時,內部的this指向該對象。若是函數獨立調用,那麼該函數內部的this,則指向undefined。可是在非嚴格模式中,當this指向undefined時,它會被自動指向全局對象。

從結論中咱們能夠看出,想要準確肯定this指向,找到函數的調用者以及區分他是不是獨立調用就變得十分關鍵。

// 爲了可以準確判斷,咱們在函數內部使用嚴格模式,由於非嚴格模式會自動指向全局
function fn() {
    'use strict';
    console.log(this);
}

fn();  // fn是調用者,獨立調用
window.fn();  // fn是調用者,被window所擁有

在上面的簡單例子中,fn()做爲獨立調用者,按照定義的理解,它內部的this指向就爲undefined。而window.fn()則由於fn被window所擁有,內部的this就指向了window對象。

那麼掌握了這個規則,如今回過頭去看看上面的三個例子,經過添加/去除嚴格模式,那麼你就會發現,原來this已經變得不那麼虛無縹緲,已經有跡可循了。

可是咱們須要特別注意的是demo03。在demo03中,對象obj中的c屬性使用this.a + 20來計算。這裏咱們須要明確的一點是,單獨的{}是不會造成新的做用域的,所以這裏的this.a,因爲並無做用域的限制,因此它仍然處於全局做用域之中。因此這裏的this實際上是指向的window對象。

那麼咱們修改一下demo03的代碼,你們能夠思考一下會發生什麼變化。

'use strict';
var a = 20;
function foo () {
    var a = 1;
    var obj = {
        a: 10,
        c: this.a + 20,
        fn: function () {
            return this.a;
        }
    }
    return obj.c;

}
console.log(foo());    // ?
console.log(window.foo());  // ?
  • 實際開發中,並不推薦這樣使用this;
  • 上面屢次提到的嚴格模式,須要你們認真對待,由於在實際開發中,如今基本已經所有采用嚴格模式了,而最新的ES6,也是默認支持嚴格模式。

再來看一些容易理解錯誤的例子,加深一下對調用者與是否獨立運行的理解。

var a = 20;
var foo = {
    a: 10,
    getA: function () {
        return this.a;
    }
}
console.log(foo.getA()); // 10

var test = foo.getA;
console.log(test());  // 20

foo.getA()中,getA是調用者,他不是獨立調用,被對象foo所擁有,所以它的this指向了foo。而test()做爲調用者,儘管他與foo.getA的引用相同,可是它是獨立調用的,所以this指向undefined,在非嚴格模式,自動轉向全局window。

稍微修改一下代碼,你們自行理解。

var a = 20;
function getA() {
    return this.a;
}
var foo = {
    a: 10,
    getA: getA
}
console.log(foo.getA());  // 10

靈機一動,再來一個。以下例子。

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

function active(fn) {
    fn(); // 真實調用者,爲獨立調用
}

var a = 20;
var obj = {
    a: 10,
    getA: foo
}

active(obj.getA);
3、使用call,apply顯示指定this

JavaScript內部提供了一種機制,讓咱們能夠自行手動設置this的指向。它們就是call與apply。全部的函數都具備着兩個方法。它們除了參數略有不一樣,其功能徹底同樣。它們的第一個參數都爲this將要指向的對象。

以下例子所示。fn並不是屬於對象obj的方法,可是經過call,咱們將fn內部的this綁定爲obj,所以就可使用this.a訪問obj的a屬性了。這就是call/apply的用法。

function fn() {
    console.log(this.a);
}
var obj = {
    a: 20
}

fn.call(obj);

而call與applay後面的參數,都是向將要執行的函數傳遞參數。其中call以一個一個的形式傳遞,apply以數組的形式傳遞。這是他們惟一的不一樣。

function fn(num1, num2) {
    console.log(this.a + num1 + num2);
}
var obj = {
    a: 20
}

fn.call(obj, 100, 10); // 130
fn.apply(obj, [20, 10]); // 50

由於call/apply的存在,這讓JavaScript變得十分靈活。所以就讓call/apply擁有了不少有用處的場景。簡單總結幾點,也歡迎你們補充。

  • 將類數組對象轉換爲數組
function exam(a, b, c, d, e) {

    // 先看看函數的自帶屬性 arguments 什麼是樣子的
    console.log(arguments);

    // 使用call/apply將arguments轉換爲數組, 返回結果爲數組,arguments自身不會改變
    var arg = [].slice.call(arguments);

    console.log(arg);
}

exam(2, 8, 9, 10, 3);

// result:
// { '0': 2, '1': 8, '2': 9, '3': 10, '4': 3 }
// [ 2, 8, 9, 10, 3 ]
//
// 也經常使用該方法將DOM中的nodelist轉換爲數組
// [].slice.call( document.getElementsByTagName('li') );
  • 根據本身的須要靈活修改this指向
var foo = {
    name: 'joker',
    showName: function() {
      console.log(this.name);
    }
}
var bar = {
    name: 'rose'
}
foo.showName.call(bar);
  • 實現繼承
// 定義父級的構造函數
var Person = function(name, age) {
    this.name = name;
    this.age  = age;
    this.gender = ['man', 'woman'];
}

// 定義子類的構造函數
var Student = function(name, age, high) {

    // use call
    Person.call(this, name, age);
    this.high = high;
}
Student.prototype.message = function() {
    console.log('name:'+this.name+', age:'+this.age+', high:'+this.high+', gender:'+this.gender[0]+';');
}

new Student('xiaom', 12, '150cm').message();

// result
// ----------
// name:xiaom, age:12, high:150cm, gender:man;

簡單給有面向對象基礎的朋友解釋一下。在Student的構造函數中,藉助call方法,將父級的構造函數執行了一次,至關於將Person中的代碼,在Sudent中複製了一份,其中的this指向爲從Student中new出來的實例對象。call方法保證了this的指向正確,所以就至關於實現了繼承。Student的構造函數等同於下。

var Student = function(name, age, high) {
    this.name = name;
    this.age  = age;
    this.gender = ['man', 'woman'];
    // Person.call(this, name, age); 這一句話,至關於上面三句話,所以實現了繼承
    this.high = high;
}
  • 在向其餘執行上下文的傳遞中,確保this的指向保持不變

以下面的例子中,咱們期待的是getA被obj調用時,this指向obj,可是因爲匿名函數的存在致使了this指向的丟失,在這個匿名函數中this指向了全局,所以咱們須要想一些辦法找回正確的this指向。

var obj = {
    a: 20,
    getA: function() {
        setTimeout(function() {
            console.log(this.a)
        }, 1000)
    }
}

obj.getA();

常規的解決辦法很簡單,就是使用一個變量,將this的引用保存起來。咱們經常會用到這方法,可是咱們也要藉助上面講到過的知識,來判斷this是否在傳遞中被修改了,若是沒有被修改,就沒有必要這樣使用了。

var obj = {
    a: 20,
    getA: function() {
        var self = this;
        setTimeout(function() {
            console.log(self.a)
        }, 1000)
    }
}

另外就是藉助閉包與apply方法,封裝一個bind方法。

function bind(fn, obj) {
    return function() {
        return fn.apply(obj, arguments);
    }
}

var obj = {
    a: 20,
    getA: function() {
        setTimeout(bind(function() {
            console.log(this.a)
        }, this), 1000)
    }
}

obj.getA();

固然,也可使用ES5中已經自帶的bind方法。它與我上面封裝的bind方法是同樣的效果。

var obj = {
    a: 20,
    getA: function() {
        setTimeout(function() {
            console.log(this.a)
        }.bind(this), 1000)
    }
}
4、構造函數與原型方法上的this

在封裝對象的時候,咱們幾乎都會用到this,可是,只有少數人搞明白了在這個過程當中的this指向,就算咱們理解了原型,也不必定理解了this。因此這一部分,我認爲將會爲這篇文章最重要最核心的部分。理解了這裏,將會對你學習JS面向對象產生巨大的幫助。

結合下面的例子,我在例子拋出幾個問題你們思考一下。

function Person(name, age) {

    // 這裏的this指向了誰?
    this.name = name;
    this.age = age;   
}

Person.prototype.getName = function() {

    // 這裏的this又指向了誰?
    return this.name;
}

// 上面的2個this,是同一個嗎,他們是否指向了原型對象?

var p1 = new Person('Nick', 20);
p1.getName();

咱們已經知道,this,是在函數調用過程當中肯定,所以,搞明白new的過程當中到底發生了什麼就變得十分重要。

經過new操做符調用構造函數,會經歷如下4個階段。

  • 建立一個新的對象;
  • 將構造函數的this指向這個新對象;
  • 指向構造函數的代碼,爲這個對象添加屬性,方法等;
  • 返回新對象。

所以,當new操做符調用構造函數時,this其實指向的是這個新建立的對象,最後又將新的對象返回出來,被實例對象p1接收。所以,咱們能夠說,這個時候,構造函數的this,指向了新的實例對象,p1。

而原型方法上的this就好理解多了,根據上邊對函數中this的定義,p1.getName()中的getName爲調用者,他被p1所擁有,所以getName中的this,也是指向了p1。

好啦,我所知道的,關於this的一切,已經總結完了,但願你們在閱讀以後,可以真正學到東西,而後給我點個贊^_^。若是你發現有什麼錯誤,請在評論中指出,我會盡快修改。先謝過了。

前端基礎進階系列目錄

clipboard.png

相關文章
相關標籤/搜索