2025-12-类、观测与分页器
整个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
- @media 主要是和css优化结合起来练习的,合并同类项很好玩;
-webkit-scrollbar和scrollbar-width系列css,这个还挺麻烦的,火狐没办法用webkit只能用scrollbar,但这个优先级很高而且可以改的东西很少;grid练习;animation属性练习;
小组件
一开始想把手机端做成分页的,就去搜了分页怎么做,实际写了才发现原来分页多一个功能,多一个使用体验设计就多许多数据操作。
分页的显示分为两部分,一部分是页面内容渲染,一部分是分页器渲染:
- 页面渲染部分:对数据数组进行切割,渲染切割后的数组;
- 分页器渲染部分:根据当前页举例1和总页码数的远近渲染不同的显示效果。
基础分页器的显示效果可以分为以下三种情况:
- 【上一页(disable)】1 ... 页码数 最后一页【下一页】;
- 【上一页】1...页码数...最后一页【下一页】;
- 【上一页】1 页码数...最后一页【下一页(disable)】;
三个类型的组件:
- 上一页,下一页按钮,第一页和最后一页时分别不可点击;
- 第一页,最后一页;
- 页码数:想要显示的页码数——分为当前页,当前页左右两边空余出来的剩下页码数,一共三段。
我们会设定以下几种变量:
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()//这个是渲染分页器的函数,在这里调用就是同步二者的渲染
}
然后是分页器渲染,根据分页器有三个类型的组件,我们会写三个函数,一个是页码部分,一个是上下页按钮部分,一个是组装所有内容的函数。
这里着重介绍一下currentPages和maxShowPages 的作用,如下图所示,绿色括弧内是maxShowPages = 5在currentPages(棕色页码)变化时的移动,每次点击currentPages,实际上是带动了五个maxShowPages 的变化,我们需要确定的是当前点击页左右显示情况。
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);
}