你不知道的this—JS异步编程中的this
Javascript小学生都知道了javascript中的函数调用时会 隐性的接收两个附加的参数:this和arguments。参数this在javascript编程中占据中非常重要的地位,它的值取决于调用的模式。总的来说Javascript中函数一共有4中调用模式:方法调用模式、普通函数调用模式、构造器调用模式、apply/call调用模式。这些模式在如何初始化关键参数this上存在差异。“可能还有小伙伴不知道它们之间的区别,那我就勉为其难撸一撸吧!”
方法调用模式:函数是在某个明确的上下文对象中调用的,this绑定的是那个上下文对象。
普通函数调用模式:默认情况下,如果函数是被直接调用的,如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
构造器调用模式:函数通过new操作符调用,this绑定的是新创建的对象。
apply/call调用模式:函数通过apply或者call调用,this绑定的是指定的对象,如果把null或者undefined作为this的绑定对象传入call/apply,在调用时会被忽略,实际应用的是默认绑定规则。
下面举一个简单的综合例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
var a=2; function foo(b) { this .b=b; console.log( this .a); } var obj={ a:4, foo:foo }; foo(); //普通函数调用,输出2 obj.foo(); //作为对象方法调用,输出4 foo.call(obj); //call显示绑定,输出4 foo.call( null ); //输出2 var bar= new foo(8); //构造函数调用,输出了undefined(由console.log(a)打印) console.log(bar.b) //输出8 |
上面的例子在浏览器环境中已经测试通过了,在Node环境中在函数外面定义的变量不会成为全局对象的属性,理解这个例子的输出结果对于上面提到的四种调用方式大概就理解了。在大多数情况下,每次遇到函数调用(注意是每次,不管调用时这个函数位于哪里,只要遇到调用这个函数就要停下来确定里面的this),只要仔细区分上面的四种调用模式,就能很快确定函数中的this绑定的是哪个对象。但是有一类情况很特殊,你不能一眼或者两眼就能看出函数调用的模式,那就是JavaScript中的异步函数调用。下面介绍几种实际开发过程中常用的异步函数调用中this绑定的例子。
1.超时调用和间歇调用
超时调用需要使用 window 对象的 setTimeout() 方法,它接受两个参数:要执行的代码和以毫秒表示的时间(即在执行代码前需要等待多少毫秒)。其中,第一个参数可以是一个包含JavaScript代码的字符串(就和在eval() 函数中使用的字符串一样),也可以是一个函数。setTimeout() 的第二个参数告诉 JavaScript 再过多长时间把当前任务添加到队列中。如果队列是空的,那么添加的代码会立即执行;如果队列不是空的,那么它就要等前面的代码执行完了以后再执行。
下面对setTimeout()的两次调用都会在一秒钟后显示一个警告框。
1
2
3
|
setTimeout( function () { alert( "Hello world!" ); }, 1000); |
下面看一下setTimeout的回调函数中存在this的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var a=5; function foo() { this .a++; setTimeout( function (){ console.log( this .++a); },1000); } var obj={ a:2 }; foo.call(obj); console.log(obj.a); |
在浏览器环境测试,上述代码的输出结果是3 6,为什么会是3 和6呢,首先我们知道超时函数的回调函数是异步的,所以先输出的是最后一条语句执行的结果。foo.call(obj)语句通过call绑定obj,所以foo函数执行时内部的this绑定的是obj,所以this.a++使得obj的a属性增加了1.接下来通过超时函数设置回调的匿名函数一秒后加入到任务队列。所以在执行最后一条语句时,超时函数里的回调函数还没有执行,所以最后一条语句输出为3,接下来当任务队列里的回调函数被调用执行时,输出的是6,也就是全局变量a加1,因此超时调用的回调代码都是在全局作用域中执行的,函数中的this的值指向全局对象,这里补充说明一下在严格模式下this绑定的是undefined。
那么间歇调用setInterval方法是什么情况呢。稍微小改一下上面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var a=5; function foo() { this .a++; setInterval( function (){ console.log(++ this .a); },1000); } var obj={ a:2 }; foo.call(obj); console.log(obj.a); |
上面的代码输出为3 6 7 8 9·····
也就是说间歇调用和超时调用的情况一样,回调函数也是在全局环境中执行的。
2.事件处理程序
(1)HTML事件处理程序
1
2
|
<!-- 输出 "Click Me" --> < input type = "button" value = "Click Me" onclick = "alert(this.value)" > |
所以你觉得你懂这个东东了,《JS高程》红宝书中说,直接在HTML中添加事件处理会动态创建一个事件处理函数,执行这个函数时的this为目标元素。那我们看个例子,瞧瞧自己是不是真的懂了。
点击一个div,让div里的文本从5每隔一秒递减一直到0
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<script type="text/javascript">
function test(){
this.innerHTML='5';
var timer=setInterval(function(){
if (this.innerHTML==1) {
clearInterval(timer); }
this.innerHTML--;
},1000)
}
</script>
</head>
<body>
<div onclick="test()">沐浴星光</div>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<script type="text/javascript">
function test(target){
target.innerHTML='5';
var timer=setInterval(function(){
if (target.innerHTML==1) {
clearInterval(timer); }
target.innerHTML--;
},1000)
}
</script>
</head>
<body>
<div onclick="test(event.target)">沐浴星光</div>
</body>
</html>
(2)DOM0 级事件处理程序
使用DOM0级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的 this 引用当前元素。来看一个例子。
1
2
3
4
|
var btn = document.getElementById( "myBtn" ); btn.onclick = function (){ alert( this .id); //"myBtn" }; |
(3)DOM2 级事件处理程序
1
2
3
4
5
|
var btn = document.getElementById( "myBtn" ); btn.addEventListener( "click" , function (){ alert( this .id); //"myBtn" }, false ); |
与DOM0级方法一样,这里添加的事件处理程序也是在其依附的元素的作用域中运行。
在旧版本的IE浏览器中有一种特殊情况,旧版本的IE可以通过attachEvent() 添加事件处理程序,在IE中使用attachEvent() 与使用DOM0级方法的主要区别在于事件处理程序的作用域。在使用DOM0级方法的情况下,事件处理程序会在其所属元素的作用域内运行;在使用 attachEvent() 方法的情况下,事件处理程序会在全局作用域中运行,因此 this 等于 window。来看下面的例子。
1
2
3
4
|
var btn = document.getElementById( "myBtn" ); btn.attachEvent( "onclick" , function (){ alert( this === window); //true }); |
为了加深理解,我们看下面一个比较难懂的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
functionJSClass(){ this .m_Text = 'division element' ; this .m_Element = document.createElement( 'div' ); this .m_Element.innerHTML = this .m_Text; this .m_Element.addEventListener( 'click' , this .func); // this.m_Element.onclick = this.func; } JSClass.prototype.Render= function (){ document.body.appendChild( this .m_Element); } JSClass.prototype.func = function (){ alert( this .m_Text); }; var jc =newJSClass(); jc.Render(); // add div jc.func(); // 输出 division element |
click添加的div元素division element会输出underfined,为什么?
答案:division element undefined
解析:第一次输出很好理解,func()作为对象的方法调用,所以输出division element,点击添加的元素时,this其实已经指向this.m_Element,也就是事件的目标元素(事件对象的currentTarget属性值-或者说是注册事件处理程序的元素),因为是this.m_Element调用的addEventListener函数,所以内部的this全指向它了,而这个元素并没有m_Text属性,所以输出undefined。
1
2
3
4
5
|
<ul id= "myLinks" > <li id= "goSomewhere" >Go somewhere</li> <li id= "doSomething" >Do something</li> <li id= "sayHi" >Say hi</li> </ul> |
其中包含3个被单击后会执行操作的列表项。按照传统的做法,需要像下面这样为它们添加3个事件处理程序。
1
2
3
4
5
6
7
8
9
10
11
12
|
var item1 = document.getElementById( "goSomewhere" ); var item2 = document.getElementById( "doSomething" ); var item3 = document.getElementById( "sayHi" ); item1.addEventListener( "click" , function (event){ alert( this .id); //"goSomewhere" }); item2.addEventListener( "click" , function (event){ alert( this .id); //"oSomething" }); item3.addEventListener( "click" , function (event){ alert( this .id); //"sayHi" }); |
如果在一个复杂的 Web 应用程序中,对所有可单击的元素都采用这种方式,那么结果就会有数不清的代码用于添加事件处理程序。此时,可以利用事件委托技术解决这个问题。使用事件委托,只需在DOM树中尽量最高的层次上添加一个事件处理程序,如下面的例子所示。
1
2
3
4
|
var list=document.getElementById( '"myLinks' ); list.addEventListener( 'click' , function (event){ alert( this .id); }) |
那上面的例子能否实现事件委托前的功能呢,我们用下面的代码在浏览器中测试一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<!DOCTYPE html> < html > < head > < meta charset = "utf-8" /> < title ></ title > </ head > < body > < ul id = "myLinks" > < li id = "goSomewhere" >Go somewhere</ li > < li id = "doSomething" >Do something</ li > < li id = "sayHi" >Say hi</ li > </ ul > </ body > < script type = "text/javascript" > var list=document.getElementById('myLinks'); list.addEventListener('click',function(event){ alert(this.id); }) </ script > </ html > |
测试结果
也就是说不论点击哪一个列表,弹出的是父元素的ID,那么该怎么改写才能实现预期的功能呢?我们知道事件对象event有很多属性,其中包括两个属性currentTarget和target,在事件处理程序内部,对象this 始终等于currentTarget 的值(也就是添加事件处理程序的元素),而target则只包含事件的实际目标。如果直接将事件处理程序指定给了目标元素则 this、currentTarget 和target包含相同的值。如果事件处理程序是被委托代理的,那么这些值一般不同。来看下面的例子。
1
2
3
4
5
6
|
var list=document.getElementById( 'myLinks' ); list.addEventListener( 'click' , function (event){ alert(event.currentTarget===list) //ture alert( this ===list) //ture }); |
这也解释了上面错误的事件委托为什么一直弹出“myLinks”了。正确的事件委托程序是:
1
2
3
4
|
var list=document.getElementById( 'myLinks' ); list.addEventListener( 'click' , function (event){ alert(event.target.id); }) |
3.Ajax请求中的this
最后简要说明一下ajax请求中的this
1
2
3
4
5
6
7
8
9
10
11
12
|
var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function (){ if (xhr.readyState == 4){ if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){ alert(xhr.responseText); } else { alert( "Request was unsuccessful: " + xhr.status); } } }; xhr.open( "get" , "example.txt" , true ); xhr.send( null ); |
这个例子在onreadystatechange事件处理程序中使用了xhr对象,没有使用this对象,原因是onreadystatechange事件处理程序的作用域问题。如果使用this对象,在有的浏览器中会导致函数执行失败,或者导致错误发生。因此,使用实际的XHR对象实例变量是较为可靠的一种方式。
参考:
《JavaScript高级程序设计》
《You Don't Konw JS:This&Object Prototypes》
《JavaScript语言精粹》
你不知道的this—JS异步编程中的this的更多相关文章
- JS魔法堂:深究JS异步编程模型
前言 上周5在公司作了关于JS异步编程模型的技术分享,可能是内容太干的缘故吧,最后从大家的表情看出"这条粉肠到底在说啥?"的结果:(下面是PPT的讲义,具体的PPT和示例代码在h ...
- 深究JS异步编程模型
前言 上周5在公司作了关于JS异步编程模型的技术分享,可能是内容太干的缘故吧,最后从大家的表情看出"这条粉肠到底在说啥?"的结果:(下面是PPT的讲义,具体的PPT和示例代码在h ...
- 前端分享----JS异步编程+ES6箭头函数
前端分享----JS异步编程+ES6箭头函数 ##概述Javascript语言的执行环境是"单线程"(single thread).所谓"单线程",就是指一次只 ...
- JS异步编程 (2) - Promise、Generator、async/await
JS异步编程 (2) - Promise.Generator.async/await 上篇文章我们讲了下JS异步编程的相关知识,比如什么是异步,为什么要使用异步编程以及在浏览器中JS如何实现异步的.最 ...
- JS异步编程 (1)
JS异步编程 (1) 1.1 什么叫异步 异步(async)是相对于同步(sync)而言的,很好理解. 同步就是一件事一件事的执行.只有前一个任务执行完毕,才能执行后一个任务.而异步比如: setTi ...
- 一个例子读懂 JS 异步编程: Callback / Promise / Generator / Async
JS异步编程实践理解 回顾JS异步编程方法的发展,主要有以下几种方式: Callback Promise Generator Async 需求 显示购物车商品列表的页面,用户可以勾选想要删除商品(单选 ...
- js异步编程
前言 以一个煮饭的例子开始,例如有三件事,A是买菜.B是买肉.C是洗米,最终的结果是为了煮一餐饭.为了最后一餐饭,可以三件事一起做,也可以轮流做,也可能C需要最后做(等A.B做完),这三件事是相关的, ...
- node.js异步编程的几种模式
Node.js异步编程的几种模式 以读取文件为例: 1.callback function const fs = require('fs'); //callback function fs.readF ...
- 【转】C# Async/Await 异步编程中的最佳做法
Async/Await 异步编程中的最佳做法 Stephen Cleary 近日来,涌现了许多关于 Microsoft .NET Framework 4.5 中新增了对 async 和 await 支 ...
随机推荐
- Redis JedisPool
获取连接池,通常连接池为单例,这里使用 双端检测机制保证只有一个实例 public class JedisPoolUtil { private static volatile JedisPool je ...
- Double Checked Locking 模式
转自:http://blog.csdn.net/wwsoon/article/details/1485886 之前在使用Double Check Locking 模式时,发现自己还是不太理解.于是写个 ...
- bzoj 3295 动态逆序对 CDQ分支
容易看出ans[i]=ans[i-1]-q[i],q[i]为删去第i个数减少的逆序对. 先用树状数组算出最开始的逆序对,预处理出每个数前边比它大的和后边比它小的,就求出了q[i]的初始值. 设b[i] ...
- MVC复杂模型绑定
当初遇到业务需求ajax提交一组对象数组到服务器.但是苦于mvc的默认绑定器.绑定不上去.好吧只有靠自己了. 当初就是参考这个大大的博客:http://www.cnblogs.com/xfrog/ar ...
- FragmentPagerAdapter+ViewPager+Fragment
FragmentPagerAdapter中会在滑动到2页时,会预加载第三个页面.如果在这些页面中都有网络请求,那么当你还没有看到第三页时,第三页的数据请求已经发出.这样就会造成,当已进入该页面,可能会 ...
- socket编程基础
socket编程 什么是socket 定义 socket通常也称作套接字,用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过套接字向网络发出请求或者应答网络请求. socket起源于Unix ...
- 100_1小记ressons analysis
想到一首诗用以自勉: look to the master, follow the master, walk with the master, see through the master, beco ...
- shell判断条件整理
1.字符串判断 str1 = str2 当两个字符串串有相同内容.长度时为真 str1 != str2 当字符串str1和str2不等时为真 -n str1 当字符串的长度大于0时为真(串非空) -z ...
- nginx正则表达式
1.^:匹配字符串的开始位置: 2..*: .匹配任意字符,*匹配数量0到正无穷: 3.\. 斜杠用来转义,\.匹配.: 4.(jpg|gif|png|bmp)匹配jpg或gif或png或bmp 5. ...
- idea之resource配置
1.问题 在idea中配置springmvc项目,用hibernate管理数据库,在web.xml中作如下配置: <!--配置hibernate数据库连接--> <listener& ...