在 JavaScript,切换上下文最主要的场景是函数调用。 重学前端-1.7 JS执行3-函数 - 图1

this 关键字的行为

调用函数时使用的引用,决定了函数执行时刻的 this 值。

  1. function showThis(){
  2. console.log(this);
  3. }
  4. var o = {
  5. showThis: showThis
  6. }
  7. showThis(); // global
  8. o.showThis(); // o

普通函数的 this 值由“调用它所使用的引用”决定,其中奥秘就在于:我们获取函数的表达式,它实际上返回的并非函数本身,而是一个 Reference 类型(记得我们在类型一章讲过七种标准类型吗,正是其中之一)。

Reference 类型由两部分组成:一个对象和一个属性值。不难理解 o.showThis 产生的 Reference 类型,即由对象 o 和属性“showThis”构成。
当做一些算术运算(或者其他运算时),Reference 类型会被解引用,即获取真正的值(被引用的内容)来参与运算,而类似函数调用、delete 等操作,都需要用到 Reference 类型中的对象。
在这个例子中,Reference 类型中的对象被当作 this 值,传入了执行函数时的上下文当中。
实际上从运行时的角度来看,this 跟面向对象毫无关联,它是与函数调用时使用的表达式相关。

这个设计来自 JavaScript 早年,通过这样的方式,巧妙地模仿了 Java 的语法,但是仍然保持了纯粹的“无类”运行时设施。

改为箭头函数后,不论用什么引用来调用它,都不影响它的 this 值。

创建了一个类 C,并且实例化出对象 o,再把 o 的方法赋值给了变量 showThis。

  1. class C {
  2. showThis() {
  3. console.log(this);
  4. }
  5. }
  6. var o = new C();
  7. var showThis = o.showThis;
  8. showThis(); // undefined
  9. o.showThis(); // o

按照我们上面的方法,不难验证出:生成器函数、异步生成器函数和异步普通函数跟普通函数行为是一致的,异步箭头函数与箭头函数行为是一致的。

this 关键字的机制

函数能够引用定义时的变量,如上文分析,函数也能记住定义时的 this,因此,函数内部必定有一个机制来保存这些信息。

在 JavaScript 标准中,为函数规定了用来保存定义时上下文的私有属性[[Environment]]。当一个函数执行时,会创建一条新的执行环境记录,记录的外层词法环境(outer lexical environment)会被设置成函数的[[Environment]]。

这个动作就是切换上下文了,我们假设有这样的代码:

  1. var a = 1;
  2. foo();
  3. var b = 2;
  4. function foo(){ // 在别处定义了foo:
  5. console.log(b); // 2
  6. console.log(a); // error
  7. }

这里的 foo 能够访问 b(定义时词法环境),却不能访问 a(执行时的词法环境),这就是执行上下文的切换机制了。

JavaScript 用一个栈来管理执行上下文,这个栈中的每一项又包含一个链表。如下图所示:
image.png
当函数调用时,会入栈一个新的执行上下文,函数调用结束时,执行上下文被出栈。

this 则是一个更为复杂的机制,JavaScript 标准定义了 [[thisMode]] 私有属性:

  • lexical:表示从上下文中找 this,这对应了箭头函数。
  • global:表示当 this 为 undefined 时,取全局对象,对应了普通函数。
  • strict:当严格模式时使用,this 严格按照调用时传入的值,可能为 null 或者 undefined。

方法的行为跟普通函数有差异,恰恰是因为 class 设计成了默认按 strict 模式执行。我们可以用 strict 达成与上一节中方法的例子一样的效果:

  1. "use strict"
  2. function showThis(){
  3. console.log(this);
  4. }
  5. var o = {
  6. showThis: showThis
  7. }
  8. showThis(); // undefined
  9. o.showThis(); // o

函数创建新的执行上下文中的词法环境记录时,会根据[[thisMode]]来标记新纪录的[[ThisBindingStatus]]私有属性。
代码执行遇到 this 时,会逐层检查当前词法环境记录中的[[ThisBindingStatus]],当找到有 this 的环境记录时获取 this 的值。
这样的规则的实际效果是,嵌套的箭头函数中的代码都指向外层 this,例如:

  1. var o = {}
  2. o.foo = function foo(){
  3. console.log(1,this);
  4. return () => {
  5. console.log(2, this);
  6. return () => console.log(3, this);
  7. }
  8. }
  9. o.foo()()(); // 1,o; 2, o; 3, o

这个例子中,我们定义了三层嵌套的函数,最外层为普通函数,两层都是箭头函数。这里调用三个函数,获得的 this 值是一致的,都是对象 o。

操作 this 的内置函数

Function.prototype.call 和 Function.prototype.apply 可以指定函数调用时传入的 this 值,call 和 apply 作用是一样的,只是传参方式有区别.示例如下:

  1. function foo(a, b, c){
  2. console.log(this, a, b, c);
  3. }
  4. foo.call({}, 1, 2, 3); // {} 1 2 3
  5. foo.apply({}, [1, 2, 3]); // {} 1 2 3

此外,还有 Function.prototype.bind 它可以生成一个绑定过的函数,这个函数的 this 值固定了参数:

  1. function foo(a, b, c){
  2. console.log(this, a, b, c);
  3. }
  4. foo.bind({}, 1, 2, 3)();

call、bind 和 apply 用于不接受 this 的函数类型如箭头、class 都不会报错。

补充:new 与 this

通过 new 调用函数,跟直接调用的 this 取值有明显区别。
image.png🔚