一、核心原则
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)
通过 call
、apply
、bind
手动指定 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
) - 严格模式:
this
为undefined
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 优先级最高"
关键要点
- this 是动态的:在调用时确定,不是定义时
- 优先级记清楚:new > 显式绑定 > 隐式绑定 > 默认绑定
- 箭头函数特殊:没有自己的 this,继承外层
- 对象字面量陷阱 :
{}
不是作用域,箭头函数继承全局 - 类中用箭头:防止 this 丢失的最佳实践
- 现代优先:优先使用扩展运算符替代 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);
}
}