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,因此可以访问全局变量 a。bar() 函数也是如此,它实际上是在 window 对象上创建了一个属性 b。
在严格模式下,foo() 函数独立调用,this 绑定到 undefined,因此 console.log(this) 输出 undefined。bar() 函数试图在 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 提供了 call、apply 和 bind 这三个方法来实现显式绑定。
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)
解释:
call 和 apply 的区别在于参数的传递方式。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);
总结:
- 显式绑定:使用
call、apply和bind明确地指定函数的this值。 call和apply的区别在于参数的传递方式。bind创建一个新的函数,并将this值永久绑定到该函数。bind常用于事件处理程序中,确保this指向正确的对象。
4. new 绑定 (New Binding)
new 绑定发生在通过 new 关键字调用函数时。当使用 new 关键字调用函数时,会执行以下步骤:
- 创建一个新的空对象。
- 将新对象的
__proto__属性指向构造函数的prototype属性,从而建立原型链。 - 将
this绑定到这个新对象。 - 执行构造函数中的代码。
- 如果构造函数没有显式返回一个对象,则返回新创建的对象;否则,返回构造函数显式返回的对象。
代码示例:
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 = a 将 a 的值赋给新对象的 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 的绑定会遵循一定的优先级。优先级从高到低依次为:
new绑定- 显式绑定 (
call/apply/bind) - 隐式绑定
- 默认绑定
代码示例:
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 代码。