跟着月影学 JavaScript | 青训营笔记

104 阅读7分钟

跟着月影学 JavaScript | 青训营笔记

这是我参与「第四届青训营 」笔记创作活动的的第3天

各司其职

让HTML、CSS和JS职能分离 以一个简单的切换浅色模式与深色模式的demo作为例子

第一个版本

 const btn = document.getElementById('modeBtn');
  btn.addEventListener('click', (e) => {
    const body = document.body;
    if(e.target.innerHTML === '🌞') {
      body.style.backgroundColor = 'black';
      body.style.color = 'white';
      e.target.innerHTML = '🌜';
    } else {
      body.style.backgroundColor = 'white';
      body.style.color = 'black';
      e.target.innerHTML = '🌞';
    }
  });

当按下id为modeBtn的按钮时切换背景色、字体颜色以及图标的内容,思考一下该如何改进

第二个版本

const btn = document.getElementById('modeBtn');
  btn.addEventListener('click', (e) => {
    const body = document.body;
    if(body.className !== 'night') {
      body.className = 'night';
    } else {
      body.className = '';
    }
  });

这个版本我们添加了一个css类名为night的样式,并将浅色样式设为默认样式,当点击按钮后覆盖默认样式,修改为深色模式,这样就简单地将css和js进行了有效的解耦

第三个版本

  <input id="modeCheckBox" type="checkbox">
  <div class="content">
    <header>
      <label id="modeBtn" for="modeCheckBox"></label>
      <h1>深夜食堂</h1>
    </header>
    <main>
      <div class="pic">
        <img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
      </div>
      <div class="description">
        <p>
            这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈
            眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返 [6]  。
        </p>
      </div>
    </main>
  </div>
  #modeCheckBox {
    display: none;
  }
    
 #modeBtn:after{
    content:'🌞';
 }
  #modeCheckBox:checked + .content {
    background-color: black;
    color: white;
    transition: all 1s;
  }
​
 #modeCheckBox:checked + .ccontent #modeBtn:after{
    content:'🌜';
 }

这个版本我们直接省略了就是代码,通过一个checkbox类型的input元素控制原来的按钮上操作,这一版本直接肉眼可见的更加职责独立

让HTML、CSS和JS职能分离地有点显而易见,这令我们的代码更加的简洁,可读性更强,对于样式的操作我们应尽量避免不必要的js代码,尽管有js对css地操作难以避免,我们也可以考虑用改变class属性来替代直接改变样式,这样还可以提高渲染效率。

组件封装

好的ui组件具备正确性、扩展性、复用性。

像我们常用的ui组件库,例如ElemeentUI、bootstrap等常用的ui组件库都是组件封装的最好例子,将我们日常开发中常用的单元抽离出来经过

结构设计、展现效果、行为设计封装成组件,就能的提高我们的代码简洁度,可读性以及复用性,并且通过重构,插件化、模板化、抽象化(形成组件框架) 去不断优化我们的组件,令它更加的灵活,适应更多的场景

过程抽象

应用函数式编程思想,用于处理局部细节控制的一些方法

为了让需求覆盖不同的场景,因此将需求剥离出来,这个过程称为过程抽象,去构成一些高阶函数(HOF)

  • 高阶函数(HOF):类似拦截器,常用的高阶函数有Once、Throttle、Debounced、Consumer/2、Iteraticve

编程范式

  • 命令式

    • 更注重怎么做
  • 声明式

    • 更注重做什么

来看一些例子

一段真实的代码

  get layerTransformInvert() {
    if(this[_layerTransformInvert]) return this[_layerTransformInvert];
    const m = this.transformMatrix;
    if(m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0) {
      return null;
    }
    this[_layerTransformInvert] = mat2d.invert(m);
    return this[_layerTransformInvert];
  }

这段代码用于检验是否为一个2D的单位矩阵 这一串代码看起来很笨但是如果场景是在动画中每一阵刷新中都会调用一次,为了提高性能达到60fps或更高,再加上这一段代码不需要扩展,这个笨办法反倒成了最优解 因此我们看代码不能只在意代码风格,而是结合场景去客观的得出的代码好与坏

Leftpad事件

  function leftpad(str, len, ch) {
      str = String(str);
      var i = -1;
      if (!ch && ch !== 0) ch = ' ';
      len = len - str.length;
      while (++i < len) {
          str = ch + str;
      }
      return str;
  } 

事件的槽点在于:

  • npm模块粒度
  • 代码风格
  • 代码质量/效率 那么我们怎么来改进这样一串简单代码
  function leftpad(str, len, ch) {
      str = "" + str;
      const padLen = len - str.length;
      if(padLen <= 0) {
        return str;
      }
      return (""+ch).repeat(padLen)+str;
  } 

