揭秘 JavaScript 中的 this:谁在调用我?

136 阅读6分钟

深入理解 this:函数运行环境指针

在JavaScript中,this 是一个非常重要的关键字,它代表了函数执行时的上下文对象。理解 this 的行为对于编写健壮的JavaScript代码至关重要。本文将结合内存模型(栈内存、堆内存)、调用栈、执行上下文、作用域和作用域链等概念,深入探讨 this 的不同表现形式及其背后的原理。

首先来了解一下this 和 outer 的联系

虽然 thisouter 是两个独立的概念,但它们共同影响了函数的行为:

  • this 的绑定:决定了函数执行时的上下文对象,即函数内部的 this 指向哪个对象。
  • outer 的作用域链:确保了函数可以访问其定义时所在的上下文中的变量和声明,即使该函数是在不同的上下文中被调用的。

相关JavaScript执行上下文讲解:JavaScript执行上下文理解

内存模型与执行上下文

JavaScript 中的代码执行是基于执行上下文的。每当一段代码被执行时,都会创建一个新的执行上下文。这个上下文包含了这段代码运行所需的一切信息,包括变量环境(Variable Environment)和词法环境(Lexical Environment),它们共同决定了哪些变量和函数可以在当前代码块中被访问。

  • 栈内存: 用于存储基本数据类型(如数字、布尔值等)和对象的引用,以及对象地址、函数地址等。
  • 堆内存: 则用来存放对象本身。

当函数被调用时,会创建一个新的执行上下文并将其压入调用栈。执行上下文包含了一个指向外部环境的链接,即作用域链(scope chain),这使得内部函数可以访问外部函数的变量。

this 的几种常见形式

根据函数的调用方式不同,this 的绑定规则也会有所变化。以下是几种常见的 this 表现形式:

  1. 对象方法调用

    • 当函数作为对象的方法被调用时,this 指向调用该方法的对象。例如:

      var x = 2;
      var obj = {
        x:1,
        foo: function() {
          console.log(this.x); // 输出 obj 对象
        }
      };
      obj.foo(); // 1
      // 函数被调用,this指向调用者 obj ,所以这里打印1
      

注意:在 JavaScript 中,this 的指向是在函数被调用的那一刹那确定的,而不是在函数定义时。这意味着 this 的值取决于函数是如何被调用的,而不是它在哪里或如何被定义的。

  1. 普通函数调用

    • 在非严格模式下,如果函数不是作为对象的方法被调用,那么 this 将指向全局对象,在浏览器中是 window,而在 Node.js 环境中则是 global。在严格模式下,this 将是 undefined
  • 注意:
    • 在 Node.js 环境下,如果你发现 foo() 输出 undefined,即使没有显式添加严格模式('use strict'),这可能是因为你正在使用的是较新的 Node.js 版本。从 Node.js 12.x 开始,默认情况下,所有模块都运行在严格模式下,无论是否显示声明了 'use strict';。这意味着即使你没有在代码中添加严格模式声明,你的代码也会以严格模式执行。

示例:

 var x = 2;
 var obj = { 
     x: 1, 
     foo: function() { 
         console.log(this.x); 
     } 
 }; 
 var foo = obj.foo; // 将 obj 对象中的 foo 方法赋值给 foo 变量 
 obj.foo(); // 调用 obj 对象中的 foo 方法 
 foo(); // 直接调用 foo 变量所引用的函数 作为普通函数

打印结果:

image.png

  • 函数引用var foo = obj.foo; 创建了一个新的变量 foo,它保存了对 obj.foo 函数的引用。这意味着 fooobj.foo 实际上指向同一个函数。
  • 调用上下文:关键区别在于调用的方式。obj.foo() 是作为对象的方法被调用,因此 this 指向 obj。而 foo() 是作为一个普通的函数被调用,this 指向全局对象或在严格模式下是 undefined

严格模式:

image.png

  1. 构造函数调用

    • 使用 new 关键字调用函数时,this 指向新创建的实例对象。例如:

      function Obj() {
        this.instanceProperty = 'I am an instance';
      }
      const instance = new Obj();
      console.log(instance.instanceProperty); // 输出 "I am an instance"
      
  • 注意:
    • 在这个例子中,this 指向的是由 new Obj() 创建的新对象 instance。构造函数中的任何对 this 的引用实际上都是在操作这个新对象,允许你在新对象上定义属性和方法。通过这种方式,你可以创建多个具有相同结构但不同数据的对象实例。
  1. 指定 this 的调用方式

    • callapply, 和 bind 方法,允许你显式地指定函数执行时的 this 值。call 和 apply 立即执行函数,并接受参数列表或参数数组;而 bind 返回一个新的函数,其 this 值被永久绑定到指定的对象。

示例:

var name = "刀郎"
var a = {
    name:'薛之谦',
    func1:function(){
        console.log(this.name);
    },
    func2:function(){
        // setTimeout是一个异步函数(普通函数),普通函数中this指向的是window对象。
        setTimeout(function(){
            this.func1(); 
        }.bind(a),1000)// 通过bind()方法改变this指向,将this指向a对象。
    }
}
a.func2();  
  1. 箭头函数

    • 箭头函数不拥有自己的 this,而是继承自外层函数的 this 值。这意味着箭头函数中的 this 是在其定义时确定的,而不是在其调用时确定的。例如:

          // 定义一个全局变量 x
           var x = 2;
      
          // 定义一个对象 obj 来模拟 outerFunction 作为一个方法被调用
          const obj = {
              x: 1,
              outerFunction: function() {
                  // 使用 this.x 确保 outerFunction 的 this 指向 obj
                  console.log('Outer function this.x:', this.x);
      
                  // 定义一个箭头函数 innerArrow
                  const innerArrow = () => {
                      // 箭头函数内的 this 继承自 outerFunction 的 this
                      console.log('Inner arrow this.x:', this.x);
                  };
      
                  // 调用箭头函数
                  innerArrow();
              }
          };
      
          // 以对象的方法形式调用 outerFunction
          obj.outerFunction();
      
          // 如果直接调用 outerFunction 作为普通函数,则 this 指向全局对象
          const standaloneFunction = obj.outerFunction;
          standaloneFunction(); // 注意:这里的 this.x 可能不是预期的结果
      

结果如下:

image.png

结合实际案例

考虑以下代码片段:

var name = "刀郎";
var a = {
    name: '薛之谦',
    func1: function() {
        console.log(this.name);
    },
    func2: function() {
        setTimeout(() => {
            this.func1(); // 箭头函数保持外层 this,这里指的是 a 对象
        }, 1000);
    }
};
a.func2();

结果如下: image.png 在这个例子中,我们使用了箭头函数来确保 setTimeout 回调中的 this 正确地指向 a 对象,从而避免了直接调用 .call(a) 所带来的问题。这样做不仅简化了代码,还保证了 this 的一致性。

总结

this 的行为取决于函数的调用方式,它可以是指向全局对象、特定对象、实例对象,或者是由 callapplybind 显式指定的对象。理解 this 的工作原理以及如何通过不同的调用方式来控制它的值,可以帮助开发者写出更加清晰和可靠的代码。此外,箭头函数的引入为处理 this 提供了一种新的思路,特别是在涉及异步操作和回调函数的情况下。希望通过对 this 的深入了解,能帮助你在开发过程中更好地利用这一特性,提升代码的质量。