首頁 > web前端 > js教程 > 主體

深入淺析JavaScript系列(13):This? Yes,this!_javascript技巧

WBOY
發布: 2016-05-16 15:21:43
原創
957 人瀏覽過

前言

在這篇文章裡,我們將討論跟執行上下文直接相關的更多細節。討論的主題就是this關鍵字。實踐證明,這個主題很難,在不同執行上下文中this的確定經常會發生問題。

許多程式設計師習慣的認為,在程式語言中,this關鍵字與物件導向程式開發緊密相關,其完全指向由建構器新建立的物件。在ECMAScript規範中也是這樣實現的,但正如我們將看到那樣,在ECMAScript中,this並不限於只用來指向新創建的物件。

英文翻譯: Dmitry A. Soshnikov在Stoyan Stefanov的幫助下發布: 2010-03-07http://dmitrysoshnikov.com/ecmascript/chapter-3-this/俄文原文: Dmitry A. Soshnikov: Zeroglif發布: 2009-06-28; 更新:2010-03-07http://dmitrysoshnikov.com/ecmascript/ru-chapter-3-this/本文絕大部分內容參考了:http://www.denisdeng.com/? p=900部分句子參考了:justin的中文翻譯

讓我們更詳細的了解一下,在ECMAScript中this到底是什麼?

定義

this是執行上下文中的一個屬性:

activeExecutionContext = { VO: {...}, this: thisValue};

這裡VO是我們前一章要討論的變數物件。

this與上下文中可執行程式碼的類型有直接關係,this值在進入上下文時確定,並且在上下文運行期間永久不變。

以下讓我們更詳細研究這些案例:

全域程式碼中的this

在這裡一切都簡單。在全域程式碼中,this始終是全域物件本身,這樣就有可能間接的引用到它了。

// 顯示定義全域物件的屬性this.a = 10; // global.a = 10alert(a); // 10 // 透過賦值給一個無標示符隱式b = 20;alert(this. b); // 20 // 也是透過變數宣告隱含宣告的// 因為全域上下文的變數物件是全域物件自身var c = 30;alert(this.c); // 30

函數程式碼中的this

在函數程式碼中使用this時很有趣,這種情況很難且會導致很多問題。

在這種類型的程式碼中,this值的首要特點(或許是最主要的)是它不是靜態的綁定到一個函數。

正如我們上面曾提到的那樣,this是進入上下文時確定,在一個函數代碼中,這個值在每一次完全不同。

不管怎樣,在程式碼執行時的this值是不變的,也就是說,因為它不是一個變量,就不可能為其分配一個新值(相反,在Python程式語言中,它明確的定義為物件本身,在運行期間可以不斷改變)。

