JS进阶 | 青训营笔记

117 阅读6分钟

这是我参与「第四届青训营 」笔记创作活动的的第4天,老师带我们深入讲解了JS的进阶用法,作为前端开发的三大基石之一,它的作用是毋庸置疑的。

一、前言

课程目标:

  1. 怎样写好js代码
  2. 写程序的共性问题与原则问题

书籍推荐:

  1. 犀牛书
  2. 红宝书
  3. 《JavaScript The Good Parts》

写好JS的原则:

  • 各司其责:让HTML、CSS、JS职能分离

    • HTML负责结构,CSS负责表现(样式) ,JS负责行为
  • 组件封装:好的UI组件具备正确性、拓展性、复用性

  • 过程抽象:应用函数式编程思想

二、JS三大原则

各司其职

  • 版本一

    // 使用JS负责表现,没有各司其责
    const btn = document.getElementById('modeBtn')l
    btn.addEventListener('click', (e)=>{
        const body = document.body;
        if (e.target.innerHTML === 'Sun Mode'){
            body.style.backgroundColor = 'black';
            body.style.color = 'white';
            e.target.innerHTML === 'Moon Mode';
        }else {
            body.style.backgroundColor = 'white';
            body.style.color = 'black';
            e.target.innerHTML === 'Sun Mode';
        }
    )
    
  • 版本二

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

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <link rel="stylesheet" href="./style.css">
      <title>深夜食堂</title>
    </head>
    <body>
      <!-- 虚拟checkbox,设置display为none -->
      <input id="modeCheckBox" type="checkbox">
      <div class="content">
        <header>
          <!-- 设置label,for属性值为checkbox,达到实际控制的作用 -->
          <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>
    </body>
    </html>
    
    body, html {
      width: 100%;
      height: 100%;
      max-width: 600px;
      padding: 0;
      margin: 0;
      overflow: hidden;
    }
    ​
    body {
      box-sizing: border-box;
    }
    ​
    .content {
      height: 100%;
      padding: 10px;
      transition: background-color 1s, color 1s;
    }
    ​
    div.pic img {
      width: 100%;
    }
    ​
    /* 处理模式切换的checkBox */#modeCheckBox {
      display: none;
    }
    ​
    #modeBtn {
      font-size: 2rem;
      float: right;
    }
    ​
    #modeBtn::after {
      content: '🌞';
    }
    ​
    /* 处于checked状态时,对类名为content的元素,进行样式修改 */
    #modeCheckBox:checked + .content {
      background-color: black;
      color: white;
      transition: all 1s;
    }
    ​
    #modeCheckBox:checked + .content #modeBtn::after {
      content: '🌜';
    }
    

结论:

  • HTML、CSS、JS各司其职
  • 应当避免不必要的由JS直接操作样式
  • 可以用class来表示状态
  • 纯展示类交互寻求零JS方案

组件封装⭐

组件是指Web页面上抽出来一个个包含模板(HTML)、功能(JS)、样式(CSS)的单元。

好的组件具备封装性、正确性、拓展性、复用性

用原生JS写原生电商网站的轮播图,该如何实现?

总结(组件封装的基本方法):

  • 组件设计的原则

    • 封装性
    • 真确性
    • 拓展性
    • 复用性
  • 实现组件的步骤

    • 结构设计

    • 展现效果

    • 行为设计

      • API(功能):设计一些接口来操作
      • Event(控制流): 自定义事件解耦
  • 三次重构

    • 插件化
    • 模板化
    • 抽象化(组件框架)

过程抽象⭐

  • 用来处理局部细节控制的一些方法
  • 函数式编程思想的基础应用

为了能够让"只执行一次"的需求覆盖不同的事件处理,我们可以将需求剥离。这个过程我们称之为过程抽象

下面这段代码是调用过程抽象来实现的防抖效果:

function once(fn){
    return function(...args){
        if (fn){
            const ret = fn.apply(this, args);
            fn = null;
            return ret;
        }
    }
}
const foo = once(()=>{
    console.log("你好")
})
foo();
foo();
foo(); // 实际只调用了一次

