JavaScript中的this

大翰仔仔發表於2019-02-27

一般說到JS的this,都會想起在函式中變來變去的this。但是事情的發生都是有規則的約束,JS中的this也不例外,下面我們來看一下JS中this指向的規則

在JS中,有四條規則影響著this的指向

  1. 預設指向:對於一個函式,如果沒有被打點呼叫,則該函式裡的this指向全域性(window)
  2. 隱式指向:對於一個函式,如果被打點呼叫,則該函式裡的this指向呼叫該函式的物件
  3. 顯式指向:通過call,apply,bind改變函式裡的this指向
  4. new一個物件:建構函式的this指向該例項化物件
優先順序為: 4 > 3 > 2 > 1

一 、預設指向

-> 對於一個函式,如果沒有被打點呼叫,則該函式裡的this指向全域性(window)

1)先看一個30km/h的小例子

var age = 18;
function showAge () {
  var age = 100;
  console.log('my age is: ' + this.age);
}
showAge();
複製程式碼

JavaScript中的this
執行的結果為 my age is : 18

其實這也是符合預設指向這個規則的,在這個小例子中,showAge函式在執行的時候並沒有被打點呼叫×××.showAge(),所以showAge函式裡面的this指向全域性window,即this.age就是全域性的age為18。

可能有人會有疑問,為什麼不是列印100呢。首先要清楚的一點是,在這個小例子中,列印的是this.age而不是age,所以要到this指向的物件上找age,而不是單純地去找age這個變數,如果改成console.log('my age is: ' + age),列印的就是100了。

2)再來一個60km/h的小例子

var age = 18;
var obj = {
    age : 100,
    showAge : function () {
      setTimeout(function () {
        console.log('my age is: ' + this.age);        
      }, 300);
      return '請稍等300毫秒';
    }
}
obj.showAge();
複製程式碼

JavaScript中的this
執行的結果為my age is : 18

嗯???為什麼還是18???其實把函式執行的位置捋清楚也是符合規則的。 首先,showAge函式定義在一個物件中,裡面有個定時器setTimeout,定時器等待300毫秒後執行列印操作,這應該沒問題。 然後,在外面執行obj.showAge(),打點呼叫obj裡的showAge方法,所以showAge裡的this指向obj,但是,我們要操作的語句是在定時器setTimeout裡面的函式裡的,所以我們可以粗略地模擬一下這個函式

function setTimeout (callback, delay) {
    ...
    if (...) {
        callback();
    }
    ...
}
複製程式碼

好了,大概就是這樣子~~ 我們可以看出,在定時器中,作為回撥函式的callback只是單純地在符合條件後執行了,那麼,這裡的callback函式是不是沒有被誰打點呼叫,所以該函式的this應該指向全域性物件window。所以列印的就是全域性的age,等於18。

同時也可以得到一個普遍的結論:一般的回撥函式的this指向全域性物件window

二、隱式指向

-> 對於一個函式,如果被打點呼叫,則該函式裡的this指向呼叫該函式的物件

1)先看一個30kn/h的例子

var age = 18;
var obj = {
    age : 100,
    showAge : function () {
      console.log('my age is: ' + this.age);
    }
}
obj.showAge();
複製程式碼

JavaScript中的this

執行結果是my age is: 100

obj.showAge()可以看出,函式showAgeobj這個物件呼叫,所以showAge函式裡面的this 指向obj這個物件 所以你也可以把showAge函式的執行語句理解為console.log('my age is: ' + obj.age),所以列印的就是物件obj上的age,為100。

2)再來一個60km/h的例子

var obj1 = {
    age : 18,
    showAge : function () {
      console.log('my age is: ' + this.age);
    }
}
var obj2 = {
    age : 100,
    showAge : function () {}
}
obj2.showAge = obj1.showAge;
obj2.showAge();
複製程式碼

JavaScript中的this

執行結果是my age is: 100

首先,執行obj2.showAge = obj1.showAge,把obj1showAge函式的引用給obj2showAge,所以obj2showAge的執行內容就變成了obj1showAge內容 然後執行obj2.showAge()shoeAgethis指向obj2,所以列印的是obj2的age,為100。

三、顯示指向

-> 通過call,apply,bind改變函式裡的this指向

先簡單說一下call apply bind 的作用和使用方法 call apply 都是改變一個函式的this指向並執行該函式,區別是傳參列表不同,call(obj, 引數1, 引數2, ...) apply(obj, arguments) bind是改變一個函式的this指向並返回一個新的函式,傳參結合callapply的,只接收第一次繫結的this

1)先來一個30km/h的例子

