学习 JavaScript | 青训营笔记

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

0x1 各司其职

让 HTML、CSS 和 JavaScript 职能分离

举例:通过 JavaScript 控制页面的 日间模式夜间模式 的切换

v1.0:依据需求直接编程

const btn = document.getElementById('dnchange');
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 = '日';
    }
});

此时 HTML、CSS 同时混杂在 JavaScript 代码中,应当避免不必要的由 JS 直接操作样式

v2.0:将 CSS 部分分离独立

.day {
    background: white;
    color: black;
}
.night {
    background: black;
    color: white;
}
const btn = document.getElementById('dnchange');
btn.addEventListener('click', (e) => {
    const body = document.body;
    if(body.className !== 'night') {
        body.className = 'night';
    } else {
        body.className = 'day';
    }
});

由于当前页面属于纯展示页面,此时交互应寻求零 JS 的方案

v3.0:取消 JavaScript 部分,通过多选框的checked的变化切换

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title></title>
        <style>
            body {
                margin: 0;
                padding: 0;
            }
            .content {
                background: white;
                color: black;
                transition: all 1s;
            }
            #btn {
                display: none;
            }
            #btn:checked + .content {
                background: black;
                color: white;
                transition: all 1s;
            }
        </style>
    </head>
    <body>
        <input id="btn" type="checkbox" />
        <div class="content">
            <header>
                <label id="lab" for="btn">[日夜切换]</label>
                <h2>文章标题</h2>
            </header>
            <main>
                <p>
                    段落内容
                </p>
            </main>
        </div>
    </body>
</html>

0x2 组件封装

好的 UI 组件应具备正确性、扩展性、复用性

举例:轮播图

HTML 结构:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title></title>
        <style>
            #slider {
                position: relative;
                width: auto;
            }
            .list ul {
                list-style-type: none;
                position: relative;
                margin: 0;
                padding: 0;
            }
            .list_item,
            .list_item--selected {
                position: absolute;
                transition: opacity 1s;
                opacity: 0;
                text-align: center;
            }
            .list_item--selected {
                transition: opacity 1s;
                opacity: 1;
            }
        </style>
        <script type="text/javascript">
            class Slider{
                constructor(id, cycle = 3000) {
                    this.container = document.getElementById(id);
                    this.items = this.container.querySelectorAll('.list_item .list_item--selected');
                    this.cycle = cycle;
                    const controller = this.container.querySelector('.list_control');
                    if(controller) {
                        const buttons = container.querySelectorAll('.list_ctrl, .list_ctrl--selected');
                        controller.addEventListener('mousemove', evt => {
                            const idx = Array.from(buttons).indexOf(evt.target);
                            if(idx >= 0) {
                                this.slideTo(idx);
                                this.stop();
                            }
                        });
                        controller.addEventListener('mouseout', evt => {
                            this.start();
                        });
                        this.container.addEventListener('slide', evt => {
                            const idx = evt.detail.index;
                            const selected = controller.querySelector('.list_ctrl, .list_ctrl--selected');
                            if(selected) selected.className = 'list_ctrl';
                            buttons[idx].className = 'list_ctrl--selected';
                        });
                    }
                    const previous = this.controlled.querySelector('.list_previous');
                    if(previous) {
                        previous.addEventListener('click', evt => {
                            this.stop();
                            this.slidePrevious();
                            this.start();
                            evt.preventDefault();
                        });
                    }
                    const next = this.container.querySelector('.list_next');
                    if(next) {
                        next.addEventListener('click', evt => {
                            this.stop();
                            this.slideNext();
                            this.start();
                            evt.preventDefault();
                        });
                    }
                }
                
                getSelectedItem() {		// 获取图片项
                    const selected = this.container.querySelector('.list_item--selected');
                    return selected;
                }
                getSelectedItemIndex() {	// 获取图片在数组中的位置
                    return Array.from(this.items).indexOf(this.getSelectedItem());
                }
                slideTo(idx) {	// 导航至
                    let selected = this.getSelectedItem();
                    if(selected) {
                        selected.className = 'list_item';
                    }
                    let item = this.items[idx];
                    if(item) {
                        item.className = 'list_item--selected';
                    }
                    /* 开发自定义事件 */
                    const detail = { index: idx };
                    const event = new CustomEvent('slide', { bubbles: true, detail });
                    this.container.dispatchEvent(event);
                }
                slideNext() {	// 下一张
                    const currentIdx = this.getSelectedItemIndex();
                    const nextIdx = (currentIdx + 1) % this.items.length;
                    this.slideTo(nextIdx);
                }
                slidePrevious(){	// 上一张
                    const currentIdx = this.getSelectedItemIndex();
                    const previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
                    this.slideTo(previousIdx);
                }
                
                start() {
                    this.stop();
                    this._timer = setInteval(() => this.slideNext(), this.cycle);
                }
                stop() {
                    clearInterval(this._timer);
                }
            }
            
            const slider = new Slider('slider');
            slider.start();
        </script>
    </head>
    <body>
        <div id="slider" class="list">
            <ul>
                <li class="list_item--selected">
                    <img src="./1.png" />
                </li>
                <li class="list_item">
                    <img src="./2.png" />
                </li>
                <li class="list_item">
                    <img src="./3.png" />
                </li>
                <li class="list_item">
                    <img src="./4.png" />
                </li>
            </ul>
            <a class="list_next"></a>
            <a class="list_previous"></a>
            <div class="list_control">
                <span class="list_ctrl--selected"></span>
                <span class="list_ctrl"></span>
                <span class="list_ctrl"></span>
                <span class="list_ctrl"></span>
            </div>
        </div>
    </body>
