本文详细解析 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 不再是坑~