在JavaScript中,每当我们使用函数或者阅读已经写好的函数时,我们经常会看到里面用到this关键字的情况。
对于一个JS的初学者(尤其是对于有“主流”语言编程经验的人),会觉得this是让人凌乱的东西。
为了理解this,我们需要理解函数的调用方法。
在《Javascript Good Parts》一书中,将函数分为了四种调用方式:
方法调用;
函数式调用;
构造器调用;
apply(call)调用。
通读《Javascript Good Parts》的4.3节(2009年4月第一版)后,我对调用方式做了一个简短的总结。
当每个函数被调用的时候,他将会收到两个参数:this和arguments。为了方便起见,我们先写一个函数。
// 例1
var f = function(x, y) {
var count = x + y + this.z;
this.total = count;
console.log(this.total);
};
当函数被调用的时候,就会得到this和arguments。
一、arguments是什么?
arguments是比较好理解的,它是一个传入实际参数的数组。这里之所以用“实际参数”,是为了和this相对应,在我们的例子中,arguments就是x和y。
由于有arguments的存在,所以js可以不必在函数定义的时候规定参数,比如上一个函数完全可以写成。
// 例2
var f = function() {
var count = arguments[0] + arguments[1] + this.z;
this.total = count;
console.log(this.total);
};
虽然上面的函数可读性变差了,但是它和例1的功能是一样的。
基于以上原理,JS并不会支持如Java一样重载,如果有这样的代码:
// 例3
var o = {
f: function(){ console.log(1); },
f: function(){ console.log(2); },
};
//注意,这段代码没有错误的,JS真是自由的国度。
但是o只会承认第二的方法,即,后面的芳芳会覆盖前面的。
二、开始this吧
下面到了this,其实上面提到的四种调用方式的区别,从this的角度看,只是this所代表的地方不同。
2.1 方法式调用。
这要求函数必须在一个对象里面,比如:
// 例4
var o = {
f: function() {
console.log(this); //
}
};
当我们使用o.f()的时候,打印出来的就是对象o。
可以看出,函数作为方法调用中,函数使用的this就是函数所在的对象。
2.2 函数式调用。
// 例5
var f = function() {
console.log(this);
};
f()执行的时候,打印出来的是全局对象。如果你使用浏览器的开发者工具(比如打开Chrome,按F12后用Console界面)输入例子,你会得到Window对象。
2.3 构造器式调用。
// 例6
var Fun = function() {
console.log(this);
};
Fun.prototype.x = function() {
return 1;
};
Fun.prototype.y = 2;
对于例6中的函数,当我们执行var f = new Fun();的时候,this是什么呢?是Fun.prototype,即Fun的原型链。
2.4 apply/call式调用
我们先了解一下apply和call调用函数的方式:
Fun.apply(Obj, [x, y]);
Fun.call(Obj, x, y);
以上两个方式,都是以Obj为对象,以x和y为参数,调用方法Fun。所不同的是apply要求参数是数组,call要求参数挨个传入。
这样我们就可以方法式调用来理解apply式的调用。
我们大致可以了解this是什么了,this就是当前函数的执行环境。那么以下几个理解或许有助于我们记忆。
如例5中函数式调用,可以把f()看作是window.f();
例6中new的调用方式可以看成Fun.call(Fun.prototype, null),当然最后还有需要把生成结果返回给f;
再进一步,所有的前三种调用方式都可以用call来模拟。如例4中o.f()可以看做f.call(o, null),而例5中f()可以看做f.call(window, null);
而把this和arguments并列理解,也就可以解释为什么函数调用时,为什么要function.apply(object, arguments)了。
三、还有几个问题
以上的解释可以理解函数调用中大部分this的问题,但是我们要注意几个问题。
3.1 匿名函数的调用。
(function() { console.log(this); })();
JS很自由,它允许函数没有名字,这种情况下,this仍然指向全局对象(浏览器中为window)。
3.2 方法中函数式的调用。
这个说法很拗口,但是确有存在。
// 例7
var o = {
f: function() {
var innerF = function() {
console.log(this);
};
innerF();
}
};
当我们使用o.f();的时候,innerF也被顺便调用了,但是打印出来的结果仍然是全局对象。
这很让人头疼,如果说例5中f()打印出来的是全局对象是因为函数f就写在window下,这里innerF距离window隔了好几层。
但是现实如此,我们只能接受。具体原因估计要查ECMAScript最初开发者的交流邮件!
这里就引入了一个问题,如果innerF想调用对象o的代码怎么办?
好在可以“曲线救国”:
var o = {
f: function() {
var that = this;
var innerF = function() {
console.log(that);
};
innerF();
}
};
当使用o.f();的时候,打印出来的就是对象o,而不是全局对象。
3.3 回调函数中的this。
// 例8
var f = function(cb) {
cb();
};
var o = {
callback: function() {
console.log(this);
}
};
f(o.callback);的结果仍然是window,可见函数的调用方式决定着this,把this和arguments作为参数理解,就更容易理解了。
当然回调函数被调用的方式是由原函数决定的。
对例8稍作修改,就可以得到不一样的结果。
// 例9
var f = function(callback) {
var innerO = {
x: 1,
cb: callback
};
innerO.cb();
};
var o = {
callback: function() {
console.log(this);
}
};
f(o.callback);
var f = function(callback) {
var innerO = {
x: 1,
cb: callback
};
innerO.cb();
};
var o = {
callback: function() {
console.log(this);
}
};
执行例9的f(o.callback);,打印结果是innerO对象。所以回调函数的使用就要看原函数的作者的意图了。
以上对于this的理解只是表象的总结,深入的了解需要查阅资料和了解浏览器的解析器
诚所谓,路漫漫其修远兮,吾将上下而求索。
var f = function(cb) {
cb();
};
var o = {
callback: function() {
console.log(this);
}
};
f(o.callback);
var f = function(cb) {
cb();
};
var o = {
callback: function() {
console.log(this);
}
};
f(o.callback);
var f = function(cb) {
cb();
};
var o = {
callback: function() {
console.log(this);
}
};
f(o.callback);
var f = function(cb) {
cb();
};
var o = {
callback: function() {
console.log(this);
}
};
f(o.callback);