javascript
javascript:this 关键字
前言
看過[阮一峰]()的關于 this 的教程,講了很多比較好的例子,但沒有對其本質的東西解釋清楚,而且部分例證存在問題;于是,打算重寫本章節(jié),從this的本質入手;
本文為作者的原創(chuàng)作品,轉載需注明出處;
this 是什么?
this可以理解為一個指針,指向調用對象;
判斷 this 是什么的四個法則
官網(wǎng)定義
先來看第一段官方的解釋,
In JavaScript, as in most object-oriented programming languages, this is a special keyword that is used within methods to refer to the object on which a method is being invoked. The value of this is determined using a simple series of steps:
大致翻譯如下,
this是這么一個特殊的關鍵字,它是用來指向一個當前正在被調用( a being invoked )方法的調用對象的;( 等等,這句話其實隱藏了一個非常關鍵的信息,那就是this是在運行期 生效的,怎么生效的?在運行期,this被賦值,將某個對象賦值給this,與聲明期無關,也就是說,this是運行期相關的 );this的賦值場景,歸納起來,分為如下四種情況,
參考 function prototype call 小節(jié)
參考 function prototype bind 小節(jié)
備注:被一個對象調用?何解?其實就是指語句obj.func(),這個時候,func()方法內部的this將會被賦值為obj對象的引用,也就是指向obj;
獨立方法 ( standalone function )?在運行期,如果func方法被obj關聯(lián)調用的,既是通過obj.func()的方式,那么它就不是standalone的;如果是直接被調用,沒有任何對象關聯(lián),既是通過func()調用,那么這就是standalone的。
this 運行期相關
官網(wǎng)定義 2
再來看另外一句非常精煉的描述,來加深理解
The this keyword is relative to the execution context, not the declaration context.this關鍵字與運行環(huán)境有關而與聲明環(huán)境無關;(補充,而作用域鏈和閉包是在函數(shù)的聲明期創(chuàng)建的,參考創(chuàng)建時機)
補充,是如何與函數(shù)的運行期相關的,參考this 指針運行時賦值
我的補充
法則 #3 和 #4,大多數(shù)情況都非常容易理解,有幾種情況需要特別注意,
函數(shù)嵌套
需要注意的是object對象中的函數(shù)內部再次嵌套函數(shù)的情況,
執(zhí)行
> obj.f1();this: objectthis: windows可以看到,在運行期,被調用函數(shù) f1() 中的this指向 obj_,而被調用函數(shù) _f2() 中的this指向的是 windows ( global object );因為 f1 函數(shù)在當前的運行時中是通過 obj.f1() 進行的關聯(lián)調用,所以,根據(jù)定義 #3,在當前的運行期間,_f1()_ 內部的 this 是指向 obj 對象的( 通過將 obj 的引用直接賦值給 this ),而, f2 函數(shù)在運行期是沒有與其它 object 進行關聯(lián)調用,所以,在當前的運行時期,_f2_ 是一個 standalone 的函數(shù),所以,根據(jù)定義 #4,在當前的運行期間,_f2()_ 的內部this是指向 windows 的。(注意,這里我反復強調當前運行期間,是因為this是在運行時被賦值的,所以,要特別注意的是,即使某個函數(shù)的定義不變,但在不同的執(zhí)行環(huán)境(運行環(huán)境)中,this是會發(fā)生變化;)
參看函數(shù)回調場景-1和函數(shù)回調場景-2
參看將函數(shù)賦值-standalone以及相關變種章節(jié)
可見,要判斷this在運行期到底指的是什么,并沒有那么容易,但是,只要牢牢的把握好兩點,就可以迎刃而解,
- this是運行期相關的
更確切的說,this是在運行期被賦值的,所以,它的值是在運行期動態(tài)確定的。 - this是否與其它對象關聯(lián)調用
這里的關聯(lián)調用指的是 javascript 的一種語法,既是調用語句顯式的寫為obj.func(),另外需要注意的是,_javascript_ 方法的調用不會隱式的隱含 this。只要沒有顯式的關聯(lián)調用,那么就是standalone的調用,就符合法則 #4,所以,this指向 _Global Object_。
this 的 Object
注意,this定義中所指的Object指的是 javascript 的 Object 類型,既是通過
var o1 = {}; var o2 = new Object(); var o3 = Object.create(Object.prototype);這樣的方式構建出來的對象;
備注,最開始,自己有個思維的誤區(qū),認為既然 javascript 一切皆為對象,那么this指針是指向對象的,那么是不是也可以指向Function,Number等對象?答案是否定的。
起初,我是按照上面的邏輯來理解的,直到當我總結到bind 是如何實現(xiàn)的小節(jié)后,發(fā)現(xiàn)Function對象在調用方法屬性bind的時候,bind方法內部的this指向的是Function,這才恍然大悟,this的Object實際上是可以指向任何 javascript Object的,包括 Object_、_Function 等。
this 是變化的
我們來看這樣一個例子,
var C = "王麻子";var A = {name: '張三',describe: function () {return '姓名:'+ this.name;} };var B = {name: '李四' };// 執(zhí)行, > A.describe();'張三'> B.describe = A.describe; > B.describe()'李四'> var describe = A.describe; > describe();'王麻子'可以看到,雖然 A.describe 方法的定義不變,但是其運行時環(huán)境發(fā)生了變化,_this_ 的指向也就發(fā)生了變化。
> B.describe = A.describe; > B.describe()'李四'在運行時,相當于運行的是 B 的 describe 方法
> var describe = A.describe; > describe();'王麻子'在運行時,相當于運行的是 windows 的 describe 方法
方法調用沒有隱含 this
經(jīng)常寫 Java 代碼的原因,經(jīng)常會習慣性的認為只要在對象方法里面調用某個方法或者屬性,隱含了 this,比如
public class Person{String name;public String getName(){return name;}public String getName2(){return this.name;}}而 Javascript 實際上并沒有這種隱含的表達方式;詳細驗證過程參考將函數(shù)賦值-standalone
關聯(lián)調用 - 容易混淆的場景
從this 是什么章節(jié)中,為了方便對 #3 進行描述,我起了個名字叫做 關聯(lián)調用 ;那么有些情況看似是 _關聯(lián)調用_,實則不然;
我們有一個標準的對象,定義如下,
var name = "windows"; var obj = {name: "obj",foo: function () {console.log("this: "+ this.name);} };通過標準的 關聯(lián)調用 的方式,我們進行如下的調用,
> obj.foo() 'this: obj'根據(jù)法則 #3 既 關聯(lián)調用 的定義,得到 this -> obj_;如果事事都如此的簡單,如此的標準,那可就好了,總會有些讓人費解的情況,現(xiàn)在來看看如下的一些特殊的例子,加深對 _關聯(lián)調用 的理解。
將函數(shù)賦值 - standalone
> var fooo = obj.foo > fooo();'this: windows'輸出的 windows_,既是 _this -> global object_,而不是我們期望的 _obj_;為什么?原因是,_obj.foo 其實是 foo 函數(shù)的函數(shù)地址,通過 var fooo = obj.foo 將該函數(shù)的地址賦給了變量 _fooo_,那么當執(zhí)行
> fooo();的時候,fooo() 執(zhí)行的是是一個standalone的方法,根據(jù)法則 #4,所以該方法內部的this指向的是 Global Object_;注意,_obj.foo 表示函數(shù) foo 的入口地址,所以,變量 fooo 等價與 foo 函數(shù)。
備注:由于受到寫 Java 代碼習慣的原因,很容易將這里解釋為默認執(zhí)行的是this.fooo(),_fooo()_ 的調用隱含了this,因此就會想到,由于this指向的 Global Object_,所以這里當然返回的就是this: windows;但是,這樣解釋,是不對的,因為 _Javascript 壓根沒有這種隱含this的概念,參看用例,
var name = "windows";var o = {name : "o",f2 : function(){console.log( "o -> f2");console.log( "this: "this.name );},f : function(){console.log("f.this -> " + this.name);var f2 = function(){console.log( "f -> f2");console.log( this.name );}f2(); // f -> f2this.f2(); // o -> f2}}可以看到,在 o.f() 函數(shù)中,如果 f2() 的調用隱含了this,那么 f2() 和 this.f2() 兩者調用應該是等價的;但是,在實際執(zhí)行過程中,_f2()_ 和 this.f2() 執(zhí)行的是兩個截然不同的方法,因此 f2() ≠ this.f2()_,所以 _f2() 并沒有隱示的表示為 _this.f2()_;
將函數(shù)賦值變種 - 匿名 standalone 函數(shù)立即執(zhí)行
> (obj.foo = obj.foo)() 'this: windows'首先,立即執(zhí)行 foo 函數(shù),然后將 foo 函數(shù)賦值給對象 obj 對象的 foo 屬性;等價于執(zhí)行如下的代碼,
var name = "windows"; var obj = { name : "obj" }; (obj.foo = function () {console.log("this: " + this.name); })();輸出,
'this: windows'可以看到,_this_ -> _global object_,這里為什么指向的是 _global object_?其實這里的立即執(zhí)行過程,就是執(zhí)行的如下代碼,
(function () {console.log("this: " + this.name); }());由此可以看出,實際上進行一個匿名函數(shù)的立即執(zhí)行;也就是說執(zhí)行過程中并沒有使用 關聯(lián)調用_,而是一次 _standalone 函數(shù)的自身調用,所以根據(jù)法則 #4,_this_ -> _global object_。執(zhí)行完以后,將該匿名函數(shù)賦值給 _obj.foo_。
再次執(zhí)行,
> obj.foo();'this: obj'這次執(zhí)行的過程是一次標準的 關聯(lián)調用 過程,所以根據(jù)法則 #3,_this_ -> _obj_。
作為判斷條件 - 匿名函數(shù)立即執(zhí)行
> (false || obj.foo)() 'windows'等價于執(zhí)行,
(false || function () {console.log("this: " + this.name); })()原理和函數(shù)賦值變種-匿名 standalone 函數(shù)立即執(zhí)行 一致,等價于立即執(zhí)行如下的匿名函數(shù)
(function () {console.log("this: " + this.name); })()其實,把這個例子再做一個細微的更改,其中邏輯就看得更清楚了,為 foo 函數(shù)添加一個返回值 return true
var name = "windows"; var obj ={name: "obj",foo: function () {console.log("this: "+ this.name);return true;} };再次執(zhí)行,
> (false || obj.foo)() 'windows'true可見,_obj.foo_ 函數(shù)執(zhí)行以后,返回 _true_。上述代碼其實等價于執(zhí)行如下的代碼,
(false || function () {console.log("this: " + this.name);return true; })()函數(shù)回調場景 0 - 基本原理
var counter = {count: 0,inc: function () {'use strict';this.count++;} };function callIt(callback) {callback(); }> callIt(counter.inc)TypeError: Cannot read property 'count' of undefined可以看到,把一個定義有this關鍵字的函數(shù)作為其它函數(shù)的回調函數(shù),是危險的,因為this在運行期會被重新賦值,上述例子很直觀的描述了這一點,之所以報錯,是因為this指向了 _Global Object_。要解決這樣的問題,可以使用bind,調用的時候改為
> callIt(counter.inc.bind(counter))1函數(shù)回調場景 1 - setTimeout
var name = "Bob"; var nameObj ={ name : "Tom", showName : function(){ console.log(this.name); }, waitShowName : function(){ setTimeout(this.showName, 1000); } }; // 執(zhí)行,> nameObj.waitShowName();'Tom'undefinedsetTimeout(this.showName, 1000);將 nameObj.showName 函數(shù)作為回調函數(shù)參數(shù)傳遞給 setTimeout_;那么為什么當 _setTimeout 執(zhí)行回調的時候,_nameObj.showName_ 方法返回的是 undefined 呢?為什么不是返回全局對象對應的 name Bob_?原因只有一個,那就是 _setTimeout 有自己的 this 對象,而它沒有 name 屬性,而在回調 showName 函數(shù)的時候,_showName_ 函數(shù)中的 this 正是 setTimeout 上下文中的 this_,而該 _this 并沒有定義 name 屬性,所以這里返回 _undefined_。
函數(shù)回調場景 2 - DOM 對象
var o = new Object();o.f = function () {console.log(this === o); }o.f() // true,得到期望的結果 this -> o但是,如果將f方法指定給某個click事件,this的指向發(fā)生了改變,
$('#button').on('click', o.f);點擊按鈕以后,返回的是false,是因為在執(zhí)行過程中,this不再指向對象o了而改為指向了按鈕的DOM對象了;Sounds Good,但問題是,怎么被改動的?看了一下 jQuery 的源碼,_event.js_,摘錄重要的片段如下,
function on( elem, types, selector, data, fn, one ) {.......if ( one === 1 ) {origFn = fn;fn = function( event ) {// Can use an empty set, since event contains the infojQuery().off( event );return origFn.apply( this, arguments );};// Use same guid so caller can remove using origFnfn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );}....... }o.f 函數(shù)的地址賦值給 fn 參數(shù),_fn_ -> origFn_,最后是通過origFn.apply( this, arguments );來調用 _o.f 函數(shù)的,而這里的 this 就是當前的 DOM 對象,既是這個按鈕 button_;通過這樣的方式,在執(zhí)行過程中,通過回調函數(shù) _$("button").on(...) 成功的將新的 this 對象 button 注入了 o.f 函數(shù)。那么如何解決呢?參看function.prototype.apply())
的小節(jié)#3,動態(tài)綁定回調函數(shù)。
函數(shù)回調場景 3 - 數(shù)組對象方法的回調
var obj = {name: '張三',times: [1, 2, 3],print: function () {this.times.forEach(function (n) {console.log(this.name);});} };> obj.print();'undefined''undefined''undefined'這里我們期望的是,依次根據(jù)數(shù)組 times 的長度,輸出 obj.name 三次,但是實際運行結果是,數(shù)組雖然循環(huán)了三次,但是每次輸出都是 _undefined_,那是因為匿名函數(shù)
function(n){console.log(this.name); }作為數(shù)組 times 的方法 forEach 的回調函數(shù)執(zhí)行,在 forEach 方法內部該匿名函數(shù)必然是作為 standalone 方法執(zhí)行的,所以,this指向了 _Global Object_;
進一步,為什么“在 forEach 方法內部該匿名函數(shù)必然是作為 standalone 方法執(zhí)行的”?為什么必然是作為 standalone 方法執(zhí)行?是因為不能在 forEach 函數(shù)中使用 this.fn() 的方式來調用該匿名回調函數(shù)( fn 作為參數(shù)引用該匿名回調函數(shù) ),因為如果這樣做,在運行時期會報錯,因為在 forEach 函數(shù)的 this 對象中找不到 fn 這樣的屬性,而該 this 對象指向的是 obj.times 數(shù)組對象。因此,得到結論“在 forEach 方法內部該匿名函數(shù)必然是作為 standalone 方法執(zhí)行的”
解決辦法,使用 bind
obj.print = function () {this.times.forEach(function (n) {console.log(this.name);}.bind(this)); };> obj.print()'張三''張三''張三'將 obj 對象作為 this 綁定到該匿名函數(shù)上,然后再作為回調函數(shù)參數(shù)傳遞給 forEach 函數(shù),這樣,在 forEach 函數(shù)中,用 standalone 的方式調用 fn 的時候,_fn_ 中的 this 指向的就是數(shù)組對象 obj 對象,這樣,我們就能順利的輸出 obj.name 了。
綁定 this
有上述描述可知,this的值在運行時根據(jù)不同上下文環(huán)境有不同的值,因此我們說this的值是變化的,這就給我們的編程帶來了麻煩,有時候,我們期望,得到一個固定的this。Javascript 提供了call、apply以及bind這三個方法,來固定this的指向;這三個方法存儲在 function.prototype 域中,
function.prototype.call()
總結起來,就是解決函數(shù)在調用的時候,如何解決this動態(tài)變化的問題。
調用格式,
func.call(thisValue, arg1, arg2, ...)第一個參數(shù)是在運行時用來賦值給 func 函數(shù)內部的 this 的。
通過f.call(obj)的方式調用函數(shù),在運行時,將 obj 賦值給 _this_;
var obj = {};var f = function () {return this; };f() === this // true f.call(obj) === obj // truecall方法的參數(shù)是一個對象,如果參數(shù)為 空_、_null 或者 _undefined_,則使用默認的全局對象;
var n = 123; var obj = { n: 456 };function a() {console.log(this.n); }> a.call() 123 > a.call(null) 123 > a.call(undefined) 123 > a.call(window) 123 > a.call(obj) 456如果call方法的參數(shù)是一個原始值,那么這個原始值會自動轉成對應的包裝對象,然后賦值給 this
var f = function () {return this; };> f.call(5)[Number: 5]call方法可以接受多個參數(shù),第一個參數(shù)就是賦值給 this 的對象,
var obj = {name : 'obj' }function add(a, b) {console.log(this.name);return a + b; }> add.call(obj, 1, 2) obj3call方法可以調用對象的原生方法;
var obj = {}; obj.hasOwnProperty('toString') // false// “覆蓋”掉繼承的 hasOwnProperty 方法 obj.hasOwnProperty = function () {return true; }; obj.hasOwnProperty('toString') // trueObject.prototype.hasOwnProperty.call(obj, 'toString') // false方法 hasOwnProperty 是對象 obj 從 Object.prototype 中繼承的方法,如果一旦被覆蓋,就不會得到正確的結果,那么,我們可以使用call的方式調用原生方法,將 obj 作為 this 在運行時調用,這樣,變通的,我們就可以調用 obj 對象所繼承的原生方法了。
function.prototype.apply()
總結起來,和call一樣,就是解決函數(shù)在調用的時候,如何解決this動態(tài)變化的問題。
apply方法的作用與call方法類似,也是改變this指向,然后再調用該函數(shù)。唯一的區(qū)別就是,它接收一個數(shù)組作為函數(shù)執(zhí)行時的參數(shù),使用格式如下。 func.apply(thisValue, [arg1, arg2, ...]) apply方法的第一個參數(shù)也是this所要指向的那個對象,如果設為null或undefined,則等同于指定全局對象。第二個參數(shù)則是一個數(shù)組,該數(shù)組的所有成員依次作為參數(shù),傳入原函數(shù)。原函數(shù)的參數(shù),在call方法中必須一個個添加,但是在apply方法中,必須以數(shù)組形式添加
function f(x,y){console.log(x+y); }f.call(null,1,1) // 2 f.apply(null,[1,1]) // 2找出數(shù)組最大的元素
var a = [10, 2, 4, 15, 9]; Math.max.apply(null, a) // 15將數(shù)組的空元素變?yōu)?undefined
Array.apply(null, ["a",,"b"]) // [ 'a', undefined, 'b' ]空元素與undefined的差別在于,數(shù)組的forEach方法會跳過空元素,但是不會跳過undefined。因此,遍歷內部元素的時候,會得到不同的結果。
var a = ['a', , 'b'];function print(i) {console.log(i);}a.forEach(print)// a// bArray.apply(null, a).forEach(print)// a// undefined// b綁定回調函數(shù)的對象
函數(shù)回調場景-2我們看到this被動態(tài)的更改為了 DOM 對象 _button_,這往往不是我們所期望的,所以,我們可以再次綁定回調函數(shù)來固定this,如下,
這樣,我們用 f 函數(shù)封裝原來的回調函數(shù) o.f_,并使用apply方法固定住this,使其永遠指向 _object o,這樣,就達到了this不被動態(tài)修改的目的。
function.prototype.bind()
總結起來,其實就是在把函數(shù)作為參數(shù)傳遞的時候,如何解決this動態(tài)變化的問題。
解決的問題
在認識關聯(lián)調用 - 容易混淆的場景中,我們濃墨重彩的描述了將函數(shù)賦值以后,導致this在運行期發(fā)生變化的種種場景,而且在編程過程當中,也是非常容易導致問題的場景;那么有沒有這么一種機制,即便是在函數(shù)賦值后,在運行期依然能夠保護并固定住我的this?答案是有的,那就是bind。下面,我們來看一個例子,
var d = new Date(); d.getTime() // 1481869925657我們使用語句 d.getTime() 通過對象 d 關聯(lián)調用函數(shù) getTime()_,根據(jù)法則 #3,函數(shù) _getTime() 內部的 this 指向的是對象 d_,然后從 _d 對象中成功獲取到了時間。但是,我們稍加改動,將對象 d 中的函數(shù) getTime 賦值給另外一個變量,在執(zhí)行呢?
var print = d.getTime; print() // Uncaught TypeError: this is not a Date object.Wow~, 畫風突變,得不到時間了,而且還拋出了一個程序異常,好玩,你的程序因此崩潰.. 這就是this在執(zhí)行期動態(tài)變化所導致的,當我們將函數(shù) d.getTime 賦值給 print_,然后語句 _print() 表示將函數(shù) getTime 作為 standalone 的函數(shù)在運行期調用,所以,內部的this發(fā)生變化,指向了 _Global Object_,也因此,我們得不到時間了,但我們得到一個意想不到的異常..
Ok, 別怕,孩子,bind登場了,
var print = d.getTime.bind(d); print() // 148186992565在 賦值過程中_,將函數(shù)通過bind語法綁定this對象 _d 以后,再賦值給一個新的變量;這樣,即便 print() 再次作為 standalone 的函數(shù)在運行期調用,this的指向也不再發(fā)生變化,而是固定的指向了對象 _d_。
bind 是如何實現(xiàn)的
if(!('bind' in Function.prototype)){Function.prototype.bind = function(){var fn = this; // 當前調用 bind 的當前對象 fn ( fn.bind(..) )var context = arguments[0]; // 用來綁定 this 對象的參數(shù)var args = Array.prototype.slice.call(arguments, 1);var fnbound = function(){return fn.apply(context, args);}return fnbound;} }給Function對象的prototype原型中新增一個屬性bind,該bind是一個 function 函數(shù);這里要特別特別注意,每次bind調用以后,返回的是一個新的function,
var fnbound = function(){return fn.apply(context, args);}return fnbound;通過 fnbound 函數(shù)套一層原函數(shù) fn 作為閉包,然后返回這個新的 function _fnbound_;大部分教程就是這樣介紹即止了;其實,我想問的是,為什么bind要這么設計,直接返回fn.apply(context, args);不是挺好嗎?為什么還要在外面套一層新函數(shù) _fnbound_?Ok,這里我就來試圖解釋下原因吧;
采用反證法,如果,我們不套這么一層新函數(shù) _fubound_,看看,會怎樣?于是,我們得到如下的實現(xiàn),
if(!('bind' in Function.prototype)){Function.prototype.bind = function(){var fn = this; // 當前調用 bind 的當前對象 fn ( fn.bind(..) )var context = arguments[0]; // 用來綁定 this 對象的參數(shù)var args = Array.prototype.slice.call(arguments, 1);return fn.apply(context, args);} }直接返回fn.apply(context, args),oh,頓時,我明白了,fn.apply(...)這是一條執(zhí)行命令啊,它會立即執(zhí)行 fn_,將 _fn 執(zhí)行的結果返回.. 而我們這里的bind的初衷只是擴充 fn 函數(shù)的行為(既綁定this對象),然后返回一個函數(shù)的引用,而正式因為我們無法在綁定以后,直接返回原有函數(shù)的引用,所以,這里,我們才需要創(chuàng)建一個新的函數(shù)并返回這個新的函數(shù)的引用,已達到bind的設計目的。Ok,這下總算是清楚了。
特性
綁定匿名函數(shù)
obj.print = function () {this.times.forEach(function (n) {console.log(this.name);}.bind(this)); };可見,我們可以直接改匿名函數(shù)執(zhí)行bind,然后在將其賦值給某個對象;更詳細的用例參考函數(shù)回調場景 3 - 數(shù)組對象方法的回調
作為函數(shù)直接調用
var altwrite = document.write; altwrite("hello");在瀏覽器運行這個例子,得到錯誤Uncaught ReferenceError: alwrite is not defined,這個錯誤并沒有真正保留底層的原因,真正的原因是,_document_ 對象的 write 函數(shù)再執(zhí)行的時候,內部this指向了 Global Object
為了解決上述問題,我們可以bind document 對象,
altwrite.bind(document)("hello")注意這里的寫法,altwrite.bind(document)返回的是一個Function,所以可以直接跟參數(shù)調用。
綁定函數(shù)參數(shù)
除了綁定this對象意外,還可以綁定函數(shù)中的參數(shù),看如下的例子,
var add = function (x, y) {return x * this.m + y * this.n; }var obj = {m: 2,n: 2 };var newAdd = add.bind(obj, 5);newAdd(5); // 20add.bind(obj, 5);除了綁定 add 函數(shù)的this對象為 obj 以外,將其固定為 obj 以外,還綁定了 add 函數(shù)的第一個參數(shù) x_,并將其固定為 _5_;這樣,得到的 _newAdd 函數(shù)只能接收一個參數(shù),那就是 y 了,因為 x 已經(jīng)被bind綁定且固定了,所以可以看到,隨后執(zhí)行的語句newAdd(5)傳遞的實際上是 y 參數(shù)。
若綁定 null 或者 undefined
如果bind方法的第一個參數(shù)是 null 或 _undefined_,等于將this綁定到全局對象,函數(shù)運行時this指向 _Global Object_。
var name = 'windows';function add(x, y) {console.log(this.name);return x + y; }var plus = add.bind(null, 5); // 綁定了 x 參數(shù)> plus(10) // 賦值的是 y 參數(shù),于是執(zhí)行的是 5 + 10'windows'15改寫原生方法的使用方式
首先,
> [1, 2, 3].push(4)4 // 輸出新增后數(shù)組的長度等價于
Array.prototype.push.call([1, 2, 3], 4)第一個參數(shù) [1, 2, 3] 綁定 push 函數(shù)的this關鍵字,第二個參數(shù) _4_,是需要被添加的值。
補充一下
為什么說這里是等價的?我們來解讀一下
> [1, 2, 3].push(4)4 // 輸出新增后數(shù)組的長度的執(zhí)行過程,_[1, 2, 3]_ 作為數(shù)組對象,調用其原型中的 Array.prototype.push 方法,很明顯,采用的是關聯(lián)調用,因此 push 函數(shù)內部的 this 指向的是數(shù)組對象 _[1, 2, 3]_;而這里,我們通過
Array.prototype.push.call([1, 2, 3], 4)這樣的調用方式,只是換湯不換藥,同樣是執(zhí)行的數(shù)組中的原型方法 _push_,只是this的傳遞方式不同而已,這里是通過bind直接將this賦值為數(shù)組對象 _[1, 2, 3]_,而不是通過之前的關聯(lián)調用;所以,兩種調用方式是等價的。
補充完畢
再次,
call 方法調用的是 Function 對象的原型方法既 Function.prototype.call(...)_,那么我們再來將它 _bind 一下,看看會有什么結果
> var push = Function.prototype.call.bind(Array.prototype.push);> push([1, 2, 3], 4);4 // 返回數(shù)組長度// 或者寫為> var a = [1, 2, 3]; > push(a, 4);4 > a[1, 2, 3, 4]我們得到了一個具備數(shù)組 push 操作的一個新的函數(shù) push(...) ( 注: bind 每次回返回一個新的函數(shù) );
那是為什么呢?
可以看到,背后的核心是,
push([1, 2, 3], 4);等價于執(zhí)行
Array.prototype.push.call([1, 2, 3], 4)所以,我們得證明Function.prototype.call.bind(Array.prototype.push)([1, 2, 3], 4)與Array.prototype.push.call([1, 2, 3], 4)兩個函數(shù)的執(zhí)行過程是等價的( 注意,為什么比較的是執(zhí)行過程等價,因為call函數(shù)是立即執(zhí)行的,而bind返回的是一個函數(shù)引用,所以必須比較兩者的執(zhí)行過程 );其實,要證明這個問題,最直接方法就是去查看函數(shù)Function.prototype.call的源碼,可惜,我在官網(wǎng) MDN Function.prototype.call() 上面也沒有看到源碼;那么這里,其實可以做一些推理,
Function.prototype.call.bind(Array.prototype.push)([1, 2, 3], 4)
通過bind,這里返回一個新的 call 函數(shù),該函數(shù)綁定了 Array.prototype.push Function 對象做為其this對象;那么Function.prototype.call函數(shù)內部會怎么執(zhí)行呢?我猜想應該就是執(zhí)行this.apply(context, params)之類的,this表示的是 Array.prototype.push_,context表示的既是這里的數(shù)組對象 _[1, 2, 3]_, params表示的既是這里的參數(shù) _4
Array.prototype.push.call([1, 2, 3], 4)
同理,由上述Function.prototype.call函數(shù)內部的執(zhí)行過程是執(zhí)行this.apply(context, params)的推斷來看,this依然是指向的 Array.prototype.push_,context表示的既是這里的數(shù)組對象 _[1, 2, 3]_, params表示的既是這里的參數(shù) _4_;所以,這里的調用方式與 _Function.prototype.call.bind(Array.prototype.push)([1, 2, 3], 4) 的方式等價;所以,我們得出如下結論,
Array.prototype.push.call([1, 2, 3], 4) <=> Function.prototype.call.bind(Array.prototype.push)([1, 2, 3], 4) <=> push([1, 2, 3], 4)
使用 bind 的一些注意事項
每次返回一個新函數(shù)
bind方法每運行一次,就返回一個新函數(shù),這會產(chǎn)生一些問題。比如,監(jiān)聽事件的時候,不能寫成下面這樣。
element.addEventListener('click', o.m.bind(o));上面代碼中,click 事件綁定bind方法新生成的一個匿名函數(shù)。這樣會導致無法取消綁定,所以,下面的代碼是無效的。
element.removeEventListener('click', o.m.bind(o));正確的方法是寫成下面這樣,使得 add 和 remove 使用的是同一個函數(shù)的引用。
var listener = o.m.bind(o); element.addEventListener('click', listener); // ... element.removeEventListener('click', listener);use strict
使用嚴格模式,該部分可以參考阮一峰的教程嚴格模式,說得非常詳細;不過應用到面向對象編程里面,主要就是為了避免this在運行期動態(tài)指向 _Global Object_,如果發(fā)生這類的情況,報錯;例如
function f() {'use strict';this.a = 1; };f();// 報錯,this未定義當執(zhí)行過程中,發(fā)現(xiàn)函數(shù) f 中的this指向了 _Global Object_,則報錯。
構造函數(shù)中的 this
this -> Object.prototype instance
構造函數(shù)比較特別,_javascript_ 解析過程不同于其它普通函數(shù);
假如我們有如下的構造函數(shù),
var Person = function(name, age){this.name = name;this.age = age; }當 javascript 語法解析器解析到如下語句以后,
var p = new Person('張三', 35);實際上執(zhí)行的是,
function new( /* 構造函數(shù) */ constructor, /* 構造函數(shù)參數(shù) */ param1 ) {// 將 arguments 對象轉為數(shù)組var args = [].slice.call(arguments);// 取出構造函數(shù)var constructor = args.shift();// 創(chuàng)建一個空對象,繼承構造函數(shù)的 prototype 屬性var context = Object.create(constructor.prototype);// 執(zhí)行構造函數(shù)var result = constructor.apply(context, args);// 如果返回結果是對象,就直接返回,則返回 context 對象return (typeof result === 'object' && result != null) ? result : context; }備注:_arguments_ 可表示一個函數(shù)中所有的參數(shù),也就是一個函數(shù)所有參數(shù)的結合。
下面,我們一步一步的來分析該構造函數(shù)的實現(xiàn),弄清楚this指的是什么,
constructor
就是 Person 構造函數(shù),
context
var context = Object.create(constructor.prototype);通過 constructor.prototype 創(chuàng)建了一個新的對象,也就是 Person.prototype 的一個實例 _Person.prototype isntance_;
constructor.apply(context, args);
注意,這步非常關鍵,_context_ 作為 constructor 構造函數(shù)的this,所以
var Person = function(name, age){this.name = name;this.age = age; }中的this在執(zhí)行過程中指向的實際上就是該 context 對象。
result
是constructor.apply(context, args);方法調用的返回值,我們當前用例中,_Person_ 構造函數(shù)并沒有返回任何東西,所以,這里是 _null_。
return (typeof result === 'object' && result != null) ? result : context;
new方法的最后返回值,如果 result 不為 null_,則返回 _result 否則返回的是 context_;我們這個用例,當初始化構造函數(shù)完成以后,返回的是 _context 既 _Person.prototype instance_,也就是構造函數(shù)中的this指針;這也是大多數(shù)構造函數(shù)應用的場景。
Object.prototype instance -> Object.prototype
var Obj = function (p) {this.p = p; };Obj.prototype.m = function() {return this.p; };執(zhí)行,
> var o = new Obj('Hello World!');> o.p 'Hello World!'> o.m() 'Hello World!'說實話,當我第一次看到這個例子的時候,_o.p_ 還好理解,_o_ 就是表示構造函數(shù) Obj 內部的this對象,是一個通過 Object.create(Obj.prototype) 得到的一份 Obj.prototype 的實例對象;但是,當我看到 o.m 的時候,還是有點懵逼,_Obj.prototype_ 并不是代表的this呀,_Object.create(Obj.prototype)_ 才是( 既 Obj.prototype instance ),所以在 Obj.prototype 上定義的 m 方法,怎么可以通過 o.m() 既通過 Obj.prototype instance 來調用呢?( 注意,關系 o -> Object.create(Obj.prototype) -> Obj.prototype instance -> this != Obj.prototype ) 當理解到 prototype 的涵義有,才知道,_Obj.prototype instance_ 會繼承 Obj.prototype 中的公共屬性的,所以,這里通過 Obj.prototype 對象定義的 m 函數(shù)可以通過 Object.prototype instance 進行調用。
References
本文轉載自筆者的私人博客,傷神的博客,http://www.shangyang.me/2017/...
[Javascript中this關鍵字詳解](
http://www.cnblogs.com/justan...
jQuery Fundamentals Chapter - The this keyword
總結
以上是生活随笔為你收集整理的javascript:this 关键字的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 正则表达式-匹配
- 下一篇: AJAX应用【股票案例、验证码校验】