文章结构


普通的 this

使用new操作符调用构造函数生成对象时,构造函数内的this为当前所new出来的对象。

1
2
3
4
5
代码一:
function Car() {
console.log(this);
}
new Car();

输出完整的对象结构:

1
Car {}

下面的代码定义了一个对象benz,并调用该对象的printHistory(),由对象benz调用方法体,所以方法体内的this为对象benz本身。

1
2
3
4
5
6
7
8
代码二:
var benz = {
printHistory : function() {
console.log(this);
}
};
benz.printHistory();

输出完整的类结构:

1
{ printHistory: [Function] }

以上两种情况,this的含义都很明确,即当前的对象本身。


外漏函数中的 this

1
2
3
4
5
6
7
8
9
10
代码三:
var benz = {
printHistory : function() {
console.log(this);
}
};
// 将对象中的方法暴露出来成为外漏函数
var printHistory = benz.printHistory;
printHistory();

执行代码后,控制台输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
D:\desk\JavaScript>node this.js
{ DTRACE_NET_SERVER_CONNECTION: [Function], // 打印开始...为全局对象或Window
DTRACE_NET_STREAM_END: [Function],
... ... ...
... ... ...
... ... ...
COUNTER_HTTP_CLIENT_REQUEST: [Function], // sodino.com
COUNTER_HTTP_CLIENT_RESPONSE: [Function],
global: [Circular],
process:
process {
title: '管理员: Node.js command prompt - node this.js',
version: 'v5.7.0',
moduleLoadList: // sodino.com
[ 'Binding contextify',
'Binding natives',
'NativeModule events',

以上代码打印的this直接为控制台全局对象GLOBAL或浏览器Window了。
这个现象会让很多人困惑。
要理解该现象的原因要从查看调用链去理解:被调用函数是一个对象的属性还是函数自己。
如果它被作为属性调用,那么this的值将变成该属性的对象,否则this的值将被默认赋值为控制台全局对象GLOBAL或浏览器的Window了。


嵌套函数中的 this

对 benz::printHistory()中增加一嵌套函数,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
代码四:
var benz = {
printHistory : function() {
// print history after AD2000
console.log(this); // 这里this是benz本身
(function(){
// print history before AD2000
console.log(this); // 这里this是Global或Window
})();
}
};
benz.printHistory();

控制台输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
D:\desk\JavaScript>node this.js
{ printHistory: [Function] } // 第一次打印仍为benz本身
{ DTRACE_NET_SERVER_CONNECTION: [Function], // 第二次打印开始...为全局对象或Window
DTRACE_NET_STREAM_END: [Function],
... ... ...
... ... ...
... ... ...
COUNTER_HTTP_CLIENT_REQUEST: [Function], // sodino.com
COUNTER_HTTP_CLIENT_RESPONSE: [Function],
global: [Circular],
process:
process {
title: '管理员: Node.js command prompt - node this.js',
version: 'v5.7.0',
moduleLoadList: // sodino.com
[ 'Binding contextify',
'Binding natives',
'NativeModule events',

以上代码,第一次打印this,仍是benz本身;但在嵌套函数中打印的this则为控制台全局对象或浏览器的Window了。
this不同于普通的对象声明,this的值无法在被嵌套的函数作用域中保持不变,这是JavaScript的缺陷。
但回想上文中“被调用函数是一个对象的属性还是函数自己”这一判断标准,嵌套函数并不是benz中的属性,所以this值为控制台全局对象或浏览器的Window也可以顺利理解了。

如果想在嵌套函数使用到上下文的this,有两种办法:
方法一:新声明一个变量持有上下文this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
代码五:
var benz = {
printHistory : function() {
// print something after AD2000
console.log(this);
var self = this; // 声明新变量持有this
(function(){
// print something before AD2000
console.log(self); // 引用上下文this
})();
}
};
benz.printHistory();

该段代码执行的控制台输出如下:

1
2
3
D:\desk\JavaScript>node this.js
{ printHistory: [Function] }
{ printHistory: [Function] }

方法二:使用Function.prototype.bind(thisArg[, arg1[, arg2[, …]]])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
代码六:
var benz = {
printHistory : function() {
// print something after AD2000
console.log(this);
(function(){
// print something before AD2000
console.log(this);
}).bind(this)();
}
};
benz.printHistory();

Function.prototype.bind()方法会返回一个新的函数体,新函数体的功能和原先函数保持一致,只是在新函数体内this的值为bind()方法参数中第一个参数的值。
bind()方法返回新函数后,就通过“bind(this) ()“直接被执行了。
由此,为控制台的两处打印信息都是benz本身:

1
2
3
D:\desk\JavaScript>node this.js
{ printHistory: [Function] }
{ printHistory: [Function] }

在维持上下文环境一致的实现选择上,bind()是更受推荐的方法。

由此,也引出下面的内容:如何篡改函数体中的this.


篡改函数中的 this

上文已经通过Function.prototype.bind()方法将嵌套函数中this的值指定为bind()的第一个参数值。JavaScript还有另外两个方法也可以实现同样的效果。

Function.prototype.apply(thisArg, [argsArray])
Function.prototype.call(thisArg[, arg1[, arg2[, …]]])

apply()和call()方法的功能是相同的,都是调用一个对象的一个方法,并用第一个参数的值替换该方法原来的对象,即替换掉方法中的this的值。
apply()与call()方法的区别只是所接收的方法参数不同,除了第一个参数外,apply()接收的第二个参数为数组,即允许将多个参数合并为一个数组传入,而call()只能从第二个参数起逐个一一传入。

通过对上文中代码二的修改,使用apply()或call()来篡改函数中this的实现如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
代码七:
var benz = {
printHistory : function() {
console.log(this);
}
};
var printHistory = benz.printHistory;
printHistory.apply(benz); // 指定了printHistory的上下文环境仍为benz
printHistory.call(benz); // 当没有额外的参数时,apply()和call()的用法是相同的

以上代码的控制台输出都打印出了benz本身,如下所示:

1
2
3
D:\desk\JavaScript>node this.js
{ printHistory: [Function] }
{ printHistory: [Function] }

另外,以下的几个方法同样也可以篡改callback函数中的this值,只是能用性没有apply()/call()/bind()强。
Array.prototype.find(callback[, thisArg])

Array.prototype.every(callback[, thisArg])

Array.prototype.forEach(callback[, thisArg])

Array.prototype.some(callback[, thisArg])


严格模式的判断

根据ECMAScript 3和非严格的ECMAScript 5对函数调用的规定,调用上下文(this的值)是全局对象。
在严格模式下,调用上下文(this的值)则是undefined。
所以”this“可以用来判断当前是否是严格模式。方法如下:

1
2
3
4
5
6
7
8
9
代码八:
'use strict'
// 'use strict'启用了严格模式,可以注释掉取消该模式
var isStrict = (function(){
return !this;
}());
console.log('isStrict=' + isStrict);

以上代码在控制台输出为:

1
2
D:\desk\JavaScript>node this.js
isStrict=true

总结

函数体内this就只有两个值,对象本身或运行时环境的全局对象。
在非严格模式下,当在Node.js控制台时,全局对象值为 GLOBAL ;当在HTML浏览器运行时,全局对象值为Window。
在严格模式下,全局对象是undefined。


后记

注意”总结“里这一句”函数体内this就只有两个值“特意说明了是在”函数体内“。那在函数体外呢?
sodino做了下测试,在不同的运行时环境中,函数体外this的值根据运行时环境而有所不同。

直接运行代码九中这一句打印语句

1
2
代码九:
console.log(this);

在Node.js控制台运行时,this是个{}空对象,如下图:
node.js

在Chrome浏览器中,this是Window。如下图:
Window


About Sodino