高阶函数HOF:

function HOFO(fn){
    return function(...args){
        return fn.apply(this, args);
    }
}
  • 特点

    • 以函数作为参数
    • 以函数作为返回值
    • 常用于作为函数装饰器
  • 常用高阶函数

    • Once

      function once(fn) {
        return function(...args) {
          if(fn) {
            const ret = fn.apply(this, args);
            fn = null;
            return ret;
          }
        }
      }
      
    • Throttle节流

      function throttle(fn, time = 500){
        let timer;
        return function(...args){
          if(timer == null){
            fn.apply(this,  args);
            timer = setTimeout(() => {
              timer = null;
            }, time)
          }
        }
      }
      ​
      btn.onclick = throttle(function(e){
        circle.innerHTML = parseInt(circle.innerHTML) + 1;
        circle.className = 'fade';
        setTimeout(() => circle.className = '', 250);
      });
      
    • Debounce防抖

      <script src="https://s1.qhres2.com/!bd39e7fb/animator-0.2.0.min.js"></script>
      <div id="bird" class="sprite bird1"></div>
      
      html, body {
        margin:0;
        padding:0;
      }
      ​
      .sprite {
        display:inline-block; overflow:hidden; 
        background-repeat: no-repeat;
        background-image:url(https://p1.ssl.qhimg.com/d/inn/0f86ff2a/8PQEganHkhynPxk-CUyDcJEk.png);
      }
      ​
      .bird0 {width:86px; height:60px; background-position: -178px -2px}
      .bird1 {width:86px; height:60px; background-position: -90px -2px}
      .bird2 {width:86px; height:60px; background-position: -2px -2px}
      ​
      #bird{
        position: absolute;
        left: 100px;
        top: 100px;
        transform: scale(0.5);
        transform-origin: -50% -50%;
      }
      
      var i = 0;
      setInterval(function(){
        bird.className = "sprite " + 'bird' + ((i++) % 3);
      }, 1000/10);
      ​
      function debounce(fn, dur){
        dur = dur || 100;
        var timer;
        return function(){
          clearTimeout(timer);
          timer = setTimeout(() => {
            fn.apply(this, arguments);
          }, dur);
        }
      }
      ​
      document.addEventListener('mousemove', debounce(function(evt){
        var x = evt.clientX,
            y = evt.clientY,
            x0 = bird.offsetLeft,
            y0 = bird.offsetTop;
        
        console.log(x, y);
        
        var a1 = new Animator(1000, function(ep){
          bird.style.top = y0 + ep * (y - y0) + 'px';
          bird.style.left = x0 + ep * (x - x0) + 'px';
        }, p => p * p);
        
        a1.animate();
      }, 100));
      
    • Consumer

      function consumer(fn, time){
        let tasks = [],
            timer;
        
        return function(...args){
          tasks.push(fn.bind(this, ...args));
          if(timer == null){
            timer = setInterval(() => {
              tasks.shift().call(this)
              if(tasks.length <= 0){
                clearInterval(timer);
                timer = null;
              }
            }, time)
          }
        }
      }
      ​
      function add(ref, x){
        const v = ref.value + x;
        console.log(`${ref.value} + ${x} = ${v}`);
        ref.value = v;
        return ref;
      }
      ​
      let consumerAdd = consumer(add, 1000);
      ​
      const ref = {value: 0};
      for(let i = 0; i < 10; i++){
        consumerAdd(ref, i);
      }
      
      function consumer(fn, time){
        let tasks = [],
            timer;
        
        return function(...args){
          tasks.push(fn.bind(this, ...args));
          if(timer == null){
            timer = setInterval(() => {
              tasks.shift().call(this)
              if(tasks.length <= 0){
                clearInterval(timer);
                timer = null;
              }
            }, time)
          }
        }
      }
      ​
      btn.onclick = consumer((evt)=>{
        let t = parseInt(count.innerHTML.slice(1)) + 1;
        count.innerHTML = `+${t}`;
        count.className = 'hit';
        let r = t * 7 % 256,
            g = t * 17 % 128,
            b = t * 31 % 128;
        
        count.style.color = `rgb(${r},${g},${b})`.trim();
        setTimeout(()=>{
          count.className = 'hide';
        }, 500);
      }, 800)
      
    • Iterative:使奇数行变色

      <ul>
        <li>a</li>
        <li>b</li>
        <li>c</li>
        <li>d</li>
        <li>e</li>
        <li>f</li>
        <li>g</li>
      </ul>
      
      const isIterable = obj => obj != null 
        && typeof obj[Symbol.iterator] === 'function';
      ​
      function iterative(fn) {
        return function(subject, ...rest) {
          if(isIterable(subject)) {
            const ret = [];
            for(let obj of subject) {
              ret.push(fn.apply(this, [obj, ...rest]));
            }
            return ret;
          }
          return fn.apply(this, [subject, ...rest]);
        }
      }
      ​
      const setColor = iterative((el, color) => {
        el.style.color = color;
      });
      ​
      const els = document.querySelectorAll('li:nth-child(2n+1)');
      setColor(els, 'red');
      

