谈谈Javascript的this指针

http://developer.51cto.com/art/200907/136245.htm

 

Javascript this 指针一个非常容易理解错,进而用错的特性。特别是对于接触静态语言比较久了的同志来说更是如此。

示例说明

我们先来看一个最简单的 Javascript this 指针示例:

1.  <script type=< span="">"text/javascript" >  </script type=<>

2.      var  name = "Kevin Yang" ;  

3.      function  sayHi(){  

4.         alert(" 你好,我的名字叫 "  + name);  

5.      }  

6.      sayHi();  

7.   

这段代码很简单,我们定义了一个全局字符串对象 name 和函数对象 sayHi 。运行会弹出一个打招呼的对话框, 你好,我的名字叫 Kevin Yang”

我们把这段代码稍微改一改:

1.  <script type=< span="">"text/javascript" >  </script type=<>

2.      var  name = "Kevin Yang" ;  

3.      function  sayHi(){  

4.         alert(" 你好,我的名字叫 "  + this .name);  

5.      }  

6.      sayHi();  

7.   

这段代码和上段代码的区别就在于 sayHi 函数在使用 name 的时候加上了 this. 前缀。运行结果和上面一摸一样。这说明 this.name 引用的也还是全局的 name 对象。

开头我们不是说了,函数也是普通的对象,可以将其当作一个普通变量使用。我们再把上面的代码改一改:

1.  <script type=< span="">"text/javascript" >  </script type=<>

2.      var  name = "Kevin Yang" ;  

3.      function  sayHi(){  

4.         alert(" 你好,我的名字叫 "  + this .name);  

5.      }  

6.      var  person = {};  

7.      person.sayHello = sayHi;  

8.      person.sayHello();  

9.   

这一次,我们又创建了一个全局对象 person ,并将 sayHi 函数对象赋给 person 对象的 sayHello 属性。运行结果如下:

 运行结果

这一次打招呼的内容就有点无厘头了,我们发现 this.name 已经变成 undefined 了。这说明,在 sayHello 函数内部执行时已经找不着 this.name 对象了。如果我们重新定义 person 对象,在其上面加上一个 name 属性又会怎么样呢?

var person = {name:"Marry"}; 运行代码发现打招呼的 变了:

是不是看出点道道了呢?

判别 Javascript this 指针指导性原则

Javascript 里面, this 指针代表的是执行当前代码的对象的所有者。

在上面的示例中我们可以看到,第一次,我们定义了一个全局函数对象 sayHi 并执行了这个函数,函数内部使用了 this 关键字,那么执行 this 行代码的对象是 sayHi (一切皆对象的体现), sayHi 是被定义在全局作用域中。其实在 Javascript 中所谓的全局对象,无非是定义在 window 这个根对象下的一个属性而已。因此, sayHi 的所有者是 window 对象。也就是说,在全局作用域下,你可以通过直接使用 name 去引用这 个对象,你也可以通过 window.name 去引用同一个对象。因而 this.name 就可以翻译为 window.name 了。

再来看第二个 this 的示例。我们定义了一个 person 的对象,并定义了它的 sayHello 属性,使其指向 sayHi 全局对象。那么这个时候, 当我们运行 person.sayHello 的时候, this 所在的代码所属对象就是 sayHello 了(其实准确来说, sayHi sayHello 是只 不过类似两个指针,指向的对象实际上是同一个),而 sayHello 对象的所有者就是 person 了。第一次, person 里面没有 name 属性,因此弹 出的对话框就是 this.name 引用的就是 undefined 对象( Javascript 中所有只声明而没有定义的变量全都指向 undefined 象);而第二次我们在定义 person 的时候加了 name 属性了,那么 this.name 指向的自然就是我们定义的字符串了。

理解了上面所说的之后,我们将上面最后一段示例改造成面向对象式的代码。

1.  <script type=< span="">"text/javascript" >  </script type=<>

2.      var  name = "Kevin Yang" ;  

3.      function  sayHi(){  

4.         alert(" 你好,我的名字叫 "  + this .name);  

5.      }  

6.      function  Person(name){  

7.          this .name = name;  

8.      }  

9.      Person.prototype.sayHello = sayHi;  

10.     var  marry = new  Person("Marry" );     

11.     marry.sayHello();  

12.     var  kevin = new  Person("Kevin" );  

13.     kevin.sayHello();  

14.  

在上面这段代码中,我们定义了一个 Person (实际上还是一个对象),然后在这个类的原型(类原型相当于 C++ 中的静态成员变量的概念) 中定义了 sayHello 属性,使其指向全局的 sayHi 对象。运行代码我们可以看到, marry kevin 都成功的向我们打了声 招呼

在这段代码中有两点需要思考的,一个是 new 我们很熟悉,但是在这里 new 到底做了什么操作呢?另外一个是,这里执行 sayHello 的时候, this 指针为什么能够正确的指向 marry kevin 对象呢?

