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

栏目: JavaScript · 发布时间: 5年前

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

这篇文章的产生,是基于冴羽大大的 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.堆栈是什么?为什么不同类型的数据要分开放呢?

栈
堆

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.
复制代码

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

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

我已经把最关键的几个步骤都标红了,如果在第三步返回的 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.a);
}
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 那么输出肯定是 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 不为 arguments , super , this 或 new.target 定义本地绑定。 对 ArrowFunction 中的 arguments , super , this 或 new.target 的任何引用都必须解析为词法作用域中的绑定。"

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

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序员第二步

程序员第二步

尹华山 / 人民邮电出版社 / 2013-11 / 45.00元

这本书是写给程序员和项目经理的。作者结合自身的丰富成长历程,通俗易懂地讲述了一名程序员如何才能成为一名优秀的项目经理。内容涉及职业规划、学习方法、自我修炼、团队建设、项目管理等,书中理清了项目管理领域中典型的误区及具有迷惑性的观点,并对项目中的难点问题提出了针对性的解决方法。 全书行文流畅,严谨中带着活泼,理智中透着情感,给读者带来轻松愉快的阅读感受。书中诸多富有创见的观点,让人耳目一新,引......一起来看看 《程序员第二步》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

SHA 加密
SHA 加密

SHA 加密工具