前言
在JavaScript中,this关键字的行为常让开发者困惑。本文将通过示例解析this的绑定规则、箭头函数特性及call/apply/bind的使用区别。
一、this的四种调用方式
1. 普通函数调用(默认绑定)
var name = '全局名称';
function fn() {
console.log(this.name);
}
fn(); // 输出"全局名称"(非严格模式)
- 此时
this指向全局对象(浏览器中为window,Node.js为global) - 严格模式(
"use strict")下this为undefined
2. 对象方法调用(隐式绑定)
const obj = {
name: '对象名称',
fn: function() {
console.log(this.name);
}
}
obj.fn(); // 输出"对象名称"
this指向调用该方法的对象(即点符号前的对象,这里是obj对象)
3. 构造函数调用(new绑定)
function Person(name) {
this.name = name;
}
const p = new Person('构造的名称');
console.log(p.name); // 输出"构造的名称"
-
new操作符会:- 创建新对象
- 将
this绑定到新对象 - 执行构造函数代码
- 返回新对象
-
通过
new关键字调用构造函数时,this指向 新创建的实例对象
4. 事件处理函数调用
<button id="btn">点击</button>
<script>
btn.onclick = function() {
console.log(this); // 输出<button>元素
}
</script>
- 事件处理函数中,
this指向触发事件的DOM元素
优先级顺序为:
new绑定>显式绑定>隐式绑定>默认绑定箭头函数不遵循上述规则,它的
this在定义时确定,且无法通过call / apply / bind修改。
二、箭头函数的this特性
箭头函数没有自己的this,而是继承外层作用域的this值:
const obj = {
fn: function() {
setTimeout(() => {
console.log(this); // 继承fn的this(指向obj)
}, 100);
}
};
obj.fn();
关键区别:
| 特性 | 普通函数 | 箭头函数 |
|---|---|---|
this绑定 | 动态绑定 | 静态继承 |
| 可用call/bind | 是 | 否 |
| 构造函数 | 可 | 不可 |
三、call/apply/bind:显式绑定this
1. call & apply
const obj = { name: '指定对象' };
function demo(a, b) {
console.log(this.name, a, b);
}
// call:参数逐个传递
demo.call(obj, 1, 2); // 输出"指定对象 1 2"
// apply:参数数组传递
demo.apply(obj, [1, 2]); // 输出"指定对象 1 2"
2. bind:永久绑定
const boundFn = demo.bind(obj, 1); // bind返回的是一个函数,需要手动执行
boundFn(2); // 输出"指定对象 1 2"
- 返回新函数,
this永久绑定 - 支持参数预填充(柯里化,如果不了解柯里化的同学可以看柯里化)
三者的核心区别:
| 方法 | 执行时机 | 返回值 | 参数形式 |
|---|---|---|---|
| call() | 立即执行 | 函数结果 | 逗号分隔列表 |
| apply() | 立即执行 | 函数结果 | 数组 |
| bind() | 延迟执行 | 绑定后的函数 | 逗号分隔列表 |
四、函数的双重身份:callable 与 constructor
JavaScript函数具有双重能力:
function MyFunc() {
// 作为普通函数
if (!new.target) {
console.log('Called as function');
return;
}
// 作为构造函数
this.prop = 'value';
}
// 1. 函数调用([[call]])
MyFunc(); // 输出"Called as function"
// 2. 构造函数调用([[construct]])
const instance = new MyFunc();
console.log(instance.prop); // 输出"value"
关键机制:
-
函数内部通过
new.target检测调用方式 -
构造函数中:
- 自动创建新对象,绑定到
this - 隐式返回
this(除非显式返回对象)
- 自动创建新对象,绑定到
五、实战应用场景
1. 解决回调函数this丢失
// 传统方案:闭包保存this
const obj = {
value: 42,
init() {
const that = this;
button.addEventListener('click', function() {
console.log(that.value);
});
}
}
// 现代方案:箭头函数
init() {
button.addEventListener('click', () => {
console.log(this.value);
});
}
// 绑定方案:bind
init() {
button.addEventListener('click', this.handler.bind(this));
}
2. 类数组转为真实数组
function listArgs() {
// arguments是类数组对象
const arr = Array.prototype.slice.call(arguments);
arr.push('new');
return arr;
}
listArgs(1, 2); // [1, 2, 'new']
六、练习题
题目 1:默认绑定
function printThis() {
console.log(this);
}
printThis();
答案:window(严格模式为 undefined)
解析:函数直接调用时,非严格模式下 this 默认指向全局对象(浏览器中为 window)。
题目 2:隐式绑定(方法调用)
const user = {
name: "Alice",
greet() {
console.log(this.name);
}
};
user.greet();
答案:"Alice"
解析:通过对象调用方法时,this 指向调用该方法的对象(即 user)。
题目 3:隐式绑定丢失(回调函数)
const user = {
name: "Bob",
greet() {
console.log(this.name);
}
};
setTimeout(user.greet, 100);
答案:""(输出空字符串,实际指向 window.name)
解析:将方法作为回调传递时,this 会丢失并回退到默认绑定(window)。
修复:setTimeout(() => user.greet(), 100) 或 setTimeout(user.greet.bind(user), 100)。
题目 4:显式绑定(call)
function showName() {
console.log(this.name);
}
const obj = { name: "Charlie" };
showName.call(obj);
答案:"Charlie"
解析:call 强制将 this 绑定到第一个参数(此处为 obj)。
题目 5:箭头函数(词法作用域)
const person = {
name: "David",
greet: () => {
console.log(this.name);
}
};
person.greet();
答案:""(指向 window.name)
解析:箭头函数的 this 继承自外层作用域(此处是全局作用域),与调用方式无关。
题目 6:构造函数(new 绑定)
function Animal(name) {
this.name = name;
}
const cat = new Animal("Tom");
console.log(cat.name);
答案:"Tom"
解析:使用 new 调用构造函数时,this 指向新创建的对象实例(即 cat)。
总结规律:
- 独立调用 → 默认绑定(
window/undefined) - 对象方法调用 → 隐式绑定(指向对象)
- call/apply/bind → 显式绑定(指向参数)
- 箭头函数 → 继承外层
this(定义时确定) - new 调用 → 绑定到新实例
总结要点
-
this绑定优先级:new>call/apply/bind>方法调用>默认绑定 -
箭头函数解决
this穿透问题,但不可作为构造函数 -
根据场景选择绑定方法:
- 立即调用 →
call/apply - 延迟执行 →
bind
- 立即调用 →
-
函数的双重角色通过
new.target区分
理解this的绑定规则和函数执行机制,能有效避免常见的作用域陷阱,写出更健壮的JavaScript代码。