深入理解 JavaScript 中的 this 关键字:从 "薛定谔的指向" 到 "精准操控"

73 阅读8分钟

如果你是 JavaScript 开发者,那你一定对 this 这个小家伙又爱又恨 —— 它就像个调皮的小精灵,在不同场景下总爱扮演不同角色,时而指向全局对象,时而黏上某个实例,偶尔还会玩失踪。今天咱们就来给这个小精灵画个 "行为手册",让它乖乖听你的话!

一、为什么需要 this?它到底解决了啥问题?

先问个扎心的问题:如果没有 this,咱们的代码会变成啥样?

想象一下,你要写个打招呼的功能,得这样显式传参:

function 喊名字(人) {​

 return 人.名字.toUpperCase();​

}​

function 打招呼(人) {​

 var 问候语 = '哈喽,我是' + 喊名字(人);​

 console.log(问候语);​

}​

var 我 = { 名字: '汤姆' };​

 打招呼(我); // 输出 "哈喽,我是汤姆"

这就像每次说话都得把对象举在手里 —— 多累啊!有了 this 之后,相当于给对象装了个 "隐形追踪器":

ScreenShot_2025-11-27_115445_886.png

  • 用 call 强制让 speek 的 this 指向 myname

  • speek 内部调用 identify.call(this) 时,this 已绑定 myname,因此 identify 也能访问 `myname.name

看到没?this 就像个智能代词,自动帮你找到 "说话的人",代码瞬间清爽了不少~

二、this 的五大种绑定规则:看懂这些就不会晕了

this 最让人头疼的点在于:它的值不是定义时 "写死" 的,而是调用时 "动态确定" 的。就像相亲时介绍人说 "这是我朋友"—— 至于是哪个朋友,得看介绍人是谁。

1. 默认绑定:单身函数的自由生活

当函数 "单身" 时(没有被任何对象调用),this 就会赖在全局对象家里

(浏览器里是 window,Node 里是 global)。

浏览器里是 window:

ScreenShot_2025-11-27_123018_167.png

Node 里是 global:

ScreenShot_2025-11-27_123734_785.png

这里再给大家扔一个经典的 "坑王" 例子,保证新手看了直呼上当:

var a = 1
function foo() {
  console.log(this.a);
}
function bar () {
  var a = 2
  foo()
}
bar()
// 猜猜输出啥?答案是 1!

ScreenShot_2025-11-27_150404_529.png 是不是有人觉得应该输出 2?毕竟 foo() 是在 bar() 里面调用的,而 bar() 里明明定义了 a=2。但咱别忘了 this 的核心规则:它只看函数自己是怎么被调用的,不看它在哪个函数里被调用

在这个例子里,foo() 依然是 "单身狗" 式的独立调用,没有任何对象给它当靠山,所以它的 this 还是指向全局对象。全局对象里的 a 是 1,那自然就输出 1 了。至于 bar() 里的 a=2,那只是 bar 函数的局部变量,跟 foo 里的 this.a 半毛钱关系都没有 —— 就像你邻居家的钱,再多也不是你的一样。

看看 这个例子:

function foo() {

 var a = 1;

 bar(this.a); // 这里的 this 正躺在全局对象沙发上

}

function bar(x) {

 console.log(x); // 输出 undefined(全局对象表示:我家没这玩意儿)

}

foo(); // 单身函数独自出门

如果全局对象家里有 a 这个成员(比如 var a = 2),那它就会乖乖输出 2—— 毕竟寄人篱下,总得听主人的嘛~

2. 隐式绑定:有对象罩着就是不一样

当函数成为某个对象的 "小弟"(作为方法被调用)时,this 就会抱紧这个对象的大腿。

function foo() {

 console.log(this.a); // 看看老大是谁

}

var obj = {

 a: 1,

 foo: foo // 收 foo 当小弟

};

obj.foo(); // 输出 1,this 死心塌地跟着 obj

但这里有个坑:多层对象调用时,this 只认 "直接上级"。就像公司里,你只听直属领导的,董事长再大也管不到你日常工作:

var obj2 = {

 a: 2,

 foo: obj // obj 带着小弟 foo 来投靠 obj2

};

obj2.foo.foo(); // 输出 1,this 依然认 obj 这个直接领导

(这种情况也叫 "隐式丢失",本质是函数最终还是被直接上级调用的)

3. 显式绑定:强行指定 "主人" 的操作

有时候我们想手动给函数找个 "主人",这时 callapplybind 这三兄弟就派上用场了 —— 它们就像 "介绍信",直接指定 this 该跟着谁。

  • call:带参数列表的介绍信,参数一个个传(适合人少的情况)

  • apply:带参数数组的介绍信,参数打包成数组传给函数(适合人多的情况)

  • bind:永久生效的介绍信,先绑定this和部分参数,返回一个 “半成品” 函数,想啥时候调用都行(一旦绑定,终身不变)

var 老板 = { 工资: 1000 };

function 算总工资(奖金, 补贴) {

 console.log(this.工资, 奖金 + 补贴);

}

// call 介绍:这是老板,给100奖金+200补贴

算总工资.call(老板, 100, 200); // 输出 "1000 300"

// apply 介绍:这是老板,奖金补贴都在数组里

var 福利 = [100, 200];

算总工资.apply(老板, 福利); // 输出 "1000 300"

// bind 绑定:以后这函数就跟老板混了,先预支200奖金+400补贴

const 长期工 = 算总工资.bind(老板, 200, 400);

长期工(300); // 输出 "1000 900"(200+400+300=900)

ScreenShot_2025-11-27_152852_561.png

注意 bind 是 "死忠粉"—— 就算你后来想给它换主人,它也坚决不从!

4. new 绑定:当 "爹" 的感觉

new 关键字调用函数时,JavaScript 会自动创建一个新对象,而 this 就成了这个新对象的 "爹",负责给它添置家产(属性和方法)。

// 先给孩子准备点祖传家产(原型链上的属性)
Person.prototype.say = 'hello'
function Person() {
  // var obj = {name: '宝宝'}      //1
  // Person.call(obj)   //2
  this.name = '宝宝'   // 3 
  // obj.__proto__ = Person.prototype  //4
  // return obj  // 5
}
let p = new Person() 
let p2 = new Person() 

但有个特殊情况:如果构造函数 "偏心",非要返回一个外部对象,那 new 生出来的孩子就会被抛弃:

function Person() {

 this.名字 = '宝宝';

 return { 财产: 10000 }; // 带个富二代回来

}

let 孩子 = new Person();

console.log(孩子); // 输出 { 财产: 10000 }(亲生的被丢了)

(只有返回引用类型时才会这样,返回基本类型比如 return 123 是无效的)

5. 箭头函数:天生 "认死理"

箭头函数是个特例 —— 它没有自己的 this,永远继承外层作用域的 this,而且一旦确定就改不了,堪称 JavaScript 界的 "认死理" 代表。

function foo() {

 // 箭头函数 bar 说:我就跟 foo 的 this 混了!

 var bar = () => {

  this.a = 2; // 这里的 this 就是 foo 的 this

 };

 bar();

}
var obj = {

 a: 1,

 baz: foo

};

obj.baz(); // 调用后,obj.a 变成 2

console.log(obj); // 输出 { a: 2, baz: \[Function: foo] }

就像孩子跟着家长姓 ——foothis 指向 obj(隐式绑定),箭头函数 bar 作为 "孩子",自然也跟着 obj 姓,所以修改 this.a 其实就是改 obj.a

三、全局作用域中的 this:根正苗红的 "大地主"

在全局作用域里,this 就是那个最大的 "地主"—— 浏览器里叫 window,Node 里叫 globalthis 一句话道破天机:

console.log(this); // 浏览器里输出 window,Node 里输出 global

所有没找到 "主人" 的变量和函数,最终都会成为这个大地主的财产(比如全局变量 var a = 1 其实等价于 this.a = 1)。

四、优先级排序:当多种规则冲突时听谁的?

当多种绑定规则同时出现时,总得有个 "老大" 吧?它们的优先级是这样的:

  1. new 绑定(毕竟 "生娃" 是头等大事)

  2. 显式绑定(call/apply/bind,手动指定的权力很大)

  3. 隐式绑定(跟着直接上级混)

  4. 默认绑定(实在没人要,才回全局老家)

而箭头函数根本不参与这场 "权力游戏"—— 它只认外层作用域的 this,管你什么 newcall 啊,全都不好使!

五、实战避坑指南:这些场景要注意

  1. 对象方法赋值后调用:方法被单独拿出来调用时,会变成默认绑定
var obj = {

 a: 1,

 foo: function() { console.log(this.a); }

};

var bar = obj.foo;

bar(); // undefined(变成默认绑定了)
  1. 箭头函数的正确使用场景:适合回调函数,不适合作为对象方法
var obj = {

 a: 1,

 foo: () => { console.log(this.a); } // 这里的 this 继承全局

};

obj.foo(); // undefined(坑!别这么用)

总结:this 其实很简单

记住一句话:this 就是 "函数调用时的上下文对象"。判断步骤可以分三步走:

  1. 函数是用 new 调用的吗?是 → this 指向新实例
  1. 函数是用 call/apply/bind 调用的吗?是 → this 指向指定对象
  1. 函数是被某个对象调用的吗?是 → this 指向那个对象
  1. 都不是?→ 全局对象(严格模式下是 undefined
  1. 是箭头函数?→ 继承外层作用域的 this