witch@&*weaves

2025-12-类、观测与分页器

yyFBbq.png

整个12月都在写音乐库,写小项目还是很好玩的。

你原来是这个

映射 = map

表示一种数据结构,一个集合,存储键值对的集合,键与值之间的关系是映射关系。

arguments与剩余参数

arguments原来是一个关键字,不是一个字面量,它是一个类数组对象,每一个函数都有arguments,即使没有定义也拥有一个arguments[0]

《探索ES6》说:

剩余参数可以完全替换 JavaScript 臭名昭著的特殊变量 arguments。它们的优点是始终是数组

好吧我才学会 arguments它就已经臭名昭著了!剩余参数比arguments多的好处就是不需要Array.from就可以返回真数组,剩余参数是剩余运算符...加上字面量(形参)构成的,这样构成的剩余参数含义是:包含剩下所有实际参数的数组。

function oldWay() {
    // 需要转换才能用数组方法
    const args = Array.from(arguments)
    const sum = args.reduce((a, b) => a + b, 0)
    return sum
}

console.log(oldWay(1, 2, 3, 4))  // 10

function newWay(...args) {  // args已经是真数组
    const sum = args.reduce((a, b) => a + b, 0)
    return sum
}

所以和arguments还是有很大不同的,只是使用剩余参数方法表示的参数数组更具有灵活性,使用也更方便,也能在箭头函数里使用。

练习与新技巧

class类

其实感觉不太适合我这个小项目,但练习嘛。

class Fruit{
	constructor(name,fruit,options){
		this.name = name;
		this.fruit = fruit;
		this.config = {
			number: options.number,
			state: options.state,
		}
	}
	init(){
		console.log(`${this.name}吃了${this.config.number}${this.fruit},它们是${this.config.state}的`)
	}
}
const umi = new Fruit('Umi','苹果',{number:3,state:'腐烂'})
umi.init()
//Umi吃了3个苹果,它们是腐烂的

options传值疑问

模拟命名参数和配置对象都是利用了映射关系,对参数进行集中管理。

模拟命名参数: 一般来说人们在调用函数时传参用的都是位置映射,比如:

function dog(name,color){
	alert(`它是${name},毛色是${color}`)
}
dog('Buley','blue')//它是Buley,毛色是blue

第一个传入的'Buley'就是name,第二个传入的'blue'就是color。

但是还有一种方法,也就是通过名称来执行映射,在调用函数时给实参命名,与函数内的形参进行映射,这里顺序就不重要了。

const params = {color: 'blue', name: 'Buley'};
function dog({name,color}){
	//相当于
	// const name = params.name;
	// const color = params.color;
	alert(`它是${name},毛色是${color}`)
}
dog(params)//它是Buley,毛色是blue

js不支持命名参数,所以这里是用了ES6的解构赋值来模拟命名参数,对参数解构再进行匹配传参。

这样做的好处是,可以只传入自己想传入的值,经常与默认值结合使用,这次小项目里我也有用这个方法:

配置对象是把一些零散的参数全部集中到一个对象参数里:

class Fruit{
	constructor(number,state){
			this.number = number
			this.state = state
	}
}
//改为
class Fruit{
	constructor(config){
		this.config = {
			number: config.number,
			state: config.state,
		}
	}
}

在上面类中,属性命名阶段对options参数与config参数内的属性进行了绑定,options.number指option的number属性,第一次进行调用时option便成了:

options = {
	number: 3,
	state:'腐烂'
}

接下来会发生对象之间的传参:

this.config = {
	number: options.number,
	state: options.state,
}

//此刻options对象已被传值

options.number = 3
options.state = '腐烂'

//所以
config = {
	number: 3,
	state:'腐烂'
}

如此便实现了传参。

节流函数

用来在改变窗口大小时只调整一次卡片filter。

节流和防抖函数用来控制函数执行的频率,防抖是函数调用后到达规定时间再执行,如果中间持续触发函数,每次都会重新计算时间,直到最后一次触发,函数到达规定时间被调用;节流是无论如何只会在规定时间内触发一次函数,如果设定规定时间为一分钟,一分钟内无论触发多少次也只执行一次函数。

非常顾名思义,所以使用场景也非常清晰,一个是塔防一个是经济。

