防抖
你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。
这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。因为防抖动的轮子很多,这里也不重新自己造个轮子了,直接使用 underscore 的源码来解释防抖动。
/** * underscore 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行 * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {boolean} immediate 设置为ture时,是否立即调用函数 * @return {function} 返回客户调用函数 */ _.debounce = function(func, wait, immediate) { var timeout, args, context, timestamp, result; var later = function() { // 现在和上一次时间戳比较 var last = _.now() - timestamp; // 如果当前间隔时间少于设定时间且大于0就重新设置定时器 if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { // 否则的话就是时间到了执行回调函数 timeout = null; if (!immediate) { result = func.apply(context, args); if (!timeout) context = args = null; } } }; return function() { context = this; args = arguments; // 获得时间戳 timestamp = _.now(); // 如果定时器不存在且立即执行函数 var callNow = immediate && !timeout; // 如果定时器不存在就创建一个 if (!timeout) timeout = setTimeout(later, wait); if (callNow) {
防抖
你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。
这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。因为防抖动的轮子很多,这里也不重新自己造个轮子了,直接使用 underscore 的源码来解释防抖动。
/** * underscore 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行 * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {boolean} immediate 设置为ture时,是否立即调用函数 * @return {function} 返回客户调用函数 */ _.debounce = function(func, wait, immediate) { var timeout, args, context, timestamp, result; var later = function() { // 现在和上一次时间戳比较 var last = _.now() - timestamp; // 如果当前间隔时间少于设定时间且大于0就重新设置定时器 if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { // 否则的话就是时间到了执行回调函数 timeout = null; if (!immediate) { result = func.apply(context, args); if (!timeout) context = args = null; } } }; return function() { context = this; args = arguments; // 获得时间戳 timestamp = _.now(); // 如果定时器不存在且立即执行函数 var callNow = immediate && !timeout; // 如果定时器不存在就创建一个 if (!timeout) timeout = setTimeout(later, wait); if (callNow) { // 如果需要立即执行函数的话 通过 apply 执行 result = func.apply(context, args); context = args = null; } return result; }; };
整体函数实现的不难,总结一下。
- 对于按钮防点击来说的实现:一旦我开始一个定时器,只要我定时器还在,不管你怎么点击都不会执行回调函数。一旦定时器结束并设置为
null
,就可以再次点击了。 - 对于延时执行函数来说的实现:每次调用防抖动函数都会判断本次调用和之前的时间间隔,如果小于需要的时间间隔,就会重新创建一个定时器,并且定时器的延时为设定时间减去之前的时间间隔。一旦时间到了,就会执行相应的回调函数。
// 如果需要立即执行函数的话 通过 apply 执行 result = func.apply(context, args); context = args = null; } return result; }; };
整体函数实现的不难,总结一下。
- 对于按钮防点击来说的实现:一旦我开始一个定时器,只要我定时器还在,不管你怎么点击都不会执行回调函数。一旦定时器结束并设置为
null
,就可以再次点击了。 - 对于延时执行函数来说的实现:每次调用防抖动函数都会判断本次调用和之前的时间间隔,如果小于需要的时间间隔,就会重新创建一个定时器,并且定时器的延时为设定时间减去之前的时间间隔。一旦时间到了,就会执行相应的回调函数。
节流
防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
/** * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。 * 如果想忽略结尾函数的调用,传入{trailing: false} * 两者不能共存,否则函数不能执行 * @return {function} 返回客户调用函数 */ _.throttle = function(func, wait, options) { var context, args, result; var timeout = null; // 之前的时间戳 var previous = 0; // 如果 options 没传则设为空对象 if (!options) options = {}; // 定时器回调函数 var later = function() { // 如果设置了 leading,就将 previous 设为 0 // 用于下面函数的第一个 if 判断 previous = options.leading === false ? 0 : _.now(); // 置空一是为了防止内存泄漏,二是为了下面的定时器判断 timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { // 获得当前时间戳 var now = _.now(); // 首次进入前者肯定为 true // 如果需要第一次不执行函数 // 就将上次时间戳设为当前的 // 这样在接下来计算 remaining 的值时会大于0 if (!previous && options.leading === false) previous = now; // 计算剩余时间 var remaining = wait - (now - previous); context = this; args = arguments; // 如果当前调用已经大于上次调用时间 + wait // 或者用户手动调了时间 // 如果设置了 trailing,只会进入这个条件 // 如果没有设置 leading,那么第一次会进入这个条件 // 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了 // 其实还是会进入的,因为定时器的延时 // 并不是准确的时间,很可能你设置了2秒 // 但是他需要2.2秒才触发,这时候就会进入这个条件 if (remaining <= 0 || remaining > wait) { // 如果存在定时器就清理掉否则会调用二次回调 if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { // 判断是否设置了定时器和 trailing // 没有的话就开启一个定时器 // 并且不能不能同时设置 leading 和 trailing timeout = setTimeout(later, remaining); } return result; }; };
继承
在 ES5 中,我们可以使用如下方式解决继承的问题
function Super() {} Super.prototype.getNumber = function() { return 1 } function Sub() {} let s = new Sub() Sub.prototype = Object.create(Super.prototype, { constructor: { value: Sub, enumerable: false, writable: true, configurable: true } })
以上继承实现思路就是将子类的原型设置为父类的原型
在 ES6 中,我们可以通过 class
语法轻松解决这个问题
class MyDate extends Date { test() { return this.getTime() } } let myDate = new MyDate() myDate.test()
但是 ES6 不是所有浏览器都兼容,所以我们需要使用 Babel 来编译这段代码。
如果你使用编译过得代码调用 myDate.test()
你会惊奇地发现出现了报错
因为在 JS 底层有限制,如果不是由 Date
构造出来的实例的话,是不能调用 Date
里的函数的。所以这也侧面的说明了:ES6 中的 class
继承与 ES5 中的一般继承写法是不同的。
既然底层限制了实例必须由 Date
构造出来,那么我们可以改变下思路实现继承
function MyData() { } MyData.prototype.test = function () { return this.getTime() } let d = new Date() Object.setPrototypeOf(d, MyData.prototype) Object.setPrototypeOf(MyData.prototype, Date.prototype)
以上继承实现思路:先创建父类实例 => 改变实例原先的 _proto__
转而连接到子类的 prototype
=> 子类的 prototype
的 __proto__
改为父类的 prototype
。
通过以上方法实现的继承就可以完美解决 JS 底层的这个限制。