我们来把上面定义 和实例化类对象的操作重新 翻译 一下:

1.  <script type=< span="">"text/javascript" >  </script type=<>

2.      var  name = "Kevin Yang" ;  

3.      function  sayHi(){  

4.         alert(" 你好,我的名字叫 "  + this .name);  

5.      }  

6.      function  Person(name){  

7.          var  this ;  

8.          this .name = name;  

9.          return  this ;  

10.     }  

11.     Person.prototype.sayHello = sayHi;  

12.     var  marry = Person("Marry" );     

13.     marry.sayHello();  

14.     var  kevin = Person("Kevin" );  

15.     kevin.sayHello();  

16.  

当然这段代码并不能正确执行,但是它可以帮助你更好的理解这个过程。

当我们使用 new 关键字实例化一个 对象的时候, Javascript 引擎会在这个对象内部定义一个新的对象并将其存入 this 指针。所有此对 象内部用到 this 的代码实际上都是指向这个新的对象。如 this.name = name ,实际上是将参数中的 name 对象赋值给了这个新创建的对象。函数对象执行完之后 Javascript 引擎会将此对象返回给你,于是就有 marry 变量得到的对象的 name “Marry” ,而 kevin 变量得到的对象的 name 属性确实 “Kevin”

容易误用的情况

理解了 this 指针后,我们再来看看一些很容易误用 this 指针的情况。

示例 1—— 内联式绑定 Dom 元素的事件处理函数

1.  <script type=< span="">"text/javascript" >  </script type=<>

2.      function  sayHi(){  

3.         alert(" 当前点击的元素是 "  + this .tagName);  

4.      }      

5.     

6.  <input id=< span="">"btnTest"  type="button"  value=" 点击我 "  οnclick="sayHi()" > </input id=<>

在此例代码中,我们绑定了 button 的点击事件,期望在弹出的对话框中打印出点击元素的标签名。但运行结果却是:

也就是 this 指针并不是指向 input 元素。这是因为当使用内联式绑定 Dom 元素的事件处理函数时,实际上相当于执行了以下代码:

1.  <script type=< span="">"text/javascript" >      </script type=<>

2.      document.getElementById("btnTest" ).onclick = function (){  

3.          sayHi();  

4.      }  

5.   

在这种情况下 sayHi 函数对象的所有权并没有发生转移,还是属于 window 所有。用上面的指导原则一套我们就很好理解为什么 this.tagName undefined 了。

那么如果我们要引用元素本身怎么办呢?

我们知道, onclick 函数是属于 btnTest 元素的,那么在此函数内部, this 指针正是指向此 Dom 对象,于是我们只需要把 this 作为参数传入 sayHi 即可。

1.  <script type=< span="">"text/javascript" >  </script type=<>

2.      function  sayHi(el){  

3.         alert(" 当前点击的元素是 "  + el.tagName);  

4.      }  

5.     

6.  <input id=< span="">"btnTest"  type="button"  value=" 点击我 "  οnclick="sayHi(this)" > 等价代码如下:  </input id=<>

7.   

8.  <script type=< span="">"text/javascript" >   </script type=<>

9.      document.getElementById("btnTest" ).onclick = function (){  

10.         sayHi(this );  

11.     }  

12.  

示例 2—— 临时变量导致的 this 指针丢失

1.  <script type=< span="">"text/javascript" >  </script type=<>

2.      var  Utility = {  

3.          decode:function (str){  

4.              return  unescape(str);  

5.          },  

6.          getCookie:function (key){  

7.              // ...  省略提取 cookie 字符串的代码  

8.              var  value = "i%27m%20a%20cookie" ;  

9.              return  this .decode(value);  

10.         }  

11.     };  

12.     alert(Utility.getCookie("identity" ))  

13.  

我们在写稍微有点规模的 Js 库的时候,一般都会自己封装一个 Utility 的类,然后将一些常用的函数作为 Utility 类的属性,如客户端经常会 用到的 getCookie 函数和解码函数。如果每个函数都是彼此独立的,那么还好办,问题是,函数之间有时候会相互引用。例如上面的 getCookie 数,会对从 document.cookie 中提取到的字符串进行 decode 之后再返回。如果我们通过 Utility.getCookie 去调用的话,那 么没有问题,我们知道, getCookie 内部的 this 指针指向的还是 Utility 对象,而 Utility 对象时包含 decode 属性的。代码可以成 功执行。

但是有个人不小心这样使用 Utility 对象呢?

1.  <script type=< span="">"text/javascript" >  </script type=<>

2.      function  showUserIdentity(){  

3.          //  保存 getCookie 函数到一个局部变量,因为下面会经常用到  

4.          var  getCookie = Utility.getCookie;  

5.          alert(getCookie("identity" ));  

6.      }  

