前言
this关键字是js中最复杂的机制之一,它的指向问题一直都是js中的一大难点。本文将详细介绍它的各种用法及判断它的具体指向。
this到底是什么
对于那些没有时间学习this机制的js人员来说,this的绑定一直是一件非常令人头疼的事,但this是非常重要的,盲目的猜测试错并不能让我们真正了解this 的机制。学习this的第一步是明白它既不指向函数自身也不指向函数的词法作用域,它实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。有些朋友可能还是听的云里雾里,不知道我在讲什么,没关系让我们一起往下看。
为什么要用this
讲它的指向前,我们先来想一下既然它这么麻烦为什么还要使用它呢,就不能不用了吗?显然是不能的,,this 关键字在 js 中扮演着重要的角色,它提供了灵活且强大的机制来处理函数调用时的上下文,下面是为什么需要使用this的几个关键原因:
1. 对象方法的自我引用
this 提供了一种机制,使得方法内部可以方便地访问和操作其所属对象的数据成员和其他方法。通过 this,我们可以确保代码中对属性或方法的引用是针对当前对象的,而不是其他任何对象。
var obj = {
name: "Object",
greet: function() {
console.log("Hello, I'm " + this.name);
}
};
obj.greet(); // Hello, I'm Object
在这个例子中,this.name 确保了我们总是引用的是 obj 对象的 name 属性,而不会意外地引用到其他地方的同名属性。
2. 动态上下文
js 是一种非常灵活的语言,它的函数可以在不同的上下文中被调用。而this就能允许函数根据它们如何被调用来适应不同的环境。这使得代码更加通用,能够处理多种情况而不必为每种情况编写单独的函数。
例如,同一个函数可以作为多个不同对象的方法来调用,每次调用时 this 都会自动绑定到调用它的那个对象。
function sayHi() {
console.log("Hi, I'm " + this.name);
}
var person1 = { name: "Alice", speak: sayHi };
var person2 = { name: "Bob", speak: sayHi };
person1.speak(); // Hi, I'm Alice
person2.speak(); // Hi, I'm Bob
3. 构造函数模式
当使用 new 关键字调用函数时,this 被绑定到新创建的对象实例。这种模式允许开发者定义构造函数,从而可以创建具有相同结构但不同数据的多个对象。
function Person(name) {
this.name = name;
}
var alice = new Person("Alice");
var bob = new Person("Bob");
console.log(alice.name); // Alice
console.log(bob.name); // Bob
4. 显式控制上下文
js 提供了 call, apply, 和 bind 方法,让开发者可以显式地控制函数执行时的 this 值。这些方法允许我们临时或者永久地改变函数执行时this的绑定对象,这增加了代码的灵活性和可复用性,使我们可以将同一个函数逻辑应用于不同的对象或数据集。
function introduce(greeting) {
console.log(greeting + ", my name is " + this.name);
}
var person = { name: "Charlie" };
introduce.call(person, "Hello"); // Hello, my name is Charlie
5. 避免全局污染
通过正确使用 this,可以减少直接操作全局对象的可能性,从而降低代码之间的耦合度,并提高代码的安全性和维护性。特别是在严格模式下,未绑定的 this 将是 undefined,而非指向全局对象,这有助于防止意外地向全局作用域添加属性或修改全局状态。
this的四种调用方式
在 js 中,this 的值是不固定的,它在函数执行的一刹那被调用方式决定。以下是四种主要的 this 调用形式及其行为:
1. 简单调用(直接调用)
当一个函数被直接调用时,即不作为对象的方法、构造函数或通过特定方法改变其 this 绑定的情况下,那么这个函数的 this 将指向全局对象(在浏览器中是 window 对象,如果是后端node的话则是global)。不过,在严格模式下,this 会是 undefined。
function simpleFunction() {
console.log(this);
}
simpleFunction(); // 非严格模式下: window, 严格模式下: undefined
2. 作为对象方法调用
当函数作为对象的方法被调用时,this 指向的是该对象本身。这是最直观的情况,因为在这种情况下,this 表示调用该方法的对象。
const obj = {
value: 'Hello',
method: function() {
console.log(this.value);
}
};
obj.method(); // 输出: Hello
3. 构造函数调用
当使用 new 关键字调用函数时,该函数被视为构造函数,它会创建一个新的对象,并将 this 绑定到这个新实例上。构造函数内部的代码会初始化新对象的属性和方法,最后返回这个新对象。
function Constructor(name) {
this.name = name;
}
const instance = new Constructor('World');
console.log(instance.name); // 输出: World
4. 指定this调用方式
在js里,我们可以通过 call, apply, 和 bind 方法来显式地设置函数执行时的 this 值。
call 和 apply:这两个方法立即调用函数,区别在于它们传递参数的方式不同。call 接受一系列以逗号分隔的参数,而 apply 接受一个参数数组。
const obj1 = { value: 'A' };
const obj2 = { value: 'B', method: function() { console.log(this.value); } };
obj2.method.call(obj1); // 输出: A
obj2.method.apply(obj1); // 输出: A
bind:与 call 和 apply 不同,bind 创建并返回一个新函数,这个新函数的 this 被永久绑定到传入的第一个参数。这不会立即执行函数,而是创建一个带有预设 this 的新函数。
const obj1 = { value: 'A' };
const obj2 = { value: 'B', method: function() { console.log(this.value); } };
const boundMethod = obj2.method.bind(obj1);
boundMethod(); // 输出: A
了解上面这四种调用形式对于我们正确理解和预测 this 在不同上下文中的行为非常重要。由于箭头函数没有自己的 this 绑定,它们遵循包含它们的词法作用域的 this 规则,因此不在上述四种形式之列。
有些人可能疑惑这个看着怎么和上面为什么要用this的有几点那么像呢,不会是在水字数吧,其实不是,这两种描述并不冲突,而是从不同的角度来解释 this 的作用和行为
this 的四种绑定规则
讲完上面那些,让我们来看看在函数的执行过程中调用位置如何决定this的绑定对象,这里会有四条规则,我们只需要找到调用位置,然后判断需要应用这四条规则中的哪一条就行了。
为了更详细地解释 this 的四种绑定规则,我们将深入探讨每个规则,并结合《你不知道的 JavaScript(上卷)》中的代码示例。这些例子不仅展示了每种绑定规则的工作原理,还帮助理解在不同上下文中 this 的行为。
默认绑定
默认绑定是最简单的情况,发生在函数作为独立函数被调用时。在这种情况下,this 指向全局对象(非严格模式下),或 undefined(严格模式下)。下面是一个展示默认绑定的例子:
function foo() {
"use strict"; // 启用严格模式
console.log(this.a);
}
var a = 2;
foo(); // undefined, 因为是在严格模式下调用的,所以 this 是 undefined
这段代码中,foo 函数直接被调用,因此它遵循默认绑定规则。由于启用了严格模式,this 被设置为 undefined,而在非严格模式下,this 会指向全局对象 window(如果是node,则会指向全局对象global),并输出 2。
隐式绑定
隐式绑定是指当函数作为对象的方法被调用时,this 自动绑定到该对象。如果方法从对象中取出后被调用,这种隐式的绑定就会丢失(又叫作隐式丢失),this 将遵循默认绑定规则。
function foo() {
console.log(this.a);
}
var obj1 = {
a: 2,
foo: foo
};
obj1.foo(); // 2, 因为 foo 是作为 obj1 的方法调用的
// 如果方法被复制给另一个变量或者从对象中取出后调用
var bar = obj1.foo;
bar(); // undefined 或 window.a (取决于是否启用严格模式),因为此时 foo 不再是 obj1 的方法
在这个例子中,foo 作为 obj1 的方法被调用时,this 指向 obj1。然而,当 foo 被赋值给 bar 并单独调用时,this 的隐式绑定就失效了,转而遵循默认绑定规则。
来看一个复杂点的隐式丢失问题的例子:
var handler = {
message: "Hello!",
greet: function(event) {
console.log(this.message);
}
};
setTimeout(handler.greet, 1000); // undefined, 因为 setTimeout 内部以普通函数方式调用了 greet,导致 this 失效
在这个例子中,handler.greet 被作为回调传递给 setTimeout。当定时器触发并执行 greet 时,它是作为一个普通的函数调用的,而不是作为 handler 的方法。因此,this 的隐式绑定丢失,导致输出 undefined。为了避免这种情况,可以使用箭头函数、bind 方法或其他方式来确保 this 正确地绑定到期望的对象。
可以看到理解隐式丢失的重要性在于,它可以帮助开发者预测和避免由于 this 绑定不当而导致的错误。所以我们应该在编写代码时特别是在处理函数引用和回调的情况下更加谨慎。
显示绑定
显式绑定是通过 call, apply, 和 bind 方法来实现的。这些方法允许开发者直接控制函数执行时的 this 值。显式绑定指的是使用 call 或 apply 来立即调用函数,并指定 this 的值。
让我们来看显式绑定的例子:
function foo() {
console.log('name: ' + this.name);
}
var obj1 = {
name: "obj1"
};
foo.call(obj1); // name: obj1, 使用 call 显式设置了 this 为 obj1
foo.apply(obj1); // name: obj1, 使用 apply 显式设置了 this 为 obj1
// 使用 call 和 apply 传递参数
function bar(arg1, arg2) {
console.log('args: ' + arg1 + ', ' + arg2);
console.log('name: ' + this.name);
}
bar.call(obj1, 'hello', 'world'); // args: hello, world; name: obj1
bar.apply(obj1, ['hello', 'world']); // args: hello, world; name: obj1
在这个例子中,foo 和 bar 函数都通过 call 和 apply 被显式地绑定了 this 到 obj1 对象。call 和 apply 的区别在于参数的传递方式:call 接受的是参数列表,而 apply 接受的是参数数组。这使得我们可以根据需要选择合适的方法来传递参数。
硬绑定
下面让我们来硬绑定,硬绑定是通过 bind 创建一个新的函数,其 this 永久绑定到指定的对象,即使这个新函数再次被作为方法调用或传递给其他上下文,它的 this 也不会改变。
function foo() {
console.log('name: ' + this.name);
}
var obj2 = {
name: "obj2"
};
var bar = foo.bind(obj2);
bar(); // name: obj2, 使用 bind 创建的新函数,其 this 永久绑定为 obj2
// 即使将 bar 作为另一个对象的方法调用,this 仍然指向 obj2
var obj3 = {
name: "obj3",
bar: bar
};
obj3.bar(); // name: obj2, 尽管 bar 是 obj3 的方法,但 this 仍然是 obj2
这段代码展示了如何使用 bind 方法创建一个新函数 bar,其 this 永久绑定到 obj2。无论 bar 是以何种方式被调用,它的 this 都不会改变,始终指向 obj2。这在处理回调函数、事件处理器或者其他可能影响 this 绑定的情境中非常有用。
值得注意的是,硬绑定可以防止 this 的隐式丢失问题。例如,在设置定时器或添加事件监听器时,如果不小心可能会导致 this 失效。通过使用 bind,我们可以确保函数内部的 this 指向我们预期的对象。
综上所述,显式绑定和硬绑定提供了强大的工具,让开发者能够精确控制 this 的行为。理解这些机制可以帮助避免常见的 this 相关错误,提高代码的可靠性和可维护性。
new绑定
当使用 new 关键字调用函数时,JavaScript 会创建一个全新的对象,并将 this 绑定到这个新对象上。构造函数通常用来初始化新对象的状态。如果不使用 new 来调用构造函数,那么 this 将不会正确绑定。
function Foo(name) {
this.name = name;
}
var a = new Foo("a");
console.log(a.name); // "a"
var b = Foo("b"); // 忘记使用 new,this 没有正确绑定,可能导致全局对象受到影响
console.log(b); // undefined, 因为没有返回值
console.log(window.name); // "b" (非严格模式下), 因为 this 指向了全局对象
在这段代码中,Foo 作为一个构造函数被调用。当使用 new 关键字时,this 正确绑定到了新创建的对象 a。然而,如果没有使用 new,则会导致 this 错误地指向全局对象,可能造成意外的结果。
终于讲完了这四种绑定方式,最后再来看下箭头函数吧
箭头函数
箭头函数不拥有自己的 this,而是继承自定义时所在的词法作用域。因此,它们不受上述四种绑定规则的影响。
const obj4 = {
a: 2,
foo: () => {
console.log("arrow: " + this.a);
}
};
obj4.foo(); // arrow: undefined 或 window.a (取决于是否启用严格模式),因为箭头函数的 this 继承自外层作用域
这里展示了箭头函数的行为。与常规函数不同,箭头函数不会创建自己的 this,而是使用定义时所在的作用域的 this,这使得它在某些情况下表现得不同于其他类型的函数。
由于篇幅原因,笔者决定将this的优先级、绑定例外和this词法放到下一篇来写,期待一下吧,关注一下吧~