代码改变世界

javascript中this对象之 —— 意乱情迷

2013-04-25 23:45  MoltBoy  阅读(430)  评论(0编辑  收藏  举报

  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上下文中执行的结果。

  绑定函数能提供强大的动态函数创建功能,但不能滥用,毕竟每个函数都会带来额外的开销。