深入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 按照下面的过程执行 :

    1. 令 ref 为解释执行 MemberExpression 的结果;
    2. 令 func 为 GetValue(ref);
    3. 令 argList 为解释执行 Arguments 的结果 , 产生参数值们的内部列表;
    4. 如果 Type(func) is not Object ,抛出一个 TypeError 异常;
    5. 如果 IsCallable(func) is false ,抛出一个 TypeError 异常;
    6. 如果 Type(ref) 为 Reference,那么 如果 IsPropertyReference(ref) 为 true,那么 令 thisValue 为 GetBase(ref). 否则 , ref 的基值是一个环境记录项 令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的结果;
    7. 否则 , 假如 Type(ref) 不是 Reference. 令 thisValue 为 undefined;
    8. 返回调用 func 的 [[Call]] 内置方法的结果 , 传入 thisValue 作为 this 值和列表 argList 作为参数列表;

    这里主要关注第一步和第六步,简单描述就是:

    1. 计算 MemberExpression 的结果赋值给 ref;

    2. 判断 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
最后更新时间: 2019-7-7 21:55:38