在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屬性可讀寫
(本文已經完結)
上一篇文章:(一)面向對象的javascript 下一篇文章 (三)閉包和高階函數