var foo = {x: 10}; var bar = { x: 20, test: function () { alert(this === bar); // true alert(this.x); // 20 this = foo; // 错误,任何时候不能改变this的值 alert(this.x); // 如果不出错的话,应该是10,而不是20 } }; // 在进入上下文的时候// this被当成bar对象// determined as "bar" object; why so - will// be discussed below in detail bar.test(); // true, 20 foo.test = bar.test; // 不过,这里this依然不会是foo// 尽管调用的是相同的function foo.test(); // false, 10 
登入後複製

那麼,影響了函數程式碼中this值的變化有幾個因素:

首先,在通常的函數呼叫中,this是由啟動上下文程式碼的呼叫者來提供的,也就是呼叫函數的父上下文(parent context )。 this取決於呼叫函數的方式。

為了在任何情況下準確無誤的確定this值,有必要理解和記住這重要的一點。正是呼叫函數的方式影響了呼叫的上下文中的this值,沒有別的什麼(我們可以在一些文章,甚至是在關於javascript的書籍中看到,它們聲稱:「this值取決於函數如何定義,如果它是全域函數,this設定為全域對象,如果函數是一個物件的方法,this將總是指向這個物件。繼續我們的主題,可以看到,即使是正常的全域函數也會被呼叫方式的不同形式激活,這些不同的呼叫方式導致了不同的this值。

function foo() { alert(this);} foo(); // global alert(foo === foo.prototype.constructor); // true // 但是同一个function的不同的调用表达式,this是不同的 foo.prototype.constructor(); // foo.prototype 
登入後複製

有可能作為一些物件定義的方法來呼叫函數,但是this將不會設定為這個物件。

var foo = { bar: function () { alert(this); alert(this === foo); }}; foo.bar(); // foo, true var exampleFunc = foo.bar; alert(exampleFunc === foo.bar); // true // 再一次,同一个function的不同的调用表达式,this是不同的 exampleFunc(); // global, false 
登入後複製

那麼,呼叫函數的方式如何影響this值?為了充分理解this值的確定,需要詳細分析其內部類型之一-引用類型(Reference type)。

引用型別(Reference type)

使用偽代碼我們可以將引用類​​型的值可以表示為擁有兩個屬性的物件-base(即擁有屬性的那個物件),和base中的propertyName 。

var valueOfReferenceType = { base: <base object>, propertyName: <property name>}; 
登入後複製

引用类型的值只有两种情况:

1. 当我们处理一个标示符时

2. 或一个属性访问器

标示符的处理过程在下一篇文章里详细讨论,在这里我们只需要知道,在该算法的返回值中,总是一个引用类型的值(这对this来说很重要)。

标识符是变量名,函数名,函数参数名和全局对象中未识别的属性名。例如,下面标识符的值:

var foo = 10;function bar() {} 
登入後複製

在操作的中间结果中,引用类型对应的值如下:

var fooReference = { base: global, propertyName: 'foo'}; var barReference = { base: global, propertyName: 'bar'}; 
登入後複製

为了从引用类型中得到一个对象真正的值,伪代码中的GetValue方法可以做如下描述:

function GetValue(value) { if (Type(value) != Reference) { return value; } var base = GetBase(value); if (base === null) { throw new ReferenceError; } return base.[[Get]](GetPropertyName(value)); } 
登入後複製

内部的[[Get]]方法返回对象属性真正的值,包括对原型链中继承的属性分析。

GetValue(fooReference); // 10GetValue(barReference); // function object "bar" 
登入後複製

属性访问器都应该熟悉。它有两种变体:点(.)语法(此时属性名是正确的标示符,且事先知道),或括号语法([])。

foo.bar();foo['bar'](); 
登入後複製

在中间计算的返回值中,我们有了引用类型的值。

var fooBarReference = { base: foo, propertyName: 'bar'}; GetValue(fooBarReference); // function object "bar" 
登入後複製

引用类型的值与函数上下文中的this值如何相关?——从最重要的意义上来说。 这个关联的过程是这篇文章的核心。 一个函数上下文中确定this值的通用规则如下:

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用括号()的左边是引用类型的值,this将设为引用类型值的base对象(base object),在其他情况下(与引用类型不同的任何其它属性),这个值为null。不过,实际不存在this的值为null的情况,因为当this的值为null的时候,其值会被隐式转换为全局对象。注:第5版的ECMAScript中,已经不强迫转换成全局变量了,而是赋值为undefined。

我们看看这个例子中的表现:

function foo() { return this;} foo(); // global 
登入後複製

我们看到在调用括号的左边是一个引用类型值(因为foo是一个标示符)。

var fooReference = { base: global, propertyName: 'foo'}; 
登入後複製

相应地,this也设置为引用类型的base对象。即全局对象。
同样,使用属性访问器:

var foo = { bar: function () { return this; }}; foo.bar(); // foo 
登入後複製

我们再次拥有一个引用类型,其base是foo对象,在函数bar激活时用作this。

var fooBarReference = { base: foo, propertyName: 'bar'}; 
登入後複製

但是,用另外一种形式激活相同的函数,我们得到其它的this值。

var test = foo.bar;test(); // global 
登入後複製

因为test作为标示符,生成了引用类型的其他值,其base(全局对象)用作this 值。
var testReference = { base: global, propertyName: 'test'};
现在,我们可以很明确的告诉你,为什么用表达式的不同形式激活同一个函数会不同的this值,答案在于引用类型(type Reference)不同的中间值。

function foo() { alert(this);} foo(); // global, because var fooReference = { base: global, propertyName: 'foo'}; alert(foo === foo.prototype.constructor); // true // 另外一种形式的调用表达式 foo.prototype.constructor(); // foo.prototype, because var fooPrototypeConstructorReference = { base: foo.prototype, propertyName: 'constructor'}; 
登入後複製

另外一个通过调用方式动态确定this值的经典例子:

function foo() { alert(this.bar);} var x = {bar: 10};var y = {bar: 20}; x.test = foo;y.test = foo; x.test(); // 10y.test(); // 20 
登入後複製

函数调用和非引用类型

因此,正如我们已经指出,当调用括号的左边不是引用类型而是其它类型,这个值自动设置为null,结果为全局对象。
让我们再思考这种表达式:

(function () { alert(this); // null => global})(); 
登入後複製

在这个例子中,我们有一个函数对象但不是引用类型的对象(它不是标示符,也不是属性访问器),相应地,this值最终设为全局对象。

更多复杂的例子:

var foo = { bar: function () { alert(this); }}; foo.bar(); // Reference, OK => foo(foo.bar)(); // Reference, OK => foo (foo.bar = foo.bar)(); // global&#63;(false || foo.bar)(); // global&#63;(foo.bar, foo.bar)(); // global&#63; 
登入後複製

为什么我们有一个属性访问器,它的中间值应该为引用类型的值,在某些调用中我们得到的this值不是base对象,而是global对象?

问题在于后面的三个调用,在应用一定的运算操作之后,在调用括号的左边的值不在是引用类型。

1.第一个例子很明显———明显的引用类型,结果是,this为base对象,即foo。

2.在第二个例子中,组运算符并不适用,想想上面提到的,从引用类型中获得一个对象真正的值的方法,如GetValue。相应的,在组运算的返回中———我们得到仍是一个引用类型。这就是this值为什么再次设为base对象,即foo。

3.第三个例子中,与组运算符不同,赋值运算符调用了GetValue方法。返回的结果是函数对象(但不是引用类型),这意味着this设为null,结果是global对象。

4.第四个和第五个也是一样——逗号运算符和逻辑运算符(OR)调用了GetValue 方法,相应地,我们失去了引用而得到了函数。并再次设为global。

引用类型和this为null

有一种情况是这样的:当调用表达式限定了call括号左边的引用类型的值, 尽管this被设定为null,但结果被隐式转化成global。当引用类型值的base对象是被活动对象时,这种情况就会出现。
下面的实例中,内部函数被父函数调用,此时我们就能够看到上面说的那种特殊情况。正如我们在第12章知道的一样,局部变量、内部函数、形式参数储存在给定函数的激活对象中。

function foo() { function bar() { alert(this); // global } bar(); // the same as AO.bar()} 
登入後複製

活动对象总是作为this返回,值为null——(即伪代码的AO.bar()相当于null.bar())。这里我们再次回到上面描述的例子,this设置为全局对象。

有一种情况除外:如果with对象包含一个函数名属性,在with语句的内部块中调用函数。With语句添加到该对象作用域的最前端,即在活动对象的前面。相应地,也就有了引用类型(通过标示符或属性访问器), 其base对象不再是活动对象,而是with语句的对象。顺便提一句,它不仅与内部函数相关,也与全局函数相关,因为with对象比作用域链里的最前端的对象(全局对象或一个活动对象)还要靠前。

var x = 10; with ({ foo: function () { alert(this.x); }, x: 20 }) { foo(); // 20 } // because var fooReference = { base: __withObject, propertyName: 'foo'}; 
登入後複製

同样的情况出现在catch语句的实际参数中函数调用:在这种情况下,catch对象添加到作用域的最前端,即在活动对象或全局对象的前面。但是,这个特定的行为被确认为ECMA-262-3的一个bug,这个在新版的ECMA-262-5中修复了。这样,在特定的活动对象中,this指向全局对象。而不是catch对象。

try { throw function () { alert(this); };} catch (e) { e(); // ES3标准里是__catchObject, ES5标准里是global } // on idea var eReference = { base: __catchObject, propertyName: 'e'}; // ES5新标准里已经fix了这个bug,// 所以this就是全局对象了var eReference = { base: global, propertyName: 'e'}; 
登入後複製

同样的情况出现在命名函数(函数的更对细节参考第15章Functions)的递归调用中。在函数的第一次调用中,base对象是父活动对象(或全局对象),在递归调用中,base对象应该是存储着函数表达式可选名称的特定对象。但是,在这种情况下,this总是指向全局对象。

(function foo(bar) { alert(this); !bar && foo(1); // "should" be special object, but always (correct) global })(); // global 
登入後複製

作为构造器调用的函数中的this

还有一个与this值相关的情况是在函数的上下文中,这是一个构造函数的调用。

function A() { alert(this); // "a"对象下创建一个新属性 this.x = 10;} var a = new A();alert(a.x); // 10 
登入後複製

在这个例子中,new运算符调用“A”函数的内部的[[Construct]] 方法,接着,在对象创建后,调用内部的[[Call]] 方法。 所有相同的函数“A”都将this的值设置为新创建的对象。

函数调用中手动设置this

在函数原型中定义的两个方法(因此所有的函数都可以访问它)允许去手动设置函数调用的this值。它们是.apply和.call方法。他们用接受的第一个参数作为this值,this 在调用的作用域中使用。这两个方法的区别很小,对于.apply,第二个参数必须是数组(或者是类似数组的对象,如arguments,反过来,.call能接受任何参数。两个方法必须的参数是第一个——this。

例如:

var b = 10; function a(c) { alert(this.b); alert(c);} a(20); // this === global, this.b == 10, c == 20 a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40 
登入後複製

结论

在这篇文章中,我们讨论了ECMAScript中this关键字的特征(对比于C++ 和 Java,它们的确是特色)。我希望这篇文章有助于你准确的理解ECMAScript中this关键字如何工作。同样,我很乐意在评论中回到你的问题。

來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!