一个简单的函数消灭业务代码的低级错误

日常的前端开发业务代码中,我们经常都需要调试数据,所以要经常更改某些参数的初始化数据,或者更改过程中的数据。

但是很多时候,改了数据调试完之后就忘记改回去了,某个调试的场景是依赖一个参数的修改还好,但是需要依赖几个参数的修改,就很容易漏改回去了。

举两个例子:

一、我们有一个按钮来触发弹窗的打开,而控制弹窗显隐为变量showDialog,初始化值为false,但是产品说弹窗里面的样式有点问题,所以我们设置了showDialog的值为true然后修改内容(因为不可能每次都点击按钮来打开看弹窗效果),最后修改完再将showDialog设置回false。

//伪代码
var showDialog = false;  //控制窗口显隐,调试需要依赖其变量值
btn.onclick = ()=>{
  showDialog = true; 
}

二、我们有个抽奖的活动,逻辑是请求接口之后拿到抽奖的prize_id之后,对比prize_id的内容,然后决定在视图中显示出来,但是我们需要调试某个抽奖结果的内容,当然不会叫接口改返回的prize_id了,所以我们可能会改传入显示模块的值。

//伪代码
fetch().then(prize_id => {
  showResult(prize_id)  //显示抽奖结果,调试需要依赖其传入值
})

function showResult(prize_id){
  //显示抽奖结果的代码
}

问题就在于,很多时候我们最后忘了改回去,就会出现弹窗直接打开了、每次抽奖都抽中某个奖品的结果了,这种低级错误是不应该犯的,但是我也见过某些app真的直接这样上了测试的代码。

所以在这些业务代码中,我相信也没什么人会做构建前的校验脚本或者单元测试的,所以我们需要一个简单的函数来控制变量的赋值,来避免这种低级错误。

特意写了一个简单的包:https://github.com/ershing/dev-debugger

用法:

//引入包dev-debugger
import DevDebugger from 'dev-debugger'
//初始化dgb实例来控制变量的测试值
let dbg = new DevDebugger({ debug: true })
//绑定获取替换的方法,也可以直接调用dbg.debugVal
let _r = dbg.debugVal.bind(dbg)

实例有两个方法:debugVal和debugCaseTag

/*
  debugVal(pro, dev)
  @params 传入第一参数为生产值,第二参数为调试值
*/
//也可以绑定方便后面调用
let _r = dbg.debugVal.bind(dbg)


/*
  debugCaseTag(pro, tag)
  @params 传入第一参数为生产值,第二参数为自命名的唯一标签名称
*/
//前提需要配合初始化的传参
let dbg = new DevDebugger({ 
  debug: true,
  caseName: 'testPrize1',  //调试的用例
  cases: {  //用例参数集
    'testPrize1': {
      'myPrize': 3   //标签名称对应的调试值
    },
    'testPrize2': {
       'myPrize': 6   //标签名称对应的调试值
    }
  }
})

//也可以绑定方便后面调用 
let _rt = dbg.debugCaseTag.bind(dbg)

所以上面的例子可以这样写:

一、控制showDialog的变量值

//伪代码
var showDialog = _r(false, true);  //debug时值为true
btn.onclick = ()=>{
  showDialog = true; 
}

二、控制传入显隐函数的值

//伪代码
fetch().then(prize_id => {
  showResult(_r(prize_id, 3))  //debug时为3
})

function showResult(prize_id){
  //显示抽奖结果的代码
}

当然上面也可以用debugCaseTag方法来将调试的值放在初始化的函数当中。

然而,在我们构建代码的时候,当然不想有任何调试的代码和调试的值的,所以我又写了一个babel插件:https://github.com/ershing/babel-plugin-dev-debugger

用法:

//修改babel.config.js文件
module.exports = {
  "plugins": 
  process.env.NODE_ENV === "production" 
  ? ["babel-plugin-dev-debugger"] 
  : []
}

注意:

使用这个babel插件的话,需要在各自文件中import包dev-debugger(也利于单文件组件的独立调试),而且不要将实例方法赋值出去,可以直接dbg.debugVal或dbg.debugCaseTag使用,也可以bind之后_t或_rt使用,但不要再赋值给其他变量。

写了一个个人资产管理的后台系统

关于个人记账的,本来想写一个app来着,后来觉得记账这种事应该是自己定时回归的,不应该是app那种经常可以打开的方式,过于便捷反而过于随便,所以需要一种有仪式感的事情,就好像以前会打开记账本,一条一条记账那种感觉。

为了记账,最后统一为一个个人资产管理的后台,也挺直观,可以看到各项资产组成和趋势,最后就快速地写了一个项目,没有考虑代码的太多东西,只考虑功能的快速实现,而且只能记当前年的账,还特意参考了spendee这个app的可以计划记账的功能,特别喜欢。

