在javascript中,this關鍵字總讓一些初學者迷惑,Function.prototype.call, Function.prototype.apply這兩個方法普遍的運用。咱們有必要理解這幾個概念。javascript
一:thishtml
跟別的語言截然不同的是,javascript的this老是指向一個對象,而具體指向那個對象在運行時基於函數的執行環境動態綁定的,非函數被聲明時的環境。java
(1).this的指向程序員
除去不經常使用的with和eval狀況,具體到實際的應用中,this的指向大體分爲下面4種。設計模式
簡單作介紹:數組
1)做爲對象的方法調用瀏覽器
當函數做爲對象的方法被調用時,this指向該對象:閉包
var obj = { a:1, getA:function() { alert( this === obj ); //true alert( this.a ); //1 } } obj.getA()
2)做爲普通函數調用app
當函數不做爲對象的屬性被調用時,也就是咱們常說的普通函數方式,此時的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)能夠猜到,這個對象還要知足:
(本文已經完結)
上一篇文章:(一)面向對象的javascript 下一篇文章 (三)閉包和高階函數