this 關鍵字是 JavaScript 中最複雜的機制之一。它是一個很特別的關鍵字,被自動定義在所有函式的作用域中。this 的繫結一直是一件非常令人困惑的事,即使經驗豐富的開發者有時也較難說清它的指向。
當一個函式被呼叫時,會建立一個活動記錄(有時候也稱為執行上下文)。這個記錄會包含函式在哪裡被呼叫(呼叫棧)、函式的呼叫方法、傳入的引數等資訊。this 就是記錄(上下文)的其中一個屬性,會在函式執行的過程中用到。
一、this的指向this 總是指向執行時的當前物件。
JavaScript 的 this 總是指向一個物件,而具體指向哪個物件是在執行時基於函式的執行環境動態繫結的,而非函式被宣告時的環境。
也就是說 this 的繫結和函式宣告的位置沒有任何關係,只取決於函式的呼叫方式。
除了使用 with 和 eval 的情況,常用的可分為以下幾種:
作為物件的方法呼叫。作為普通函式呼叫。構造器呼叫。Function.prototype.call 或 Function.prototype.apply 呼叫。1. 作為物件的方法呼叫物件的方法被呼叫時,this 指向該物件。
var obj = { a: 1, getA() { alert ( this === obj ); // 輸出:true alert ( this.a ); // 輸出: 1 }};obj.getA();
2. 作為普通函式呼叫
當函式不作為物件的屬性被呼叫時,也就是我們常說的普通函式方式,此時的 this 總是指向全域性物件。
這裡注意物件的方法被單獨複製出來後執行,那麼原來的this會丟失,變成指向全域性物件
//在瀏覽器的JavaScript 裡,這個全域性物件是window 物件。window.name = 'globalName';var getName = function(){ return this.name;};console.log( getName() ); // 輸出:globalName//或者:window.name = 'globalName';var myObject = { name: 'sven', getName: function(){ return this.name; }};var getName = myObject.getName;console.log( getName() ); // globalName
3. 作為物件的方法呼叫通常情況下,構造器裡的 this 就指向返回的這個物件;
如果構造器不顯式地返回任何資料,或者是返回一個非物件型別的資料,this指向返回的這個物件;如果構造器顯式地返回了一個物件,則例項化的結果會返回這個物件,而不是this;//構造器裡的this 就指向返回的這個物件,見如下程式碼:var MyClass = function(){ this.name = 'sven';};var obj = new MyClass();alert ( obj.name ); // 輸出:svenvar MyClass = function(){ this.name = 'sven'; return { // 顯式地返回一個物件 name: 'anne' }};var obj = new MyClass();alert ( obj.name ); // 輸出:annevar MyClass = function(){ this.name = 'sven' return 'anne'; // 返回string 型別};var obj = new MyClass();alert ( obj.name ); // 輸出:sven
4. call 或 apply 呼叫
apply和call可以動態地改變傳入函式的 this
var obj1 = { name: 'sven', getName: function(){ return this.name; }};var obj2 = { name: 'anne'};console.log( obj1.getName() ); // 輸出: svenconsole.log( obj1.getName.call( obj2 ) ); // 輸出:anne
二、call, apply 和 bind簡介及區別call 和 apply作用一模一樣,區別僅在於傳入引數形式的不同。
applyapply 接受兩個引數,第一個引數指定了函式體內 this 物件的指向,第二個引數為一個帶下標的集合,這個集合可以為陣列,也可以為類陣列。
類陣列:
物件本身要可以存取屬性;物件的 length 屬性可讀寫。callcall 傳入的引數數量不固定,第一個引數也是代表函式體內的 this 指向,從第二個引數開始往後,每個引數被依次傳入函式
bindbind 引數型別和 call 相同,不過它不會執行函式,而是修改 this 後返回一個新的函式
var func = function( a, b, c ){ alert ( [ a, b, c ] );};func.apply( null, [ 1, 2, 3 ] );func.call( null, 1, 2, 3 );
call 和 bind 是包裝在 apply 上面的語法糖:
Function.prototype.call = function( context ){ var argus = []; for (var i = 1; i < arguments.length; i++) { argus.push(arguments[i]); } this.apply(context, argus);};Function.prototype.bind = function( context ){ var self = this; // 儲存原函式 // 返回一個新的函式 return function(){ // 執行新的函式的時候,會把之前傳入的 context // 當作新函式體內的 this return self.apply( context, arguments ); }};
用途改變 this 指向借用其他物件的方法: 在操作類陣列(比如 arguments) 的時候,可以找 Array.prototype 物件借用方法Array.prototype.push.call(a, 'first' )三、eval 和 with詞法作用域由寫程式碼期間函式所宣告的位置來定義,而 eval 和 with 可以在執行時修改詞法作用域。
eval(..) 和 with 會在執行時修改或建立新的作用域,以此來欺騙其他在書寫時定義的詞 法作用域。 看起來它們能實現更復雜的功能,並且程式碼更具有擴充套件性。
但這些功能已經過時,效能也較差,使用不當容易導致難以預料的問題,已經不被提倡,嚴格模式下會被限制和禁止,所以不要輕易使用它們。
evaleval(..) 函式可以接受一個字串為引數,並將其中的內容視為好像在書寫時就存在於程式中這個位置的程式碼。換句話說,可以在你寫的程式碼中用程式生成程式碼並執行,就好像程式碼是寫在那個位置的一樣。
function foo(str, a) { eval( str ); console.log( a, b );}var b = 2;foo( "var b = 3;", 1 ); // 1, 3// 相當於function foo(str, a) { var b = 3; console.log( a, b );}
字串的內容可以是一段動態生成的函式程式碼。
JavaScript中 還有其他一些功能效果和eval(..)很相似。setTimeout、setInterval 的第一個引數可以是字串。
withwith 可以改變作用域,通常被當作重複引用同一個物件中的多個屬性的快捷方式,可以不需要重複引用物件本身。
var obj = { a: 1, b: 2, c: 3};// 單調乏味的重複 "obj" obj.a = 2;obj.b = 3;obj.c = 4;// 簡單的快捷方式 with (obj) { a = 3; b = 4; c = 5;}
效能
eval 和 with效能差
JavaScript 引擎會在編譯階段進行數項的效能最佳化。其中有些最佳化依賴於能夠根據程式碼的詞法進行靜態分析,並預先確定所有變數和函式的定義位置,才能在執行過程中快速找到識別符號。
但如果引擎在程式碼中發現了 eval(..) 或 with,就無法在詞法分析階段明確知道 eval(..) 會接收到什麼程式碼,這些程式碼會如何對作用域進行修改,也無法知道傳遞給 with 用來建立新詞法作用域的物件的內容到底是什麼。
因此如果大量使用會導致效能變差。