项目地址:https://github.com/ershing/asset-admin

附上部分图:

写一个throttle函数

上一篇写了(写一个debounce函数),这篇就写一下throttle函数。

throttle是怎样的一个函数呢?和debounce函数又有什么区别呢?

throttle就是节流阀,控制在一定时间范围内只执行一次函数,其实根据节流阀的名字也可以很容易理解到,水不断来的时候,通过你节流阀,就可以控制你自己的实际流量,单位时间内怎样流出。

举个实际例子,例如我们需要监听窗口的大小变化来改变另一个东西的样式,那么我们肯定监听resize来触发我们的函数,但是实际上,resize触发函数会在一秒内触发很多次,难道一秒内我们就要执行函数这么多次了吗?显示,这不是我们想要的,可能我们想要的只是300ms执行一次就够了。

好了,这就不难理解这个函数做了什么工作了。

废话少说,直接开写,首先肯定需要传入执行的函数,还有我们知道需要控制某个时间内只执行一次,那会有一个interval参数,那先写个开头:

function throttle(fn, interval) {
  let _fn = fn  //保存函数的引用
  return function(){
    let _self = this
    let args = arguments
    setTimeout(function(){
      _fn.apply(_self, args)
    }, interval)
  }
}

这个开头和debounce开头一样,关键就是后面的逻辑了,也是和debounce的区别,我们想要这个函数在某个时间段只执行一次,如果在这个时间段内继续触发函数,则不执行,那么就可以增加代码为:

function throttle(fn, interval) {
  let _fn = fn  //保存函数的引用
  let timer
  return function(){
    //函数还没执行,则直接返回
    if(timer){
      return
    }
    let _self = this
    let args = arguments
    timer = setTimeout(function(){
      _fn.apply(_self, args)
      clearTimeout(timer)
      timer = null  //注意执行完要清空timer
    }, interval)
  }
}

好了,我们发觉,那如果第一次函数被触发呢?不应该直接返回吧,第一次那肯定立即要执行,不用延时吧,ok,再加个标识符:

function throttle(fn, interval) {
  let _fn = fn  //保存函数的引用
  let timer,
      firstTime = true
  return function(){
    let _self = this
    let args = arguments
    //判断第一次进来就立即执行
    if(firstTime){
      _fn.apply(_self, args)
      firstTime = false
      return
    }
    //函数还没执行,则直接返回
    if(timer){
      return
    }
    timer = setTimeout(function(){
      _fn.apply(_self, args)
      clearTimeout(timer)
      timer = null  //注意执行完要清空timer
    }, interval || 500)
  }
}

其实具体的debounce和throttle函数实现起来有很多种写法,但是都大同小异,因为基本原理都是一样的,做了各自不同扩充而已。其实理解debounce和throttle的区别很容易,debounce就相当于以触发动作为参考,延时了设定的时间就执行一次函数,如果动作在设定的时间内仍然继续触发,那么继续以新触发的动作为参考;而throttle就是第一次触发就立即执行了,然后以执行的时间为参考,延时了设定的时间之后再触发就再执行,但是如果下一次触发仍在前一次执行的设定延时时间内,则不执行了,大概,它们就是如此。

写一个debounce函数

这里尝试写一个debounce函数的过程。

debounce是什么?就是防抖动,假设我们监听了输入框的change事件,绑定了一个函数来验证,那么我们在输入的过程中就会不断触发这个函数,很多时候我们其实不想这个函数触发得这么频繁,因为用户都是不断输入的,没必要不断验证,我们真实希望的是,用户停止的时候才触发这个函数,所以我们希望给一个delay的时间,在这个时间内,不断触发的函数,也只按最后一次来触发。

就好像上图那样,只要你触发后面,如果不超过delay时间再次触发的话,也会继续等,直至最后一次触发超过delay的时间,函数才执行一次,很好理解。

当然了,如果希望的是立即执行函数的话,情况就会是下面这种情况:

只要你触发的时间比上次函数执行时间超过了delay的时间,就会被立即执行。

这里写的debounce函数就不考虑立即执行这种情况,按前面的情况来写一下。

首先,这个函数名称就叫做debounce,这个函数接受两个参数,一个是防抖执行的函数,一个是延时delay的时间,所以我们会用到setTimeout这个方法,而且我们执行了debounce之后需要返回一个被触发的函数,考虑到我们需要,会先有:

function debounce(fn, delay) {
  let _fn = fn  //保存函数的引用
  return function(){
    let _self = this
    let args = arguments
    setTimeout(function(){
      _fn.apply(_self, args)
    }, delay)
  }
}

这样执行下来,也只不过每个函数都执行一次延时而已,还没达到我们想要的效果,所以我们还需要做的是,这个函数在delay时间内,也就是还没执行前,再次触发的话,就将上一次设定的延时器清除,那这就容易增加代码为:

