回调、匿名函数、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
)