一般說到JS的this,都會想起在函式中變來變去的this。但是事情的發生都是有規則的約束,JS中的this也不例外,下面我們來看一下JS中this指向的規則
在JS中,有四條規則影響著this的指向
- 預設指向:對於一個函式,如果沒有被打點呼叫,則該函式裡的this指向全域性(window)
- 隱式指向:對於一個函式,如果被打點呼叫,則該函式裡的this指向呼叫該函式的物件
- 顯式指向:通過call,apply,bind改變函式裡的this指向
- 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();
複製程式碼
執行的結果為 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();
複製程式碼
執行的結果為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();
複製程式碼
執行結果是my age is: 100
由obj.showAge()
可以看出,函式showAge
被obj
這個物件呼叫,所以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();
複製程式碼
執行結果是my age is: 100
首先,執行obj2.showAge = obj1.showAge
,把obj1
的showAge
函式的引用給obj2
的showAge
,所以obj2
的showAge
的執行內容就變成了obj1
的showAge
內容
然後執行obj2.showAge()
,shoeAge
的this
指向obj2
,所以列印的是obj2
的age,為100。
三、顯示指向
-> 通過call,apply,bind改變函式裡的this指向
先簡單說一下call
apply
bind
的作用和使用方法
call
apply
都是改變一個函式的this指向並執行該函式,區別是傳參列表不同,call(obj, 引數1, 引數2, ...)
apply(obj, arguments)
bind
是改變一個函式的this指向並返回一個新的函式,傳參結合call
和apply
的,只接收第一次繫結的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相同
複製程式碼
執行結果是:my age is: 18
首先,執行語句從左往右看,呼叫obj
物件中的showAge
方法,然後使用call
方法改變showAge
方法中的this
為newObj
物件。所以,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;
}
複製程式碼
執行結果依舊為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 ();
複製程式碼
執行結果為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; //返回這個上下文
}
複製程式碼
是的,你沒看錯,就只有四行程式碼,但是這四行程式碼理解起來可不簡單,具體的請看程式碼的註釋~~
最後的最後,希望你能從這篇文章中能學到東西就好~感謝閱讀