原文:all this 审阅校对:leeyeh
习惯了传统语言的你或许觉得JavaScript中的this
跟Java这些面向对象语言相似,保存了实体属性的一些值。其实不然。将它视作幻影魔神 比较恰当,手提一个装满未知符文的灵龛 。
以下内容我希望大家都来了解。全是掏箱底的干货,其中大部分占用了我很多时间才掌握。
全局this
浏览器宿主的全局环境中,this
指的是window
对象。
1
2
3
<script type ="text/javascript" >
console .log(this === window );
</script >
示例
浏览器中在全局环境下,使用var
声明变量其实是赋值到了this
或window
里面。
1
2
3
4
5
<script type ="text/javascript" >
var foo = "bar" ;
console .log(this .foo);
console .log(window .foo);
</script >
示例
任何情况下,创建变量时没有使用var
或者let
(ECMAScript 6),也是在向全局this
中添加或改变属性。
1
2
3
4
5
6
7
8
9
10
11
<script type ="text/javascript" >
foo = "bar" ;
function testThis () {
foo = "foo" ;
}
console .log(this .foo);
testThis();
console .log(this .foo);
</script >
示例
Node命令行(REPL)中,this
是全局命名空间。可以通过global
来访问。
1
2
3
4
5
6
7
> this
{ ArrayBuffer: [Function: ArrayBuffer],
Int8Array: { [Function: Int8Array] BYTES_PER_ELEMENT: 1 },
Uint8Array: { [Function: Uint8Array] BYTES_PER_ELEMENT: 1 },
...
> global === this
true
在Node环境里执行的JS脚本中,this
其实是个空对象,有别于global
。
test.js 1
2
console .log(this );
console .log(this === global);
当尝试在Node中执行JS脚本时,脚本中全局作用域中的var
并不会将变量添加到全局this
,这与在浏览器中是不一样的。
test.js 1
2
var foo = "bar" ;
console .log(this .foo);
1
2
$ node test.js
undefined
…但在命令行里进行求值却会添加到this
身上。
1
2
3
4
5
> var foo = "bar" ;
> this.foo
bar
> global.foo
bar
在Node里执行的脚本中,创建变量时没带var
或let
关键字,会修改全局的global
但不是this
(译注:上面已经提到this
和global
不是同一个对象,所以这里就不奇怪了)。
test.js 1
2
3
foo = "bar" ;
console .log(this .foo);
console .log(global.foo);
1
2
3
$ node test.js
undefined
bar
但在Node命令行里,就会赋值给两者了。
译注:简单来说,Node脚本中global
和this
是区别对待的,而Node命令行中,两者可等效为同一对象。
函数或方法里的this
除了DOM的事件回调或者提供了执行上下文(后面会提到)的情况,函数被正常调用(不带new
)时,里面的this
指向的是全局作用域。
1
2
3
4
5
6
7
8
9
10
11
<script type ="text/javascript" >
foo = "bar" ;
function testThis () {
this .foo = "foo" ;
}
console .log(this .foo);
testThis();
console .log(this .foo);
</script >
示例
test.js 1
2
3
4
5
6
7
8
9
foo = "bar" ;
function testThis () {
this .foo = "foo" ;
}
console .log(global.foo);
testThis();
console .log(global.foo);
还有个例外,就是使用了"use strict";
。此时this
是undefined
。
1
2
3
4
5
6
7
8
9
10
11
<script type ="text/javascript" >
foo = "bar" ;
function testThis () {
"use strict" ;
this .foo = "foo" ;
}
console .log(this .foo);
testThis();
</script >
示例
当用调用函数时使用了new
关键字,此刻this
指代一个新的上下文,不再指向全局this
。
1
2
3
4
5
6
7
8
9
10
11
12
13
<script type ="text/javascript" >
foo = "bar" ;
function testThis () {
this .foo = "foo" ;
}
console .log(this .foo);
new testThis();
console .log(this .foo);
console .log(new testThis().foo);
</script >
示例
我习惯将这个新的上下文称作实例。
原型中的this
函数创建后其实以一个函数对象的形式存在着。既然是对象,则其自动获得了一个叫做prototype
(原型)的属性,你可以自由地对这个属性进行属性添加或修改。当配合new
关键字来调用一个函数创建实例后,此刻便能直接访问到原型身上的值。当然,是通过this
来进行的。
1
2
3
4
5
6
7
8
function Thing () {
console .log(this .foo);
}
Thing.prototype.foo = "bar" ;
var thing = new Thing();
console .log(thing.foo);
示例
当通过new
的方式创建了多个实例后,他们会共用一个原型。比如,每个实例的this.foo
都返回相同的值,除非你在某个实例中重写了this.foo
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Thing () {
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
console .log(this .foo);
}
Thing.prototype.setFoo = function (newFoo) {
this .foo = newFoo;
}
var thing1 = new Thing();
var thing2 = new Thing();
thing1.logFoo();
thing2.logFoo();
thing1.setFoo("foo" );
thing1.logFoo();
thing2.logFoo();
thing2.foo = "foobar" ;
thing1.logFoo();
thing2.logFoo();
示例
在实例中,this
是个特殊的对象,而this
自身其实只是个关键字。你可以把this
想象成在实例中访问挂载在原型上的值的一种途径,同时对this
里面属性的改变又会隐藏原型上原来的值。如果需要访问挂载在原型上的值,你可以删除挂载在实例上的值…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Thing () {
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
console .log(this .foo);
}
Thing.prototype.setFoo = function (newFoo) {
this .foo = newFoo;
}
Thing.prototype.deleteFoo = function () {
delete this .foo;
}
var thing = new Thing();
thing.setFoo("foo" );
thing.logFoo();
thing.deleteFoo();
thing.logFoo();
thing.foo = "foobar" ;
thing.logFoo();
delete thing.foo;
thing.logFoo();
示例
…或者直接引用函数的原型。
1
2
3
4
5
6
7
8
9
10
function Thing () {
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
console .log(this .foo, Thing.prototype.foo);
}
var thing = new Thing();
thing.foo = "foo" ;
thing.logFoo();
示例
同一函数创建的所有实例均共享一个原型。如果你给原型赋值了一个数组,那么所有实例都能获取到这个数组。除非你在某个实例中对其进行了重写,实事上是进行了覆盖。
1
2
3
4
5
6
7
8
9
function Thing () {
}
Thing.prototype.things = [];
var thing1 = new Thing();
var thing2 = new Thing();
thing1.things.push("foo" );
console .log(thing2.things);
示例
通常上面的做法是不正确的(译注:改变thing1
的同时也影响了thing2
)。如果你想每个实例互不影响,那么请在函数里创建这些值,而不是在原型上。
1
2
3
4
5
6
7
8
9
10
function Thing () {
this .things = [];
}
var thing1 = new Thing();
var thing2 = new Thing();
thing1.things.push("foo" );
console .log(thing1.things);
console .log(thing2.things);
示例
多个原型可以形成原型链,这样this
便会在原型链上逐步往上找直到找到你想引用的值。
1
2
3
4
5
6
7
8
9
10
11
function Thing1 () {
}
Thing1.prototype.foo = "bar" ;
function Thing2 () {
}
Thing2.prototype = new Thing1();
var thing = new Thing2();
console .log(thing.foo);
示例
很多人便是利用这个特性在JS中模拟经典的对象继承。
注意,在用于开成原型链的函数中对this
进行修改会覆盖之前同名的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Thing1 () {
}
Thing1.prototype.foo = "bar" ;
function Thing2 () {
this .foo = "foo" ;
}
Thing2.prototype = new Thing1();
function Thing3 () {
}
Thing3.prototype = new Thing2();
var thing = new Thing3();
console .log(thing.foo);
示例
我习惯将赋值到原型上的函数称作方法。上面某些地方便使用了方法这样的字眼,比如logFoo
方法。这些方法中的this
同样具有在原型链上查找引用的魔力。通常将最初用来创建实例的函数称作构造函数。
原型链方法中的this
是从实例中的this
开始住上查找整个原型链的。也就是说,如果原型链中某个地方直接对this
进行赋值覆盖了某个变量,那么,不管是在哪一级原型链的方法中,我们拿到的都会是覆盖后的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Thing1 () {
}
Thing1.prototype.foo = "bar" ;
Thing1.prototype.logFoo = function () {
console .log(this .foo);
}
function Thing2 () {
this .foo = "foo" ;
}
Thing2.prototype = new Thing1();
var thing = new Thing2();
thing.logFoo();
示例
在JavaScript中,函数可以嵌套函数,也就是你可以在函数里面继续定义函数。但内层函数是通过闭包获取外层函数里定义的变量值的,而不是直接继承this
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Thing () {
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
var info = "attempting to log this.foo:" ;
function doIt () {
console .log(info, this .foo);
}
doIt();
}
var thing = new Thing();
thing.logFoo();
示例
上面示例中,doIt
函数中的this
指代全局作用域,如果使用了"use strict";
声明的话是undefined
。对于很多新手来说,理解这点是非常头疼的。
还有更奇葩的。把实例的方法作为参数传递时,实例是不会跟着过去的。也就是说,此时方法中的this
在调用时指向的是全局this
,如果使用了"use strict";
声明的话是undefined
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Thing () {
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
console .log(this .foo);
}
function doIt (method) {
method();
}
var thing = new Thing();
thing.logFoo();
doIt(thing.logFoo);
示例
所以很多人习惯将this
暂存起来,用个叫self
或者其他什么的变量来保存,以将外层与内层的this
区分开来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Thing () {
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
var self = this ;
var info = "attempting to log this.foo:" ;
function doIt () {
console .log(info, self.foo);
}
doIt();
}
var thing = new Thing();
thing.logFoo();
示例
…但上面的方式不是万能的,在将方法做为参数传递时,就不起作用了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Thing () {
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
var self = this ;
function doIt () {
console .log(self.foo);
}
doIt();
}
function doItIndirectly (method) {
method();
}
var thing = new Thing();
thing.logFoo();
doItIndirectly(thing.logFoo);
示例
解决方法就是传递的时候使用bind
方法显示指明上下文,bind
方法是所有函数或方法都具有的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Thing () {
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
console .log(this .foo);
}
function doIt (method) {
method();
}
var thing = new Thing();
doIt(thing.logFoo.bind(thing));
示例
同时也可以使用apply
或call
来调用该方法或函数,让它在一个新的上下文中执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Thing () {
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
function doIt () {
console .log(this .foo);
}
doIt.apply(this );
}
function doItIndirectly (method) {
method();
}
var thing = new Thing();
doItIndirectly(thing.logFoo.bind(thing));
示例
使用bind
可以任意改变函数或方法的执行上下文,即使它没有被绑定到一个实例的原型上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Thing () {
}
Thing.prototype.foo = "bar" ;
function logFoo (aStr) {
console .log(aStr, this .foo);
}
var thing = new Thing();
logFoo.bind(thing)("using bind" );
logFoo.apply(thing, ["using apply" ]);
logFoo.call(thing, "using call" );
logFoo("using nothing" );
示例
避免在构造函数中返回任何东西,因为返回的东西可能覆盖本来该返回的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
function Thing () {
return {};
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
console .log(this .foo);
}
var thing = new Thing();
thing.logFoo();
示例
但,如果你在构造函数里返回的是个原始值比如字符串或者数字什么的,上面的错误就不会发生了,返回语句将被忽略。所以最好别在一个将要通过new
来调用的构造函数中返回任何东西,即使你是清醒的。如果你想实现工厂模式,那么请用一个函数来创建实例,并且不通过new
来调用。当然这只是个人建议。
诚然,你也可以使用Object.create
从而避免使用new
。这样也能创建一个实例。
1
2
3
4
5
6
7
8
9
10
11
12
function Thing () {
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
console .log(this .foo);
}
var thing = Object .create(Thing.prototype);
thing.logFoo();
示例
这种方式不会调用该构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
function Thing () {
this .foo = "foo" ;
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
console .log(this .foo);
}
var thing = Object .create(Thing.prototype);
thing.logFoo();
示例
正因为Object.create
没有调用构造函数,这在当你想实现继承时是非常有用的,随后你可能想要重写构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Thing1 () {
this .foo = "foo" ;
}
Thing1.prototype.foo = "bar" ;
function Thing2 () {
this .logFoo();
Thing1.apply(this );
this .logFoo();
}
Thing2.prototype = Object .create(Thing1.prototype);
Thing2.prototype.logFoo = function () {
console .log(this .foo);
}
var thing = new Thing2();
示例
对象中的this
可以在对象的任何方法中使用this
来访问该对象的属性。这与用new
得到的实例是不一样的。
1
2
3
4
5
6
7
8
var obj = {
foo: "bar" ,
logFoo: function () {
console .log(this .foo);
}
};
obj.logFoo();
示例
注意这里并没有使用new
,也没有用Object.create
,更没有函数的调用来创建对象。也可以将函数绑定到对象,就好像这个对象是一个实例一样。
1
2
3
4
5
6
7
8
9
var obj = {
foo: "bar"
};
function logFoo () {
console .log(this .foo);
}
logFoo.apply(obj);
示例
此时使用this
没有向上查找原型链的复杂工序。通过this
所拿到的只是该对象身上的属性而已。
1
2
3
4
5
6
7
8
9
10
var obj = {
foo: "bar" ,
deeper: {
logFoo: function () {
console .log(this .foo);
}
}
};
obj.deeper.logFoo();
示例
也可以不通过this
,直接访问对象的属性。
1
2
3
4
5
6
7
8
9
10
var obj = {
foo: "bar" ,
deeper: {
logFoo: function () {
console .log(obj.foo);
}
}
};
obj.deeper.logFoo();
示例
DOM 事件回调中的this
在DOM事件的处理函数中,this
指代的是被绑定该事件的DOM元素。
1
2
3
4
5
6
7
8
9
10
function Listener () {
document .getElementById("foo" ).addEventListener("click" ,
this .handleClick);
}
Listener.prototype.handleClick = function (event) {
console .log(this );
}
var listener = new Listener();
document .getElementById("foo" ).click();
示例
…除非你通过bind
人为改变了事件处理器的执行上下文。
1
2
3
4
5
6
7
8
9
10
function Listener () {
document .getElementById("foo" ).addEventListener("click" ,
this .handleClick.bind(this ));
}
Listener.prototype.handleClick = function (event) {
console .log(this );
}
var listener = new Listener();
document .getElementById("foo" ).click();
示例
HTML中的this
HTML标签的属性中是可以写JS的,这种情况下this
指代该HTML元素。
1
2
3
4
<div id ="foo" onclick ="console.log(this);" > </div >
<script type ="text/javascript" >
document .getElementById("foo" ).click();
</script >
示例
重写this
无法重写this
,因为它是一个关键字。
1
2
3
function test () {
var this = {};
}
示例
eval
中的this
eval
中也可以正确获取当前的 this
。
1
2
3
4
5
6
7
8
9
function Thing () {
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
eval ("console.log(this.foo)" );
}
var thing = new Thing();
thing.logFoo();
示例
这里存在安全隐患。最好的办法就是避免使用eval
。
使用Function
关键字创建的函数也可以获取this
:
1
2
3
4
5
6
7
function Thing () {
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = new Function ("console.log(this.foo);" );
var thing = new Thing();
thing.logFoo();
示例
使用with
时的this
使用with
可以将this
人为添加到当前执行环境中而不需要显示地引用this
。
1
2
3
4
5
6
7
8
9
10
11
12
13
function Thing () {
}
Thing.prototype.foo = "bar" ;
Thing.prototype.logFoo = function () {
with (this ) {
console .log(foo);
foo = "foo" ;
}
}
var thing = new Thing();
thing.logFoo();
console .log(thing.foo);
示例
正如很多人认为的那样,很多人认为使用with
是不好的,因为会产生歧义。
jQuery中的this
一如HTML DOM元素的事件回调,jQuery库中大多地方的this
也是指代的DOM元素。页面上的事件回调和一些便利的方法比如$.fn.each
(译注:原文为$.each
,是错误的,下面的示例代码也体现了应该是jQuery对象上的方法$.fn.each
,感谢@leeyeh 发现)都是这样的。
1
2
3
4
5
6
7
8
9
10
11
12
13
<div class ="foo bar1" > </div >
<div class ="foo bar2" > </div >
<script type ="text/javascript" >
$(".foo" ).each(function () {
console .log(this );
});
$(".foo" ).on("click" , function () {
console .log(this );
});
$(".foo" ).each(function () {
this .click();
});
</script >
示例
传递 this
如果你用过underscore.js 或者lo-dash 你便知道,这两个库中的很多方法允许你传递一个参数来显式指定执行的上下文。比如_.each
。自ECMAScript 5标准后,一些原生的JS方法也允许传递上下文参数,比如forEach
。事实上,上文提到的bind
,apply
还有call
已经给我们手动指定函数执行上下文的能力了。
1
2
3
4
5
6
7
8
9
10
11
12
13
function Thing (type) {
this .type = type;
}
Thing.prototype.log = function (thing) {
console .log(this .type, thing);
}
Thing.prototype.logThings = function (arr) {
arr.forEach(this .log, this );
_.each(arr, this .log, this );
}
var thing = new Thing("fruit" );
thing.logThings(["apples" , "oranges" , "strawberries" , "bananas" ]);
示例
这样可以使得代码简洁些,不用层层嵌套bind
,也不用不断地缓存this
。
一些编程语言上手很简单,比如Go语言手册 可以被快速读完。然后你差不多就掌握这门语言了,只是在实战时会有些小的问题或陷阱在等着你。
而JavaScript不是这样的。手册难读。非常多缺陷在里面,以至于人们抽离出了JavaScript精粹
(The Good Parts )。最好的文档可能是MDN 上的了。所以我建议你看看那上面关于this
的介绍,并且始终在搜索JS相关问题时加上”mdn” 来获得最好的文档资料。静态代码检查也是极好的,比如jshint 。
欢迎勘误及讨论,我的推特@bjorntipling。