快 2026 年了,谁还在为 this 挠头?看完这篇让你彻底从懵圈到精通
前言
各位 前端er 们,谁还没被 JavaScript 里的 this 虐过?这玩意简直就是编程界的 “变脸大师”,翻脸比孙猴子还快。一会儿是全局对象,一会儿是某个实例,一会儿又跟着调用场景改头换面,这不就是活生生的 “百变马丁” 吗?写代码时总被它搞得晕头转向,调试半天就因为 this 指向不对,真有种 “一杯茶一包烟,一个this改一天” 的崩溃感。如果你还搞不懂 JavaScript 里面的 this,那这篇将让你搞定原理并且拿捏用法,把那些绕人的绑定规则掰扯得明明白白!
一、为什么要有 this?—— 让代码 “优雅到飞起”
想象一下,你写了个函数,想在不同对象上复用它。要是没有this,就得每次手动传对象参数,想想都麻烦!this就像个 “智能代词”,悄咪咪地帮我们传递对象引用。通俗来说就是 this 提供了一种更优雅的方式来隐式的传递一个对象引用,可以让代码更简洁易于复用。
比如下面这两种写法:
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greet = 'hello, I am ' + identify(context);
console.log(greet);
}
var myname = {
name: 'henry'
}
speak(myname);
我们定义 identify 函数接收对象参数,返回其 name 大写值;speak 函数接收对象,调用 identify 拼接问候语并打印;最后创建含 name 的 myname 对象,传给 speak 执行,输出 hello, I am HENRY。核心是手动传递对象参数实现复用。
输出结果:
![]()
结果没错,henry 确确实实大写了,但每次都要手动传参你自己不嫌麻烦吗?这时候我们请出大名鼎鼎的--this!
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greet = 'hello, I am ' + identify.call(this);
console.log(greet);
}
var myname = {
name: 'henry'
}
speak.call(myname);
![]()
诶,你会发现,我2个函数都没有传参,但是都输出了结果。这里有2个关键点:
- 用
call强制让speak的this指向myname; -
speak内部调用identify.call(this)时,this已绑定myname,因此identify也能访问myname.name。
至于call是个啥,往下看吧,嘿嘿!
二、this 用在哪?—— “代词” 的舞台
this就像 “变色龙”,在不同场景下指代不同的角色:
-
全局作用域:在浏览器里,
this就等于window(就像全局舞台的 “C 位”)。比如你直接写console.log(this),打印的就是window对象。
比如我在 Google Chrome 上面写console.log(this):
![]()
但在node.js里,打印出来的是global:
![]()
-
函数作用域:这就是
this的 “主战场”了,在这它的身份变化可多了,咱们接着往下看。
三、this 的绑定规则 —— 给 this “定规矩”
1. 默认绑定 —— “自由散漫” 的 this
当函数被独立调用时,this就指向window(严格模式下是undefined)。就像你一个人逛街,没对象陪~
var a = 1;
function foo() {
console.log(this.a);
}
function bar() {
var a = 2;
foo();
}
bar();
灵魂拷问:输出结果是多少?肯定有人说2,而且还不少!你不服了,这调用bar(),然后里面在调用foo(), 不应该根据变量提升先找bar()里面的a = 2吗?说明前面还没看懂嘻嘻,当函数被独立调用--就一个foo(),它就是指向全局,不管那些杂七杂八的,你就是一个人逛街,没有对象陪,所以答案就是全局的a = 1。
![]()
最后就是打印出了1,这就是函数的独立调用。
2. 隐式绑定 —— “依附对象” 的 this
当函数被上下文对象调用时,this就绑定到这个对象上。比如:
function foo() {
console.log(this);
}
var a = 1;
var obj = {
foo: foo
}
obj.foo();
好,最后的调用obj.foo()。首先它不是单独调用,那么就要用到上下文对象调用,咱们一句话说清核心:obj.foo() 是通过对象 obj 调用函数 foo,所以 foo 里的 this 直接绑定到 obj,最终打印 obj 整个对象。
![]()
3. 隐式丢失 —— “层层剥离” 的 this
当函数被多层对象调用时,this会指向最近的那个对象。是不是有点像小时候玩的游戏🎮 “击鼓传花”,传到最后花落谁家?
好,那看一个例子:
function foo() {
console.log(this.a);
}
var obj = {
a: 1,
foo: foo
}
var obj2 = {
a: 2,
foo: obj
}
obj2.foo.foo();
OK,懵了⊙▃⊙吧。怎么回事连续调用2次,最后输出的到底是obj里的还是obj2里的?答案是obj!
关键误区提醒:
不要以为 obj2 在前面,this 就指向 obj2!
this 只看 「函数执行时的直接调用者」,和外层嵌套的对象(obj2)无关。如果想让 this 指向 obj2,需要让 obj2 直接调用 foo(比如 obj2.foo = foo; obj2.foo())。
就好比我们英语的 就近原则,离谁近就指向谁!
输出结果:
![]()
4. 显式绑定 —— “强行指定” 的 this(call、apply、bind 三位好室友登场!)
咱们可以把这三个方法想象成你的三个 “热心室友”:
-
call 室友:急性子,直接帮函数把
this绑定到目标对象,参数一个个传。 - apply 室友:爱偷懒,参数打包成数组传给函数。
-
bind 室友:慢性子,先绑定
this和部分参数,返回一个 “半成品” 函数,想啥时候调用都行。
var obj = {
a: 1
}
function foo(x, y) {
console.log(this.a, x + y);
}
foo.call(obj, 1, 2); // call室友出马
foo.apply(obj, [1, 2]); // apply室友偷懒
const bar = foo.bind(obj, 1, 2);
bar(); // bind室友慢性子
-
foo.call(obj, 1, 2):call 室友 “急性子”,直接把 foo 的 this 绑定到 obj,再逐个传入参数 1 和 2 → 打印 obj 的 a(1)和 1+2(3),输出:1 3; -
foo.apply(obj, [1, 2]):apply 室友 “爱偷懒”,同样绑定 this 到 obj,但参数要打包成数组 [1,2] 传入 → 结果和 call 一致,输出:1 3; -
const bar = foo.bind(obj, 2, 3); bar():bind 室友 “慢性子”,先绑定 this 到 obj、预传参数 1 和 2,返回 “半成品” 函数 bar,调用 bar 时才执行 → 打印 obj 的 a(1)和 1+2(3),输出:1 3。
结果和我们分析的一样:
![]()
5. new 绑定 —— “实例专属” 的 this
new关键字就像个 “专属定制工厂”,专门用构造函数为新对象 “量身打造” 身份。当我们用new调用构造函数时,this会直接绑定到这个刚创建的实例对象上,相当于工厂把定制好的 “专属身份” 直接赋给了新实例。
但这个 “定制工厂” 有个特殊规则,分三种情况:
情况 1:正常定制 —— 返回绑定 this 的实例
当构造函数里没有手动return,或者只return了undefined(默认隐含)时,工厂会按流程完成 “定制”,返回绑定了this的实例对象。
function Person(name) {
this.name = name;
}
let p1 = new Person('henry');
console.log(p1.name); // 输出"henry"
console.log(p1); // 输出Person { name: "henry" },this绑定生效
![]()
情况 2:放弃定制 —— return 引用类型,返回手动指定的对象
如果构造函数里主动return了一个引用类型(比如对象、数组、函数等复杂数据),相当于你给工厂递了一个 “现成产品”,工厂会直接放弃原本的定制流程,返回这个手动指定的引用类型,原本绑定this的实例会被直接忽略。
function Person(name) {
this.name = name;
return { age: 20 };
}
let p2 = new Person('harvest');
console.log(p2); // 输出{ age: 20 },实例被忽略
console.log(p2.name); // 输出undefined,拿不到原本的name属性
![]()
情况 3:无视 return —— return 原始类型,依然返回实例
如果构造函数里return的是原始数据类型(比如数字、字符串、布尔值、null、undefined),这个return会被工厂 “无视”,依然按原规则返回绑定了this的实例对象,相当于你递了个 “无效产品”,工厂还是按定制流程来。
function Person(name) {
this.name = name;
return 100; // return 无效
}
let p3 = new Person('henry');
console.log(p3.name); // 输出"henry",实例正常生效
![]()
6. 箭头函数 —— “没有 this” 的 this
箭头函数里没有自己的this,它的this是外层非箭头函数的 this。就像 “小跟班”,永远跟着外层函数的this走,不会被任何绑定规则改变。
function foo() {
var bar = () => {
this.a = 2;
}
bar();
}
var obj = {
a: 1,
baz: foo
}
obj.baz();
console.log(obj);
再次灵魂拷问:a 是 1 还是 2 ? 答案:2 !
关键逻辑:
-
obj.baz()是隐式绑定:foo 函数被 obj 调用,所以 foo 里的this指向 obj; - 箭头函数
bar没有自己的this,直接 “偷” 了外层 foo 的this(也就是 obj); - 执行
bar()时,this.a = 2其实是给obj.a赋值,把原来的 1 改成了 2; - 最后打印 obj,a 属性已经变成 2,结果就是
{ a: 2, baz: 函数foo }。
![]()
OK,是不是觉得自己已经拿捏this了?
总结
从 “自由散漫” 的默认绑定、“依附对象” 的隐式绑定,到 “强行指定” 的三个“热心”的室友,再到 “定制化” 的 new 绑定(三种情况),最后是 “粘人跟班” 的箭头函数 —— 只要找准场景对号入座,this就再也不会让你头疼啦!
掌握
this,让你的代码更上一层楼吧!