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

前言

这篇文章的产生,是基于冴羽大大的JavaScript 深刻之从 ECMAScript 规范解读 this的思考,这是对应掘金连接,文中详细的论述了来龙去脉,建议各位均可以去了解一下,颇有帮助,而且这篇文章在写做时,也有冴羽大大的帮助,再次表示感谢~javascript

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

那为何还有这篇文章呢?由于不少的同窗在冴羽大大的博客下评论没有看懂,我也是其中的一员,因而我决定要弄明白为何,如今也把个人一些整理分享出来,但愿对你们也有帮助。git

再啰嗦一句,对于知道了各类状况下 this 如何判断的同窗来讲,这篇文章并不会告诉你如何进行 this 指向的判断,更多的是知道为何这样判断,不知足于知其然,更知其因此然。github

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

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

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

  1. resolved 翻译为 已完成函数

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

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

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

举个例子: 如今咱们有一个对象:time ,而后他有三个属性:this

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

相关文章
相关标签/搜索