首页 » 编写高质量代码:改善JavaScript程序的188个建议 » 编写高质量代码:改善JavaScript程序的188个建议全文在线阅读

《编写高质量代码:改善JavaScript程序的188个建议》建议89:正确应用this

关灯直达底部

this总是指向当前作用域对象,如果当前定义对象的作用域没有发生变化,则this会指向当前对象。但是,this关键字的用法比较灵活,这也在一定程度上干扰了用户对于它所代表对象的准确判定。另外,this关键字可以存在于任何位置,并不局限于对象的方法内,还可以被应用在全局域内、函数内,以及其他特殊上下文环境中。

(1)函数的引用和调用

函数的引用和调用分别表示不同的概念,虽然它们都无法改变函数的定义作用域,但是引用函数能够改变函数的执行作用域,而调用函数是不会改变函数的执行作用域的。继续上面的示例进行说明:


var o={

name:/"对象o/",

f:function{

return this;

}

}

o.o1={

name:/"对象o1/",

me:o.f//引用对象o的方法f

}


可以看到,函数中的this所代表的是当前执行域对象o1:


var who=o.o1.me;

alert(who.name);//字符串/"对象o1/",说明当前this代表对象o1


如果把对象o1的me属性值改为函数调用:


o.o1={

name:/"对象o1/",

me:o.f//调用对象o的方法f

}


则会看到,函数中的this所代表的是定义函数时所在的作用域对象o:


var who=o.o1.me;

alert(who.name);//字符串/"对象o/",说明当前this代表对象o


(2)call和apply

call和apply方法可以直接改变被执行函数的作用域,使其作用域指向所传递的参数对象。因此,函数中包含的this关键字也指向参数对象。


function f{

//如果当前执行域对象的构造函数等于当前函数,则表示this为实例对象

if(this.constructor==arguments.callee)alert(/"this=实例对象/");

//如果当前执行域对象等于window,则表示this为Window对象

else if(this==window)alert(/"this=window对象/");

//如果当前执行域对象为其他对象,则表示this为其他对象

else alert(/"this==其他对象n this.constructor=/"+this.constructor);

}

f;//this指向Window对象

new f;//this指向实例对象

f.call(1);//this指向数值实例对象


在这个示例中,当直接调用函数f时,因为函数的执行作用域为全局域,所以this代表window。当使用new运算符调用函数时,将创建一个新的实例对象,函数的执行作用域为实例对象所在的上下文,因此,this就指向这个新创建的实例对象。

而在使用call方法执行函数f时,call会把函数f的作用域强制修改为参数对象所在的上下文。由于call方法的参数值为数字1,因此JavaScript解释器会把数字1强制封装为数值对象,此时this就会指向这个数值对象。

再看一个很有趣的用法。在下面这个示例中,call方法把函数f强制转换为对象o的一个方法并执行,这样函数f中的this就指代对象o,this.x的值就等于1,而this.y的值就等于2,结果就返回3。


function f{//函数f

alert(this.x+this.y);

}

var o={//对象直接量

x:1,

y:2

}

f.call(o);//执行函数f,返回值为3


(3)原型继承

JavaScript通过原型模式实现类的延续和继承,如果父类的成员中包含了this关键字,当子类继承了父类的这些成员时,this的指向就会很迷惑人。

在一般情况下,子类继承父类的方法后,this可能指向子类的实例对象,也可能指向子类的原型对象,而不是子类的实例对象。例如:


function Base{//基类

this.m=function{//基类的方法m

return/"Base/";

};

this.a=this.m;//基类的属性a,调用当前作用域中的m方法

this.b=this.m;//基类的方法b,引用当前作用域中的m方法

this.c=function{//基类的方法c,以闭包结构调用当前作用域中的m方法

return this.m;

}

}

function F{//子类

this.m=function{

return/"F/"

}

}

F.prototype=new Base;//继承基类

var f=new F;//实例化子类

alert(f.a);//字符串/"Base/",说明this.m中this指向F的原型对象

alert(f.b);//字符串/"Base/"

alert(f.c);//字符串/"F/",说明this.m中this指向F的实例对象


