JS `this` 绑定机制的五种情况:深入理解与避免陷阱

嘿,各位代码界的英雄们,晚上好!我是你们的老朋友,今天咱们来聊聊 JavaScript 中那个让人又爱又恨的 this。这玩意儿就像个变色龙,一会儿指东,一会儿指西,搞得新手晕头转向。别怕,今天我就要扒光它的底裤,让它在咱们面前无所遁形!

咱们的目标是:用最接地气的方式,把 this 的五种绑定机制讲明白,让你以后再也不怕它出来捣乱。

this:一个谜一样的男人(或女人)

首先,我们要明白,this 永远指向一个对象。问题是,它指向哪个对象?这就取决于函数是怎么被调用的。this 的指向是在运行时确定的,而不是定义时。

可以把 this 看作是函数的“上下文”,它代表了函数执行时所处的环境。

五大绑定机制:this 的变形术

下面,咱们就来逐一揭秘 this 的五种绑定机制,并配上生动的例子,保证让你一听就懂,一学就会。

  1. 默认绑定 (Default Binding)

    这是最简单的一种情况,也是最容易被忽略的。当函数独立调用时,this 会指向全局对象。在浏览器中,全局对象就是 window,而在 Node.js 中,全局对象是 global

    function whoIsThis() {
     console.log("This is:", this);
    }
    
    whoIsThis(); // 在浏览器中,输出:This is: Window { ... }
    // 在 Node.js 中,输出:This is: Object [global] { ... }
    
    var myVar = "我是全局变量";
    function checkGlobal() {
     console.log(this.myVar);
    }
    
    checkGlobal(); // 输出:我是全局变量

    注意: 在严格模式 ("use strict";) 下,默认绑定会将 this 绑定到 undefined,而不是全局对象。这是一种安全机制,可以防止意外地修改全局变量。

    "use strict";
    function whoIsThisStrict() {
     console.log("This is:", this);
    }
    
    whoIsThisStrict(); // 输出:This is: undefined

    总结: 默认绑定就像一个懒汉,啥也不管,直接把 this 指向全局对象(或者 undefined,如果开启了严格模式)。

    绑定类型 适用场景 this 指向 是否受严格模式影响
    默认绑定 函数独立调用,没有明确的上下文。 全局对象 (浏览器: window, Node.js: global) 是 (指向 undefined)
  2. 隐式绑定 (Implicit Binding)

    当函数作为某个对象的方法被调用时,this 会指向该对象。换句话说,谁调用了函数,this 就指向谁。

    var myObject = {
     name: "张三",
     sayHello: function() {
       console.log("你好,我是" + this.name);
     }
    };
    
    myObject.sayHello(); // 输出:你好,我是张三

    在这个例子中,sayHello 函数是 myObject 对象的方法,因此 this 指向 myObject

    隐式绑定的一个常见陷阱:丢失绑定

    var anotherObject = {
     name: "李四",
     myMethod: myObject.sayHello // 将 myObject 的 sayHello 方法赋值给 anotherObject 的 myMethod
    };
    
    anotherObject.myMethod(); // 输出:你好,我是 undefined (或者全局对象上的 name 属性,如果存在)

    为什么会这样?因为 anotherObject.myMethod() 实际上是独立调用了 myMethod 函数,而不是作为 myObject 的方法调用。因此,this 发生了丢失,指向了全局对象(或者 undefined,如果开启了严格模式)。

    解决方法:使用 bindcallapply 强制绑定 this 我们稍后会讲到这些方法。

    更隐蔽的丢失绑定:回调函数

    var button = {
     text: "点击我",
     click: function() {
       console.log("按钮文本是:" + this.text); //期望输出: 按钮文本是:点击我
     },
     addEventListener: function(type, callback) {
       // 模拟 addEventListener
       callback(); // 实际执行时,this 指向 window (或 undefined,如果开启了严格模式)
     }
    };
    
    button.addEventListener("click", button.click); //输出:按钮文本是:undefined (或者全局对象上的 text 属性,如果存在)

    在这个例子中,addEventListener 只是简单地调用了 callback 函数,并没有指定 this 的指向。因此,callback 函数中的 this 仍然会指向全局对象(或者 undefined,如果开启了严格模式)。

    解决方法:

    • 使用箭头函数:箭头函数没有自己的 this,它会继承外层作用域的 this

      var button = {
       text: "点击我",
       click: () => { // 使用箭头函数
         console.log("按钮文本是:" + this.text);
       },
       addEventListener: function(type, callback) {
         callback();
       }
      };
      
      button.addEventListener("click", button.click); //输出:按钮文本是:点击我
    • 使用 bindcallapply 强制绑定 this

    总结: 隐式绑定就像一个跟屁虫,函数在哪儿被调用,它就指向哪儿。但是要小心丢失绑定,尤其是在回调函数中。

    绑定类型 适用场景 this 指向 是否受严格模式影响
    隐式绑定 函数作为对象的方法被调用。 调用函数的对象。
    丢失绑定 隐式绑定的 this 指向丢失,变为默认绑定。 全局对象 (浏览器: window, Node.js: global) 或 undefined 是 (指向 undefined)
  3. 显式绑定 (Explicit Binding)

    有时候,我们想要手动指定 this 的指向,这时候就可以使用显式绑定。JavaScript 提供了三个方法来实现显式绑定:callapplybind

    • call

      call 方法可以调用一个函数,并指定该函数内部 this 的指向,同时可以传递参数给该函数。

      function greet(greeting) {
       console.log(greeting + ",我是" + this.name);
      }
      
      var person = {
       name: "王五"
      };
      
      greet.call(person, "你好"); // 输出:你好,我是王五

      call 的第一个参数是要绑定的 this 对象,后面的参数是传递给函数的参数列表。

    • apply

      apply 方法和 call 方法类似,也可以调用一个函数,并指定该函数内部 this 的指向,同时可以传递参数给该函数。不同的是,apply 接受一个包含参数的数组作为第二个参数。

      function greet(greeting, age) {
       console.log(greeting + ",我是" + this.name + ",今年" + age + "岁");
      }
      
      var person = {
       name: "王五"
      };
      
      greet.apply(person, ["你好", 30]); // 输出:你好,我是王五,今年30岁

      apply 的第一个参数是要绑定的 this 对象,第二个参数是一个包含参数的数组。

    • bind

      bind 方法会创建一个新的函数,并将该函数内部 this 永久绑定到指定的对象。bind 方法不会立即执行函数,而是返回一个新的函数。

      function greet(greeting) {
       console.log(greeting + ",我是" + this.name);
      }
      
      var person = {
       name: "王五"
      };
      
      var greetPerson = greet.bind(person); // 创建一个新的函数,并将 this 绑定到 person 对象
      greetPerson("你好"); // 输出:你好,我是王五

      bind 的第一个参数是要绑定的 this 对象,后面的参数是传递给函数的预设参数。

      bind 的一个重要特性:柯里化

      bind 可以用来实现柯里化,也就是将一个接受多个参数的函数转换成一系列接受单个参数的函数。

      function multiply(a, b) {
       return a * b;
      }
      
      var multiplyByTwo = multiply.bind(null, 2); // 创建一个新的函数,预设第一个参数为 2
      console.log(multiplyByTwo(5)); // 输出:10

      在这个例子中,multiply.bind(null, 2) 创建了一个新的函数 multiplyByTwo,它接受一个参数 b,并将 a 固定为 2。

    总结: 显式绑定就像一个遥控器,可以精确控制 this 的指向。callapply 立即执行函数,而 bind 返回一个新的函数,方便以后调用。

    绑定类型 适用场景 this 指向 是否受严格模式影响
    显式绑定 需要手动指定 this 指向。 callapplybind 指定的对象。
  4. new 绑定 (New Binding)

    当使用 new 关键字调用一个函数时,会发生以下几件事情:

    1. 创建一个新的空对象。
    2. 将新对象的 __proto__ 属性指向构造函数的 prototype 属性。
    3. this 绑定到新对象。
    4. 执行构造函数中的代码。
    5. 如果构造函数没有显式返回一个对象,则返回新对象。
    function Person(name) {
     this.name = name;
     console.log("我被创建了,我的名字是:" + this.name);
    }
    
    var person1 = new Person("赵六"); // 输出:我被创建了,我的名字是:赵六
    
    console.log(person1.name); // 输出:赵六

    在这个例子中,new Person("赵六") 创建了一个新的对象 person1,并将 this 绑定到 person1。因此,this.name = nameperson1name 属性设置为 "赵六"。

    new 绑定的优先级高于隐式绑定。

    var obj = {
     name: "钱七",
     myMethod: function() {
       console.log("我的名字是:" + this.name);
     }
    };
    
    var Person = function(name) {
     this.name = name;
     this.myMethod = obj.myMethod; // 将 obj 的 myMethod 赋值给 Person 的实例
    };
    
    var person1 = new Person("孙八");
    person1.myMethod(); // 输出:我的名字是:孙八 (而不是钱七)

    在这个例子中,person1.myMethod() 实际上是调用了 obj.myMethod 函数,但是由于 person1 是通过 new 关键字创建的,因此 this 仍然指向 person1,而不是 obj

    总结: new 绑定就像一个造物主,它创建一个新的对象,并将 this 绑定到这个新对象。

    绑定类型 适用场景 this 指向 是否受严格模式影响
    new 绑定 使用 new 关键字调用函数(构造函数)。 新创建的对象。
  5. 箭头函数 (Arrow Functions)

    箭头函数是 ES6 引入的一种新的函数语法。箭头函数的一个重要特性是,它没有自己的 this。箭头函数会继承外层作用域的 this,也就是定义时所处的上下文。

    var myObject = {
     name: "周九",
     sayHello: () => {
       console.log("你好,我是" + this.name); // this 指向外层作用域的 this (通常是 window 或 global)
     }
    };
    
    myObject.sayHello(); // 输出:你好,我是 undefined (或者全局对象上的 name 属性,如果存在)

    在这个例子中,sayHello 是一个箭头函数,它没有自己的 this,因此 this 指向外层作用域的 this,也就是全局对象(或者 undefined,如果开启了严格模式)。

    箭头函数非常适合用于回调函数,可以避免 this 丢失的问题。

    var button = {
     text: "点击我",
     click: function() {
       setTimeout(() => {
         console.log("按钮文本是:" + this.text); // this 指向 button 对象
       }, 1000);
     }
    };
    
    button.click(); // 一秒后输出:按钮文本是:点击我

    在这个例子中,setTimeout 接受一个箭头函数作为回调函数。由于箭头函数没有自己的 this,它会继承 click 函数的 this,也就是 button 对象。

    箭头函数不能用作构造函数,也不能使用 new 关键字调用。

    总结: 箭头函数就像一个寄生虫,它没有自己的 this,而是依附于外层作用域的 this

    绑定类型 适用场景 this 指向 是否受严格模式影响
    箭头函数 简化函数语法,避免 this 丢失。 外层作用域的 this