</html>

对该组件进一步优化有以下方法:

  1. 重构:插件化

    将控制元素抽取成插件,插件与组件之间通过 依赖注入 的方式建立联系

  2. 重构:模板化

    将 HTML 模板化,使其更易于扩展

  3. 重构:抽象化

    将组件通用模型抽象出来

组件封装基本方法:

  • 结构设计
  • 展现效果
  • 行为控制——API(功能)、Event(控制流)

组件设计的原则:

  • 封装性
  • 正确性
  • 扩展性
  • 复用性

0x3 过程抽象

应用函数式编程思想

  1. 操作次数限制

    • 一些异步交互
    • 一次性的 HTTP 请求
    const list = document.querySelector('ul');
    const buttons = list.querySelectorAll('button');
    buttons.forEach((button) => {
        buttons.addEventListener('click', (evt) => {
            const target = evt.target;
            target.parentNode.className = 'completed';
            setTimeout(() => {
                list.removeChild(target.parentNode);
            }, 2000);
        });
    });
    

    上述代码中,当短时间内多次单击按钮会触发 BUG

    为解决该 BUG,可通过对回调函数封装一个once(),修改如下:

    function once(fn) {
        return function(...args) {
            if(fn) {
                const ret = fn.apply(this, args);
                fn = null;
                return ret;
            }
        }
    }
    
    const list = document.querySelector('ul');
    const buttons = list.querySelectorAll('button');
    buttons.forEach((button) => {
        buttons.addEventListener('click', once((evt) => {
            const target = evt.target;
            target.parentNode.className = 'completed';
            setTimeout(() => {
                list.removeChild(target.parentNode);
            }, 2000);
        }));
    });
    
  2. 高阶函数

    1. 比如上面例子中的once()函数,为了能让 “ 只执行一次 ” 的需求覆盖不同的事件处理,从而将该需求剥离出来。这个剥离过程称为过程抽象

    2. 函数装饰器

      • 以函数作为参数
      • 以函数作为返回值
      function HOF0(fn) {
          return function(...args) {
              return fn.apply(this, args);
          }
      }
      
    3. 常用高阶函数(once 等),基于 HOF 进行延申

      1. Throotle--节流函数

        /* 500ms 点击计数 */
        
        function throotle(fn, time = 500) {
            let timer;
            return function(...args) {
        		if(timer == null) {
                    fn.apply(this, args);
                    timer = setTimeout(() => {
                        timer = null;
                    }, time)
                }
            }
        }
        
        /* 设置每次点击间隔 500ms */
        btn.onclick = throotle(function(e) {
            circle.innerHTML = parseInt(circle.innerHTML) + 1;
            circle.className = 'fade';
            setTimeout(() => circle.className='', 250);
        })
        
      2. Debounce--防抖函数

        /* 笨鸟先飞小游戏 */
        
        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.top = x0 +ep * (x - x0) + 'px';
            }, p => p*p);
            a1.animate();
        }, 100));
        
      3. 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);
        }
        
      4. Iterative--可迭代函数

        /* 将列表中奇数项的文本颜色设置为红色 */
        
        const isIterable = obj => obj != null && typeof obj[Symbol.iteator] === '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');
        
    4. 为什么要使用高阶函数

      高阶函数是一种纯函数(pure function),通过高阶函数对非纯函数(inpure function)进行装饰,从而提高代码可维护性

  3. 编程范式

    编程 Programming
    命令式 Imperative
    声明式 Declarative
    面向过程 Procedural
    如 C语言
    面向对象 ObjectOriented
    如 C++
    逻辑式 Logic
    如 Prolog
    函数式 Functional
    如 Haskell

    举例说明命令式与声明式的特点:将列表的数值翻一倍

    let list = [1, 2, 3, 4];
    let mapl = [];
    for(let i = 0; i < list.length; i++) {
        mapl.push(list[i] * 2);
    }
    console.log(mapl);
    

    命令式特点:强调要做什么 What

    let list = [1, 2, 3, 4];
    const double = x => x*2;
    list = list.map(double);
    console.log(list);
    

    声明式特点:强调要怎么做 How

    举例:按钮样式切换

    • 命令式

      switcher.onclick = function(evt) {
          if(evt.target.className==='on'){
              evt.target.className = 'off';
          } else {
              evt.target.className = 'on';
          }
      }
      
    • 声明式

      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'
      );
      
    • 三态——当切换功能出现第三种状态时

      命令式需要添加一段新的else if逻辑分支

      声明式需要添加一句新的evt => 函数语句

      体现了声明式优秀的可扩展性

