说说JavaScript中函数的防抖-Debounce-与节流-Throttle

为何要防抖和节流有时候会在项目开发中频繁地触发一些事件,如 resize、 scroll、 keyup、 keydown等,或者诸如输入框的实时搜索功能,我们知道如果事件处理函数无限制调用,会大大加重浏览器的工作量,有可能导致页面卡顿影响体验;后台接口的频繁调用,不仅会影响客户端体验,还会大大增加服务器的负担。而如果对这些调用函数增加一个限制,让其减少调用频率,岂不美哉? 针对这个问题,一般有两个方案:防抖 (Debounce) 节流 (Throttle) 防抖(Debounce)我对函数防抖的定义:当函数被连续调用时,该函数并不执行,只有当其全部停止调用超过一定时间后才执行1次。 一个被经常提起的例子: 上电梯的时候,大家陆陆续续进来,电梯的门不会关上,只有当一段时间都没有人上来,电梯才会关门。Talk is cheap,我们直接 show code 吧。 先做基本的准备(篇幅原因,HTML部分省略): let container = document.getElementById('container');// 事件处理函数function handle(e) { onsole.log(Math.random()); }// 添加滚动事件container.addEventListener('scroll', handle);我们发现,每滚动一下,控制台就会打印出一行随机数。 基础防抖我们现在写一个最基础的防抖处理: function debounce(func, wait) { var timeout;//标记 return function() { clearTimeout(timeout); timeout = setTimeout(func, wait); }}事件也做如下改写: container.addEventListener('scroll', debounce(handle, 1000));现在试一下, 我们会发现只有我们停止滚动1秒钟的时候,控制台才会打印出一行随机数。 标准防抖以上基础版本会有两个问题,请看如下代码: // 处理函数function handle(e) { console.log(this); //输出Window对象 console.log(e); //undefined}没错,当我们不使用防抖处理时,handle()函数的this指向调用此函数的container,而在外层使用防抖处理后,this的指向会变成Window。其次,我们也要获取到事件对象event。 所以我们要对防抖函数做以下改写: function debounce(fn, wait) { let timeout; return function() { clearTimeout(timeout); timeout = setTimeout(()=>{ fn.apply(this,arguments)//使用apply改变this指向 }, wait); }}当然了,如果使用箭头函数便可以省去外层声明。 ...

June 17, 2019 · 1 min · jiezi

underscore-诞生记二-链式调用与混入mixin

上篇文章讲述了 underscore 的基本结构搭建,本文继续讲链式调用与混入。 如果你还没看过第一篇文章,请点击 “underscore 诞生记(一)—— 基本结构搭建” 链式调用在 JQuery 中,我们经常使用到链式调用,如: $('.div') .css('color', 'red') .show();那么在 underscore 中,是否支持链式调用呢?答案是支持的,只不过默认不开启链式调用罢了。 想要实现链式调用,通常我们会在支持链式调用的函数中返回对象本身: let car = { run(name) { console.log(`${name}老司机开车啦喂!`); return this; }, stop() { console.log('车停了'); },};car.run('奔驰').stop();// 奔驰老司机开车啦喂!// 车停了那么在每个 _ 方法下都 return this , 显然不大优雅缺乏可控性!尝试着写个通用方法 chain() 开启链式调用。 _.chain = function(obj) { // 获得一个经underscore包裹后的实例 var instance = _(obj); // 标识当前实例支持链式调用 instance._chain = true; return instance;};// 小试牛刀_.chain([1, 2, 3]);/* { _chain: true, _wrapped: [1, 2, 3]} */返回的为一个实例对象,后面的方法判断 _chain 属性是否为 true,为 true 的话再调用一次 chain() 方法再返回原来实例即可。我们在之前用于给 prototype 复制方法的 each() 函数加入判断吧 ...

April 29, 2019 · 4 min · jiezi

underscore-诞生记一-基本结构搭建

