深入解析 JavaScript 中 this 的指向规则与演变

作者:纸飞机时间:2026-10-30 02:25:18浏览:196

本文详细解析 JavaScript 中 this 关键字的指向机制。从构造器、对象方法、全局环境到 DOM 事件,逐一剖析 this 在不同场景下的行为。重点对比 ES5 中 call/apply/bind 对上下文的改变,以及 ES6 箭头函数如何基于词法作用域固定 this 指向,帮助开发者避免常见的 this 陷阱。

JavaScript 中 this 的基本概念

JavaScript 常被视为一门极具灵活性的语言,这种灵活性有时也表现为其复杂性。它融合了函数式编程与面向对象编程的特性,并具备动态语言的特质,功能强大(尽管与 C++ 等静态类型语言相比仍有差异)。

本文聚焦于 this 关键字。在面向对象语境下,this 通常指向当前类的实例,且不可被直接赋值。若脱离类或对象单独讨论 this 是没有意义的。如果代码完全采用函数式风格而非面向对象风格,this 的使用频率会大幅降低,甚至完全消失。因此,使用 this 通常意味着你正在以对象或类的方式编写代码;否则,它仅是函数调用时的一个副作用。

JavaScript 中的 this 行为

this 在函数内部被创建,指向调用时所在函数所绑定的对象。尽管表述略显拗口,但这是准确的。this 本身不能被直接赋值,但可以通过 call 或 apply 方法改变其指向。

以往使用 this 时,开发者常因不确定其指向而感到困惑。下文将列举 this 在各种场景下的具体应用。

本文涵盖以下主题:this 与构造器、this 与对象、this 与函数、全局环境中的 this、this 与 DOM/事件、通过 call/apply 改变 this、ES5 新增的 bind 方法,以及 ES6 箭头函数中的 this。

this 与构造器和对象的关系

1. this 与构造器

this 是类定义时构造器中常用的关键字,二者结合使用非常自然。


/**
 * 页签
 *
 * @class Tab
 * @param nav {string} 页签标题的class
 * @param content {string} 页面内容的class
 *
 */
function Tab(nav, content) {
  this.nav = nav
  this.content = content
}
 
Tab.prototype.getNav = function() {
  return this.nav;
};
Tab.prototype.setNav = function(nav) {
  this.nav = nav;
};
Tab.prototype.add = function() {
};

按照 JavaScript 的惯例,属性和字段应挂载在 this 上,而方法则应定义在原型上。

2. this 与对象

JavaScript 允许不通过类直接创建对象。虽然类通常被视为对象的模板,但 JS 支持无类创建对象,开发者甚至可以编写大量功能代码而不定义任何类。面向对象编程(OOP)强调的是面向对象而非面向类,因此这种写法是完全可行的。


var tab = {
  nav: '',
  content: '',
  getNav: function() {
    return this.nav;
  },
  setNav: function(n) {
    this.nav = n;
  }
}

this 与函数的关系

3. this 与函数

this 与独立函数结合并无实际意义,因为 this 本质上与面向对象相关。纯粹的函数仅用于封装和复用,属于低级抽象。


function showMsg() {
  alert(this.message)
}
showMsg() // undefined

定义 showMsg 函数并以普通函数方式调用时,this.message 为 undefined。因此,严禁在纯函数内部直接使用 this。但在某些情况下,可以通过 call 或 apply 来调用此类函数。


function showMsg() {
  alert(this.message)
}
 
var m1 = {
  message: '输入的电话号码不正确'
}
var m2 = {
  message: '输入的身份证号不正确'
}
 
showMsg.call(m1) // '输入的电话号码不正确'
showMsg.call(m2) // '输入的身份证号不正确'

这种方法可以节省代码量。当两个类或对象拥有相似方法时,无需编写两份代码,只需定义一个函数,然后将其绑定到各自的原型或对象上。这本质上仍是使用对象或类(方式1或方式2),只是间接使用而已。

全局环境中的 this 指向

4. 全局环境中的 this

前文提到的“指向调用时所在函数所绑定的对象”虽然拗口,但完全正确,没有多余的字。全局环境包含不同的宿主对象:在浏览器环境中是 window,在 Node.js 环境中是 global。此处重点讨论浏览器环境中的 this。

在浏览器环境中,非函数作用域内的 this 指向 window。

alert(window === this) // true 因此,许多开源 JavaScript 库采用以下写法:

