很多人当谈到 JavaScript 中的 this
的时候会感到头疼,因为在 JavaScript 中,this
是动态绑定,或称为运行期绑定的,这就导致 JavaScript 中的 this
关键字有能力具备多重含义,带来灵活性的同时,也为初学者带来不少困惑。
上下文 vs 作用域
每个函数调用都有与之相关的作用域和上下文。首先需要澄清的问题是上下文和作用域是不同的概念。很多人经常将这两个术语混淆。
作用域(scope) 是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。而上下文(context)是用来指定代码某些特定部分中 this
的值。
从根本上说,作用域是基于函数(function-based)的,而上下文是基于对象(object-based)的。换句话说,作用域是和每次函数调用时变量的访问有关,并且每次调用都是独立的。上下文总是被调用函数中关键字 this
的值,是调用当前可执行代码的对象的引用。说的通俗一点就是:this
取值,是在函数真正被调用执行的时候确定的,而不是在函数定义的时候确定的。
全局上下文
无论是否在严格模式下,在全局执行上下文中(在任何函数体外部)this
都指向全局对象。当然具体的全局对象和宿主环境有关。
在浏览器中, window
对象同时也是全局对象:
console.log(this === window); // true
NodeJS 中,则是 global
对象:
console.log(this); // global
函数上下文
由于其运行期绑定的特性,JavaScript 中的 this
含义要丰富得多,它可以是全局对象、当前对象或者任意对象,这完全取决于函数的调用方式。JavaScript 中函数的调用有以下几种方式:作为函数调用,作为对象方法调用,作为构造函数调用,和使用 apply
或 call
调用。下面我们将按照调用方式的不同,分别讨论 this
的含义。
作为函数直接调用
作为函数直接调用时,要注意 2 种情况:
非严格模式
在非严格模式下执行函数调用,此时 this
默认指向全局对象。
function f1(){ return this; } //在浏览器中: f1() === window; //在浏览器中,全局对象是window //在Node中: f1() === global;
严格模式 ‘use strict’;
在严格模式下,this
将保持他进入执行上下文时的值,所以下面的 this
并不会指向全局对象,而是默认为 undefined 。
'use strict'; // 这里是严格模式 function test() { return this; }; test() === undefined; // true
作为对象的方法调用
在 JavaScript 中,函数也是对象,因此函数可以作为一个对象的属性,此时该函数被称为该对象的方法,在使用这种调用方式时,内部的 this
指向该对象。
var Obj = { prop: 37, getProp: function() { return this.prop; } }; console.log(Obj.getProp()); // 37
上面的例子中,当 Obj.getProp()
被调用时,方法内的 this
将指向 Obj
对象。值得注意的是,这种行为根本不受函数定义方式或定义位置的影响。在前面的例子中,我们在定义对象 Obj
的同时,将成员 getProp
定义了一个匿名函数。但是,我们也可以首先定义函数,然后再将其附加到 Obj.getProp
。所以,下面的代码和上面的例子是等价的:
var Obj = { prop: 37 }; function independent() { return this.prop; } Obj.getProp = independent; console.log(Obj.getProp()); // logs 37
JavaScript 非常灵活,现在我们把对象的方法赋值给一个变量,然后直接调用这个函数变量又会发生什么呢?
var Obj = { prop: 37, getProp: function() { return this.prop; } }; var test = Obj.getProp console.log(test()); // undefined
可以看到,这时候 this
指向全局对象,这个例子 test
只是引用了 Obj.getProp
函数,也就是说这个函数并不作为 Obj
对象的方法调用,所以,它是被当作一个普通函数来直接调用。因此,this
指向全局对象。
一些坑
我们来看看下面这个例子:
var prop = 0; var Obj = { prop: 37, getProp: function() { setTimeout(function() { console.log(this.prop) // 结果是 0 ,不是37! },1000) } }; Obj.getProp();
正如你所见, setTimeout
中的 this
向了全局对象,这里不是把它当作函数的方法使用吗?这一点经常让很多初学者疑惑;这种问题是很多异步回调函数中也会普遍会碰到,通常有个土办法解决这个问题,比如,我们可以利用 闭包 的特性来处理:
var Obj = { prop: 37, getProp: function() { var self = this; setTimeout(function() { console.log(self.prop) // 37 },1000) } }; Obj.getProp();
其实,setTimeout
和 setInterval
都只是在全局上下文中执行一个函数而已,即使是在严格模式下:
'use strict'; function foo() { console.log(this); // Window } setTimeout(foo, 1);
记住 setTimeout
和 setInterval
都只是在全局上下文中执行一个函数而已,因此 this 指向全局对象。 除非你实用箭头函数,Function.prototype.bind
方法等办法修复。至于解决方案会在后续的文章中继续讨论。
作为构造函数调用
JavaScript 支持面向对象式本赛季,与主流的面向对象式本赛季语言不同,JavaScript 并没有类(class)的概念,而是使用基于原型(prototype)的继承方式。作为又一项约定通用的准则,构造函数以大写字母开头,提醒调用者使用正确的方式调用。
当一个函数用作构造函数时(使用 new
关键字),它的 this
被绑定到正在构造的新对象,也就是我们常说的实例化出来的对象。
function Person(name) { this.name = name; } var p = new Person(''); console.log(p.name); // ""
几个陷阱
如果构造函数具有返回对象的 return
语句,则该返回对象将是 new
表达式的结果。
function Person(name) { this.name = name; return { title : "Tian开发" }; } var p = new Person(''); console.log(p.name); // undefined console.log(p.title); // "Tian开发"
相应的,JavaScript 中的构造函数也很特殊,如果不使用 new
调用,则和普通函数一样, this 仍然执行全局:
function Person(name) { this.name = name; console.log(this); // Window } var p = Person('');
箭头函数中的 this
在箭头函数中,this
与 封闭的词法上下文的 this
保持一致,也就是说由上下文确定。
var obj = { x: 10, foo: function() { var fn = () => { return () => { return () => { console.log(this); //{x: 10, foo: ?} 即 obj console.log(this.x); //10 } } } fn()()(); } } obj.foo();
obj.foo
是一个匿名函数,无论如何, 这个函数中的 this 指向它被创建时的上下文(在上面的例子中,就是 obj
对象)。这同样适用于在其他函数中创建的箭头函数:这些箭头函数的this 被设置为外层执行上下文。
// 创建一个含有bar方法的obj对象,bar返回一个函数,这个函数返回它自己的this, // 这个返回的函数是以箭头函数创建的,所以它的this被永久绑定到了它外层函数的this。 // bar的值可以在调用中设置,它反过来又设置返回函数的值。 var obj = { bar: function() { var x = (() => this); return x; } }; // 作为obj对象的一个方法来调用bar,把它的this绑定到obj。 // x所指向的匿名函数赋值给fn。 var fn = obj.bar(); // 直接调用fn而不设置this,通常(即不使用箭头函数的情况)默认为全局对象,若在严格模式则为undefined console.log(fn() === obj); // true // 但是注意,如果你只是引用obj的方法,而没有调用它(this是在函数调用过程中设置的) var fn2 = obj.bar; // 那么调用箭头函数后,this指向window,因为它从 bar 继承了this。 console.log(fn2()() == window); // true
在上面的例子中,一个赋值给了 obj.bar
的函数(称为匿名函数 A),返回了另一个箭头函数(称为匿名函数 B)。因此,函数B的this被永久设置为 obj.bar
(函数A)被调用时的 this
。当返回的函数(函数B)被调用时,它this始终是最初设置的。在上面的代码示例中,函数B的 this
被设置为函数A的 this
,即 obj
,所以它仍然设置为 obj
,即使以通常将 this
设置为 undefined
或全局对象(或者如前面示例中全局执行上下文中的任何其他方法)进行调用。
填坑
我们回到上面 setTimeout 的坑:
var prop = 0; var Obj = { prop: 37, getProp: function() { setTimeout(function() { console.log(this.prop) // 结果是 0 ,不是37! },1000) } }; Obj.getProp();
通常情况我,我们在这里期望输出的结果是 37
,用箭头函数解决这个问题相当简单:
var Obj = { prop: 37, getProp: function() { setTimeout(() => { console.log(this.prop) // 37 },1000) } }; Obj.getProp();
原型链中的 this
相同的概念在定义在原型链中的方法也是一致的。如果该方法存在于一个对象的原型链上,那么 this
指向的是调用这个方法的对象,就好像该方法本来就存在于这个对象上。
var o = { f : function(){ return this.a + this.b; } }; var p = Object.create(o); p.a = 1; p.b = 4; console.log(p.f()); // 5
在这个例子中,对象 p
没有属于它自己的f属性,它的f属性继承自它的原型。但是这对于最终在 o
中找到 f
属性的查找过程来说没有关系;查找过程首先从 p.f
的引用开始,所以函数中的 this
指向 p
。也就是说,因为f是作为p的方法调用的,所以它的this
指向了 p
。这是 JavaScript 的原型继承中的一个有趣的特性。
你也会看到下面这种形式的老代码,道理是一样的:
function Person(name) { this.name = name; } Person.prototype = { getName:function () { return this.name } }; var p = new Person(''); console.log(p.getName()); // ""
getter 与 setter 中的 this
再次,相同的概念也适用时的函数作为一个 getter
或者 一个 setter
调用。用作 getter
或 setter
的函数都会把 this
绑定到正在设置或获取属性的对象。
function sum() { return this.a + this.b + this.c; } var o = { a: 1, b: 2, c: 3, get average() { return (this.a + this.b + this.c) / 3; } }; Object.defineProperty(o, 'sum', { get: sum, enumerable: true, configurable: true}); console.log(o.average, o.sum); // logs 2, 6
作为一个DOM事件处理函数
当函数被用作事件处理函数时,它的 this
指向触发事件的元素(一些浏览器在使用非addEventListener
的函数动态添加监听函数时不遵守这个约定)。
// 被调用时,将关联的元素变成蓝色 function bluify(e){ console.log(this === e.currentTarget); // 总是 true // 当 currentTarget 和 target 是同一个对象是为 true console.log(this === e.target); this.style.backgroundColor = '#A5D9F3'; } // 获取文档中的所有元素的列表 var elements = document.getElementsByTagName('*'); // 将bluify作为元素的点击监听函数,当元素被点击时,就会变成蓝色 for(var i=0 ; i < elements.length; i++){ elements[i].addEventListener('click', bluify, false); }
作为一个内联事件处理函数
当代码被内联on-event 处理函数调用时,它的this指向监听器所在的DOM元素:
<button onclick="alert(this.tagName.toLowerCase());"> Show this </button>
上面的 alert 会显示 button
。注意只有外层代码中的 this
是这样设置的:
<button onclick="alert((function(){return this})());"> Show inner this </button>
在这种情况下,没有设置内部函数的 this
,所以它指向 global
/window
对象(即非严格模式下调用的函数未设置 this
时指向的默认对象)。
使用 apply 或 call 调用
JavaScript 中函数也是对象,对象则有方法,apply
和 call
就是函数对象的方法。这两个方法异常强大,他们允许切换函数执行的上下文环境(context),即 this
绑定的对象。很多 JavaScript 中的技巧以及类库都用到了该方法。让我们看一个具体的例子:
function Point(x, y){ this.x = x; this.y = y; this.moveTo = function(x, y){ this.x = x; this.y = y; } } var p1 = new Point(0, 0); p1.moveTo(1, 1); console.log(p1.x,p1.y); //1 1 var p2 = {x: 0, y: 0}; p1.moveTo.apply(p2, [10, 10]); console.log(p2.x,p2.y); //10 10
在上面的例子中,我们使用构造函数生成了一个对象 p1
,该对象同时具有 moveTo
方法;使用对象字面量创建了另一个对象 p2
,我们看到使用 apply
可以将 p1
的方法 apply 到 p2
上,这时候 this
也被绑定到对象 p2
上。另一个方法 call
也具备同样功能,不同的是最后的参数不是作为一个数组统一传入,而是分开传入的:
function Point(x, y){ this.x = x; this.y = y; this.moveTo = function(x, y){ this.x = x; this.y = y; } } var p1 = new Point(0, 0); p1.moveTo(1, 1); console.log(p1.x,p1.y); //1 1 var p2 = {x: 0, y: 0}; p1.moveTo.call(p2, 10, 10); // 只是参数不同 console.log(p2.x,p2.y); //10 10
.bind() 方法
ECMAScript 5 引入了 Function.prototype.bind
。调用 f.bind(someObject)
会创建一个与 f
具有相同函数体和作用域的函数,但是在这个新函数中,this
将永久地被绑定到了 bind
的第一个参数,无论这个函数是如何被调用的。
function f(){ return this.a; } //this被固定到了传入的对象上 var g = f.bind({a:"azerty"}); console.log(g()); // azerty var h = g.bind({a:'yoo'}); //bind只生效一次! console.log(h()); // azerty var o = {a:37, f:f, g:g, h:h}; console.log(o.f(), o.g(), o.h()); // 37, azerty, azerty
填坑
上面我们已经讲了使用箭头函数填 setTimeout 的坑,这次我们使用 bind
方法来试试:
var prop = 0; var Obj = { prop: 37, getProp: function() { setTimeout(function() { console.log(this.prop) // 37 }.bind(Obj),1000) } }; Obj.getProp();
同样可以填坑,但是看上去没有使用箭头函数来的优雅。
小结
本文介绍了 JavaScript 中的 this 关键字在各种情况下的含义,虽然这只是 JavaScript 中一个很小的概念,但借此我们可以深入了解 JavaScript 中函数的执行环境,而这是理解闭包等其他概念的基础。掌握了这些概念,才能充分发挥 JavaScript 的特点,才会发现 JavaScript 语言特性的强大。
很是全面很是长