this的五种绑定规则:深入分析`new`绑定、`call`/`apply`/`bind`的显式绑定、隐式绑定和默认绑定。

JavaScript 中 this 的五种绑定规则:深入剖析

大家好,今天我们来深入探讨 JavaScript 中 this 的五种绑定规则。this 是 JavaScript 中一个非常重要的概念,理解它的绑定机制对于编写健壮、可预测的代码至关重要。我们将逐一分析 new 绑定、call/apply/bind 的显式绑定、隐式绑定和默认绑定,并通过大量的代码示例来加深理解。

1. 默认绑定 (Default Binding)

默认绑定是 this 绑定中最基础的一种情况。当函数在非严格模式下独立调用时,this 会默认绑定到全局对象。在浏览器环境中,全局对象通常是 window;在 Node.js 环境中,全局对象是 global

在严格模式下,默认绑定则会将 this 绑定到 undefined,以避免意外地修改全局对象。

代码示例:

非严格模式:

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

var a = 2;

foo(); // 输出: 2 (this 指向 window)

function bar() {
  this.b = 3;
}

bar();
console.log(window.b); // 输出: 3

严格模式:

"use strict";

function foo() {
  console.log(this); // 输出: undefined
}

foo();

function bar() {
  this.b = 3; // 报错:TypeError: Cannot set property 'b' of undefined
}

bar();

解释:

在非严格模式下,foo() 函数独立调用,this 默认绑定到 window,因此可以访问全局变量 abar() 函数也是如此,它实际上是在 window 对象上创建了一个属性 b

在严格模式下,foo() 函数独立调用,this 绑定到 undefined,因此 console.log(this) 输出 undefinedbar() 函数试图在 undefined 上设置属性 b,导致 TypeError 错误。

总结:

  • 默认绑定:函数独立调用时,this 默认绑定到全局对象(非严格模式)或 undefined(严格模式)。
  • 严格模式下避免意外修改全局对象,因此将 this 设置为 undefined

2. 隐式绑定 (Implicit Binding)

隐式绑定发生在函数作为对象的方法被调用时。在这种情况下,this 会绑定到调用该方法的对象。

代码示例:

var obj = {
  a: 2,
  foo: function() {
    console.log(this.a);
  }
};

obj.foo(); // 输出: 2 (this 指向 obj)

var obj2 = {
  a: 3,
  bar: function() {
    function baz() {
      console.log(this.a);
    }
    baz(); // 默认绑定
  }
};

obj2.bar(); // 输出: undefined (严格模式下会报错)

解释:

在第一个例子中,foo() 函数作为 obj 对象的方法被调用,因此 this 绑定到 obj,可以访问 obj.a

在第二个例子中,bar() 函数也是 obj2 的方法,但 baz() 函数是在 bar() 内部独立调用的。因此,baz() 函数的 this 使用的是默认绑定,在非严格模式下指向 window,而 window 上没有 a 属性,所以输出 undefined。如果在严格模式下,baz() 中的 this 会是 undefined,访问 this.a 会抛出错误。

隐式丢失 (Implicit Loss):

隐式绑定有一个常见的问题,就是隐式丢失。当我们将对象的方法赋值给另一个变量,或者将方法作为回调函数传递时,可能会导致 this 绑定丢失,回到默认绑定。

代码示例:

var obj = {
  a: 2,
  foo: function() {
    console.log(this.a);
  }
};

var bar = obj.foo; // 将 foo 函数赋值给 bar
bar(); // 输出: undefined (非严格模式,this 指向 window)

setTimeout(obj.foo, 100); // 输出: undefined (非严格模式,this 指向 window)

解释:

在第一个例子中,bar 只是 obj.foo 函数的一个引用,它并没有和 obj 对象绑定。因此,当 bar() 被独立调用时,this 使用的是默认绑定,指向 window

setTimeout 的例子中,setTimeout 接收一个函数作为参数,并在稍后执行。当 obj.foo 作为回调函数传递给 setTimeout 时,this 绑定也丢失了,setTimeout 内部会使用默认绑定。

解决隐式丢失的方法:

可以使用 bind 或者箭头函数来解决隐式丢失的问题。

  • 使用 bind
var obj = {
  a: 2,
  foo: function() {
    console.log(this.a);
  }
};

var bar = obj.foo.bind(obj);
bar(); // 输出: 2 (this 指向 obj)

setTimeout(obj.foo.bind(obj), 100); // 输出: 2 (this 指向 obj)
  • 使用箭头函数:
var obj = {
  a: 2,
  foo: function() {
    setTimeout(() => {
      console.log(this.a);
    }, 100);
  }
};

obj.foo(); // 输出: 2 (this 指向 obj)

总结:

  • 隐式绑定:函数作为对象的方法被调用时,this 绑定到调用该方法的对象。
  • 隐式丢失:将对象的方法赋值给另一个变量,或者将方法作为回调函数传递时,可能导致 this 绑定丢失,回到默认绑定。
  • 使用 bind 或箭头函数可以解决隐式丢失的问题。箭头函数没有自己的 this,它会捕获其所在上下文的 this 值。

3. 显式绑定 (Explicit Binding)

