深入JavaScript this关键字
作者:Seiya
时间:2019年06月10日
前言
许多程序员习惯的认为,在程序语言中,this
关键字与面向对象程序开发紧密相关,其完全指向由构造器新创建的对象。在 ECMAScript 规范中也是这样实现的,但在ECMAScript 中,this
并不限于只用来指向新创建的对象。
定义
this
是执行上下文中的一个属性。
this
与上下文中可执行代码的类型有直接关系,this
值在进入上下文时确定,并且在上下文运行期间永久不变。
全局代码中的this
在全局代码中,this
始终是全局对象本身,这样就有可能间接的引用到它了,举例如下:
/* 显示定义全局对象的属性 */
this.a = 10; // global.a = 10
alert(a); // 10
/* 通过赋值给一个无标示符隐式 */
b = 20;
alert(this.b); // 20
/* 也是通过变量声明隐式声明的,因为全局上下文的变量对象是全局对象自身 */
var c = 30;
alert(this.c); // 30
函数代码中的this
这种类型的代码中,this
的首要特点是它不是静态的绑定到一个函数。
this
是进入上下文时确定,在一个函数代码中,这个值在每一次完全不同。不管怎样,在代码运行时的 this
值是不变的,也就是说,因为它不是一个变量,就不可能为其分配一个新值。
var foo = {x: 10};
var bar = {
x: 20,
test: function () {
alert(this === bar);
alert(this === foo);
alert(this.x);
/* 下面代码会报错,任何时候不能改变this的值 */
this = foo;
alert(this.x); // 如果不出错的话,应该是10,而不是20
}
};
/* 在进入上下文的时候,this 被当成 bar 对象 */
bar.test(); // true, false, 20
/* 这里 this 是 foo 对象,因为调用的是相同的 function */
foo.test = bar.test;
foo.test(); // false, true, 10
this
是由激活上下文代码的调用者来提供的,即调用函数的父上下文。即使是正常的全局函数也会被调用方式的不同形式激活,这些不同的调用方式导致了不同的this值。
function foo() { alert(this) }
foo(); // this 是 global 对象
alert(foo === foo.prototype.constructor); // true
/* 但是同一个 function 的不同的调用表达式,this 是不同的 */
foo.prototype.constructor(); // this 是 foo.prototype 对象
为了充分理解 this
值的确定,需要详细分析其内部类型之一 ———— 引用类型(Reference type)。
规范类型
ECMAScript 的类型分为语言类型和规范类型。语言类型是开发者直接使用 ECMAScript 可以操作的。其实就是我们常说的Undefined, Null, Boolean, String, Number, 和 Object。
而规范类型相当于 meta-values,是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型的。规范类型包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。它们的作用是用来描述语言底层行为逻辑。
引用类型(Reference)
尤雨溪:这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。
Reference 的构成由三个组成部分:
base value
base value 就是属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种;
referenced name
referenced name 就是属性的名称
strict reference
strict reference 严格引用标志
举例:
var foo = { bar: function () { return this; } }; foo.bar(); /* bar 对应的Reference是: */ var BarReference = { base: foo, propertyName: 'bar', strict: false };
获取 Reference 组成部分的其中两个抽象操作:
GetValue
返回 Reference 的 base value ,包括对原型链中继承的属性分析;
注意:
调用 GetValue,返回的将是具体的值,而不再是一个 Reference
IsPropertyReference
如果 base value 是个对象或 HasPrimitiveBase 是 true,那么返回 true,否则返回 false;
确定 this 值
函数上下文中的 this 值
ECMAScript 5.1 规范 11.2.3 函数调用一节中规定:
产生式 CallExpression : MemberExpression Arguments 按照下面的过程执行 :
- 令 ref 为解释执行 MemberExpression 的结果;
- 令 func 为 GetValue(ref);
- 令 argList 为解释执行 Arguments 的结果 , 产生参数值们的内部列表;
- 如果 Type(func) is not Object ,抛出一个 TypeError 异常;
- 如果 IsCallable(func) is false ,抛出一个 TypeError 异常;
- 如果 Type(ref) 为 Reference,那么 如果 IsPropertyReference(ref) 为 true,那么 令 thisValue 为 GetBase(ref). 否则 , ref 的基值是一个环境记录项 令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的结果;
- 否则 , 假如 Type(ref) 不是 Reference. 令 thisValue 为 undefined;
- 返回调用 func 的 [[Call]] 内置方法的结果 , 传入 thisValue 作为 this 值和列表 argList 作为参数列表;
这里主要关注第一步和第六步,简单描述就是:
计算 MemberExpression 的结果赋值给 ref;
判断 ref 是不是一个 Reference 类型:
如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
如果 ref 不是 Reference,那么 this 的值为 undefined
详细解释:
计算 MemberExpression 的结果赋值给 ref
MemberExpression : - PrimaryExpression - FunctionExpression // 函数定义表达式 - MemberExpression [ Expression ] // 属性访问表达式 - MemberExpression . IdentifierName // 属性访问表达式 - new MemberExpression Arguments // 对象创建表达式
简单理解 MemberExpression 其实就是()左边的部分。
【举例如下】:
function foo() { console.log(this) } foo(); // MemberExpression 是 foo function foo() { return function() { console.log(this) } } foo()(); // MemberExpression 是 foo() var foo = { bar: function () { return this; } } foo.bar(); // MemberExpression 是 foo.bar
判断 ref 是不是一个 Reference 类型
关键就在于看规范是如何处理各种 MemberExpression,返回的结果是不是一个Reference类型。
简单解释:
如果调用括号()的左边是引用类型的值,this将设为引用类型值的base对象(base object),在其他情况下(与引用类型不同的任何其它属性),这个值为 null。不过,实际不存在 this 的值为 null 的情况,因为当 this 的值为 null 的时候,其值会被隐式转换为全局对象。
注:
第5版的 ECMAScript 中,已经不强迫转换成全局变量了,而是赋值为 undefined。非严格模式下,this 的值为 undefined 的时候,其值会被隐式转换为全局对象。
举例如下:
var foo = { bar: function () { alert(this); } }; foo.bar(); // Reference, OK => foo (foo.bar)(); // Reference, OK => foo(组运算的返回仍是一个引用类型) (foo.bar = foo.bar)(); // undefind(赋值运算符调用了 GetValue 方法,返回的结果是函数对象,不是 Reference) (false || foo.bar)(); // undefind(逗号运算符调用了 GetValue 方法,返回的结果是函数对象,不是 Reference) (foo.bar, foo.bar)(); // undefind(逻辑运算符调用了 GetValue 方法,返回的结果是函数对象,不是 Reference)
补充一种普遍的情况:
function foo() { console.log(this) } foo(); /* foo 对应的 Reference 是:*/ var fooReference = { base: EnvironmentRecord, name: 'foo', strict: false };
这里 ref 为 Reference,由于 base value 是 EnvironmentRecord,而不是一个对象。所以 IsPropertyReference(ref) 的结果为 false,那么 this 的值为 ImplicitThisValue(ref),而该函数始终返回 undefined。
函数调用中手动设置 this
在函数原型中定义的两个方法(因此所有的函数都可以访问它)允许去手动设置函数调用的 this 值,他们用接受的第一个参数作为this值,this 在调用的作用域中使用:
apply
对于
apply
,第二个参数必须是数组,或者是类似数组的对象,如arguments
;
call
call
能接受任何参数
举例如下:
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 == 30
a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40