在上面的示例中,基类Base包含4个成员,其中成员b和c以不同方式引用当前作用域内的方法m,而成员a存储着当前作用域内的方法m的调用值。在将这些成员继承给子类F后,其中m、b和c成为原型对象的方法,而a成为原型对象的属性。但c的值为一个闭包体,当在子类的实例中调用它时,实际上它的返回值已经成为实例对象的成员,也就是说,闭包体在哪里被调用,其中包含的this就会指向哪里。所以,可以看到f.c中的this指向实例对象,而不是F类的原型对象。

为了避免因继承关系而影响父类中this所代表的对象,除了通过上面介绍的方法把函数的引用传递给父类的成员外,还可以为父类定义私有函数,然后再把它的引用传递给其他父类成员,这样就避免了由于函数闭包的原因而改变this的值。例如:


function Base{

var_m=function{//定义基类的私有函数_m

return/"Base/";

};

this.a=_m;

this.b=_m;

}


这样基类的私有函数_m就具有完全隐私性,外界其他任何对象都无法直接访问基类的私有函数_m。因此,在一般情况下,在定义方法时,对于相互依赖的方法,可以把它定义为私有函数,并且以引用函数的方式对外公开,这样就避免了外界对于依赖方法的影响。

(4)异步调用

异步调用就是通过事件机制或计时器来延迟或调整函数的调用时间和时机。因为调用函数的执行作用域不再是原来的定义作用域,所以函数中的this总是指向引发该事件的对象。例如:


<input type=/"button/"/>

<script language=/"javascript/"type=/"text/javascript/">

var button=document.getElementsByTagName(/"input/")[0];

var o={};

o.f=function{

if(this==o)alert(/"this=o/");

if(this==window)alert(/"this=window/");

if(this==button)alert(/"this=button/");

}

button.onclick=o.f;

</script>


根据上面的讲解可知,这里的方法f所包含的this不再指向对象o,而是指向按钮button,因为它是被传递给按钮的事件处理函数之后再被调用的。由于函数的执行作用域发生了变化,所以不再指向定义方法时所指定的对象。

如果使用DOM 2级标准为按钮注册事件处理函数:


if(window.attachEvent){//兼容IE

button.attachEvent(/"onclick/",o.f);

}

else{//兼容符合DOM标准的浏览器

button.addEventListener(/"click/",o.f,true);

}


则会看到,在IE中,this指向window和button,而在符合DOM标准的浏览器中仅指向button。在IE中,attachEvent是Window对象的方法,在调用该方法时,执行作用域为全局作用域,所以this会指向window。同时由于该方法被注册到按钮对象上,因此它的真正执行作用域应该为button对象所在的上下文。这一点可以在符合DOM标准的浏览器中看到。这种解释可能很勉强,但在IE中this同时指向Window和Button对象本身。

为了解决这个问题,可以借助call或apply方法强制在对象o上执行f方法,也就是说,强制改变f方法的执行作用域,避免因为环境的不同而影响函数作用域的变化。代码如下:


if(window.attachEvent){

button.attachEvent(/"onclick/",function{//以闭包的形式封装call方法来强制执行f

o.f.call(o);

});

}

else{

button.addEventListener(/"click/",function{

o.f.call(o);

},true);

}


这样,当再次预览时,其中包含的this关键字始终指向对象o,也就是说,f方法的执行作用域始终与它的定义作用域保持一致。

(5)定时器

异步调用的另一种形式就是使用定时器来调用函数。定时器就是通过调用Window对象的setTimeout或setInterval方法来延期调用函数。例如,可以这样来设计延期调用方法o.f。


var o={};

o.f=function{

if(this==o)alert(/"this=o/");

if(this==window)alert(/"this=window/");

if(this==button)alert(/"this=button/");

}

setTimeout(o.f,100);


经测试发现,在IE中,this指向Window和Button对象,具体原因与上面讲解的attachEvent方法相同。但是,在符合DOM标准的浏览器中,this指向Window对象,而不是Button对象,因为setTimeout方法是在全局作用域中被执行的,所以this自然指向Window对象。要解决这个问题,仍然可以使用call或apply方法来实现:


setTimeout(function{

o.f.call(o);

},100);