var obj = {
    age : 100,
    showAge : function () {
      console.log('my age is: ' + this.age);
    }
}
var newObj = {
    age : 18
}
obj.showAge.call(newObj);  //apply效果一樣

//var func = obj.showAge.bind(newObj);  //拿個變數接收bind方法執行後返回的新函式
//func();  //結果和call apply相同

複製程式碼

JavaScript中的this

執行結果是:my age is: 18

首先,執行語句從左往右看,呼叫obj物件中的showAge方法,然後使用call方法改變showAge方法中的thisnewObj物件。所以,showAge函式裡的執行語句可以理解為console.log('my age is: ' + newObj.age),所以列印的是newObj裡的age,為18。

2)剖析一下call改變this的原理,車速100km/h

雖然知道call(apply bind 不做解釋,有興趣的可以看完call的原理後自己嘗試還原)能改變一個函式this的指向,那麼它裡面是怎樣操作然後改變一個函式的this指向的呢?

首先,在隱式指向中我們知道,對於一個函式,如果被打點呼叫,則該函式裡的this指向呼叫該函式的物件,那麼我們就可以利用這點來模擬一下call方法

在原型鏈上編寫,模擬就要真實一點~~

Function.prototype.myCall = function (context) {
    var ctx = null;
    //保證不汙染原物件
    if (context) {
        ctx = JSON.parse(JSON.stringify(context));
    }else {
        ctx = window;
    }
    
    var arg = arguments,  //儲存函式的實參列表
        len = arg.length,  
        args = [];  

    ctx.fn = this; 
    for (var i = 1; i < len; i++) {
        //從實參列表的第一位開始遍歷,因為第零位為指向的上下文
        args.push('arg[' + i + ']');
    }
    //遍歷完後 args = [ 'arg[1]', 'arg[2]', ...]

    var result = eval( 'ctx.fn(' + args.join(',') + ')' );
    //上述語句可理解為 eval('ctx.fn(arg[1], arg[2], ...)')

    delete ctx.fn;
    return result;
}
複製程式碼

JavaScript中的this
JavaScript中的this

執行結果依舊為my age is: 18

首先,從結果看,myCall函式沒有問題,然後我們來分析一下函式裡面到底主要乾了些啥 (整體思路見程式碼註釋)

①通過ctx.fn = this把呼叫myCall的函式變為目標上下文的一個方法,由此可以改變fn方法的this為目標上下文,也就是呼叫myCall的函式的this指向了目標上下文

②把呼叫myCall的函式的引數整合,通過eval方法把字串解析為指令碼並執行

好了,整體思路就是如此,這塊不懂也沒關係,我們主要講的是改變this的指向~

四、通過new例項化一個物件(主要講解this問題,建構函式知識不做過多概述)

-> 建構函式的this指向該例項化物件

1)先來一個30km/h的例子

function Person (name, age) {
    this.name = name;
    this.age = age;
    this.introduceYourself = function () {
      console.log('my name is ' + this.name, 'and I\'m ' + this.age + ' years old.');
    }
}
var person = new Person('xuyede', 18);
person.introduceYourself ();
複製程式碼

JavaScript中的this

執行結果為my name is xuyede and I'm 18 years old.

為了更好地讓大家理解,我們可以把new Person()的過程分成4個步驟理解 (當然不是真的是這個過程,只是為了讓大家能理解new的過程,稍後會揭曉真的過程~)

①在Person函式裡面的第一行隱式生成一個物件 var this = {} ②讓該this的原型指向該函式的原型 this.__proto__ = Person.prototype ③執行 this.xxx = xxx ④在函式最後一行把這個this返回出去

所以,你可以理解為變數person拿到了建構函式裡面this的引用,所以person可以使用在建構函式裡設定在this上的值

建構函式還涉及原型、原型鏈的知識,在這裡就不過多闡述這方面的知識

2)剖析一下new這個東西到底幹了什麼,車速180km/h

話不多說,上程式碼~~~

function myNew() {
    var func = [].shift.call(arguments);  //把實參列表的第一位擷取出來,獲得建構函式
    var instance = Object.create(func.prototype);  //宣告一個上下文,原型為建構函式的原型
    func.apply(instance, arguments);  //執行該建構函式,並改變建構函式的this為instance
    return instance;  //返回這個上下文
}
複製程式碼

JavaScript中的this

是的,你沒看錯,就只有四行程式碼,但是這四行程式碼理解起來可不簡單,具體的請看程式碼的註釋~~

最後的最後,希望你能從這篇文章中能學到東西就好~感謝閱讀

相關文章