Loading... # 跟着月影学JavaScript ## 写好JavaScript的三个原则 1. 各司其责:HTML,CSS,JavaScript三者分离,各自做自己应该做的事。 2. 组件封装:一个UI组件应该拥有正确性,可扩展性以及可复用性。 3. 过程抽象:利用函数闭包,函数式编程的思想。 ## 各司其责 ### 举个例子:深夜食堂 - 一个网页要求实现浅色深色两种模式。 #### 版本一 ```js 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 = '白昼'; } }); ``` - 缺点:由JS直接控制样式,没有使用CSS。 #### 版本二 ```js const btn = document.getElementById('modeBtn'); btn.addEventListener('click', (e) => { const body = document.body; if(body.className !== 'night') { body.className = 'night'; } else { body.className = ''; } }); ``` - 优点:由JS控制元素的class标签最为合适,由CSS设置元素样式。 - 缺点:JS代码可复用性低,一次性代码。 #### 版本三 ```html <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> ``` ```css #modeCheckBox { display: none; } #modeCheckBox:checked + .content { background-color: black; color: white; transition: all 1s; } ``` - 优点:仅使用了HTML和CSS,没有JS代码。 #### 总结 - HTML,CSS,JS各司其责 - 避免不必要的JS直接操作样式 - 可以利用class来表示状态 - 对于纯展示的页面可以尝试仅使用HTML和CSS实现 ## 组件封装 ### 举个例子:轮播图 - 使用原生的JS写一个轮播图 - 结构:HTML ```html <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> ``` 使用HTML确定页面结构,对于轮播图可以使用无需列表实现。 - 表现:CSS ```css #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; } ``` 使用CSS的绝对定位将所有的列表项叠在一起,切换动画使用transition。 - 行为:JS ```js 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); ``` - 轮播图支持五个动作: - getSelectedItem():获取选中的列表项 - getSelectedItemIndex():获取选中的项的下标 - slideTo(idx):跳转到某一项 - slideNext():跳转到下一项 - slidePrevious():跳转到上一项 - 控制流: ```html <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> ``` ```js const detail = {index: idx} const event = new CustomEvent('slide', {bubbles:true, detail}) this.container.dispatchEvent(event) ``` 此处控制流使用了自定义事件解耦合 ### 改进方式 - 插件化: ```js 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(); }); } } ``` 将slider组件通过依赖注入的方式插入插件 - HTML模版化 ```js 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>`; } ... } ``` 将HTML模版化,放入js中进行生成,更易于扩展 - 抽象: ```js class Component{ constructor(id, opts = {name, data:[]}){ this.container = document.getElementById(id); this.options = opts; this.container.innerHTML = this.render(opts.data); } registerPlugins(...plugins){ plugins.forEach(plugin => { const pluginContainer = document.createElement('div'); pluginContainer.className = `.${name}__plugin`; pluginContainer.innerHTML = plugin.render(this.options.data); this.container.appendChild(pluginContainer); plugin.action(this); }); } render(data) { /* abstract */ return '' } } ``` 将组件通用模型抽象出来(组件框架) ### 总结 - 组件设计的原则:封装性,正确性,可扩展性,可复用性 - 实现组件的步骤:结构,表现,行为 - 优化: - 插件化 - 模版化 - 抽象化(组件框架) - 听完月影老师讲这段组件封装再结合之前学习的react框架恍然大悟理解了react的设计理念。 ## 过程抽象 - 在实际项目开发过程中,经常会遇到很多需要大量重复或者大量相似的,并且与实际业务没什么直接关联的功能,例如前端请求次数的限制,限制某个方法只执行一次,防抖操作等等。 - 为了实现这些功能,经常会导致项目中存在大量重复代码,不仅可能会破坏业务逻辑,还大大降低了代码可读性。 - 过程抽象,将那些需要重复的操作抽象出来,利用函数闭包的语法,实现类似装饰器的效果,将业务代码加上这些功能。 ### 例子 - 操作次数限制:一次性的点击操作。 ```js const list = document.querySelector('ul'); const buttons = list.querySelectorAll('button'); buttons.forEach((button) => { button.addEventListener('click', (evt) => { const target = evt.target; target.parentNode.className = 'completed'; setTimeout(() => { list.removeChild(target.parentNode); }, 2000); }); }); ``` 点击一个复选框,两秒钟后消除复选框 这段代码可以实现操作,但是如果用户在还没有消失的时候连续点击多次,就会触发多个click事件,导致在元素已经被删除后继续尝试删除导致报错。 考虑如何能让这个函数只能执行一次。 ### 高阶函数 - 高阶函数指一个函数的参数和返回值都是一个函数的函数 - 向高阶函数中传入一个函数,做一些装饰,再将这个函数返回,通过装饰的行为给函数加上一些功能 #### HOF0(等价函数) - 传入和返回的函数等价 ```js function HOF0(fn) { return function(...args) { return fn.apply(this, args); } } ``` #### 常用高阶函数 ##### Once - 修饰传入的函数使其只能执行一次 ```js function once(fn) { return function(...args) { if(fn) { const ret = fn.apply(this, args); fn = null; return ret; } } } const foo = once(() => { console.log('bar'); }); foo(); foo(); foo(); ``` 可以看到调用了三次经过修饰的foo函数,但是由于第一次执行之后闭包中就将函数设置为了null,因此foo只会执行一次 <img src="https://s2.loli.net/2022/07/26/C8HIm6VBL2KObTo.png" alt="image-20220726151614219" style="zoom:50%;" style=""> ##### Throttle - 节流函数,修饰传入的函数使得其每过一定时间只能执行一次 ```js 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); }); ``` 第一次调用throttle返回的函数时,timer为null,执行一次函数然后设置一个延迟,时间到后再把timer设置为null,也就是说在time的时间之内再次点击按钮,由于timer不为null,因此不会执行函数,这样就实现每time毫秒只能点击一次的操作。 ##### Debounced - 防抖函数,在鼠标不停活动的时候不调用函数,当鼠标活动后停下来一段时间后如果没有移动才触发函数 ```js 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)); ``` 在debounce函数中定义了一个timer,每次调用debounce函数返回的函数时,会清除定时器之后设置一个延时执行,这样如果在延时还没有结束的时候又移动鼠标(触发函数)就会清除定时器,导致上次移动操作没有被执行,直到某次鼠标移动停止并且在dur时间内没有再次移动,小鸟才会移动位置。 <img src="https://s2.loli.net/2022/07/26/uwsDVmAFRovEdIM.png" alt="image-20220726152730706" style="zoom:50%;" style=""> ##### Consumer - 定时执行函数,设置一个时间,可以不断调用某个函数,但是只会定时每隔一段时间执行一次 ```js 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) } } } ``` 当第一次调用函数时,会把函数push到一个数组(队列)中,接着由于是第一次执行,timer为null,因此创建一个定时任务,每隔time时间取出队列一个任务进行执行,如果在此期间再次调用此函数,则会将任务加入队列,又由于timer不是null,就会在队列内等待执行。当队列所有任务都执行结束之后,timer会被置为null,这样下一次再次调用的时候就可以重新创建一个定时任务。 ## 编程范式 - 命令式以及声明式 <img src="https://p2.ssl.qhimg.com/t01cf6fae692ce96bf0.png" style="zoom:50%;" /> - 命令式 ```js let list = [1, 2, 3, 4]; let mapl = []; for(let i = 0; i < list.length; i++) { mapl.push(list[i] * 2); } ``` - 声明式 ```js let list = [1, 2, 3, 4]; const double = x => x * 2; list.map(double); ``` - 命令式更加看重问题是如何一步一步执行的,而声明式更看重语意的表达 - 在JavaScript中,允许使用声明式和命令式两种写法,一般推荐使用声明式写法 ### 举个例子 - Toggle(开关) #### 命令式 ```js switcher.onclick = function(evt){ if(evt.target.className === 'on'){ evt.target.className = 'off'; }else{ evt.target.className = 'on'; } } ``` #### 声明式 ```js 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' ); ``` - 命令式更注重业务是如何实现的,而声明式定义了一个列表,不断循环切换这个列表的状态 - 声明式的优点:当前这个只实现了两个状态开和关的切换,而如果需要更多状态的切换,就只需要在调用toggle的时候添加项就可以了,而命令式如果想添加状态只能添加elseif,相对而言声明式更简洁。 ## 总结 - 经过半节课的JavaScript的学习,了解到如何写好JavaScript,JS的编写理念以及不同编程范式的使用。 - 通过轮播图的案例,学习到插件的使用方法,发现了自己以前写轮播图方式的不足以及不可扩展性。 Last modification:July 26, 2022 © Allow specification reprint Like 如果觉得我的文章对你有用,请随意赞赏