我們在學(xué)習(xí)JavaScript的過程中,由于對一些概念理解得不是很清楚轧葛,但是又想要通過一些方式把它記下來禽车,于是就很容易草率的給這些概念定下一些方便自己記憶的有偏差的結(jié)論。
危害比較大的是遣耍,有的不準(zhǔn)確的結(jié)論在網(wǎng)上還廣為流傳闺阱。
比如對于this指向的理解中,有這樣一種說法:誰調(diào)用它舵变,this就指向誰酣溃。在我剛開始學(xué)習(xí)this的時候,我非常相信這句話纪隙。因為在一些情況下赊豌,這樣理解也還算說得通∶嘣郏可是我常常會在開發(fā)中遇到一些不一樣的情況碘饼,一個由于this的錯誤調(diào)用,可以讓我懵逼一整天悲伶。那個時候我也查資料艾恼,在群里問大神,可是我仍然搞不清楚“我特么到底錯哪里了”麸锉。
其實只是因為我的認(rèn)知中有一個不準(zhǔn)確的結(jié)論钠绍。
所以,我認(rèn)為需要有這樣一篇文章花沉,來幫助大家全方位的解讀this五慈。讓大家對this,有一個正確的主穗,全面的認(rèn)知。
在這之前毙芜,我們回顧一下執(zhí)行上下文忽媒。
在前面幾篇文章中,我有好幾個地方都提到執(zhí)行上下文的生命周期腋粥,為了防止大家沒有記住晦雨,再發(fā)一次架曹,如下圖。
執(zhí)行上下文的創(chuàng)建階段闹瞧,會分別生成變量對象绑雄,建立作用域鏈,確定this指向奥邮。其中變量對象與作用域鏈我們都已經(jīng)明白了万牺。本文的關(guān)鍵,就是確定this指向洽腺。
首先脚粟,我們需要得出一個非常重要的,并且一定要牢記于心的結(jié)論蘸朋,this的指向核无,是在函數(shù)被調(diào)用的時候確定的。也就是執(zhí)行上下文被創(chuàng)建時確定的藕坯。
因此团南,一個函數(shù)中的this指向,可以非常靈活炼彪。比如下面的例子中吐根,同一個函數(shù)由于調(diào)用方式的不同,this指向了不一樣的對象霹购。
var a = 10;
var obj = {
a: 20
}
function fn() {
console.log(this.a);
}
fn(); // 10
fn.call(obj); // 20
除此之外佑惠,在函數(shù)執(zhí)行過程中,this一旦被確定齐疙,就不可更改了膜楷。
var a = 10;
var obj = {
a: 20
}
function fn() {
this = obj; // 這句話試圖修改this,運行后會報錯
console.log(this.a);
}
fn();
一贞奋、全局對象中的this
關(guān)于全局對象的this赌厅,我之前在總結(jié)變量對象的時候提到過,它是一個比較特殊的存在轿塔。全局環(huán)境中的this特愿,指向它本身。因此勾缭,這也相對簡單揍障,沒有那么多復(fù)雜的情況需要考慮。
// 通過this綁定到全局對象
this.a2 = 20;
// 通過聲明綁定到變量對象俩由,但在全局環(huán)境中毒嫡,變量對象就是它自身
var a1 = 10;
// 僅僅只有賦值操作,標(biāo)識符會隱式綁定到全局對象
a3 = 30;
// 輸出結(jié)果會全部符合預(yù)期
console.log(a1);
console.log(a2);
console.log(a3);
二幻梯、函數(shù)中的this
在總結(jié)函數(shù)中this指向之前兜畸,我想我們有必要通過一些奇怪的例子努释,來感受一下函數(shù)中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());
這幾個例子需要花點時間仔細感受一下咬摇,如果你暫時沒想明白怎么回事伐蒂,也不用著急,我們一點一點來分析肛鹏。
分析之前逸邦,我們直接了當(dāng)拋出結(jié)論。
在一個函數(shù)上下文中龄坪,this由調(diào)用者提供昭雌,由調(diào)用函數(shù)的方式來決定。如果調(diào)用者函數(shù)健田,被某一個對象所擁有烛卧,那么該函數(shù)在調(diào)用時,內(nèi)部的this指向該對象妓局。如果函數(shù)獨立調(diào)用总放,那么該函數(shù)內(nèi)部的this,則指向undefined好爬。但是在非嚴(yán)格模式中局雄,當(dāng)this指向undefined時,它會被自動指向全局對象存炮。
從結(jié)論中我們可以看出炬搭,想要準(zhǔn)確確定this指向,找到函數(shù)的調(diào)用者以及區(qū)分他是否是獨立調(diào)用十分關(guān)鍵穆桂。
// 為了能夠準(zhǔn)確判斷宫盔,我們在函數(shù)內(nèi)部使用嚴(yán)格模式,因為非嚴(yán)格模式會自動指向全局
function fn() {
'use strict';
console.log(this);
}
fn(); // fn是調(diào)用者享完,獨立調(diào)用
window.fn(); // fn是調(diào)用者灼芭,被window所擁有
在上面的簡單例子中,fn()
作為獨立調(diào)用者般又,按照定義的理解彼绷,它內(nèi)部的this指向就為undefined。而window.fn()
則因為fn被window所擁有茴迁,內(nèi)部的this就指向了window對象寄悯。
掌握了這個規(guī)則,現(xiàn)在回過頭去看看上面的三個例子堕义,通過添加/去除嚴(yán)格模式热某,你就會發(fā)現(xiàn),原來this已經(jīng)變得不那么虛無縹緲,已經(jīng)有跡可循了昔馋。
但是我們需要特別注意的是demo03。在demo03中,對象obj中的c屬性使用this.a + 20
來計算。這里我們需要明確的一點是灵莲,單獨的{}
不會形成新的作用域鸟整,因此這里的this.a
,由于并沒有作用域的限制毯侦,它仍然處于全局作用域之中。所以這里的this其實是指向的window對象。
那么我們修改一下demo03的代碼倦蚪,大家可以思考一下會發(fā)生什么變化。
'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()); // ?
實際開發(fā)中陵且,并不推薦這樣使用this;
上面多次提到的嚴(yán)格模式个束,需要大家認(rèn)真對待慕购,因為在實際開發(fā)中,現(xiàn)在基本已經(jīng)全部采用嚴(yán)格模式了茬底,而最新的ES6沪悲,也是默認(rèn)支持嚴(yán)格模式。
再來看一些容易理解錯誤的例子阱表,加深一下對調(diào)用者與是否獨立運行的理解殿如。
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是調(diào)用者最爬,他不是獨立調(diào)用涉馁,被對象foo所擁有,因此它的this指向了foo烂叔。而test()
作為調(diào)用者谨胞,盡管他與foo.getA的引用相同,但是它是獨立調(diào)用的蒜鸡,因此this指向undefined胯努,在非嚴(yán)格模式,自動轉(zhuǎn)向全局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(); // 真實調(diào)用者,為獨立調(diào)用
}
var a = 20;
var obj = {
a: 10,
getA: foo
}
active(obj.getA);
三溉箕、使用call晦墙,apply顯示指定this
JavaScript內(nèi)部提供了一種機制,讓我們可以自行手動設(shè)置this的指向肴茄。它們就是call與apply晌畅。所有的函數(shù)都具有這兩個方法。它們除了參數(shù)略有不同之外寡痰,其功能完全一樣抗楔。它們的第一個參數(shù)都為this將要指向的對象。
如下例子所示拦坠。fn并非屬于對象obj的方法连躏,但是通過call,我們將fn內(nèi)部的this綁定為obj贞滨,因此就可以使用this.a訪問obj的a屬性了入热。這就是call/apply的用法。
function fn() {
console.log(this.a);
}
var obj = {
a: 20
}
fn.call(obj);
call與applay后面的參數(shù)疲迂,都是向?qū)⒁獔?zhí)行的函數(shù)傳遞參數(shù)才顿。其中call以一個一個的形式傳遞,apply以數(shù)組的形式傳遞尤蒿。這是他們唯一的不同郑气。
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變得更加靈活腰池。
也因此他們的使用場景就多種多樣尾组。簡單總結(jié)幾點,也歡迎大家補充示弓。
將類數(shù)組對象轉(zhuǎn)換為數(shù)組
function exam(a, b, c, d, e) {
// 先看看函數(shù)的自帶屬性 arguments 什么是樣子的
console.log(arguments);
// 使用call/apply將arguments轉(zhuǎn)換為數(shù)組, 返回結(jié)果為數(shù)組讳侨,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轉(zhuǎn)換為數(shù)組
// [].slice.call( document.getElementsByTagName('li') );
根據(jù)自己的需要靈活修改this指向
var foo = {
name: 'joker',
showName: function () {
console.log(this.name);
}
}
var bar = {
name: 'rose'
}
foo.showName.call(bar);
實現(xiàn)繼承
// 定義父級的構(gòu)造函數(shù)
var Person = function (name, age) {
this.name = name;
this.age = age;
this.gender = ['man', 'woman'];
}
// 定義子類的構(gòu)造函數(shù)
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;
簡單給有面向?qū)ο蠡A(chǔ)的朋友解釋一下。在Student的構(gòu)造函數(shù)中奏属,借助call方法跨跨,將父級的構(gòu)造函數(shù)執(zhí)行了一次,相當(dāng)于將Person中的代碼囱皿,在Sudent中復(fù)制了一份勇婴,其中的this指向為從Student中new出來的實例對象。call方法保證了this的指向正確嘱腥,因此就相當(dāng)于實現(xiàn)了繼承耕渴。Student的構(gòu)造函數(shù)等同于下。
var Student = function (name, age, high) {
this.name = name;
this.age = age;
this.gender = ['man', 'woman'];
// Person.call(this, name, age); 這一句話齿兔,相當(dāng)于上面三句話橱脸,因此實現(xiàn)了繼承
this.high = high;
}
在向其他執(zhí)行上下文的傳遞中础米,確保this的指向保持不變
如下面的例子中,我們期待的是getA被obj調(diào)用時添诉,this指向obj屁桑,但是由于匿名函數(shù)的存在導(dǎo)致了this指向的丟失,在這個匿名函數(shù)中this指向了全局吻商,因此我們需要想一些辦法找回正確的this指向掏颊。
var obj = {
a: 20,
getA: function () {
setTimeout(function () {
console.log(this.a)
}, 1000)
}
}
obj.getA();
常規(guī)的解決辦法很簡單,就是使用一個變量艾帐,將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();
當(dāng)然今野,也可以使用ES5中已經(jīng)自帶的bind方法。它與我上面封裝的bind方法是一樣的效果罐农。
var obj = {
a: 20,
getA: function () {
setTimeout(function () {
console.log(this.a)
}.bind(this), 1000)
}
}
ES6中也常常使用箭頭函數(shù)的方式來替代這種方案
四条霜、構(gòu)造函數(shù)與原型方法上的this
在封裝對象的時候,我們幾乎都會用到this涵亏,但是宰睡,只有少數(shù)人搞明白了在這個過程中的this指向,就算我們理解了原型气筋,也不一定理解到了this拆内。所以這一部分,我認(rèn)為將會為這篇文章最重要最核心的部分宠默。理解了這里麸恍,將會對你學(xué)習(xí)JS面向?qū)ο螽a(chǎn)生巨大的幫助。
結(jié)合下面的例子搀矫,我拋出幾個問題大家思考一下抹沪。
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();
我們已經(jīng)知道冰垄,this蹬癌,是在函數(shù)調(diào)用過程中確定权她,因此,搞明白new的過程中到底發(fā)生了什么就變得十分重要逝薪。
通過new操作符調(diào)用構(gòu)造函數(shù)隅要,會經(jīng)歷以下4個階段。
- 創(chuàng)建一個新的對象董济;
- 將構(gòu)造函數(shù)的this指向這個新對象步清;
- 指向構(gòu)造函數(shù)的代碼,為這個對象添加屬性虏肾,方法等廓啊;
- 返回新對象。
因此封豪,當(dāng)new操作符調(diào)用構(gòu)造函數(shù)時谴轮,this其實指向的是這個新創(chuàng)建的對象,最后又將新的對象返回出來吹埠,被實例對象p1接收第步。因此,我們可以說缘琅,這個時候粘都,構(gòu)造函數(shù)的this,指向了新的實例對象:p1刷袍。
而原型方法上的this就好理解多了翩隧,根據(jù)上邊對函數(shù)中this的定義,p1.getName()
中的getName為調(diào)用者做个,他被p1所擁有鸽心,因此getName中的this,也是指向了p1居暖。
好啦顽频,我所知道的,關(guān)于this的一切太闺,已經(jīng)總結(jié)完了糯景,希望大家在閱讀之后,能夠真正學(xué)到東西省骂,然后給我點個贊蟀淮!
下一篇:前端基礎(chǔ)進階(八):在chrome開發(fā)者工具中觀察函數(shù)調(diào)用棧、作用域鏈與閉包
上一篇:前端基礎(chǔ)進階(六):setTimeout與循環(huán)閉包面試題詳解
前端基礎(chǔ)進階目錄