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

84 阅读10分钟

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

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

写好JS的一些原则

  • 各司其职:让HTML、CSS、JavaScript职能分离
  • 组件封装:好的UI具有正确性、扩展性、复用性
  • 过程抽象:应用函数式编程思维

各司其职

image.png

版本一:

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 = '🌞';
  }
});

通过对文本及样式的修改,达到效果,但是修改DOM的次数较多(3次),造成回流和重绘,使得性能降低

版本二:

const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
  const body = document.body;
  if(body.className !== 'night') {
    body.className = 'night';
  } else {
    body.className = '';
  }
});
body.night {
  background-color: black;
  color: white;
  transition: all 1s;
}

#modeBtn::before {
  content: '🌞';
}
body.night #modeBtn::before {
  content: '🌜';
}

相较于第一版,第二版在DOM操作上仅对class类名进行了操作,性能更好,可读性更强

版本三:

须知:

  • labelinput两个标签可以相关联

    • 你可以点击关联的标签来聚焦或者激活这个输入元素,就像直接点击输入元素一样。这扩大了元素的可点击区域,让包括使用触屏设备在内的用户更容易激活这个元素。
    • 使用方法:
      1. <label> 需要一个 for 属性,其值和 <input> 的 id 一样

        <label for="cheese">Do you like cheese?</label>
        <input type="checkbox" name="cheese" id="cheese">
        
      2. 将 <input> 直接放在 <label> 里,此时则不需要 for 和 id 属性,因为关联已隐含存在

        <label>Do you like peas?
          <input type="checkbox" name="peas">
        </label>
        
  • 选择器的组合

    #modeCheckBox:checked + .content {
      background-color: black;
      color: white;
      transition: all 1s;
    }
    

    以上使用了多种选择器,#modeCheckBox对应的元素是一个复选框,那么它就有对应得状态为cheked(选中)

    +是相邻选择器,处于两个选择器之间,当第二个元素紧跟在第一个元素之后,并且两个元素都是属于同一个父元素的子元素,则第二个元素将被选中

    所以它选择得是复选框选中状态下,相邻的.content元素

<!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">
  <title>深夜食堂</title>
</head>
<body>
  <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>
</body>
</html>
body, html {
  width: 100%;
  height: 100%;
  max-width: 600px;
  padding: 0;
  margin: 0;
  overflow: hidden;
}

body {
  box-sizing: border-box;
}

.content {
  padding: 10px;
  transition: background-color 1s, color 1s;
}

div.pic img {
  width: 100%;
}

#modeCheckBox {
  display: none;
}

#modeCheckBox:checked + .content {
  background-color: black;
  color: white;
  transition: all 1s;
}

#modeBtn {
  font-size: 2rem;
  float: right;
}

#modeBtn::after {
  content: '🌞';
}

#modeCheckBox:checked + .content #modeBtn::after {
  content: '🌜';
}

解析:

点击<label id="modeBtn" for="modeCheckBox"></label>,就等同于点击了<input>复选框,使用复选框的状态来调整样式

总结

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

组件封装

image.png

<div id="my-slider" class="slider-list">
  <ul>
    <li class="slider-list__item--selected">
      <img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png"/>
    </li>
    <li class="slider-list__item">
      <img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg"/>
    </li>
    <li class="slider-list__item">
      <img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg"/>
    </li>
    <li class="slider-list__item">
      <img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg"/>
    </li>
  </ul>
</div>

我们使用类名slider-list__item--selected表示当前展示图

#my-slider{
  position: relative;
  width: 790px;
}

.slider-list ul{
  list-style-type:none;
  position: relative;
  padding: 0;
  margin: 0;
}

.slider-list__item,
.slider-list__item--selected{
  position: absolute;
  transition: opacity 1s;
  opacity: 0;
  text-align: center;
}

.slider-list__item--selected{
  transition: opacity 1s;
  opacity: 1;
}

我们通过相对定位,将所有图片都叠加在一起,并且将不展示的图片设置成透明

class Slider{
  constructor(id){
    this.container = document.getElementById(id);
    this.items = this.container
    .querySelectorAll('.slider-list__item, .slider-list__item--selected');
  }
  getSelectedItem(){
    const selected = this.container
      .querySelector('.slider-list__item--selected');
    return selected
  }
  getSelectedItemIndex(){
    return Array.from(this.items).indexOf(this.getSelectedItem());
  }
  slideTo(idx){
    const selected = this.getSelectedItem();
    if(selected){ 
      selected.className = 'slider-list__item';
    }
    const item = this.items[idx];
    if(item){
      item.className = 'slider-list__item--selected';
    }
  }
  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);  
  }
}