主要是使用setTimeout()来实现,要调用的函数在setTimeout里执行,所以会使用apply重新绑定执行上下文的this指向,避免其因为回调函数丢失this上下文指向全局。而此函数逻辑主要是数据驱动的原理,通过创建变量timer来保存闭包环境,判定timer的值来判定函数的执行状态。

防抖,防抖比节流多的一个操作是clearTimeout(timer),用来清除上一次调用里的返回值。

funciton debounce(func,delay){
	let timer = null //初始化
	return function(){ //  //这里也可以使用剩余参数(...args)
		let context = this
		let args = arguments //  //用了剩余参数的话这一步就不需要了,因为剩余参数已经收集了所有的参数
		clearTimeout(timer) //timer再次初始化,清除可能已经有的延迟进程
		timer = setTimeout( //timer赋值为构造函数,使func获得延迟状态
			()=>{
				func.apply(context,args)//执行函数
				timer = null //更新状态,显式表示定时任务结束
			},delay
		)
	}

}

节流,节流不需要clearTimeout(timer),但节流需要判断函数有没有执行过,也就是判断闭包环境变量timer有没有被赋值过。

function throttle(func,delay){
	let timer = null
	return function(...args){
		let context = this
		if(timer)return
		timer = setTimeout( 
			()=>{
				func.apply(context,args)
				timer = null 
			},delay
		)
	}

}

Intersection Observer API

靠这个搞懂了API的使用思路,有种红警地图编辑器的感觉,默了几遍到现在又有点忘了,还是不太熟。

好像是很新的API,用来代替原来监测鼠标滚动值触发函数的策略,现在只需要设置一个观测器,观测这个观测器与上级容器(或者设定好的容器)相交程度多少就行了,这种对比取代具体数值的方法真的很现代啊。

众所周知API是方法和属性的集合,Intersection Observer API就拥有一个IntersectionObserver()构造器来构造函数,IntersectionObserver()本身便是一个原型对象,此对象拥有两个参数,一个参数是对交叉上级容器的规定options对象(root,rootMargin,threshold),一个参数是回调函数,此回调函数用来规定观测器触发后的行为,而它返回的实例便是IntersectionObserver实例对象,此对象继承构造器的所有方法和属性,拥有四个方法来控制观测器观测开闭和停止,以及返回值。

我们要做的就是先设定好产品内部零件,再去使用产品,因为我们可能同时使用很多数量的同一产品,所以我们会把所有产品放到一个数组内,在回调函数里将数组作为参数,遍历每一个元素修改零件。

下面是一个懒加载实现。

