JS 执行机制 - this

97 阅读9分钟
var bar = {
    myName:"juejin.cn",
    printName: function () {
        console.log(myName)
    }    
}
function foo() {
    let myName = "掘金"
    return bar.printName
}
let myName = "稀土掘金"
let _printName = foo()
_printName()
bar.printName()

上面代码,在printName函数中使用的变量myName是属于全局作用域下面的,所以最终打印出来的值都是“稀土掘金”。这是因为JS的作用域链是由词法作用域决定的,而词法作用域是由代码结构来确定的。

按常理来讲,调用bar.printName方法时,该方法内部的变量 myName 应该使用 bar 对象中的,因为它们是一个整体,大多数面向对象语言都是这样设计的,比如 C++ 代码:

#include <iostream>
using namespace std;
class Bar{
    public:
    char* myName;
    Bar(){
      myName = "juejin.cn";
    }
    void printName(){
       cout<< myName <<endl;
    }  
} bar;

char* myName = "稀土掘金";
int main() {
  bar.printName();
  return 0;
}

在这段 C++ 代码中,同样调用了 bar 对象中的 printName 方法,最后打印出来的值就是 bar 对象的内部变量 myName 值——“junjin.cn”,而并不是最外面定义变量 myName 的值——“稀土掘金”,所以在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JS 的作用域机制并不支持这一点,基于这个需求,JS 又搞出来另外一套 this 机制。

所以,在 JS 中可以使用 this 实现在 printName 函数中访问到 bar 对象的 myName 属性。只需要修改代码如下:

printName: function () {
             console.log(this.myName)
           }    

那么打印结果,将变为“juejin.cn”

JavaScript 中的 this

this 是和执行上下文绑定的,每一个执行上下文中都有一个this

JS中只有 this 变量是在运行时动态绑定的,其它所有变量都是编译时静态绑定的。

Screenshot 2024-04-19 at 17.54.29.png

执行上下文主要分为三种:

  • 全局执行上下文;
  • 函数执行上下文;
  • eval执行上下文;

所以对应的 this 也有三种:

  • 全局执行上下文中的 this
  • 函数中的 this
  • eval 中的 this

全局执行上下文中的 this

可以在控制台中输入console.log(this)来打印出来全局执行上下文中的 this,最终输出的是 window 对象。

所以可以得出这样一个结论:全局执行上下文中的 this 是指向 window 对象的。(更严谨的说法是,在非严格模式下,全局上下文的this指向的是window对象,严格模式下指的是undefined)

这也是 this作用域链的唯一交点,作用域链this是两套不同系统,作用域链的最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象。

函数执行上下文中的 this

function foo(){
  console.log(this)
}
foo()

执行这段代码,打印出来的也是window对象,这说明在默认情况下调用一个函数,其执行上下文中的 this 也是指向window对象的。

eval执行上下文中的this

eval()是JS中的一个函数,它接受一个字符串参数,并将该字符串作为JS代码执行。非常强大,同时也存在一些风险,需谨慎使用。

eval()中的 this 指向取决于 eval 被调用的环境。

  • 如果是在全局上下文中调用eval, this 指向window;

    eval(function() { 
        console.log(this); // 在全局上下文中指向 window 
    }());
    
  • 如果在函数上下文中调用eval,this 绑定到该函数的this上下文

    function myFunction() {
      eval(function() {
        console.log(this); // 指向 myFunction 的 this 上下文
      }());
    }
    myFunction();
    
  • 严格模式下,无论eval在哪里调用,this 总是指向undefined

  • 嵌套的eval调用,this 指向与最外层调用的上下文一致

    function outer() {
      eval(function() {
        eval(function() {
          console.log(this); // 指向 outer 函数的 this 上下文
        }.bind(this));
      });
    }
    outer();
    

如果想要使执行上下文中的 this 指向其他对象,可以通过以下方式来设置。

1. 通过函数的call方法设置

通过函数的 call 方法来设置函数执行上下文的 this 指向,比如下面这段代码,并没有直接调用 foo 函数,而是调用了 foocall 方法,并将 bar 对象作为 call 方法的参数。

let bar = {
  myName : "掘金",
  test1 : 1
}
function foo(){
  this.myName = "稀土掘金"
}
foo.call(bar)
console.log(bar)
console.log(myName)

执行这段代码,然后观察输出结果,foo函数内部的this已经指向了 bar 对象,因为通过打印 bar 对象,可以看出 barmyName 属性已经由“掘金”变为“稀土掘金”了,同时在全局执行上下文中打印 myName,JS引擎提示该变量为定义。

除了 call 方法外,还可以使用 bindapply 方法来设置函数执行上下文中的 this

2. 通过对象调用方法设置

var myObj = {
  name : "极客时间", 
  showThis: function(){
    console.log(this)
  }
}
myObj.showThis()

在这段代码中,定义了一个 myObj 对象,该对象是由一个 name 属性和一个 showThis 方法组成的,然后再通过 myObj 对象来调用 showThis 方法。执行这段代码,你可以看到,最终输出的 this 值是指向 myObj 的。

所以,使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的

其实,你也可以认为 JS 引擎在执行myObject.showThis()时,将其转化为了:

myObj.showThis.call(myObj)

