月影老师讲JS | 青训营笔记

123 阅读6分钟

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

今天听月影老师讲解了Javascript,下面来分享一下我觉得比较有意思的内容。

写好前端的一些原则

各司其职

让HTML、CSS和JS职能分离

深夜食堂的案例教会我们:

  • 应该避免不必要的由JS直接操作样式

  • 可以用class来表示状态

  • 纯展示类交互寻求零JS方案

组件封装

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

  • 组件设计的原则:封装性、正确性、扩展性、复用性

  • 实现组件的步骤:结构设计、展现效果、行为设计

  • 三次重构:插件化 模块化 抽象化(组件框架)

过程抽象

应用函数式编程思想

这里主要想分享一下函数的节流和防抖,这也是在面试过程中经常会被问到的

对节流与防抖的理解

  • 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
  • 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。

防抖函数的应用场景:

  • 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次
  • 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤lodash.debounce

节流函数的适⽤场景:

  • 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

​ 应用场景:1.提交表单 2.高频监听事件

函数防抖的实现:

function debounce(fn, wait) {
  var timer = null;

  return function() {
    var context = this,
      args = [...arguments];

    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

函数节流的实现:

// 时间戳版
function throttle(fn, delay) {
  var preTime = Date.now();

  return function() {
    var context = this,
      args = [...arguments],
      nowTime = Date.now();

    // 如果两次时间间隔超过了指定时间,则执行函数。
    if (nowTime - preTime >= delay) {
      preTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

// 定时器版
function throttle (fun, wait){
  let timeout = null
  return function(){
    let context = this
    let args = [...arguments]
    if(!timeout){
      timeout = setTimeout(() => {
        fun.apply(context, args)
        timeout = null 
      }, wait)
    }
  }
}

然后来介绍一些算法题

判断是否是4的幂

这个题目很简单,我们先来分析一下:

1、如果一个数是4的幂,则一定是2的幂,那么2的幂要如何判断呢?2的幂有一个特点就是二进制只有一个1。比如2的二进制是10,8的二进制是1000...

我们借助这个特性,就可以通过(num & (num - 1))是否为0来进行判断了。

相信大家和我一样,刚看到这个式子的时候很懵。我们前面说过了2的幂的二进制的特点是只有一个1,而(num & (num - 1))可以做到让num二进制中1的个数减去1。这个证明也很简单,我们只考虑num的最后两位,比如是01,那么减去1之后这两位会变成00,&之后是00(个数少了1)....这里我们不做过多证明。

2、当然只是这样肯定不够的,比如8是2的幂却不是4的。在此条件的基础上还要保证1的位数一定在奇数位

(num&0x55555555) == num

(或者这里我们可以根据4的特性,4 % 3 == 1)

所以最终的代码:

//版本1
function isPowerOfFour(num){
  num = parseInt(num);
  
  return num > 0 &&
         (num & (num - 1)) === 0 &&
         (num & 0xAAAAAAAAAAAAA) === 0;
}

//版本2
function isPowerOfFour(num) {
    nun = parseInt(num);
    return num > 0 && (num & (num - 1)) == 0 && num % 3 == 1;
}

洗牌问题

网上常见的版本:

const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

function shuffle(cards) {
  return [...cards].sort(() => Math.random() > 0.5 ? -1 : 1);
}

但是这样的问题是index越小排在前面的概率越大。这显然不是我们想要的结果。所以下面介绍一种改进的算法

正确的写法:

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

思路是每次随机抽出一张牌与倒数第i位互换(i为抽牌的次数)

这个算法还可以进行改进:

采用生成器

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

分红包问题

切西瓜法

主要的思路是类似于切西瓜一样,每次都是切最大的那块。(通过Math.max寻找最大的)

切的实现也是利用随机数,会得到两块。

这里需要注意的是我们需要从数组中删除切的这一块,并把切好的两块添加进数组。

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

但是这样抽出来的红包相对来说比较均匀,不够刺激呀~~~~

下面我们介绍第二种方法

抽牌法

这里思路利用的就是我们前面介绍的洗牌问题。

时间复杂度O(n),缺点是空间复杂度比较高。

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

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

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