三、高阶函数HOF

为什么使用高阶函数?

首先要理解纯函数和非纯函数,纯函数就是给定输入,可以预计输出的函数,高阶函数就是纯函数的一种

非纯函数是一种具有不可预知性的函数,会降低系统的可维护性

因此,高阶函数的使用可以减少代码中非纯函数的数量

Pure and inpure function

// 纯函数 pure function
function add(x, y){return x+y};
console.log(assert(3+6===9, 'fail'));
// 非纯函数 inpure function
let idx = 0;
function count(){
    return ++idx;
}
count();
count();
count();

案例:实现奇数行变色

<div id="app">
  <ul>
    <li>1</li>
    <li>1</li>
    <li>1</li>
    <li>1</li>
    <li>1</li>
    <li>1</li>
  </ul>
</div>
  • 不使用高阶函数

    function setColor(el, color){
      el.style.color = color;
    }
    // setColor(app, 'red');
    function setColors(els, color){
      els.forEach(el=>{
        setColor(el, color);
      })
    }
    setColors([...document.querySelectorAll('li:nth-child(2n+1)')], 'skyblue')
    
  • 使用高阶函数

    function setColor(el, color){
      el.style.color = color;
    }
    // setColor(app, 'red');// 使用高阶函数
    const isIterable = obj => obj != null 
      && typeof obj[Symbol.iterator] === 'function';
    ​
    function iterative(fn) {
      return function(subject, ...rest) {
        if(isIterable(subject)) {
          const ret = [];
          for(let obj of subject) {
            ret.push(fn.apply(this, [obj, ...rest]));
          }
          return ret;
        }
        return fn.apply(this, [subject, ...rest]);
      }
    }
    ​
    const setColors = iterative(setColor);
    setColors([...document.querySelectorAll('li:nth-child(2n+1)')], 'skyblue')
    ​
    // 下面的代码是说明高阶函数的强大复用性
    const addMany = iterative(add);
    console.log(addMany([1,2,3,4], 5)); // [6, 7, 8, 9]
    

编程范式 ⭐

Javascript同时支持两种范式,而两种范式又可以分为两类

  • 命令式:怎么做

    • 面向过程

    • 面向对象

    • 命令式实现开关切换

      switcher.onclick = (evt)=>{
          if (evt.target.className === 'on'){
              evt.target.className === 'off';
          }else {
              evt.target.className === 'on';
          }
      }
      
  • 声明式:做什么(推荐⭐)

    • 逻辑式编程

    • 函数式编程

    • 声明式实现开关切换

      #switcher {
        display: inline-block;
        background-color: black;
        width: 50px;
        height: 50px;
        line-height: 50px;
        border-radius: 50%;
        text-align: center;
        cursor: pointer;
      }
      ​
      #switcher.on {
        background-color: green;
      }
      ​
      #switcher.off {
        background-color: red;
      }
      ​
      #switcher.on:after {
        content: 'on';
        color: white;
      }
      ​
      #switcher.off:after {
        content: 'off';
        color: white;
      }
      
      function toggle(...actions){
        return function(...args){
          let action = actions.shift();
          actions.push(action);
          return action.apply(this, args);
        }
      }
      ​
      switcher.onclick = toggle(
        evt => evt.target.className = 'off',
        evt => evt.target.className = 'on'
      );
      


