重构保持函数的单一职责

来源:http://www.chinese-glasses.com 作者:Web前端 人气:162 发布时间:2020-04-22
摘要:时间: 2019-09-25阅读: 123标签: 重构2.单一职责表现形式 建立起总的架构 创建一个沙箱 保护私有变量,避免全局污染 (function(window){})(winow) 把 window 作为形参传进去的两个优点: 1、减少作

时间: 2019-09-25阅读: 123标签: 重构2.单一职责表现形式

建立起总的架构

  1. 创建一个沙箱 保护私有变量,避免全局污染 (function(window){})(winow)
    • window 作为形参传进去的两个优点:
    • 1、减少作用域的搜索
    • 2、提高压缩效率
  2. 建立一个构造函数,function Guoqi(){} // 作用:每一个 Guoqi 对象都是一个伪数组
    • 用替换原型对象的方法 Guoqi.prototype = {}; 注意:constructor 属性要指向构造函数本身
    • 为了方便我们书写,不用每次创建的实例都带有new 关键字,有一个小技巧
      • 在原型对象上 创建一个 init 方法 在构造函数中 直接 return new Guoqi.prototype.init();
      • 就可以成功的隐藏 new 关键字,
      • 但是 最关键的一步 ,一定要将 Guoqi.prototype.init.prototype = Guoqi.prototype
      • 这样的话,init 的实例 都可以用 Guoqi.prototype 原型上的方法啦
      • 最后把 window.Guoqi = window.Q = GuoqiGuoqiQ 作为接口 挂在window上,方便在外部直接调用
      • length 作为原型对象的一个对象,放在原型中,默认值为0,确保 Q 对象为一个伪数组
  3. init 方法的 充实, Q 对象中 可以传入多个参数需要进行判断
    • 传入的是字符串的话,那么将分情况
      • 如果以'<'开头的话,那么传入的是标签,调用下方的 parseHtml 方法,创建一个标签
      • 否则(传入的是DOM元素)的话 将作为选择器 用 qsa 的方法,获取到对象
    • 传入的是 DOM 对象的话 selector.nodeType 那么直接存放到对象中 this[0] = selector;this.length = 1; 保证其是一个伪数组
    • 传入的是 Q 对象的话,selector.constructor.name = Guoqi (因为 Q 对象都是用 Guoqi 构造函数创建出来的),直接返回 selector
    • 传入的函数 function 的话 ,暂时先不做操作 typeof selector === "function", 例如jQuery中的入口函数 $(function(){})
    • 传入的是 数组(伪数组)的话,selector.length >=0 , 把数组的值,依次传入到this中
    • 否则的话 不管传入的是什么,this[0] = selector; this.length = 1;
    • 这样就确保,不管 Q( ) 传入的什么,我们都得到了一个__伪数组__
  4. 添加扩展方法的功能 extend( obj ) 在extend中传入一个对象
    • Guoqi.extend = Guoqi.prototype.extend = function(){ } 利用联等符号,代表分别给构造函数,和构造函数原型添加extend方法
    • extend 使用的 混入继承的原理,把 obj 的功能拷贝到 this 对象上去,(注意:此时两个this的指向不同
    • Guoqi.extend({}) 给构造函数扩展的方法称为静态方法,作为工具性的方法使用(each,map,next...)
    • 给构造函数添加的 each 方法,要注意:this指向遍历的当前对象,而且一旦返回值为 false 的话,就停止遍历;即
      if( callback.call( array[i], i, array[i] ) === false ) break;
    • map 方法this指向的是window,只有当返回值不为 undefined 或者 null 的时候,才往返回的新数组中添加已遍历的元素,否则的话即:
      if( callback( array[i], i ) != undefined){ newArr.push( callback( array[i], i ) ) }; 最后__return newArr__;
    • Guoqi.prototype.extend({}) 给实例添加的方法称为实例方法,每一个实例对象都可以调用
    • 给实例添加的方法,语法:Q().each(function(){}); 所以要把 each 方法转给Q实例对象,例如 appendTo, each, map
      Guoqi.fn.extend({ each : function(callback){ return Guoqi.each(this,callback); }});
  5. 添加 核心方法 toArray,get
    • toArray 功能:将获取到的Q对象(伪数组) 返回为 一个 DOM 元素的真数组
    • get(index) 功能:如果没有传入 index 索引,那么将返回 一个DOM元素的真数组
      • 如果传入了一个 index ,那么返回 index 对应值的DOM元素
      • 支持传入负数,那么将从后面倒着往前数,比如:传入-1,那么对象的就是 length-1 的DOM元素

单一职责的定义可以理解为:一个对象或者方法,只做一件事。

DOM 操作模块

  1. 首先在 沙箱中定义一个 function parseHtml( str ){} 创建根据传入的标签,创建Q对象
    • 创建的时候,可以随意创建 div 标签,把我们想要创建的标签,添加到div中,div.innerHTML = str;
    • 然后把div的 childNodes 返回就可以,但是要__注意__,childNodes会随着我们渲染在页面上之后,length会发生改变
    • 所以,我们需要建立一个空数组,把 childNodes 里面的DOM元素,依次放到数组中,
    • 那么,parseHtml 函数,返回的就是一个__真的数组__,在init方法中'<'判断下进行调用,在转换成Q对象
  2. appendTo(selector) 实例方法 功能:将this对象追加到selector的子元素中
    • 该方法也要根据传入的对象, DOM元素,DOM对象,Q对象,伪数组... 做出判断 ★(所以前提是init方法的完善)
    • 完善之后,先把selector 转换为 一个Q对象 ---> Q(selector)
    • 建立一个数组objQ,和一个变量tempDom,(为了下面的返回值考虑)
    • 对this 和 selector 进行嵌套循环,并且要对this[i]进行克隆,注意克隆的数量,要保证本体为最后一个,不要克隆多了
    • 并且不断将 this 的值赋值给tempDom保存起来, tempDom = j === Q(selector).length-1 ? this[i]:this[i].cloneNode(true)
    • 再把 tempDom 添加到 数组objQ中去,
    • 考虑返回值的问题,我们的返回值应该是,添加到 selector中所有的this元素,包括克隆的,所以objQ中的元素,便是我们应该返回的值
    • 这样的话,我们的链式编程的链便遭到了破坏,所以我们就要创建一个__end()__方法,来返回上一个的链对象
    • 这里的话,我们就需要将当前this保存下来,以便于调用end()方法可以返回
    • 即:将objQ转换为Q对象,给其添加一个preObj属性指向this,然后把Q(obj)return出去
  3. end() 实例方法,当链遭到破坏的时候,调用一下可以找到上一级的链(只能找到一级)
    • 我们上面的工作已经做得非常充实,所以end()方法 只需要 return this.preObj || this ; (注意:如果没有上一级的话,把自己return出去)
  4. pushStack()实例方法, 就是appendTo方法后面保存上一级对象的内容,

    • 因为可能有很多方法会遭到链破坏,所以我们封装一个方法,实现复用的效果
    • 即:

      function pushStack(arr){ 
      var newObjQ = Q(arr); 
      newObjQ.preObj = this; 
      return newObjQ; }
      
    • 注意:该方法虽然实例不会直接调用,但是,因为在appendTo方法中 应该是this调用的,才能找到this的上一级链对象(虽然之后可以借用call/apply方法实现,但是这样的话就更麻烦了);

    • 就是说,不能直接当私有函数(例如parseHtml)放到沙箱中,这样的话上一级对象就是window,实现起来更麻烦
  5. prependTo append preprend remove ... 实例方法 实现 都是和appendTo一样的原理

    • prependTo 功能:this对象 添加到 selector对象子元素的最前面
      • 实现 selector[j].insertBefore( tempDom, selector[j].firstChild );
      • 注意:该方法和 appendTo方法一样,链会遭到破坏
    • append this对象的子元素 添加 selector的对象
      • 实现 Q(selector).appendTo(this); return this; 链不会遭到破坏
    • prepend this对象的子元素最前面 添加 selector的对象
      • 实现 Q(selector).prependTo(this); return this; 链不会遭到破坏
    • remove 方法 找到this中父节点 删除其中的所有this元素,包括this本身
      • 实现 this.each(function(){ this.parentNode.removeChild( this ); })
    • next 方法 找到this对象中紧邻的 下一个元素 (而不是所有的同辈(兄弟)元素) 链会遭到破坏

      • 实现方法 : 本来用this.nextElementSibling 就可以实现,但是有兼容问题
      • 所以我们给Guoqi构造函数扩展了一个next(dom)方法,循环遍历dom元素,寻找其下一个节点,若是nodeType === 1的话,直接return dom;否则的话return null;
      • function next(dom){
                while( dom = dom.nextSibling ){
                    if( dom.nodeType === 1 ){
                        return dom;
                    }
                }
                return null;
            }
        
      • 然后next实例方法,只需要 return pushStack( this.map(function(v){ return Guoqi.next(v) }) );

  6. filter(selector) 方法 为this实例对象和selector找到相同的元素
    • 准备好一个ret空数组,对this和Guoqi(selector)进行嵌套循环遍历,若this[i] == Guoqi(selector)[j],就添加到数组ret中去
    • 最后把 ret 这个数组 转换为 实例对象 返回回去 即:return Guoqi(ret);
  7. unique() 功能:去除相同的元素
    • 准备一个空数组 arr, 调用this.toArray()方法,把实例对象转换为真正的数组thisArr,然后进行循环遍历
    • if( arr.indexOf( thisArr(i) )== -1 ){ arr.push(thisArr[i]) };
    • 然后 return Guoqi( arr );
  8. children(selector): 功能:若没有传入参数 就查找到所有的子元素(仅限于儿子辈的),若传入了参数,就找到与selector相匹配的子元素

    • 原理:找到this实例对象中的所有的子元素,组成一个数组,找到selector中的所有元素 组成一个数组,然后寻找两个数组之间相同的元素即可(调用filter方法),注意:涉及到了unique去重的问题
    • 首先

      var subList = this.map(function(v){ 
        return Guoqi.map(v.children,function(v1){ 
        return v1 }) }); 
      // 此时 sunList是一个二维数组,即:数组里面套数组
      
    • 需要把subList 合成一个 一维数组,借用concat方法:var newSub = [].concat.apply([],subList);

    • 然后对 newSub进行去重(需要将数组转换为Q对象才能调用unique方法), var subQ = Guoqi(newSub).unique();
    • 需要对selector进行判断,如果 selector存在的话,那么 return subQ.filter( Guoqi(selector) ); 如果不存在的话,那么直接 return subQ;
  9. find(selector) : 功能:find一般是找指定的元素,在this实例对象的所有后代元素中找,所以一般都传入参数

    • find方法和children方法一样,只需要把

      var subList = this.map(function(v){
        return Guoqi.map(__v.querySelectorAll(selector)__,function(v1){
        return v1 }) });
      
    • v.querySelectorAll(selector); 来找到所有的后代元素(应该还有更好的办法,欢迎来补充)

  10. nextAll 工具方法 跟next方法如出一辙:

    • function next(dom){
             while( dom = dom.nextSibling ){
                   if( dom.nodeType === 1 ){
                              return dom;
                    }
             }
             return null;
      }
      
    • next,prev,nextAll,prevAll 方法 都常会在 封装实例方法中用到,所以可以封装为工具方法; prevAll,prev 两个方法和next,nextAll 原理一样,所以不再详细赘述

    • 注意:nextAll,prevAll,children 这些链都遭到了破坏,不过可以使用end恢复
  11. nextAll 实例方法 既然有了上面的nextAll的工具方法,那么相对应的实例方法就简单了很多,

    • 只需要把this实例对象的每一个dom元素调用 nextAll(dom),组成一个数组(用map方法简单),
    • 然后把这些数组用concat组合在一起进行__去重(unique)__,进而转换为Guoqi对象即可
  12. siblings 实例方法

    • 有了prevAll 和 nextAll方法,siblings就变得简单多了,只要两者组合在一起即可

      var nextSiblings = this.nextAll().toArray();
      var prevSiblings = this.prevAll().toArray();
      
    • 返回 Guoqi( prevSiblings.concat( nextSiblings ) );

遵守单一职责的实例太多了,下面简单列举一下。

事件操作 模块

  1. on 事件的实现 (我们选择先实现on事件,是因为on事件是通用的,实现on事件之后,其他的具体事件都可以用on事件来实现)
    • on 事件 语法:Guoqi.fn.extend(on: function( 事件列表,callback ){ });
    • 事件列表 可以有实现多个事件 中间用空格隔开
    • 这就意味着 我们要对this实例对象进行遍历,也要对事件列表进行遍历
    • 添加事件的时候,我们选择使用 addEventListener("事件名",callback);
  2. 各个事件的实现

    • 首先我们可以从控制台打印出来所有的事件
    • 创建一个DOM对象div,for( var k in div ){ if( k.slice(0,1) === "on" ){ arr.push(k) } }; 这样就把所有事件放到arr数组中去了
    • 循环遍历数组中每个值,只保留事件名的部分,即:v = v.slice(2);
    • 然后添加在原型上:Guoqi.fn[v] = function(callback){ return this.on(v,callback) };

      css 样式操作模块的实现

  3. 首先我们需要考虑css的参数情况 语法:Guoqi.fn.css = function( name,value){ }

    • 只有一个参数 即:value == undefined 可能是对象,也可能是一个字符串

      • 字符串( typeof name === "string" ),即:需要获取实例对象的值:一般情况下,我们获取的是实例对象中第一个元素的值
        (this[0] / this.get(0)) 可以直接 return this[0].style[name] ,但是值得注意的是,这样只能获取行内样式的值
      • 所以 我们还需要 || window.getComputedStyle(this[0])[name]; 但是getComputedStyle在低版本的IE浏览器中(currentStyle)不支持,
        如果考虑严谨的话,可以封装一个getStyle获取样式的函数

         function getStyle(dom,style){
          return dom.currentStyle?dom.currentStyle[style]:
          window.getComputedStyle(dom)[style]
        }
        
      • 对象 不仅需要循环遍历this实例对象,还需要遍历name这个键值对的值:做到for( var k in name ){ this[i].style[k] = name[k] };
        注意要返回 this实例对象,便于链式编程

    • 有两个参数 name value 设置单个样式

      • 这样的话,只需要循环遍历this对象,即:this[i].style[name] = value; 并且要把this实例对象返回回去,实现链式编程

原生的API方面

属性 样式操作模块的实现 (与css实现原理相似)

  1. hasClass(className) 判断实例对象 有没有该类名,
    • 实现:我们需要分别对this实例对象进行遍历,和他们的所有的类名进行遍历,
    • 为了方便操作,需要把实例对象的类名转换为数组,并去除前后空格(遍历检查),然后使用__indexOf()__方法,若为-1,则返回false, >=0或者!=-1则为true;
    • 即:var classNameValues = this(指的是实例对象中的dom).className.trim().split(" "); classNameValus.indexOf(className) >=0 --->true
    • 可以使用some方法,(只要有一个满足条件直接放回true) 即:return this.toAArray().some(function(v){ return v.trim().split(" ")indexOf(className) >= 0 };
  2. addClass(className) 添加类名
    • 又需要分清况讨论了,若是没有类名:即classNameValues.length == 0,则直接添加 this.className = className;
    • 若是有class,但是没有该类名,则需要追加;即:if( classNameValues.indexOf(className) == -1 ){this.className += " "+classNaame};__(注意要用空格分隔开)__;
    • 若是已经有该类名了,则什么都不需要做(不能重复添加)
    • addClass可是直接用数组来实现简单一些:只要if( classNameValues.indexOf(className) == -1 ){ classNameValues.push(className) };this.className = classNameValues.join(" ");
  3. removeClass(className) 删除类名
    • 就是把指定的类名给删除掉,需要进行循环遍历所有类名数组 classNameValues,然后用splice来把指定的类名从数组中给截取掉;
    • 即var index;(来记录查到指定类名的索引)
      js while( (index= classNameValues.indexof(className))>=0 ){ classNameValues.splice(index,1);} this.className = classNameValues.join(" ");
    • 还可以使用 map方法; 即:this.className = classNameValues.map(function(v){if(v != className){return v;}}).join(" "); 利用map方法产生一个新数组,简单一些
  4. toggleClass(className) 切换类名

    • 直接进行判断

      if(this.hasClass(className)){ 
      this.removeClass(className)
        }else{ 
      this.addClass(className) 
        };
      
    • 对上面的代码实现复用,减少代码冗余; 如果有该类名的话,直接删除,没有类名的话,就添加

  5. attr(name,value) 对属性样式的设置 和css 原理一样:还是要分情况:

    • 一个参数时,类型为字符串,获取属性值:用getAttribute(name);
    • 一个参数时,类型为对象,数值多个属性值 ,循环遍历该对象用setAttribute(k,name[k])来进行设置
    • 两个参数时,设置单个属性值,直接进行设置即可:setAttribute(name,value);
  6. prop(name,value) 与上述情况一样,分类进行考虑,
    • 但是注意的是。prop针对的是input标签这些单属性的值,值为布尔类型,例如disabled,checked,selected
    • 设置和获取的时候不用setAttribute,getAttribute,直接进行赋值即可,this[name] = value;
    • 注:如果对其进行赋值,例如disabled,不论赋值为true还是false,都不可编辑,除非移除该属性removeProp
  7. removeAttr与removeProp,个人认为实现原理一样,都是使用removeAttribute
    • 即:遍历this实例对象,然后进行 this.removeAttribute(name);

trimRight()和trimLeft():trimRight 只负责去除右边的空白,其它地方一概不管。 trimLeft 只负责去除右边的空白,其它地方也一概不关。

入口函数,即init方法中selector传入的数函数的情况

  1. 方法一:直接使用window.addEventListener("load",selector); 可以实现累加,开辟不同的入口函数,互不影响
  2. 方法二:利用函数的技巧:记录下来window.onload的值,进行判断如果是一个函数的话,则他执行,在执行selector传入的,否则的话,直接把selector赋值给window.onload

    • 即:

      var oldFn = windiw.onload ; 
      if(typeof oldFn == "function"){
        window.onlad = function(){ 
      oldFn();selector(); 
        }}else{ 
      window.onload = selector(); 
        }
      
  3. 方法三:利用数组的技巧:建立一个私有数组,var loads = []; 直接把一个个的selector给push到数组中去,

    • 然后定义一个

      window.onload = function(){  
        Guoqi.each(loads,function(){ this(); }) 
      }   // 把数组中的selector依次执行
      

concat(): concat 只负责连接两个或更多的数组,并返回结果。不会涉及删除数组的操作。

toFixed(): toFixed 只把 Number 类型的值四舍五入为指定小数位数的数字。不会执行其它操作。

JQuery 的 API

$.each() 只负责遍历,要处理什么,自己再动手操作。

css() 只负责设置 DOM 的 style ,不会设置 innerHTML 。

animate() 只负责执行 CSS 属性集的自定义动画,不会涉及其它操作。

说是这样说,但是大家看着可能会有点懵,看不出来遵守单一原则有什么好处,下面看一个实例。

3.实例-数组处理

如下例子:

现有一批的录入学生信息,但是数据有重复,需要把数据根据 id 进行去重。然后把为空的信息,改成'--'。

let students=[ { id:5, name:'守候', sex:'男', age:'', }, { id:2, name:'浪迹天涯', sex:'男', age:'' }, { id:5, name:'守候', sex:'', age:'' }, { id:3, name:'鸿雁', sex:'', age:'20' }];function handle(arr) { //数组去重 let _arr=[],_arrIds=[]; for(let i=0;iarr.length;i++){ if(_arrIds.indexOf(arr[i].id)===-1){ _arrIds.push(arr[i].id); _arr.push(arr[i]); } } //遍历替换 _arr.map(item={ for(let key in item){ if(item[key]===''){ item[key]='--'; } } }); return _arr;}console.log(handle(students))

运行结果没有问题,但是大家想一下,

1.如果改了需求,比如,学生信息不会再有重复的记录,要求把去重的函数去掉,无论,就是整个函数都要改了,还影响到下面的操作。

2.如果项目另一个地方也是同样的操作,但是不需要去重。这样只能再写一个基本一样的函数,因为上面的函数无法复用。如下

function handle1(arr) { //数组深拷贝 let _arr=JSON.parse(JSON.stringify(arr)); //遍历替换 _arr.map(item={ for(let key in item){ if(item[key]===''){ item[key]='--'; } } }); return _arr;}

本文由10bet发布于Web前端,转载请注明出处:重构保持函数的单一职责

关键词:

最火资讯