引言 |
在这篇文章里我们将会讨论与执行上下文直接相关的更多细节。讨论的主题就是this关键字。实践证明,这个主题是足够难的并且在不同的执行上下文中判定this的值经常会引发出许多问题。
许多编程人员习惯于认为编程语言中的this关键字和面向对象编程时密切相关的,准确的说就是this指向通过构造函数新创建的对象(译者注:c++中this指针只能在一个类的成员函数中调用,它表示当前对象的地址)。在ECMAScript中这个理念也是被实现了的,然而,我们必须要明白,在ECMAScript中它不仅仅局限在指向创建的对象。下面让我们详细看看ECMAScript中确切的this值。
定义 |
this是执行上下文的一个属性:
1 activeExecutionContext={ 2 VO:{...}, 3 this:thisValue 4 }
在这里VO是变量对象,在前面的章节中我们已经讨论过。this与上下文的执行代码类型是直接相关的。一旦进入上下文它的值就被决定了,并且当代码在上下文执行时其值是不可变的。让我们更详细的考虑这些情形。
全局代码中的this值 |
在全局代码中,一切事情都是很简单的,this值就是全局对象本身,因此,间接引用this是可能的:
1 //全局对象的显式属性定义 2 this.a=10;//global.a=10 3 alert(a);//10 4 //通过对无类型标识符赋值的隐式定义 5 b=20; 6 alert(this.a);//20 7 //通过变量声明隐式定义,因为全局上下文的变量对象就是全局对象自身 8 var c=30; 9 alert(this.c);//30
函数代码中的this值 |
当this使用在函数代码中事情就变得有趣多了。这种情况是最难的也会引发很多的问题。在函数代码中,this值的第一(可能也是最重要的)特性就是不能 静态的绑定到一个函数上。正如上面已经提到的,this值在进入上下文时就已经被决定了,在函数代码的情形下,每次this值可以是完全不一样的。
然而,在代码运行时,this值是不可改变的。例如,不能给this赋一个新值,因为this不是变量(相反,python编程语言中,可以显式的定义self对象,在运行的时候self对象可重复改变)
1 var foo={x:10}; 2 var bar={ 3 x:20, 4 test:function(){ 5 alert(this===bar);//true 6 alert(this.x);//20 7 this=foo;//error,不能改变this值 8 alert(this.x);//如果这里没有错的话,结果会是10,不是20 9 } 10 }; 11 //当进入上下文时,this值由"bar"对象决定的。为什么这样-将在下面详细讨论 12 bar.test();//true,20 13 foo.test=bar.test; 14 //然而,此时在这里的this将指向"foo"-即使我们调用同一个函数 15 foo.test();//false,10
在函数代码中是什么影响了this值的变化?有多方面的因素。
首先,在一般的函数调用中,this由触发上下文代码的调用者提供。例如,调用函数的父上下文环境。并且this值由调用形式所决定(换句话说,就是由函 数调用的语法形式决定)。为了能够在任何上下文中准确无误的判定this值,必须去理解和记住这一个重要理论。准确的调用形式,例如调用函数的方式,影响 调用上下文的的this值,别无其他了。
(当我们在一些关于javascript 的文章和书本中会看到“this值由函数如何定义来决定:如果是全局函数,this值被设置为全局对象,如果函数时一个对象的方法,this值设置为这个 对象”-这是一个错误的描述)。从下面我们可以看出即使是一般的全局函数,也能够被产生不同this值的调用表达式所触发。
1 function foo(){ 2 alert(this); 3 } 4 foo();//global 5 alert(foo===foo.prototype.constructor);//true 6 //但是对于同一函数的另一种调用表达式形式,this值是不同的 7 foo.prototype.constructor();//foo.prototype
调用一个被定义为对象方法的函数,但是this值不会被设置为这个对象也是同样可能的:
1 var foo={ 2 bar:function(){ 3 alert(this); 4 alert(this===foo); 5 } 6 }; 7 foo.bar();//foo,true 8 var exampleFunc=foo.bar; 9 alert(exampleFunc===foo.bar);//true 10 //同一函数的另一种调用表达式形式,得到不同的this值 11 exampleFunc();//global,false
调用形式是如何影响this值了?为了充分理解this值的确定值,必须详细考虑一种内置类型-Reference
类型
引用类型 |
使用伪代码编程思想。Reference
类型的值可以由含两个属性的对象表示:base(例如,属性属于这个对象)和这个base中的属性名:
1 var valueOfReferenceType={ 2 base:<base object>, 3 propertyName:<property name> 4 }
Reference
类型的值仅仅只有两种情况:
- 当我们处理一个标识符时
- 或者获取属性
标识符通过标识符获取进程来处理,这会在第四节:作用域链中详细考虑。在这里我们仅仅关注这里面一直有Reference
类型(对this值是很重要的) 的算法返回情况 。
标识符是变量名,函数名,函数参数名和全局对象的未声明(译者注:前面没有var)属性名。例如,下面标识符的值:
1 var foo = 10; 2 function bar() {}
操作符的中间结果,对应的Reference
值如下:
1 var fooReference={ 2 base;global, 3 propertyName:'foo' 4 }; 5 var barReference={ 6 base:global, 7 propertyName:'bar' 8 };
为了能够从Reference
类型的值中获取一个对象的真实值,这里有一个GetValue
方法,在伪代码中描述如下:
1 function GetValue(value) { 3 if (Type(value) != Reference) { 4 return value; 5 } 7 var base = GetBase(value); 9 if (base === null) { 10 throw new ReferenceError; 11 } 13 return base.[[Get]](GetPropertyName(value)); 15 }
内置的[[Get]]
方法返回对象属性的真实值,也包括从原型链中解析出的继承属性:
1 GetValue(fooReference); // 10 2 GetValue(barReference); // function object "bar"
我们也知道属性获取有两种方式:点操作符(当属性名是正确的标识符并且已知的),或者用中括号:
1 foo.bar();
2 foo['bar']();
返回中间计算结果时,我们可以获得Reference
类型的值:
1 var fooBarReference = { 2 base: foo, 3 propertyName: 'bar' 4 }; 6 GetValue(fooBarReference); // function object "bar"
因此,在许多重要的场合,Reference
类型的值是如何和函数上下文中的this值关联的?来到本文的重要时刻。在函数上下文中决定this值的一般规则如下:
在函数上下文中的this值由调用者提供并由当前的调用表达式形式所决定(这个函数在语法意义上如何书写的)。
如果在调用(...)的左边,有一个Reference
类型的值,这样this值被这设置为Reference
类型的base对象。
在其他所有的情形(例如与Reference
类型不同的任何类型值),this值一直设置为null。但由于this值为null没
有任何意义,它会隐式的转化为全局对象
我们看下面的例子:
1 function foo() { 2 return this; 3 } 5 foo(); // global
我们看到在调用括号的左边是一个引用类型值(因为foo是一个标示符):
1 var fooReference = { 2 base: global, 3 propertyName: 'foo' 4 };
相应地,this也设置为引用类型的base对象。即全局对象。同样,使用属性访问器:
1 var foo = { 2 bar: function () { 3 return this; 4 } 5 }; 6 7 foo.bar(); // foo
同样,我们拥有一个引用类型的值,其base是foo对象,在函数bar激活时将base设置给this。
1 var fooBarReference = { 2 base: foo, 3 propertyName: 'bar' 4 };
但是,如果用另一种方式激活相同的函数,this的值将不同。
1 var test = foo.bar; 2 test(); // global
因为test作为标识符,产生了其他引用类型的值,该值的base(全局对象)被设置为this的值。
1 var testReference = { 2 base: global, 3 propertyName: 'test' 4 }
现在,我们可以很明确的说,为什么用不同的形式激活同一个函数会产生不同的this,答案在于不同的引用类型(type Reference)的中间值。
1 function foo() { 2 alert(this); 3 } 5 foo(); // global, because 7 var fooReference = { 8 base: global, 9 propertyName: 'foo' 10 }; 12 alert(foo === foo.prototype.constructor); // true 14 // another form of the call expression 16 foo.prototype.constructor(); // foo.prototype, because 18 var fooPrototypeConstructorReference = { 19 base: foo.prototype, 20 propertyName: 'constructor' 21 };
另一个通过调用方式动态确定this的值的经典例子:
1 function foo() { 2 alert(this.bar); 3 } 4 5 var x = {bar: 10}; 6 var y = {bar: 20}; 7 8 x.test = foo; 9 y.test = foo; 10 11 x.test(); // 10 12 y.test(); // 20
函数调用和非引用类型 |
那么,正如我们已经指出,当调用括号的左边不是引用类型而是其它类型,this的值自动设置为null,实际最终this的值被隐式转换为全局对象。让我们思考下面这种函数表达式:
1 (function () { 2 alert(this); // null => global 3 })();
在这个例子中,我们有一个函数对象但不是引用类型的对象(因为它不是标示符,也不是属性访问器),相应地,this的值最终被设为全局对象。更多复杂的例子:
1 var foo = { 2 bar: function () { 3 alert(this); 4 } 5 }; 6 7 foo.bar(); // Reference, OK => foo 8 (foo.bar)(); // Reference, OK => foo 9 10 (foo.bar = foo.bar)(); // global? 11 (false || foo.bar)(); // global? 12 (foo.bar, foo.bar)(); // global?
那么,为什么我们有一个属性访问器,它的中间值应该为引用类型的值,但是在某些调用中我们得到this的值不是base对象,而是global对象?问题出现在后面的三个调用,在执行一定的操作运算之后,在调用括号的左边的值不再是引用类型。
第一个例子很明显———明显的引用类型,结果是,this为base对象,即foo。
在 第二个例子中,分组操作符(译者注:这里的分组操作符就是指foo.bar外面的括号"()")没有实际意义,想想上面提到的,从引用类型中获得一个对象 真正的值的方法,如GetValue 。相应的,在分组操作的返回值中——我们得到的仍是一个引用类型。这就是this的值为什么再次被设为base对象,即 foo。
第三个例子中,与分组操作符不同,赋值操作符调用了GetValue方法。返回的结果已经是函数对象(不是引用类型),这意味着this的值被设为null,实际最终结果是被设置为global对象。
第四个和第五个也是一样——逗号操作符和逻辑操作符(OR)调用了GetValue 方法,相应地,我们失去了引用类型的值而得到了函数类型的值,所以this的值再次被设为global对象。
引用类型和this为null |
有一种情况,如果调用方式确定了引用类型的值,不管怎样,只要this的值被设置为null,其最终就会被隐式转换成global。当引用类型值的 base对象是激活对象时,就会导致这种情况。下面的实例中,内部函数被父函数调用,此时我们就能够看到上面说的那种特殊情况。正如我们在 第二章学到的一样,局部变量、内部函数、形式参数都储存在给定函数的活动对象中。
1 function foo() { 2 function bar() { 3 alert(this); // global 4 } 5 bar(); // the same as AO.bar() 6 }
激活对象总是作为this的值返回——null(即伪代码AO.bar()相当于null.bar())。这里我们再次回到上面描述的情况,this的值最终还是被设置为全局对象。
有一种情况除外:“在with语句中调用函数,且在with对象(译者注:即下面例子中的__withObject)中包含函数名属性时”。 with语句将其对象添加在作用域链最前端,即在激活对象的前面。那么对应的,引用类型有值(通过标识符或属性访问器),其base对象不再是激活对象, 而是with语句的对象。顺便提一句,这种情况不仅跟内部函数相关,还跟全局函数相关,因为with对象比作用域链里的最前端的对象(全局对象或一个激活 对象)还要靠前。
1 var x = 10; 2 3 with ({ 4 5 foo: function () { 6 alert(this.x); 7 }, 8 x: 20 9 10 }) { 11 12 foo(); // 20 13 14 } 15 16 // because 17 18 var fooReference = { 19 base: __withObject, 20 propertyName: 'foo' 21 };
在catch语句的实际参数中的函数调用存在类似情况:在这种情况下,catch对象被添加到作用域的最前端,即在激活对象或全局对象的前面。但 是,这个特定的行为被确认为是ECMA-262-3的一个bug,这个在新版的ECMA-262-5中修复了。修复后,在特定的激活对象中,this指向 全局对象。而不是catch对象。
1 try { 2 throw function () { 3 alert(this); 4 }; 5 } catch (e) { 6 e(); // __catchObject - in ES3, global - fixed in ES5 7 } 8 9 // on idea 10 11 var eReference = { 12 base: __catchObject, 13 propertyName: 'e' 14 }; 15 16 // but, as this is a bug 17 // then this value is forced to global 18 // null => global 19 20 var eReference = { 21 base: global, 22 propertyName: 'e' 23 };
同样的情况出现的命名函数(函数的更多细节参考Chapter 5. Functions)的递归调用中。在函数的第一次调用中,base对象是父激活对象(或全局对象),在递归调用中,base对象应该是存储着函数表达式可选名称的特定对象。但是,在这种情况下,this的值也总是被设置为global。
1 (function foo(bar) { 2 3 alert(this); 4 5 !bar && foo(1); // "should" be special object, but always (correct) global 6 7 })(); // global
构造函数中的this |
还有一个在函数的上下文中与this的值相关的情况是:函数作为构造器调用时。
1 function A() { 2 alert(this); // newly created object, below - "a" object 3 this.x = 10; 4 } 5 6 var a = new A(); 7 alert(a.x); // 10
在这个例子中,new操作符调用“A”函数内部的[[Construct]]方法,接着,在对象创建后,调用其内部的[[Call]]方法,所有相同的函数“A”都将this的值设置为新创建的对象。
手动设置函数调用的this值 |
在Function.prototype中定义了两个方法允许手动设置函数调用时this的值,它们是apply和call方法(所有的函数都 可以访问它们)。它们用接受的第一个参数作为this的值,this在调用的作用域中使用。这两个方法的区别不大,对于apply,第二个参数必须是数组 (或者是类似数组的对象,如arguments,相反,call能接受任何参数。两个方法必须的参数都是第一个参数值—this。
例如:
1 var b = 10; 3 function a(c) { 4 alert(this.b); 5 alert(c); 6 } 8 a(20); // this === global, this.b == 10, c == 20 10 a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30 11 a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40
相关推荐
深入理解JavaScript系列(6):S.O.L.I.D五大原则之单一职责SRP 深入理解JavaScript系列(7):S.O.L.I.D五大原则之开闭原则OCP 深入理解JavaScript系列(8):S.O.L.I.D五大原则之里氏替换原则LSP 深入理解...
深入理解JavaScript系列(13):This Yes this 深入理解JavaScript系列(14):作用域链 Scope Chain 深入理解JavaScript系列(15):函数(Functions) 深入理解JavaScript系列(16):闭包(Closures) 深入...
在JavaScript编程语言中,this关键字是一个非常重要且经常引起困惑的概念。本文将深入探讨this关键字在不同执行上下文中的行为及其确定方式,希望能帮助读者更好地理解和掌握这一关键概念。 首先,要明确的是this是...
《JavaScript内核系列》和《JavaScript面向对象基础》这两本书是深入理解JavaScript编程的重要资源。JavaScript,作为一种广泛应用于Web开发的脚本语言,其内核和面向对象特性是开发者必须掌握的基础知识。以下是对...
"智能社JavaScript系列视频资料"显然是一份专门针对学习和深入理解JavaScript的教育资源,包含PPT讲解和源码实例,是提升JavaScript技能的理想资源。 1. **JavaScript基础** - 变量与数据类型:JavaScript支持var...
【狂神说系列 JavaScript笔记】是一份全面且深入的JavaScript学习资源,旨在帮助开发者和初学者深入理解这门广泛应用于Web开发的脚本语言。这份笔记涵盖了JavaScript的基础语法、核心概念以及高级特性,旨在构建一个...
该文档是根据博客园汤姆大叔的深入理解JavaScript系列(http://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html)博文整理而成,主要内容包括: 1.编写高质量JavaScript代码的基本要点 2.揭秘命名函数...
在JavaScript中,函数是一种非常重要的编程单元,它不仅可以封装一系列的操作逻辑,还可以作为数据进行传递和处理。此外,函数内部的关键字`this`的指向问题也经常让人感到困惑。本文将通过具体的示例来探讨...
"一系列JavaScript项目的最佳实践"这个主题涵盖了从项目结构、编码规范到测试和部署等多个方面。以下是一些核心的知识点: 1. **项目结构**:良好的项目结构能让代码更易于理解和维护。通常包括src(源代码)、dist...
首先,LinkedList由一系列节点(Node)组成,每个节点包含两部分:数据和指向下一个节点的引用。在JavaScript中,我们可以创建一个Node类来表示这个结构: ```javascript class Node { constructor(data) { this....
JavaScript,作为全球最广泛使用的编程语言之一,是创建交互式网页和动态应用的关键工具。"传智播客JavaScript L7"教程是这个广泛学习资源系列的第七部分,它旨在深入探讨JavaScript的核心概念和技术,帮助初学者和...
JavaScript中的this关键字是一个非常重要的概念,它与函数的执行上下文紧密相关,决定了函数的上下文对象,即函数内的代码可以访问哪些属性和方法。在ECMAScript标准中,this的值在函数调用时确定,并且不是静态绑定...
这个"JavaScript经典实用教程"很可能包含了上述各个方面的详细讲解,通过bookinfo.dat和一系列.pdg文件,读者可以逐步学习和掌握JavaScript的各个方面。每个.pdg文件可能对应教程的一个章节或主题,通过阅读和实践,...
在《JavaScript类库大全》这一文档中,主要介绍了一系列的JavaScript类库及其使用方法。文档强调用户无需记住所有细节,只需在需要时进行复制粘贴即可。这表明文档中的代码片段或函数设计为方便快捷地解决常见的网页...
BOM并不是一个正式标准,而是浏览器厂商为了实现对浏览器窗口的控制而提出的一系列接口集合,如window、navigator、location、screen等。通过这些接口,开发者可以获取用户信息、操作浏览器窗口、发起网络请求等。 ...
JavaScript语法与C++和Java有诸多相似之处,但更为宽松。变量在JavaScript中无需预先声明,可以直接使用`var`关键字进行定义,如`var myVariable = "Hello, World!";`。数据类型包括基本类型(如字符串、数字、布尔...
在JavaScript编程中,对象创建模式是优化代码结构和提高可维护性的重要手段。本文将深入探讨两种常见的对象创建模式:函数语法糖和对象常量。 首先,我们来看**函数语法糖**模式。这是一种通过扩展Function....