摘要:執行完以后,將該匿名函數賦值給。上述代碼其實等價于執行如下的代碼,函數回調場景基本原理可以看到,把一個定義有關鍵字的函數作為其它函數的回調函數,是危險的,因為在運行期會被重新賦值,上述例子很直觀的描述了這一點,之所以報錯,是因為指向了。
前言
看過[阮一峰]()的關于 this 的教程,講了很多比較好的例子,但沒有對其本質的東西解釋清楚,而且部分例證存在問題;于是,打算重寫本章節,從this的本質入手;
本文為作者的原創作品,轉載需注明出處;
this 是什么?this可以理解為一個指針,指向調用對象;
判斷 this 是什么的四個法則官網定義
先來看第一段官方的解釋,
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:
If the function is invoked using Function.call or Function.apply, this will be set to the first argument passed to call/apply. If the first argument passed to call/apply is null or undefined, this will refer to the global object (which is the window object in Web browsers).
If the function being invoked was created using Function.bind, this will be the first argument that was passed to bind at the time the function was created.
If the function is being invoked as a method of an object, this will refer to that object.
Otherwise, the function is being invoked as a standalone function not attached to any object, and this will refer to the global object.
大致翻譯如下,
this是這么一個特殊的關鍵字,它是用來指向一個當前正在被調用( a being invoked )方法的調用對象的;( 等等,這句話其實隱藏了一個非常關鍵的信息,那就是this是在運行期 生效的,怎么生效的?在運行期,this被賦值,將某個對象賦值給this,與聲明期無關,也就是說,this是運行期相關的 );this的賦值場景,歸納起來,分為如下四種情況,
如果方法是被Function.call或者Function.apply調用執行.... bla..bla..
參考 function prototype call 小節
如果是被Function.bind... bla...bla
參考 function prototype bind 小節
如果某個方法在運行期是被一個對象( an object )調用( 備注:這里為了便于理解,我針對這種情況,自己給起了個名稱叫關聯調用 ),在運行期,會將該 object 的引用賦值給該方法的this。
備注:被一個對象調用?何解?其實就是指語句obj.func(),這個時候,func()方法內部的this將會被賦值為obj對象的引用,也就是指向obj;
如果該方法在運行期被當做一個沒有依附在任何 object 上的一個獨立方法被調用(is being invoked as a standalone function not attached to any object ),那么該方法內部的this將會被賦值為全局對象(在瀏覽器端就是 windows )
獨立方法 ( standalone function )?在運行期,如果func方法被obj關聯調用的,既是通過obj.func()的方式,那么它就不是standalone的;如果是直接被調用,沒有任何對象關聯,既是通過func()調用,那么這就是standalone的。
官網定義 2
再來看另外一句非常精煉的描述,來加深理解
The this keyword is relative to the execution context, not the declaration context.
this關鍵字與運行環境有關而與聲明環境無關;(補充,而作用域鏈和閉包是在函數的聲明期創建的,參考創建時機)
補充,是如何與函數的運行期相關的,參考this 指針運行時賦值
我的補充法則 #3 和 #4,大多數情況都非常容易理解,有幾種情況需要特別注意,
函數嵌套
需要注意的是object對象中的函數內部再次嵌套函數的情況,
var name = "windows"; var obj = { name:"object", f1:function(){ console.log("this: "+this.name) function f2(){ console.log("this: " + this.name) } f2(); } };
執行
> obj.f1(); this: object this: windows
可以看到,在運行期,被調用函數 f1() 中的this指向 obj_,而被調用函數 _f2() 中的this指向的是 windows ( global object );因為 f1 函數在當前的運行時中是通過 obj.f1() 進行的關聯調用,所以,根據定義 #3,在當前的運行期間,_f1()_ 內部的 this 是指向 obj 對象的( 通過將 obj 的引用直接賦值給 this ),而, f2 函數在運行期是沒有與其它 object 進行關聯調用,所以,在當前的運行時期,_f2_ 是一個 standalone 的函數,所以,根據定義 #4,在當前的運行期間,_f2()_ 的內部this是指向 windows 的。(注意,這里我反復強調當前運行期間,是因為this是在運行時被賦值的,所以,要特別注意的是,即使某個函數的定義不變,但在不同的執行環境(運行環境)中,this是會發生變化;)
回調函數
參看函數回調場景-1和函數回調場景-2
函數賦值
參看將函數賦值-standalone以及相關變種章節
可見,要判斷this在運行期到底指的是什么,并沒有那么容易,但是,只要牢牢的把握好兩點,就可以迎刃而解,
this是運行期相關的
更確切的說,this是在運行期被賦值的,所以,它的值是在運行期動態確定的。
this是否與其它對象關聯調用
這里的關聯調用指的是 javascript 的一種語法,既是調用語句顯式的寫為obj.func(),另外需要注意的是,_javascript_ 方法的調用不會隱式的隱含 this。只要沒有顯式的關聯調用,那么就是standalone的調用,就符合法則 #4,所以,this指向 _Global Object_。
注意,this定義中所指的Object指的是 javascript 的 Object 類型,既是通過
var o1 = {}; var o2 = new Object(); var o3 = Object.create(Object.prototype);
這樣的方式構建出來的對象;
備注,最開始,自己有個思維的誤區,認為既然 javascript 一切皆為對象,那么this指針是指向對象的,那么是不是也可以指向Function,Number等對象?答案是否定的。
起初,我是按照上面的邏輯來理解的,直到當我總結到bind 是如何實現的小節后,發現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: "李四" }; // 執行, > A.describe(); "張三" > B.describe = A.describe; > B.describe() "李四" > var describe = A.describe; > describe(); "王麻子"
可以看到,雖然 A.describe 方法的定義不變,但是其運行時環境發生了變化,_this_ 的指向也就發生了變化。
> B.describe = A.describe; > B.describe() "李四"
在運行時,相當于運行的是 B 的 describe 方法
> var describe = A.describe; > describe(); "王麻子"
在運行時,相當于運行的是 windows 的 describe 方法
方法調用沒有隱含 this經常寫 Java 代碼的原因,經常會習慣性的認為只要在對象方法里面調用某個方法或者屬性,隱含了 this,比如
public class Person{ String name; public String getName(){ return name; } public String getName2(){ return this.name; } }
而 Javascript 實際上并沒有這種隱含的表達方式;詳細驗證過程參考將函數賦值-standalone
關聯調用 - 容易混淆的場景從this 是什么章節中,為了方便對 #3 進行描述,我起了個名字叫做 關聯調用 ;那么有些情況看似是 _關聯調用_,實則不然;
我們有一個標準的對象,定義如下,
var name = "windows"; var obj = { name: "obj", foo: function () { console.log("this: "+ this.name); } };
通過標準的 關聯調用 的方式,我們進行如下的調用,
> obj.foo() "this: obj"
根據法則 #3 既 關聯調用 的定義,得到 this -> obj_;如果事事都如此的簡單,如此的標準,那可就好了,總會有些讓人費解的情況,現在來看看如下的一些特殊的例子,加深對 _關聯調用 的理解。
將函數賦值 - standalone> var fooo = obj.foo > fooo(); "this: windows"
輸出的 windows_,既是 _this -> global object_,而不是我們期望的 _obj_;為什么?原因是,_obj.foo 其實是 foo 函數的函數地址,通過 var fooo = obj.foo 將該函數的地址賦給了變量 _fooo_,那么當執行
> fooo();
的時候,fooo() 執行的是是一個standalone的方法,根據法則 #4,所以該方法內部的this指向的是 Global Object_;注意,_obj.foo 表示函數 foo 的入口地址,所以,變量 fooo 等價與 foo 函數。
備注:由于受到寫 Java 代碼習慣的原因,很容易將這里解釋為默認執行的是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 -> f2 this.f2(); // o -> f2 } }
可以看到,在 o.f() 函數中,如果 f2() 的調用隱含了this,那么 f2() 和 this.f2() 兩者調用應該是等價的;但是,在實際執行過程中,_f2()_ 和 this.f2() 執行的是兩個截然不同的方法,因此 f2() ≠ this.f2()_,所以 _f2() 并沒有隱示的表示為 _this.f2()_;
將函數賦值變種 - 匿名 standalone 函數立即執行> (obj.foo = obj.foo)() "this: windows"
首先,立即執行 foo 函數,然后將 foo 函數賦值給對象 obj 對象的 foo 屬性;等價于執行如下的代碼,
var name = "windows"; var obj = { name : "obj" }; (obj.foo = function () { console.log("this: " + this.name); })();
輸出,
"this: windows"
可以看到,_this_ -> _global object_,這里為什么指向的是 _global object_?其實這里的立即執行過程,就是執行的如下代碼,
(function () { console.log("this: " + this.name); }());
由此可以看出,實際上進行一個匿名函數的立即執行;也就是說執行過程中并沒有使用 關聯調用_,而是一次 _standalone 函數的自身調用,所以根據法則 #4,_this_ -> _global object_。執行完以后,將該匿名函數賦值給 _obj.foo_。
再次執行,
> obj.foo(); "this: obj"
這次執行的過程是一次標準的 關聯調用 過程,所以根據法則 #3,_this_ -> _obj_。
作為判斷條件 - 匿名函數立即執行> (false || obj.foo)() "windows"
等價于執行,
(false || function () { console.log("this: " + this.name); })()
原理和函數賦值變種-匿名 standalone 函數立即執行 一致,等價于立即執行如下的匿名函數
(function () { console.log("this: " + this.name); })()
其實,把這個例子再做一個細微的更改,其中邏輯就看得更清楚了,為 foo 函數添加一個返回值 return true
var name = "windows"; var obj ={ name: "obj", foo: function () { console.log("this: "+ this.name); return true; } };
再次執行,
> (false || obj.foo)() "windows" true
可見,_obj.foo_ 函數執行以后,返回 _true_。上述代碼其實等價于執行如下的代碼,
(false || function () { console.log("this: " + this.name); return true; })()函數回調場景 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關鍵字的函數作為其它函數的回調函數,是危險的,因為this在運行期會被重新賦值,上述例子很直觀的描述了這一點,之所以報錯,是因為this指向了 _Global Object_。要解決這樣的問題,可以使用bind,調用的時候改為
> callIt(counter.inc.bind(counter)) 1函數回調場景 1 - setTimeout
var name = "Bob"; var nameObj ={ name : "Tom", showName : function(){ console.log(this.name); }, waitShowName : function(){ setTimeout(this.showName, 1000); } }; // 執行, > nameObj.waitShowName(); "Tom" undefined
setTimeout(this.showName, 1000);將 nameObj.showName 函數作為回調函數參數傳遞給 setTimeout_;那么為什么當 _setTimeout 執行回調的時候,_nameObj.showName_ 方法返回的是 undefined 呢?為什么不是返回全局對象對應的 name Bob_?原因只有一個,那就是 _setTimeout 有自己的 this 對象,而它沒有 name 屬性,而在回調 showName 函數的時候,_showName_ 函數中的 this 正是 setTimeout 上下文中的 this_,而該 _this 并沒有定義 name 屬性,所以這里返回 _undefined_。
函數回調場景 2 - DOM 對象var o = new Object(); o.f = function () { console.log(this === o); } o.f() // true,得到期望的結果 this -> o
但是,如果將f方法指定給某個click事件,this的指向發生了改變,
$("#button").on("click", o.f);
點擊按鈕以后,返回的是false,是因為在執行過程中,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 info jQuery().off( event ); return origFn.apply( this, arguments ); }; // Use same guid so caller can remove using origFn fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); } ....... }
o.f 函數的地址賦值給 fn 參數,_fn_ -> origFn_,最后是通過origFn.apply( this, arguments );來調用 _o.f 函數的,而這里的 this 就是當前的 DOM 對象,既是這個按鈕 button_;通過這樣的方式,在執行過程中,通過回調函數 _$("button").on(...) 成功的將新的 this 對象 button 注入了 o.f 函數。那么如何解決呢?參看function.prototype.apply())
的小節#3,動態綁定回調函數。
var obj = { name: "張三", times: [1, 2, 3], print: function () { this.times.forEach(function (n) { console.log(this.name); }); } }; > obj.print(); "undefined" "undefined" "undefined"
這里我們期望的是,依次根據數組 times 的長度,輸出 obj.name 三次,但是實際運行結果是,數組雖然循環了三次,但是每次輸出都是 _undefined_,那是因為匿名函數
function(n){ console.log(this.name); }
作為數組 times 的方法 forEach 的回調函數執行,在 forEach 方法內部該匿名函數必然是作為 standalone 方法執行的,所以,this指向了 _Global Object_;
進一步,為什么“在 forEach 方法內部該匿名函數必然是作為 standalone 方法執行的”?為什么必然是作為 standalone 方法執行?是因為不能在 forEach 函數中使用 this.fn() 的方式來調用該匿名回調函數( fn 作為參數引用該匿名回調函數 ),因為如果這樣做,在運行時期會報錯,因為在 forEach 函數的 this 對象中找不到 fn 這樣的屬性,而該 this 對象指向的是 obj.times 數組對象。因此,得到結論“在 forEach 方法內部該匿名函數必然是作為 standalone 方法執行的”
解決辦法,使用 bind
obj.print = function () { this.times.forEach(function (n) { console.log(this.name); }.bind(this)); }; > obj.print() "張三" "張三" "張三"
將 obj 對象作為 this 綁定到該匿名函數上,然后再作為回調函數參數傳遞給 forEach 函數,這樣,在 forEach 函數中,用 standalone 的方式調用 fn 的時候,_fn_ 中的 this 指向的就是數組對象 obj 對象,這樣,我們就能順利的輸出 obj.name 了。
綁定 this有上述描述可知,this的值在運行時根據不同上下文環境有不同的值,因此我們說this的值是變化的,這就給我們的編程帶來了麻煩,有時候,我們期望,得到一個固定的this。Javascript 提供了call、apply以及bind這三個方法,來固定this的指向;這三個方法存儲在 function.prototype 域中,
總結起來,就是解決函數在調用的時候,如何解決this動態變化的問題。
調用格式,
func.call(thisValue, arg1, arg2, ...)
第一個參數是在運行時用來賦值給 func 函數內部的 this 的。
通過f.call(obj)的方式調用函數,在運行時,將 obj 賦值給 _this_;
var obj = {}; var f = function () { return this; }; f() === this // true f.call(obj) === obj // true
call方法的參數是一個對象,如果參數為 空_、_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方法的參數是一個原始值,那么這個原始值會自動轉成對應的包裝對象,然后賦值給 this
var f = function () { return this; }; > f.call(5) [Number: 5]
call方法可以接受多個參數,第一個參數就是賦值給 this 的對象,
var obj = { name : "obj" } function add(a, b) { console.log(this.name); return a + b; } > add.call(obj, 1, 2) obj 3
call方法可以調用對象的原生方法;
var obj = {}; obj.hasOwnProperty("toString") // false // “覆蓋”掉繼承的 hasOwnProperty 方法 obj.hasOwnProperty = function () { return true; }; obj.hasOwnProperty("toString") // true Object.prototype.hasOwnProperty.call(obj, "toString") // false
方法 hasOwnProperty 是對象 obj 從 Object.prototype 中繼承的方法,如果一旦被覆蓋,就不會得到正確的結果,那么,我們可以使用call的方式調用原生方法,將 obj 作為 this 在運行時調用,這樣,變通的,我們就可以調用 obj 對象所繼承的原生方法了。
function.prototype.apply()總結起來,和call一樣,就是解決函數在調用的時候,如何解決this動態變化的問題。
apply方法的作用與call方法類似,也是改變this指向,然后再調用該函數。唯一的區別就是,它接收一個數組作為函數執行時的參數,使用格式如下。
func.apply(thisValue, [arg1, arg2, ...])
apply方法的第一個參數也是this所要指向的那個對象,如果設為null或undefined,則等同于指定全局對象。第二個參數則是一個數組,該數組的所有成員依次作為參數,傳入原函數。原函數的參數,在call方法中必須一個個添加,但是在apply方法中,必須以數組形式添加
function f(x,y){ console.log(x+y); } f.call(null,1,1) // 2 f.apply(null,[1,1]) // 2
找出數組最大的元素
var a = [10, 2, 4, 15, 9]; Math.max.apply(null, a) // 15
將數組的空元素變為 undefined
Array.apply(null, ["a",,"b"]) // [ "a", undefined, "b" ]
空元素與undefined的差別在于,數組的forEach方法會跳過空元素,但是不會跳過undefined。因此,遍歷內部元素的時候,會得到不同的結果。
var a = ["a", , "b"]; function print(i) { console.log(i); } a.forEach(print) // a // b Array.apply(null, a).forEach(print) // a // undefined // b
綁定回調函數的對象
函數回調場景-2我們看到this被動態的更改為了 DOM 對象 _button_,這往往不是我們所期望的,所以,我們可以再次綁定回調函數來固定this,如下,
var o = new Object(); o.f = function () { console.log(this === o); } var f = function (){ o.f.apply(o); // 或者 o.f.call(o); }; $("#button").on("click", f);
這樣,我們用 f 函數封裝原來的回調函數 o.f_,并使用apply方法固定住this,使其永遠指向 _object o,這樣,就達到了this不被動態修改的目的。
function.prototype.bind()總結起來,其實就是在把函數作為參數傳遞的時候,如何解決this動態變化的問題。
解決的問題在認識關聯調用 - 容易混淆的場景中,我們濃墨重彩的描述了將函數賦值以后,導致this在運行期發生變化的種種場景,而且在編程過程當中,也是非常容易導致問題的場景;那么有沒有這么一種機制,即便是在函數賦值后,在運行期依然能夠保護并固定住我的this?答案是有的,那就是bind。下面,我們來看一個例子,
var d = new Date(); d.getTime() // 1481869925657
我們使用語句 d.getTime() 通過對象 d 關聯調用函數 getTime()_,根據法則 #3,函數 _getTime() 內部的 this 指向的是對象 d_,然后從 _d 對象中成功獲取到了時間。但是,我們稍加改動,將對象 d 中的函數 getTime 賦值給另外一個變量,在執行呢?
var print = d.getTime; print() // Uncaught TypeError: this is not a Date object.
Wow~, 畫風突變,得不到時間了,而且還拋出了一個程序異常,好玩,你的程序因此崩潰.. 這就是this在執行期動態變化所導致的,當我們將函數 d.getTime 賦值給 print_,然后語句 _print() 表示將函數 getTime 作為 standalone 的函數在運行期調用,所以,內部的this發生變化,指向了 _Global Object_,也因此,我們得不到時間了,但我們得到一個意想不到的異常..
Ok, 別怕,孩子,bind登場了,
var print = d.getTime.bind(d); print() // 148186992565
在 賦值過程中_,將函數通過bind語法綁定this對象 _d 以后,再賦值給一個新的變量;這樣,即便 print() 再次作為 standalone 的函數在運行期調用,this的指向也不再發生變化,而是固定的指向了對象 _d_。
bind 是如何實現的if(!("bind" in Function.prototype)){ Function.prototype.bind = function(){ var fn = this; // 當前調用 bind 的當前對象 fn ( fn.bind(..) ) var context = arguments[0]; // 用來綁定 this 對象的參數 var args = Array.prototype.slice.call(arguments, 1); var fnbound = function(){ return fn.apply(context, args); } return fnbound; } }
給Function對象的prototype原型中新增一個屬性bind,該bind是一個 function 函數;這里要特別特別注意,每次bind調用以后,返回的是一個新的function,
var fnbound = function(){ return fn.apply(context, args); } return fnbound;
通過 fnbound 函數套一層原函數 fn 作為閉包,然后返回這個新的 function _fnbound_;大部分教程就是這樣介紹即止了;其實,我想問的是,為什么bind要這么設計,直接返回fn.apply(context, args);不是挺好嗎?為什么還要在外面套一層新函數 _fnbound_?Ok,這里我就來試圖解釋下原因吧;
采用反證法,如果,我們不套這么一層新函數 _fubound_,看看,會怎樣?于是,我們得到如下的實現,
if(!("bind" in Function.prototype)){ Function.prototype.bind = function(){ var fn = this; // 當前調用 bind 的當前對象 fn ( fn.bind(..) ) var context = arguments[0]; // 用來綁定 this 對象的參數 var args = Array.prototype.slice.call(arguments, 1); return fn.apply(context, args); } }
直接返回fn.apply(context, args),oh,頓時,我明白了,fn.apply(...)這是一條執行命令啊,它會立即執行 fn_,將 _fn 執行的結果返回.. 而我們這里的bind的初衷只是擴充 fn 函數的行為(既綁定this對象),然后返回一個函數的引用,而正式因為我們無法在綁定以后,直接返回原有函數的引用,所以,這里,我們才需要創建一個新的函數并返回這個新的函數的引用,已達到bind的設計目的。Ok,這下總算是清楚了。
特性obj.print = function () { this.times.forEach(function (n) { console.log(this.name); }.bind(this)); };
可見,我們可以直接改匿名函數執行bind,然后在將其賦值給某個對象;更詳細的用例參考函數回調場景 3 - 數組對象方法的回調
var altwrite = document.write; altwrite("hello");
在瀏覽器運行這個例子,得到錯誤Uncaught ReferenceError: alwrite is not defined,這個錯誤并沒有真正保留底層的原因,真正的原因是,_document_ 對象的 write 函數再執行的時候,內部this指向了 Global Object
為了解決上述問題,我們可以bind document 對象,
altwrite.bind(document)("hello")
注意這里的寫法,altwrite.bind(document)返回的是一個Function,所以可以直接跟參數調用。
除了綁定this對象意外,還可以綁定函數中的參數,看如下的例子,
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); // 20
add.bind(obj, 5);除了綁定 add 函數的this對象為 obj 以外,將其固定為 obj 以外,還綁定了 add 函數的第一個參數 x_,并將其固定為 _5_;這樣,得到的 _newAdd 函數只能接收一個參數,那就是 y 了,因為 x 已經被bind綁定且固定了,所以可以看到,隨后執行的語句newAdd(5)傳遞的實際上是 y 參數。
如果bind方法的第一個參數是 null 或 _undefined_,等于將this綁定到全局對象,函數運行時this指向 _Global Object_。
var name = "windows"; function add(x, y) { console.log(this.name); return x + y; } var plus = add.bind(null, 5); // 綁定了 x 參數 > plus(10) // 賦值的是 y 參數,于是執行的是 5 + 10 "windows" 15
首先,
> [1, 2, 3].push(4) 4 // 輸出新增后數組的長度
等價于
Array.prototype.push.call([1, 2, 3], 4)
第一個參數 [1, 2, 3] 綁定 push 函數的this關鍵字,第二個參數 _4_,是需要被添加的值。
補充一下
為什么說這里是等價的?我們來解讀一下
> [1, 2, 3].push(4) 4 // 輸出新增后數組的長度
的執行過程,_[1, 2, 3]_ 作為數組對象,調用其原型中的 Array.prototype.push 方法,很明顯,采用的是關聯調用,因此 push 函數內部的 this 指向的是數組對象 _[1, 2, 3]_;而這里,我們通過
Array.prototype.push.call([1, 2, 3], 4)
這樣的調用方式,只是換湯不換藥,同樣是執行的數組中的原型方法 _push_,只是this的傳遞方式不同而已,這里是通過bind直接將this賦值為數組對象 _[1, 2, 3]_,而不是通過之前的關聯調用;所以,兩種調用方式是等價的。
補充完畢
再次,
call 方法調用的是 Function 對象的原型方法既 Function.prototype.call(...)_,那么我們再來將它 _bind 一下,看看會有什么結果
> var push = Function.prototype.call.bind(Array.prototype.push); > push([1, 2, 3], 4); 4 // 返回數組長度 // 或者寫為 > var a = [1, 2, 3]; > push(a, 4); 4 > a [1, 2, 3, 4]
我們得到了一個具備數組 push 操作的一個新的函數 push(...) ( 注: bind 每次回返回一個新的函數 );
那是為什么呢?
可以看到,背后的核心是,
push([1, 2, 3], 4);
等價于執行
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)兩個函數的執行過程是等價的( 注意,為什么比較的是執行過程等價,因為call函數是立即執行的,而bind返回的是一個函數引用,所以必須比較兩者的執行過程 );其實,要證明這個問題,最直接方法就是去查看函數Function.prototype.call的源碼,可惜,我在官網 MDN Function.prototype.call() 上面也沒有看到源碼;那么這里,其實可以做一些推理,
Function.prototype.call.bind(Array.prototype.push)([1, 2, 3], 4)
通過bind,這里返回一個新的 call 函數,該函數綁定了 Array.prototype.push Function 對象做為其this對象;那么Function.prototype.call函數內部會怎么執行呢?我猜想應該就是執行this.apply(context, params)之類的,this表示的是 Array.prototype.push_,context表示的既是這里的數組對象 _[1, 2, 3]_, params表示的既是這里的參數 _4
Array.prototype.push.call([1, 2, 3], 4)
同理,由上述Function.prototype.call函數內部的執行過程是執行this.apply(context, params)的推斷來看,this依然是指向的 Array.prototype.push_,context表示的既是這里的數組對象 _[1, 2, 3]_, params表示的既是這里的參數 _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方法每運行一次,就返回一個新函數,這會產生一些問題。比如,監聽事件的時候,不能寫成下面這樣。
element.addEventListener("click", o.m.bind(o));
上面代碼中,click 事件綁定bind方法新生成的一個匿名函數。這樣會導致無法取消綁定,所以,下面的代碼是無效的。
element.removeEventListener("click", o.m.bind(o));
正確的方法是寫成下面這樣,使得 add 和 remove 使用的是同一個函數的引用。
var listener = o.m.bind(o); element.addEventListener("click", listener); // ... element.removeEventListener("click", listener);use strict
使用嚴格模式,該部分可以參考阮一峰的教程嚴格模式,說得非常詳細;不過應用到面向對象編程里面,主要就是為了避免this在運行期動態指向 _Global Object_,如果發生這類的情況,報錯;例如
function f() { "use strict"; this.a = 1; }; f();// 報錯,this未定義
當執行過程中,發現函數 f 中的this指向了 _Global Object_,則報錯。
構造函數中的 this this -> Object.prototype instance構造函數比較特別,_javascript_ 解析過程不同于其它普通函數;
假如我們有如下的構造函數,
var Person = function(name, age){ this.name = name; this.age = age; }
當 javascript 語法解析器解析到如下語句以后,
var p = new Person("張三", 35);
實際上執行的是,
function new( /* 構造函數 */ constructor, /* 構造函數參數 */ param1 ) { // 將 arguments 對象轉為數組 var args = [].slice.call(arguments); // 取出構造函數 var constructor = args.shift(); // 創建一個空對象,繼承構造函數的 prototype 屬性 var context = Object.create(constructor.prototype); // 執行構造函數 var result = constructor.apply(context, args); // 如果返回結果是對象,就直接返回,則返回 context 對象 return (typeof result === "object" && result != null) ? result : context; }
備注:_arguments_ 可表示一個函數中所有的參數,也就是一個函數所有參數的結合。
下面,我們一步一步的來分析該構造函數的實現,弄清楚this指的是什么,
constructor
就是 Person 構造函數,
context
var context = Object.create(constructor.prototype);通過 constructor.prototype 創建了一個新的對象,也就是 Person.prototype 的一個實例 _Person.prototype isntance_;
constructor.apply(context, args);
注意,這步非常關鍵,_context_ 作為 constructor 構造函數的this,所以
var Person = function(name, age){ this.name = name; this.age = age; }
中的this在執行過程中指向的實際上就是該 context 對象。
result
是constructor.apply(context, args);方法調用的返回值,我們當前用例中,_Person_ 構造函數并沒有返回任何東西,所以,這里是 _null_。
return (typeof result === "object" && result != null) ? result : context;
new方法的最后返回值,如果 result 不為 null_,則返回 _result 否則返回的是 context_;我們這個用例,當初始化構造函數完成以后,返回的是 _context 既 _Person.prototype instance_,也就是構造函數中的this指針;這也是大多數構造函數應用的場景。
Object.prototype instance -> Object.prototypevar Obj = function (p) { this.p = p; }; Obj.prototype.m = function() { return this.p; };
執行,
> var o = new Obj("Hello World!"); > o.p "Hello World!" > o.m() "Hello World!"
說實話,當我第一次看到這個例子的時候,_o.p_ 還好理解,_o_ 就是表示構造函數 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 函數可以通過 Object.prototype instance 進行調用。
References本文轉載自筆者的私人博客,傷神的博客,http://www.shangyang.me/2017/...
[Javascript中this關鍵字詳解](
http://www.cnblogs.com/justan...
jQuery Fundamentals Chapter - The this keyword
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107460.html
摘要:當談到語言與其他編程語言相比時,你可能會聽到一些令人困惑東西,其中之一是工廠函數和構造函數。好的,讓我們用構造函數做同樣的實驗。當我們使用工廠函數創建對象時,它的指向,而當從構造函數創建對象時,它指向它的構造函數原型對象。 showImg(https://segmentfault.com/img/bVbr58T?w=1600&h=900); 當談到JavaScript語言與其他編程語言...
摘要:它代表函數運行時,自動生成的一個內部對象,只能在函數內部使用類似的還有。總結關鍵字就是,誰調用我,我就指向誰。注意由于已經被定義為函數內的一個變量。因此通過關鍵字定義或者將聲明為一個形式參數,都將導致原生的不會被創建。 題目 封裝函數 f,使 f 的 this 指向指定的對象 。 輸入例子 bindThis(function(a, b) { return this.test +...
摘要:中函數的調用有以下幾種方式作為對象方法調用,作為函數調用,作為構造函數調用,和使用或調用。作為構造函數調用中的構造函數也很特殊,構造函數,其實就是通過這個函數生成一個新對象,這時候的就會指向這個新對象如果不使用調用,則和普通函數一樣。 this 是 JavaScript 比較特殊的關鍵字,本文將深入淺出的分析其在不同情況下的含義,可以這樣說,正確掌握了 JavaScript 中的 th...
摘要:原文許多人被中的關鍵字給困擾住了,我想混亂的根源來自人們理所當然地認為中的應該像中的或中的一樣工作。盡管有點難理解,但它的原理并不神秘。在瀏覽器中,全局對象是對象。運算符創建一個新對象并且設置函數中的指向調用函數的新對象。 原文:Understanding the this keyword in JavaScript 許多人被JavaScript中的this關鍵字給困擾住了,我想混亂的...
摘要:的關鍵字總是讓人捉摸不透,關鍵字代表函數運行時,自動生成的一個內部對象,只能在函數內部使用,因為函數的調用場景不同,的指向也不同。其實只要理解語言的特性就很好理解。個人對中的關鍵字的理解如上,如有不正,望指正,謝謝。 javascript的this關鍵字總是讓人捉摸不透,this關鍵字代表函數運行時,自動生成的一個內部對象,只能在函數內部使用,因為函數的調用場景不同,this的指向也不...
摘要:關鍵字計算為當前執行上下文的屬性的值。毫無疑問它將指向了這個前置的對象。構造函數也是同理。嚴格模式無論調用位置,只取顯式給定的上下文綁定的,通過方法傳入的第一參數,否則是。其實并不屬于特殊規則,是由于各種事件監聽定義方式本身造成的。 this 是 JavaScript 中非常重要且使用最廣的一個關鍵字,它的值指向了一個對象的引用。這個引用的結果非常容易引起開發者的誤判,所以必須對這個關...
閱讀 960·2021-11-24 09:39
閱讀 3383·2021-10-27 14:20
閱讀 2322·2019-08-30 14:08
閱讀 3360·2019-08-29 16:34
閱讀 2175·2019-08-26 12:14
閱讀 2103·2019-08-26 11:54
閱讀 2771·2019-08-26 11:44
閱讀 2473·2019-08-26 11:38