1. 简介underscore 是一款成熟可靠的第三方开源库,正如 jQuery 统一了不同浏览器之间的 DOM 操作的差异,让我们可以简单地对 DOM 进行操作,underscore 则提供了一套完善的函数式编程的接口,让我们更方便地在 JavaScript 中实现函数式编程。 jQuery 在加载时,会把自身绑定到唯一的全局变量 $ 上,underscore 与其类似,会把自身绑定到唯一的全局变量 _ 上,这也是为啥它的名字叫 underscore 的原因。 在搭建 underscore 之前,让我们先来了解一下什么是 “立即执行函数(IIFE)”. 2. 立即执行函数(IIFE)立即执行函数,顾名思义,就是定义好的匿名函数立即执行,写法如下: (function(name) { console.log(name);})('suporka');其作用是:通过定义一个匿名函数,创建了一个新的函数作用域,相当于创建了一个“私有”的命名空间,该命名空间的变量和方法,不会破坏污染全局的命名空间。 // 函数外部拿不到内部的变量,因此不会造成变量污染,内部的变量在内部使用即可(function() { var name = 'suporka';})();console.log(name); // name is undefinded3. 全局变量 _ 的挂载当我们在浏览器中使用 _.map([1,2,3], function(item){console.log(item)}) 时, _ 是挂载在 Window对象上的,如果我们想在 node 环境中使用呢 ? (function() { // root 为挂载对象,为 self 或 global 或 this 或 {} var root = (typeof self == 'object' && self.self === self && self) || (typeof global == 'object' && global.global === global && global) || this || {}; // _ 应该是一个对象,对象内有属性函数 var _ = {}; root._ = _; _.VERSION = '1.9.1'; // 给我们的 underscore 一个版本号吧})();4. 函数式风格 && 面向对象风格的双重实现首先我们实现一个倒装字符串的方法 ...

April 26, 2019 · 5 min · jiezi

跟underscore一起学如何写函数库

