换个角度看 JavaScript 中的 (this) => { 整理 (JavaScript 深入之从 ECMAScript 规范解读 this ) }

4,497 阅读8分钟

前言

这篇文章的产生,是基于冴羽大大的JavaScript 深入之从 ECMAScript 规范解读 this的思考,这是对应掘金链接,文中详细的论述了前因后果,建议各位都可以去了解一下,很有帮助,并且这篇文章在写作时,也有冴羽大大的帮助,再次表示感谢~

文中的 ES5 规范是参考 颜海镜大大 的译本,也在这里表示感谢。

那为什么还有这篇文章呢?因为很多的同学在冴羽大大的博客下评论没有看懂,我也是其中的一员,于是我决定要弄明白为什么,现在也把我的一些整理分享出来,希望对大家也有帮助。

再啰嗦一句,对于知道了各种情况下 this 如何判断的同学来说,这篇文章并不会告诉你如何进行 this 指向的判断,更多的是知道为什么这样判断,不满足于知其然,更知其所以然。

一. 从 Reference Type (引用类型)开始:

Reference Type :引用类型。在 ES5 文档标准中,将Reference 描述为 a resolved name binding

颜大的 ES5 译本 中,译为已解决的命名绑定。

  1. resolved 翻译为 已完成

  2. name binding 翻译为 命名绑定 没有任何问题,如果有后端语言经验的同学可能更好理解。

那我们再解释下命名绑定:绑定是有双方的,把 命名 ,也就是 我们取的名字 ,要绑定在 某个东西 上面,换言之,就是用 名字 来描述了一个什么 东西

1.为什么需要用一个名字来描述,它没有自己本身的名字吗?

举个例子: 现在我们有一个对象:time ,然后他有三个属性:

 time {
   second: 32,
   minute: 12,
   hour: 10
 }

2.这个对象是存在什么地方的?

我们定义完成后,它必须存在于某一个地方,才能在后面的代码中获取到它。

存在哪由 time 本身的特性来决定,因为它是一个对象,内部的属性是可以添加也可以减少的,换言之,它的大小并不固定。所以我们把它存在了 里面。

如果它的大小固定呢?例如 JavaScript 中的 6 种基本类型的值 :nullundefinedBooleanNumberStringSymbol,既然大小固定,我们就可以放在 里面。


3.堆栈是什么?为什么不同类型的数据要分开放呢?

  • :程序运行时系统分配的一小块内存,大小在编译期时由编译器参数决定。
  • :可以理解为当前可以使用的空闲内存,其大小是需要代码编写的人员自己去申请和释放。(在 JS 中,V8 下有自动垃圾回收机制不需要我们自己操作) [这里只做简单解释,有需要可以自行 Google 更多信息]

4.引用类型存在堆中,和例子有什么关系?

Okay。如果你已经理解了我们的 time 是存在堆中的,那就很好理解了。现在我要用到 time 里面 second属性的值, 我们都知道用 time.second 就可以拿到,但是为什么 time.second 或者 time[’second’] 就可以访问到 second 属性的值呢?

看起来这个问题很蠢是不是,哈哈,但是仔细想想,按理来说:这个值是在内存里面的一小块上面,那我们需要找到这一块内存,才能取到这个值啊。

现在就很好理解了。那其实 time.second 或者 time[’second’] 他们是和内存里面的真正存放 second 的值那个内存位置 是绑定在一起的。只要你用到了 time.second 或者 time[’second’],那编译器就找到,哦,这就是存在xxxxx 地址里面的值也就是 32

二. Reference Typethis 有什么关系?

thisJavascript 中一直是一个初学者难以理解的点,有一些甚至写了 2 年的项目也没搞明白为什么 this 有这样那样的不同。

这里我们先不从使用的场景上来看 this 的指向,还是回归到本源。

站在编译器的角度,是怎么样去理解 this 指向呢?因为this 的指向的判断,常常发生于函数的调用中,那我们就来看看ES5 文档标准中的 11.2.3 Function Calls (函数调用)。

一共分为 8 个步骤:


1. Let ref be the result of evaluating MemberExpression.

2. Let func be GetValue(ref).

3. Let argList be the result of evaluating Arguments, producing an internal list of argument values (see 11.2.4).

4. If Type(func) is not Object, throw a TypeError exception.

5. If IsCallable(func) is false, throw a TypeError exception.

6. If Type(ref) is Reference, then

    a.If IsPropertyReference(ref) is true, then
    i.Let thisValue be GetBase(ref).

    b. Else, the base of ref is an Environment Record
    i.Let thisValue be the result of calling the Implicit This Value concrete method of GetBase(ref).

7. Else, Type(ref) is not Reference.

    a.Let thisValue be undefined.