const observe = new IntersectionObserver(
(entries,observe)=>{
	for(let entry of entries){
        if(entry.isIntersecting){
          const img = entry.target
          img.src = img.dataset.url
        }
},
{
	rootMargin:'50px',
}
)

const lazy = document.querySelectorAll('img')//

Array.from(lazy).forEach(
        (item)=>{
          observe.observe(item)
        }
      )

ES6的解构赋值

这个属于是看明白了但还没怎么用过,只在节流函数里用过数组参数合并[...args],类的函数里引用constructor里的参数{args1,args2} = this

一些CSS

小组件

一开始想把手机端做成分页的,就去搜了分页怎么做,实际写了才发现原来分页多一个功能,多一个使用体验设计就多许多数据操作。

分页的显示分为两部分,一部分是页面内容渲染,一部分是分页器渲染:

基础分页器的显示效果可以分为以下三种情况:

  1. 【上一页(disable)】1 ... 页码数 最后一页【下一页】;
  2. 【上一页】1...页码数...最后一页【下一页】;
  3. 【上一页】1 页码数...最后一页【下一页(disable)】;

三个类型的组件:

  1. 上一页,下一页按钮,第一页和最后一页时分别不可点击;
  2. 第一页,最后一页;
  3. 页码数:想要显示的页码数——分为当前页,当前页左右两边空余出来的剩下页码数,一共三段。
yZapZQ.png

我们会设定以下几种变量:

pageSize //每页显示多少个(假如要显示图片)图片

totalItems //数组长度,也就是总图片数

totalPages //总页数,通过totalItems/pageSize计算得到

currentPages //当前页,默认是1

maxShowPages //想要除开头和结尾显示的最大页数(包括...)

fullData //也就是存储图片信息的数组,这里设定拥有存图片链接的属性imgLink

首先页面渲染部分:

function renderDate(){
//我们会先计算当前页想要渲染数据的起始索引,和结束索引,需要用到变量currentPage和pageSize

const startIndex = (currentPage - 1) * pageSize
const endIndex = starIndex + pageSize

//再切割图片数组,获得要渲染的新数组

const currentData = fullData.slice(startIndex, endIndex)

//下面就是操纵dom进行渲染了
//先清空一下容器

dataContainer.innerHTML = '';

// 渲染新数据
currentData.forEach(item => {
  const div = document.createElement('div');
  div.className = 'data-item';
  div.textContent = item.content;
  dataContainer.appendChild(div);//dataContainer是一个dom变量
})

renderPaginator()//这个是渲染分页器的函数,在这里调用就是同步二者的渲染
}

然后是分页器渲染,根据分页器有三个类型的组件,我们会写三个函数,一个是页码部分,一个是上下页按钮部分,一个是组装所有内容的函数。

这里着重介绍一下currentPagesmaxShowPages 的作用,如下图所示,绿色括弧内是maxShowPages = 5currentPages(棕色页码)变化时的移动,每次点击currentPages,实际上是带动了五个maxShowPages 的变化,我们需要确定的是当前点击页左右显示情况。

yZfvot.png
function getPaginationPages(){
//我们会先创建一个数组变量,来存要渲染的页码字符串
//接下来就是根据不同情况来判定往数组里按顺序push什么样的页码字符串内容
let arr = []

//再计算第二种情况时currentPage两边会各有多少页

const side = Math.floor((maxShowPages - 2) / 2) 
//-2是因为第一页和最后一页恒定,而...也会占了两个名额

//如果总页数比最大显式页少那就不需要辨析情况了,直接渲染所有数字。
if (totalPages <= maxShowPages + 2) {
  for (let i = 1; i <= total; i++)arr.push(i)
  return arr
}

//计算当前页左右的起始索引,和结束索引,控制数字变量

let start = currentPage - side
let end = currentPage + side

//两个判定条件变量,判定当前页与左右顶点的距离情况。
const isNearStart = currentPage <= maxShowPages - 1
const isNearEnd = currentPage >= totalPages - maxShowPages + 2

if (isNearStart) {//开头
  for (let i = 1; i <= maxShowPages; i++) {
	arr.push(i)
  }
  arr.push('...')//
  arr.push(totalPages)
} else if (isNearEnd) {//结尾
  arr.push(1)
  arr.push('...')
  for (let i = totalPages - maxShowPages + 1; i <= totalPages; i++) {
	arr.push(i)
  }
} else {//中间
  arr.push(1)
  arr.push('...')
  for (let i = start; i <= end; i++) {
	arr.push(i)
  }
  arr.push('...')
  arr.push(totalPages)
}
return arr;

  

      }

    }
}

dom操作,设定按钮创建样式等规则

createPageButton(text, isActive) {

const button = document.createElement('button');
button.textContent = text;
//这里就是true就可以的意思,不传值,也就是没条件为真
if (isActive) {
  button.style.backgroundColor = '#c0855e'
  button.style.color = 'white'
}
//这里就是增加条件
if (text === '上一页' && currentPage === 1) {
  button.disabled = true
} else if (text === '下一页' && currentPage === totalPages) {
  button.disabled = true
}

return button;
}

组装!

renderPaginator() {
paginationContainer.innerHTML = ''; // 清空旧分页控件

//创建上一页按钮
const prevButton = createPageButton('上一页', currentPage > 1)
prevButton.className = 'prev-button'
prevButton.addEventListener('click', () => {
  if (currentPage > 1) {
	currentPage--;
	renderData();
  }
});
paginationContainer.appendChild(prevButton);


//创建页码按钮
const pages = getPaginationPages()
pages.forEach(item => {
  if (item === '...') {//字符串为...的情况
	const span = document.createElement('span')
	span.textContent = '...'
	paginationContainer.appendChild(span)
  } else {
	const pageButton = createPageButton(item, item === currentPage)
	pageButton.addEventListener('click', () => {
	  config.currentPage = item
	  renderData()
	})
	paginationContainer.appendChild(pageButton)
  }
})



//创建下一页按钮
const nextButton = createPageButton('下一页', currentPage < totalPages());
nextButton.className = 'next-button'
nextButton.addEventListener('click', () => {
  if (currentPage < totalPages()) {
	currentPage++;
	renderData();
  }
});
paginationContainer.appendChild(nextButton);

}

#weekly