7.      showUserIdentity();  

8.   

这个时候运行代码会抛出异常 “this.decode is not a function” 。运用上面我们讲到的指导原则,很好理解,因为此时 Utility.getCookie 对象被赋给了临时变量 getCookie ,而临 时变量是属于 window 对象的 —— 只不过外界不能直接引用,只对 Javascript 引擎可见 —— 于是在 getCookie 函数内部的 this 指针指向 的就是 window 对象了,而 window 对象没有定义一个 decode 的函数对象,因此就会抛出这样的异常来。

这个问题是由于引入了临时变量导致的 this 指针的转移。解决此问题的办法有几个:

不引入临时变量,每次使用均使用 Utility.getCookie 进行调用 getCookie 函数内部使用 Utility.decode 显式引用 decode 对象而不通过 this 指针隐式引用(如果 Utility 是一个实例化的对象,也即是通过 new 生成的,那么此法不可用)

使用 Funtion.apply 或者 Function.call 函数指定 this 指针

前面两种都比较好理解,第三种需要提一下。正是因为 this 指针的指向很容易被转移丢失,因此 Javascript 提供了两个类似的函数 apply call 来允许函数在调用时重新显式的指定 this 指针。

修正代码如下:

1.  <script type=< span="">"text/javascript" >  </script type=<>

2.      function  showUserIdentity(){  

3.          //  保存 getCookie 函数到一个局部变量,因为下面会经常用到  

4.          var  getCookie = Utility.getCookie;  

5.          alert(getCookie.call(Utility,"identity" ));  

6.          alert(getCookie.apply(Utility,["identity" ]));  

7.      }  

8.      showUserIdentity();  

9.   

call apply 只有语法上的差异,没有功能上的差别。

示例 3—— 函数传参时导致的 this 指针丢失

我们先来看一段问题代码:

1.  <script type=< span="">"text/javascript" >  </script type=<>

2.      var  person = {  

3.          name:"Kevin Yang" ,  

4.          sayHi:function (){  

5.              alert(" 你好,我是 " +this .name);  

6.          }  

7.      }  

8.      setTimeout(person.sayHi,5000);  

9.   

这段代码期望在访客进入页面 5 秒钟之后向访客打声招呼。 setTimeout 函数接收一个函数作为参数,并在指定的触发时刻执行这个函数。可是,当我们等了 5 秒钟之后,弹出的对话框显示的 this.name 却是 undefined

其实这个问题和上一个示例中的问题是类似的,都是因为临时变量而导致的问题。当我们执行函数的时候,如果函数带有参数,那么这个时候 Javascript 引擎会创建一个临时变量,并将传入的参数复制(注意, Javascript 里面都是值传递的,没有引用传递的概念)给此临时变量。也 就是说,整个过程就跟上面我们定义了一个 getCookie 的临时变量,再将 Utility.getCookie 赋值给这个临时变量一样。只不过在这个示 例中,容易忽视临时变量导致的 bug

函数对象传参
对于函数作为参数传递导致的 this 指针丢失的问题,目前很多框架都已经有方法解决了。

Prototype 的解决方案 —— 传参之前使用 bind 方法将函数封装起来,并返回封装后的对象

1.  <script type=< span="">"text/javascript" >  </script type=<>

2.      var  person = {  

3.          name:"Kevin Yang" ,  

4.          sayHi:function (){  

5.              alert(" 你好,我是 " +this .name);  

6.          }  

7.      }  

8.      var  boundFunc = person.sayHi.bind(person,person.sayHi);  

9.      setTimeout(boundFunc,5000);  

10.  

bind 方法的实现其实是用到了 Javascript 又一个高级特性 —— 闭包。我们来看一下源代码:

1.  function bind(){  

2.      if (arguments.length <  2  && arguments[0] === undefined)   

3.          return this;  

4.      var __method  = thisargs  = $A(arguments), object  = args .shift();  

5.      return function(){  

6.          return __method.apply(object, args.concat($A(arguments)));  

7.      }  

8. 

首先将 Javascript this 指针存入函数内部临时变量,然后在返回的函数对象中引用此临时变量从而形成闭包。

微软的 Ajax 库提供的方案 —— 构建委托对象

1.  <script type=< span="">"text/javascript" >  </script type=<>

2.      var  person = {  

3.          name:"Kevin Yang" ,  

4.          sayHi:function (){  

5.              alert(" 你好,我是 " +this .name);  

6.          }  

7.      }      

8.      var  boundFunc = Function.createDelegate(person,person.sayHi);  

9.      setTimeout(boundFunc,5000);  

10.  

其实本质上和 prototype 的方式是一样的。

著名的 Extjs 库的解决方案采用的手法和微软是一样的。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值