这是我参与「第四届青训营 」笔记创作活动的的第4天,老师带我们深入讲解了JS的进阶用法,作为前端开发的三大基石之一,它的作用是毋庸置疑的。
一、前言
课程目标:
- 怎样写好js代码
- 写程序的共性问题与原则问题
书籍推荐:
- 犀牛书
- 红宝书
- 《JavaScript The Good Parts》
写好JS的原则:
-
各司其责:让HTML、CSS、JS职能分离
- HTML负责结构,CSS负责表现(样式) ,JS负责行为
-
组件封装:好的UI组件具备正确性、拓展性、复用性
-
过程抽象:应用函数式编程思想
二、JS三大原则
各司其职
-
版本一
// 使用JS负责表现,没有各司其责 const btn = document.getElementById('modeBtn')l btn.addEventListener('click', (e)=>{ const body = document.body; if (e.target.innerHTML === 'Sun Mode'){ body.style.backgroundColor = 'black'; body.style.color = 'white'; e.target.innerHTML === 'Moon Mode'; }else { body.style.backgroundColor = 'white'; body.style.color = 'black'; e.target.innerHTML === 'Sun Mode'; } ) -
版本二
const btn = document.getElementById('modeBtn'); btn.addEventListener('click', (e) => { const body = document.body; if(body.className !== 'night') { body.className = 'night'; } else { body.className = ''; } }); -
版本三
<!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"> <link rel="stylesheet" href="./style.css"> <title>深夜食堂</title> </head> <body> <!-- 虚拟checkbox,设置display为none --> <input id="modeCheckBox" type="checkbox"> <div class="content"> <header> <!-- 设置label,for属性值为checkbox,达到实际控制的作用 --> <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 { height: 100%; padding: 10px; transition: background-color 1s, color 1s; } div.pic img { width: 100%; } /* 处理模式切换的checkBox */ #modeCheckBox { display: none; } #modeBtn { font-size: 2rem; float: right; } #modeBtn::after { content: '🌞'; } /* 处于checked状态时,对类名为content的元素,进行样式修改 */ #modeCheckBox:checked + .content { background-color: black; color: white; transition: all 1s; } #modeCheckBox:checked + .content #modeBtn::after { content: '🌜'; }
结论:
- HTML、CSS、JS各司其职
- 应当避免不必要的由JS直接操作样式
- 可以用class来表示状态
- 纯展示类交互寻求零JS方案
组件封装⭐
组件是指Web页面上抽出来一个个包含模板(HTML)、功能(JS)、样式(CSS)的单元。
好的组件具备封装性、正确性、拓展性、复用性
用原生JS写原生电商网站的轮播图,该如何实现?
总结(组件封装的基本方法):
-
组件设计的原则
- 封装性
- 真确性
- 拓展性
- 复用性
-
实现组件的步骤
-
结构设计
-
展现效果
-
行为设计
- API(功能):设计一些接口来操作
- Event(控制流): 自定义事件解耦
-
-
三次重构
- 插件化
- 模板化
- 抽象化(组件框架)
过程抽象⭐
- 用来处理局部细节控制的一些方法
- 函数式编程思想的基础应用
为了能够让"只执行一次"的需求覆盖不同的事件处理,我们可以将需求剥离。这个过程我们称之为过程抽象
下面这段代码是调用过程抽象来实现的防抖效果:
function once(fn){
return function(...args){
if (fn){
const ret = fn.apply(this, args);
fn = null;
return ret;
}
}
}
const foo = once(()=>{
console.log("你好")
})
foo();
foo();
foo(); // 实际只调用了一次
高阶函数HOF:
function HOFO(fn){
return function(...args){
return fn.apply(this, args);
}
}
-
特点
- 以函数作为参数
- 以函数作为返回值
- 常用于作为函数装饰器
-
常用高阶函数
-
Once
function once(fn) { return function(...args) { if(fn) { const ret = fn.apply(this, args); fn = null; return ret; } } } -
Throttle节流
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防抖
<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
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); }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:使奇数行变色
<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');
-
三、高阶函数HOF
为什么使用高阶函数?
首先要理解纯函数和非纯函数,纯函数就是给定输入,可以预计输出的函数,高阶函数就是纯函数的一种
非纯函数是一种具有不可预知性的函数,会降低系统的可维护性
因此,高阶函数的使用可以减少代码中非纯函数的数量
Pure and inpure function
// 纯函数 pure function
function add(x, y){return x+y};
console.log(assert(3+6===9, 'fail'));
// 非纯函数 inpure function
let idx = 0;
function count(){
return ++idx;
}
count();
count();
count();
案例:实现奇数行变色
<div id="app">
<ul>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
</ul>
</div>
-
不使用高阶函数
function setColor(el, color){ el.style.color = color; } // setColor(app, 'red'); function setColors(els, color){ els.forEach(el=>{ setColor(el, color); }) } setColors([...document.querySelectorAll('li:nth-child(2n+1)')], 'skyblue') -
使用高阶函数
function setColor(el, color){ el.style.color = color; } // setColor(app, 'red'); // 使用高阶函数 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 setColors = iterative(setColor); setColors([...document.querySelectorAll('li:nth-child(2n+1)')], 'skyblue') // 下面的代码是说明高阶函数的强大复用性 const addMany = iterative(add); console.log(addMany([1,2,3,4], 5)); // [6, 7, 8, 9]
编程范式 ⭐
Javascript同时支持两种范式,而两种范式又可以分为两类
-
命令式:怎么做
-
面向过程
-
面向对象
-
命令式实现开关切换
switcher.onclick = (evt)=>{ if (evt.target.className === 'on'){ evt.target.className === 'off'; }else { evt.target.className === 'on'; } }
-
-
声明式:做什么(推荐⭐)
-
逻辑式编程
-
函数式编程
-
声明式实现开关切换
#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; }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' );
-
四、JS算法
- 使用场景?
- 效率?
- 风格?
- 约定?
- 设计?
Leftpad事件
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;
}
return str;
}
console.log(leftpad('12', 5, '0'));
代码简洁+效率提升
function leftpad(str, len, ch){
str = "" + str;
const padLen = len - str.length;
if (padLen<=0) return str;
return (""+ch).repeat(padLen) + str;// 调用字符串的repeat方法
}
console.log(leftpad('12', 5, '0'));
案例1.1:数据抽象的交通灯
<ul id="traffic" class="wait">
<li></li>
<li></li>
<li></li>
</ul>
#traffic {
display: flex;
flex-direction: column;
}
#traffic li {
display: inline-block;
width: 50px;
height: 50px;
background-color: gray;
margin: 5px;
border-radius: 50%;
}
#traffic.stop li:nth-child(1) {
background-color: #a00;
}
#traffic.wait li:nth-child(2) {
background-color: #aa0;
}
#traffic.pass li:nth-child(3) {
background-color: #0a0;
}
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);
案例1.2:过程抽象的交通灯
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();
}
}());
案例1.3:异步+函数式编程的交通灯⭐
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();
案例2:判断是否是4的幂
<input id="num" value="24"></input>
<button id="checkBtn">
判断
</button>
// function isPowerOfFour(num) {
// num = parseInt(num);
// while(num > 1) {
// if(num % 4) return false;
// num /= 4;
// }
// return num === 1;
// }
// function isPowerOfFour(num) {
// num = parseInt(num);
// while(num > 1) {
// if(num & 0b11) return false;
// num >>>=2;
// }
// return num === 1;
// }
// 推荐:使用正则匹配,
// function isPowerOfFour(num) {
// num = parseInt(num).toString(2); // 转化为2进制字符串
// return /^1(?:00)*$/.test(num); // 正则表达式匹配
// }
function isPowerOfFour(num){
num = parseInt(num);
// (num & (num - 1)) === 0判断是否为 2的幂
return num > 0 &&
(num & (num - 1)) === 0 &&
(num & 0xAAAAAAAAAAAAA) === 0;
}
num.addEventListener('input', function(){
num.className = '';
});
checkBtn.addEventListener('click', function(){
let value = num.value;
num.className = isPowerOfFour(value) ? 'yes' : 'no';
});
案例3.1:洗牌的错误方式
<div id="app">洗牌-错误写法</div>
<hr/>
<div id="log"></div>
<script>
window.console = JCode.logger(log);
</script>
sort方法并不是两两位置均匀交换的,导致越小的数字,放到序列前面的概率变大
const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function shuffle(cards) {
return [...cards].sort(() => Math.random() > 0.5 ? -1 : 1);
}
console.log(shuffle(cards));
const result = Array(10).fill(0);
for(let i = 0; i < 1000000; i++) {
const c = shuffle(cards);
for(let j = 0; j < 10; j++) {
result[j] += c[j];
}
}
console.table(result);
案例3.2:洗牌的正确方式
<div id="app">洗牌-正确写法</div>
<hr/>
<div id="log"></div>
<script>
window.console = JCode.logger(log);
</script>
算法:这个算法是符合生活实际的,并且是等概率操作的
- 选定一个随机索引
- 让这个索引位置的牌与最后一张牌交换
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;
}
console.log(shuffle(cards));
const result = Array(10).fill(0);
for(let i = 0; i < 10000; i++) {
const c = shuffle(cards);
for(let j = 0; j < 10; j++) {
result[j] += c[j];
}
}
console.table(result);
案例3.3:洗牌之生成器
提升性能
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]); // 洗牌
console.log(result.next().value); // 只取一张牌
console.log([result.next().value,
result.next().value,
result.next().value]); // 取三张牌
案例4.1:分红包-切西瓜法
算法:每次都筛选出最大的一块进行切分,O(m*n)的时间复杂度
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>';
}
案例4.2:分红包-抽牌法
把红包总金额看作从0到99.99的数列,插入9个分隔符
O(n)的时间复杂度,空间复杂度较高 码上掘金 (juejin.cn)
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];
}
}
// 0, 1, 2....9999
// 49 199
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 - 1; i++) {
result.push(pick.next().value);
}
result.sort((a, b) => a - b);
result.push(amount);
for(let i = result.length - 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>';
}