const slider = new Slider('my-slider');
slider.slideTo(3);
  • constructor 初始化

    传入元素的id值,查找到对应的元素并赋值给this.container;查找到该元素下所有slider,并存放在this.items

  • getSelectedItem 获取当前slider元素

    this.container中直接查找类名为slider-list__item--selected元素

  • getSelectedItemIndex 获取当前slider元素的下标

  • slideTo 跳转slider

    根据传入的idx(下标),跳转到对应下标的slider;首先替换当前slider的类名;然后获取到下标为idxslider并将其类名转为slider-list__item--selected

控制轮播图: 在原有的代码上增添两个箭头以及小圆点用于控制轮播图

  <a class="slide-list__next"></a>
  <a class="slide-list__previous"></a>
  <div class="slide-list__control">
    <span class="slide-list__control-buttons--selected"></span>
    <span class="slide-list__control-buttons"></span>
    <span class="slide-list__control-buttons"></span>
    <span class="slide-list__control-buttons"></span>
  </div>

slide-list__control-buttons--selected表示当前选中的小圆点

.slide-list__control{
  position: relative;
  display: table;
  background-color: rgba(255, 255, 255, 0.5);
  padding: 5px;
  border-radius: 12px;
  bottom: 30px;
  margin: auto;
}

.slide-list__next,
.slide-list__previous{
  display: inline-block;
  position: absolute;
  top: 50%;
  margin-top: -25px;
  width: 30px;
  height:50px;
  text-align: center;
  font-size: 24px;
  line-height: 50px;
  overflow: hidden;
  border: none;
  background: transparent;
  color: white;
  background: rgba(0,0,0,0.2);
  cursor: pointer;
  opacity: 0;
  transition: opacity .5s;
}

.slide-list__previous {
  left: 0;
}

.slide-list__next {
  right: 0;
}

#my-slider:hover .slide-list__previous {
  opacity: 1;
}


#my-slider:hover .slide-list__next {
  opacity: 1;
}

.slide-list__previous:after {
  content: '<';
}

.slide-list__next:after {
  content: '>';
}

.slide-list__control-buttons,
.slide-list__control-buttons--selected{
  display: inline-block;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  margin: 0 5px;
  background-color: white;
  cursor: pointer;
}

.slide-list__control-buttons--selected {
  background-color: red;
}
class Slider{
  // cycle 自动播放的时间
  constructor(id, cycle = 3000){
    this.container = document.getElementById(id);
    this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
    this.cycle = cycle;

    const controller = this.container.querySelector('.slide-list__control');
    if(controller){
      const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
      // 小圆点鼠标滑过就会跳转,并且停止图片的轮播
      controller.addEventListener('mouseover', 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('.slide-list__control-buttons--selected');
        if(selected) selected.className = 'slide-list__control-buttons';
        buttons[idx].className = 'slide-list__control-buttons--selected';
      })
    }
    
    const previous = this.container.querySelector('.slide-list__previous');
    if(previous){
      previous.addEventListener('click', evt => {
        this.stop();
        this.slidePrevious();
        this.start();
        evt.preventDefault();
      });
    }
    
    const next = this.container.querySelector('.slide-list__next');
    if(next){
      next.addEventListener('click', evt => {
        this.stop();
        this.slideNext();
        this.start();
        evt.preventDefault();
      });
    }
  }
  getSelectedItem(){
    let selected = this.container.querySelector('.slider-list__item--selected');
    return selected
  }
  getSelectedItemIndex(){
    return Array.from(this.items).indexOf(this.getSelectedItem());
  }
  slideTo(idx){
    let selected = this.getSelectedItem();
    if(selected){ 
      selected.className = 'slider-list__item';
    }
    let item = this.items[idx];
    if(item){
      item.className = 'slider-list__item--selected';
    }
  
    const detail = {index: idx}
    const event = new CustomEvent('slide', {bubbles:true, detail})
    this.container.dispatchEvent(event)
  }
  slideNext(){
    let currentIdx = this.getSelectedItemIndex();
    let nextIdx = (currentIdx + 1) % this.items.length;
    this.slideTo(nextIdx);
  }
  slidePrevious(){
    let currentIdx = this.getSelectedItemIndex();
    let previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
    this.slideTo(previousIdx);  
  }
  start(){
    this.stop();
    this._timer = setInterval(()=>this.slideNext(), this.cycle);
  }
  stop(){
    clearInterval(this._timer);
  }
}

