JS 基础|挖一挖 this 原理

吐槽君 分类:javascript

前言

此文参照 ECMAScript 规范,结合各大佬的博客,死啃 this 指向的原理。从 ECMAScript 角度切入,给自己对 this 指向更深入的理解,下文将从题目切入,以题目为例子讲解通过 ECMAScript 规范为思路求解 this 指向的方式

题目

var value = 1;

var foo = {
  value: 2,
  bar: function () {
    console.log(this)
  }
}

('123', foo.bar)();
 

规范

查阅规范中的 11.2.3 函数调用章节

产生式 CallExpression : MemberExpression Arguments 按照下面的过程执行 :

1. 令 ref 为解释执行 MemberExpression 的结果 .
2. 令 func 为 GetValue(ref).
3. 令 argList 为解释执行 Arguments 的结果 , 产生参数值们的内部列表 (see 11.2.4).
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 作为参数列表

产生式 CallExpression : CallExpression Arguments以完全相同的方式执行,除了第1步执行的是其中的CallExpression。
注:假如func是一个原生的ECMAScript对象,返回的结果永远不会是Reference类型,调用一个宿主对象是否返回一个Reference类型的值由实现决定。 若一Reference值返回,则它必须是一个非严格的属性引用。
 

参照规范,结合例子梳理原理

01:确定 MemberExpression

参照 step1:令 ref 为解释执行 MemberExpression 的结果 .

确定 MemberExpression,题中为 ('123', foo.bar), 赋值给 MemberExpression

下面结论参照 《JavaScript深入之从ECMAScript规范解读this》

MemberExpression 成员表达式,简单理解即为:括号的左边部分,具体可以参见 https://github.com/mqyqingfeng/Blog/issues/7
 

02:计算 ref

准备工作:执行 Type(ref) 前需要先计算 ref 的值

此时 ref = ('123', foo.bar),它是携带逗号运算符的表达式,需要参照逗号运算符的计算

下面内容参照规范中的 11.14 逗号运算符

语法:

Expression :
AssignmentExpression
Expression , AssignmentExpression

ExpressionNoIn :
AssignmentExpressionNoIn
ExpressionNoIn , AssignmentExpressionNoIn
 
语义:

产生式 Expression : Expression , AssignmentExpression 按照下面的过程执行 :
1. 令 lref 为解释执行 Expression 的结果 .
2. Call GetValue(lref).
3. 令 rref 为解释执行 AssignmentExpression 的结果 .
4. 返回 GetValue(rref).

ExpressionNoIn 执行完全按照Expression相同的方式,除了AssignmentExpressionNoIn替代了AssignmentExpression。

注: GetValue必须调用,即使它的值没有用,因为它可能有附加效果。
 

按以上的规范,对于表达式 ('123', foo.bar),计算结果过程为

// 以下为伪代码
// 参照逗号运算符规范中的步骤 3、4

rref = foo.bar
return GetValue(rref)
 

GetValue 规范中的 8.7.1 GetValue(v)

- GetBase(V)。 返回引用值 V 的基值组件。
- GetReferencedName(V)。 返回引用值 V 的引用名称组件。
- IsStrictReference(V)。 返回引用值 V 的严格引用组件。
- HasPrimitiveBase(V)。 如果基值是 Boolean, String, Number,那么返回 true。
- IsPropertyReference(V)。 如果基值是个对象或 HasPrimitiveBase(V) 是 true,那么返回 true;否则返回 false。
- IsUnresolvableReference(V)。 如果基值是 undefined 那么返回 true,否则返回 false。

本规范使用以下抽象操作来操作引用:

1. 如果 Type(V) 不是引用 , 返回 V。
2. 令 base 为调用 GetBase(V) 的返回值。
3. 如果 IsUnresolvableReference(V), 抛出一个 ReferenceError 异常。
4. 如果 IsPropertyReference(V), 那么
        a. 如果 HasPrimitiveBase(V) 是 false, 那么令 get 为 base 的 [[Get]] 内部方法 , 否则令 get 为下面定义的特殊的 [[Get]] 内部方法。
        b. 将 base 作为 this 值,传递 GetReferencedName(V) 为参数,调用 get 内部方法,返回结果。
