JavaScript 中 this 指向完全指南

一、核心原则

this 的指向在函数调用时确定,而非定义时。关键看函数是如何被调用的。


二、this 绑定规则(按优先级排序)

1. new 绑定(优先级最高)

使用 new 调用构造函数时,this 指向新创建的对象。

ini 复制代码
function Person(name) {
  this.name = name;
}
const p = new Person('张三');
console.log(p.name); // 张三

2. 显式绑定(call/apply/bind)

通过 callapplybind 手动指定 this

javascript 复制代码
function greet() {
  console.log(this.name);
}
const obj = { name: '李四' };
greet.call(obj); // 李四

3. 隐式绑定(对象方法调用)

作为对象方法调用时,this 指向调用该方法的对象。

javascript 复制代码
const obj = {
  name: '王五',
  sayName() {
    console.log(this.name);
  }
};
obj.sayName(); // 王五

4. 默认绑定(优先级最低)

独立函数调用时:

  • 非严格模式:this 指向全局对象(浏览器中是 window
  • 严格模式:thisundefined
scss 复制代码
function foo() {
  console.log(this);
}
foo(); // 非严格模式: window,严格模式: undefined

三、隐式绑定丢失

3.1 什么是隐式绑定丢失?

函数引用在传递过程中脱离了对象上下文,导致 this 指向改变。

3.2 常见丢失场景

场景1:赋值给变量

javascript 复制代码
const obj = {
  name: '张三',
  sayName() {
    console.log(this.name);
  }
};

const fn = obj.sayName;  // 只复制了函数引用
fn();  // undefined(this 丢失,指向全局)

原因: fn() 是独立函数调用,没有对象前缀,适用默认绑定规则。

场景2:作为回调函数

javascript 复制代码
const obj = {
  name: '李四',
  sayName() {
    console.log(this.name);
  }
};

setTimeout(obj.sayName, 1000);  // undefined

原因: setTimeout 内部执行 callback(),是独立调用。

场景3:传递给其他函数

scss 复制代码
function execute(fn) {
  fn();  // 独立调用
}

const obj = {
  name: '王五',
  sayName() {
    console.log(this.name);
  }
};

execute(obj.sayName);  // undefined

3.3 解决方案

方案1:bind 硬绑定

ini 复制代码
const fn = obj.sayName.bind(obj);
execute(fn);  // 王五 ✓

方案2:箭头函数包裹

scss 复制代码
setTimeout(() => obj.sayName(), 1000);  // 王五 ✓

方案3:在类中使用箭头函数(见第五章)


四、call/apply/bind 详解

4.1 三者对比

特性 call apply bind
执行时机 立即执行 立即执行 返回新函数
参数传递 逐个传递 数组传递 逐个传递
返回值 函数执行结果 函数执行结果 绑定后的新函数

4.2 call - 立即调用,参数逐个传

javascript 复制代码
function greet(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation);
}

const person = { name: '张三' };
greet.call(person, 'Hello', '!');  // Hello, 张三!

语法: fn.call(thisArg, arg1, arg2, ...)

4.3 apply - 立即调用,参数数组传

arduino 复制代码
function greet(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation);
}

const person = { name: '李四' };
greet.apply(person, ['Hello', '!']);  // Hello, 李四!

语法: fn.apply(thisArg, [arg1, arg2, ...])

4.4 bind - 返回新函数,延迟调用

javascript 复制代码
function greet(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation);
}

const person = { name: '王五' };
const boundGreet = greet.bind(person, 'Hello');
boundGreet('!');  // Hello, 王五!

语法: const newFn = fn.bind(thisArg, arg1, arg2, ...)

4.5 为什么有三种形式?

设计目的

  • call:立即调用 + 参数明确
  • apply:立即调用 + 参数是数组
  • bind:创建绑定函数,用于回调场景

历史演进

javascript 复制代码
// 1. call/apply (ES3) - 借用方法
const arr = { 0: 'a', 1: 'b', length: 2 };
Array.prototype.slice.call(arr);

