介紹
this 可以說是 javascript 中最耐人尋味的一個特性,學習this 的第一步就是明白 this 既不是指向函數自身也不指向函數的作用域。 this 實際上是在函數被調用時發生的綁定,它指向什麼地方完全取決於函數在哪裡被調用。
為什麼需要綁定this
this代指當前的上下文環境,在不經意間容易改變:
var info = "This is global info"; var obj = { info: 'This is local info', getInfo: getInfo } function getInfo() { console.log(this.info); } obj.getInfo() //This is local info getInfo() //This is global info 當前上下文環境被修改了
在上面的例子中,我們在對象內部創建一個屬性getInfo,對全局作用域下的getInfo進行引用,而它的作用是打印當前上下文中info的值,當我們使用obj.getInfo
進行調用時,它會打印出對象內部的info的值,此時this指向該對象。而當我們使用全局的函數時,它會打印全局環境下的info變量的值,此時this指向全局對象。
這個例子告訴我們:
1、同一個函數,調用的方式不同,this的指向就會不同,結果就會不同。
2、對象內部的屬性的值為引用類型時,this的指向不會一直綁定在原對象上。
其次,還有不經意間this丟失的情況:
var info = "This is global info"; var obj = { info: 'This is local info', getInfo: function getInfo() { console.log(this.info); var getInfo2 = function getInfo2() { console.log(this.info); } getInfo2(); } } obj.getInfo(); //This is local info //This is global info
上面的例子中,對象obj中定義了一個getInfo方法,方法內定義了一個新的函數,也希望拿到最外層的該對象的info屬性的值,但是事與願違,函數內函數的this被錯誤的指向了window全局對象上面,這就導致了錯誤。
解決的方法也很簡單,在最外層定義一個變量,存儲當前詞法作用域內的this指向的位置,根據變量作用域的關系,之後的函數內部還能訪問這個變量,從而得到上層函數內部this的真正指向。
var info = "This is global info"; var obj = { info: 'This is local info', getInfo: function getInfo() { console.log(this.info); var self = this; //將外層this保存到變量中 var getInfo2 = function getInfo2() { console.log(self.info); //指向外層變量代表的this } getInfo2(); } } obj.getInfo(); //This is local info //This is local info
然而這樣也會有一些問題,上面的self變量等於重新引用了obj對象,這樣的話可能會在有些時候不經意間修改了整個對象,而且當需要取得多個環境下的this指向時,就需要聲明多個變量,不利於管理。
有一些方法,可以在不聲明類似於self這種變量的條件下,綁定當前環境下的上下文,確保編程內容的安全。
如何綁定this
1. call, apply
call和apply是定義在Function.prototype
上的兩個函數,他們的作用就是修正函數執行的上下文,也就是this的指向問題。
以call為例,上述anotherFun想要輸出local thing就要這樣修改:
... var anotherFun = obj.getInfo; anotherFun.call(obj) //This is local info
函數調用的參數:
Function.prototype.call(context [, argument1, argument2 ])
Function.prototype.apply(context [, [ arguments ] ])
從這裡就可以看到,call和apply的第一參數是必須的,接受一個重新修正的上下文,第二個參數都是可選的,他們兩個的區別在於,call從第二個參數開始,接受傳入調用函數的值是一個一個單獨出現的,而apply是接受一個數組傳入。
function add(num1, num2, num3) { return num1 + num2 + num3; } add.call(null, 10, 20, 30); //60 add.apply(null, [10, 20, 30]); //60
當接受的context為undefined或null時,會自動修正為全局對象,上述例子中為window
2. 使用Function.prototype.bind進行綁定
ES5中在Function.prototype
新增了bind
方法,它接受一個需要綁定的上下文對象,並返回一個調用的函數的副本,同樣的,它也可以在後面追加參數,實現函數的柯裡化。
Function.prototype.bind(context[, argument1, argument2]) //函數柯裡化部分 function add(num1 ,num2) { return num1 + num2; } var anotherFun = window.add.bind(window, 10); anotherFun(20); //30
同時,他返回一個函數的副本,並將函數永遠的綁定在傳入的上下文中。
... var anotherFun = obj.getInfo.bind(obj) anotherFun(); //This is local info
polyfill
polyfill是一種為了向下兼容的解決方案,在不支持ES5的老舊浏覽器上,如何使用bind方法呢,就得需要使用舊的方法重寫一個bind方法。
if (!Function.prototype.bind) { Function.prototype.bind = function (obj) { var self = this; return function () { self.call(obj); } } }
上面的寫法實現了返回一個函數,並且將上下文修正為傳入的參數,但是沒有實現柯裡化部分。
... Function.prototype.bind = function(obj) { var args = Array.prototype.slice.call(arguments, 1); //記錄下所有第一次傳入的參數 var self = this; return function () { self.apply(obj, args.concat(Array.prototype.slice.call(arguments))); } }
當使用bind進行綁定之後,即不能再通過call,apply進行修正this指向,所以bind綁定又稱為硬綁定。
3. 使用new關鍵字進行綁定
在js中,函數有兩種調用方式,一種是直接進行調用,一種是通過new關鍵字進行構造調用。
function fun(){console.log("function called")} //直接調用 fun() //function called //構造調用 var obj = new fun() //function called
那普通的調用和使用new關鍵字的構造調用之間,又有哪些區別呢?
准確的來說,就是new關鍵字只是在調用函數的基礎上,多增加了幾個步驟,其中就包括了修正this指針到return回去的對象上。
var a = 5; function Fun() { this.a = 10; } var obj = new Fun(); obj.a //10
幾種綁定方式的優先級比較
以下面這個例子來進行幾種綁定狀態的優先級權重的比較
var obj1 = { info: "this is obj1", getInfo: () => console.log(this.info) } var obj2 = { info: "this is obj2", getInfo: () => console.log(this.info) }
1. call,apply和默認指向比較
首先很顯然,根據使用頻率來想,使用call和apply會比直接調用的優先級更高。
obj1.getInfo() //this is obj1 obj2.getInfo() //this is obj2 obj1.getInfo.call(obj2) //this is obj2 obj2.getInfo.call(obj1) //this is obj1
使用call和apply相比於使用new呢?
這個時候就會出現問題了,因為我們沒辦法運行類似 new function.call(something)
這樣的代碼。所以,我們通過bind方法返回一個新的函數,再通過new判斷優先級。
var obj = {} function foo(num){ this.num = num; } var setNum = foo.bind(obj); setNum(10); obj.num //10 var obj2 = new setNum(20); obj.num //10 obj2.num //20
通過這個例子我們可以看出來,使用new進行構造調用時,會返回一個新的對象,並將this修正到這個對象上,但是它並不會改變之前的對象內容。
那麼問題來了,上面我們寫的bind的polyfill明顯不具備這樣的能力。而在MDN上有一個bind的polyfill方法,它的方法如下:
if (!Function.prototype.bind) { Function.prototype.bind = function (oThis) { if (typeof this !== "function") { throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); } var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function () {}, fBound = function () { return fToBind.apply(this instanceof fNOP ? this : oThis || this, aArgs.concat(Array.prototype.slice.call(arguments))); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; }; }
上面的polyfill首先判斷需要綁定的對象是否為函數,防止使用Function.prototype.bind.call(something)
時,something不是一個函數造成未知錯誤。之後讓需要返回的fBound函數繼承自this,返回fBound。
特殊情況
當然,在某些情況下,this指針指向也存在一些意外。
箭頭函數
ES6中新增了一種定義函數方式,使用"=>"進行函數的定義,在它的內部,this的指針不會改變,永遠指向最外層的詞法作用域。
var obj = { num: 1, getNum: function () { return function () { //this丟失 console.log(this.num); //此處的this指向window } } } obj.getNum()(); //undefined var obj2 = { num: 2, getNum: function () { return () => console.log(this.num); //箭頭函數內部綁定外部getNum的this,外部this指向調用的對象 } } obj2.getNum()(); //2
軟綁定
上面提供的bind方法可以通過強制修正this指向,並且再不能通過call,apply進行修正。如果我們希望即能有bind效果,但是也能通過call和apply對函數進行二次修正,這個時候就需要我們重寫一個建立在Function.prototype
上的方法,我們給它起名為"軟綁定"。
if (!Function.prototype.softBind) { Function.prototype.softbind = function (obj) { var self = this; var args = Array.prototype.slice.call(arguments, 1); return function () { return self.apply((!this || this === (window || global)) ? obj : this, args.concat(Array.prototype.slice.call(arguments))); } } }
bind,call的妙用
在平日裡我們需要將偽數組元素變為正常的數組元素時,往往通過Array.prototype.slice
方法,正如上面的實例那樣。將arguments這個對象變為真正的數組對象,使用 Array.prototype.slice.call(arguments)
進行轉化.。但是,每次使用這個方法太長而且繁瑣。所以有時候我們就會這樣寫:
var slice = Array.prototype.slice; slice(arguments); //error
同樣的問題還出現在:
var qsa = document.querySelectorAll; qsa(something); //error
上面的問題就出現在,內置的slice和querySelectorAll方法,內部使用了this,當我們簡單引用時,this在運行時成為了全局環境window,當然會造成錯誤。我們只需要簡單的使用bind,就能創建一個函數副本。
var qsa = document.querySelectorAll.bind(document); qsa(something);
同樣的,使用因為call和apply也是一個函數,所以也可以在它們上面調用bind方法。從而使返回的函數的副本本身就帶有修正指針的功能。
var slice = Function.prototype.call.bind(Array.prototype.slice); slice(arguments);
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或工作能帶來一定的幫助,如果有疑問大家可以留言交流。