function debounce(fn, delay) {
  let _fn = fn  //保存函数的引用  
  let timer
  return function(){
    clearTimeout(timer)
    let _self = this
    let args = arguments
    timer = setTimeout(function(){
      _fn.apply(_self, args)
    }, delay)
  }
}

这就基本实现了debounce函数了,其实就是利用了一个闭包的形式,将原本的timer保存起来,只要函数还没执行(也就是delay时间内)再次触发的话,就会先清除了前一个的timer了,基本实现了防抖的效果。

js创建对象的7种模式

其实我们在新建一个对象的时候,通常会用什么方法呢?我觉得,大多数的人都会这样新建,简单直接:

var myObject = {
  name:'test',
  content:'hello world',
  do:function(){console.log('nothing')}
}

在这里,以上面例子来详细说说js创建对象的7种模式:

第一种:工厂模式

function createObject(name, content){
  return {
    name,
    content,
    do:function(){console.log('nothing')}
  }
}

var myObject = createObject('test', 'hello world')

这种模式非常直观,丢进去,丢出来。

第二种:构造函数模式

function createObject(name, content){
  this.name = name
  this.content = content
  this.do = function(){console.log('nothing')}
}

var myObject = new createObject('test', 'hello world')

这种模式也很常用,定义了一个构造函数,然后对象都是new出来的。

第三种:原型模式

function createObject(){
}
createObject.prototype.name = 'test'
createObject.prototype.content = 'hello world'
createObject.do = function(){console.log('nothing')}
var myObject = new createObject()

这种模式相对构造函数模式,是在它的原型上直接创建属性,后面new出来的对象就会继承下来这些属性和方法。

第四种:构造函数模式和原型模型组合

function createObject(name, content){
  this.name = name
  this.content = content
}
createObject.prototype = {
  constructor: createObject,
  do:function(){console.log('nothing')}
}
var myObject = new createObject('test', 'hello world')

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性,结果,每个实例都会有一个自己的一份实例属性和方法,但同时共享着对方法的引用,最大限度地节省了内存,是目前使用最广泛的一种创建自定义类型的方法。

第五种:动态原型模式

function createObject(name, content){
  this.name = name
  this.content = content
  if(typeof this.do != "function"){
    createObject.prototype.do = function(){console.log('nothing')}
  }
}

var myObject = new createObject('test', 'hello world')

这里就是判断是否存在do这个方法前提下动态在原型上添加,而且只在调用函数的时候才会执行,可以说是非常好的一种模式。

第六种:寄生构造函数模式

function SpectialArray(){
  var values = new Array()
  values.push().apply(values, arguments)
  values.toPipedString = function(){
    return this.join('|')
  }
  return values
}

var colors = new SpecialArray('red', 'blue', 'green')
alert(colors.toPipedString())  //"red|blue|green"

其实这种模式也很容易理解,我们不能直接修改Array构造函数,就借用来寄生一下,添加我们需要的属性。

第七种:稳妥构造函数模式

function createObject(name){
  var o = new Object()
  o.do = function(){console.log(name)}
  return o
}
var myObject = createObject('name')

看起来有点熟对吧,其实不完全熟,它其实是通过myObject的方法才能访问到数据成员的这样一种方式,没有其他方式能够访问,myObject就是保存着一个稳妥的对象。

好了,关于js创建对象的7种模式,大抵如上。

混杂模式与标准模式的一些区别

关于文档模式,已在上一篇文章(文档模式)中说过,这里就说说混杂模式和标准模式的一些区别。

众所周知的一个区别,就是CSS盒模型了,我们都知道标准模式中,CSS盒模型是w3c标准盒模型,也就是它的height和width就是里面内容(content)的高和宽。

那混杂模式呢?它会按照IE5.5的CSS盒模型,就是它的height和width是包含padding和border的宽度的,这个就是非常明显的区别,当然了,IE8及之前只支持IE盒模型,到IE8+之后,就可以通过设置box-sizing的值来切换,默认值是content-box,就是w3c标准盒模型,而设置为border-box,就是回到IE盒模型了。

基本来说,就如图:

一个div元素里面只包含图片的时候,混杂模式和标准模式也是不一样的,在标准模式下,不管IE还是标准浏览器,在图片底部都有3像素的空白。但在混杂模式下,标准浏览器(Chrome)中div距图片底部默认没有空白。

如果是在IE的混杂模式下,给行内元素是可以设置高度和宽度的,标准模式下肯定是无效的。

在混杂模式下,IE和其他浏览器对百分比宽度的解析是不一样的。如果父级是行内块或者浮动或者有定位的元素,给子元素设置百分比宽度100%时,IE的混杂会以父级的100%算(父级没有设置宽度,再往上一层),而标准浏览器是取决于内容的宽度,所以关键还是看父级是否行内块或者浮动或者有定位的元素。

