我們在學習JavaScript的過程中,由于對一些概念理解得不是很清楚,但是又想要通過一些方式把它記下來,于是就很容易草率的給這些概念定下一些方便自己記憶的有偏差的結論。
危害比較大的是,有的不準確的結論在網上還廣為流傳。
比如對于this指向的理解中,有這樣一種說法:誰調用它,this就指向誰。在我剛開始學習this的時候,我非常相信這句話。因為在一些情況下,這樣理解也還算說得通??墒俏页3陂_發中遇到一些不一樣的情況,一個由于this的錯誤調用,可以讓我懵逼一整天。那個時候我也查資料,在群里問大神,可是我仍然搞不清楚“我特么到底錯哪里了”。
其實只是因為我的認知中有一個不準確的結論。
所以,我認為需要有這樣一篇文章,來幫助大家全方位的解讀this。讓大家對this,有一個正確的,全面的認知。
在這之前,我們回顧一下執行上下文。
在前面幾篇文章中,我有好幾個地方都提到執行上下文的生命周期,為了防止大家沒有記住,再發一次,如下圖。
執行上下文的創建階段,會分別生成變量對象,建立作用域鏈,確定this指向。其中變量對象與作用域鏈我們都已經明白了。本文的關鍵,就是確定this指向。
首先,我們需要得出一個非常重要的,并且一定要牢記于心的結論,this的指向,是在函數被調用的時候確定的。也就是執行上下文被創建時確定的。
因此,一個函數中的this指向,可以非常靈活。比如下面的例子中,同一個函數由于調用方式的不同,this指向了不一樣的對象。
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();
一、全局對象中的this
關于全局對象的this,我之前在總結變量對象的時候提到過,它是一個比較特殊的存在。全局環境中的this,指向它本身。因此,這也相對簡單,沒有那么多復雜的情況需要考慮。
// 通過this綁定到全局對象
this.a2 = 20;
// 通過聲明綁定到變量對象,但在全局環境中,變量對象就是它自身
var a1 = 10;
// 僅僅只有賦值操作,標識符會隱式綁定到全局對象
a3 = 30;
// 輸出結果會全部符合預期
console.log(a1);
console.log(a2);
console.log(a3);
二、函數中的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);
三、使用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變得更加靈活。
也因此他們的使用場景就多種多樣。簡單總結幾點,也歡迎大家補充。
將類數組對象轉換為數組
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)
}
}
ES6中也常常使用箭頭函數的方式來替代這種方案
四、構造函數與原型方法上的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的一切,已經總結完了,希望大家在閱讀之后,能夠真正學到東西,然后給我點個贊!
下一篇:前端基礎進階(八):在chrome開發者工具中觀察函數調用棧、作用域鏈與閉包
上一篇:前端基礎進階(六):setTimeout與循環閉包面試題詳解
前端基礎進階目錄