回调、匿名函数、this

为何需要回调

js语言被发明出来,最初就是要做web网页开发的。

有很多是用户事件触发后,才要执行的代码,没办法直接执行。


比如,一个薪资统计的网页程序

让用户输入一段文本包含:员工姓名、薪资、年龄。

格式如下:

薛蟠     4560 25
薛宝钗   35776 23
薛宝琴   14346 18
王熙凤   24460 25
王子腾   55660 45
王仁     15034 65

当用户点击统计按钮,执行统计代码,把薪资在 2万 以上、以下的人员名单分别打印出来

<!DOCTYPE html>
<html>

<body>
  
  <span style='color:blue'>
  请输入员工薪资记录</span> <br><br>
  
  <textarea id="salary" rows="20" cols="80"></textarea>
  <br> 
  <button id='go'>统计</button>
  
  <div>
  <br><br>
  <span id="result" style='color:blue'>
  分类结果</span> <br><br>
  <pre>
  </pre>
  <div>

  <script>    
  function salaryStats(event){    
  
    let info = document.querySelector('#salary').value

    // 薪资20000 以上 和 以下 的人员名单
    let salary_above_20k = []
    let salary_below_20k = []
    for (let line of info.split('\n')){
      if (!line.trim())
        continue
      let parts = line.split(' ')
      let newparts = []
      for (let one of parts) 
        if (one) newparts.push(one)
      
      let [name,salary,age] = newparts
      if (parseInt(salary) >= 20000)
        salary_above_20k.push(name)
      else
        salary_below_20k.push(name)
    }

    document.querySelector('pre').innerText  = 
    `
    薪资20000 以上的有:
    ${salary_above_20k.join(' ')}
    
    薪资20000 以下的有:
    ${salary_below_20k.join(' ')}
    `   
  }

  // 注册事件回调函数
  document.querySelector('#go').addEventListener("click", salaryStats );
  </script>
  
</body>
</html>

其中 salaryStats 函数 就是统计代码,

但是因为没法预知用户何时输入完信息,何时点击统计按钮

程序不可能一开始就执行这个函数

必须先定义处理统计的代码函数

然后,通过如下代码

// 注册事件回调函数
document.querySelector('#go').addEventListener("click", salaryStats );

告诉js引擎,在统计按钮被点击后,再调用

所以 salaryStats 这种 先定义,后面在某个时候被调用的函数,叫 回调函数

英文称之为 callback function


网页事件回调在后面 js web开发教程种有详细的讲解

异步架构

再看一个例子

要求代码实现:等待2秒后,再执行后续操作


Python 语言代码是这样写的

import time

print('这里是等待2秒前的代码')
time.sleep(2) # 等待2秒
print('这里是等待2秒的后续代码')

而 js 语言代码是这样写的

// 把2秒后要执行的代码放在这个定义的函数中
function taskAfter2Second() {
  console.log("这里是等待2秒的后续代码");
}

setTimeout(   
  taskAfter2Second, 
  2000 // 2000毫秒,就是2秒
)

console.log("这里是设置后的代码");

和 Python、Java 等语言不同,js 的执行引擎设计是 异步架构

这里的 异步 是什么意思呢?

就是 碰到阻塞性的调用,比如 定时等待、网络操作、磁盘IO等等, js引擎都不会停止后面代码的执行来 这些操作完成。

而是 :

  • 定义一个 回调函数

    这个函数里面是 阻塞性操作(比如等待2秒)完成后,被js引擎调用的后续处理代码,

    因为不是 直接调用执行的函数,而是先定义,等事情完成,回头再调用,所以叫 回调 函数。

  • 继续执行后面的代码,

    比如上面 设置回调后的代码 ,它们是在回调函数之前执行的。


这种异步的架构设计,设计模式里面 叫做 reactor 模式

系统底层实现基本都是一个 主循环处理各种事件,比如 网络socket收到数据、文件读取数据返回、定时器超时 等等。

然后调用相应的 用户代码进行处理。

碰到阻塞性的操作,不会等,而是记录好操作完成的回调代码,继续执行后面的用户代码。

用户代码都执行完了,就返回主循环,处理下一个需要处理的事件。


这种架构, 特别适合 IO bound(也就是 高IO,低CPU) 的软件系统,典型的就是网站服务系统。

因为它能高效的利用CPU。

为什么呢?


我们以银行服务,举个例子:

一个银行里面的服务员,分别服务6个客户

客户1 是开户服务 ,其中 客户填表格,耗时1分钟, 填完表格后 服务员登记开户,耗时10秒