通过用效率更高的repeat()替代while循环,代码也变得更简洁 那么repeat()是怎么提高效率的呢,我们来看一看repeat()的源码

var RequireObjectCoercible = require('es-abstract/2019/RequireObjectCoercible');
  var ToString = require('es-abstract/2019/ToString');
  var ToInteger = require('es-abstract/2019/ToInteger');
​
  module.exports = function repeat(count) {
    var O = RequireObjectCoercible(this);
    var string = ToString(O);
    var n = ToInteger(count);
    // Account for out-of-bounds indices
    if (n < 0 || n == Infinity) {
      throw RangeError('String.prototype.repeat argument must be greater than or equal to 0 and not be Infinity');
    }
​
    var result = '';
    while (n) {
      if (n % 2 == 1) {
        result += string;
      }
      if (n > 1) {
        string += string;
      }
      n >>= 1;
    }
    return result;
  };

第一版本的while循环需要补几位就循环几次,复杂度为O(n) 而repeat()方法中,我们忽略它的依赖方法,在它的while循环中循环次数n在每次循环最后右移一位即除以2,因此复杂独为0(log(n))

算法题判断一个数是否为4的幂

简单地思路就是循环判断判断是否被4整除,能被整除则除以四直到出现无法被4整除或除到0为止,用位操作性能更好但,原理也大致相同,我们就来看一种时间复杂度为O(1)的方法

​
function isPowerOfFour(num){
  num = parseInt(num);
  
  return num > 0 &&
         (num & (num - 1)) === 0 &&
         (num & 0xAAAAAAAAAAAAA) === 0;
}
​
num.addEventListener('input', function(){
  num.className = '';
});
​
checkBtn.addEventListener('click', function(){
  let value = num.value;
  num.className = isPowerOfFour(value) ? 'yes' : 'no';
});

它只有三个判断条件 第一个数字判断大于零 第二个条件(num & (num - 1))这一步操作会使原来的数字转换为二进制数后的最后一位变成0,如果只有一个1,这个数字就变成了0,然而二进制数如果只有一个1那么它肯定就是2的幂,也就是判断了这个数是2的幂 第三个条件0xAAAAAA转换为二进制数就是10101010这样的奇数位为0偶数位为1的数字,二进制数和它进行与操作的数字如果出现偶数位为1则结果不等于零,二进制数中除第一位,奇数全为零且大于零,则这个数字是4的幂,加上第二步判断已经排除第一位为0的数字,因此得出满足这三个条件的数字则为4的幂 可以说是解答的非常巧妙,然而事实上只要满足二进制数第一位为0,其余偶数位权威0且奇数位至少有一个1的数字便是4的幂,利用这个原理,我们用正则表达式也可以轻松的实现

function isPowerOfFour(num) {
  num = parseInt(num).toString(2);
  
  return /^1(?:00)*$/.test(num);
}

虽然效率不如刚才的版本,但是也是一种非常巧妙地构思

再来看一个洗牌的算法

错误的方法

const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
​
function shuffle(cards) {
  return [...cards].sort(() => Math.random() > 0.5 ? -1 : 1);
}
​
console.log(shuffle(cards));
​
const result = Array(10).fill(0);
​
for(let i = 0; i < 1000000; i++) {
  const c = shuffle(cards);
  for(let j = 0; j < 10; j++) {
    result[j] += c[j];
  }
}
​
console.table(result);

这一串代码看上去好像没有什么问题,运行结果好像也是对的,但是如果我们运行的足够多次,会发现如果这个数字越小上出现在前面的概率越高

这是因为sort方法并不是完全的两两换位的算法,因此第一张牌被到后面的概率相对对更低,那么我们来看看更正确的实现方法

const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
​
function shuffle(cards) {
  const c = [...cards];
  for(let i = c.length; i > 0; i--) {
    const pIdx = Math.floor(Math.random() * i);
    [c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]];
  }
  return c;
}
​
console.log(shuffle(cards));
​
const result = Array(10).fill(0);
​
for(let i = 0; i < 10000; i++) {
  const c = shuffle(cards);
  for(let j = 0; j < 10; j++) {
    result[j] += c[j];
  }
}
​
console.table(result);

总结

今天是进入第二阶段前的最后一节课,明显的强度提升,干货满满,我们学习了过了JavaScript或者其他编程语言之后,并不是学会了便是掌握了,我们开发的过程中,该怎样去写更“好”的代码的思路都是相通的,这才是今天真正的主题,我们在开发的过程中会遇到许许多多的复杂的场景,我们要根据场景去选择更适合的代码,而不是一昧的追求更高难度,但是我们也应该具备不断优化代码的细微与能力,因此尽管是前端,但是前端工程师也是工程师,想成为工程师算法都是必不可少的,可能前端平时的应用没有那么多,但也是我们应该具备的能力。