显式绑定允许我们明确地指定函数的 this 值。JavaScript 提供了 callapplybind 这三个方法来实现显式绑定。

  • call(thisArg, arg1, arg2, ...): 使用指定的 this 值和参数列表调用函数。
  • apply(thisArg, [arg1, arg2, ...]): 使用指定的 this 值和一个包含参数的数组来调用函数。
  • bind(thisArg, arg1, arg2, ...): 创建一个新的函数,当调用该函数时,它的 this 值会被设置为 thisArg,并且可以预先传入一些参数。bind 返回的是一个新函数,而不是直接调用函数。

代码示例:

function foo(a, b) {
  console.log(this.a + a + b);
}

var obj = {
  a: 2
};

foo.call(obj, 1, 2); // 输出: 5 (this 指向 obj)
foo.apply(obj, [1, 2]); // 输出: 5 (this 指向 obj)

var bar = foo.bind(obj, 1);
bar(2); // 输出: 5 (this 指向 obj)

解释:

callapply 的区别在于参数的传递方式。call 接收参数列表,而 apply 接收一个包含参数的数组。

bind 方法会创建一个新的函数,该函数的 this 值被永久绑定到 bind 的第一个参数。bind 还可以预先传入一些参数,这些参数会在调用新函数时自动传入。

bind 的应用:

bind 的一个常见应用是在事件处理程序中,确保 this 指向正确的对象。

function Button(content) {
  this.content = content;
  this.showContent = function() {
    console.log(this.content);
  }
}

var myButton = new Button("Awesome Button!");
var btn = document.createElement('button');
btn.innerHTML = myButton.content;
btn.addEventListener('click', myButton.showContent.bind(myButton), false); // 使用 bind 绑定 this
document.body.appendChild(btn);

总结:

  • 显式绑定:使用 callapplybind 明确地指定函数的 this 值。
  • callapply 的区别在于参数的传递方式。
  • bind 创建一个新的函数,并将 this 值永久绑定到该函数。
  • bind 常用于事件处理程序中,确保 this 指向正确的对象。

4. new 绑定 (New Binding)

new 绑定发生在通过 new 关键字调用函数时。当使用 new 关键字调用函数时,会执行以下步骤:

  1. 创建一个新的空对象。
  2. 将新对象的 __proto__ 属性指向构造函数的 prototype 属性,从而建立原型链。
  3. this 绑定到这个新对象。
  4. 执行构造函数中的代码。
  5. 如果构造函数没有显式返回一个对象,则返回新创建的对象;否则,返回构造函数显式返回的对象。

代码示例:

function Foo(a) {
  this.a = a;
  console.log(this);
}

var bar = new Foo(2); // 输出: Foo {a: 2} (this 指向新创建的对象)
console.log(bar.a); // 输出: 2

解释:

当使用 new Foo(2) 调用 Foo 函数时,this 绑定到新创建的对象。因此,this.a = aa 的值赋给新对象的 a 属性。

显式返回对象的情况:

function Foo(a) {
  this.a = a;
  return { b: 3 }; // 显式返回一个对象
}

var bar = new Foo(2);
console.log(bar.a); // 输出: undefined
console.log(bar.b); // 输出: 3

解释:

当构造函数显式返回一个对象时,new 关键字会忽略构造函数内部的 this 绑定,直接返回构造函数返回的对象。因此,bar 实际上指向的是 { b: 3 },而不是通过 new 创建的对象。

总结:

  • new 绑定:使用 new 关键字调用函数时,this 绑定到新创建的对象。
  • 如果构造函数没有显式返回一个对象,则返回新创建的对象;否则,返回构造函数显式返回的对象。

5. 优先级规则

当多种绑定规则同时存在时,this 的绑定会遵循一定的优先级。优先级从高到低依次为:

  1. new 绑定
  2. 显式绑定 (call/apply/bind)
  3. 隐式绑定
  4. 默认绑定

代码示例:

function foo(a) {
  this.a = a;
}

var obj1 = {
  a: 2,
  foo: foo
};

var obj2 = {
  a: 3
};

obj1.foo(5); // 隐式绑定
console.log(obj1.a); // 输出: 5

obj1.foo.call(obj2, 10); // 显式绑定
console.log(obj2.a); // 输出: 10

var bar = new obj1.foo(15); // new 绑定
console.log(obj1.a); // 5
console.log(bar.a); // 输出: 15

解释:

  • obj1.foo(5) 使用隐式绑定,this 指向 obj1,因此 obj1.a 被修改为 5。
  • obj1.foo.call(obj2, 10) 使用显式绑定,this 指向 obj2,因此 obj2.a 被修改为 10。
  • new obj1.foo(15) 使用 new 绑定,this 指向新创建的对象,因此新对象的 a 属性被设置为 15。obj1.a 仍然是 5,因为 new 绑定创建了一个新的对象。

总结:

  • 优先级规则:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定。
  • 当多种绑定规则同时存在时,优先级最高的规则会生效。

总结:掌握 this 绑定,编写可预测的代码

通过今天的学习,我们深入了解了 JavaScript 中 this 的五种绑定规则:默认绑定、隐式绑定、显式绑定和 new 绑定。理解这些规则对于编写健壮、可预测的代码至关重要。记住,this 的绑定取决于函数的调用方式,而不是函数的定义位置。理解并掌握这些绑定规则,可以让你写出更加清晰、易于维护的 JavaScript 代码。

发表回复

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