四、JS算法

  • 使用场景?
  • 效率?
  • 风格?
  • 约定?
  • 设计?

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;
    }
    return str;
}
console.log(leftpad('12', 5, '0'));

代码简洁+效率提升

function leftpad(str, len, ch){
    str = "" + str;
    const padLen = len - str.length;
    if (padLen<=0) return str;
    return (""+ch).repeat(padLen) + str;// 调用字符串的repeat方法
}
console.log(leftpad('12', 5, '0'));

案例1.1:数据抽象的交通灯

<ul id="traffic" class="wait">
  <li></li>
  <li></li>
  <li></li>
</ul>
#traffic {
  display: flex;
  flex-direction: column;
}
​
#traffic li {
  display: inline-block;
  width: 50px;
  height: 50px;
  background-color: gray;
  margin: 5px;
  border-radius: 50%;
}
​
#traffic.stop li:nth-child(1) {
  background-color: #a00;
}
​
#traffic.wait li:nth-child(2) {
  background-color: #aa0;
}
​
#traffic.pass li:nth-child(3) {
  background-color: #0a0;
}
const traffic = document.getElementById('traffic');
​
const stateList = [  {state: 'wait', last: 1000},  {state: 'stop', last: 3000},  {state: 'pass', last: 3000},];
​
function start(traffic, stateList){
  function applyState(stateIdx) {
    const {state, last} = stateList[stateIdx];
    traffic.className = state;
    setTimeout(() => {
      applyState((stateIdx + 1) % stateList.length);
    }, last)
  }
  applyState(0); // 递归调用
}
​
start(traffic, stateList);

案例1.2:过程抽象的交通灯

const traffic = document.getElementById('traffic');
​
function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
​
// 轮询方法
function poll(...fnList){
  let stateIndex = 0;
  
  return async function(...args){
    let fn = fnList[stateIndex++ % fnList.length];
    return await fn.apply(this, args);
  }
}
​
async function setState(state, ms){
  traffic.className = state;
  await wait(ms);
}
​
let trafficStatePoll = poll(setState.bind(null, 'wait', 1000),
                            setState.bind(null, 'stop', 3000),
                            setState.bind(null, 'pass', 3000));
​
(async function() {
  // noprotect
  while(1) {
    await trafficStatePoll();
  }
}());

案例1.3:异步+函数式编程的交通灯⭐

const traffic = document.getElementById('traffic');
​
function wait(time){
  return new Promise(resolve => setTimeout(resolve, time));
}
​
function setState(state){
  traffic.className = state;
}
​
async function start(){
  //noprotect
  while(1){
    setState('wait');
    await wait(1000);
    setState('stop');
    await wait(3000);
    setState('pass');
    await wait(3000);
  }
}
​
start();

案例2:判断是否是4的幂

<input id="num" value="24"></input>
<button id="checkBtn">
    判断
</button>
// function isPowerOfFour(num) {
//   num = parseInt(num);
​
//   while(num > 1) {
//     if(num % 4) return false;
//     num /= 4;
//   }
//   return num === 1;
// }
​
// function isPowerOfFour(num) {
//   num = parseInt(num);
​
//   while(num > 1) {
//     if(num & 0b11) return false;
//     num >>>=2;
//   }
//   return num === 1;
// }
// 推荐:使用正则匹配,
// function isPowerOfFour(num) {
//   num = parseInt(num).toString(2); // 转化为2进制字符串
  