客户2、3、4、5、6 是取钱服务, 服务员处理耗时10秒


可以采用 同步服务模式 或者 异步服务模式 两种方式

  • 同步服务模式

    客户1 来了要开户, 先要填表格,耗时1分钟。

    服务员等表填完了, 再花10秒钟完成后续的开户任务。

    客户1 耗费了 1分10秒


    然后分别处理客户2、3、4、5、6 的 取钱服务, 各自需要服务员花10秒,一共50秒完成


    所有任务, 总共耗时 2分钟

  • 异步模式

    客户1 来了要开户, 先要填表格。

    服务员发现填表过程中,自己是等待状态,所以让 客户1到旁边去填, 告诉他,填好后来找我。

    然后处理客户2、3、4、5、6 的 取钱服务, 各自需要服务员花10秒,一共50秒完成

    又过了10秒(也就是1分钟后),客户1 填表完成,服务员再花10秒钟完成登记开户。


    所有任务, 总共耗时 1分 10秒


所以,异步服务模式 大大的提高了效率,因为它能大大减少服务员闲置的状态。


看到这个例子,肯定有些读者觉得有种似曾相识的感觉。

对了,这个好像 操作系统多线程 调度的概念。

操作系统可以让处于执行阻塞操作(读文件,等待网络消息)的线程让出CPU执行权,让其它线程占据CPU执行代码。

这样大大减少了CPU闲置 ,这是 操作系统 对 多个线程 的 调度


而异步的软件架构,是程序自身(比如js引擎)实现的 单线程 自身的内部代码 整理和调度。

相比 操作系统多线程方式,它的效率更高

因为,多线程调度涉及到 操作系统调用,导致 CPU的执行模式切换,是要额外耗费资源的。


而且,没有多线程操作共享资源的同步问题。因为实际上,这是单线程

匿名函数

在上面的例子中,

// 把2秒后要执行的代码放在这个定义的函数中
function taskAfter2Second() {
  console.log("这里是等待2秒的后续代码");
}

setTimeout(   
  taskAfter2Second, 
  2000 // 2000毫秒,就是2秒
)

我们定义了一个函数 给它起名为 taskAfter2Second ,

这个函数名 只被用到一次, 就是作为 setTimeout 的参数。

js中有很多这样的回调,那就需要动脑筋 想很多 这种只用一次的函数名 。

可以使用 匿名函数 避免这种起名字的麻烦。

如下

setTimeout(
  // 直接定义函数,不用起名
  function() {
    console.log("这里是等待2秒的后续代码");
  }, 
  2000 // 2000毫秒,就是2秒
)

直接把函数定义在 回调参数处,不需要给他起名字。


其实,函数定义时起的名字 本质上就是一个变量名, 对应后面定义的函数对象。

所以,函数也可以这样定义

let add100 = function (a){
  return a + 100;
}

我们当然可以省略这个 变量名,直接定义函数对象。



匿名函数如果 单独定义在非参数位置,并且不赋值给变量,需要加上括号,如下

(function (a){
  return a + 100;
})

往往这样的定义,是为了定义后,就立即调用,比如

(function (a){
  return a + 100;
})(30)

那么如果这样的话,为何还要定义一个函数呢?干嘛不直接执行函数体里面的代码。

这样有一个好处,如果一段功能代码需要定义不少变量(这些变量只对这段代码有用),可以这样封装在函数中,不污染名字空间

箭头函数

ES6 引入了 箭头函数 这种新的定义匿名函数的语法,更加精简

比如

(a) => { 
  return a + 100;
}

如果箭头函数只有一个参数,可以省略参数周围的括号

a => { 
  return a + 100;
}

但是如果有多个参数,或者没有参数, 就不能省略参数周围的括号了。


如果 箭头函数 体内 只有一行代码,并且是返回一个值,可以省略 return 和 花括号

a => a + 100

js中有很多函数方法,参数是另外一个回调函数,这个回调函数就是根据传入的参数 计算处一个结果返回这个结果。

回调函数 可以使用这种精简的写法 ,使得代码非常简洁。


比如,前面学过数组有个map方法,参数是一个回调函数, map方法会 以数组里面的元素作为参数, 依次调用 回调函数, 返回值依次放入新的列表中 ,作为整个map调用的返回值

这个map方法非常常用,比如,要计算一个数组里面所有数字的 平方,存放到另外一个数组中。

var a1 = [1,2,3,4]
a1.map(x=>x**2)