原文:https://zhehuaxuan.github.io/… 作者:zhehuaxuan目的Underscore 是一个 JavaScript 工具库,它提供了一整套函数式编程的实用功能,但是没有扩展任何 JavaScript 内置对象。本文主要梳理underscore内部的函数组织与调用逻辑的方式和思想。通过这篇文章,我们可以:了解underscore在函数组织方面的巧妙构思;为自己书写函数库提供一定思路;我们开始!自己写个函数库前端的小伙伴一定不会对jQuery陌生,经常使用$.xxxx的形式进行调用,underscore使用_.xxxx,如果自己在ES5语法中写过自定义模块的话,就可以写出下面一段代码://IIFE函数(function(){ //获取全局对象 var root = this; //定义对象 var _ = {}; //定义和实现函数 .first = function(arr,n){ var n = n || 0; if(n==0) return arr[0]; return arr.slice(0,n); } //绑定在全局变量上面 root. = ;})();console.log(this);在Chrome浏览器中打开之后,打印出如下结果:我们看到在全局对象下有一个_属性,属性下面挂载了自定义函数。 我们不妨使用.first(xxxx)在全局环境下直接调用。console.log(.first([1,2,3,4]));console.log(.first([1,2,3,4],1));console.log(.first([1,2,3,4],3));输出结果如下:没问题,我们的函数库制作完成了,我们一般直接这么用,也不会有太大问题。underscore是怎么做的?underscore正是基于上述代码进行完善,那么underscore是如何接着往下做的呢?容我娓娓道来!对兼容性的考虑首先是对兼容性的考虑,工具库当然需要考虑各种运行环境。// Establish the root object, window (self) in the browser, global// on the server, or this in some virtual machines. We use self// instead of window for WebWorker support.var root = typeof self == ‘object’ && self.self === self && self || typeof global == ‘object’ && global.global === global && global || this || {};上面是underscore1.9.1在IIFE函数中的源码,对应于我们上面自己写的var root = this;。在源码中作者也作了解释: 创建root对象,并且给root赋值。怎么赋值呢?浏览器端:window也可以是window.self或者直接self服务端(node):globalWebWorker:self虚拟机:thisunderscore充分考虑了兼容性,使得root指向对局对象。支持两种不同风格的函数调用在underscore中我们可以使用以下两种方式调用:函数式的调用:console.log(.first([1,2,3,4]));对象式调用:console.log(([1,2,3,4])).first();在underscore中,它们返回的结果都是相同的。第一种方式我们现在就没有问题,难点就是第二种方式的实现。对象式调用的实现解决这个问题要达到两个目的:是一个函数,并且调用返回一个对象;这个对象依然能够调用挂载在_对象上声明的方法。我们来看看underscore对于_的实现:var _ = function(obj) { if (obj instanceof ) return obj; if (!(this instanceof )) return new (obj); this.wrapped = obj;};不怕,我们不妨调用([1,2,3,4]))看看他是怎么执行的!第一步:if (obj instanceof ) return obj;传入的对象及其原型链上有_类型的对象,则返回自身。我们这里的[1,2,3,4]显然不是,跳过。第二步:if (!(this instanceof )) return new (obj);,如果当前的this对象及其原型链上没有_类型的对象,那么执行new操作。调用([1,2,3,4]))时,this为window,那么(this instanceof )为false,所以我们执行new ([1,2,3,4])。第三步:执行new ([1,2,3,4]),继续调用_函数,这时obj为[1,2,3,4]this为一个新对象,并且这个对象的__proto__指向.prototype(对于new对象执行有疑问,请猛戳此处)此时(obj instanceof )为false(this instanceof )为true所以此处会执行this.wrapped = obj;,在新对象中,添加_wrapped属性,将[1,2,3,4]挂载进去。综合上述函数实现的效果就是: ([1,2,3,4]))<=====>new ([1,2,3,4])然后执行如下构造函数:var _ = function(obj){ this.wrapped = obj}最后得到的对象为:我们执行如下代码:console.log(([1,2,3,4]));console.log(.prototype);console.log(([1,2,3,4]).proto == .prototype);看一下打印的信息:这表明通过(obj)构建出来的对象确实具有两个特征:下面挂载了我们传入的对象/数组对象的_proto_属性指向_的prototype到此我们已经完成了第一个问题。接着解决第二个问题:这个对象依然能够调用挂载在_对象上声明的方法我们先来执行如下代码:([1,2,3,4]).first();此时JavaScript执行器会先去找([1,2,3,4])返回的对象上是否有first属性,如果没有就会顺着对象的原型链上去找first属性,直到找到并执行它。我们发现([1,2,3,4])返回的对象属性和原型链上都没有first!那我们自己先在.prototype上面加一个first属性上去试试:(function(){ //定义 var root = typeof self == ‘object’ && self.self === self && self || typeof global == ‘object’ && global.global === global && global || this || {}; var _ = function(obj) { if (obj instanceof ) return obj; if (!(this instanceof )) return new (obj); this.wrapped = obj; }; .first = function(arr,n){ var n = n || 0; if(n==0) return arr[0]; return arr.slice(0,n); } .prototype.first = function(arr,n){ var n = n || 0; if(n==0) return arr[0]; return arr.slice(0,n); } root. = ;})();我们在执行打印一下:console.log(([1,2,3,4]));效果如下:原型链上找到了first函数,我们可以调用first函数了。如下:console.log(([1,2,3,4]).first());可惜报错了:于是调试一下:我们发现arr是undefined,但是我们希望arr是[1,2,3,4]。我们马上改一下.prototype.first的实现(function(){ var root = typeof self == ‘object’ && self.self === self && self || typeof global == ‘object’ && global.global === global && global || this || {}; var _ = function(obj) { if (obj instanceof ) return obj; if (!(this instanceof )) return new (obj); this.wrapped = obj; }; .first = function(arr,n){ var n = n || 0; if(n==0) return arr[0]; return arr.slice(0,n); } .prototype.first = function(arr,n=0){ arr = this.wrapped; if(n==0) return arr[0]; return arr.slice(0,n); } root. = ;})();我们在执行一下代码:console.log(([1,2,3,4]).first());效果如下:我们的效果似乎已经达到了!现在我们执行下面的代码:console.log(([1,2,3,4]).first(2));调试一下:凉凉了。其实我们希望的是:将[1,2,3,4]和2以arguments的形式传入first函数我们再来改一下: //.prototype.first = function(arr,n=0){ // arr = this.wrapped; // if(n==0) return arr[0]; // return arr.slice(0,n); //} .prototype.first=function(){ /** * 搜集待传入的参数 */ var that = this.wrapped; var args = [that].concat(Array.from(arguments)); console.log(args); }我们再执行下面代码:([1,2,3,4]).first(2);看一下打印的效果:参数都已经拿到了。我们调用函数一下first函数,我们继续改代码:.prototype.first=function(){ /** * 搜集待传入的参数 / var that = this._wrapped; var args = [that].concat(Array.from(arguments)); /* * 调用在_属性上的first函数 */ return .first(…args);}这样一来.prototype上面的函数的实现都省掉了,相当于做一层代理;而且我们不用再维护两套代码,一旦修改实现,两边都要改。一举两得!执行一下最初我们的代码:console.log(.first([1,2,3,4]));console.log(.first([1,2,3,4],1));console.log(.first([1,2,3,4],3));现在好像我们所有的问题都解决了。但是似乎还是怪怪的。 我们每声明一个函数都得在原型链上也声明一个同名函数。形如下面:.a = function(args){ //a的实现}.prototype.a = function(){ //调用.a(args)}.b = function(args){ //b的实现}.prototype.b = function(){ //调用.b(args)}.c = function(args){ //c的实现}.prototype.c = function(){ //调用.c(args)}…1000个函数之后…会不会觉得太恐怖了!我们能不能改成如下这样呢?.a = function(args){ //a的实现}.b = function(args){ //b的实现}.c = function(args){ //c的实现}1000个函数之后….mixin = function(){ //将_属性中声明的函数都挂载在_prototype上面}.mixin();上面这么做好处大大的:我们可以专注于函数库的实现,不用机械式的复写prototype上的函数。underscore也正是这么做的!我们看看mixin函数在underscore中的源码实现:// Add your own custom functions to the Underscore object..mixin = function(obj) { .each(.functions(obj), function(name) { var func = [name] = obj[name]; .prototype[name] = function() { var args = [this.wrapped]; push.apply(args, arguments); return chainResult(this, func.apply(, args)); }; }); return ;}; // Add all of the Underscore functions to the wrapper object..mixin();有了上面的铺垫,这个代码一点都不难看懂,首先调用.each函数,形式如下: .each(arrs, function(item) { //遍历arrs数组中的每一个元素 }我们一想就明白,我们在_对象属性上实现了自定义函数,那么现在要把它们挂载到—.prototype属性上面,当然先要遍历它们了。我们可以猜到.functions(obj)肯定返回的是一个数组,而且这个数组肯定是存储_对象属性上面关于我们实现的各个函数的信息。我们看一下_.function(obj)的实现:.functions = .methods = function(obj) { var names = []; /** ** 遍历对象中的属性 **/ for (var key in obj) { //如果属性值是函数,那么存入names数组中 if (.isFunction(obj[key])) names.push(key); } return names.sort();};确实是这样的!我们把上述实现的代码整合起来:(function(){ /** * 保证兼容性 / var root = typeof self == ‘object’ && self.self === self && self || typeof global == ‘object’ && global.global === global && global || this || {}; /* * 在调用(obj)时,让其执行new (obj),并将obj挂载在_wrapped属性之下 / var _ = function(obj) { if (obj instanceof _) return obj; if (!(this instanceof _)) return new _(obj); this._wrapped = obj; }; //自己实现的first函数 _.first = function(arr,n){ var n = n || 0; if(n==0) return arr[0]; return arr.slice(0,n); } //判断是否是函数 .isFunction = function(obj) { return typeof obj == ‘function’ || false; }; //遍历生成数组存储_对象的函数值属性 .functions = .methods = function(obj) { var names = []; for (var key in obj) { if (.isFunction(obj[key])) names.push(key); } return names.sort(); }; //自己实现的遍历数组的函数 .each = function(arrs,callback){ for(let i=0;i<arrs.length;i++){ callback(arrs[i]); } } var ArrayProto = Array.prototype; var push = ArrayProto.push; //underscore实现的mixin函数 .mixin = function(obj) { console.log(.functions(obj)); //我们打印一下.functions()到底存储了什么? .each(.functions(obj), function(name) { var func = [name] = obj[name]; .prototype[name] = function() { var args = [this.wrapped]; push.apply(args, arguments); return func.apply(, args); }; }); return ; }; //执行minxin函数 .mixin(); root. = ;})();我们看一下.functions(obj)返回的打印信息:确实是_中自定义函数的属性值。我们再来分析一下each中callback遍历各个属性的实现逻辑。var func = [name] = obj[name]; .prototype[name] = function() { var args = [this.wrapped]; push.apply(args, arguments); return func.apply(, args);};第一句:func变量存储每个自定义函数第二句: .prototype[name]=function();在.prototype上面声明相同属性的函数第三句:args变量存储_wrapped下面挂载的值第四句:跟var args = [that].concat(Array.from(arguments));作用相似,将两边的参数结合起来第五句:执行func变量指向的函数,执行apply函数,将上下文对象_和待传入的参数args`传入即可。我们再执行以下代码:console.log(.first([1,2,3,4]));console.log(.first([1,2,3,4],1));console.log(.first([1,2,3,4],3));结果如下:Perfect!这个函数在我们的浏览器中使用已经没有问题。但是在Node中呢?又引出新的问题。再回归兼容性问题我们知道在Node中,我们是这样的://a.jslet a = 1;module.exports = a;//index.jslet b = require(’./a.js’);console.log(b) //打印1那么:let _ = require(’./underscore.js’)([1,2,3,4]).first(2);我们也希望上述的代码能够在Node中执行。所以root. = _是不够的。underscore是怎么做的呢?如下:if (typeof exports != ‘undefined’ && !exports.nodeType) { if (typeof module != ‘undefined’ && !module.nodeType && module.exports) { exports = module.exports = ; } exports. = ;} else { root. = ;}我们看到当全局属性exports不存在或者不是DOM节点时,说明它在浏览器中,所以: root. = _;如果exports存在,那么就是在Node环境下,我们再来进行判断:如果module存在,并且不是DOM节点,并且module.exports也存在的话,那么执行:exports = module.exports = ;在统一执行:exports. = _;附录下面是最后整合的阉割版underscore代码:(function(){ /* * 保证兼容性 / var root = typeof self == ‘object’ && self.self === self && self || typeof global == ‘object’ && global.global === global && global || this || {}; /* * 在调用(obj)时,让其执行new _(obj),并将obj挂载在_wrapped属性之下 */ var _ = function(obj) { if (obj instanceof _) return obj; if (!(this instanceof _)) return new _(obj); this._wrapped = obj; }; //自己实现的first函数 _.first = function(arr,n){ var n = n || 0; if(n==0) return arr[0]; return arr.slice(0,n); } //判断是否是函数 _.isFunction = function(obj) { return typeof obj == ‘function’ || false; }; //遍历生成数组存储_对象的函数值属性 _.functions = .methods = function(obj) { var names = []; for (var key in obj) { if (.isFunction(obj[key])) names.push(key); } return names.sort(); }; //自己实现的遍历数组的函数 _.each = function(arrs,callback){ for(let i=0;i<arrs.length;i++){ callback(arrs[i]); } } var ArrayProto = Array.prototype; var push = ArrayProto.push; //underscore实现的mixin函数 _.mixin = function(obj) { .each(.functions(obj), function(name) { var func = _[name] = obj[name]; _.prototype[name] = function() { var args = [this.wrapped]; push.apply(args, arguments); return func.apply(, args); }; }); return _; }; //执行minxin函数 .mixin(); if (typeof exports != ‘undefined’ && !exports.nodeType) { if (typeof module != ‘undefined’ && !module.nodeType && module.exports) { exports = module.exports = ; } exports. = ; } else { root. = _; }})();欢迎各位大佬拍砖!同时您的点赞是我写作的动力~谢谢。 ...

March 15, 2019 · 5 min · jiezi