this 指向
问题
JavaScript 中 this 的指向规则是什么?如何改变 this 的指向?
答案
this 是 JavaScript 中的关键字,它的值在运行时确定,取决于函数的调用方式,而不是函数的定义位置。
this 绑定规则
四种绑定规则
1. 默认绑定
独立函数调用时,this 指向全局对象(非严格模式)或 undefined(严格模式)。
function foo(): void {
console.log(this);
}
foo(); // 非严格模式: window/global
// 严格模式: undefined
// 严格模式
'use strict';
function bar(): void {
console.log(this); // undefined
}
2. 隐式绑定
作为对象方法调用时,this 指向调用该方法的对象。
const obj = {
name: 'Alice',
sayName(): void {
console.log(this.name);
},
};
obj.sayName(); // 'Alice',this 指向 obj
// 隐式丢失
const fn = obj.sayName;
fn(); // undefined,this 丢失,指向全局
// 回调中的隐式丢失
setTimeout(obj.sayName, 100); // undefined
3. 显式绑定
使用 call、apply、bind 显式指定 this。
function greet(greeting: string, punctuation: string): void {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: 'Alice' };
// call: 立即执行,参数逐个传递
greet.call(person, 'Hello', '!'); // 'Hello, Alice!'
// apply: 立即执行,参数以数组传递
greet.apply(person, ['Hi', '?']); // 'Hi, Alice?'
// bind: 返回新函数,不立即执行
const boundGreet = greet.bind(person, 'Hey');
boundGreet('~'); // 'Hey, Alice~'
4. new 绑定
使用 new 调用构造函数时,this 指向新创建的对象。
function Person(this: any, name: string) {
this.name = name;
console.log(this); // Person { name: 'Alice' }
}
const person = new (Person as any)('Alice');
绑定优先级
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
function foo(this: any): void {
console.log(this.a);
}
const obj1 = { a: 1, foo };
const obj2 = { a: 2 };
// 隐式 vs 显式
obj1.foo.call(obj2); // 2(显式优先)
// 显式 vs new
const BoundFoo = foo.bind(obj1);
const instance = new (BoundFoo as any)(); // undefined(new 优先,this 指向新对象)
箭头函数
箭头函数没有自己的 this,继承外层作用域的 this(词法绑定)。
const obj = {
name: 'Alice',
// 普通函数
sayName(): void {
console.log(this.name); // 'Alice'
},
// 箭头函数
sayNameArrow: () => {
console.log(this.name); // undefined(继承外层,这里是全局)
},
// 箭头函数的正确使用
delayedSay(): void {
setTimeout(() => {
console.log(this.name); // 'Alice'(继承 delayedSay 的 this)
}, 100);
},
};
obj.sayName(); // 'Alice'
obj.sayNameArrow(); // undefined
obj.delayedSay(); // 'Alice'
箭头函数特点
const arrowFn = () => {
console.log(this);
};
// 1. 没有自己的 this
// 2. 不能用 call/apply/bind 改变 this
arrowFn.call({ a: 1 }); // 仍然是外层 this
// 3. 不能作为构造函数
// new arrowFn(); // TypeError
// 4. 没有 arguments 对象
const fn = () => {
// console.log(arguments); // ReferenceError
};
// 5. 没有 prototype
console.log(arrowFn.prototype); // undefined
手写实现
手写 call
interface Function {
myCall<T, A extends any[], R>(
this: (this: T, ...args: A) => R,
thisArg: T,
...args: A
): R;
}
Function.prototype.myCall = function(thisArg: any, ...args: any[]): any {
// 处理 null/undefined
thisArg = thisArg ?? globalThis;
// 将 thisArg 转为对象
thisArg = Object(thisArg);
// 创建唯一 key,避免覆盖原有属性
const key = Symbol('fn');
// 将函数作为 thisArg 的方法
thisArg[key] = this;
// 调用方法
const result = thisArg[key](...args);
// 删除临时方法
delete thisArg[key];
return result;
};
// 测试
function greet(this: any, greeting: string): string {
return `${greeting}, ${this.name}`;
}
console.log(greet.myCall({ name: 'Alice' }, 'Hello')); // 'Hello, Alice'
手写 apply
interface Function {
myApply<T, A extends any[], R>(
this: (this: T, ...args: A) => R,
thisArg: T,
args?: A
): R;
}
Function.prototype.myApply = function(thisArg: any, args?: any[]): any {
thisArg = thisArg ?? globalThis;
thisArg = Object(thisArg);
const key = Symbol('fn');
thisArg[key] = this;
const result = args ? thisArg[key](...args) : thisArg[key]();
delete thisArg[key];
return result;
};
// 测试
function sum(this: any, a: number, b: number): number {
return this.base + a + b;
}
console.log(sum.myApply({ base: 10 }, [1, 2])); // 13
手写 bind
interface Function {
myBind<T, A extends any[], R>(
this: (this: T, ...args: A) => R,
thisArg: T,
...boundArgs: Partial<A>
): (...args: any[]) => R;
}
Function.prototype.myBind = function(thisArg: any, ...boundArgs: any[]): any {
const fn = this;
const boundFn = function(this: any, ...args: any[]): any {
// 判断是否作为构造函数调用
const isNew = this instanceof boundFn;
return fn.apply(
isNew ? this : thisArg,
[...boundArgs, ...args]
);
};
// 继承原函数原型
if (fn.prototype) {
boundFn.prototype = Object.create(fn.prototype);
}
return boundFn;
};
// 测试
function Person(this: any, name: string, age: number) {
this.name = name;
this.age = age;
}
const BoundPerson = Person.myBind(null, 'Alice');
const p = new (BoundPerson as any)(18);
console.log(p.name, p.age); // 'Alice', 18
常见场景
事件处理器
class Button {
text = 'Click me';
// ❌ 普通方法会丢失 this
handleClick(): void {
console.log(this.text);
}
// ✅ 方法 1: 箭头函数属性
handleClickArrow = (): void => {
console.log(this.text);
};
// ✅ 方法 2: 构造函数中绑定
constructor() {
this.handleClick = this.handleClick.bind(this);
}
}
const btn = new Button();
// 模拟事件绑定
const element = document.createElement('button');
element.addEventListener('click', btn.handleClickArrow); // ✅ 正常工作
回调函数
const obj = {
data: [1, 2, 3],
// ❌ this 丢失
processWrong(): void {
this.data.forEach(function(item) {
console.log(this); // undefined 或 window
});
},
// ✅ 方法 1: 箭头函数
processArrow(): void {
this.data.forEach((item) => {
console.log(this.data); // [1, 2, 3]
});
},
// ✅ 方法 2: thisArg 参数
processThisArg(): void {
this.data.forEach(function(this: typeof obj, item) {
console.log(this.data);
}, this);
},
// ✅ 方法 3: 保存 this
processSelf(): void {
const self = this;
this.data.forEach(function(item) {
console.log(self.data);
});
},
};
常见面试问题
Q1: 说说 this 的绑定规则及优先级?
答案:
四种绑定规则(优先级从高到低):
| 规则 | 场景 | this 指向 |
|---|---|---|
| new 绑定 | new Foo() | 新创建的对象 |
| 显式绑定 | call/apply/bind | 指定的对象 |
| 隐式绑定 | obj.method() | 调用的对象 |
| 默认绑定 | foo() | globalThis 或 undefined |
Q2: 箭头函数和普通函数的 this 有什么区别?
答案:
| 特性 | 普通函数 | 箭头函数 |
|---|---|---|
| this 来源 | 调用时确定 | 定义时确定(词法) |
| 能否改变 | 可以(call/apply/bind) | 不能 |
| 作为构造函数 | 可以 | 不能 |
| arguments | 有 | 没有 |
| prototype | 有 | 没有 |
const obj = {
fn: function() { console.log(this); }, // obj
arrow: () => { console.log(this); }, // 外层 this
};
Q3: call、apply、bind 的区别?
答案:
| 方法 | 执行时机 | 参数形式 | 返回值 |
|---|---|---|---|
call | 立即执行 | 逐个传递 | 函数返回值 |
apply | 立即执行 | 数组传递 | 函数返回值 |
bind | 不执行 | 逐个传递 | 新函数 |
fn.call(obj, 1, 2); // 立即执行
fn.apply(obj, [1, 2]); // 立即执行
const bound = fn.bind(obj, 1); // 返回新函数
bound(2); // 后续调用
Q4: 如何解决 this 丢失问题?
答案:
class Counter {
count = 0;
// 问题:作为回调时 this 丢失
increment(): void {
this.count++;
}
}
const counter = new Counter();
// 解决方案:
// 1. 箭头函数属性
class Counter1 {
count = 0;
increment = () => { this.count++; };
}
// 2. bind 绑定
const bound = counter.increment.bind(counter);
// 3. 包装箭头函数
setTimeout(() => counter.increment(), 100);
Q5: 下面代码输出什么?
const obj = {
name: 'obj',
getName: function() {
return this.name;
},
getNameArrow: () => {
return this.name;
},
nested: {
name: 'nested',
getName: function() {
return this.name;
},
},
};
console.log(obj.getName()); // ?
console.log(obj.getNameArrow()); // ?
console.log(obj.nested.getName()); // ?
const fn = obj.getName;
console.log(fn()); // ?
答案:
console.log(obj.getName()); // 'obj'(隐式绑定)
console.log(obj.getNameArrow()); // undefined(箭头函数,外层 this)
console.log(obj.nested.getName()); // 'nested'(隐式绑定)
console.log(fn()); // undefined(默认绑定,this 丢失)
Q6: class 中的 this 指向问题——方法赋值为什么会丢失 this?
答案:
在 class 中定义的方法存储在原型对象上,当我们把方法赋值给一个变量或作为回调传递时,方法与原始对象之间的绑定关系就断了,this 会回退到默认绑定(严格模式下为 undefined,class 内部默认启用严格模式)。
class Logger {
prefix = '[LOG]';
// 普通方法——定义在 Logger.prototype 上
log(message: string): void {
console.log(`${this.prefix} ${message}`);
}
}
const logger = new Logger();
logger.log('hello'); // ✅ '[LOG] hello'
// 赋值给变量后,隐式绑定丢失
const detachedLog = logger.log;
detachedLog('hello'); // ❌ TypeError: Cannot read properties of undefined (reading 'prefix')
class 内部的代码自动运行在严格模式下,默认绑定的 this 是 undefined 而非 globalThis,所以访问 undefined.prefix 会直接抛出 TypeError。
三种解决方案对比:
| 方案 | 写法 | 原理 | 内存影响 |
|---|---|---|---|
| 箭头函数属性 | log = () => {} | 箭头函数捕获构造时的 this | 每个实例一份副本 |
| 构造函数中 bind | this.log = this.log.bind(this) | 返回绑定了 this 的新函数 | 每个实例一份副本 |
| 调用时包装箭头函数 | () => obj.log() | 保持隐式绑定的调用形式 | 无额外开销 |
class Logger {
prefix = '[LOG]';
// ✅ 方案 1:箭头函数属性(最常用)
log = (message: string): void => {
console.log(`${this.prefix} ${message}`);
};
}
class Logger2 {
prefix = '[LOG]';
log(message: string): void {
console.log(`${this.prefix} ${message}`);
}
constructor() {
// ✅ 方案 2:构造函数中 bind
this.log = this.log.bind(this);
}
}
// ✅ 方案 3:调用时包装箭头函数
const logger3 = new Logger();
setTimeout(() => logger3.log('hello'), 100);
当方法需要作为回调传递(如事件监听、setTimeout、Array.prototype.map 等),优先使用箭头函数属性,代码最简洁且不会遗忘绑定。如果关注内存(大量实例场景),则在调用时使用箭头函数包装。
Q7: 箭头函数和普通函数中 this 的区别——结合实际场景
答案:
核心区别在于 this 的确定时机:普通函数的 this 在调用时动态确定,箭头函数的 this 在定义时静态确定(词法绑定,继承外层作用域的 this)。
// 场景 1:对象方法
const counter = {
count: 0,
// 普通函数:this 由调用方式决定
increment() {
this.count++;
},
// 箭头函数:this 继承定义时外层作用域(这里是模块/全局作用域)
incrementArrow: () => {
// this 不是 counter,而是外层作用域的 this
// this.count++; // ❌ 不会修改 counter.count
},
};
counter.increment(); // ✅ this === counter
counter.incrementArrow(); // ❌ this !== counter
// 场景 2:回调函数——箭头函数更合适
class UserService {
users: string[] = [];
fetchUsers(): void {
// 模拟异步请求
Promise.resolve(['Alice', 'Bob']).then((data) => {
// ✅ 箭头函数:this 继承 fetchUsers 的 this,即 UserService 实例
this.users = data;
});
}
fetchUsersWrong(): void {
Promise.resolve(['Alice', 'Bob']).then(function (data) {
// ❌ 普通函数:this 为 undefined(严格模式)
// this.users = data; // TypeError
});
}
}
// 场景 3:原型方法——普通函数更合适
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
// ✅ 原型方法用普通函数,所有实例共享,且 this 指向调用实例
Animal.prototype.speak = function (): string {
return `${this.name} makes a sound`;
};
// ❌ 箭头函数不能用于原型方法,this 不会指向实例
Animal.prototype.speakArrow = () => {
// this 是外层作用域的 this,不是 Animal 实例
return `undefined makes a sound`;
};
- 需要动态 this(对象方法、原型方法)→ 使用普通函数
- 需要继承外层 this(回调、定时器、Promise 链)→ 使用箭头函数
- 永远不要用箭头函数定义对象字面量的方法或原型方法
Q8: React 类组件中 this 的绑定方式对比
答案:
在 React 类组件中,事件处理函数作为回调传递给 JSX 时会丢失 this 绑定。以下是四种常见的绑定方式及其优缺点:
import React, { Component } from 'react';
interface State {
count: number;
}
class Counter extends Component<{}, State> {
state: State = { count: 0 };
// 方式 1:构造函数中 bind
constructor(props: {}) {
super(props);
this.handleClick1 = this.handleClick1.bind(this);
}
handleClick1(): void {
this.setState({ count: this.state.count + 1 });
}
// 方式 2:类属性 + 箭头函数(最推荐)
handleClick2 = (): void => {
this.setState({ count: this.state.count + 1 });
};
// 方式 3:render 中 bind(不推荐)
handleClick3(): void {
this.setState({ count: this.state.count + 1 });
}
// 方式 4:render 中箭头函数包装(不推荐)
handleClick4(): void {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
{/* 方式 1: 构造函数 bind */}
<button onClick={this.handleClick1}>+1</button>
{/* 方式 2: 箭头函数属性 */}
<button onClick={this.handleClick2}>+1</button>
{/* 方式 3: render 中 bind(每次渲染创建新函数) */}
<button onClick={this.handleClick3.bind(this)}>+1</button>
{/* 方式 4: render 中箭头函数(每次渲染创建新函数) */}
<button onClick={() => this.handleClick4()}>+1</button>
</div>
);
}
}
四种方式对比:
| 方式 | 绑定时机 | 每次渲染创建新函数? | 对 PureComponent 影响 | 推荐度 |
|---|---|---|---|---|
| 构造函数 bind | 实例化时 | 否 | 无影响 | 推荐 |
| 箭头函数属性 | 实例化时 | 否 | 无影响 | 最推荐 |
| render 中 bind | 每次渲染 | 是 | 导致不必要的重渲染 | 不推荐 |
| render 中箭头函数 | 每次渲染 | 是 | 导致不必要的重渲染 | 不推荐 |
每次 render 调用都会创建一个新的函数引用,如果子组件是 PureComponent 或使用了 React.memo,浅比较会发现 props 变了,导致子组件不必要地重新渲染,抵消了性能优化效果。
React 官方推荐使用函数组件 + Hooks,函数组件中不存在 this 问题。如果是新项目,请直接使用函数组件。详见 React Hooks 原理。
Q9: this 在 setTimeout / setInterval 中的表现
答案:
setTimeout 和 setInterval 的回调函数是被全局调用的,因此回调中的 this 遵循默认绑定规则——非严格模式下指向 globalThis,严格模式下为 undefined。
const user = {
name: 'Alice',
greet(): void {
console.log(`Hello, I'm ${this.name}`);
},
greetLater(): void {
// ❌ 普通函数回调:this 丢失
setTimeout(function () {
console.log(`Hello, I'm ${this.name}`); // 'Hello, I'm undefined'
}, 100);
// ✅ 箭头函数回调:this 继承 greetLater 的 this
setTimeout(() => {
console.log(`Hello, I'm ${this.name}`); // 'Hello, I'm Alice'
}, 100);
},
};
user.greetLater();
setTimeout(obj.method, delay) 相当于先将 obj.method 赋值给一个临时变量再调用,等价于:
const temp = obj.method; // 方法引用被提取
temp(); // 默认绑定,this 丢失
setInterval 中的常见错误与修复:
class Poller {
count = 0;
timer: ReturnType<typeof setInterval> | null = null;
// ❌ 错误写法:this 丢失
startWrong(): void {
this.timer = setInterval(function (this: any) {
this.count++; // TypeError: Cannot read properties of undefined
console.log(this.count);
}, 1000);
}
// ✅ 修复方案 1:箭头函数
startArrow(): void {
this.timer = setInterval(() => {
this.count++;
console.log(this.count); // 1, 2, 3...
}, 1000);
}
// ✅ 修复方案 2:bind
startBind(): void {
this.timer = setInterval(
function (this: Poller) {
this.count++;
console.log(this.count);
}.bind(this),
1000
);
}
stop(): void {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
}
只要回调函数中需要访问外层的 this,就使用箭头函数——这是最简洁、最不容易出错的方案。setTimeout、setInterval、Promise.then、Array.prototype.forEach 等场景都适用。
Q10: 手写实现 Function.prototype.bind(考虑 new 调用)
答案:
bind 的核心行为:
- 返回一个新函数,调用时
this绑定到指定对象 - 支持柯里化(预置部分参数)
- 当返回的函数被
new调用时,this应指向新创建的实例而非绑定的对象(new优先级高于bind) - 通过
new创建的实例应能正确访问原函数的prototype
interface Function {
myBind<T>(thisArg: T, ...args: any[]): (...args: any[]) => any;
}
Function.prototype.myBind = function (thisArg: any, ...boundArgs: any[]) {
// 边界检查:调用者必须是函数
if (typeof this !== 'function') {
throw new TypeError('myBind must be called on a function');
}
const originalFn = this;
const boundFn = function (this: any, ...callArgs: any[]) {
// 合并预置参数和调用时参数
const finalArgs = [...boundArgs, ...callArgs];
// 关键判断:是否通过 new 调用
// 如果是 new 调用,this 是 boundFn 的实例,应使用这个新对象作为 this
// 如果是普通调用,使用绑定的 thisArg
const context = this instanceof boundFn ? this : thisArg;
return originalFn.apply(context, finalArgs);
};
// 维护原型链:让 new boundFn() 创建的实例能访问 originalFn.prototype
// 使用 Object.create 避免直接赋值导致修改 boundFn.prototype 影响原函数
if (originalFn.prototype) {
boundFn.prototype = Object.create(originalFn.prototype);
}
return boundFn;
};
测试用例:
// 测试 1:基础绑定
function greet(this: any, greeting: string, name: string): string {
return `${greeting}, ${name}! I'm ${this.role}`;
}
const adminGreet = greet.myBind({ role: 'Admin' }, 'Hello');
console.log(adminGreet('Alice')); // 'Hello, Alice! I'm Admin'
// 测试 2:柯里化
const hiAlice = greet.myBind({ role: 'User' }, 'Hi', 'Alice');
console.log(hiAlice()); // 'Hi, Alice! I'm User'
// 测试 3:new 调用时忽略绑定的 this
function Person(this: any, name: string, age: number) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function (): string {
return `Hi, I'm ${this.name}`;
};
const BoundPerson = Person.myBind({ role: 'ignored' }, 'Alice');
const p = new (BoundPerson as any)(25);
console.log(p.name); // 'Alice'(参数正确传递)
console.log(p.age); // 25
console.log(p.sayHi()); // "Hi, I'm Alice"(原型链正确继承)
console.log(p instanceof BoundPerson); // true
这是 ECMAScript 规范的规定。new 操作符的语义是创建一个全新的对象并将其作为 this,如果 bind 返回的函数被 new 调用时仍使用预绑定的 thisArg,就会违反 new 的语义,导致构造出来的对象不正确。所以 new 绑定的优先级高于显式绑定。
完整实现:支持 toString 与 length 属性
生产级别的 bind 实现还应处理 name、length 等函数元属性:
Function.prototype.myBind = function (thisArg: any, ...boundArgs: any[]) {
if (typeof this !== 'function') {
throw new TypeError('myBind must be called on a function');
}
const originalFn = this;
const boundFn = function (this: any, ...callArgs: any[]) {
const context = this instanceof boundFn ? this : thisArg;
return originalFn.apply(context, [...boundArgs, ...callArgs]);
};
// 维护原型链
if (originalFn.prototype) {
boundFn.prototype = Object.create(originalFn.prototype);
}
// 设置函数的 length(剩余期望参数数量)
const boundLength = Math.max(0, originalFn.length - boundArgs.length);
Object.defineProperty(boundFn, 'length', { value: boundLength });
// 设置函数的 name
Object.defineProperty(boundFn, 'name', {
value: `bound ${originalFn.name}`,
});
return boundFn;
};