深入理解 JavaScript 中的 this:设计初衷、绑定规则与常见陷阱
在 JavaScript 开发中,this 是一个既基础又容易让人困惑的概念。它看似简单,却因绑定规则依赖于函数调用方式 而非声明位置 ,常常导致意料之外的行为。本文将从 this 的本质出发,结合语言设计背景、执行机制以及实际代码示例,系统性地梳理 this 的行为规律,并为后续讨论"绑定丢失"问题预留空间。
从自由变量说起:为什么需要 this?
在深入 this 之前,不妨先回顾一个更基础的概念:自由变量(free variable) 。
考虑如下代码:
js
var name = "全局";
function greet() {
console.log("你好," + name);
}
greet(); // 你好,全局
函数 greet 内部使用了变量 name,但它并未在函数内部声明。这个 name 就是一个自由变量 。JavaScript 引擎会沿着词法作用域链(Lexical Scope Chain) 向外查找,最终在全局作用域中找到 name 的定义。
这种机制是静态的------变量的查找路径在代码书写时就已确定,与函数如何被调用无关。这也是 JavaScript 中绝大多数变量访问的行为模式。
然而,面向对象编程带来了一个新需求:同一个函数可能属于多个对象,希望在运行时动态地知道"当前是哪个对象在调用我" 。
例如:
js
var person1 = { name: "Alice", sayHi: greet };
var person2 = { name: "Bob", sayHi: greet };
person1.sayHi(); // 期望输出:你好,Alice
person2.sayHi(); // 期望输出:你好,Bob
如果 greet 依然依赖词法作用域中的 name,它永远只能访问到全局的 "全局",而无法感知调用者是谁。词法作用域在此失效了。
于是,JavaScript 引入了 this ------ 一个不依赖词法作用域、而由调用方式决定的特殊关键字。它让函数能够在运行时动态获取"调用上下文",从而实现对所属对象的自引用。
换句话说:自由变量靠"写在哪"决定值,
this靠"怎么调"决定值。
正是这种设计,使得 this 成为了 JavaScript 执行模型中一个独特而关键的存在------它打破了静态作用域的规则,引入了动态上下文的能力,但也因此带来了理解上的挑战。
、this 是什么?
在 JavaScript 中,this 是一个运行时绑定的上下文对象引用 。它不是一个变量,而是一个关键字,其值在函数被调用时动态确定,取决于函数是如何被调用的,而不是在哪里定义的。
这与 JavaScript 中其他变量(如自由变量)的查找机制截然不同------后者遵循词法作用域(Lexical Scope) ,由函数声明的位置决定;而 this 则完全由调用方式 决定,属于动态作用域的一种体现。
、this 的设计初衷
JavaScript 最初被设计为一种轻量级脚本语言,用于在浏览器中操作 DOM。为了支持面向对象编程(OOP),即使在没有 class 的早期版本中,也需要一种机制让函数能够访问所属对象的属性和方法。
于是,this 被引入:当一个函数作为对象的方法被调用时,this 自动指向该对象 。这样,开发者就可以在方法内部通过 this.xxx 访问对象自身的数据。
然而,由于 JavaScript 函数是一等公民(first-class citizens),可以被赋值、传递、独立调用,这就导致同一个函数在不同调用场景下 this 指向可能完全不同------这种灵活性也带来了复杂性。
、var 与 let 声明对全局对象的影响
这一点与 this 的默认绑定密切相关:
- 使用
var在全局作用域声明的变量,会自动挂载到全局对象 上(如window.myVar = ...)。 - 使用
let或const声明的变量则不会挂载到全局对象。
js
var a = 1;
let b = 2;
console.log(window.a); // 1
console.log(window.b); // undefined
因此,在非严格模式下,若 this 指向 window,通过 this.a 可以访问到 var a,但无法访问 let b。这也解释了为什么在某些代码中 this.xxx 能"神奇地"访问到全局变量------其实是访问了挂载在 window 上的属性。
使用var声明挂载变量到window对象上并不是一件好的事情,他会污染全局环境
、JavaScript 执行机制与 this 的"例外性"
JavaScript 引擎在执行代码前会经历编译阶段 (包括词法分析、作用域构建等)。变量和函数的作用域链 在编译阶段就已确定,这就是词法作用域的基础。
然而,this 是一个例外 :它的值无法在编译阶段确定 ,必须等到运行时 根据调用栈和调用方式动态计算。这意味着:
- 即使两个完全相同的函数体,只要调用方式不同,
this就可能指向完全不同的对象。 this与作用域链无关,它属于执行上下文(Execution Context) 的一部分,而非词法环境。

