你不知道的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事件处理程序

在事件处理函数内部, this 值等于事件的目标元素,例如:


1

2

<!-- 输出  "Click Me" -->

<input type="button" value="Click Me" onclick="alert(this.value)">

  • (2)DOM0 级事件处理程序

使用DOM0级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的 this 引用当前元素。来看一个例子。


1

2

3

4

var btn = document.getElementById("myBtn");

btn.onclick = function(){

alert(this.id); //"myBtn"

};

  • (3)DOM2 级事件处理程序

要在按钮上为 click 事件添加事件处理程序,可以使用下列代码:


1

2

3

4

5

varbtn = 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。

关于事件处理程序理解上面这些还不够,我们还要关注一下事件委托,对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如, click 事件会一直冒泡到 document 层次。也就是说,我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。以下面的 HTML 代码为例。


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语言精粹》

来自为知笔记(Wiz)

时间: 2024-10-13 19:16:07

你不知道的this—JS异步编程中的this的相关文章

JS异步编程 (1)

JS异步编程 (1) 1.1 什么叫异步 异步(async)是相对于同步(sync)而言的,很好理解. 同步就是一件事一件事的执行.只有前一个任务执行完毕,才能执行后一个任务.而异步比如: setTimeout(function cbFn(){ console.log('learnInPro'); }, 1000); console.log('sync things'); setTimeout就是一个异步任务,当JS引擎顺序执行到setTimeout的时候发现他是个异步任务,则会把这个任务挂起,

一个例子读懂 JS 异步编程: Callback / Promise / Generator / Async

JS异步编程实践理解 回顾JS异步编程方法的发展,主要有以下几种方式: Callback Promise Generator Async 需求 显示购物车商品列表的页面,用户可以勾选想要删除商品(单选或多选),点击确认删除按钮后,将已勾选的商品清除购物车,页面显示剩余商品. 为了便于本文内容阐述,假设后端没有提供一个批量删除商品的接口,所以对用户选择的商品列表,需要逐个调用删除接口. 用一个定时器代表一次接口请求.那思路就是遍历存放用户已选择商品的id数组,逐个发起删除请求del,待全部删除完成

JS 异步编程六种方案

前言我们知道Javascript语言的执行环境是"单线程".也就是指一次只能完成一件任务.如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务. 这种模式虽然实现起来比较简单,执行环境相对单纯,但是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行.常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行. 为了解决这个问题,Javascript语言将任务的执行模式分成两种

Javascript教程:js异步编程的4种方法详述(转载)

文章收集转载于(阮一峰的网络日志) 你可能知道,Javascript语言的执行环境是“单线程”(single thread). 所谓“单线程”,就是指一次只能完成一件任务.如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推. 这种模式的好处是实现起来比较简单,执行环境相对单纯:坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行.常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方

js异步编程

前言 以一个煮饭的例子开始,例如有三件事,A是买菜.B是买肉.C是洗米,最终的结果是为了煮一餐饭.为了最后一餐饭,可以三件事一起做,也可以轮流做,也可能C需要最后做(等A.B做完),这三件事是相关的,抽象起来有三种场景. 顺序做 先买菜,再买肉,最后洗米, 即 A->B->C. 并发做 买菜,买肉,洗米,一起做. 交集做 买菜,买肉必须先做完,才能做洗米. 场景就是这样,接下来就是如何考虑用js实现. function A(callback){ setTimeout(function(){ c

Async/Await 异步编程中的最佳做法

近日来,涌现了许多关于 Microsoft .NET Framework 4.5 中新增了对 async 和 await 支持的信息. 本文旨在作为学习异步编程的“第二步”:我假设您已阅读过有关这一方面的至少一篇介绍性文章. 本文不提供任何新内容,Stack Overflow.MSDN 论坛和 async/await FAQ 这类在线资源提供了同样的建议. 本文只重点介绍一些淹没在文档海洋中的最佳做法. 本文中的最佳做法更大程度上是“指导原则”,而不是实际规则. 其中每个指导原则都有一些例外情况

异步编程中的最佳做法

原文链接 近日来,涌现了许多关于 Microsoft .NET Framework 4.5 中新增了对 async 和 await 支持的信息. 本文旨在作为学习异步编程的“第二步”:我假设您已阅读过有关这一方面的至少一篇介绍性文章. 本文不提供任何新内容,Stack Overflow.MSDN 论坛和 async/await FAQ 这类在线资源提供了同样的建议. 本文只重点介绍一些淹没在文档海洋中的最佳做法. 本文中的最佳做法更大程度上是“指导原则”,而不是实际规则. 其中每个指导原则都有一

异步编程中的最佳做法(Async/Await) --转

近日来,涌现了许多关于 Microsoft .NET Framework 4.5 中新增了对 async 和 await 支持的信息. 本文旨在作为学习异步编程的“第二步”:我假设您已阅读过有关这一方面的至少一篇介绍性文章. 本文不提供任何新内容,Stack Overflow.MSDN 论坛和 async/await FAQ 这类在线资源提供了同样的建议. 本文只重点介绍一些淹没在文档海洋中的最佳做法. 本文中的最佳做法更大程度上是“指导原则”,而不是实际规则. 其中每个指导原则都有一些例外情况

探究SynchronizationContext在.Net异步编程中的地位

原文:探究SynchronizationContext在.Net异步编程中的地位 引言: 多线程编程/异步编程非常复杂,有很多概念和工具需要去学习,贴心的.NET提供Task线程包装类和await/async异步编程语法糖简化了异步编程方式. 相信很多开发者都看到如下异步编程实践原则:   实践原则  说明  例外情况  ①  避免 Async Void  最好使用 async Task 方法而不是 async void 方法  事件处理程序  ②  始终使用 await  不要混合阻塞式代码和