接下来稍微改变下调用方式,把 showThis 赋给一个全局对象,然后再调用该对象,代码如下所示:

var myObj = {
  name : "掘金",
  showThis: function(){
    this.name = "稀土掘金"
    console.log(this)
  }
}
var foo = myObj.showThis
foo()

执行这段代码,你会发现 this 又指向了全局 window 对象。

所以通过以上两个例子的对比,可以得出下面这样两个结论:

  • 在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window
  • 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身

3. 通过构造函数中设置

function CreateObj(){
  this.name = "掘金"
}
var myObj = new CreateObj()

在这段代码中,使用 new 创建了对象 myObj。当执行 new CreateObj() 的时候,JS引擎做了如下四件事:

  • 首先创建了一个空对象 tempObj
  • 接着调用 CreateObj.call 方法,将 tempObj 作为call方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象;
  • 然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象;
  • 最后返回 tempObj 对象。

伪代码如下:

  var tempObj = {}
  CreateObj.call(tempObj)
  return tempObj

这样,就通过 new 关键字构建好了一个新对象,并且构造函数中的 this 其实就是新对象本身。 参考

this 的设计缺陷以及应对方案

同变量提升一样,JS中的this也会带来一些不符合直觉的执行结果。

1. 嵌套函数中的 this 不会从外层函数中继承

var myObj = {
  name : "极客时间", 
  showThis: function(){
    console.log(this)
    function bar(){console.log(this)}
    bar()
  }
}
myObj.showThis()

在这段代码的 showThis 方法里面添加了一个 bar 方法,然后接着在 showThis 函数中调用了 bar 函数,那么现在的问题是:bar 函数中的 this 是什么?

如果你刚接触 JS,那么你可能会很自然地觉得,bar 中的 this 应该和其外层 showThis 函数中的 this 是一致的,都是指向 myObj 对象的,这很符合人的直觉。但实际情况却并非如此,执行这段代码后,你会发现函数 bar 中的 this 指向的是全局 window 对象,而函数 showThis 中的 this 指向的是 myObj 对象。这就是 JS 中非常容易让人迷惑的地方之一,也是很多问题的源头。

this 的绑定可通过 newcall/apply/bind对象调用等方式进行,但不会通过作用域链绑定 this,所以,对于独立调用的函数,如果未进行有效的 this 绑定的话,this 就会绑定到 window 对象(非严格模式)或者 undefined(严格模式)

可以通过以下两个小技巧来解决这个问题。

  • 在函数内部声明一个变量self用来保存this
    showThis 函数中声明一个变量 self 用来保存 this,然后在 bar 函数中使用 self.

    var myObj = {
    name : "掘金", 
    showThis: function(){
       console.log(this)
       var self = this
       function bar(){
         self.name = "稀土掘金"
       }
       bar()
     }
    }
    myObj.showThis()
    console.log(myObj.name)
    console.log(window.name)
    

    执行这段代码,可以看到它输出了我们想要的结果,最终 myObj 中的 name 属性值变成了“稀土掘金”。其实,这个方法的的本质是把 this 体系转换为了作用域的体系。

  • 使用ES6中的箭头函数来解决

    var myObj = {
    name : "掘金", 
    showThis: function(){
      console.log(this)
      var bar = ()=>{
        this.name = "稀土掘金"
        console.log(this)
      }
      bar()
     }
    }
    myObj.showThis()
    console.log(myObj.name)
    console.log(window.name)
    

    执行这段代码,你会发现它也输出了我们想要的结果,也就是箭头函数 bar 里面的 this 是指向 myObj 对象的。这是因为 ES6 中的箭头函数并不会创建其自身的执行上下文,本身也就没有 this,所以箭头函数中的 this 默认是继承上一层函数的 this

通过上面的两个例子,可以知道 this 没有作用域的限制,这点和变量不一样,所以嵌套函数不会从调用它的函数中继承 this,这样会造成很多不符合直觉的代码。

2. 普通函数中的 this 默认指向全局对象 window

在默认情况下调用一个函数,其执行上下文中的 this 是默认指向全局对象 window 的。

在实际工作中,我们并不希望函数执行上下文中的 this 默认指向全局对象,因为这样会打破数据的边界,造成一些误操作。如果要让函数执行上下文中的 this 指向某个对象,最好的方式是通过 call 方法来显示调用。

这个问题可以通过设置 JS 的“严格模式”来解决。在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined,这就解决上面的问题了。

小结

为了满足在对象内部的方法中使用对象内部的属性的需求, 引入了 this

根据 this 所在的执行上下文不同, 分为:

  • 全局执行上下文的 this: 指向 window(非严格模式) 或undefined(严格模式);
  • 函数执行上下文的 this; 默认指向 window
  • eval执行上下文的 this

可以通过:

  • 隐式绑定:obj.fn(),this 被绑定到obj上;
  • 显示绑定:call、apply、bind,可以直接指定 this 的绑定对象;
  • new绑定:创建一个全新的对象,并将这个新对象绑定到函数调用的 this

来改变 this 的指向。

注意 this 带来的问题:

嵌套函数中的this不会继承外层函数的this,可以通过

  • 使用self保存this指针;
  • 箭头函数

来解决。