在前面的文章中,我们深入了解了 JavaScript 执行栈和执行上下文 以及 作用域和作用域链。今天我们来聊聊 JavaScript 中另一个重要概念——this 指向。
还记得我们在执行上下文文章中提到的吗?当 JavaScript 创建执行上下文时会做三件事:
- 生成变量对象(VO)
- 确定作用域(Scope)
- 确定 this 的值
今天我们就来深入了解第三点——this 的指向机制。
什么是 this?
想象一下现实生活中的场景:
- 在家里,你说"我的房间",指的是你自己的房间
- 在公司,你说"我的办公桌",指的是你在公司的办公桌
- 在朋友家,你说"我坐这里",指的是你当前所在的位置
this 就像是代码中的"我",它总是指向当前的执行环境。但是,this 的指向不是在写代码时确定的,而是在函数被调用时确定的。
this 的核心特点
- 动态绑定:this 的值在函数调用时才确定
- 总是对象:this 总是指向一个对象(严格模式下可能是 undefined)
- 调用方式决定指向:不同的调用方式会导致不同的 this 指向
this 指向的五种情况
简单来说,this 的指向遵循以下规律:
- 默认绑定:独立函数调用时,this 指向全局对象(严格模式下为 undefined)
- 隐式绑定:作为对象方法调用时,this 指向调用对象
- 显式绑定:通过 call/apply/bind 调用时,this 指向指定对象
- new 绑定:通过 new 调用时,this 指向新创建的对象
- 箭头函数:this 继承外层作用域的 this(静态绑定)
接下来我们通过实际例子来理解这些规律。
1. 默认绑定 - 独立函数调用
默认绑定是最简单的情况
非严格模式下的默认绑定
function sayHello() {
console.log('Hello, I am', this);
}
// 独立函数调用
sayHello(); // Hello, I am Window (浏览器) 或 Global (Node.js)
当函数独立调用时(没有任何前缀),this 会指向全局对象:
- 浏览器环境:
window - Node.js 环境:
global
严格模式下的默认绑定
function sayHelloStrict() {
'use strict';
console.log('Hello, I am', this);
}
sayHelloStrict(); // Hello, I am undefined
在严格模式下,独立函数调用时 this 为 undefined,这样设计是为了避免意外修改全局对象。
容易混淆的情况
这里有个很容易踩坑的地方:
const user = {
name: '小明',
greet: function() {
console.log('你好,我是', this.name);
}
};
// 情况1:作为方法调用
user.greet(); // 你好,我是 小明
// 情况2:赋值后独立调用
const greetFunc = user.greet;
greetFunc(); // 你好,我是 undefined (严格模式) 或 你好,我是 (非严格模式)
关键理解:虽然 greetFunc 和 user.greet 是同一个函数,但调用方式不同,this 指向就不同!
user.greet():通过对象调用,this 指向 usergreetFunc():独立调用,this 指向全局对象或 undefined
2. 隐式绑定 - 对象方法调用
隐式绑定就像你在某个场所时,"我"自然指向你在那个场所的身份。
基本的隐式绑定
const student = {
name: '张三',
age: 18,
introduce: function() {
console.log(`我叫${this.name},今年${this.age}岁`);
}
};
student.introduce(); // 我叫张三,今年18岁
当函数作为对象的方法被调用时,this 指向调用该方法的对象。
多层对象的隐式绑定
当对象有多层嵌套时,this 指向最后一层调用它的对象:
const company = {
name: '科技公司',
department: {
name: '开发部',
team: {
name: '前端组',
introduce: function() {
console.log(`我是${this.name}`);
}
}
}
};
company.department.team.introduce(); // 我是前端组
记住:无论调用链有多长,this 总是指向最后一个点前面的对象。
隐式绑定的丢失
这是一个非常容易出错的地方!让我们看几个例子:
情况1:赋值导致绑定丢失
const person = {
name: '李四',
sayName: function() {
console.log('我的名字是', this.name);
}
};
// 直接调用 - this 指向 person
person.sayName(); // 我的名字是 李四
// 赋值后调用 - this 绑定丢失
const getName = person.sayName;
getName(); // 我的名字是 undefined
情况2:作为参数传递时绑定丢失
const user = {
name: '王五',
greet: function() {
console.log('Hello,', this.name);
}
};
function executeCallback(callback) {
callback(); // 这里是独立调用!
}
executeCallback(user.greet); // Hello, undefined
情况3:复杂的调用链
const obj1 = {
name: 'obj1',
getName: function() {
return this.name;
}
};
const obj2 = {
name: 'obj2',
getObj1Name: function() {
return obj1.getName(); // 注意:这里是 obj1.getName()
}
};
const obj3 = {
name: 'obj3',
getNameIndirect: function() {
const fn = obj1.getName; // 赋值给变量
return fn(); // 独立调用
}
};
console.log(obj1.getName()); // obj1 - 直接调用
console.log(obj2.getObj1Name()); // obj1 - obj1.getName() 中 this 指向 obj1
console.log(obj3.getNameIndirect()); // undefined - fn() 是独立调用
关键理解:
obj1.getName():this 指向 obj1fn = obj1.getName; fn():this 指向全局对象或 undefined
3. DOM 事件中的 this
在 DOM 事件处理中,this 有特殊的指向规律。
事件处理函数中的 this
当使用传统的函数作为事件处理器时,this 指向绑定事件的元素:
<button id="myButton">点击我</button>
<ul id="colorList">
<li>红色</li>
<li>蓝色</li>
<li>绿色</li>
</ul>
const button = document.getElementById('myButton');
const colorList = document.getElementById('colorList');
// 按钮点击事件
button.addEventListener('click', function() {
console.log('按钮被点击了');
console.log('this 指向:', this); // this 指向 button 元素
console.log('按钮文本:', this.textContent); // 点击我
});
// 列表点击事件(事件委托)
colorList.addEventListener('click', function(event) {
console.log('this 指向:', this); // this 指向 ul 元素
console.log('触发事件的元素:', event.target); // 实际被点击的 li 元素
if (event.target.tagName === 'LI') {
console.log('你选择了:', event.target.textContent);
}
});
this vs event.target
这是一个重要的区别:
- this:指向绑定事件的元素
- event.target:指向实际触发事件的元素
// 当点击 li 元素时:
// this → ul 元素(事件绑定在 ul 上)
// event.target → li 元素(实际被点击的元素)
事件处理中的常见问题
在事件处理函数内部定义其他函数时,要注意 this 的指向:
const button = document.getElementById('myButton');
button.addEventListener('click', function() {
console.log('外层 this:', this); // button 元素
// 问题:内层函数的 this 指向
const innerFunction = function() {
console.log('内层 this:', this); // Window 或 undefined(严格模式)
};
innerFunction();
// 解决方案1:保存 this 引用
const self = this;
const innerFunction2 = function() {
console.log('内层 this:', self); // button 元素
};
innerFunction2();
// 解决方案2:使用箭头函数(后面会详细讲)
const innerFunction3 = () => {
console.log('箭头函数 this:', this); // button 元素
};
innerFunction3();
});
4. 显式绑定 - 改变 this 指向
有时候我们需要主动改变函数内部 this 的指向,JavaScript 提供了三个方法:call、apply 和 bind。
call 方法
call 方法可以指定函数执行时 this 的指向,并立即执行函数:
// 基本语法:func.call(thisArg, arg1, arg2, ...)
const person1 = {
name: '张三',
age: 25
};
const person2 = {
name: '李四',
age: 30
};
function introduce(hobby, city) {
console.log(`我是${this.name},今年${this.age}岁,喜欢${hobby},住在${city}`);
}
// 使用 call 改变 this 指向
introduce.call(person1, '编程', '北京');
// 输出:我是张三,今年25岁,喜欢编程,住在北京
introduce.call(person2, '音乐', '上海');
// 输出:我是李四,今年30岁,喜欢音乐,住在上海
apply 方法
apply 方法与 call 类似,区别在于参数传递方式:
// 基本语法:func.apply(thisArg, [arg1, arg2, ...])
// 使用 apply,参数以数组形式传递
introduce.apply(person1, ['编程', '北京']);
// 输出:我是张三,今年25岁,喜欢编程,住在北京
// apply 的实用场景:数组操作
const numbers = [5, 6, 2, 3, 7];
// 找出数组中的最大值
const max = Math.max.apply(null, numbers);
console.log('最大值:', max); // 7
// 找出数组中的最小值
const min = Math.min.apply(null, numbers);
console.log('最小值:', min); // 2
// 数组合并
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push.apply(arr1, arr2);
console.log(arr1); // [1, 2, 3, 4, 5, 6]
bind 方法
bind 方法不会立即执行函数,而是返回一个新的函数,新函数的 this 被永久绑定:
// 基本语法:func.bind(thisArg, arg1, arg2, ...)
const person = {
name: '王五',
greet: function() {
console.log(`你好,我是${this.name}`);
}
};
// 直接调用
person.greet(); // 你好,我是王五
// 赋值给变量会丢失 this
const greetFunc = person.greet;
greetFunc(); // 你好,我是undefined(或报错)
// 使用 bind 绑定 this
const boundGreet = person.greet.bind(person);
boundGreet(); // 你好,我是王五
实际应用场景
1. 事件处理中保持 this
class Counter {
constructor() {
this.count = 0;
this.element = document.getElementById('counter');
this.button = document.getElementById('increment');
// 错误的方式:this 会丢失
// this.button.addEventListener('click', this.increment);
// 正确的方式:使用 bind 绑定 this
this.button.addEventListener('click', this.increment.bind(this));
}
increment() {
this.count++;
this.element.textContent = this.count;
console.log('当前计数:', this.count);
}
}
const counter = new Counter();
2. 回调函数中的 this 问题
const user = {
name: '小明',
hobbies: ['读书', '游泳', '编程'],
showHobbies() {
console.log(`${this.name}的爱好:`);
// 问题:forEach 回调中的 this 指向 window
this.hobbies.forEach(function(hobby) {
console.log(`${this.name}喜欢${hobby}`); // this.name 是 undefined
});
console.log('\n修复后:');
// 解决方案1:使用 bind
this.hobbies.forEach(function(hobby) {
console.log(`${this.name}喜欢${hobby}`);
}.bind(this));
// 解决方案2:使用箭头函数(推荐)
this.hobbies.forEach((hobby) => {
console.log(`${this.name}喜欢${hobby}`);
});
}
};
user.showHobbies();
3. 函数借用
// 借用数组方法处理类数组对象
function convertToArray() {
// arguments 是类数组对象,没有数组方法
console.log('arguments:', arguments);
console.log('是否为数组:', Array.isArray(arguments)); // false
// 借用数组的 slice 方法转换为真正的数组
const realArray = Array.prototype.slice.call(arguments);
console.log('转换后:', realArray);
console.log('是否为数组:', Array.isArray(realArray)); // true
return realArray;
}
convertToArray(1, 2, 3, 4, 5);
call、apply、bind 的区别总结
| 方法 | 执行时机 | 参数传递 | 返回值 | 使用场景 | |------|----------|----------|--------|-----------|| | call | 立即执行 | 逐个传递 | 函数执行结果 | 临时改变this并执行 | | apply | 立即执行 | 数组传递 | 函数执行结果 | 参数为数组时使用 | | bind | 不执行 | 逐个传递 | 新函数 | 永久绑定this |
function test(a, b) {
console.log(this.name, a, b);
}
const obj = { name: '测试对象' };
// call:立即执行,参数逐个传递
test.call(obj, 1, 2); // 测试对象 1 2
// apply:立即执行,参数数组传递
test.apply(obj, [1, 2]); // 测试对象 1 2
// bind:返回新函数,不立即执行
const boundTest = test.bind(obj, 1, 2);
boundTest(); // 测试对象 1 2
5. 箭头函数中的 this
箭头函数是 ES6 引入的新特性,它在 this 绑定方面有着独特的行为。
箭头函数 this 的特点
核心特点:箭头函数没有自己的 this,它会继承外层作用域的 this。
// 传统函数 vs 箭头函数
const obj = {
name: '测试对象',
// 传统函数
traditionalMethod: function() {
console.log('传统函数 this:', this.name); // 测试对象
setTimeout(function() {
console.log('传统函数内部 this:', this.name); // undefined(this 指向 window)
}, 1000);
},
// 箭头函数
arrowMethod: function() {
console.log('外层 this:', this.name); // 测试对象
setTimeout(() => {
console.log('箭头函数内部 this:', this.name); // 测试对象
}, 1000);
}
};
obj.traditionalMethod();
obj.arrowMethod();
实际应用场景
1. 定时器中的 this
class Timer {
constructor() {
this.seconds = 0;
this.minutes = 0;
}
start() {
// 使用箭头函数,this 指向 Timer 实例
setInterval(() => {
this.seconds++;
if (this.seconds >= 60) {
this.minutes++;
this.seconds = 0;
}
console.log(`${this.minutes}:${this.seconds.toString().padStart(2, '0')}`);
}, 1000);
// 如果使用传统函数,this 会指向 window
// setInterval(function() {
// this.seconds++; // 报错!this 不是 Timer 实例
// }, 1000);
}
}
const timer = new Timer();
timer.start();
2. 事件处理中的 this
class ButtonHandler {
constructor(buttonId) {
this.clickCount = 0;
this.button = document.getElementById(buttonId);
this.init();
}
init() {
// 箭头函数保持 this 指向
this.button.addEventListener('click', () => {
this.handleClick();
});
// 传统函数需要 bind
// this.button.addEventListener('click', this.handleClick.bind(this));
}
handleClick() {
this.clickCount++;
console.log(`按钮被点击了 ${this.clickCount} 次`);
}
}
const handler = new ButtonHandler('myButton');
3. 数组方法中的 this
const processor = {
prefix: '处理:',
processItems(items) {
// 箭头函数中的 this 指向 processor
return items.map(item => {
return this.prefix + item;
});
// 传统函数需要额外处理
// return items.map(function(item) {
// return this.prefix + item;
// }.bind(this));
}
};
const result = processor.processItems(['苹果', '香蕉', '橙子']);
console.log(result); // ['处理:苹果', '处理:香蕉', '处理:橙子']
箭头函数的限制
1. 无法通过 call、apply、bind 改变 this
const obj1 = { name: '对象1' };
const obj2 = { name: '对象2' };
// 传统函数可以改变 this
const traditionalFunc = function() {
console.log('传统函数 this:', this.name);
};
traditionalFunc.call(obj1); // 传统函数 this: 对象1
traditionalFunc.call(obj2); // 传统函数 this: 对象2
// 箭头函数无法改变 this
const arrowFunc = () => {
console.log('箭头函数 this:', this.name);
};
arrowFunc.call(obj1); // 箭头函数 this: undefined(this 指向全局)
arrowFunc.call(obj2); // 箭头函数 this: undefined(this 指向全局)
2. 不能作为构造函数
// 传统函数可以作为构造函数
function TraditionalConstructor(name) {
this.name = name;
}
const instance1 = new TraditionalConstructor('测试'); // 正常工作
// 箭头函数不能作为构造函数
const ArrowConstructor = (name) => {
this.name = name;
};
// const instance2 = new ArrowConstructor('测试'); // 报错!
嵌套箭头函数的 this
const nested = {
name: '嵌套对象',
method: function() {
console.log('外层 this:', this.name); // 嵌套对象
const inner = () => {
console.log('第一层箭头函数 this:', this.name); // 嵌套对象
const deeper = () => {
console.log('第二层箭头函数 this:', this.name); // 嵌套对象
setTimeout(() => {
console.log('定时器中的箭头函数 this:', this.name); // 嵌套对象
}, 100);
};
deeper();
};
inner();
}
};
nested.method();
最佳实践建议
- 需要动态 this 时使用传统函数:对象方法、事件处理器、构造函数
- 需要继承外层 this 时使用箭头函数:回调函数、定时器、数组方法
- 简化代码时使用箭头函数:简短的函数表达式
// 推荐的使用方式
const api = {
baseUrl: 'https://api.example.com',
// 对象方法使用传统函数
request: function(endpoint) {
return fetch(this.baseUrl + endpoint)
.then(response => response.json()) // 箭头函数用于回调
.then(data => {
return this.processData(data); // 箭头函数中的 this 指向 api
})
.catch(error => {
console.error('请求失败:', error);
});
},
// 数据处理方法
processData: function(data) {
return data.map(item => ({ // 箭头函数用于数组处理
...item,
processed: true
}));
}
};
6. new 绑定 - 构造函数调用
当使用 new 关键字调用函数时,会发生特殊的 this 绑定。
new 操作符的工作原理
当使用 new 调用函数时,JavaScript 会执行以下步骤:
- 创建一个新的空对象
- 将这个空对象的原型指向构造函数的 prototype
- 将构造函数的 this 绑定到这个新对象
- 执行构造函数代码
- 如果构造函数没有返回对象,则返回这个新对象
function Person(name, age) {
this.name = name;
this.age = age;
this.introduce = function() {
console.log(`我是${this.name},今年${this.age}岁`);
};
}
// 使用 new 创建实例
const person1 = new Person('张三', 25);
const person2 = new Person('李四', 30);
person1.introduce(); // 我是张三,今年25岁
person2.introduce(); // 我是李四,今年30岁
console.log(person1.name); // 张三
console.log(person2.name); // 李四
new 绑定的优先级
new 绑定的优先级非常高,甚至可以覆盖 bind 绑定:
function Person(name) {
this.name = name;
}
const obj = { name: '对象' };
// 使用 bind 绑定 this
const BoundPerson = Person.bind(obj);
BoundPerson('测试'); // 此时 obj.name 变为 '测试'
console.log(obj.name); // 测试
// 使用 new 调用 bind 后的函数
const instance = new BoundPerson('实例');
console.log(instance.name); // 实例(new 的优先级更高)
console.log(obj.name); // 测试(obj 没有被影响)
7. this 绑定的优先级
当多种绑定规则同时出现时,需要了解它们的优先级:
优先级排序(从高到低)
- new 绑定 > 显式绑定(bind) > 显式绑定(call/apply) > 隐式绑定 > 默认绑定
- 箭头函数比较特殊,它没有自己的 this,无法被任何方式改变
function test() {
console.log(this.name);
}
const obj1 = { name: '对象1' };
const obj2 = { name: '对象2' };
// 1. 默认绑定
test(); // undefined(严格模式)
// 2. 隐式绑定
obj1.test = test;
obj1.test(); // 对象1
// 3. 显式绑定覆盖隐式绑定
obj1.test.call(obj2); // 对象2
// 4. bind 绑定
const boundTest = test.bind(obj1);
boundTest(); // 对象1
boundTest.call(obj2); // 对象1(bind 无法被 call 覆盖)
// 5. new 绑定覆盖 bind 绑定
function Constructor(name) {
this.name = name;
}
const BoundConstructor = Constructor.bind(obj1);
const instance = new BoundConstructor('新实例');
console.log(instance.name); // 新实例
console.log(obj1.name); // undefined(obj1 没有被影响)
8. 常见陷阱和解决方案
1:方法作为回调函数
class Timer {
constructor() {
this.time = 0;
}
tick() {
this.time++;
console.log('时间:', this.time);
}
start() {
// 陷阱:this 绑定丢失
// setInterval(this.tick, 1000); // 报错
// 解决方案1:箭头函数
setInterval(() => this.tick(), 1000);
// 解决方案2:bind
// setInterval(this.tick.bind(this), 1000);
}
}
const timer = new Timer();
timer.start();
2:事件处理中的 this
class ButtonController {
constructor(buttonId) {
this.clickCount = 0;
this.button = document.getElementById(buttonId);
this.init();
}
init() {
// 陷阱:事件处理函数中的 this 指向 button 元素
// this.button.addEventListener('click', this.handleClick); // 错误
// 解决方案1:箭头函数
this.button.addEventListener('click', (e) => this.handleClick(e));
// 解决方案2:bind
// this.button.addEventListener('click', this.handleClick.bind(this));
}
handleClick(event) {
this.clickCount++;
console.log(`点击次数: ${this.clickCount}`);
}
}
3:对象方法的解构赋值
const user = {
name: '用户',
greet() {
console.log(`你好,我是${this.name}`);
}
};
// 陷阱:解构赋值导致 this 丢失
const { greet } = user;
greet(); // 你好,我是undefined
// 解决方案:保持方法调用形式
user.greet(); // 你好,我是用户
// 或者使用 bind
const boundGreet = user.greet.bind(user);
boundGreet(); // 你好,我是用户
总结
通过本文的学习,我们深入了解了 JavaScript 中 this 的各种绑定规则。让我们回顾一下核心要点:
this 绑定的七种情况
- 默认绑定:独立函数调用时,this 指向全局对象(非严格模式)或 undefined(严格模式)
- 隐式绑定:作为对象方法调用时,this 指向调用对象
- DOM 事件绑定:事件处理函数中,this 指向绑定事件的元素
- 显式绑定:通过 call、apply、bind 主动指定 this 的指向
- 箭头函数:没有自己的 this,继承外层作用域的 this
- new 绑定:构造函数调用时,this 指向新创建的对象
- 严格模式影响:严格模式下默认绑定为 undefined
优先级规则
当多种绑定规则同时出现时,优先级从高到低为:
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
箭头函数的 this 无法被改变,始终继承定义时的外层作用域。