參考《JavaScript設(shè)計(jì)模式與開發(fā)實(shí)踐》
this
跟別的語言大相徑庭的是歹茶,JavaScript的this總是指向一個(gè)對(duì)象,而具體指向哪個(gè)對(duì)象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境動(dòng)態(tài)綁定的你弦,而非函數(shù)被聲明時(shí)的環(huán)境惊豺。
- this的指向
除去不常用的with和eval的情況,具體到實(shí)際應(yīng)用中禽作,this的指向大致可以分為以下4種尸昧。- 作為對(duì)象的方法調(diào)用。
- 作為普通函數(shù)調(diào)用旷偿。
- 構(gòu)造器調(diào)用烹俗。
- Function.prototype.call或Function.prototype.apply調(diào)用。
1. 作為對(duì)象的方法調(diào)用
當(dāng)函數(shù)作為對(duì)象的方法被調(diào)用時(shí)萍程,this指向該對(duì)象:
var obj = {
a: 1,
getA: function(){
alert ( this === obj ); // 輸出:true
alert ( this.a ); // 輸出: 1
}
};
obj.getA();
2. 作為普通函數(shù)調(diào)用幢妄。
當(dāng)函數(shù)不作為對(duì)象的屬性被調(diào)用時(shí),也就是我們常說的普通函數(shù)方式茫负,此時(shí)的this總是指向全局對(duì)象蕉鸳。在瀏覽器的JavaScript里,這個(gè)全局對(duì)象是window對(duì)象忍法。
window.name = 'globalName';
var getName = function(){
return this.name;
}
console.log(getName()); //輸出:globalName
//或者
var myObject = {
name: 'sven',
getName: function(){
console.log('this',this);
return this.name;
}
}
var getName = myObject.getName;
console.log(getName());
有時(shí)候我們會(huì)遇到一些困擾潮尝,比如在div節(jié)點(diǎn)的事件函數(shù)內(nèi)部,有一個(gè)局部的callback方法饿序,callback被作為普通函數(shù)調(diào)用時(shí)衍锚,callback內(nèi)部的this指向了window,但我們往往是想讓它指向該div節(jié)點(diǎn)嗤堰,見如下代碼:
<html>
<body>
<div id="div1">我是一個(gè)div</div>
</body>
<script>
window.id = 'window';
document.getElementById('div1').onclick = function () {
alert(this.id); // 輸出:'div1'
var callback = function () {
alert(this.id); // 輸出:'window'
}
callback();
};
</script>
</html>
此時(shí)有一種簡(jiǎn)單的解決方案戴质,可以用一個(gè)變量保存div節(jié)點(diǎn)的引用:
<html>
<body>
<div id="div1">我是一個(gè)div</div>
</body>
<script>
window.id = 'window';
document.getElementById('div1').onclick = function () {
var that = this;
var callback = function () {
alert(that.id); // 輸出:'div1'
}
callback();
};
</script>
</html>
在ECMAScript 5的strict模式下,這種情況下的this已經(jīng)被規(guī)定為不會(huì)指向全局對(duì)象踢匣,而是undefined:
function func(){
"use strict"
alert(this); // 輸出:undefined
}
func();
3. 構(gòu)造器調(diào)用
JavaScript中沒有類告匠,但是可以從構(gòu)造器中創(chuàng)建對(duì)象,同時(shí)也提供了new運(yùn)算符离唬,使得構(gòu)造器看起來更像一個(gè)類后专。
除了宿主提供的一些內(nèi)置函數(shù),大部分JavaScript函數(shù)都可以當(dāng)作構(gòu)造器使用输莺。構(gòu)造器的外表跟普通函數(shù)一模一樣戚哎,它們的區(qū)別在于被調(diào)用的方式裸诽。當(dāng)用new運(yùn)算符調(diào)用函數(shù)時(shí),該函數(shù)總會(huì)返回一個(gè)對(duì)象型凳,通常情況下丈冬,構(gòu)造器里的this就指向返回的這個(gè)對(duì)象,見如下代碼:
宿主提供的一些內(nèi)置函數(shù):
javascript是一門編程語言甘畅,運(yùn)行的環(huán)境是虛擬機(jī)(chrome是v8埂蕊,別的瀏覽器也有),這個(gè)虛擬機(jī)在標(biāo)準(zhǔn)內(nèi)稱作javascript的運(yùn)行時(shí)疏唾,這個(gè)運(yùn)行時(shí)本身就是javascript的宿主環(huán)境了蓄氧,不過在瀏覽器端,也把瀏覽器稱作它的宿主環(huán)境(虛擬機(jī)寄宿在瀏覽器內(nèi))槐脏。
宿主我個(gè)人理解就是宿主對(duì)象喉童,在瀏覽器里就是指window對(duì)象,內(nèi)置函數(shù)指的就是window原型上的一些函數(shù)顿天,例如new Date(); new Array();
var MyClass = function () {
this.name = 'sven';
};
var obj = new MyClass();
alert(obj.name); // 輸出:sven
但用new調(diào)用構(gòu)造器時(shí)堂氯,還要注意一個(gè)問題,如果構(gòu)造器顯式地返回了一個(gè)object類型的對(duì)象露氮,那么此次運(yùn)算結(jié)果最終會(huì)返回這個(gè)對(duì)象祖灰,而不是我們之前期待的this:
var MyClass = function () {
this.name = 'sven';
return {
// 顯式地返回一個(gè)對(duì)象
name: 'anne'
}
};
var obj = new MyClass();
alert(obj.name); // 輸出:anne
如果構(gòu)造器不顯式地返回任何數(shù)據(jù)钟沛,或者是返回一個(gè)非對(duì)象類型的數(shù)據(jù)畔规,就不會(huì)造成上述問題:
var MyClass = function () {
this.name = 'sven';
return 'anne'; // 返回string類型
};
var obj = new MyClass();
alert(obj.name); // 輸出:sven
4. Function.prototype.call或Function.prototype.apply調(diào)用。
跟普通的函數(shù)調(diào)用相比恨统,用Function.prototype.call或Function.prototype.apply可以動(dòng)態(tài)地改變傳入函數(shù)的this:
var obj1 = {
name: 'sven',
getName: function () {
return this.name;
}
};
var obj2 = {
name: 'anne'
};
console.log(obj1.getName()); // 輸出: sven
console.log(obj1.getName.call(obj2)); // 輸出:anne
call和apply方法能很好地體現(xiàn)JavaScript的函數(shù)式語言特性叁扫,在JavaScript中,幾乎每一次編寫函數(shù)式語言風(fēng)格的代碼畜埋,都離不開call和apply莫绣。在JavaScript諸多版本的設(shè)計(jì)模式中,也用到了call和apply悠鞍。在下一節(jié)會(huì)詳細(xì)介紹它們对室。
丟失的this
這是一個(gè)經(jīng)常遇到的問題,我們先看下面的代碼:
var obj = {
myName: 'sven',
getName: function () {
return this.myName;
}
};
console.log(obj.getName()); // 輸出:'sven'
var getName2 = obj.getName;
console.log(getName2()); // 輸出:undefined
當(dāng)調(diào)用obj.getName時(shí)咖祭,getName方法是作為obj對(duì)象的屬性被調(diào)用的掩宜,根據(jù)2.1.1節(jié)提到的規(guī)律,此時(shí)的this指向obj對(duì)象么翰,所以obj.getName()輸出'sven'牺汤。
當(dāng)用另外一個(gè)變量getName2來引用obj.getName,并且調(diào)用getName2時(shí)浩嫌,根據(jù)2.1.2節(jié)提到的規(guī)律檐迟,此時(shí)是普通函數(shù)調(diào)用方式补胚,this是指向全局window的,所以程序的執(zhí)行結(jié)果是undefined追迟。
再看另一個(gè)例子溶其,document.getElementById這個(gè)方法名實(shí)在有點(diǎn)過長(zhǎng),我們大概嘗試過用一個(gè)短的函數(shù)來代替它怔匣,如同prototype.js等一些框架所做過的事情:
var getId = function (id) {
return document.getElementById(id);
};
getId('div1');
我們也許思考過為什么不能用下面這種更簡(jiǎn)單的方式:
<html>
<body>
<div id="div1">我是一個(gè)div</div>
</body>
<script>
var getId = document.getElementById;
getId('div1');
</script>
</html>
在Chrome握联、Firefox、IE10中執(zhí)行過后就會(huì)發(fā)現(xiàn)每瞒,這段代碼拋出了一個(gè)異常金闽。這是因?yàn)樵S多引擎的document.getElementById方法的內(nèi)部實(shí)現(xiàn)中需要用到this。這個(gè)this本來被期望指向document剿骨,當(dāng)getElementById方法作為document對(duì)象的屬性被調(diào)用時(shí)代芜,方法內(nèi)部的this確實(shí)是指向document的。
但當(dāng)用getId來引用document.getElementById之后浓利,再調(diào)用getId挤庇,此時(shí)就成了普通函數(shù)調(diào)用,函數(shù)內(nèi)部的this指向了window贷掖,而不是原來的document嫡秕。
我們可以嘗試?yán)胊pply把document當(dāng)作this傳入getId函數(shù),幫助“修正”this:
document.getElementById = (function (func) {
return function () {
return func.apply(document, arguments);
}
})(document.getElementById);
var getId = document.getElementById;
var div = getId('div1');
alert(div.id); // 輸出:div1
個(gè)人嘗試以下寫法苹威,也可行:
<html>
<body>
<div id="div1">我是一個(gè)div</div>
</body>
<script>
var getId = document.getElementById;
var div = getId.call(document, 'div1');
alert(div.id); // 輸出:div1
</script>
</html>
call和apply
- call和apply的區(qū)別
Function.prototype.call和Function.prototype.apply都是非常常用的方法昆咽。它們的作用一模一樣,區(qū)別僅在于傳入?yún)?shù)形式的不同牙甫。
apply接受兩個(gè)參數(shù)掷酗,第一個(gè)參數(shù)指定了函數(shù)體內(nèi)this對(duì)象的指向,第二個(gè)參數(shù)為一個(gè)帶下標(biāo)的集合窟哺,這個(gè)集合可以為數(shù)組泻轰,也可以為類數(shù)組,apply方法把這個(gè)集合中的元素作為參數(shù)傳遞給被調(diào)用的函數(shù):
var func = function (a, b, c) {
alert([a, b, c]); // 輸出[ 1, 2, 3 ]
};
func.apply(null, [1, 2, 3]);
在這段代碼中且轨,參數(shù)1浮声、2、3 被放在數(shù)組中一起傳入func函數(shù)旋奢,它們分別對(duì)應(yīng)func參數(shù)列表中的a泳挥、b、c黄绩。
call傳入的參數(shù)數(shù)量不固定羡洁,跟apply相同的是,第一個(gè)參數(shù)也是代表函數(shù)體內(nèi)的this指向爽丹,從第二個(gè)參數(shù)開始往后筑煮,每個(gè)參數(shù)被依次傳入函數(shù):
var func = function (a, b, c) {
alert([a, b, c]); // 輸出[ 1, 2, 3 ]
};
func.call(null, 1, 2, 3);
當(dāng)調(diào)用一個(gè)函數(shù)時(shí)辛蚊,JavaScript的解釋器并不會(huì)計(jì)較形參和實(shí)參在數(shù)量、類型以及順序上的區(qū)別真仲,JavaScript的參數(shù)在內(nèi)部就是用一個(gè)數(shù)組來表示的袋马。從這個(gè)意義上說,apply比call的使用率更高秸应,我們不必關(guān)心具體有多少參數(shù)被傳入函數(shù)虑凛,只要用apply一股腦地推過去就可以了。
call是包裝在apply上面的一顆語法糖软啼,如果我們明確地知道函數(shù)接受多少個(gè)參數(shù)桑谍,而且想一目了然地表達(dá)形參和實(shí)參的對(duì)應(yīng)關(guān)系,那么也可以用call來傳送參數(shù)祸挪。
當(dāng)使用call或者apply的時(shí)候锣披,如果我們傳入的第一個(gè)參數(shù)為null,函數(shù)體內(nèi)的this會(huì)指向默認(rèn)的宿主對(duì)象贿条,在瀏覽器中則是window:
var func = function (a, b, c) {
alert(this === window); // 輸出true
};
func.apply(null, [1, 2, 3]);
但如果是在嚴(yán)格模式下雹仿,函數(shù)體內(nèi)的this還是為null:
var func = function (a, b, c) {
"use strict";
alert(this === null); // 輸出true
};
func.apply(null, [1, 2, 3]);
有時(shí)候我們使用call或者apply的目的不在于指定this指向,而是另有用途整以,比如借用其他對(duì)象的方法胧辽。那么我們可以傳入null來代替某個(gè)具體的對(duì)象:
Math.max.apply( null, [ 1, 2, 5, 3, 4 ] ) // 輸出:5
-
call和apply的用途
前面說過,能夠熟練使用call和apply公黑,是我們真正成為一名JavaScript程序員的重要一步邑商,本節(jié)我們將詳細(xì)介紹call和apply在實(shí)際開發(fā)中的用途。
1. 改變this指向
call和apply最常見的用途是改變函數(shù)內(nèi)部的this指向帆调,我們來看個(gè)例子:var obj1 = { name: 'sven' }; var obj2 = { name: 'anne' }; window.name = 'window'; var getName = function () { alert(this.name); }; getName(); // 輸出: window getName.call(obj1); // 輸出: sven getName.call(obj2); // 輸出: anne
當(dāng)執(zhí)行g(shù)etName.call( obj1 )這句代碼時(shí)奠骄,getName函數(shù)體內(nèi)的this就指向obj1對(duì)象豆同,所以此處的
var getName = function(){ alert ( this.name ); };
實(shí)際上相當(dāng)于:
var getName = function(){ alert ( obj1.name ); // 輸出: sven };
在實(shí)際開發(fā)中番刊,經(jīng)常會(huì)遇到this指向被不經(jīng)意改變的場(chǎng)景,比如有一個(gè)div節(jié)點(diǎn)影锈,div節(jié)點(diǎn)的onclick事件中的this本來是指向這個(gè)div的
document.getElementById('div1').onclick = function () { alert(this.id); // 輸出:div1 };
假如該事件函數(shù)中有一個(gè)內(nèi)部函數(shù)func芹务,在事件內(nèi)部調(diào)用func函數(shù)時(shí),func函數(shù)體內(nèi)的this就指向window鸭廷,而不是我們預(yù)期的div枣抱,見如下代碼:
document.getElementById('div1').onclick = function () { alert(this.id); // 輸出:div1 var func = function () { alert(this.id); // 輸出:undefined } func(); };
這時(shí)候我們用call來修正func函數(shù)內(nèi)的this,使其依然指向div:
document.getElementById('div1').onclick = function () { var func = function () { alert(this.id); // 輸出:div1 } func.call(this); };
使用call來修正this的場(chǎng)景辆床,我們并非第一次遇到佳晶,在上一小節(jié)關(guān)于this的學(xué)習(xí)中,我們就曾經(jīng)修正過document.getElementById函數(shù)內(nèi)部“丟失”的this讼载,代碼如下:
document.getElementById = (function (func) { return function () { return func.apply(document, arguments); } })(document.getElementById); var getId = document.getElementById; var div = getId('div1'); alert(div.id); // 輸出:div1
2. Function.prototype.bind
大部分高級(jí)瀏覽器都實(shí)現(xiàn)了內(nèi)置的Function.prototype.bind轿秧,用來指定函數(shù)內(nèi)部的this指向中跌,即使沒有原生的Function.prototype.bind實(shí)現(xiàn),我們來模擬一個(gè)也不是難事菇篡,代碼如下:Function.prototype.bind = function (context) { var self = this; // 保存原函數(shù)return function(){ // 返回一個(gè)新的函數(shù)return self.apply(context, arguments); // 執(zhí)行新的函數(shù)的時(shí)候漩符,會(huì)把之前傳入的context // 當(dāng)作新函數(shù)體內(nèi)的this } }; var obj = { name: 'sven' }; var func = function () { alert(this.name); // 輸出:sven }.bind(obj); func();
我們通過Function.prototype.bind來“包裝”func函數(shù),并且傳入一個(gè)對(duì)象context當(dāng)作參數(shù)驱还,這個(gè)context對(duì)象就是我們想修正的this對(duì)象嗜暴。
在Function.prototype.bind的內(nèi)部實(shí)現(xiàn)中,我們先把func函數(shù)的引用保存起來议蟆,然后返回一個(gè)新的函數(shù)闷沥。當(dāng)我們?cè)趯韴?zhí)行func函數(shù)時(shí),實(shí)際上先執(zhí)行的是這個(gè)剛剛返回的新函數(shù)咐容。在新函數(shù)內(nèi)部狐赡,self.apply( context, arguments )這句代碼才是執(zhí)行原來的func函數(shù),并且指定context對(duì)象為func函數(shù)體內(nèi)的this疟丙。
這是一個(gè)簡(jiǎn)化版的Function.prototype.bind實(shí)現(xiàn)颖侄,通常我們還會(huì)把它實(shí)現(xiàn)得稍微復(fù)雜一點(diǎn),使得可以往func函數(shù)中預(yù)先填入一些參數(shù):Function.prototype.bind = function () { var self = this, // 保存原函數(shù) context = [].shift.call(arguments), // 需要綁定的this上下文 args = [].slice.call(arguments); // 剩余的參數(shù)轉(zhuǎn)成數(shù)組 return function () { // 返回一個(gè)新的函數(shù) return self.apply(context, [].concat.call(args, [].slice.call(arguments))); // 執(zhí)行新的函數(shù)的時(shí)候享郊,會(huì)把之前傳入的context當(dāng)作新函數(shù)體內(nèi)的this // 并且組合兩次分別傳入的參數(shù)览祖,作為新函數(shù)的參數(shù) } }; var obj = { name: 'sven' }; var func = function (a, b, c, d) { alert(this.name); // 輸出:sven alert([a, b, c, d]) // 輸出:[ 1, 2, 3, 4 ] }.bind(obj, 1, 2); func(3, 4);
3. 借用其他對(duì)象的方法
我們知道,杜鵑既不會(huì)筑巢炊琉,也不會(huì)孵雛展蒂,而是把自己的蛋寄托給云雀等其他鳥類,讓它們代為孵化和養(yǎng)育苔咪。同樣锰悼,在JavaScript中也存在類似的借用現(xiàn)象。
借用方法的第一種場(chǎng)景是“借用構(gòu)造函數(shù)”团赏,通過這種技術(shù)箕般,可以實(shí)現(xiàn)一些類似繼承的效果:var A = function (name) { this.name = name; }; var B = function () { A.apply(this, arguments); }; B.prototype.getName = function () { return this.name; }; var b = new B('sven'); console.log(b.getName()); // 輸出:'sven'
借用方法的第二種運(yùn)用場(chǎng)景跟我們的關(guān)系更加密切。
函數(shù)的參數(shù)列表arguments是一個(gè)類數(shù)組對(duì)象舔清,雖然它也有“下標(biāo)”丝里,但它并非真正的數(shù)組,所以也不能像數(shù)組一樣体谒,進(jìn)行排序操作或者往集合里添加一個(gè)新的元素杯聚。這種情況下,我們常常會(huì)借用Array.prototype對(duì)象上的方法抒痒。比如想往arguments中添加一個(gè)新的元素幌绍,通常會(huì)借用Array.prototype.push:(function () { Array.prototype.push.call(arguments, 3); console.log(arguments); // 輸出[1,2,3] })(1, 2);
在操作arguments的時(shí)候,我們經(jīng)常非常頻繁地找Array.prototype對(duì)象借用方法。
想把a(bǔ)rguments轉(zhuǎn)成真正的數(shù)組的時(shí)候傀广,可以借用Array.prototype.slice方法痢虹;想截去arguments列表中的頭一個(gè)元素時(shí),又可以借用Array.prototype.shift方法主儡。那么這種機(jī)制的內(nèi)部實(shí)現(xiàn)原理是什么呢奖唯?我們不妨翻開V8的引擎源碼,以Array.prototype.push為例糜值,看看V8引擎中的具體實(shí)現(xiàn):function ArrayPush() { var n = TO_UINT32(this.length); // 被push的對(duì)象的length var m = % _ArgumentsLength(); // push的參數(shù)個(gè)數(shù) for (var i = 0; i < m; i++) { this[i + n] = % _Arguments(i); // 復(fù)制元素(1) } this.length = n + m; // 修正length屬性的值(2) return this.length; };
通過這段代碼可以看到丰捷,Array.prototype.push實(shí)際上是一個(gè)屬性復(fù)制的過程,把參數(shù)按照下標(biāo)依次添加到被push的對(duì)象上面寂汇,順便修改了這個(gè)對(duì)象的length屬性病往。至于被修改的對(duì)象是誰,到底是數(shù)組還是類數(shù)組對(duì)象骄瓣,這一點(diǎn)并不重要停巷。
由此可以推斷,我們可以把“任意”對(duì)象傳入Array.prototype.push:var a = {}; Array.prototype.push.call(a, 'first'); alert(a.length); // 輸出:1 alert(a[0]); // first
這段代碼在絕大部分瀏覽器里都能順利執(zhí)行榕栏,但由于引擎的內(nèi)部實(shí)現(xiàn)存在差異畔勤,如果在低版本的IE瀏覽器中執(zhí)行,必須顯式地給對(duì)象a設(shè)置length屬性:
var a = { length: 0 };
前面我們之所以把“任意”兩字加了雙引號(hào)扒磁,是因?yàn)榭梢越栌肁rray.prototype.push方法的對(duì)象還要滿足以下兩個(gè)條件庆揪,從ArrayPush函數(shù)的(1)處和(2)處也可以猜到,這個(gè)對(duì)象至少還要滿足:
- 對(duì)象本身要可以存取屬性妨托;
- 對(duì)象的length屬性可讀寫缸榛。
對(duì)于第一個(gè)條件,對(duì)象本身存取屬性并沒有問題兰伤,但如果借用Array.prototype.push方法的不是一個(gè)object類型的數(shù)據(jù)内颗,而是一個(gè)number類型的數(shù)據(jù)呢? 我們無法在number身上存取其他數(shù)據(jù),那么從下面的測(cè)試代碼可以發(fā)現(xiàn)敦腔,一個(gè)number類型的數(shù)據(jù)不可能借用到Array.prototype. push方法:
var a = 1; Array.prototype.push.call(a, 'first'); alert(a.length); // 輸出:undefined alert(a[0]); // 輸出:undefined
對(duì)于第二個(gè)條件均澳,函數(shù)的length屬性就是一個(gè)只讀的屬性,表示形參的個(gè)數(shù)会烙,我們嘗試把一個(gè)函數(shù)當(dāng)作this傳入Array.prototype.push:
var func = function () { }; Array.prototype.push.call(func, 'first'); alert(func.length); // 報(bào)錯(cuò):cannot assign to read only property ‘length’ of function(){}
bind例子:
const func1 = function(){
console.log('this',JSON.stringify(this))
}
const obj = {
name: 'obj'
}
const func2 = func1.bind(obj);
const func3 = func1;
func1();
func2();
console.log('func2 === func1',func2 === func1)
console.log('func3 === func1',func3 === func1)