在javascript中,this關鍵字總讓一些初學者迷惑,Function.prototype.call, Function.prototype.apply這兩個方法廣泛的運用。我們有必要理解這幾個概念。
一:this
跟別的語言大相徑庭的是,javascript的this總是指向一個物件,而具體指向那個物件在執行時基於函式的執行環境動態繫結的,非函式被宣告時的環境。
(1).this的指向
除去不常用的with和eval情況,具體到實際的應用中,this的指向大致分為下面4種。
- 作為物件的方法呼叫
- 作為普通函式呼叫
- 構造器呼叫
- Function.prototype.call或Function.prototype.apply呼叫
簡單做介紹:
1)作為物件的方法呼叫
當函式作為物件的方法被呼叫時,this指向該物件:
var obj = {
a:1,
getA:function() {
alert( this === obj ); //true
alert( this.a ); //1
}
}
obj.getA()複製程式碼
2)作為普通函式呼叫
當函式不作為物件的屬性被呼叫時,也就是我們常說的普通函式方式,此時的this總是指向全域性物件。在瀏覽器裡,就是window物件
window.name = 'globalName';
var getName = function() {
return this.name
}
//console.log( getName() ) // globalName
var myObject = {
name : 'seven',
getNameA : function() {
return this.name
}
}
var a = myObject.getNameA
console.log( a() )複製程式碼
有的時候我們會遇見一些困擾,比如在div節點內部,有一個區域性的callback方法,callback作為普通的函式呼叫時 ,callback內部的this指向了window,但我們往往想讓他指向該div節點。如下
<div id="div1">
div1
</div>
<script type="text/javascript">
document.getElementById("div1").onclick = function() {
var that = this;
console.log(this.id)
var callback = function(){
console.log(that.id)//this.id = xx
}
callback()
}
</script>複製程式碼
注意:在ES5的strict模式下,this已經被規定不會指向全域性物件,而是undefined
3)構造器呼叫
javascript目前沒有類,但是從構造器中建立物件,同時也提供了new運算子,使得構造器看起來更像一個類。
除了宿主提供的一些內建函式,大部分javascript函式都可以當做構造器來使用。構造器的外表跟普通函式一模一樣,他們的區別在於被呼叫的方式。當用new運算子呼叫函式時,該函式會返回一個物件,通常情況下,構造器裡的this就指向返回的這個物件,如下
var myClass = function( name , sex ) {
this.name = name;
this.sex = sex;
};
var newObj = new myClass('jj','sxxx');
console.log(newObj.sex + newObj.name)複製程式碼
但是用new呼叫構造器時,還要注意一個問題,如果構造器顯式的返回了一個object型別的物件,那麼此次運算結果最終會返回這個物件,而不是我們之前期待的this
var myClass = function( name ) {
this.name = name ;
return {
name : 'anam'
}
}
var myObj = new myClass('jack')
console.log(myObj.name) ;//anam複製程式碼
如果構造器不顯式的返回任何資料,或者是返回一個物件型別的資料,就不會造成上述問題
var myClass = function( name ) {
this.name = name ;
name : 'anam'
}
var myObj = new myClass('jack')
console.log(myObj.name) ;//jack複製程式碼
4)Function.prototype.call和Function.prototype.apply呼叫
跟普通的函式呼叫相比,用Function.prototype.call或Function.prototype.apply可以動態的改變傳入函式的this
var obj1 = {
name: 'seven',
getName: function() {
return this.name
}
}
var obj2 = {
name : 'jack'
}
console.log(obj1.getName()) ;//seven
console.log(obj1.getName.call(obj2)) //jack複製程式碼
call和apply能很好的體現javascript的函式語言特性,在javascript中,幾乎每一次編寫函式式語言風格程式碼都離不開call和apply。在javascript諸多版本的設計模式中,也用到了call和apply。在以後我們分析中會更多說到。
(2)丟失的this
這是一個經常遇到的問題
var obj = {
myname : 'sven',
getNname : function() {
return this.myname;
}
}
console.log(obj.getNname()) // sven
var getname2 = obj.getNname
console.log(getname2()) ;//undefined複製程式碼
當呼叫obj.getName時,getName方法是作為obj物件的屬性被呼叫的。(本文1.1)此時,this指向obj物件。
所以obj.getName輸出 'sven'
當另外一個變數getName2來引用obj.getName,並且呼叫getname2時,(本文1.2)提到的規律,此時是普通函式呼叫方式,this是指向全域性window的,所以程式執行的是undefined.
我們再來看一個例子。
document.getElementById()這個方法名字實在有點長。我們嘗試用一個短的函式代替它。
var getId = function(id) {
return document.getElementById(id)
}
getId('div1')複製程式碼
我們也許想過為什麼不用下面更簡單方式
var getId = document.getElementById;
getId('div1')複製程式碼
我們在瀏覽器中執行會出現一個錯誤,這是因為許多瀏覽器引擎的document.getElementById方法內部需要用到this。這個this本來被期望指向document,當getElementById方法作為document物件屬性被呼叫時,方法內部的this確實是指向document的。
當getId來引用document.ElementById 之後,再呼叫getId,此時就成了普通函式呼叫,函式內部的this指向了window,而不是原來的document.
我們可以嘗試著利用apply把document當做this傳入getId函式。幫助修正this
document.getElementById = (function( func ) {
return function() {
return func.apply( document, arguments )
}
})(document.getElementById)
var getId = document.getElementById;
var div = getId('div1');
console.log(div) ;//<div id="div1">div1</div>
console.log(div.id) ;//div1複製程式碼
二:call和apply
ES3給Function的原型定義了兩個方法。Function.prototype.call和Function.prototype.apply。在實際開發中,特別是在一些函式式程式碼編寫中,call和apply方法尤其有用。在javascript的設計模式中,應用也十分廣泛。能熟練應用這兩個方法,是成為一名javascript程式設計師的重要一步。
(1)call和apply的區別。
Function.prototype.call和Function.prototype.apply都是非常常用的方法,它們的作用一模一樣,區別僅僅在傳入引數形式的不同。
apply接受兩個引數,第一個引數指定了函式體內this物件的指向,第二個引數為一個帶下標的集合(這個集合可以是陣列,也可以為類陣列)。apply方法把這個集合的元素作為引數傳遞給被呼叫的函式。
var func = function( a, b, c ){
console.log([a,b,c]) ;//輸出[1, 2, 3]
}
console.log(func())
func.apply(null, [1,2,3])複製程式碼
在這段程式碼中,引數1,2,3被放在陣列中一起傳入func函式。它們分別對應func引數列表中的a,b,c
call傳入引數數量不固定,跟apply相同的是,第一個引數也是代表函式體內的this指向,從第二個引數開始往後,每個引數被依次傳入函式。
var func = function( a, b, c ) {
console.log([a, b, c]) //[1, 2, 3]
}
func.call( null, 1, 2, 3 ) 複製程式碼
當呼叫一個函式時,javascript的直譯器並不會計較形參和實參的數量,型別以及順序上的區別,javascript的引數在內部就是用一個陣列來表示的。從這個意義上說,apply比call的使用率更高。我們不必關心具體有多少引數被傳入函式,只要apply一股腦的推過去就行。
call是包裝在apply上面的一顆語法糖,如果我們明確知道了函式接受多少個引數,而且想一目瞭然的表達形參和實參的對應關係。那麼也可以用call來傳送引數。
當使用call或apply的時候,如果我們傳入的第一個引數為null,函式體內的this會指向預設的宿主物件。在瀏覽器中則是window.
var func = function( a, b, c ){
console.log( this === window ) //true
};
func.apply(null,[1,2,3])複製程式碼
但是在嚴格模式下,函式體內的this還是為null
var func = function( a, b, c ){
"use strict"
console.log( this === null ) //true
};
func.apply(null,[1,2,3])複製程式碼
有時我們使用call或者apply的目的不在於指定this指向,而是另有用途,比如借用其它的物件方法。那麼我們可以傳入null來代替某個具體物件。
Math.max.apply(null,[1,2,3,4,5,6,7]) //7複製程式碼
(2)call和apply的用途
前面說過,能夠熟練使用call和apply,是成為一名正真的javascript程式設計師的重要一步,下面我們就來詳細說說call和apply在實際開發中的用途。
1).改變this的指向
call和apply最常見的用途就是改變this的指向,下面我們來看個例子:
var obj1 = {
name : 'seven'
}
var obj2 = {
name : 'anne'
}
window.name = 'window'
var getName = function() {
console.log(this.name)
}
getName();
getName.call(obj1)
getName.call(obj2)複製程式碼
當執行getName.call(obj1)時,getName函式體內的this就指向obj1物件,所以此處的
var getName = function() {
console.log(this.name)
}複製程式碼
相當於:
var getName = function() {
console.log(obj.name)
}複製程式碼
在實際開發中,經常會遇到this指向被不經意改變的場景,比如有一個div節點,func函式體內的this就指向的window,而不是我們預期的div.
<div id="div1">div1</div>
<script type="text/javascript">
document.getElementById('div1').onclick = function(){
console.log(this.id) //div1
}
</script>複製程式碼
假如該事件函式中有一個內部函式func,在事件的內部呼叫 func函式時,函式體內的this就指向了window,而不是我們預期的div,見如下程式碼:
document.getElementById('div1').onclick = function(){
console.log(this.id) //div1
function func(){
console.log(this.id) //undefined
}
func()
}複製程式碼
這個時候,我們用call來修正func函式內的this,使其依然指向div
document.getElementById('div1').onclick = function(){
console.log(this.id) //div1
function func(){
console.log(this.id) //div1
}
func.call(this)
}複製程式碼
使用call修正this的場景,我們並非第一次遇到,上一節中,我們曾經修復過document.getElementById函式內部“丟失”的this,程式碼如下:
document.getElementById = (function( func ){
return function() {
return func.apply( document, arguments );
}
})( document.getElementById )
var getId = document.getElementById
var div = getId('div1')
console.log(div.id) //div1複製程式碼
2).Function.prototype.bind
大部分高階瀏覽器都實現了內建的Function.prototype.bind,用來指定函式內部的this指向。(即使沒有原生的Function.prototype.bind,模擬起來也不算難事)
Function.prototype.bind = function( context ) {
var self = this;
return function() {
return self.apply( context, arguments )
}
};
var obj = {
name : 'seven'
}
var getName = function() {
console.log(this.name)
}.bind(obj)
getName() ; //seven複製程式碼
我們通過Function.prototype.bind來“包裝” func函式,並且傳入一個物件context當做引數,這個context物件就是我們要修正的this物件。
3).借用其它物件的方法
我們知道,杜鵑既不會築巢,也不會孵鳥,而是把自己的蛋生在其它的鳥巢,讓他們代為孵化和養育,同樣,在javascript中也存在借用現象。
借用的第一種方法是“借用建構函式”,通過技術,可以實現一些類似的繼承結果。
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複製程式碼
借用方法的第二種運用場景跟我們的關係更密切。
函式的引數列表 arguments 是一個類陣列物件,雖然它也有下標,但它並非正真的陣列,所以不能像陣列一樣,進行排序操作或者往集合裡新增一個新的元素。這種情況下,我們常常會借用Array.prototype物件上的方法,比如:想往argumments中新增一個新的元素,通常會借用Array.prototype.push
(function(){
Array.prototype.push.call( arguments, 3)
console.log(arguments) //[12, 1, 1, 23, 3]
})(12,1,1,23)複製程式碼
在操作arguments時,我們非常頻繁的找Array.prototype物件借用方法。
想把arguments轉成真正的陣列的時候,可以借用Array.prototype.slice方法,想擷取arguments列表中的頭一個元素時,可以使用Array.prototype.shift方法,這種機制的內部原理,我們可以翻開V8引擎原始碼,以Array.prototype.push方法為例。看看其實現
function ArrayPush() {
var n = TO_UINT32( this.length ); //被push的物件的length
var m = %_ArgumentsLength(); //push的引數個數
for (var i = 0; i < m; i++) {
this[i + n] = %_ArgumentsLength( i ); //複製元素 (1)
}
this.length = n + m; //修正length屬性的值 (2)
return this.length;
};複製程式碼
從這段程式碼我們看出,Array.prototype.push實際上是一個屬性複製的過程,把引數按照下標依次新增到被push的物件上面,順便修改了這個物件的length屬性,至於被修改的物件是誰,到底是陣列還是類陣列物件,這一點並不重要。
由此,我們可以推斷,我們把“任意物件”傳入Array.prototype.push:
var a = {};
Array.prototype.push.call(a, 'frist');
Array.prototype.push.call(a, 'second');
console.log(a.length) //2
console.log(a[0]) //frist複製程式碼
對於“任意物件”,我們從ArrayPush()函式的(1)和(2)可以猜到,這個物件還要滿足:
- 物件的本身可以讀取屬性
- 物件的lenth屬性可讀寫