jsvascript 中的 this/apply/call
this
-
定義:javascript 中 this 總是指向一個對象,指向哪個對象,則根據函數執行環境動態綁定,不是函數聲明。
-
this 的指向
1.對象方法調用
var obj = { a:0, getA:function(){ console.log(this === obj); // true console.log(this.a) // 0 } } obj.getA()
分析:
obj.getA()
這個 getA 方法屬於 obj 對象,而obj.getA()
,這 obj 屬於宿主對象直接調用方法,該函數裏 this 指向宿主對象,無論是在嚴格模式下還是非嚴格模式下,this 依然指向宿主對象
-
普通函數調用
注: 有一點得注意,就是對象方法賦值變量問題,將這種情況拆開分析- 函數全局調用
window.name = 'ldy' function whoIsName (){ return this.name } console.log(whoIsName()) // ldy
分析: 方法
whoIsName
被默認掛載到全局對象 window 上,是可以通過window.whoIsName
獲取該方法。而調用whoIsName()
方法時,而該函數裏 this 特性總是指向一對象,所以該函數裏 this 指向 window。其實在嚴格模式下,該函數裏的 this 是 undefined,那是因爲該函數被認爲沒有宿主對象,所以爲 undefined;其實你可以將window.whoIsName = function(){return this.name}
,這時 this 纔會指向 window,我說的這是在嚴格模式。-
對象賦值調用函數
``` window.name = 'ldy' var myObj = { name:'12', getName:function(){ return this.name }, } var getName = myObj.getName; console.log(getName()); // ldy ```
分析: 將 myObj 對象的 getName 方法賦值 getName 變量,這時這個方法被間接掛在了 window 上,所以 this 就指向了 window,在嚴格模式下也是無法找對 this 的宿主對象,也是 undefined;
-
構造器調用 this
var Myself = function(){ this.name = 's'; // return 'l' return { l:'l' } } var obj = new Myself(); console.log(obj, obj.name)
分析: 進行對象覆蓋問題,return 的對象字面量對類實例的覆蓋;而字符串是無法進行覆蓋,由於值內存和地址內存導致的主要原因。地址內存:返回的引用類型,存放於地址內存,返回的基本類型存放於值內存(棧內存)。
-
Function.prototype.call 或 Function.prototype.apply 調用
- 可以動態改變函數內部的 this 指向
var myObj = { name:'ldy', getName:function(){ return this.name } } var on = { name:"sly" } console.log(myObj.getName());// ldy console.log(myObj.getName.call(on))// sly
分析: 將函數的方法的宿主對象改變,將掛載在 myObj 的 getName 函數掛載到了 on 對象上;導致我們看到 this 指向的改變。
call 和 apply
-
區別:作用一致,參數形式不同;
var func = function( a, b, c ){ console.log ( [ a, b, c ] ); // 輸出 [ 1, 2, 3 ] }; // func.apply( null, [ 1, 2, 3 ] ); func.call(null, 1,2,3)
分析: 上面的代碼只是用於傳參,用於 this 指向的改變,可以參考 this 指向的第四點,此地,不再贅述。
-
bind 函數實現:其實在大部分瀏覽器都基本內置了 bind 方法,來改變 this 的指向,但是是如何實現 bind 方法的,請參考下面的代碼:
Function.prototype.bind = function(context){ var that = this; var context = [].shift.call(arguments);// 借用[].shift方法,給掛載到了arguments類數組上並執行shift方法 var args = [].slice.call(arguments); return function(){ // args 傳進去是可行;[].concat.call( args, [].slice.call( arguments ) ),這個傳進去也是可行的; // 具體會有差別,目前還沒有認證其差別 that.apply((!this|| this===window)?context:that, [].concat.call( args, [].slice.call( arguments ) )) } }
分析: bind 函數實現,也是借用了 apply 方法的實現,利用 call 實現,可以藉助 es6 語法去實現,理解其原理即可,
that.call(context, ...args)
,其他實現方法,還未有找到資料去證明其實現方案。 -
通過 call 或者 apply 綁定實現繼承
var A = function(){ this.ca = function(){} this.l = 'l' } A.app = function(){} var B = function(){ A.call(this, arguments) } var b = new B(); console.log(b)
- 其實通過 call 或 apply 實現繼承,有個問題,無法繼承原型方法
- 接下來實現原型繼承:
var A = function(){ this.ca = function(){} this.l = 'l' this.a=['q'] } A.prototype.app = function(){} A.a = function(){} var a = new A(); var B = function(){ } B.prototype = a; var b = new B(); var b1 = new B() b1.a.push(23) console.log(b, b1)
分析: 子類繼承父類所有原型和類方法,但是不同實例中改變父類的屬性,會影響其他子類實例的獲取父類屬性的值。
- 組合模式繼承
var A = function(){ this.ca = function(){} this.l = 'l' this.a=['q'] } A.prototype.app = function(){} A.a = function(){} var a = new A(); var B = function(){ A.call(this, arguments) } B.prototype = a; var b = new B(); var b1 = new B() b1.a.push(23) console.log(b, b1)
分析: 組合繼承,不同實例間獲取父類的值,改變父類的值,互不干擾。
總結: 本篇先簡單的介紹這三種,其實還有原型式,寄生式等,下篇將會介紹 js 面向對象多態與繼承等問題。
-
閉包中 this 指向:指向全局對象,當然也是可以通過 call 或者 apply 去修改
var name = 'oi' var c = { name:'l', a:function(){ return function(){ return this.name } } } console.log(c.a()()) // oi
分析: 調用 c.a()時,返回一個 function 函數,這個函數默認掛載到全局,作用域爲全局,其實可以做個實驗,將 c.a()給掛到另一個對象中,那麼 this 的指向,就指向被掛載到的那個對象。
-
硬綁定:修改一次,無法再次修改 this 的指向,
var f = function(){ console.log(this.n) } var o = { n:"876" } var ba = function(){ f.call(o) } ba() ba.call(window)
-
軟綁定:爲了保持和硬綁定同樣的效果,並且保留隱式綁定或者顯示綁定修改 this 的能力
Function.prototype.softBind = function(){ var that = this; var context = [].shift.call(arguments); var args = [].slice.call(arguments); var bound = function(){ return that.apply((!this|| this===window)?context:this, [].concat.call( args, [].slice.call( arguments ) )) } // bound.prototype = Object.create(that.prototype) // 繼承function原型特性 return bound }
實例:
var foo = function(){ // console.log(this) console.log(this.sco) } var liu1 = { sco:'sco1' } var liu2 = { sco:"sco2" } var liu3 = { sco:'sco3' } var fool = foo.softBind(liu1); fool(); // sco1 liu2.foo = foo.softBind(liu1);// 隱式綁定 liu2.foo(); // sco2 fool.call(liu3); // sco3 顯示綁定
-
箭頭函數 this 指向問題:箭頭函數並不是使用 function 關鍵字定義使用操作符來定義的=>,他的指向根據最外層作用域來決定的 this。
-
實例 1:
var o2 = { l:'09', } var o1 = { l:1 } var j = function(){ return ()=>{ console.log(this.l) } } var b1 = j.call(o1) b1.call(o2) // 1
-
實例 2:
var o1 = { l:1 } var fn = function(){ setTimeout(() => { console.log(this.l) }, 100); } fn.call(o1)
小結: 上面兩則實例是對箭頭函數的 this 取決於最外層函數的作用域。
總結: 對於上面介紹的 this 的指向綁定,在這裏分爲幾類:隱式綁定;顯示綁定;new 綁定三類;在顯示綁定存在軟綁定和硬綁定;那什麼是隱式綁定呢,其實隱式綁定其實就是直接寫進對象裏的函數或者通過 function 定義的全局的函數;顯示綁定,就是通過 call 或者 apply 改變函數的作用域;new 綁定通過 new 一個實例來改變函數或者類的作用域。軟綁定呢,就是實例中對 softBind 函數的實現方案,就是軟綁定。默認綁定到全局對象的函數,在嚴格模式下 this 是 undefined。
參考文檔
- javascript 設計模式與開發實踐
- 你不知道的 javascript(上卷)