"
this
到底是谁?"------每个 JS 开发者都曾深夜发问的灵魂拷问。
在 JavaScript 中,this
是一个既强大又令人困惑的关键字。它不像 Java 或 C++ 那样在定义时就确定,而是在运行时 、根据调用方式 动态绑定。今天,我们就来揭开 this
的神秘面纱,从底层机制讲起,再手写实现 call
、apply
、bind
,让你彻底掌握这个"变色龙"!
🔍 一、this
到底是谁?------决定 this
的四大法则
this
的值不是在函数定义时决定的,而是在函数被调用时决定的 。绝大多数情况下,函数的调用方式决定了 this
的指向。
✅ 法则 1:全局环境中的 this
无论是否开启严格模式,全局环境下的 this
都指向全局对象。
- 浏览器中:
window
- Node.js 中:
global
js
console.log(this === window); // true (浏览器)
✅ 法则 2:直接调用函数(独立函数调用)
这是最容易出错的地方!
js
function foo() {
console.log(this);
}
foo(); // 非严格模式:window;严格模式:undefined
- 非严格模式 :
this
指向全局对象(window
) - 严格模式 :
this
为undefined
⚠️ 注意:
this
不能在执行期间被赋值,它是自动绑定的。
✅ 法则 3:对象方法调用 ------ "谁调用,this
就是谁"
js
const obj = {
name: 'Alice',
greet() {
console.log(this.name); // this 指向 obj
}
};
obj.greet(); // 'Alice'
即使函数被赋值给变量,调用时 this
依然取决于调用者:
js
const fn = obj.greet;
fn(); // undefined (严格模式) 或 window.name (非严格)
// 因为此时是直接调用,不是 obj 调用!
✅ 法则 4:构造函数调用(new
)
使用 new
调用函数时,this
指向新创建的实例对象。
js
function Person(name) {
this.name = name; // this 指向新创建的 person 实例
}
const p = new Person('Bob');
🧰 二、掌控 "this" 的三大法宝
既然this
的指向如此善变,有没有办法让它听话呢?JavaScript 提供了三种方法来明确指定this
的值:call、apply 和 bind。
call:逐个传递参数
call
方法允许你指定函数执行时的this
值,并逐个传递参数:
javascript
function introduce(age, hobby) {
console.log(`我是${this.name},今年${age}岁,喜欢${hobby}`);
}
const person = { name: "李四" };
introduce.call(person, 25, "打篮球"); // 我是李四,今年25岁,喜欢打篮球
apply:数组传递参数
apply
与call
的作用相同,唯一的区别是apply
接受一个数组作为参数:
javascript
introduce.apply(person, [25, "打篮球"]); // 我是李四,今年25岁,喜欢打篮球
在 ES6 的扩展运算符出现之前,apply
在处理数组参数时特别有用,比如求数组中的最大值:
javascript
const numbers = [1, 3, 2, 5, 4];
console.log(Math.max.apply(null, numbers)); // 5
bind:永久绑定
bind
方法会创建一个新函数,这个新函数的this
被永久绑定到指定的值,无论之后如何调用:
javascript
const boundIntroduce = introduce.bind(person);
boundIntroduce(25, "打篮球"); // 我是李四,今年25岁,喜欢打篮球
// 即使作为其他对象的方法调用,this依然不变
const anotherPerson = { name: "王五" };
anotherPerson.introduce = boundIntroduce;
anotherPerson.introduce(30, "游泳"); // 我是李四,今年30岁,喜欢游泳
bind
还可以实现参数柯里化(Currying),即预先设置部分参数:
javascript
const introduceWithAge = introduce.bind(person, 25);
introduceWithAge("打篮球"); // 我是李四,今年25岁,喜欢打篮球
🔧 三、底层实现:亲手打造 call、apply 和 bind
理解这些方法的底层实现,能帮助我们更深入地掌握它们的工作原理。
手写 call 方法
javascript
Function.prototype.myCall = function(context) {
// 检查调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("调用者必须是函数");
}
// 获取参数(第一个参数是context,剩下的是函数参数)
const args = [...arguments].slice(1);
let result;
// 如果未提供context,默认指向全局对象
context = context || window;
// 使用Symbol避免属性名冲突
const fnSymbol = Symbol('fn');
context[fnSymbol] = this;
// 调用函数
result = context[fnSymbol](...args);
// 清理痕迹
delete context[fnSymbol];
return result;
};
这里使用Symbol
作为属性名,是为了避免覆盖context
中可能已存在的fn
属性,这是一种更安全的实现方式。
手写 apply 方法
javascript
Function.prototype.myApply = function(context) {
if (typeof this !== "function") {
throw new TypeError("调用者必须是函数");
}
let result;
context = context || window;
const fnSymbol = Symbol('fn');
context[fnSymbol] = this;
// apply接受数组作为参数
if (arguments[1]) {
result = context[fnSymbol](...arguments[1]);
} else {
result = context[fnSymbol]();
}
delete context[fnSymbol];
return result;
};
apply
与call
的主要区别在于参数的处理方式,apply
接收一个数组作为函数参数。
手写 bind 方法
javascript
Function.prototype.myBind = function(context) {
if (typeof this !== "function") {
throw new TypeError("调用者必须是函数");
}
// 保存原函数和初始参数
const fn = this;
const args = [...arguments].slice(1);
// 返回一个新函数
function boundFn() {
// 当新函数被当作构造函数调用时,this应该指向实例
// 否则指向bind时指定的context
return fn.apply(
this instanceof boundFn ? this : context,
args.concat(...arguments)
);
}
// 维护原型链
boundFn.prototype = Object.create(fn.prototype);
return boundFn;
};
bind
的实现稍微复杂一些,主要是要处理新函数被当作构造函数调用的情况。这时this
应该指向新创建的实例,而不是bind
时指定的context
。
⚡ 四、箭头函数:打破常规的 "this"
ES6 引入的箭头函数带来了一种全新的this
绑定方式:它不会创建自己的this
,而是继承自外层作用域的this
。
javascript
const obj = {
name: "箭头函数",
normalFn: function() {
console.log(this.name); // 箭头函数
const arrowFn = () => {
console.log(this.name); // 箭头函数(继承自normalFn的this)
};
arrowFn();
}
};
obj.normalFn();
箭头函数的this
一旦确定就不会改变,即使使用call
、apply
或bind
也无法修改:
javascript
const arrowFn = () => {
console.log(this);
};
arrowFn.call({ name: "测试" }); // 仍然指向全局对象
这使得箭头函数非常适合作为回调函数,尤其是在处理异步操作时,可以避免this
指向混乱的问题。
🎯 总结:掌握 "this" 的核心原则
this
的指向由函数被调用的方式决定,而非定义时的环境- 全局环境中,
this
指向全局对象 - 函数直接调用时,
this
在非严格模式下指向全局对象,严格模式下为undefined
- 作为对象方法调用时,
this
指向调用该方法的对象 call
、apply
可以立即调用函数并指定this
bind
会创建一个新函数,永久绑定this
- 箭头函数的
this
继承自外层作用域,且无法被修改