JavaScript 中的 this:一篇文章终结你的困惑
this 是 JavaScript 最让人困惑的概念之一。本文用最通俗的方式,彻底讲清楚 this 的指向规则。
灵魂拷问
const obj = {
name: '小明',
sayHi: function() {
console.log(this.name);
}
};
obj.sayHi(); // 输出什么?
const fn = obj.sayHi;
fn(); // 输出什么?
如果你能立刻说出答案,这篇文章可以跳过。
如果你犹豫了……那我们今天就把 this 彻底搞清楚。
this 到底是什么?
一句话解释:this 是函数执行时的上下文对象。
注意关键词:执行时。不是定义时,是执行时。
这意味着,同一个函数,在不同的情况下调用,this 可能指向不同的对象。
function sayHi() {
console.log(this.name);
}
const person1 = { name: '小明', sayHi };
const person2 = { name: '小红', sayHi };
person1.sayHi(); // 小明
person2.sayHi(); // 小红
同一个函数,谁调用,this 就指向谁。
四条规则判断 this
判断 this 指向,只需要记住这四条规则,优先级从高到低:
规则 1:new 绑定
如果函数是通过 new 调用的,this 指向新创建的对象。
function Person(name) {
this.name = name;
// this 指向新创建的对象
}
const p = new Person('小明');
console.log(p.name); // 小明
原理:new 做了什么?
- 创建一个新对象
- 把这个新对象的原型指向构造函数的 prototype
- 把
this绑定到这个新对象 - 执行构造函数
- 如果构造函数没有返回对象,就返回这个新对象
规则 2:显式绑定
使用 call、apply、bind 可以明确指定 this。
function sayHi() {
console.log(this.name);
}
const person = { name: '小明' };
sayHi.call(person); // 小明
sayHi.apply(person); // 小明
const boundFn = sayHi.bind(person);
boundFn(); // 小明
三者的区别:
| 方法 | 执行时机 | 参数格式 |
|---|---|---|
call | 立即执行 | 逐个传参 fn.call(obj, arg1, arg2) |
apply | 立即执行 | 数组传参 fn.apply(obj, [arg1, arg2]) |
bind | 返回新函数 | 逐个传参 fn.bind(obj, arg1, arg2) |
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: '小明' };
greet.call(person, 'Hello', '!'); // Hello, 小明!
greet.apply(person, ['Hello', '!']); // Hello, 小明!
const boundGreet = greet.bind(person, 'Hello');
boundGreet('!'); // Hello, 小明!
规则 3:隐式绑定
如果函数作为对象的方法调用,this 指向那个对象。
const obj = {
name: '小明',
sayHi: function() {
console.log(this.name);
}
};
obj.sayHi(); // 小明,this 指向 obj
注意隐式丢失:
const obj = {
name: '小明',
sayHi: function() {
console.log(this.name);
}
};
const fn = obj.sayHi; // 把方法赋值给变量
fn(); // undefined,this 指向全局对象(严格模式下是 undefined)
为什么?因为 fn() 调用时,没有"调用者",不符合"谁调用就指向谁"的规则。
常见的隐式丢失场景:
// 场景 1:赋值给变量
const fn = obj.sayHi;
fn();
// 场景 2:作为回调函数
setTimeout(obj.sayHi, 100);
// 场景 3:作为参数传递
function execute(callback) {
callback();
}
execute(obj.sayHi);
规则 4:默认绑定
如果以上规则都不适用,this 使用默认绑定。
function sayHi() {
console.log(this);
}
sayHi(); // 非严格模式:window(浏览器)/ global(Node.js)
// 严格模式:undefined
严格模式的影响:
'use strict';
function sayHi() {
console.log(this);
}
sayHi(); // undefined
优先级排序
当多个规则同时出现时,按这个优先级判断:
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
例子:
function Person(name) {
this.name = name;
}
const obj = { name: '小红' };
const BoundPerson = Person.bind(obj);
const p = new BoundPerson('小明');
console.log(p.name); // 小明,不是小红!
// 虽然 bind 了 obj,但 new 的优先级更高
箭头函数:没有自己的 this
箭头函数不遵守上述规则,它没有自己的 this,而是继承外层的 this。
const obj = {
name: '小明',
sayHi: function() {
// 这里的 this 是 obj
const inner = () => {
console.log(this.name); // 继承外层的 this,还是 obj
};
inner();
}
};
obj.sayHi(); // 小明
对比普通函数:
const obj = {
name: '小明',
sayHi: function() {
const inner = function() {
console.log(this.name); // 默认绑定,this 是 window/undefined
};
inner();
}
};
obj.sayHi(); // undefined(严格模式)或 ''(非严格模式)
箭头函数的 this 在定义时就确定了,不会改变:
const obj = {
name: '小明',
sayHi: () => {
console.log(this.name);
}
};
obj.sayHi(); // undefined,不是"小明"!
// 因为箭头函数在定义时,外层没有函数,this 是全局对象
不能对箭头函数使用 call/apply/bind 改变 this:
const fn = () => {
console.log(this.name);
};
const person = { name: '小明' };
fn.call(person); // undefined,不是"小明"
常见面试题
题目 1:基础绑定
var name = '全局';
const obj = {
name: '小明',
sayHi: function() {
console.log(this.name);
},
sayHi2: () => {
console.log(this.name);
}
};
obj.sayHi(); // ?
obj.sayHi2(); // ?
答案:
obj.sayHi()→小明(隐式绑定)obj.sayHi2()→全局(箭头函数继承外层 this,即全局对象)
题目 2:回调函数中的 this
const obj = {
name: '小明',
friends: ['小红', '小刚'],
showFriends: function() {
this.friends.forEach(function(friend) {
console.log(this.name + '的朋友:' + friend);
});
}
};
obj.showFriends(); // 输出什么?
答案:
undefined的朋友:小红
undefined的朋友:小刚
因为 forEach 的回调函数是普通函数,this 默认绑定到全局对象。
修复方法 1:使用箭头函数
showFriends: function() {
this.friends.forEach((friend) => {
console.log(this.name + '的朋友:' + friend);
});
}
修复方法 2:使用 forEach 的第二个参数
showFriends: function() {
this.friends.forEach(function(friend) {
console.log(this.name + '的朋友:' + friend);
}, this); // 传入 this 作为回调的 this
}
修复方法 3:提前保存 this
showFriends: function() {
const self = this;
this.friends.forEach(function(friend) {
console.log(self.name + '的朋友:' + friend);
});
}
题目 3:嵌套对象
const obj = {
name: '小明',
inner: {
name: '小红',
sayHi: function() {
console.log(this.name);
}
}
};
obj.inner.sayHi(); // ?
const fn = obj.inner.sayHi;
fn(); // ?
答案:
obj.inner.sayHi()→小红(隐式绑定,this 指向最近的调用对象 inner)fn()→undefined(隐式丢失,默认绑定)
题目 4:类中的 this
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(this.name);
}
}
const p = new Person('小明');
p.sayHi(); // ?
const fn = p.sayHi;
fn(); // ?
答案:
p.sayHi()→小明fn()→ 报错!Cannot read property 'name' of undefined
类中的方法默认在严格模式下运行,隐式丢失后 this 是 undefined。
修复方法:在构造函数中绑定 this
class Person {
constructor(name) {
this.name = name;
this.sayHi = this.sayHi.bind(this); // 绑定 this
}
sayHi() {
console.log(this.name);
}
}
或者使用类属性 + 箭头函数:
class Person {
constructor(name) {
this.name = name;
}
sayHi = () => {
console.log(this.name);
}
}
总结
判断 this 指向的口诀:
- new 调用:this 指向新创建的对象
- call/apply/bind:this 指向指定的对象
- 对象方法调用:this 指向调用的对象
- 独立函数调用:this 指向全局对象(严格模式是 undefined)
- 箭头函数:继承外层的 this,不能被改变
// 一个综合例子
const obj = {
name: '小明',
methods: {
sayHi: function() { console.log(this.name); },
sayHi2: () => { console.log(this.name); },
sayHi3() { console.log(this.name); }
}
};
obj.methods.sayHi(); // undefined (this 是 methods 对象,没有 name)
obj.methods.sayHi2(); // undefined (箭头函数,this 是全局对象)
obj.methods.sayHi3(); // undefined (同 sayHi)
理解 this,关键是理解函数是怎么被调用的。
下一篇,我们来聊聊数据结构中的"排队专家"——队列。