在 JavaScript 这门语言里,如果说作用域和闭包是“最难理解”的概念,那 this 绝对是“最让人抓狂”的设计。
它不遵守词法作用域(Lexical Scope),而是完全由函数的调用方式决定。这导致无数人写着写着 this 就“丢了”,打印出来是 window 或 undefined,线上 bug 层出不穷。
今天我们就把 this 的底层机制彻底扒得一干二净,用最经典的例子(包括你自己踩过的坑),让你彻底明白:this 到底是怎么绑定的。
先说一个最容易让人崩溃的例子(99% 的人都会错)
HTML
<script>
'use strict';
var bar = {
myName: 'bar',
printName: function() {
console.log("bar1", myName); // global
console.log("bar2:", this); // window
console.log("bar3:", this.myName); // undefined
}
}
function foo() {
let myName = 'foo';
return bar.printName;
}
var myName = 'global';
let _printName = foo();
_printName(); // 普通函数调用 → 默认绑定
bar.printName(); // 对象方法调用 → 隐式绑定
</script>
运行结果:
- _printName() 输出:global / window / undefined
- bar.printName() 输出:global / bar 对象 / bar
为什么?
因为 printName 这个函数在定义时是全局函数,即使它写在 bar 对象里,本质上还是全局作用域的函数。
当你把它赋值给 _printName 再调用时,它就是普通函数调用,this 默认绑定到全局对象(严格模式下是 undefined)。
这就是经典的“隐式绑定丢失”。
很多人以为“函数写在对象里,this 就一定是这个对象”,错!this 永远只看“谁在调用它”。
this 的本质:执行上下文中的 thisBinding
V8 在创建执行上下文(Execution Context)时,会同时创建三个部分:
- LexicalEnvironment(词法环境):管 let/const/block
- VariableEnvironment(变量环境):管 var/function
- thisBinding:就是我们要讲的 this 值
前两者在编译阶段就确定了(词法作用域),但 thisBinding 是执行阶段动态确定的,完全取决于你怎么调用这个函数。
《ECMAScript 规范》把 this 绑定分为四种规则,按优先级从高到低排列:
- new 绑定(最高优先级)
- 显式绑定(call/apply/bind)
- 隐式绑定(obj.foo())
- 默认绑定(普通函数调用)
记住这个优先级,90% 的 this 问题都能秒解。
规则1:默认绑定(最容易踩坑)
独立函数调用,this指向全局对象(非严格模式下),严格模式下是 undefined。
JavaScript
'use strict';
function foo() {
console.log(this); // undefined
}
foo(); // 普通调用 → 默认绑定
为什么设计成这样?
Brendan Eich 10 天设计出 JavaScript,当时想让它像 Java 一样支持面向对象,所以加了 this。
但又想让函数超级灵活(第一等公民),于是偷懒了:没调用者就指向全局。
结果就是 var 声明的变量会挂到 window 上,污染全局,let/const 不会。
这就是历史遗留问题,后人只能背锅。
规则2:隐式绑定(最常见的“this 丢失”场景)
函数作为对象的方法被调用,this 指向调用者对象。
JavaScript
var bar = {
myName: 'bar',
printName: function() {
console.log(this.myName);
}
};
bar.printName(); // 'bar' ← 隐式绑定,this = bar
但一旦把方法取出来单独调用,隐式绑定就没了:
JavaScript
let fn = bar.printName;
fn(); // undefined(严格模式)或 global(非严格)
这就是上面第一个例子的根本原因。
真实项目中常见的坑:
JavaScript
var obj = {
name: '阿宝哥',
getName: function() {
console.log(this.name);
}
};
setTimeout(obj.getName, 1000); // undefined 或 window
因为 setTimeout 内部是 window.setTimeout,相当于 fn() 调用,this 丢失。
正确写法:
JavaScript
setTimeout(() => obj.getName(), 1000); // 箭头函数无this,沿用外层this
// 或
setTimeout(obj.getName.bind(obj), 1000); // 硬绑定
Vue/React 里 this 丢失的根本原因也是这个。
规则3:显式绑定(call、apply、bind)—— 硬绑定,永不丢失
JavaScript
function foo() {
console.log(this.myName);
}
let bar = { myName: 'bar' };
foo.call(bar); // 'bar'
foo.apply(bar); // 'bar'
let bindFoo = foo.bind(bar);
bindFoo(); // 'bar',永远是 bar
bind 是最强的,一旦 bind 了,后面的 call/apply/new 都改不了(new 优先级更高,后面讲)。
经典面试题:
JavaScript
var name = 'global';
let obj = { name: 'obj' };
function foo() {
console.log(this.name);
}
let bindFoo = foo.bind(obj);
bindFoo(); // obj
bindFoo.call(window); // 还是 obj!bind 最硬
规则4:new 绑定(优先级最高)
用 new 调用构造函数,this 指向新创建的实例。
JavaScript
function CreateObj() {
this.name = 'CreateObj';
console.log(this); // CreateObj {}
}
var obj = new CreateObj(); // this 指向新对象
new 到底干了什么?(手动实现一个 new)
JavaScript
function myNew(Constructor, ...args) {
// 1. 创建空对象
const obj = {};
// 2. 链接原型链
obj.__proto__] = Constructor.prototype;
// 3. 绑定 this 执行构造函数
const result = Constructor.apply(obj, args);
// 4. 返回对象(如果构造函数返回对象则返回那个,否则返回 obj)
return typeof result === 'object' && result !== null ? result : obj;
}
你提供的最后一个代码其实想表达这个意思,但写错了位置,我帮你纠正一下:
JavaScript
function CreateObj() {
this.name = 'CreateObj';
}
CreateObj.prototype.say = function() {
console.log('hello');
};
var obj = myNew(CreateObj);
console.log(obj.name); // { name: 'CreateObj' }
obj.say(); // hello
new 绑定的优先级最高,所以:
JavaScript
function foo() {
console.log(this);
}
let bindFoo = foo.bind({ name: '被bind的' });
new bindFoo(); // foo {} ← new 赢了!
箭头函数:彻底打破规则的“异类”
箭头函数没有自己的 this,它会捕获定义时所在的外层执行上下文的 this。
JavaScript
let obj = {
name: 'obj',
foo: function() {
setTimeout(() => {
console.log(this); // obj 对象!不是 window
}, 1000);
}
};
obj.foo();
这是解决 this 丢失最优雅的方式,Vue/React 推荐使用箭头函数绑定事件就是因为这个。
面试高频题
题1:
JavaScript
var name = 'window';
var A = {
name: 'A',
say: function() {
console.log(this.name);
}
};
var B = {
name: 'B',
speak: A.say // speak 只是对同一个函数的引用
};
B.speak(); // → "B" (隐式绑定)
A.say.call(B); // → "B" (显式绑定)
答案:B(隐式绑定)/ B(显式绑定覆盖)
题2:
JavaScript
function foo() {
console.log(this.a);
}
var a = 2;
var o = { a: 3, foo };
o.foo(); // 3(隐式)
foo.call(null); // 2(call(null) 相当于默认绑定,非严格模式)
call(null) 在非严格模式下会退化为默认绑定,这也是坑。
题3:
JavaScript
let obj = {
a: 1,
foo: function() {
console.log(this.a);
}.bind({ a: 2 })
};
obj.foo(); // 2!bind 后的函数,隐式绑定无效
总结:一句话记住 this 绑定规则
谁调用我,我就是谁(除了箭头函数)
- obj.foo() → this = obj(隐式)
- foo() → this = window/undefined(默认)
- foo.call(obj) / bind(obj) → this = obj(显式)
- new foo() → this = 新实例(最高优先级)
记住这四条 + 优先级 + 箭头函数无 this,你就已经超越 95% 的前端开发者了。
this 的设计确实是个历史包袱,但理解了它的规则,你就能在 Vue、React、Node、小程序各种环境上游刃有余地驾驭它,而不是被它驾驭。
下次再遇到 this 指向问题,直接问自己一句话:
“这个函数到底是被谁调用的?”
答案就出来了。