跳到主要内容

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. 显式绑定

使用 callapplybind 显式指定 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-this-lost.ts
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 中会报 TypeError 而不是输出 undefined?

class 内部的代码自动运行在严格模式下,默认绑定的 thisundefined 而非 globalThis,所以访问 undefined.prefix 会直接抛出 TypeError

三种解决方案对比

方案写法原理内存影响
箭头函数属性log = () => {}箭头函数捕获构造时的 this每个实例一份副本
构造函数中 bindthis.log = this.log.bind(this)返回绑定了 this 的新函数每个实例一份副本
调用时包装箭头函数() => obj.log()保持隐式绑定的调用形式无额外开销
class-this-fix.ts
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);
最佳实践

当方法需要作为回调传递(如事件监听、setTimeoutArray.prototype.map 等),优先使用箭头函数属性,代码最简洁且不会遗忘绑定。如果关注内存(大量实例场景),则在调用时使用箭头函数包装。

Q7: 箭头函数和普通函数中 this 的区别——结合实际场景

答案

核心区别在于 this 的确定时机:普通函数的 this调用时动态确定,箭头函数的 this定义时静态确定(词法绑定,继承外层作用域的 this)。

arrow-vs-normal.ts
// 场景 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
arrow-callback.ts
// 场景 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
});
}
}
arrow-prototype.ts
// 场景 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 绑定。以下是四种常见的绑定方式及其优缺点:

react-class-this.tsx
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 中箭头函数每次渲染导致不必要的重渲染不推荐
为什么方式 3 和方式 4 不推荐?

每次 render 调用都会创建一个新的函数引用,如果子组件是 PureComponent 或使用了 React.memo浅比较会发现 props 变了,导致子组件不必要地重新渲染,抵消了性能优化效果。

现代 React 已不推荐类组件

React 官方推荐使用函数组件 + Hooks,函数组件中不存在 this 问题。如果是新项目,请直接使用函数组件。详见 React Hooks 原理

Q9: this 在 setTimeout / setInterval 中的表现

答案

setTimeoutsetInterval 的回调函数是被全局调用的,因此回调中的 this 遵循默认绑定规则——非严格模式下指向 globalThis,严格模式下为 undefined

timer-this.ts
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 中的常见错误与修复

interval-this.ts
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,就使用箭头函数——这是最简洁、最不容易出错的方案。setTimeoutsetIntervalPromise.thenArray.prototype.forEach 等场景都适用。

Q10: 手写实现 Function.prototype.bind(考虑 new 调用)

答案

bind 的核心行为:

  1. 返回一个新函数,调用时 this 绑定到指定对象
  2. 支持柯里化(预置部分参数)
  3. 当返回的函数被 new 调用时,this 应指向新创建的实例而非绑定的对象(new 优先级高于 bind
  4. 通过 new 创建的实例应能正确访问原函数的 prototype
my-bind.ts
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;
};

测试用例

my-bind-test.ts
// 测试 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
为什么 new 调用时要忽略 thisArg?

这是 ECMAScript 规范的规定。new 操作符的语义是创建一个全新的对象并将其作为 this,如果 bind 返回的函数被 new 调用时仍使用预绑定的 thisArg,就会违反 new 的语义,导致构造出来的对象不正确。所以 new 绑定的优先级高于显式绑定。

完整实现:支持 toString 与 length 属性

生产级别的 bind 实现还应处理 namelength 等函数元属性:

my-bind-full.ts
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;
};

相关链接