0x4 质量优化

  1. 举例一:交通灯轮转
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title></title>
        <style>
            li { color: grey; }
            #traffic { display: flex; flex-direction: column; }
            .stop li:nth-child(1) { color: red; }
            .wait li:nth-child(2) { color: yellow; }
            .pass li:nth-child(3) { color: green; }
        </style>
        <script type="text/javascript"></script>
    </head>
    <body>
        <ul id="traffic" class="wait">
            <li></li>
            <li></li>
            <li></li>
        </ul>
    </body>
</html>

v1.0:依据需求直接编程

const traffic = document.getElementById('traffic');
(function reset() {
    traffic.className = 'stop';
    setTimeout(function() {
        traffic.className = 'wait';
        setTimeout(function() {
            traffic.className = 'pass';
            setTimeout(reset, 1000);
        }, 1000);
    }, 1000);
}){};

v2.0:数据抽象

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

v3.0:过程抽象

const traffic = document.getElementById('traffic');
function wait(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
function poll(...fnList) {	/* 轮询 */
    let stateIdx = 0;
    return async function(...args) {
        let fn = fnList[stateIdx++ % 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() {
    while(1) {
        await trafficStatePoll();
    }
}());

v4.0:异步与函数式

const traffic = document.getElementById('traffic');
function wait(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
function setState(state) {
    traffic.className = state;
}
async function start() {
    while(1){
        setState('wait');
        await wait(1000);
        setState('stop');
        await wait(3000);
        setState('pass');
        await wait(3000);
    }
}
start();
  1. 举例二:判断一个数是否为 4 的幂
// 方法一:直接循环除四直至为一
function isPowerOfFour_1(num) {
    num = parseInt(num);
    while(num > 1){
        if(num % 4)	return false;
        num /= 4;
    }
    return num === 1;
}

// 方法二:通过基础位运算
function isPowerOfFour_2(num) {
    num = parseInt(num);
    while(num > 1){
        if(num & 0b11)	return false;	// 判断二进制最后两位是否为 00
        num >>>= 2;
    }
    return num === 1;
}

// 方法三:位运算进阶方法
function isPowerOfFour_3(num) {
    num = parseInt(num);
    return num > 0 &&
           (num & (num - 1)) === 0 &&
           (num & 0xAAAAAAAAAAAAA) === 0;
}


// 方法四:使用正则表达式方法
function isPowerOfFour_4(num) {
    num = parseInt(num).toString(2);
    return /^1(?:00)*$/.test(num);
}
  1. 举例三:号码抽奖
const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

function shuffle1(cards) {	// 错误写法,中奖概率不均
    return [...cards].sort(() => Math.random() > 0.5 ? -1 : 1);
}
function shuffle2(cards) {	// 正确写法,中奖概率均衡
    const a = [...cards];
    for(let i = a.length; i > 0; i--) {
        const idx = Math.floor(Math.random() * i);
        [a[idx], a[i-1]] = [a[i-1], a[idx]];
    }
    return a;
}
function * draw(cards) {	// 不需要跑完整个循环,即取即用,更适用于数值庞大且中奖数小的情况
    const a = [...cards];
    for(let i = a.length; i > 0; i--) {
        const idx = Math.floor(Math.random() * i);
        [a[idx], a[i-1]] = [a[i-1], a[idx]];
        yield a[i - 1];
    }
}

const result1 = Array(10).fill(0);
const result2 = Array(10).fill(0);
const result3 = draw(cards);

for(let i = 0; i < 1000000; i++) {
    const c = shuffle1(cards);
    const d = shuffle2(cards);
    for(let j = 0; j < 10; j++) {
        result1[j] += c[j];
        result2[j] += d[j];
    }
}
console.table(result1);
console.table(result2);

console.log([...result3]);
console.log([
    result3.next().value,
    result3.next().value,
    result3.next().value
]);
  1. 举例四:抢红包
/* 切西瓜法:每次切割最大的部分 */
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;
}
console.table(generate(0.1, 10));

/* 抽牌法:在数列中添加分隔 */
function * draw(cards) {
    const c = [...cards];
    for(let i = c.length; i > 0; i--) {
        const idx = Math.floor(Math.random() * i);
        [c[idx], c[i-1]] = [c[i-1], c[idx]];
        yield c[i - 1];
    }
}
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; i++) {
        result.push(pick.next().value);
    }
    result.sort((a, b) => a - b);
    for(let i = count - 1; i> 0; i--) {
        result[i] = result[i] - result[i - 1];
    }
    return result;
}
console.table(generate(10, 10));

0x5 Leftpad 事件

Leftpad 是一个用于添加前导零来对齐数字的 npm 模块(“ 00 01 ... 09 10 11 ... ”)。

它在被许多其他模块依赖时,其作者由于某些原因将其删除,导致许多依赖它的模块被迫无法使用。

有人指出该模块代码质量低、效率低,原代码如下:

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

经过改良后,代码如下:

function leftpad(str, len, ch) {
    str = "" + str;
    const padLen = len - str.length;
    if(padLen <= 0) {
        return str;
    }
    return ("" + ch).repeat(padLen) + str;
}

上述代码中的repeat是 ES10 中的内置函数,起到了二次幂的方法提高效率

/* repeat 简易版 */
function repeat(str, cnt) {
    var result = '';
    while(cnt) {
        if(cnt % 2 == 1){
            result += str;
        }
        if(cnt > 1){
            str += str;
        }
        n >>= 1;
    }
    return result;
}
console.log('*', 5);
posted @ 2023-01-20 15:18  SRIGT  阅读(6)  评论(0编辑  收藏  举报  来源