前言
JavaScript中的
this关键字是语言中最令人困惑但又最强大的特性之一。对于初学者甚至是有经验的开发者来说,this的行为常常让人感到困惑。本文将全面解析this的工作原理,帮助你在各种场景下准确理解和正确使用它。
一、this的基本概念
1.1 什么是this
在JavaScript中,this是一个特殊的关键字,它指向当前执行上下文中的"所有者"对象。与大多数其他语言不同,JavaScript中的this不是固定不变的,它的值取决于函数的调用位置的调用方式,所以分析this的值就是分析调用位置和调用方式。
1.2 调用位置与调用栈
调用位置就是函数在代码中被调用的位置(而不是声明的位置),想要得到调用位置,我们得分析调用栈,调用位置 由 调用栈 决定,是当前执行函数的前一个调用。
下面我们来看看什么是调用栈和调用位置。
function baz(){
//当前调用栈是:baz
//因此,当前调用位置是全局作用域
console.log("baz");
bar() // <--bar的调用位置
}
function bar(){
console.log("bar");
foo() // <--foo的调用位置
}
function foo(){
//当前调用栈是baz->bar->foo
//因此,当前调用位置在bar中
console.log("foo");
}
baz(); // <--baz的调用位置
1.3 调用方式:this的绑定规则
在JavaScript中调用方式的this绑定遵循四条基本规则:
- 默认绑定
- 隐式绑定
- 显式绑定
- new绑定
理解这些规则是掌握this的关键。我们将在接下来的章节中详细探讨每种绑定方式。
二、默认绑定
2.1 默认绑定
首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其它规则时的默认规则,优先级最低,保底使用这种方式进行绑定this
function showThis() {
console.log(this.a);
}
var a = 2;
showThis(); // 输出: 2
可以看到外我们在没有任何修饰的情况下进行独立函数调用,因此使用默认绑定
2.2 严格模式下的默认绑定
而在严格模式('use strict')下,默认绑定的行为会有所不同:this会被设置为undefined而不是全局对象。
'use strict';
function strictThis() {
console.log(this.a);
}
var a = 2;
strictThis(); // 报错:TypeError: this is undefined
三、隐式绑定
3.1 方法调用中的this
当函数作为对象的方法被调用时,this会隐式绑定到该对象。
// 函数greet作为person的方法被调用
const person = {
name: 'Alice',
greet: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
person.greet(); // 输出: Hello, my name is Alice
3.2 隐式丢失问题
隐式绑定最常见的陷阱是"隐式丢失"——当方法被赋值给变量或作为回调传递时,会丢失原来的this绑定。
下面代码中的const greetFunc = person.greet、greetFunc()调用的是greet函数本身,所以此时的greet()是一个没有任何修饰的调用,使用默认绑定
const person = {
name: 'Bob',
greet: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
const greetFunc = person.greet;
greetFunc(); // 输出: Hello, my name is undefined (this指向全局对象)
3.3 多层对象中的this
当方法调用涉及多层对象时,this绑定到最近的直接对象。
const school = {
name: 'ABC School',
student: {
name: 'Charlie',
greet: function() {
console.log(`I'm ${this.name} from ${this.school}`);
}
}
};
school.student.greet(); // 输出: I'm Charlie from undefined
四、显式绑定
4.1 call和apply方法
JavaScript提供了call和apply方法,允许我们显式指定函数调用时的this值。
function introduce(lang1, lang2) {
console.log(`My name is ${this.name} and I know ${lang1} and ${lang2}`);
}
const person = { name: 'David' };
introduce.call(person, 'JavaScript', 'Python');
// 输出: My name is David and I know JavaScript and Python
introduce.apply(person, ['Java', 'C++']);
// 输出: My name is David and I know Java and C++
4.2 硬绑定:bind方法
bind方法创建一个新函数,永久绑定指定的this值。这种方法被称为硬绑定。
const person = { name: 'Eve' };
function greet() {
console.log(`Hello, ${this.name}!`);
}
const boundGreet = greet.bind(person);
boundGreet(); // 输出: Hello, Eve!
setTimeout(boundGreet, 100); // 100ms后输出: Hello, Eve!
4.3 硬绑定的应用场景
硬绑定常用于事件处理程序和异步回调中,确保回调函数中的this指向预期的对象。
function Button() {
this.clickCount = 0;
this.handleClick = function() {
this.clickCount++;
console.log(`Button clicked ${this.clickCount} times`);
}.bind(this); // 硬绑定确保this始终指向Button实例
}
const btn = new Button();
document.querySelector('button').addEventListener('click', btn.handleClick);
五、new绑定
5.1 构造函数中的this
当使用new关键字调用函数时,会发生以下步骤:
- 创建一个新对象
- 将新对象的
[[Prototype]]链接到构造函数的prototype属性 - 将
this绑定到新创建的对象 - 如果函数没有返回其他对象,则自动返回这个新对象
function Person(name) {
this.name = name;
this.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};
}
const alice = new Person('Alice');
alice.greet(); // 输出: Hello, I'm Alice
5.2 new绑定的注意事项
如果构造函数返回一个对象,则new表达式的结果是该返回对象,而不是新创建的对象。
function Person(name) {
this.name = name;
return { name: 'Overridden' };
}
const bob = new Person('Bob');
console.log(bob.name); // 输出: Overridden
六、this的优先级规则
6.1 绑定规则的优先级顺序
当多种绑定规则同时适用时,JavaScript按照以下优先级确定this的值:
- new绑定
- 显式绑定(call/apply/bind)
- 隐式绑定(方法调用)
- 默认绑定
function foo() {
console.log(this.name);
}
const obj1 = { name: 'obj1', foo: foo };
const obj2 = { name: 'obj2', foo: foo };
obj1.foo(); // 隐式绑定: obj1
obj1.foo.call(obj2); // 显式绑定优先: obj2
new obj1.foo(); // new绑定优先: 新创建的对象
6.2 例外情况
某些情况下,显式绑定的this值会被忽略,例如当传入null或undefined时,会应用默认绑定规则。
function bar() {
console.log(this);
}
bar.call(null); // 在非严格模式下输出: Window {...}
bar.call(undefined); // 同上
七、箭头函数中的this
7.1 箭头函数的this行为
箭头函数不绑定自己的this,而是继承外层函数作用域的this值,箭头函数在涉及this绑定时的行为不会遵守上面的四条规则。
const obj = {
name: 'Frank',
regularFunc: function() {
console.log(this.name);
},
arrowFunc: () => {
console.log(this.name);
}
};
obj.regularFunc(); // 输出: Frank
obj.arrowFunc(); // 输出: undefined (this继承自外层作用域)
7.2 箭头函数的适用场景
箭头函数特别适合用作回调函数,因为它们不会改变this的绑定。
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
const timer = new Timer(); // 每秒输出递增的数字
7.3 箭头函数的限制
由于箭头函数没有自己的this,因此不能用作构造函数,也不能通过call、apply或bind来改变this绑定。
const Person = (name) => {
this.name = name; // TypeError: 箭头函数不能用作构造函数
};
const p = new Person('Grace');
八、this的特殊情况
8.1 DOM事件处理程序中的this
在DOM事件处理程序中,this通常指向触发事件的元素。
document.querySelector('button').addEventListener('click', function() {
console.log(this); // 输出: <button>元素
});
8.2 定时器回调中的this
在setTimeout或setInterval的回调函数中,this默认指向全局对象(非严格模式)或undefined(严格模式),除非使用箭头函数或显式绑定。
const obj = {
name: 'Henry',
delayedGreet: function() {
setTimeout(function() {
console.log(`Hello, ${this.name}`); // 输出: Hello, undefined
}, 100);
},
arrowDelayedGreet: function() {
setTimeout(() => {
console.log(`Hello, ${this.name}`); // 输出: Hello, Henry
}, 100);
}
};
obj.delayedGreet();
obj.arrowDelayedGreet();
九、this的最佳实践
9.1 避免this混淆的策略
为了避免this带来的困惑,可以采取以下策略:
- 在构造函数和方法中使用
this,在其他地方避免使用 - 使用箭头函数来保持
this的一致性 - 在需要明确
this指向时使用bind - 使用
const self = this模式(虽然箭头函数现在更受欢迎)
function OldSchool() {
const self = this;
this.value = 42;
setTimeout(function() {
console.log(self.value); // 使用self而不是this
}, 100);
}
9.2 现代JavaScript中的this模式
在现代JavaScript开发中,推荐的做法包括:
- 在React组件中使用箭头函数作为类属性
- 在Vue中合理使用箭头函数和普通函数
- 在模块模式中避免不必要的
this使用
// React组件示例
class MyComponent extends React.Component {
state = { count: 0 };
// 使用箭头函数确保this始终指向组件实例
handleClick = () => {
this.setState(prevState => ({ count: prevState.count + 1 }));
};
render() {
return <button onClick={this.handleClick}>Click me</button>;
}
}
十、总结
JavaScript中的this机制虽然复杂,但一旦掌握了它的绑定规则,就能写出更灵活、更强大的代码。关键点总结如下:
this的值取决于函数的调用方式,而不是定义位置- 四种绑定规则:默认、隐式、显式和new绑定
- 箭头函数不绑定自己的
this,而是继承外层作用域的this - 在不确定
this指向时,可以使用console.log(this)进行调试 - 在现代开发中,合理使用箭头函数和显式绑定可以减少
this相关的问题
理解this是成为JavaScript高手的重要一步。通过实践和经验的积累,你将能够自信地处理各种this相关的场景,写出更清晰、更健壮的代码。
🌇结尾
本文部分内容参考KYLE SIMPSON的《你不知道的JavaScript(上卷) 》
感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生
(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)
作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。