箭头函数 也可以一定义就调用

(a => a + 100)(10)

前面的示例,使用箭头函数可以这样定义

setTimeout(
  () => {console.log("这里是等待2秒的后续代码");}, 
  2000 
)

因为函数里面只有一行代码,还可以更简单

setTimeout(
  () => console.log("这里是等待2秒的后续代码") , 
  2000  
)

回调函数中的this

前面学过一个汽车类的例子,如下

class Car {    

  constructor(price, owner) {
    this.price = price
    this.owner = owner
  }

  showInfo(){
    console.log(`车辆信息如下:`)
    console.log(`车主 : ${this.owner} - 售价 : ${this.price} `)
  }
}

car1 = new Car(230000,'白月黑羽')
car1.showInfo()

前面讲过 通过哪个对象调用了这个函数,函数里面的 this 对应的是就是这个对象

我们可以把函数对象赋值给另外的变量,比如

let obj1={
  price: '3333',
  owner: '张三',
  anotherShowInfo : car1.showInfo
}

obj1.anotherShowInfo()

输出结果如下

车辆信息如下:
VM121:11 车主 : 张三 - 售价 : 3333 

因为是通过obj1 调用的 显示信息函数, this 就对应 obj1 这个对象


如果我们这样写

let obj2={
  anotherShowInfo : car1.showInfo
}

obj2.anotherShowInfo()

运行结果就会如下

车辆信息如下:
车主 : undefined - 售价 : undefined 

因为 通过 的对象 obj2,里面并没有price 和 onwner属性



如果我们想修改一下 showInfo 方法,想延迟1秒打印

那自然的,会这样写

class Car {    

  constructor(price, owner) {
    this.price = price
    this.owner = owner
  }

  showInfo_Delay1Sec(){
    // 1秒后通过回调函数打印出  this.owner 和 this.price
    setTimeout(   
      function (){
        console.log(this===window)
        console.log(`车主 : ${this.owner} - 售价 : ${this.price} `)
      }, 
      1000
    )        
  }
}
car1 = new Car(230000,'白月黑羽')
car1.showInfo_Delay1Sec()

但是执行一下,发现结果却是

车主 : undefined - 售价 : undefined 

为什么,回调里面的 this.owner 和 this.price 值是 undefined 呢?

因为setTimeout函数是js引擎实现的,它内部调用回调函数时,没有 xxx.调用前缀

js中,调用函数没有前缀,就等于通过全局对象 window 调用

也就是 window.setTimeout, 所以,回调里面的this就是windows对象。

windows对象并没有 price 、onwer属性。

所以,也出现了上述的错误显示。


这里的this带来的问题,可以说是js语言 最臭名昭著的,让人头疼的问题之一。


那么怎么解决这个问题呢?


保存this到其它变量

可以先保存this到其它变量,回调函数里面改用这个变量,比如

class Car {    

  constructor(price, owner) {
    this.price = price
    this.owner = owner
  }

  showInfo_Delay1Sec(){

    // 保存 调用对象 到self中
    let self = this
    
    setTimeout(   
      function () {
        // 使用self,也就是调用对象
        console.log(`车主 : ${self.owner} - 售价 : ${self.price} `)
      }, 
      1000
    )        
  }
}

var car1 = new Car(230000,'白月黑羽')
car1.showInfo_Delay1Sec()

箭头函数中的this

另外一个方法是:使用箭头函数,比如

class Car {    

  constructor(price, owner) {
    this.price = price
    this.owner = owner
  }

  showInfo_Delay1Sec(){
    
    setTimeout(   
      () => {
        console.log(`车主 : ${this.owner} - 售价 : ${this.price} `)
      }, 
      1000
    )        
  }
}

var car1 = new Car(230000,'白月黑羽')
car1.showInfo_Delay1Sec()

可以发现,运行结果正确

因为:箭头函数中的this比较特殊,它对应的 是 包含该箭头函数 的函数的执行环境

本例中,包含箭头函数的函数 是 showInfo_Delay1Sec (注意不是setTimeout,这里setTimeout是调用,而不是定义)

这里,我们是通过 car1 调用的showInfo_Delay1Sec, 所以 里面的执行环境, 就是 car1


如果箭头函数没有包含函数,里面this对应的就是全局对象,浏览器中就是 window

比如

var price = 1000
var owner = '白月黑羽'

setTimeout(   
  () => {
    console.log(`车主 : ${this.owner} - 售价 : ${this.price} `)
  }, 
  1000
)