(function() { // ... })(this); 或者采用以下写法:

(function() { // ... }).call(this); 例如 underscore 和 requirejs,它们将全局变量 window 传入匿名函数以缓存起来,避免直接访问全局变量。缓存的原因与 JavaScript 的作用域链有关,访问外层标识符的性能较低。具体原理涉及作用域链知识,此处不再展开。

浏览器环境中存在一个令人困惑的特性:在非函数作用域内使用 var 声明的变量默认为全局变量,并默认作为属性挂载在 window 对象上。


var andy = '刘德华'
alert(andy === window.andy) // true
alert(andy === this.andy) // true
alert(window.andy === this.andy) // true

利用这一特性,常出现如下的笔试题:


var x = 10;
function func() {
  alert(this.x)
}
var obj = {
  x: 20,
  fn: function() {
    alert(this.x)
  }
}
var fn = obj.fn
func() // 10
fn() // 10

最终输出结果均为全局的 10。请务必记住:判断 this 指向谁,应看执行时而非定义时。只要函数(function)没有绑定在对象上调用,其 this 就是 window。

this 与 DOM 事件处理

5. this 与 DOM/事件

W3C 将 DOM 实现为各种节点,节点嵌套形成 DOM 树。节点类型多样,如文本节点、元素节点等10多种。元素节点又细分为许多类型,对于 HTML 开发者而言,即熟悉的标签(Tag),如 div、ul、label 等。查看 W3C API 文档可知,其完全按照面向对象方式实现,包含 interface、extends 等概念。例如:

这是用 Java 编写的实现。既然 API 采用面向对象方式实现,必然存在类/对象,而存在类/对象,就必然涉及 this(这也是本文的核心主题)。所有 HTML 标签类的命名如 HTMLXXXElement,例如:

HTMLDivElement HTMLLabelElement HTMLInputElement ... 前文已述,this 指向当前类的实例对象。对于这些标签类而言,无需查看源码即可推断,其许多方法内部使用的 this 指向自身。这一结论使得在编写 HTML 和 JS 时,对 this 的理解更加清晰。

示例A


<!-- this 指向 div -->
<div onclick="alert(this)"></div>

示例B


<div id="nav"></div>
<script>
  nav.onclick = function() {
    alert(this) // 指向div#nav
  }
</script>

示例C


$('#nav').on('click', function() {
  alert(this) // 指向 nav
})

以上三个示例表明,在元素节点添加事件时,其响应函数(handler)执行时的 this 均指向 Element 节点自身。jQuery 保持了与标准一致的行为,但这往往令人困惑。根据“this 指向调用时所在函数所绑定的对象”这一定义,jQuery 事件 handler 中的 this 应指向 jQuery 对象,而非 DOM 节点。因此,在使用 jQuery 时,常需将事件 handler 中的 element 用 $ 包裹转换为 jQuery 对象后再进行操作。例如:


$('#nav').on('click', function() {
  var $el = $(this) // 再次转为 jQuery 对象,如果 this 直接为 jQuery 对象更好
  $el.attr('data-x', x)
  $el.attr('data-x', x)
})

有人可能产生如下疑问:


<div id="nav" onclick="getId()">ddd</div>
<script>
  function getId() {
    alert(this.id)
  }
</script>

点击 div 后,为何 id 显示为 undefined?前文并非指出向当前元素 div 吗?如果牢记前文提到的那句话,就能清楚原因。再次引用该句:

判断 this 指向谁,看执行时而非定义时,只要函数(function)没有绑定在对象上调用,它的 this 就是 window

此处函数 getId 调用时未绑定在任何对象上,可理解为以下结构:


div.onclick = function() {
  getId()
}

getId 所处匿名函数中的 this 是 div,但 getId 自身内部的 this 则不同。当然,ES5 严格模式下还存在一个陷阱。

使用 call/apply 改变 this 上下文

6. this 可以被 call/apply 改变

call 和 apply 是函数调用的另外两种方式,两者的第一个参数均可改变函数的上下文 this。call/apply 体现了 JavaScript 作为动态语言的特性。动态语言的通俗定义如下:

程序在运行时可以改变其结构,新的函数可以被引进,已有的函数可以被删除,即程序在运行时可以发生结构上的变化

通常具备以下特征的语言被视为动态语言:

动态的数据类型 动态的函数执行 动态的方法重写 动态语言多从世界第二门语言 LISP 发展而来,如已逝去的 SmallTalk/VB,目前仍活跃的 Perl/Python,以及流行的 Ruby/JavaScript。JavaScript 中动态数据类型的体现是弱类型,即在运行时才分析标识符的类型。函数动态执行体现为 eval、call/apply。方法重写则体现在原型重写。此处不展开讨论,重点说明 call/apply 对 this 的影响。


var m1 = {
  message: 'This is A'
} 
var m2 = {
  message: 'This is B'
} 
 
function showMsg() {
  alert(this.message)
}
 
showMsg() // undefined
showMsg.call(m1) // 'This is A'
showMsg.call(m2) // 'This is B'

可以看到,单独调用 showMsg 返回的是 undefined,只有将其绑定到具有 message 属性的对象上执行时才有意义。发挥想象力延伸一下,如果编写一些通用函数,可以任意绑定到多个类的原型上,从而动态地为类添加方法并节省代码。这是一种强大的功能,也是动态语言强表现力的体现。

经常听到转向 Ruby 或 Python 的人提到“编程的乐趣”,这种乐趣源于动态语言更接近人的思维(而非机器思维),更符合业务流程而非项目实现流程。同样的功能,动态语言可以用更少的代码量实现。动态语言对程序员生产力的提升,是其大行其道的主要原因。

在性能方面,动态语言并无太大优势,但其理念是:优化人的时间而不是机器的时间。提高开发者的生产力,宁可牺牲部分程序性能或购买更高配置的硬件。随着 IT 业的不断发展和摩尔定律的作用,硬件相对于人力成本一直在贬值,这一理念便有了合理的现实基础。

JavaScript 中的 call/apply 在任何一个流行的库中都会用到,但几乎只有两个作用:

配合写类工具实现OOP,如 mootools, ClassJS, class.js, 修复DOM事件里的 this,如 jQuery, events.js

关于 call 和 apply 复用:利用apply和arguments复用方法

关于 call 和 apply 的性能问题参考: 冗余换性能-从Backbone的triggerEvents说开了去

ES5 中 bind 方法的作用

7. ES5 中新增的 bind 和 this

上文第 6 点提到 call/apply 体现了 JavaScript 的动态语言特性及流行原因,其在 JS 中用途广泛。ES5 发布时将其采纳,并提出了一个更高级的方法 bind。


var modal = {
  message: 'This is A'
}
 
function showMsg() {
  alert(this.message)
}
 
var otherShowMsg = showMsg.bind(modal)
otherShowMsg() // 'This is A'

由于 ES5 才引入该方法,低版本 IE 不支持,可以通过修复 Function.prototype 来兼容。bind 只是 call/apply 的高级版,其他方面并无特殊之处。

ES6 箭头函数中的 this 行为

8. ES6 箭头函数(arrow function) 和 this

ES6 于今年 6月18日 正式发布(恰逢京东店庆日,^_^),它引入了另一种类型的函数——箭头函数。箭头函数的一个重要特征是颠覆了前文所述的规则,再次引用:

判断 this 指向谁,看执行时而非定义时,只要函数(function)没有绑定在对象上调用,它的 this 就是 window

是的,前文一直用这句话来判断 this 的指向,但在箭头函数中,前半句失效。箭头函数的特征是:定义在哪里,this 就指向哪里。即箭头函数定义在一个对象里,那么箭头函数里的 this 就指向该对象。如下:


var book = {
  author: 'John Resig',
  init: function() {
    document.onclick = ev => {
      alert(this.author) ; // 这里的 this 不是 document 了
    }
  }
};
book.init()

对象 book 里有一个属性 author,有一个 init 方法,给 document 添加了一个点击事件。如果是传统函数,我们知道 this 指向应该是 document,但箭头函数会指向当前对象 book。

箭头函数让 JavaScript 回归自然和简单:函数定义在哪,this 就指向哪。定义在对象里,它指向该对象;定义在类的原型上,指向该类的实例。这样更容易理解。

总结:

函数的上下文 this 是 JavaScript 中较难理解的部分,原因在于 JavaScript 函数自身具有多种用途,旨在实现各种语言范式(面向对象、函数式、动态)。this 本质上是与面向对象联系的,与写类、对象关联在一起,与“函数式”无关。如果你采用过程式或函数式开发,完全不会用到 this。但在浏览器端开发时却无可避免地会用到 this,这是因为浏览器对象模型(DOM)本身采用面向对象方式开发,Tag 实现为一个个的类,类的方法自然会引用类的其它方法,引用方式必然是用 this。当你给 DOM 对象添加事件时,回调函数里引用该对象就只能用 this 了。

明白了么?

相信看完全文以后,this 不再是坑~

作者最新文章