by caix in 深圳
高级技巧 - 高级函数
在 JavaScript 里面,函数拥有非常高的特权,甚至是一等公民,因此也跟Kotlin一样支持多种编程范式
| 一些写函数时的高级技巧 |
| |
| 纯函数 |
| 高阶函数 |
| 函数缓存 |
| 懒函数 |
| 柯里化 |
| 函数组合 |
纯函数
| 纯函数要满足两个条件: |
| |
| 1、给相同的参数返回相同的结果 |
| 2、不产生任何副作用 |
| |
| function double(num){ |
| return num * 2 |
| } |
| |
| 这边只要给num的值不变,它返回的结果也不会变,而且这个函数执行的过程中没有对外界造成影响,所以它是一个纯函数 |
高阶函数
| 高阶函数至少要满足下面条件中的一个: |
| |
| 1、接受函数作为参数 |
| 2、把函数作为结果返回 |
| |
| 假设我们有一个数组,我们想用它来创建一个新的数组,这个新数组中每个元素是之前的数组对应位置的元素 +1 |
| |
| 不用高阶函数的话,我们大概会这么写 |
| |
| const arr1 = [1, 2, 3]; |
| const arr2 = []; |
| for (let i = 0; i < arr1.length; i++) { |
| arr2.push(arr1[i] + 1); |
| } |
| |
| 但是JavaScript的数组对象有一个map方法,这个map方法接受一个回调,会对当前数组对象的每一个元素应用这个回调,返回一个新数组 |
| |
| const arr1 = [1, 2, 3]; |
| const arr2 = arr1.map(function(item) { |
| return item + 1; |
| }); |
| console.log(arr2); |
| |
| 这个 map函数 就是一个高阶函数,map有映射的意思,我们扫一眼很快就能明白这段代码声明了对于原来对象的转换,基于原来的数组对象的元素创建一个新的数组 |
函数缓存
| 假设我们有个很耗时的纯函数: |
| |
| function computed(str) { |
| |
| console.log('执行了10分钟') |
| |
| return '算出来了' |
| } |
| |
| 为了避免不必要的重复计算,我们可以缓存一些之前已经计算过的结果。这样再后面再遇到相同的计算时,我们可以从缓存中直接取出结果。我们在这儿需要编写一个名为cached的函数去包装我们实际要调用的函数,这个函数把目标函数作为参数,返回一个新的函数。在这个cached函数里,我们缓存之前函数调用的结果。 |
| |
| function cached(fn){ |
| |
| const cache = Object.create(null); |
| |
| |
| return function cachedFn (str) { |
| |
| |
| if ( !cache[str] ) { |
| let result = fn(str); |
| |
| |
| cache[str] = result; |
| } |
| |
| return cache[str] |
| } |
| } |
懒函数
| 函数体里面会包含各种各样的条件语句,有时候这些条件语句仅仅需要执行一次,比如说我们写单例的时候判断某个对象是否为空,如果为空我们就创建一个对象,那其实我们知道后续只要程序还在运行,这个对象是不可能为空的,但是我们每次使用时都还会判断是否为空,都会执行我们的条件判断。我们可以稍微提升一下性能通过在第一次执行后删除这些条件判断,这样后面就不判断是否为空直接拿来即用了,这就是懒函数。 |
| |
| 我们把上面的描述用简单的代码表现出来: |
| |
| let instance = null; |
| function user() { |
| if ( instance != null) { |
| return instance; |
| } else { |
| instance = new User() |
| return instance; |
| } |
| } |
| |
| 上面的代码在每次执行的时候都会执行条件判断,这边还好,如果我们的条件判断非常复杂,那其实也是一个不小的性能影响,这时候我们就可以使用懒函数的小技巧来优化代码: |
| |
| var user = function() { |
| var instance = new User(); |
| user = function() { |
| return instance; |
| }; |
| return user(); |
| } |
| |
| 这样在第一次执行后,我们用一个新函数重写了之前的函数,后面再执行这个函数的时候我们都会直接返回一个固定的值,这无疑会提高我们代码的性能。 |
| |
| 所以后续我们遇到一些只用执行一次的条件语句,我们都可以用懒函数来优化它,通过使用一个新函数来覆盖原有的函数来移除条件语句。 |
函数柯里化
| 柯里化简单来说就是把一个接受多个参数的函数转化成一串接受单个参数的函数,这么说可能有点绕,其实就是把一个一次性接受一堆参数的函数,转化成接受第一个参数返回一个接受第二个参数的函数,这个函数返回一个接受第三个参数返回一个接受第四个参数的函数,以此类推。 |
| |
| 可能好多同学第一次遇到不知道它有什么用,能一次调用完为什么要整这么花里胡哨呢? |
| |
| 1、柯里化可以让我们避免重复传相同的值 |
| 2、这其实上是创建了一个高阶函数,方便我们处理数据 |
| |
| function sum(a,b,c){ |
| return a + b + c; |
| } |
| |
| sum(1,2,3) --> 6 |
| sum(1,2) --> NaN |
| sum(1,2,3,4) --> 6 |
| |
| 那么怎样我们才能把它转化成一个柯里化的版本呢? |
| |
| function curry(fn) { |
| if (fn.length <= 1) return fn; |
| const generator = (...args) => { |
| if (fn.length === args.length) { |
| return fn(...args) |
| } else { |
| return (...args2) => { |
| return generator(...args, ...args2) |
| } |
| } |
| } |
| return generator |
| } |
| |
| 我们可以获得跟之前一梭子传递所有参数一样的结果,同时我们还可以在任何一步中缓存之前计算的结果,比如我们这次要传入(1,2,3,6),那我们是可以避免对前面三个参数进行重复计算的。 |
函数组合
| 假设我们需要实现一个把给定数字乘10然后转成字符串输出的功能,那我们需要做的有两件事: |
| |
| 1、给定数字乘10 |
| 2、数字转字符串 |
| |
| 我们拿到手大概会这么写: |
| |
| const multi10 = function(x) { return x * 10; }; |
| const toStr = function(x) { return `${x}`; }; |
| const compute = function(x){ |
| return toStr(multi10(x)); |
| }; |
| |
| 这边只有两步,所以看起来不复杂,实际情况是如果有更多的操作的话,层层嵌套很难看也容易出错,类似于这样fn3(fn2(fn1(fn0(x))))。为了避免这种情况,把调用层级扁平化,我们可以写一个compose函数专门用来把函数调用组合到一起: |
| |
| const compose = function(f,g) { |
| return function(x) { |
| return f(g(x)); |
| }; |
| }; |
| |
| 之后我们的compute函数就可以这么写了: |
| |
| let `compute` = compose(toStr, multi10); |
| compute(8); |
| |
| 通过使用 compose函数 我们可以把两个函数组合成一个函数,这让代码从右往左执行,而不是层层计算某个函数的结果作为另一个函数的参数,这样代码也更加直观。但是现在compose仅仅支持两个参数,没关系我们可以写一个支持任意参数的版本 |
| |
| function compose(...funs){ |
| return (x)=>funs.reduce((acc, fun) => fun(acc), x) |
| } |
| |
| 通过函数组合,我们可以可以声明式地指定函数间的关系,代码的可读性也大大提高,也方便我们后续对代码进行扩展跟重构,而且在React里面,当我们的高阶组件变多的时候,一个套着一个就很难看,我们就可以通过类似的方式来让我们的高阶组件层级扁平化 |
高级技巧 - 对象防篡改
| 1. 防篡改对象:--- 一级防备 (不允许给对象添加新的属性或者方法) |
| |
| 需求原因:在JavaScript中,对象可以在同一环境中任何地方被修改,而对象一旦被修改,在多人合作的项目时,及其容易造成不可预知的问题,因此,我们需要在一些情况下,去阻止对象属性的可篡改性。 |
| |
| 实现方法:ES5为对象提供了一个方法:** Object.preventExtensions(yourObjName) |
| |
| 后果:一旦使用了上述方法的对象,再篡改对象属性时,在非严格模式下,静默失败,严格模式下会抛出错误。 |
| |
| 检验对象是否被禁止篡改:Object.isExtensible(youObjName) |
| |
| |
| 2. 密封对象:--- 二级防备(在防篡改上同时不允许删除属性或方法) |
| |
| 实现原理:将对象的defineProperty()属性变为false,密封后,增加属性**-->属性值为undefined(即:被忽略),删除属性-->**依然可以访问该属性(即:被忽略),在严格模式下,增删密封对象属性都会抛出错误。 |
| |
| 实现方法:Object.seal(yourObjName) |
| |
| 检验是否被密封**:Object.isSealed(yourObjName) |
| |
| 3. 冻结对象 --- 三级防备(不允许重新对象属性---用于顶层封装) |
| |
| 特点:冻结的对象既不可扩展,也是密封的,对象的writable属性设置为false |
| |
| 实现方法:Object.freeze(yourObjName) |
| |
| 检测是否被冻结:Object.isFrozen(yourObjName) |
高级技巧 - 高级定时器
setTimeout()
| 创建定时器,程序会被挂起,时间结束时把任务添加到 JavaScript 进程栈中,又因为 JavaScript 是单线程,必须等待前面的代码执行结束,所以定时器执行函数总会比设定时间要晚。 |
| |
| 基本用法 |
| |
| setTimeout(() => { |
| |
| }, timeout); |
| |
| |
setInterval()
| 与 "setTimeout" 类似,区别在于 "setInterval" 会定期向 JavaScript 进程栈添加任务,但会有几个问题发生。(1) 某些间隔会被跳过;(2) 多个定时器的代码执行之间的间隔可能比设定值要小。(3) 前一个定时器代码未执行,当前定时器代码则被跳过。 |
| |
| 基本用法: |
| |
| setInterval(() => { |
| |
| }, interval); |
| "onclick"时间处理函数内添加一个 "setInterval" 重复定时器,且给定了 200ms 的间隔,而定时器代码需要 300ms+ 才能执行完,但 "onclick" 处理函数内还有其他代码需要执行,耗时 300ms。 |
| |
| 那么第一个定时器代码执行与 300ms 处,第二个则执行与 600ms+ 处,而 605ms 处本该被添加的定时器则被跳过,因为第二个定时器代码被添加却未被执行。 |
| 使用递归 "setTimeout" 代替 "setInterval" |
| |
| setTimeout(() => { |
| |
| setTimeout(arguments.callee, interval); |
| }, interval); |
| |
| 但上述代码存在一定问题,因为严格模式下,无法使用 "arguments.callee" 获得函数本身。建议把处理函数具名化,如下: |
| |
| setTimeout(function process() { |
| |
| setTimeout(process, 1000); |
| }, 1000); |
定时器妙用
| Yielding Processes |
| |
| 运行在浏览器中的 JavaScript 都被分配了一个确定数量的资源。如果代码运行超过特定时间或者特定语句数量就不会继续执行。过长、嵌套过深的函数调用或者是进行大量处理的循环都是造成脚本时间运行过长的主要原因。 |
| |
| 如以下代码,假设 "process" 处理需要时间过长,数组长度也较长,该函数则会阻塞进程。 |
| |
| for (let i = 0, len = data.length; i < len; i++) { |
| // 假设处理需要 200ms |
| process(data[i]) |
| } |
| |
| 要改善上述情况,先要符合两个以下条件,则可以使用 "数组分块" 技术: |
| |
| 1、该处理不需要同步完成; |
| 2、数据可以不按顺序完成。 |
| |
| setTimeout(function timeoutProcess() { |
| // 取出下一个条目并处理,data:<Array> |
| let item = data.shift(); |
| process(item); |
| // 判断是否还有条目,有则设置另一个定时器 |
| if (arr.length > 0) { |
| setTimeout(timeoutProcess, 100); |
| } |
| }, 100); |
函数防抖 (debounce)
某些高频操作是没必要的,如用户连续点击某个开关,导致请求多次发送。则可以使用 "函数防抖" ,等待用户操作停下后片刻再发送请求
"函数防抖" 的原理是利用定时器延迟执行,当重复执行时,先把原本定时器清除,再添加延迟执行代码,那么总是最后一次操作后延时执行代码。
| 核心代码: |
| |
| |
| function debounce(methods, context){ |
| clearTimeout(methods.timerId); |
| |
| methods.timerId = setTimeout(() => { |
| methods.call(context); |
| }, 100); |
| } |
| 完整实例:(以 click 事件为例) |
| |
| |
| |
| function debounce(fn, delay) { |
| |
| let timer = null; |
| return function () { |
| |
| let context = this, |
| args = arguments; |
| if (timer) { |
| clearTimeout(timer); |
| timer = null |
| } |
| timer = setTimeout(() => { |
| fn.call(this, args, '给防抖函数的额外参数') |
| }, delay); |
| } |
| } |
| |
| function printFn(args, myArgs) { |
| console.log('do it!', ...args, myArgs) |
| } |
| |
| document.getElementById('btn').addEventListener('click', debounce(printFn, 1000)) |
函数节流 (throttle)
当我们以一定频率处理某些频发事件,我们可以使用"函数节流"。
"函数节流" 的原理是利用定时器延迟执行,当重复执行时,忽略新添加的处理函数。
| 核心代码: |
| |
| |
| function throttle(methods, context) { |
| |
| if (!methods.timerId) { |
| methods.timerId = setTimeout(() => { |
| methods.timerId = null; |
| methods.call(context); |
| }, 1000); |
| } |
| } |
| 完整实例:(以 click 事件为例) |
| |
| |
| |
| function throttle(fn, delay) { |
| |
| let timer = null; |
| return function () { |
| let context = this, |
| args = arguments; |
| |
| if (!timer) { |
| timer = setTimeout(() => { |
| timer = null |
| fn.call(this, args, '给防抖函数的额外参数') |
| }, delay); |
| } |
| } |
| } |
| |
| function printFn(args, myArgs) { |
| console.log('do it!', ...args, myArgs) |
| } |
| |
| document.getElementById('btn').addEventListener('click', throttle(printFn, 1000)) |
window.requestAnimationFrame()
| requestAnimationFrame是HTML5新增的定时器。用法与setTimeout类似,都是在一段时间后执行回调函数,区别在于不用传第二个时间参数。 |
| |
| 执行回调函数的时延是根据显示器刷新率决定的,例如刷新率为 60Hz 的显示器,时延为1000ms/60,约等于16.6ms。执行回调函数的时间是比较准确的(众所周知,setTimeout和setInterval的执行回调函数时间并不准确) |
| |
| 使用cancelAnimationFrame()方法可以取消requestAnimationFrame()定时器。 |
| |
| 例子 |
| |
| 进度条动画 |
| |
| <div id="myDiv" style="background:lightblue;width:0;height:20px;line-height:20px;">0%</div> |
| <button id="btn">run</button> |
| |
| let btn = document.getElementById('btn'), |
| myDiv = document.getElementById('myDiv'), |
| timer; |
| btn.onclick = function () { |
| myDiv.style.width = '0'; cancelAnimationFrame(timer); |
| timer = requestAnimationFrame(function fn() { |
| if (parseInt(myDiv.style.width) < 500) { |
| myDiv.style.width = parseInt(myDiv.style.width) + 5 + 'px'; |
| myDiv.innerHTML = parseInt(myDiv.style.width) / 5 + '%'; |
| timer = requestAnimationFrame(fn); |
| } else { |
| cancelAnimationFrame(timer); |
| } |
| }); |
| } |
函数防抖 和 函数节流 的区别
| 函数防抖(debounce) |
| |
| 概念: 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。 |
| |
| 生活中的实例: 如果有人进电梯(触发事件),那电梯将在10秒钟后出发(执行事件监听器),这时如果又有人进电梯了(在10秒内再次触发该事件),我们又得等10秒再出发(重新计时)。 |
| |
| 函数节流(throttle) |
| |
| 概念: 规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。 |
| |
| 生活中的实例: 我们知道目前的一种说法是当 1 秒内连续播放 24 张以上的图片时,在人眼的视觉中就会形成一个连贯的动画,所以在电影的播放(以前是,现在不知道)中基本是以每秒 24 张的速度播放的,为什么不 100 张或更多是因为 24 张就可以满足人类视觉需求的时候,100 张就会显得很浪费资源。 |
| 应用场景 |
| |
| 对于函数防抖,有以下几种应用场景: |
| |
| 1、给按钮加函数防抖防止表单多次提交。 |
| 2、对于输入框连续输入进行AJAX验证时,用函数防抖能有效减少请求次数。 |
| 3、判断scroll是否滑到底部,滚动事件+函数防抖 |
| |
| 总的来说,函数防抖适合多次事件一次响应的情况 |
| |
| 对于函数节流,有如下几个场景: |
| |
| 1、游戏中的刷新率 |
| 2、DOM元素拖拽 |
| 3、Canvas画笔功能 |
| |
| 总的来说,函数防抖适合大量事件按时间做平均分配触发。 |
| |
| 函数防抖和函数节流是在时间轴上控制函数的执行次数 |
| |
| 防抖可以类比为电梯不断上乘客,节流可以看做幻灯片限制频率播放电影。 |
| 函数防抖: |
| |
| function debounce(fn, wait) { |
| var timer = null; |
| return function () { |
| var context = this |
| var args = arguments |
| if (timer) { |
| clearTimeout(timer); |
| timer = null; |
| } |
| timer = setTimeout(function () { |
| fn.apply(context, args) |
| }, wait) |
| } |
| } |
| |
| var fn = function () { |
| console.log('boom') |
| } |
| |
| setInterval(debounce(fn,500),1000) |
| |
| setInterval(debounce(fn,2000),1000) |
| |
| 之所以返回一个函数,因为防抖本身更像是一个函数修饰,所以就做了一次函数柯里化。里面也用到了闭包,闭包的变量是timer。 |
| 函数节流: |
| |
| function throttle(fn, gapTime) { |
| let _lastTime = null; |
| |
| return function () { |
| let _nowTime = + new Date() |
| if (_nowTime - _lastTime > gapTime || !_lastTime) { |
| fn(); |
| _lastTime = _nowTime |
| } |
| } |
| } |
| |
| let fn = ()=>{ |
| console.log('boom') |
| } |
| |
| setInterval(throttle(fn,1000),10) |
| |
| 如图是实现的一个简单的函数节流,结果是一秒打出一次 boom |
高级技巧 - 自定义事件
事件是JavaScript与浏览器交互的主要途径。事件是一种叫 观察者模式 的设计模式, 是一种创建松耦合的代码技术。
在代码中存在多个地方在特定的时刻相互交互的情况下,自定义事件就非常有用了
| 对象可以发布事件,用来表示在该对象生命周期中某个时刻的到来,然后其他对象可以观察该对象, 等待这些时刻的到来并透过运行相应代码来响应; |
| |
| 上述提到的 观察者模式 由两类对象组成: 主体 和 观察者 |
| |
| 主体负责发布事件, 同时观察者通过订阅这些事件来观察主体 |
| |
| 该模式的一个关键概念是: 主体并不知道观察者的任何事情, 也就是说它可以独自存在并正常运行即使观察者不存在. 从另一个方面说: 观察者知道主体并能注册事件的回调函数(事件处理程序). |
| |
| 如涉及DOM时, DOM就是主体,您的事件处理代码就是观察者 |
| |
| 事件是与DOM交互的最常见的方式,但是它们也可以用于非DOM代码中, 通过 自定义事件 |
| 自定义事件背后的概念是创建一个管理事件的对象, 让其他对象监听那些事件. |
实现代码如下
| EventTarget 基本代码 |
| |
| function EventTarget() { |
| this.handlers = {}; |
| } |
| EventTarget.prototype = { |
| constructor: EventTarget, |
| addHandler: function (type, handler) { |
| if( typeof this.handlers[type] == "undefined"){ |
| this.handlers[type] = []; |
| } |
| this.handlers[type].push(handler); |
| }, |
| fire: function(event) { |
| if(!event.targer) { |
| event.targer = this; |
| } |
| if(this.handlers[event.type] instanceof Array) { |
| let handlers = this.handlers[event.type]; |
| for (let i = 0, len = handlers.length; i < len; i++) { |
| handlers[i](event); |
| } |
| } |
| }, |
| removeHandler: function(type, handler) { |
| if(this.handler[type] instanceof Array) { |
| let handlers = this.handlers[type]; |
| for (let i = 0, len = handlers.length; i < len; i++) { |
| if(handlers[i] === hander){ |
| break; |
| } |
| handlers.splice(i, i); |
| } |
| } |
| } |
| } |
| EventTarget 类型属性和方法解释: |
| |
| 1、handlers: 用于存储事件处理程序 |
| |
| 2、addHandler(): 用于注册给定类型的事件的事件处理程序 |
| |
| 此方法接收两个参数: 事件类型和用于处理该事件的函数. 当调用该方法时, 会进行一次检查 看看handlers中是否已经存在一个针对该事类型的数组; 如果没有, 则创建一个新的,然后使用push()将该处理程序添加到数组末尾 |
| |
| 3、fire(): 用于触发一个事件 |
| |
| 先给event对象设置一个target属性, 如果他尚未被指定的话, 然后查找对应该事件类型的一组处理程序,调用各个函数, 并给出event对象 如果有其他额外的信息请自定义 |
| |
| 4、removeHandler(): 用于注销某个事件类型的事件处理程序 |
| |
| removeHandler()是addHandler()的辅助, 他们接受的参数一致. 此方法搜索事件处理程序的数组找到要删除的处理程序的位置. 如果找到了, 使用 break操作符退出循环, 然后使用 splice()方法将该项目从数组中删除. |
| EventTarget 使用 |
| |
| |
| function handlerMessage(event) { |
| console.log(`Message received: ${event.message}`); |
| } |
| |
| |
| let target = new EventTarget(); |
| |
| |
| |
| target.addHandler('message', handlerMessage); |
| |
| |
| target.fire({ |
| type: 'message', |
| message: 'Hello EventTarget' |
| }) |
| |
| |
| |
| |
| target.removeHandler('message', handlerMessage); |
| |
| |
| target.fire({ |
| type: 'message', |
| message: 'Hello EventTarget' |
| }) |
| |
| |
| |
| 上述代码中, 定义了handleMessage() 函数用于处理message事件. 它接受event对象并输出message属性. 调用target对象的addHandler()方法并传给message和handleMessage() 函数. 之后调用触发函数fire() 并传递了包含2个属性, 即 type和message的对象直接量. 它会调用message事件的事件处理程序. 这样就会在控制台输出(handleMessage()方法的打印),然后删除事件处理程序, 之后再次调用, 不会输出任何信息, 但控制台抛出异常Uncaught TypeError: Cannot read property 'message' of undefined |
| EventTarget是封装在一种自定义类型中,其他对象可以继承EventTarget并获取这个行为 |
| |
| function inheritPrototype(subType, superType){ |
| let prototype = Object(superType.prototype); |
| prototype.constructor = subType; |
| subType.prototype = prototype; |
| } |
| |
| function Person(name, age) { |
| EventTarget.call(this); |
| this.name = name; |
| this.age = age; |
| } |
| inheritPrototype(Person, EventTarget); |
| |
| Person.prototype.say = function (message) { |
| this.fire({type: 'message', message: message}); |
| } |
| |
| Person 类型使用了寄生组合继承方法来继承EventTarget. 一旦调用say()方法,便触发事件, 它包含了消息细节 |
| |
| function handlerSayMessage(event) { |
| console.log(`${event.targer.name} says: ${event.message}`); |
| } |
| |
| var person = new Person("Allen", 20); |
| |
| |
| person.addHandler("message", handlerSayMessage); |
| |
| |
| person.say(" Hi There."); |
| |
| |
高级技巧 - 拖放
| 元素绝对定位,通过 event对象 的相关属性,做封装 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)