JavaScript this 指针原理与常见误用场景解析
本文详细解析 JavaScript 中 this 指针的执行机制,阐明其指向取决于调用方式而非定义位置。通过全局函数、对象方法、构造函数及 DOM 事件处理等案例,说明 this 丢失的常见原因,并介绍 apply、call 及闭包等修复方案。
前言
JavaScript 是一门基于对象的动态语言,这意味着几乎所有事物都是对象,函数也不例外,被视为普通的对象。在 JavaScript 中,可以通过特定的设计模式来实现面向对象编程,其中 this 指针是实现面向对象特性的关键机制。然而,this 也是 JavaScript 中最容易误解和误用的特性之一,尤其是对于从静态类型语言转过来的开发者而言,理解起来颇具挑战。
示例说明
首先,我们来看一个最简单的示例:
<script type="text/javascript">
var name = "Kevin Yang";
function sayHi(){
alert("你好,我的名字叫" + name);
}
sayHi();
script>
这段代码非常基础,定义了一个全局字符串变量 name 和一个函数对象 sayHi。运行代码后,会弹出一个包含打招呼信息的对话框,显示“你好,我的名字叫 Kevin Yang”。
接下来,我们对这段代码进行微调:
<script type="text/javascript">
var name = "Kevin Yang";
function sayHi(){
alert("你好,我的名字叫" + this.name);
}
sayHi();
script>
与上一段代码的区别在于,sayHi 函数在引用 name 时使用了 this. 前缀。运行结果与之前完全一致,这表明 this.name 引用的依然是全局作用域下的 name 对象。
既然开头提到函数也是普通的对象,可以像普通变量一样使用,我们再次修改上述代码:
<script type="text/javascript">
var name = "Kevin Yang";
function sayHi(){
alert("你好,我的名字叫" + this.name);
}
var person = {};
person.sayHello = sayHi;
person.sayHello();
script>
这一次,我们创建了一个全局对象 person,并将 sayHi 函数对象赋值给 person 对象的 sayHello 属性。运行结果如下:
此时打招呼的内容变得令人困惑,我们发现 this.name 变成了 undefined。这说明在 sayHello 函数内部执行时,无法找到 this.name 对象。如果我们在 person 对象上显式添加一个 name 属性,结果会如何呢?
var person = {name:"Marry"};
运行代码后发现,打招呼的“人”发生了变化:
通过上述示例,你是否已经看出其中的规律?
判别 this 指针的指导性原则
在 JavaScript 中,this 指针代表的是执行当前代码的对象的所有者。
在上面的示例中,第一次我们定义了一个全局函数对象 sayHi 并执行该函数。函数内部使用了 this 关键字,那么执行 this 这行代码的对象是 sayHi(体现了一切皆对象的特性)。由于 sayHi 被定义在全局作用域中,而在 JavaScript 中,全局对象实际上是定义在 window 这个根对象下的一个属性。因此,sayHi 的所有者是 window 对象。这意味着,在全局作用域下,你可以直接使用 name 引用该对象,也可以通过 window.name 引用同一个对象。因此,this.name 可以理解为 window.name。
再看第二个 this 的示例。我们定义了一个 person 对象,并定义了它的 sayHello 属性,使其指向全局的 sayHi 对象。此时,当我们运行 person.sayHello 时,this 所在的代码所属对象就是 sayHello(准确来说,sayHi 和 sayHello 只是两个指向同一实际对象的指针),而 sayHello 对象的所有者是 person。第一次,person 中没有 name 属性,因此弹出的对话框显示 this.name 引用的就是 undefined 对象(在 JavaScript 中,所有只声明而未定义的变量都指向 undefined);第二次我们在定义 person 时添加了 name 属性,那么 this.name 指向的自然是我们要定义的字符串。
理解了上述原理后,我们将最后一段示例改造成面向对象风格的代码。
<script type="text/javascript">
var name = "Kevin Yang";
function sayHi(){
alert("你好,我的名字叫" + this.name);
}
function Person(name){
this.name = name;
}
Person.prototype.sayHello = sayHi;
var marry = new Person("Marry");
marry.sayHello();
var kevin = new Person("Kevin");
kevin.sayHello();
script>
在这段代码中,我们定义了一个 Person 的“类”(实际上仍是一个对象),然后在该类的原型(类原型类似于 C++ 中的静态成员变量概念)中定义了 sayHello 属性,使其指向全局的 sayHi 对象。运行代码可以看到,marry 和 kevin 都成功地向我们打了个“招呼”。
在这段代码中有两点值得思考:一是 new 关键字我们很熟悉,但在这里 new 到底执行了什么操作?二是,为什么在执行 sayHello 时,this 指针能够正确指向 marry 和 kevin 对象?
我们将上面定义“类”和实例化类对象的操作重新“翻译”一下:
<script type="text/javascript">
var name = "Kevin Yang";
function sayHi(){
alert("你好,我的名字叫" + this.name);
}
function Person(name){
var this;
this.name = name;
return this;
}
Person.prototype.sayHello = sayHi;
var marry = Person("Marry");
marry.sayHello();
var kevin = Person("Kevin");
kevin.sayHello();
script>
当然这段代码并不能正确执行,但它有助于你更好地理解这个过程。
当我们使用 new 关键字实例化一个“类”对象时,JavaScript 引擎会在这个对象内部定义一个新的对象,并将其存入 this 指针。所有此对象内部用到 this 的代码实际上都是指向这个新的对象。例如 this.name = name,实际上是将参数中的 name 对象赋值给了这个新创建的对象。函数对象执行完毕后,JavaScript 引擎会将此对象返回给你,于是 marry 变量得到的对象的 name 为“Marry”,而 kevin 变量得到的对象的 name 属性确实是“Kevin”。
容易误用的情况
理解了 this 指针后,我们再来看看一些很容易误用 this 指针的情况。
示例 1——内联式绑定 Dom 元素的事件处理函数
<script type="text/javascript">
function sayHi(){
alert("当前点击的元素是" + this.tagName);
}
script>
<input id="btnTest" type="button" value="点击我" onclick="sayHi()">
在此例代码中,我们绑定了 button 的点击事件,期望在弹出的对话框中打印出点击元素的标签名。但运行结果却是:
也就是说 this 指针并不是指向 input 元素。这是因为当使用内联式绑定 Dom 元素的事件处理函数时,实际上相当于执行了以下代码:
<script type="text/javascript">
document.getElementById("btnTest").onclick = function(){
sayHi();
}
script>
在这种情况下,sayHi 函数对象的所有权并没有发生转移,仍然属于 window 所有。用上面的指导原则来分析,我们就能很好理解为什么 this.tagName 是 undefined 了。
那么,如果我们要引用元素本身该怎么办呢?
我们知道,onclick 函数是属于 btnTest 元素的,那么在此函数内部,this 指针正是指向此 Dom 对象。于是我们只需要把 this 作为参数传入 sayHi 即可。
<script type="text/javascript">
function sayHi(el){
alert("当前点击的元素是" + el.tagName);
}
script>
<input id="btnTest" type="button" value="点击我" onclick="sayHi(this)">
等价代码如下:
<script type="text/javascript">
document.getElementById("btnTest").onclick = function(){
sayHi(this);
}
script>
示例 2——临时变量导致的 this 指针丢失
<script type="text/javascript">
var Utility = {
decode:function(str){
return unescape(str);
},
getCookie:function(key){
// ... 省略提取cookie字符串的代码
var value = "i%27m%20a%20cookie";
return this.decode(value);
}
};
alert(Utility.getCookie("identity"))
script>
我们在编写稍微有点规模的 Js 库时,通常会自己封装一个 Utility 类,并将一些常用的函数作为 Utility 类的属性,例如客户端经常用到的 getCookie 函数和解码函数。如果每个函数都是彼此独立的,那么处理起来还比较轻松;问题是,函数之间有时候会相互引用。例如上面的 getCookie 函数,会对从 document.cookie 中提取到的字符串进行 decode 之后再返回。如果我们通过 Utility.getCookie 去调用,那么没有问题,我们知道 getCookie 内部的 this 指针指向的还是 Utility 对象,而 Utility 对象包含 decode 属性。代码可以成功执行。
但是,如果有个人不小心这样使用 Utility 对象呢?
<script type="text/javascript">
function showUserIdentity(){
// 保存getCookie函数到一个局部变量,因为下面会经常用到
var getCookie = Utility.getCookie;
alert(getCookie("identity"));
}
showUserIdentity();
script>
此时运行代码会抛出异常“this.decode is not a function”。运用上面我们讲到的指导原则,很好理解,因为此时 Utility.getCookie 对象被赋给了临时变量 getCookie,而临时变量是属于 window 对象的——只不过外界不能直接引用,只对 JavaScript 引擎可见——于是在 getCookie 函数内部的 this 指针指向的就是 window 对象了,而 window 对象没有定义一个 decode 的函数对象,因此就会抛出这样的异常来。
这个问题是由于引入了临时变量导致的 this 指针的转移。解决此问题的办法有几个:
- 不引入临时变量,每次使用均使用 Utility.getCookie 进行调用
- getCookie 函数内部使用 Utility.decode 显式引用 decode 对象而不通过 this 指针隐式引用(如果 Utility 是一个实例化的对象,也即是通过 new 生成的,那么此法不可用)
- 使用 Function.apply 或者 Function.call 函数指定 this 指针
前两种方案都比较好理解,第三种需要特别提一下。正是因为 this 指针的指向很容易被转移或丢失,因此 JavaScript 提供了两个类似的函数 apply 和 call,允许函数在调用时重新显式地指定 this 指针。
修正代码如下:
<script type="text/javascript">
function showUserIdentity(){
// 保存getCookie函数到一个局部变量,因为下面会经常用到
var getCookie = Utility.getCookie;
alert(getCookie.call(Utility,"identity"));
alert(getCookie.apply(Utility,["identity"]));
}
showUserIdentity();
script>
call 和 apply 只有语法上的差异,没有功能上的差别。
示例 3——函数传参时导致的 this 指针丢失
我们先来看一段问题代码:
<script type="text/javascript">
var person = {
name:"Kevin Yang",
sayHi:function(){
alert("你好,我是"+this.name);
}
}
setTimeout(person.sayHi,5000);
script>
这段代码期望在访客进入页面 5 秒钟之后向访客打声招呼。setTimeout 函数接收一个函数作为参数,并在指定的触发时刻执行这个函数。可是,当我们等了 5 秒钟之后,弹出的对话框显示的 this.name 却是 undefined。
其实这个问题和上一个示例中的问题是类似的,都是因为临时变量而导致的问题。当我们执行函数的时候,如果函数带有参数,那么这个时候 JavaScript 引擎会创建一个临时变量,并将传入的参数复制(注意,JavaScript 里面都是值传递的,没有引用传递的概念)给此临时变量。也就是说,整个过程就跟上面我们定义了一个 getCookie 的临时变量,再将 Utility.getCookie 赋值给这个临时变量一样。只不过在这个示例中,容易忽视临时变量导致的 bug。
函数对象传参
对于函数作为参数传递导致的 this 指针丢失的问题,目前很多框架都已经有方法解决了。
Prototype 的解决方案——传参之前使用 bind 方法将函数封装起来,并返回封装后的对象
<script type="text/javascript">
var person = {
name:"Kevin Yang",
sayHi:function(){
alert("你好,我是"+this.name);
}
}
var boundFunc = person.sayHi.bind(person,person.sayHi);
setTimeout(boundFunc,5000);
script>
bind 方法的实现其实是用到了 JavaScript 又一个高级特性——闭包。我们来看一下源代码:
function bind(){
if (arguments.length < 2 && arguments[0] === undefined)
return this;
var __method = this, args = $A(arguments), object = args.shift();
return function(){
return __method.apply(object, args.concat($A(arguments)));
}
}
首先将 this 指针存入函数内部临时变量,然后在返回的函数对象中引用此临时变量从而形成闭包。
微软的 Ajax 库提供的方案——构建委托对象
<script type="text/javascript">
var person = {
name:"Kevin Yang",
sayHi:function(){
alert("你好,我是"+this.name);
}
}
var boundFunc = Function.createDelegate(person,person.sayHi);
setTimeout(boundFunc,5000);
script>
其实本质上和 prototype 的方式是一样的。
著名的 Extjs 库的解决方案采用的手法和微软是一样的。
什么时候该用JS构造器:与工厂模式、字面量的选择对比
2026-11-05 16:18:22
推荐几款轻量级JS照片墙开源库及对比
2026-11-05 15:56:19
JavaScript递归写法有哪些常见陷阱及避免方法
2026-11-05 14:53:38
JS数组浅拷贝与深拷贝怎么选?场景与方法总结
2026-11-05 14:41:02
p5.js 中 setup() 和 draw() 函数的执行机制详解
2026-11-05 12:32:38
