JavaScript从入门到放弃
1如何写好JS?
各司其职。即css就负责样式的部分,HTML就负责结构的部分,JS就负责行为的部分。不要在JS种进行一些修改样式的操作。
2 如何设计复杂UI组件(以一个轮播图为例)
2.1 结构设计
2.1.1 图片结构是一个列表型结构,所以主体用<ul>
2.1.2 使用CSS绝对定位将图片重叠在一个位置
2.1.3 轮播图切换的状态使用修饰符(modifier)
2.1.4 轮播图的切换动画使用css的transition
//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>
//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;
}
2.2 API设计
2.2.1设计原则
API设计的时候需要考虑执行什么样的原子操作,在轮播图种需要考虑以下几个方面:
getSelectedItem()获得选中的元素
getSelectedItemIndex()获取选中的元素是第几个元素,将轮播图下方小圆点与图片对应
slideTo()当前鼠标在这个小圆点时跳到哪个图片
slideNext()右边箭头点击的时候跳到下一页
slidePrevious()左边箭头点击的时候跳到上一页
2.2.2具体实现
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');
setInterval(() => {
slider.slideNext()
}, 3000)
到这里就可以实现轮播图的基本流程
2.3控制流设置
2.3.1 控制结构
//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>
//css
.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;
}
2.3.2 自定义事件
通过自定义事件实现事件的触发
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
2.3.3具体实现
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(){
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();
到这里就可以实现轮播图的功能,但是还有些需要优化的地方。
2.4优化
上面的方式存在的问题是:整个轮播图是由图片、小圆点、左右箭头三部分构成的,这样做会产生的问题是:
1.构造函数太长
2.当需要删除小圆点或左右箭头时需要改动的部分太多
优化方法是:把他们做成插件的方式引入
具体实现
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();
这样就可以把代码的耦合度降低
但是上面这样还存在一个问题就是想删除小圆点或者左右箭头还需要再把对应的HTML结构删除掉,所以需要进一步优化
优化2:改进插件/模板化
具体实现:
class Slider{
constructor(id, opts = {images:[], cycle: 3000}){
this.container = document.getElementById(id);
this.options = opts;
//通过传递的图片对象生成一段HTML代码
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();
HTML代码中就只需要一行代码即可
<div id="my-slider" class="slider-list"></div>
通过上面这样操作就可以只传递数据即可
优化3:组件模型抽象
具体实现:
//抽象出一个通用的组件类
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 = (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){
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();
3.局部细节控制
3.1会出错的情况
3.1.1逐渐消失的方块
给一个方块绑定一个onclick事件,延时2s才开始执行。如果用户在2s内一直点击的话就会报错。只执行一次的解决方案如下
block.onclick = function(evt){
block.onclick = null;
console.log('hide');
evt.target.className = 'hide';
setTimeout(function(){
document.body.removeChild(block);
}, 2000);
};
//在新的浏览器中可以使用以下方法
/**
block.addEventListener('click',function(evt){
block.onclick = null;
console.log('hide');
evt.target.className = 'hide';
setTimeout(function(){
document.body.removeChild(block);
}, 2000);
},{once:true})
*/
3.1.2 异步请求获取数据
当用户点击提交长时间未响应的时候,用户可能会多次点击。如果是支付的场景下会造成用户多次付款。所以这里应该也限制用户只能执行一次
3.2 过程抽象
有很多“只允许执行一次”的函数操作,如何进行统一的抽象?
3.2.1 once函数
抽象成一个新的函数once,他会返回一个新的函数,这样就可以保证函数只执行一次
具体实现
function once(fn){
return function(...args){
if(fn){
let ret = fn.apply(this, args);
fn = null;
return ret;
}
}
}
function foo(idx){
console.log(`I'm called:${idx}`);
}
//这样会使foo函数多次执行
foo(0);
foo(1);
foo(2);
//这样不管foo调用几次就都只能执行一次
foo = once(foo);
foo(3);
foo(4);
foo(5);
3.2.2节流函数
使用场景:监听一个鼠标的移动或者滚轮的滚动执行相关操作的时候,如果不采用节流会导致多次触发,浪费资源
具体实现
function throttle(fn, time = 500){
let timer;
return function(...args){
if(timer == null){
fn.apply(this, args);
timer = setTimeout(() => {
timer = null;
}, time)
}
}
}
//使用节流后可以保证不管用户点击多块都每500毫秒记录一次
btn.onclick = throttle(function(e){
circle.innerHTML = parseInt(circle.innerHTML) + 1;
circle.className = 'fade';
setTimeout(() => circle.className = '', 250);
});
3.2.3防抖函数
使用场景:监听用户的行为但是希望在用户停下来的时候再执行
具体实现
var i = 0;
setInterval(function(){
bird.className = "sprite " + 'bird' + ((i++) % 3);
}, 1000/10);
//防抖
function debounce(fn, dur){
dur = dur || 100;
var timer;
//在用户执行过程中,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));
3.2.4消费者
使用场景:把一些同步的操作变成异步操作的方式。当用户触发了这个事件并不立即执行而是加到一个任务队列中,过一段时间再执行
具体实现
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(x, y){
let sum = x + y;
console.log(sum);
return sum;
}
let consumerAdd = consumer(add, 1000);
let sum = 0;
for(let i = 0; i < 10; i++){
consumerAdd(sum, i);
}
用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)
}
}
}
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)
用户一直点击,右侧数据记录点击次数,但显示的速度跟点击的速度并不一样,因为使用consumer将每次点击触发的事件添加到了队列中,按照规定好的时间去执行,执行的次数与点击的次数是一致的。
3.3 声明式编程(Declarative)与指令式编程(Imperative)
声明式编程关心的是做什么,它是一种逻辑的、函数式的例如:
let list = [1, 2, 3, 4];
//要让list里的每个数据变成原来的两倍,做什么?做double操作
const double = x => x * 2;
list.map(double);
指令式编程关心的是怎么做,它面向的是过程,对象例如:
let list = [1, 2, 3, 4];
//要让list里的每个数据变成原来的两倍,怎么做?用一个for循环来实现
let map1 = [];
for(let i = 0; i < list.length; i++){
map1.push(list[i] * 2);
}
小例子
function add(x, y){
return x + y;
}
function sub(x, y){
return x - y;
}
console.log(add(add(add(1,2),3),4)); //不好!!
console.log([1, 2, 3, 4].reduce(add));
console.log([1, 2, 3, 4].reduce(sub));
使用数组的reduce可以将数组的数据依次执行传进来的函数,但可以进一步优化
function add(x, y){
return x + y;
}
function sub(x, y){
return x - y;
}
function addMany(...args){
return args.reduce(add);
}
function subMany(...args){
return args.reduce(sub);
}
console.log(addMany(1,2,3,4));
console.log(subMany(1,2,3,4));
这样可以实现任意多个数据的操作,但还存在一个问题就是每新增一个功能就要重新定义一个**Many,所以还可以再抽象
function iterative(fn){
return function(...args){
return args.reduce(fn.bind(this));
}
}
const add = iterative((x, y) => x + y);
const sub = iterative((x, y) => x - y);
console.log(add(1,2,3,4));
console.log(sub(1,2,3,4));
使用上面这种方式,当有其他新增操作时就只需要写新增操作的逻辑就可以了。
3.4高阶函数
自身输入函数或返回函数的函数称为高阶函数
3.4.1小例子
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'
);
上面的过程还可以进一步优化,使用生成器。
function * loop(list, max = Infinity){
let i = 0;
//noprotect
while(i < max){
yield list[i++ % list.length];
}
}
function toggle(...actions){
let action = loop(actions);
return function(...args){
return action.next().value.apply(this, args);
}
}
switcher.onclick = toggle(
evt => evt.target.className = 'warn',
evt => evt.target.className = 'off',
evt => evt.target.className = 'on'
);
4总结
如何写好js?
1.各司其职:JavaScript 尽量只做状态管理
2.结构、API、控制流分离设计 UI 组件
3.插件和模板化,并抽象出组件模型
4.运用过程抽象的技巧来抽象并优化局部 API
Web标准:前端的原力
1.Web标准概述
Web标准是构成Web基础、运行和发展的一系列标准的总称。Web标准并不是由一家标准组织制定。
2.Web标准介绍
2.1.国际互联网工程任务组(The Internet Engineering Task Force,简称 IETF)
HTTP/0.9:https://www.w3.org/Protocols/HTTP/AsImplemented.html
HTTP/1.0:增加了head和post方法,HTTP可选的版本号,头部字段描述和响应,状态码
https://tools.ietf.org/html/rfc1945-
HTTP/1.1:
增加了持久连接、强制性的服务器头部、更好的缓存、分块编码 -
The Transport Layer Security (TLS) Protocol Version 1.3 对HTTP的传输进行加密
2.2.ecma
ECMA各版本的存档
https://www.ecma-international.org/publications/standards/Ecma-262-arch.htm
2.3.W3C
CSS
- CSS Containment Module Level 1
- Selectors Level 3
- CSS Fonts Module Level 3
- CSS Basic User Interface Module Level 3 (CSS3 UI)
- CSS Color Module Level 3
- CSS Namespaces Module Level 3
- CSS Style Attributes
- Selectors API Level 1
- Media Queries
- A MathML for CSS Profile
- Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification
- Associating Style Sheets with XML documents 1.0 (Second Edition)
- Document Object Model (DOM) Level 2 Style Specification
DOM
- Server-Sent Events
- Progress Events
- Element Traversal Specification
- Document Object Model (DOM) Level 3 Core Specification
- Document Object Model (DOM) Level 3 Load and Save Specification
- Document Object Model (DOM) Level 3 Validation Specification
- XML Events
- Document Object Model (DOM) Level 2 HTML Specification
- Document Object Model (DOM) Level 2 Style Specification
- Document Object Model (DOM) Level 2 Traversal and Range Specification
- Document Object Model (DOM) Level 2 Views Specification
- Document Object Model (DOM) Level 2 Core Specification
- Document Object Model (DOM) Level 2 Events Specification
Graphics
- Graphics Accessibility API Mappings
- WAI-ARIA Graphics Module
- HTML Canvas 2D Context
- WebCGM 2.1
- Scalable Vector Graphics (SVG) Tiny 1.2 Specification
- Portable Network Graphics (PNG) Specification (Second Edition)
- Mobile SVG Profiles: SVG Tiny and SVG Basic
HTML
- HTML Media Capture
- HTML 5.2
- HTML 5.1 2nd Edition
- Encrypted Media Extensions
- Media Source Extensions™
- Web Storage (Second Edition)
- HTML Canvas 2D Context
- XHTML+RDFa 1.1 - Third Edition
- RDFa Core 1.1 - Third Edition
- RDFa Lite 1.1 - Second Edition
- HTML+RDFa 1.1 - Second Edition
- HTML5 Image Description Extension (longdesc)
- CSS Style Attributes
- Internationalization Tag Set (ITS) Version 2.0
- Mobile Web Best Practices 1.0
- Document Object Model (DOM) Level 2 HTML Specification
- Ruby Annotation
HTTP
Performance
- Trace Context - Level 1
- WebAssembly Core Specification
- WebAssembly JavaScript Interface
- WebAssembly Web API
- High Resolution Time Level 2
- User Timing Level 2
- Performance Timeline
- Page Visibility (Second Edition)
- Navigation Timing
Security
- Web Authentication:An API for accessing Public Key Credentials Level 1
- Web Cryptography API
- Content Security Policy Level 2
- Subresource Integrity
- Cross-Origin Resource Sharing
Web API
- WebAssembly JavaScript Interface
- High Resolution Time Level 2
- Pointer Events
- User Timing Level 2
- WebDriver
- HTML Media Capture
- Indexed Database API 2.0
- Encrypted Media Extensions
- Web Cryptography API
- WebIDL Level 1
- Media Source Extensions™
- Geolocation API Specification 2nd Edition
- Pointer Lock
- Vibration API (Second Edition)
- Web Storage (Second Edition)
- Web Notifications
- HTML5 Web Messaging
- Server-Sent Events
- Indexed Database API
- Metadata API for Media Resources 1.0
- Progress Events
- Performance Timeline
- Page Visibility (Second Edition)
- Touch Events
- Selectors API Level 1
- Navigation Timing
- Element Traversal Specification
2.3.1BOM
BOM(Browser Object Model,浏览器对象模型)HTML5规范中有一部分涵盖了BOM的主要内容,因为W3C希望将JavaScript在浏览器中最基础的部分标准化。
1.window对象,也就是ECMAScript中定义的Global对象。网页中所有全局对象、变量和函数都暴露在这个对象上。
2.location对象,通过location对象可以以编程方式操纵浏览器的导航系统。
3.navigator对象,对象提供关于浏览器的信息。
4.screen对象,保存着客户端显示器的信息。
5.history对象,提供了操纵浏览器历史记录的能力。
2.3.2DOM
DOM(Document Object Model,文档对象模型)是HTML和XML文档的编程接口。DOM表示由多层节点构成的文档,通过它开发者可以添加、删除和修改页面的各个部分。DOM现在是真正跨平台、语言无关的表示和操作网页的方式。
DOM1(DOM Level 1)主要定义了HTML和XML文档的底层结构。DOM2(DOM Level 2)和DOM3(DOM Level 3)在这些结构之上加入更多交互能力,提供了更高级的XML特性。
DOM2和DOM3是按照模块化的思路来制定标准的,每个模块之间有一定关联,但分别针对某个DOM子集。
DOM Core:在DOM1核心部分的基础上,为节点增加方法和属性。
DOM Views:定义基于样式信息的不同视图。
DOM Events:定义通过事件实现DOM文档交互。
DOM Style:定义以编程方式访问和修改CSS样式的接口。
DOM Traversal and Range:新增遍历DOM文档及选择文档内容的接口。
DOM HTML:在DOM1 HTML部分的基础上,增加属性、方法和新接口。
DOM Mutation Observers:定义基于DOM变化触发回调的接口。这个模块是DOM4级模块,用于取代Mutation Events。
2.4 WHATWG
- HTML Living Standard:https://html.spec.whatwg.org/multipage/
- DOM Living Standard:https://dom.spec.whatwg.org/
- Encoding Living Standard:https://encoding.spec.whatwg.org/
- Fetch Living Standard:https://fetch.spec.whatwg.org/
- Stream Living Standard:https://streams.spec.whatwg.org/
- Console Living Standard:https://console.spec.whatwg.org/
前端常用的HTTP知识
前端开发中的数据传输大多是基于 HTTP,了解掌握 HTTP 对于解决前端开发中遇到的问题是否有价值。
HTTP 是一个应用层协议,它的下层协议是 TCP/UDP。
报文格式
请求头
<method> <request-URL> <version>
<headers>
<entity-body>
响应头
<version> <status> <respon-phrase>
<headers>
<entity-body>
请求类型
请求类型 | 描述 |
---|---|
GET | 获取一个资源内容 |
POST | 新增一个资源 |
PUT | 更新资源内容 |
DELETE | 删除资源 |
OPTIONS | 根据返回判断是否有对其请求的权限(做一些跨域请求,看是否符合规范) |
HEAD | 只返回 head,不返回实体内容 |
PATCH | 更新部分内容 |
状态码
状态码 | 描述 |
---|---|
1xx | 请求已接受,需要继续处理 |
2xx | 请求已经正确处理 |
3xx | 重定向 |
4xx | 客户端错误 |
5xx | 服务端错误 |
常见状态码
状态码 | 描述 |
---|---|
101 | 切换协议,如:将 HTTP 协议切换为 WebSocket 协议 |
200 | 成功 |
206 | 返回部分内容,如:大文件下载 |
301 | 永久重定向,如:资源更换路径或改名 |
302 | 临时重定向,如:当前请求需要登录,临时跳转到登录页 |
304 | 资源未修改,不返回实体内容,客户端可直接读取本地缓存内容 |
400 | 错误请求 |
403 | 拒绝执行,如:无对应的访问权限 |
404 | 资源找不到,如:服务器已经删除该资源 |
413 | 请求实体过大,如:服务端限制了上传的文件大小 |
500 | 服务端内部错误,如:数据处理异常导致报错 |
502 | 作为网关或代理服务器时,上游服务器异常 |
504 | 作为网关或代理服务器时,上游服务器处理超时 |
URL
URL的写法是有一定规则的
<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>
http://<host>:<port>/<path>?<query>#<frag>
ftp://<user>:<password>@<host>:<port>/<path>;<params>
Header分类
通用
Date: Tue, 3 Oct 2019 02:16:00 GMT
Connection: close
请求
User-Agent: Mozilla/5.0 (Linux; U; Android 4.0.2; en-us; Galaxy Nexus Build/ICL53F) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30
Accept: /
响应
Server: Nginx
Last-Modified: Thu, 16 Oct 2019 10:15:16 GMT
实体
Content-Type: text/html; charset=utf-8
Content-Length: 100
扩展(自定义)一般用X开头
X-Powered-By: thinkjs-3.0.4
X-Cache: hit
Cookie
HTTP 是无状态的,两次请求之间没有关联性。但实际业务中又有关联性的场景,cookie 可以用来解决这一问题。
请求头
在发送请求时带上cookie信息
GET / HTTP/1.1
Host: m.so.com
Connection: keep-alive
Cookie:__guid=34870781.3073803881376862000
响应头
HTTP/1.1 200 OK
Server: nginx/1.2.9
Date: Wed, 08 Oct 2014 05:59:59 GMT
Connection: keep-alive
Set-Cookie: thinkjs=s4mhqotbdbg9uh917lu8d5bub5; path=/
Content-Encoding: gzip
响应头里可以设置多个Set-Cookie,如下
Set-Cookie: <name>=<value>[; <name>=<value>]... [; expires=<date>][; domain=<domain_name>] [; path=<some_path>][; secure][; httponly][; samesite=<samesite_value>]
Cookie的安全策略
Cookie的属性相关
- path
- domain (hostonly*)
- expires (max-age)
- secure
- httponly
- SameSite
只有当url与这些规则匹配的时候才可以携带cookie
XSS漏洞盗取Cookie,可以设置httponly.这样就不可以通过document.cookie来获取cookie信息
CSRF漏洞,通过设置token/samesite来防止
Session - 服务器侧对应为 Session,基于 Cookie 存放用户信息
- Cookie 有效期为 Session(随浏览器进程退出而失效)
Content-Type
返回的Content-Type:标识当前资源返回的内容是什么类型
请求的Content-Type:标识提交数据的类型
类型
- application/x-www-form-urlencoded
POST http://www.example.com HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
title=test&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3
- multipart/form-data
一般用于上传一些二进制文件的时候
POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"
title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png
PNG ... content of chrome.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--
- application/json
POST http://www.example.com HTTP/1.1
Content-Type: application/json;charset=utf-8
{"title":"test","sub":[1,2,3]}
- text/xml
POST http://www.example.com HTTP/1.1
Content-Type: text/xml
<?xml version="1.0"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value><i4>41</i4></value>
</param>
</params>
</methodCall>
性能优化
- keep-alive
解决原先的时候发送完一次请求连接就关闭了的问题- HTTP 1.0 原本不支持 Keep-Alive,后来扩充了 Connection: Keep-Alive
- HTTP 1.1 默认支持 Keep-Alive,除非显式指明 Connection: close
- HTTP 1.0 原本不支持 Keep-Alive,后来扩充了 Connection: Keep-Alive
- 减少网络传输大小
在响应头中设置压缩方式content-encoding:gzip
在请求头设置能够接收的压缩方式accept-encoding- 主要是文本资源(因为视频图片本身就有压缩,通过js再次压缩并不能减少多少,反而耗费性能)
- 大约减少 60%
- 文件过小不宜压缩(<1K)
- 缓存
-
Last-Modified:通过比对这个值可以知道资源是否更新,如果没有则返回304直接从客户端选取文件即可
-
ETag:是一个编码值,具体怎么编码并没有规定,可以选择自己喜欢的方式
Expires资源的最大有效期,在这个日期之前就使用缓存的文件,在这个日期之后就重新向服务器请求资源
-
指令 | 目的 |
---|---|
Cache-Control:max-stale=<s> | 缓存可以随意提供过期的文件。如果指定了<s>,在这段时间内,文档就不能过期。这使缓存规则放松。 |
Cache-Control:min-fresh=<s> | 至少在未来<s>秒内文档要保持新鲜。这使缓存规则更加严格 |
Cache-Control:mac-age=<s> | 缓存无法返回缓存时间超过<s>秒的文件。这使缓存规则更加严格。除非还同时发送了第一条指令。 |
Cache-Control:no-cache
Pragma:no-cache|除非资源进行了再验证,否则客户端不接受已缓存的资源
Cache-Control:no-store|缓存应尽快熊存储器中删除文档的所有痕迹,因为可能会包含敏感信息
Cache-Control:only-if-cached|只有当缓存中有副本存在时,客户端才会获取一份副本
- Local Storage 可以存一些css 、js通过js本身来控制缓存
- ServiceWorker
拦截里面的一些请求,自己制定一些规则,来控制缓存情况 - http2/http3
协议本身的一些优化
-HTTP2- 二进制传输
- 多路复用
- 头部压缩
- server push(服务器推送)
- HTTP3
- 基于 QUIC 协议(UDP)
HTTP抓包工具 - Wireshark
- Fiddler
- Firebug for Firefox
- Chrome 开发者工具
- IE8+ 自带的开发者工具
HTTP发包工具 - telnet / curl
- Fiddler *
- Tamper for Firefox
- Postman for Chrome *
- Paw for OSX
参考书籍
《图解HTTP》
《HTTP权威指南》
《Web性能权威指南》