// 2. apply (ES3) - 数组参数
Math.max.apply(null, [1, 5, 3, 2]);

// 3. bind (ES5) - 解决回调中 this 丢失
setTimeout(obj.method.bind(obj), 1000);

4.6 apply 第一个参数为什么是 null?

当函数内部不使用 this 时,可以传 null

arduino 复制代码
// Math.max 不依赖 this
const numbers = [5, 10, 3, 8];
const max = Math.max.apply(null, numbers);  // 10

何时必须传对象?

javascript 复制代码
// 数组方法依赖 this
const arr1 = [1, 2];
const arr2 = [3, 4];

// ✓ 正确:this 指向 arr1
Array.prototype.push.apply(arr1, arr2);

// ✗ 错误:this 是 null
Array.prototype.push.apply(null, arr2);  // TypeError

4.7 现代替代方案

javascript 复制代码
// 旧写法(ES5)
Math.max.apply(null, [1, 5, 3]);
Array.prototype.push.apply(arr1, arr2);

// 新写法(ES6+,推荐)
Math.max(...[1, 5, 3]);
arr1.push(...arr2);

apply 现在的使用场景:

  • 需要同时改变 this 且参数是数组时

五、箭头函数的 this

5.1 核心特性

箭头函数没有自己的 this,继承外层作用域的 this(定义时确定,无法改变)。

5.2 常见误区:对象字面量中的箭头函数

javascript 复制代码
// ❌ 错误示例
const obj = {
  name: '张三',
  sayName: () => {
    console.log(this.name);  // undefined(继承全局 this)
  }
};

obj.sayName();  // undefined

为什么? 对象字面量 {} 不是作用域,箭头函数的外层是全局作用域。

5.3 正确用法1:类中的箭头函数

ini 复制代码
// ✓ 正确:类属性箭头函数
class Person {
  constructor(name) {
    this.name = name;
    // 箭头函数捕获实例的 this
    this.sayName = () => {
      console.log(this.name);
    };
  }
}

const person = new Person('李四');
const fn = person.sayName;
fn();  // 李四(this 不丢失!)

原理: 箭头函数在构造函数内定义,捕获实例的 this,相当于:

javascript 复制代码
this.sayName = function() {
  console.log(this.name);
}.bind(this);

5.4 正确用法2:回调函数中

javascript 复制代码
const obj = {
  name: '王五',
  numbers: [1, 2, 3],
  
  printNumbers() {  // 普通方法
    // ✗ 错误:普通函数会丢失 this
    this.numbers.forEach(function(n) {
      console.log(this.name, n);  // undefined
    });
    
    // ✓ 正确:箭头函数继承 printNumbers 的 this
    this.numbers.forEach(n => {
      console.log(this.name, n);  // 王五 1, 王五 2, 王五 3
    });
  }
};

5.5 React 中的应用

scala 复制代码
// ❌ 方案1:需要手动 bind
class Button1 extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
    console.log(this.state);
  }
  
  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}

// ✓ 方案2:类属性箭头函数(推荐)
class Button2 extends React.Component {
  handleClick = () => {
    console.log(this.state);  // this 永远指向实例
  }
  
  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}

六、完整示例对比

6.1 对象方法

javascript 复制代码
// 普通函数
const obj1 = {
  name: '张三',
  sayName() {
    console.log(this.name);
  }
};
obj1.sayName();  // ✓ 张三
const fn1 = obj1.sayName;
fn1();  // ✗ undefined(this 丢失)

// 箭头函数
const obj2 = {
  name: '李四',
  sayName: () => {
    console.log(this.name);
  }
};
obj2.sayName();  // ✗ undefined(继承全局 this)

6.2 类方法

javascript 复制代码
class Person {
  constructor(name) {
    this.name = name;
  }
  
  // 普通方法
  method1() {
    console.log(this.name);
  }
  
  // 箭头函数
  method2 = () => {
    console.log(this.name);
  }
}

const person = new Person('王五');

// 普通方法
person.method1();  // ✓ 王五
const fn1 = person.method1;
fn1();  // ✗ undefined(this 丢失)

