JavaScript函数高级篇

315 阅读6分钟

一、SetTimout和SetInterval

理解书中这部分内容需要对JavaScript运行机制有一些认识:

JavaScript运行机制:

www.ruanyifeng.com/blog/2014/1…

juejin.cn/post/684490…

浏览器运行机制:zhuanlan.zhihu.com/p/102149546

提取了相关的几个要点:

(一)浏览器渲染进程

1、每个Tab页面对应一个浏览器渲染进程。

2、浏览器渲染进程是多线程进程,其中GUI渲染线程和JS引擎线程互斥。包括的线程有:

  • GUI渲染线程:渲染浏览器页面。
  • JS引擎线程: 解析并运行JavaScript。
  • 事件触发线程:管理事件队列。当事件符合其触发条件时,该线程会把事件添加到事件队列,等待JS引擎处理。
  • 定时器触发线程:计时并触发定时(setTimout,setInterval)。计时完毕回调函数会被事件触发线程添加到事件队列。
  • 异步http请求线程:处理XMLHttpRequest请求的线程。监听到状态变化后回调函数会被事件触发线程添加到事件队列。

(二)JavaScript运行机制

  • JS引擎线程可以处理同步任务和异步任务,事件触发线程管理者一个事件队列,异步任务(DOM事件回调,定时器回调,AJAX响应回调)有了运行结果就会往队列中放置一个事件。
  • JS引擎线程优先执行同步任务,同步任务会压入执行栈。
  • JS引擎处理完同步任务,空闲时就查询事件队列是否有异步任务,有则压入执行栈执行,无则挂着等待。

(一)SetTimeout

setTimeout(()=>{
	console.log("HELLO WORLD")
},250);

以上面代码为例,基于事件循环机制的认识,定时器触发线程计算到250ms后,setTimeout回调函数才被推进事件队列。因此回调函数什么时候执行,还要看JavaScript引擎线程是否空闲,事件队列中有多少个没处理任务。理想情况下,事件队列里没有其他任务且JavaScript引擎线程空闲,就会在250ms之后执行回调。

再看书中例子,就很容易理解了,假设执行onclick的回调需要耗费300ms,改回调在执行到第5ms时创建定时器。

btn.onclick=function(){
	setTimeout(()=>{
		console.log("HELLO WORLD")
	},250);
}

(二)SetInterval

JavaScript为避免定时器代码不断执行,做了一定的处理:当没有定时器的任何回调在执行时,才会将新的定时器回调添加到事件队列。在这种情况下,定时器回调的执行间隔可能回比与其小。假设执行循环定时器回调要300ms。

setInterval(()=>{
	console.log("HELLO WORLD")
},250);

JavaScript进程时间线如下,回调的执行间隔理想情况下只有200ms。

二、函数防抖与节流

基本思想:某些代码可以在几乎没有间断的情况下重复执行,譬如一直滚动页面触发滚动回调,一直输入触发输入回调。但是不能让他这样做因为耗费资源又没意义,根据交互场景,采取函数防抖或者函数节流措施。

(一)函数节流

zhuanlan.zhihu.com/p/110810391

(二)函数防抖

zhuanlan.zhihu.com/p/110733457

(三)函数节流和函数防抖的区别

什么时候应该用函数节流?什么时候应该用函数防抖?

这里只根据上面链接中给的例子,总结一些理解。

函数防抖:搜索框不断输入,输入10个字符,如果每次输入都发送依次请求就发送10次,但是用户真正要搜索的内容是10个字符拼接起来的单词,所以中间9次就是徒劳的请求。所以,函数防抖就是延迟执行回调,等待某些工作完成再去执行回调。因为最终剩下的定时器就是最后一次输入设置的定时器,所以输入完成时间+定时时间之后,这个定时器回调才进入事件队列,等待执行。

函数节流:页面滚动到一定距离出现回到顶部按钮的例子,页面一直滚动就一直计算scrollTop。如果是用函数防抖的方式,最后就是页面完全不滚动了之后才会出现回到顶部按钮,显示这是不合理的。而函数节流,就是在通过指定执行回调时间单位>触发事件的时间间隔,降低执行回调频率。在页面滚动的场景中,就是每隔200ms执行一次回调,在200ms内,只有第一次执行是生效的,之后达到200ms才执行一次,直到没有滚动事件发生。

三、高阶函数技术

函数:函数被称为“一等公民”,指的是函数与其他数据类型一样,处于平等地位。函数是一种特殊类型的对象,它们是 Function objects。

(一)高阶函数

如果一个函数符合下面两个规范的任何一个,那该函数是高阶函数。

1、若A函数,接收的参数是一个函数,则A就可以称为高阶函数;

2、若A函数,调用的返回值是一个函数,那么A就可以称为高阶函数。

常用的高阶函数有:Promise、setTimeout、数组的很多方法(Array.prototype.map等)。

高阶函数参考:juejin.cn/post/684490…

(二)组合函数

组合函数参考:juejin.cn/post/689288…(下文引用这篇文章则标为链接1)

(三)函数柯里化

函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。如下所示:

function sum(a){
	return (b)=>{
		return(c)=>{
			return a+b+c
		}
	}
}
let result=sum(1)(2)(3)

下面是使用《JavaScript高级程序设计》给出的创建柯里化函数通用方式,对函数柯里化的例子:

//创建柯里化函数的通用方式,《JavaScript高级程序设计》
function curry(fn){
	var args=Array.prototype.slice.call(arguments,1);  //arguments是curry函数的参数数组
	return function(){
		var innerArgs=Array.prototype.slice.call(arguments);  //arguments是这个匿名函数的参数数组
		var finalArgs=args.concat(innerArgs);
		return fn.apply(null,finalArgs);
	}
}
//要被柯里化的函数
var fn = function(a, b, c) {
	return [a, b, c];
};

var r1 =curry(fn,1,2,3)();
console.log(r1);   //[1,2,3]

var r2 =curry(fn,4)(5,6);
console.log(r2);   //[4,5,6]

var r3 =curry(fn)(7,8,9);
console.log(r3);   //[7,8,9]

不过在很多博客中,上面算是一个偏函数,柯里化函数是偏函数的一个特殊形式。

偏函数应用是固定一个函数的一个或多个参数,并返回一个可以接收剩余参数的函数; 柯里化是将函数转化为多个嵌套的一元函数,也就是每个函数只接收一个参数(实际可以一一个或多个参数)。

可以对上面的函数做一个改造,如下所示

//与上面相比,增加了一层,curry的入参只有指定的柯里化函数一个
function curry(fn){
	return function(){
		var args=Array.prototype.slice.call(arguments);  //注意这里的差异
		return function(){
			var innerArgs=Array.prototype.slice.call(arguments);
			var finalArgs=args.concat(innerArgs);
			return fn.apply(null,finalArgs);
		}
	}
}
var fn = function(a, b, c) {
	return [a, b, c];
};
var r1 =curry(fn)(1,2,3)();
console.log(r1);     //[1,2,3]
var r2 =curry(fn)(4)(5,6);
console.log(r2);     //[4,5,6]
var r3=curry(fn)()(7,8,9);
console.log(r3);     //[7,8,9]

参考“链接1”中的例子,利用ES6解构赋值可以进一步改造为:

function curry(fn){
	return function(...args){
		return function(...args1){
			var finalArgs=args.concat(args1);
			return fn.apply(null,finalArgs);
		}
	}
}

(四)惰性函数

惰性函数参考:juejin.cn/post/689288…