优先级:谁说了算?

当多种绑定机制同时存在时,this 的指向由优先级决定。优先级从高到低依次是:

  1. new 绑定
  2. 显式绑定 (callapplybind)
  3. 隐式绑定
  4. 默认绑定

一个复杂的例子:

var obj = {
  name: "对象A",
  method: function() {
    console.log("method - this.name:", this.name); // (2)
  }
};

function globalFunction() {
  this.name = "全局函数";
  console.log("globalFunction - this.name:", this.name); // (1)
}

var newObj = new globalFunction(); // 使用 new 绑定

newObj.method = obj.method; // 隐式绑定

newObj.method(); // (3)

obj.method.call(window); // (4)

输出:

globalFunction - this.name: 全局函数   // (1) newObj 调用构造函数,this 指向 newObj
method - this.name: 对象A              // (3) 隐式绑定,this 指向 newObj (因为方法是对象A 的 method 赋值给 newObj 的)
method - this.name: undefined         // (4) 显示绑定, call 将 obj.method 的this 指向 window,window 没有 name 属性

避免 this 的陷阱:一些建议

  • 始终明确 this 的指向: 在编写代码时,要时刻思考 this 指向哪个对象。
  • 避免全局变量污染: 尽量不要在全局作用域中定义变量,以免造成命名冲突。
  • 使用箭头函数: 在回调函数中,尽量使用箭头函数,可以避免 this 丢失的问题。
  • 善用 bindcallapply 使用这些方法可以手动指定 this 的指向,让代码更加灵活。
  • 编写单元测试: 编写单元测试可以帮助你验证 this 的指向是否正确。

总结:this,不再神秘!

通过今天的讲解,相信大家对 JavaScript 中 this 的五种绑定机制有了更深入的理解。this 就像一个谜题,需要我们认真分析函数是如何被调用的,才能找到正确的答案。只要掌握了这些规则,this 就不再是你的敌人,而是你的朋友!

希望今天的分享对大家有所帮助。下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注