Lesson 4 如何写好 JavaScript
VideoLink: 如何学好 Javascript - 掘金
CoursewareLink: 如何学好 Javascript - 掘金
各司其职
简单说就是
举例说明:写一段 JS,控制一个网页,让它支持浅色和深色两种浏览模式
-
Version One
- Code
-
<!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> <header> <button id="modeBtn">🌞</button> <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> </body> </html> -
body, html { width: 100%; height: 100%; padding: 0; margin: 0; overflow: hidden; } body { padding: 10px; box-sizing: border-box; } div.pic img { width: 100%; } #modeBtn { font-size: 2rem; float: right; border: none; background: transparent; } -
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 = '🌞'; } }); - 通过 Javascript 改变样式,不再是各司其职
-
Version Two
- Code
-
<!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> <header> <button id="modeBtn"></button> <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> </body> </html> -
body, html { width: 100%; height: 100%; max-width: 600px; padding: 0; margin: 0; overflow: hidden; } body { padding: 10px; box-sizing: border-box; transition: all 1s; } div.pic img { width: 100%; } #modeBtn { font-size: 2rem; float: right; border: none; outline: none; cursor: pointer; background: inherit; } body.night { background-color: black; color: white; transition: all 1s; } #modeBtn::after { content: '🌞'; } body.night #modeBtn::after { content: '🌜'; } -
const btn = document.getElementById('modeBtn'); btn.addEventListener('click', (e) => { const body = document.body; if(body.className !== 'night') { body.className = 'night'; } else { body.className = ''; } }); - 用 Javascript 控制元素类, 用类的样式来控制元素
-
Version Three
-
Code
-
<!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: '🌜'; } -
无需 Javascript, 更加的各司其职,但不强求
-
由上面的例子得出的结论:
-
HTML/CSS/JS 各司其职
-
应当避免不必要的由 JS 直接操作样式
-
可以用 class 来表示状态
-
纯展示类交互寻求零 JS 方案
组件封装
组件是指 Web 界面上抽出来一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元。
好的组件具备封装性、正确性、扩展性、复用性。
举例说明: 用原生 JS 写一个电商网站的轮播图
-
结构设计:HTML,轮播图是一个典型的列表结构,我们可以使用无序列表
<ul>元素来实现 -
展示效果:CSS
- 使用 CSS 绝对定位将图片重叠在同一个位置
- 轮播图切换的状态使用修饰符(modifier)
- 轮播图的切换动画使用 CSS transition
-
行为设计:API
- API设计应保证原子操作,职责单一,满足灵活性。
- getSelectedItem()
- getSelectedItemIndex()
- slideTo()
- slideNext()
- slidePrevious()
-
行为设计:控制流
- 使用自定义事件来解耦
-
代码实现:
- Version One: API 的简单实现
-
<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> -
#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); - Version Two:控制流
-
<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> <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> </div> -
#my-slider{ position: relative; width: 790px; height: 340px; } .slider-list ul{ list-style-type:none; position: relative; width: 100%; height: 100%; 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; } .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{ 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(){ 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'; 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 = (currentIdx - 1 + this.items.length) % this.items.length; this.slideTo(previousIdx); } start(){ this.stop(); // 这里使用了箭头函数(()=>{}),箭头函数中的this绑定到它被创建的上下文,通常是定义它的对象,这里的this将绑定到当前对象上。所以,每当setInterval触发时,将会调用this.slideNext()方法。 this._timer = setInterval(()=>this.slideNext(), this.cycle); // 这里直接调用了this.slideNext()方法,并且将其返回值传递给setInterval。由于setInterval需要一个函数作为第一个参数,而不是函数调用的结果,这个语句并不会按预期工作。实际上,这会立即调用this.slideNext()方法,并将返回值(如果有的话)传递给setInterval,而不是在定时间隔触发时调用该方法。 // 正确的写法是第一条语句,使用箭头函数确保在定时间隔内调用this.slideNext()方法,而不是在创建定时器时就立即调用它。 // this._timer = setInterval(this.slideNext(), this.cycle); } stop(){ clearInterval(this._timer); } } const slider = new Slider('my-slider'); slider.start();
=>是箭头函数(Arrow Function)的语法。它是ES6(ECMAScript 2015)中引入的一种新的函数定义方式,用于简化函数的声明和编写。箭头函数有两种形式:
- 单参数、单语句的箭头函数:
(param) => statement;
- 多参数、多语句的箭头函数:
(param1, param2, ...) => { // multiple statements };箭头函数相比传统函数表达式具有以下特点:
- 简洁:由于语法简洁,通常在只有一个参数和单个表达式的情况下使用,可以让代码更加紧凑。
- 没有
this绑定:箭头函数没有自己的this绑定,而是继承外部作用域的this。这在一定程度上解决了传统函数中this作用域问题。- 没有
arguments对象:箭头函数也没有自己的arguments对象,而是继承外部函数的arguments对象。箭头函数的使用场景包括但不限于:
- 简化回调函数的定义。
- 作为简单函数的快捷方式。
- 在需要保留外部作用域的
this时使用,避免this指向出现问题。需要注意的是,由于箭头函数没有自己的
this和arguments对象,所以在一些情况下,传统函数可能更适合,特别是需要动态绑定this或访问arguments对象时。
-
Version Three:插件化,解耦,将控制元素抽取成插件,插件与组件之间通过依赖注入方式建立联系
HTML 与 CSS 同上
class Slider{
constructor(id, cycle = 3000){
this.container = document.getElementById(id);
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = cycle;
}
registerPlugins(...plugins){
plugins.forEach(plugin => plugin(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';
}
const 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);
}
}
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();
});
}
}
const slider = new Slider('my-slider');
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
-
Version Four:解耦,将 HTML 模板化,更易于拓展。
<div id="my-slider" class="slider-list"></div>
CSS 代码同上
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();
-
Version Five:组件框架实现,将通用的组件模型抽象出来
HTML 与 CSS 同上
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 ''
}
}
class Slider extends Component {
constructor(id, opts = { name: 'slider-list', data: [], cycle: 3000 }) {
super(id, opts);
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = opts.cycle || 3000;
this.slideTo(0);
}
render(data) {
const content = data.map(image => `
<li class="slider-list__item">
<img src="${image}"/>
</li>
`.trim());
return `<ul>${content.join('')}</ul>`;
}
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';
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 = (currentIdx - 1 + this.items.length) % 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) {
let controller = slider.container.querySelector('.slide-list__control');
if (controller) {
let buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
controller.addEventListener('mouseover', evt => {
var 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;
let 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) {
let 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) {
let 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', {
name: 'slide-list', data: ['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();
过程抽象
- 用来处理局部细节控制的一些方法
- 函数式编程思想的基础应用
高阶函数
Once
-
操作次数限制
- 一些异步交互
- 一次性的 HTTP 请求
为了能够让“只执行一次”的需求覆盖不同的事件处理,我们可以将这个需求剥离处理,这个过程称为过程抽象。
像这样的函数也称为高阶函数。
高阶函数
- 以函数作为参数
- 以函数作为返回值
- 常用于作为函数装饰器
常用的高阶函数有:
- Once
- Throttle
- Debounce
- Consumer / 2
- Iterative
// 让 fn 函数只能执行一次
function once(fn) {
return function (...args) {
if (fn) {
const ret = fn.apply(this, args)
fn = null
return ret;
}
};
}
// 让 fn 函数在 time 内只能执行一次
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);
}
}
// 每隔 time 时间调用 fn, 实现一个 delay 调用
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]);
}
}
throttle Example
每500毫秒可记录一次
<button id="btn">点我</button>
<div id="circle">0</div>
#circle {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: red;
line-height: 50px;
text-align: center;
color: white;
opacity: 1.0;
transition: opacity .25s;
}
#circle.fade {
opacity: 0.0;
transition: opacity .25s;
}
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 Example:
<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 Example:
<div id="main">
<button id="btn">Hit</button>
<span id="count">+0</span>
</div>
#main {
padding-top: 20px;
font-size: 26px;
}
#btn {
font-size: 30px;
border-radius: 15px;
border: solid 3px #fa0;
}
#count {
position: absolute;
margin-left: 6px;
opacity: 1.0;
transform: translate(0, 10px);
}
#count.hit {
opacity: 0.1;
transform: translate(0, -20px);
transition: all .5s;
}
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 Example:
<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');
编程范式
此处为命令式与声明式,命令式强调How to do 而声明式强调 What to do,所以命令式不可避免的比声明式复杂。
Toggle Example:
<div id="switcher" class="on"></div>
#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;
}
- 命令式
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'
);
虽然看起来声明式的代码更加长,但是声明式的代码更容易维护
例如,为 Toggle 添加一个状态
#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.warn {
background-color: yellow;
}
#switcher.off {
background-color: red;
}
#switcher.on:after {
content: 'on';
color: white;
}
#switcher.warn:after {
content: 'warn';
color: black;
}
#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 = 'warn',
evt => evt.target.className = 'off',
evt => evt.target.className = 'on'
);
而命令式需要 else if 的逻辑判断
后言
要写好JavaScript代码,可以遵循以下几个原则和技巧:
-
熟悉JavaScript语言规范和最佳实践:了解JavaScript的语法、数据类型、运算符、控制流等基础知识,并了解JavaScript编码规范和最佳实践,例如使用严格模式、避免全局变量等。
-
使用合理的命名和注释:给变量、函数、类等起有意义的名字,并添加适当的注释,提高代码的可读性和可维护性。
-
使用模块化和封装:将代码分解成小的模块,每个模块负责特定的功能,并封装相关的变量和函数,避免全局污染和代码重复。
-
避免使用全局变量:全局变量容易引发命名冲突和意外修改,应该尽量避免使用全局变量,可以使用模块化的方式管理变量的作用域。
-
异步编程:JavaScript是单线程的,使用异步编程可以提高程序的性能和响应性。可以使用Promise、async/await、回调函数等方式来处理异步任务。
-
错误处理:对于可能出现错误的代码,需要进行适当的错误处理,避免程序崩溃或者产生不可预料的结果。
-
性能优化:注意代码的性能问题,避免无效的循环、重复的计算等,可以使用工具进行性能分析和优化。
-
测试和调试:编写测试用例并进行测试,使用调试工具来定位和修复问题。
-
学习和掌握常用的JavaScript库和框架:学习并熟练使用常用的JavaScript库和框架,例如React、Vue等,可以提高开发效率和代码质量。
-
不断学习和提升:JavaScript语言和相关技术在不断发展,要保持学习的态度,关注最新的技术动态和最佳实践,不断提升自己的技能水平。
这条很重要,当时第一次学js的我以为学的(至少是学完了)结果很多ES6的特性都没有