const slider = new Slider('my-slider');
slider.start();

JS代码中稍加修改,并且在slideTo中添加了自定义事件

    const detail = {index: idx}
    const event = new CustomEvent('slide', {bubbles:true, detail})
    this.container.dispatchEvent(event)

CustomEvent MDN参考

 event = new CustomEvent(typeArg, customEventInit);

参数:

  • typeArg:一个表示 event 名字的字符串
  • customEventInit:对象类型
    • detail 可选的默认值是 null 的任意类型数据,是一个与 event 相关的值
    • bubbles 一个布尔值,表示该事件能否冒泡
    • cancelable 一个布尔值,表示该事件是否可以取消

发起事件

this.container.dispatchEvent(event)

监听事件

this.container.addEventListener('slide', evt => { 
    const idx = evt.detail.index
}

优化:

  1. 插件化 Slide仅保留自我的数据与行为,通过插件的方式将需要操作的元素添加到Slider中,实现解耦

    // 小圆点
    function pluginController(slider){
      const controller = slider.container.querySelector('.slide-list__control');
      if(controller){
        const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
        controller.addEventListener('mouseover', evt=>{
          const idx = Array.from(buttons).indexOf(evt.target);
          if(idx >= 0){
            slider.slideTo(idx);
            slider.stop();
          }
        });
    
        controller.addEventListener('mouseout', evt=>{
          slider.start();
        });
    
        slider.addEventListener('slide', evt => {
          const idx = evt.detail.index
          const selected = controller.querySelector('.slide-list__control-buttons--selected');
          if(selected) selected.className = 'slide-list__control-buttons';
          buttons[idx].className = 'slide-list__control-buttons--selected';
        });
      }  
    }
    
    // 后退
    function pluginPrevious(slider){
      const previous = slider.container.querySelector('.slide-list__previous');
      if(previous){
        previous.addEventListener('click', evt => {
          slider.stop();
          slider.slidePrevious();
          slider.start();
          evt.preventDefault();
        });
      }  
    }
    
    function pluginNext(slider){
      const next = slider.container.querySelector('.slide-list__next');
      if(next){
        next.addEventListener('click', evt => {
          slider.stop();
          slider.slideNext();
          slider.start();
          evt.preventDefault();
        });
      }  
    }
    
    Class Slider {
        //...
        registerPlugins(...plugins){
            plugins.forEach(plugin => plugin(this));
        }
      //...
    }
    
    
    const slider = new Slider('my-slider');
    slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
    slider.start();
    
  2. 模块化 以上的插件化,只是将元素封装成一个个插件,在使用时引入即可。但是,修改了JavaScript代码后仍需要修改HTML代码,不太方便。

    image.png 将HTML模块化,更易于扩展,此时HTML代码只有一个容器

    <div id="my-slider" class="slider-list"></div>
    
    class Slider{
      constructor(id, opts = {images:[], cycle: 3000}){
        this.container = document.getElementById(id);
        this.options = opts;
        this.container.innerHTML = this.render();
        this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
        this.cycle = opts.cycle || 3000;
        this.slideTo(0);
      }
      render(){
        const images = this.options.images;
        const content = images.map(image => `
          <li class="slider-list__item">
            <img src="${image}"/>
          </li>    
        `.trim());
    
        return `<ul>${content.join('')}</ul>`;
      }
      registerPlugins(...plugins){
        plugins.forEach(plugin => {
          const pluginContainer = document.createElement('div');
          pluginContainer.className = '.slider-list__plugin';
          pluginContainer.innerHTML = plugin.render(this.options.images);
          this.container.appendChild(pluginContainer);
    
          plugin.action(this);
        });
      }
      getSelectedItem(){
        const selected = this.container.querySelector('.slider-list__item--selected');
        return selected
      }
      getSelectedItemIndex(){
        return Array.from(this.items).indexOf(this.getSelectedItem());
      }
      slideTo(idx){
        const selected = this.getSelectedItem();
        if(selected){ 
          selected.className = 'slider-list__item';
        }
        let item = this.items[idx];
        if(item){
          item.className = 'slider-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);  
      }
      addEventListener(type, handler){
        this.container.addEventListener(type, handler);
      }
      start(){
        this.stop();
        this._timer = setInterval(()=>this.slideNext(), this.cycle);
      }
      stop(){
        clearInterval(this._timer);
      }
    }
    
    const pluginController = {
      render(images){
        return `
          <div class="slide-list__control">
            ${images.map((image, i) => `
                <span class="slide-list__control-buttons${i===0?'--selected':''}"></span>
             `).join('')}
          </div>    
        `.trim();
      },
      action(slider){
        const controller = slider.container.querySelector('.slide-list__control');
    
        if(controller){
          const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
          controller.addEventListener('mouseover', evt => {
            const idx = Array.from(buttons).indexOf(evt.target);
            if(idx >= 0){
              slider.slideTo(idx);
              slider.stop();
            }
          });
    
          controller.addEventListener('mouseout', evt => {
            slider.start();
          });
    
          slider.addEventListener('slide', evt => {
            const idx = evt.detail.index
            const selected = controller.querySelector('.slide-list__control-buttons--selected');
            if(selected) selected.className = 'slide-list__control-buttons';
            buttons[idx].className = 'slide-list__control-buttons--selected';
          });
        }    
      }
    };
    
    const pluginPrevious = {
      render(){
        return `<a class="slide-list__previous"></a>`;
      },
      action(slider){
        const previous = slider.container.querySelector('.slide-list__previous');
        if(previous){
          previous.addEventListener('click', evt => {
            slider.stop();
            slider.slidePrevious();
            slider.start();
            evt.preventDefault();
          });
        }  
      }
    };
    
    const pluginNext = {
      render(){
        return `<a class="slide-list__next"></a>`;
      },
      action(slider){
        const previous = slider.container.querySelector('.slide-list__next');
        if(previous){
          previous.addEventListener('click', evt => {
            slider.stop();
            slider.slideNext();
            slider.start();
            evt.preventDefault();
          });
        }  
      }
    };
    
    const slider = new Slider('my-slider', {images: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png',
         'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg',
         'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg',
         'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'], cycle:3000});
    
    slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
    slider.start();
    

    以上render方法都是用于生成HTML字符串,每个模块都是一个对象,action中是行为方法

总结

  • 组件设计的原则:

    • 封装性
    • 正确性
    • 扩展性
    • 复用性
  • 实现组件的步骤:

    1. 结构化设计
    2. 展示效果
    3. 行为设计

过程抽象

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

image.png 多次点击会出现以上报错,其原因是:

在回调函数中开启定时器,在2s后删除元素;但是多次点击后,多次执行回调函数,此时会由多个定时器,但是元素在2s后已经删除,此时就找不到元素,就会出现报错

高阶函数

  • 以函数作为哦参数
  • 以函数作为返回值
  • 常用于作为函数修饰器
once 执行一次
function once (fn) {
    return function(...args) {
        if(fn){
            const ret = fn.apply(this,args)
            fn = null
            return ret
        }
    }
}
节流函数

控制每次操作的频率

function throttle(fn, time = 500){
  let timer;
  return function(...args){
    if(timer == null){
      fn.apply(this,  args);
      timer = setTimeout(() => {
        timer = null;
      }, time)
    }
  }
}
防抖

操作完成一定时间后执行

function debounce(fn, dur){
  dur = dur || 100;
  var timer;
  return function(){
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, dur);
  }
}

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

批量操作

// 是否可迭代
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]);
  }
}

示例

多个交通灯状态交换

方法一:

其实还是setTimeout的嵌套,代码精简一点

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

方法二:

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

方法三: 简单、可读性强

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

洗牌

错误写法:

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

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

这样的写法看似没错,但是根据数据统计发现,分布并不均匀

image.png

正确方法:

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

我们从牌堆中,随机抽出一张牌,将其交换到最后一个;然后牌堆(除去最后一张牌)中随机抽出一张牌,交换到倒数第二个,以此类推...

分红包

方法一: 每次都会将红包中最大的金额取出,将其分成不相等的两份;然后再取出最大金额并分成两份,循环至达到一定的份额...

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