全方位解讀this

原文連接 - http://www.jianshu.com/p/d647aa6d1ae6javascript

首先,瞭解下執行上下文的生命週期。java

圖片描述

在執行上下文的建立階段,會分別生成變量對象,創建做用域鏈,以及肯定this指向。
其中this的指向,是在函數被調用的時候肯定的。也就是執行上下文被建立時肯定的。
因此,同一個函數因爲調用方式的不一樣,this指向了不同的對象。node

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,指向window自己。所以,這也相對簡單,沒有那麼多複雜的狀況須要考慮。閉包

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

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

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

// 輸出結果會所有符合預期
console.log(a1, a2, a3);
console.log(window.a1, window.a2, window.a3);

2、函數中的this

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

// 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指向,找到函數的調用者以及區分他是不是獨立調用就變得十分關鍵。學習

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

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

可是須要特別注意的是,在上面的demo03中,對象obj中的c屬性使用this.a + 20來計算,而他的調用者obj.c並不是是一個函數。所以不適用於上面的規則,咱們要對這種方式單獨下一個結論。this

  • 當obj在全局聲明時,不管obj.c在什麼地方調用,這裏的this都指向全局對象。spa

  • 當obj在函數環境中聲明時,這個this指向undefined,在非嚴格模式下,會自動轉向全局對象**

'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()); // error

實際開發中,並不推薦這樣使用this

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

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與apply後面的參數,都是向將要執行的函數傳遞參數。其中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擁有了不少有用處的場景。簡單總結幾點,也歡迎你們補充。
1.將類數組對象轉換爲數組

function exam(a, b, c, d, e) {
    console.log(arguments); // { '0': 2, '1': 8, '2': 9, '3': 10, '4': 3 }
    var arg = [].slice.call(arguments);
    console.log(arg); // [ 2, 8, 9, 10, 3 ]
}
exam(2, 8, 9, 10, 3);

// 也經常使用該方法將DOM中的nodelist轉換爲數組
// [].slice.call( document.getElementsByTagName('li') );

2.根據本身的須要靈活修改this指向

var foo = {
    name: 'joker',
    showName: function() {
      console.log(this.name);
    }
}
var bar = {
    name: 'rose'
}
foo.showName.call(bar) // rose

3.實現繼承

// 定義父級的構造函數
var Person = function(name, age) {
    this.name = name;
    this.age  = age;
    this.gender = ['man', 'woman'];
}
// 定義子類的構造函數
var Student = function(name, age, high) {
    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('xiaoming', 12, '150cm').message(); // { 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(); // undefined

常規的解決辦法很簡單,就是使用一個變量,將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(); // 20

固然,也可使用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。

相關文章
相關標籤/搜索