8. Return the result of calling the [[Call]] internal method on func, providing thisValue as the this value and providing the list argList as the argument values.

我就不翻译了,因为就算翻译出来你可能也读得很累,那么我们用图来看下这个流程会更加直观。

我已经把最关键的几个步骤都标红了,如果在第三步返回的 func 无法通过 5 的判断的话,根本就没有讨论 this 指向的必要。

所以我们重点看:这个里面最关键的点,第 2 , 6 , 7 步骤:

  • 2 步:计算 MemberExpression 的值并且赋值给 Ref: 也就是计算 () 左边的内容的结果,并赋值给 Ref。换句话说: Ref 就是对于 () 左边的内容进行计算之后的引用。

  • 6 步:判断 ref 是否为 Reference 类型: 这个没什么好说的。

  • 7 步:判断 ref 是否是属性引用类型: 官方解释: 通过执行IsPropertyReference(V)来判断的,如果基值是个对象或 HasPrimitiveBase(V) 是 true,那么返回 true;否则返回 false。 HasPrimitiveBase(V):如果基值是 Boolean, String, Number,那么返回 true。 换成大白话,取决于Ref这个引用是基于谁的? 如果它基于一个对象 或者 Boolean, String, Number那就返回 true 否则返回 false

OK 看到这里,估计你也有些累,但是最关键的部分在下面。

三. 回过头来看 this 的 N 种情况

1.直接调用

let a = 'm';
function test() {
  console.log(this.a);
}
test(); // m

我们用刚刚所看到的 3 个步骤来判断下 this:

  1. test()Ref 就是 test引用,它关联到在内存中存储了test()的某一片段。
  2. 判断 test() 是否为引用类型 => true
  3. 判断 Ref 是否是属性引用类型 => false,它并没有定义在某个引用类型的内部。
  4. 进入到图中的第九个步骤:this = ImplicitThisValue(Ref) ,在 Environment Records 下返回 undefined ,而在非严格模式下,浏览器会把 this 指向 window

说起来很麻烦,其实理解起来很简单。

2.在对象内部调用

function test() {
  console.log(this.a);
}
let parent = {
  a: 's',
  test: test
};
parent.test(); // s
  1. parent.test()Ref 就是 parent.test引用,它关联到在内存中存储了test()的某一片段。
  2. 判断 parent.test() 是否为引用类型 => true
  3. 判断 Ref 是否是属性引用类型 => true
  4. 进入到图中的第八个步骤:this = GetBase(Ref) 那这个test() 方法是基于谁呢?很明显就是 parent,所以 this 指向 parent

3.new 关键字

let a = 'k';
function Foo() {
  console.log(this);
}
let c = new Foo();
c.a = 's'

new 关键字调用,区别于一般的函数调用,大家可以看下MDN 上的解释,明确的指出了

  1. 一个继承自 Foo.prototype 的新对象被创建。
  2. 使用指定的参数调用构造函数 Foo ,并将 this 绑定到新创建的对象

如果你仍旧想从规范的角度来解释,建议你读一下ES5 规范:11.2.2 The new Operator 以及关联的 ES5 规范:8.7.1 GetValue (V) 我反复了读了很多遍,但是没有发现如何从规范的角度去解释 this 的指向问题,最后也是请教了冴羽大大才知道 new 可能在底层有明确指定 this的过程,不适合用这样的方式解读,但是,如果你有了更好的答案,很欢迎一起讨论~

既然明白了this 指向的是 c 那么输出的是 {a : 's'}

4.箭头函数

function Foo() {
  return () => {
    return () => {
      console.log(this);
    };
  };
}
console.log(Foo()()());

new 一样箭头函数也是一个特例,但是箭头函数同样可以从对应的规范中找到 this 的答案:

建议参考ES6 规范-箭头函数-evaluation里面的一段话:

“An ArrowFunction does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment. ”

直译为:"ArrowFunction 不为 argumentssuperthisnew.target 定义本地绑定。 对 ArrowFunction 中的 argumentssuperthisnew.target 的任何引用都必须解析为词法作用域中的绑定。"

也就是说,箭头函数内部不会定义 this ,都是由它外部的词法作用域来决定的,也就是说,箭头函数的外部的 this 指向的是谁,那箭头函数内部的 this 指向的也是谁。

回到这个例子,我们知道至始至终,无论你套多少层箭头函数,this 都是指向 Foo 里面的 this,那Foo 里面的 this 根据我们之前的例子可以知道,就是指向了 window

四.最后

欢迎大家关注我的掘金专栏,后期也会更新更多优质的内容~ 有任何问题,欢迎理性和友好的讨论~

题图来自 unsplash