// 箭头函数
person.method2();  // ✓ 王五
const fn2 = person.method2;
fn2();  // ✓ 王五(this 不丢失!)

七、判断 this 指向的思维流程

kotlin 复制代码
1. 是否是 new 调用?
   → 是:this 指向新创建的对象

2. 是否使用了 call/apply/bind?
   → 是:this 指向指定的对象

3. 是否是对象方法调用(obj.method())?
   → 是:this 指向调用方法的对象

4. 是箭头函数吗?
   → 是:this 继承外层作用域的 this

5. 以上都不是?
   → 默认绑定:非严格模式指向全局对象,严格模式为 undefined

八、常见面试题

题目1:以下代码输出什么?

ini 复制代码
const obj = {
  name: '张三',
  sayName() {
    console.log(this.name);
  }
};

const fn = obj.sayName;
fn();

答案: undefined(隐式绑定丢失,this 指向全局)

题目2:以下代码输出什么?

ini 复制代码
const obj = {
  name: '李四',
  sayName: () => {
    console.log(this.name);
  }
};

obj.sayName();

答案: undefined(箭头函数继承全局 this,不是 obj)

题目3:如何让以下代码正常工作?

javascript 复制代码
const obj = {
  name: '王五',
  sayName() {
    console.log(this.name);
  }
};

setTimeout(obj.sayName, 1000);  // 需要输出"王五"

答案:

javascript 复制代码
// 方案1:bind
setTimeout(obj.sayName.bind(obj), 1000);

// 方案2:箭头函数包裹
setTimeout(() => obj.sayName(), 1000);

// 方案3:改用类
class Obj {
  constructor() {
    this.name = '王五';
    this.sayName = () => {
      console.log(this.name);
    };
  }
}
const obj = new Obj();
setTimeout(obj.sayName, 1000);

九、总结口诀

"谁调用指向谁,箭头函数看外层,new 和 bind 优先级最高"

关键要点

  1. this 是动态的:在调用时确定,不是定义时
  2. 优先级记清楚:new > 显式绑定 > 隐式绑定 > 默认绑定
  3. 箭头函数特殊:没有自己的 this,继承外层
  4. 对象字面量陷阱{} 不是作用域,箭头函数继承全局
  5. 类中用箭头:防止 this 丢失的最佳实践
  6. 现代优先:优先使用扩展运算符替代 apply

十、最佳实践建议

对象方法

javascript 复制代码
// ✓ 推荐:普通函数
const obj = {
  name: '张三',
  sayName() {
    console.log(this.name);
  }
};

类方法(需要传递)

ini 复制代码
// ✓ 推荐:箭头函数
class Person {
  name = '李四';
  sayName = () => {
    console.log(this.name);
  }
}

回调函数

ini 复制代码
// ✓ 推荐:箭头函数
obj.numbers.forEach(n => {
  console.log(this.name, n);
});

事件处理

scala 复制代码
// ✓ 推荐:类属性箭头函数
class Button extends React.Component {
  handleClick = () => {
    console.log(this.state);
  }
}
相关推荐
snow@li3 小时前
d3.js:学习积累
开发语言·前端·javascript
qyresearch_3 小时前
射频前端MMIC:5G时代的技术引擎与市场机遇
前端·5g
天蓝色的鱼鱼3 小时前
Next.js 渲染模式全解析:如何正确选择客户端与服务端渲染
前端·react.js·next.js
一枚前端小能手3 小时前
🚀 巨型列表渲染卡顿?这几个优化技巧让你的页面丝滑如德芙
前端·javascript
酷柚易汛智推官3 小时前
Electron技术深度解析:跨平台桌面开发的利器与挑战
前端·javascript·electron
llz_1123 小时前
第五周作业(JavaScript)
开发语言·前端·javascript
yannick_liu3 小时前
nuxt4 + nuxt-swiper实现官网全屏播放
前端
苏打水com3 小时前
JS基础事件处理与CSS常用属性全解析(附实战示例)
前端
W.Y.B.G4 小时前
JavaScript 计算闰年方法
开发语言·前端·javascript