//   return /^1(?:00)*$/.test(num); // 正则表达式匹配
// }
​
function isPowerOfFour(num){
  num = parseInt(num);
  // (num & (num - 1)) === 0判断是否为  2的幂
  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';
});

案例3.1:洗牌的错误方式

<div id="app">洗牌-错误写法</div>
<hr/>
<div id="log"></div>
<script>
  window.console = JCode.logger(log);
</script>

sort方法并不是两两位置均匀交换的,导致越小的数字,放到序列前面的概率变大

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);

案例3.2:洗牌的正确方式

<div id="app">洗牌-正确写法</div>
<hr/>
<div id="log"></div>
<script>
  window.console = JCode.logger(log);
</script>

算法:这个算法是符合生活实际的,并且是等概率操作的

  • 选定一个随机索引
  • 让这个索引位置的牌与最后一张牌交换
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);

案例3.3:洗牌之生成器

提升性能

const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
​
function * draw(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]];
    yield c[i - 1];
  }
}
​
const result = draw(cards);
console.log([...result]); // 洗牌
console.log(result.next().value); // 只取一张牌
console.log([result.next().value,
             result.next().value,
             result.next().value]); // 取三张牌

案例4.1:分红包-切西瓜法

算法:每次都筛选出最大的一块进行切分,O(m*n)的时间复杂度

码上掘金 (juejin.cn)

function generate(amount, count){
  let ret = [amount];
  
  while(count > 1){
    //挑选出最大一块进行切分
    let cake = Math.max(...ret),
        idx = ret.indexOf(cake),
        part = 1 + Math.floor((cake / 2) * Math.random()),
        rest = cake - part;
    
    ret.splice(idx, 1, part, rest);
    
    count--;
  }
  return ret;
}
​
const amountEl = document.getElementById('amount');
const countEl = document.getElementById('count');
const generateBtn = document.getElementById('generateBtn');
const resultEl = document.getElementById('result');generateBtn.onclick = function(){
  let amount = Math.round(parseFloat(amountEl.value) * 100);
  let count = parseInt(countEl.value);
  
  let output = [];
  
  if(isNaN(amount) || isNaN(count) 
     || amount <= 0 || count <= 0){
    output.push('输入格式不正确!');
  }else if(amount < count){
    output.push('钱不够分')
  }else{
    output.push(...generate(amount, count));
    output = output.map(m => (m / 100).toFixed(2));
  }
  resultEl.innerHTML = '<li>' + 
                        output.join('</li><li>') +
                       '</li>';
}

案例4.2:分红包-抽牌法

把红包总金额看作从0到99.99的数列,插入9个分隔符

O(n)的时间复杂度,空间复杂度较高 码上掘金 (juejin.cn)

function * draw(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]];
    yield c[i - 1];
  }
}
​
// 0, 1, 2....9999
// 49 199
function generate(amount, count){
  if(count <= 1) return [amount];
  const cards = Array(amount - 1).fill(0).map((_, i) => i + 1);
  const pick = draw(cards);
  const result = [];
  for(let i = 0; i < count - 1; i++) {
    result.push(pick.next().value);
  }
  result.sort((a, b) => a - b);
  result.push(amount);
  for(let i = result.length - 1; i > 0; i--) {
    result[i] = result[i] - result[i - 1];
  }
  return result;
}
​
const amountEl = document.getElementById('amount');
const countEl = document.getElementById('count');
const generateBtn = document.getElementById('generateBtn');
const resultEl = document.getElementById('result');generateBtn.onclick = function(){
  let amount = Math.round(parseFloat(amountEl.value) * 100);
  let count = parseInt(countEl.value);
  
  let output = [];
  
  if(isNaN(amount) || isNaN(count) 
     || amount <= 0 || count <= 0){
    output.push('输入格式不正确!');
  }else if(amount < count){
    output.push('钱不够分')
  }else{
    output.push(...generate(amount, count));
    output = output.map(m => (m / 100).toFixed(2));
  }
  resultEl.innerHTML = '<li>' + 
                        output.join('</li><li>') +
                       '</li>';
}