在混杂模式下,当我们给一个元素设百分比高度,其他浏览器(正常,inline高度无变化,inline-block和block都会按百分比),而IE是自适应到内容高度。有点跟上面设置宽度相反的感觉。

overflow溢出默认值的问题。标准模式下,溢出元素是溢出可见的,超出部分的内容呈现在它的包含元素外。在混杂模式下,IE浏览器的溢出元素会自适应内容的尺寸。其实这个联想上面也很容易理解,标准模式是定好了的,而混杂模式是靠内容来撑开的感觉。

这里主要参考了这篇总结得不错的文章:标准模式与混杂模式

 

文档模式

IE5.5引入了文档模式的概念,最初的文档模式包含两种:混杂模式和标准模式。而这个模式就是通过文档类型(doctype)的切换来实现的。

为什么会出来这个东西呢?其实浏览器发展以前,都是自己弄自己的这样一个境况,所以很多时候不同浏览器解析出来的东西大不相同,后来有了w3c标准,将相应的规范统一了起来,大家就有了一定的标准了,所以其实标准模式就是标准后的模式,而混杂模式就是保持IE5以前的行为。后来IE又提出了一种叫做准标准模式的东西,其实就是大部分按照标准,但不完全按照,这里具体就不展开这个准标准模式了。

如果文档开始的时候没有引入文档类型声明,所有的浏览器默认都会开启混杂模式的,混杂模式在不同的浏览器肯定行为差异巨大的,很多时候要用一些hack的技术,所以并不推荐这种方式,一般都开启标准模式。

那标准模式如何开启呢?就是在文档的最前面加上<!DOCTYPE html>这么一句就行了,什么意思呢?DOCTYPE,或者称为 Document Type Declaration(文档类型声明,缩写 DTD),这样,在浏览器解析 HTML 文档正文之前就可以确定当前文档的类型,以决定其需要采用的渲染模式。

我们怎么用js判断是开启了哪个模式呢?可以通过document.compatMode这个属性,如果是CSS1Compat就是标准模式,如果是BackCompat就是混杂模式。

至于混杂模式和标准模式会有什么巨大差异呢?下一篇再说说。

 

Content-Type

说Content-Type之前,先说一下MIME,什么是MIME呢?

百科一下,MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型,是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开,它是一个互联网标准,这个标 准被定义在RFC 2045、RFC 2046、RFC 2047、RFC 2048、RFC 2049等RFC中。 MIME改 善了由RFC 822转变而来的RFC 2822,这些旧标准规定电子邮件标准并不允许在邮件消 息中使用7位ASCII字符集以外的字符。正因如此,一些非英语字符消息和二进制文件 ,图像,声音等非文字消息原本都不能在电子邮件中传输(MIME可以)。MIME规定了用 于表示各种各样的数据类型的符号化方法。 此外,在万维网中使用的HTTP协议中也使 用了MIME的框架,标准被扩展为互联网媒体类型。

前面的文字啰嗦了一点,简单来说,MIME就是表示的一种类型,内容由两部分组成, 前面是数据的大类别,后面定义具体的种类。例如,image/jpeg就是表示jpeg图像类型。

而应用在HTTP中,MIME类型被定义在请求头和响应头的headers中,用Content-Type字段表示,所以我们可以知道HTTP中,Content-Type表示的是一种MIME类型。

响应(response)的头信息,Content-Type表示的是返回的文件类型,这个容易理解,那请求(request)头信息上面的Content-Type呢?代表的是什么呢?

一般,我们在POST/PUT方法的时候,会指定请求头的Content-Type,会有那么的几种类型:

application/x-www-form-urlencoded:这种是最常用的类型,其实就是key=value形式的字符串拼接,例如:key1=value1&key2=value2&key3=value3

multipart/form-data:这种在自己new Formdata()的时候,或者上传文件的时候会用到,会有特别分隔字符串,然后跟着name是什么,对应的value是什么,里面是文件的,还有另一个Content-Type来表明类型,例如:

// request body
–Boundary———————–
content-disposition: form-data; name=”field1″

value1
–Boundary———————–
content-disposition: form-data; name=”field2″

value2
–Boundary———————–
Content-Disposition: form-data; name=”file”; filename=”chrome.png”
Content-Type: image/png
Content-Transfer-Encoding: binary

$binarydata
–Boundary———————–
Content-Disposition: form-data; name=”file1″; filename=”a.txt”
Content-Type: text/plain

application/json:这种类型是现在很常用的一种,用于传递json结构的数据,很适合前后端分离的数据传递。

text/xml:XML-RPC(XML Remote Procedure Call),是一种使用 HTTP 作为传输协议,XML 作为编码方式的远程调用规范。xml相比json太过臃肿,一般用application/json会更加灵活。