、this 指向的几种典型情况
根据调用方式,this 的绑定可分为以下几类:
1. 作为对象的方法调用
js
var myObj = {
name:"极客时间",
showThis:function(){
console.log(this);//this->myObj
}
}
myObj.showThis();
作为对象的方法调用时,它指向调用该函数的对象
2. 作为普通函数调用
js
function print() {
console.log(this); // 非严格模式:window;严格模式:undefined
}
print();
作为普通函数调用时,它指向全局对象window(非严格模式)/undefined(严格模式)
3. 构造函数调用
js
function CreateObj(){
// var tempObj = {};
//CreateObj.call(tempObj);
//tempObj.__proto__ = CreateObj.prototype;
//return tempObj;
console.log(this);
this.name="极客时间";
}
var myObj = new CreateObj();
console.log(myObj);
作为构造函数调用时,它指向当前的构造函数的实例化对象
4. 使用 call / apply 绑定this
js
let bar ={
myName:"极客邦",
test:1
}
function foo(){
this.myName="极客时间";
}
// 接受指定this为第一个参数,并运行
foo.call(bar);// this 被指定为bar
// 和call 一样
foo.apply(bar);// this 被指定为bar
console.log(bar);
call和apply都能够改变this的指向,他们接受指定this为第一个参数,我的理解:你可以认为指定一个对象来调用这个函数。值得注意的是,在这段代码中二者似乎是等价的,但实际上二者在参数上有差异,在这里就不深入讨论
5. 事件处理函数中的 this
在 DOM 事件监听器中,this 默认指向触发事件的元素:
js
<a href="#" id="link">点击我</a>
<script>
document.getElementById("link").addEventListener("click",function(){
console.log(this);
})
触发事件后,可以看到,this指向的是当前触发改事件的DOM元素
、this的绑定规则
默认绑定
一般存在于最常用的函数调用类型;独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则
考虑这样一段代码:
js
function foo(){
console.log(this.a)
}
var a =2;
foo();//2
我们能看到,当我们调用foo()函数时,this.a被解析成了全局变量a 。这是为什么?是的,这就是我们说的默认绑定,this指向全局对象,当然这必须是在非严格模式下,严格模式下则会绑定到undefined。
一句话总结默认绑定:非严格模式下,当函数独立调用时,this自动绑定到全局对象上、
⚠️ 这种设计其实暴露了早期 JavaScript 的一个"历史包袱":在非严格模式下,意外的全局 this绑定可能导致隐式创建全局变量,污染全局命名空间。
隐式绑定
当函数调用的位置存在上下文对象,或者说该函数被调用时被某个对象"拥有"或"包含",隐式绑定会把函数调用中的this绑定到这个上下文对象
考虑这样一段代码:
js
function foo ()
{
console.log(this.a);
}
var obj ={
a:2,
foo:foo// 实际上是对foo的引用
}
obj.foo()//2
我们能看到 foo()函数调用时,能够访问到obj的内部属性a,这是因为它由obj调用,所以它被obj所包含。
值得注意的是,对象引用链中只有上一层或者说最后一层在调用位置起作用
js
function foo ()
{
console.log(this.a);
}
var obj2 ={
a:42,
foo:foo
}
var obj1 ={
a:2,
obj2:obj2
}
obj1.obj2.foo();//42
因为最后调用foo的是obj2,所以 foo 的 this.a 指向 obj 2中的 a
显式绑定
JS中绝大多数函数以及你自己创建的函数,都可以使用call()和apply()方法,你可以使用他们来直接指定this的绑定对象,因此我们称为显示绑定
考虑这样一段代码
js
function foo ()
{
console.log(this.a);
}
var obj ={
a:2
}
foo.call(obj);//2
按照前面的理解:foo()在全局中被调用,那么this应该默认被绑定到全局,但是这里却能够访问到obj中的a,这就是 call() 的作用 -->我们可以在调用foo时强制把它的this绑定到obj上
从this绑定的角度出发,call()和apply()是一样的,都用来强制绑定this到指定对象,他们的区别体现在其他参数上,我们这里不考虑
如果你传入了一个原始值(字符串,布尔值,数字)来当作this的绑定对象,这个值会被转换为它的对象形式(new String(),new Boolean(),new Number())。这个过程被称为"装箱"
new绑定
使用new来调用foo()时,我们会构造一个新对象并把它绑定到foo()调用中的this上。我们称为new绑定
首先我们需要重新定义JS中的构造函数-->构造函数只是一些使用new操作符时被调用的普通函数。
所以实际上,并不存在所谓的构造函数,只有对于函数的构造调用
接下来考虑这样一段代码:
js
function foo (a)
{
this.a = a;
}
var bar =new foo(2);
console.log(bar.a);//2
当我们使用new时,就会自动执行这样一些操作:
- 创建一个新对象{}
- 新对象被执行与[[prototype]]连接
- 将函数调用的this绑定到该对象
- 如果没有返回其他对象,则自动返回这个新对象
所以上述代码的实际底层是:
js
function foo (a)
{
var bar ={};
bar.__proto__ = foo().prototype
foo.call(bar);
return bar;
this.a = a;
}
绑定丢失
隐式丢失
一个常见的绑定问题就是被隐式绑定的函数会丢失绑定对象,会重新应用为默认绑定,从而使得this绑定到全局会undefined
js
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
};
var bar = obj.foo;//函数别名
var a = "global";
bar();//global
这里导致绑定丢失的原因是bar实际上也是对foo的引用,而当bar()调用时,它其实是一个函数的独立调用,所以执行了默认绑定
再看另外一种情况,在传入回调函数时:
js
function foo(){
console.log(this.a)
}
function doFoo(fn)
{
fn();
}
var obj = {
a:2,
foo:foo
};
// 把方法赋值给一个变量 ------ 绑定丢失!
var bar = obj.foo;//函数别名
var a = "global";
doFoo(obj.foo);//global
这样同样导致了绑定丢失,那么造成这种情况的原因是?
foo()函数的执行实际上转交由doFoo来执行了,而在它的执行上下文中没有a这个变量,所以沿着作用域链查找到全局中的a
这样的绑定丢失的核心是:回调函数的执行权被移交到了其他函数手中
即使是显示绑定也无法避免绑定丢失
js
function foo() {
console.log("this.a =", this.a); // 期望this指向obj,输出2
}
var obj = { a: 2 };
// 定义一个接收回调的函数
function doCallback(callback) {
callback(); // 这里执行回调,call的绑定丢失
}
// 用call显式绑定foo的this到obj,作为回调传递
doCallback(function() {
foo.call(obj); // 看似绑定了obj
});
// 改造:故意制造绑定丢失(更直观)
function doLostBind(callback) {
// 模拟实际场景中对回调的二次调用,绑定丢失
const temp = callback;
temp(); // 执行时丢失原call绑定
}
// 传递用call绑定的函数,最终绑定丢失
doLostBind(foo.call.bind(foo, obj)); // 非严格模式下输出this.a = undefined(指向window)
doLostBind(foo.call.bind(foo, obj))这种写法看似传入时做了绑定,但其实这只是生成了一个"准备绑定的函数",并没有真正的执行绑定逻辑
怎么解决绑定丢失的问题?
- 硬绑定
javascript
function foo(){
console.log(this.a)
}
var obj = {
a:2,
};
var bar = function () {
foo.call(obj);
}
bar();//2
setTimeout(bar,2);//2
bar.call(window);//2
- 在bar函数的内部,我们把foo的this强制绑定在了obj上,无论之后怎么调用bar,他都会手动在obj上调用foo
- 硬绑定不可能再修改它的this ,我们想要通过bar.call(window); 修改绑定对象,但无论你怎么修改,最后都会执行 foo.call(obj);把this重新绑定到obj上
由于硬绑定是一种很常用的模式,所以ES5提供了它的内置方法bind(),用法如下
js
function foo(temp){
console.log(this.a,temp);
return this.a+ temp
}
var obj = {
a:2,
};
var bar = foo.bind(obj)
var b = bar(3);//2 3
console.log(b);// 5
bind()会返回一个硬编码的新函数,他会把你指定的参数设置为this的上下文并调用原始函数
结语
this 是 JavaScript 中一个强大但需要谨慎使用的机制。理解它的设计初衷、绑定规则以及与作用域系统的差异,是写出健壮、可维护代码的关键。掌握 this,不仅有助于避免常见 bug,也能更深入地理解 JavaScript 的执行模型。
在下一篇文章中,我们将了解更多关于this的底层