javascript函数中的this对象和其他语言比较起来很很大不同,甚至在严格模式和非严格模式下都有不同。
大多数情况下,this对象是有函数的调用对象决定。在任务执行过程中,this对象不能被修改。ECMAScript 5引入了bind方法来设置调用中的this对象,实际上就是传递上下文,下次有时间,可以具体讨论下bind方法,很多框架都已经实现了,也就是说早就有了实际标准 —— 先上车后买票。
就这么些颇为空洞,下面就使用几个例子
function fn(){ return this; } alert(fn() === window); //true
这个例子,结果和原因都比较明显,fn()相当于全局对象window在调用,等同于window.fn(),因而this对象指向window对象。
var x = 0; function fn(){ alert(this.x); } var obj = { x: 1, f: fn, } obj.f(); //1
这个例子注意,obj对象的f属性指向fn函数体,所以最后的obj.f()调用,就是对象obj在调用函数,因此套用规则,this.x显示1。
var x = 0; function test(){ return function(){ console.log(this.x); }; } var o = {x:1}; o.f = test; o.f()(); //0
那再看这个例子,最终显示结果为0,这是为何呢?通俗点讲,对象o的属性f是test函数的引用,o.f()得到的结果是function(){console.log(this.x);},单独执行这个函数,this对象当然指向全局对象window,因而结果为0。这里就简单地理解this对象,不要去看太复杂的例子,记住:万变不离其宗,this对象指向调用包裹它本身的函数的对象。
另外还需要注意的是,使用一个变量保存this对象,可以改变上下文。柯里化的函数绑定是比较典型的传递上下文技术。在这里就不多讲,下次跟函数绑定一起谈谈。
最后总结一下,this的处理机制分为五种不同的情况:
①、全局范围,指向全局对象;
②、函数直接调用,同样指向全局对象,例如:func(); -- ES5严格模式下,会是undefined,因为不存在全局变量
③、方法调用,指向调用方法的对象,例如:fn.func(); this指向fn对象;
④、调用构造函数,例如:new func();这里this指向新创建的对象;
⑤、传递上下文,例如:apply(context, args)或者call(context, arg1, arg2...),this指向context
还有一种特别容易误解地方,内部函数,即声明在另外一个函数体内的函数,如下所示。
Fn.method = function() { function func() { // this 将会被设置为全局对象 } func(); }
大多数初学者很容易误解,代码当中的this指向Fn对象,其实指向window全局对象;若真需要使用Fn对象,可以在method方法内创建一个变量来引用Fn对象,通常情况下,开发人员习惯使用that或者self替换,约定俗成。
Fn.method = function() { var self= this; function func() { // 使用 self来指向 Fn 对象 } func(); }
通过上面的通篇描述,相信大家都能很好地理解this对象的工作机制了。若大家觉得都掌握了要点,那么在此基础上可以适当地拓展下。若到此还是云里雾里的,推荐你详读下这篇文章:http://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/。
函数的执行上下文(context)
一个函数被调用,或者直接执行,都会创建一个执行上下文(execution context),函数所有的行为都发生在此执行上下文中。要构建执行上下文,javascript首先会创建arguments变量,包含调用函数时传入的参数集合。接着会创建作用域链(scope chain),然后初始化变量,包括函数的形参(parameter)。如果函数内部有函数,则接着初始化这些内部函数;如果没有内部函数,则继续将局部变量初始为undefined,真正地赋值操作在执行上下文创建完成之后,函数执行之时才会赋值。最后给this对象赋值。
执行上下文创建完成之后,函数会逐行执行,所需的变量都会从已经构建好的执行上下文中读取。这也是创建上下文的目的和意义之一。
函数绑定
另一个能改变this对象的知识扩展点,函数绑定:指创建一个函数,在特定的上下文中指定参数调用另一个函数。该技巧经常跟回调函数和事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行上下文。
var x = 9; var obj= { x: 81, getX: function(y) {console.log('x: ' + this.x + ', y: ' + y);} }; obj.getX(); //结果为x: 81, y: undefined var fn = obj.getX; fn(); //结果为x: 9, y: undefined
上述结果的原因相信大家都知道,fn()执行时,并没有保存obj上下文,实际上它是在全局上下文中执行,因而得到的结果为9。但是,我们可以通过函数绑定技巧来传递上下文改变这样的结果,以达到我们的预期。
function bind(func, context){ var args = Array.prototype.slice.call(arguments, 2); //两次调用,args都为空,因为bind(obj.getX, obj),并未传递第三个参数 return function(){ var innerArgs = Array.prototype.slice.call(arguments); //fn(88)innerArgs为88,fn()为空 var finalArgs = args.concat(innerArgs); return func.apply(context, finalArgs); }; } var fn = bind(obj.getX, obj); //传递了函数和上下文 fn(); //结果为x: 81, y: undefined fn(88); //结果为x: 81, y: 88
bind函数使用了两种技巧:①、函数绑定;②、参数柯里化。bind函数传递了两个参数,一个为被调用的函数,一个为期望的上下文。接下来看看函数体做了哪些事情?整体上看去bind函数用到了闭包,返回了一个函数,并且在返回的函数体内访问了外面的变量args。变量args是用来保存了bind函数传递进来的实参集合,并且从第三位开始获取,去掉func、context的实际参数值;返回的函数体内,innerArgs用来保存返回函数的实际参数,也就是例子中的fn()的参数;接着将args参数和调用的innerArgs拼接成一个新数组,最后返回func在context上下文中执行的结果。
绑定函数能提供强大的动态函数创建功能,但不能滥用,毕竟每个函数都会带来额外的开销。