5. 否则 , base 必须是一个 environment record。
6. 传递 GetReferencedName(V) 和 IsStrictReference(V) 为参数调用 base 的 GetBindingValue( 见 10.2.1) 具体方法,返回结果。

GetValue 中的 V 是原始基值的 属性引用 时使用下面的 [[Get]] 内部方法。它用 base 作为他的 this 值,其中属性 P 是它的参数。采用以下步骤:

1. 令 O 为 ToObject(base)。
2. 令 desc 为用属性名 P 调用 O 的 [[GetProperty]] 内部方法的返回值。
3. 如果 desc 是 undefined,返回 undefined。
4. 如果 IsDataDescriptor(desc) 是 true,返回 desc.[[Value]]。
5. 否则 IsAccessorDescriptor(desc) 必须是 true,令 getter 为 desc.[[Get]]。
6. 如果 getter 是 undefined,返回 undefined。
7. 提供 base 作为 this 值,无参数形式调用 getter 的 [[Call]] 内部方法,返回结果。

注:上述方法之外无法访问在第一步创建的对象。实现可以选择不真的创建这个对象。使用这个内部方法给实际属性访问产生可见影响的情况只有在调用访问器函数时。
 

简单理解,使用向 GetValue 中传入引用类型,将返回引用类型的具体值

所以计算结果为 ref = GetValue(rref) = function(){console.log(this)}

ref 指向了具体的值 function(){console.log(this)} 而不是它的引用

03:判断 Type(ref) 是否为 Reference

参照 step6、7:判断 Type(ref) 是否为 Reference

Reference
 一个 引用 (Reference) 是个已解决的命名绑定。一个引用由三部分组成, 基 (base) 值, 引用名称(referenced name) 和布尔值 严格引用 (strict reference) 标志。基值是 undefined, 一个 Object, 一个 Boolean, 一个 String, 一个 Number, 一个 environment record 中的任意一个。基值是 undefined 表示此引用可以不解决一个绑定。引用名称是一个字符串。
 
Reference 例子
'use strict';
var foo;

// 标识符解析会产生 Reference
var Reference = Object.create(null);
Reference.base = EnvironmentRecord;
Reference.name = 'foo';
Reference.strict = true;

// or
foo.bar;

// 属性访问会产生 Reference
var Reference = Object.create(null);
Reference.base = foo;
Reference.name = 'bar';
Reference.strict = true;

// or 使用未声明的变量
a;
var Reference = Object.create(null);
Reference.base = undefined;
Reference.name = 'a';
Reference.strict = true;
 
Reference 参照表

1085489-20190618114016758-106041760.png

结果

根据上面的 Reference 理论,结合 ref = function(){console.log(this)} ,得到 Type(ref) 不是 Reference

04:确定 thisValue

参照 step7:假如 Type(ref) 不是 Reference. 令 thisValue 为 undefined.

所以 thisValue 为 undefined

总结

从 ECMAScript 规范角度研究 this 指向,需要参照以下不走

  1. 确定 MemberExpression 赋值给 ref

  2. 获取 ref 的结果,比如

    • foo.bar()(foo.bar)() 结果为 foo.bar
    • (foo.bar = foo.bar)()(foo || foo.bar)()(foo.bar, foo.bar)() 结果为 function(){console.log(this)}
  3. 判断 Type(ref) 是否为 Reference

    • 如果是进入函数调用的 step6
    • 如果不是进入函数调用的 step7

参考

《ECMAScript 5.1 规范》中文版

《从冴羽的《JavaScript深入之从ECMAScript规范解读this》引起的思考》

《根治JavaScript中的this-ECMAScript规范解读》

《JavaScript深入之从ECMAScript规范解读this》

《【JavaScript】从 this 指向到 reference 类型》

回复

我来回复
  • 暂无回复内容