原生JavaScript实现日历组件:从构造函数到DOM生成的完整解析

作者:青柠时间:2026-10-23 07:00:32浏览:190

本文详细解析如何使用原生JavaScript和ES6语法从零构建一个日历组件。内容涵盖构造函数初始化、参数校验、星期横轴生成、当前月及前后月份日期的计算逻辑,以及通过封装方法动态创建DOM节点的全过程。旨在帮助开发者理解前端基础实现原理,摆脱框架依赖,提升原生JS编码能力。

原生JavaScript实现日历组件:从构造函数到DOM生成的完整解析

前言

在现代前端开发中,开发者往往频繁使用各类框架,容易忽视原生JavaScript的基础能力。为了重新巩固JS基础,笔者利用业余时间开发了一个日历组件。尽管该组件的功能相对基础,质量尚需打磨,但作为练手项目,其价值在于完整展示原生JS的实现逻辑。

接下来开始具体实现。

项目代码已开源至GitHub:github.com/Zero-jian/p…

以下是日历组件的最终视觉效果。虽然界面设计较为朴素,但本文的核心重点在于JavaScript逻辑的实现,而非UI美化。

原生JavaScript实现日历组件:从构造函数到DOM生成的完整解析

日历组件核心实现思路

关于日历组件的实现思路

  • 设置默认参数
  • 检查节点参数是否传入,否则抛出错误
  • 动态创建显示本日星期几的横轴
  • 动态创建日历的日子
  • 最后添加一点dom动作就好

构造函数与参数初始化

先来看看构造函数内容


constructor(options) {
    let defaluteOptions = {
      element: null, //这是节点
      startOfWeek: 1,
      strings: {
        week: n => {
          let map = {
            0: '周日',
            1: '周一',
            2: '周二',
            3: '周三',
            4: '周四',
            5: '周五',
            6: '周六',
          }
          return map[n];
        },
        templateDay: `<li class="currentMonth">
          <span class="dayLabel">
            <span class="day"></span>
            <span class="unit">日</span>
          </span>
        </li>`
      },
      days: {},
    }
    //赋值默认参数
    this.options = Object.assign({}, defaluteOptions, options);
    //轮番就调用函数动态创建dom
    this.checkOptions()._generateTime()._generateWeekDay()._generateCurrentDay();

在初始化Calendar类对象时,首先设置数值,赋值默认参数,并调用相关方法来动态创建DOM结构。这段代码逻辑清晰,旨在为后续功能奠定基础。

该组件全程采用ES6语法编写,以确保代码符合现代前端开发标准。

参数赋值完成后,依次调用函数链:首先执行**this.checkOptions()**方法,用于校验目标节点是否存在。


checkOptions() {
    //如果节点不存在直接抛出错误
    if (!this.options.element) {
      throw new Error('element is request');
    }
    return this;
  }

接下来获取当天的年月日信息。

作为日历组件,获取当前时间的年月日作为基准参考数据至关重要。


_generateTime() {
    let data = new Date(); //时间
    let year = this.options.days.year = data.getFullYear(); //年份
    let month = this.options.days.month = data.getMonth() + 1; //月份
    let day = this.options.days.day = data.getDate(); //日子
    this.options.days.countDay = 0; //日历总日子数为7*6=42
    this.options.days.noMonth = data.getMonth() + 1; //不变的月份
    this.options.days.noYear = data.getFullYear(); //不变的年份
    return this;
  }

星期横轴与日期生成逻辑

创建星期横轴


  _generateWeekDay() {
    let {
      startOfWeek,
      strings
    } = this.options;
    let calendar = document.querySelector('.calendar');
    let ol = dom.create(`<ol class="weekdays"></ol>`);
    calendar.appendChild(ol);
    let weekIndex = this.createArray(7, startOfWeek).map((day, i) => {
      let li = dom.create(`<li>${strings.week(i)}</li>`);
      //判断是否为今天
      ol.appendChild(li);
    });
    return this;
  }

此处使用封装好的**dom.create**方法,传入HTML模板即可创建节点并返回引用。

**this.createArray()**同样是封装方法,用于生成一个长度为7的数组(对应周一至周日)。随后利用map映射和遍历操作创建对应的DOM节点,并将其添加到document.body中。

至此,星期横轴部分已完成。接下来的核心部分是生成对应星期的具体日期。虽然逻辑并不复杂,但在实现过程中仍遇到了一些细节问题,代码仅供参考。

接下来是重点部分,即创建具体的日期格子。

日期生成分为三个独立部分:上个月的剩余日期、当前月的日期、下个月的起始日期。这三部分分别封装,确保互不干扰。

具体实现代码如下。


  //创建当前月份日子
  _generateCurrentDay() {
    let date = this.options.days;
    let calendar = document.querySelector('.calendar');
    let ol = dom.create(`<ol class="days"></ol>`);
    let getWeek = this._getWeekWeek(date.year, date.month-1, date.day); //星期几
    let getMonth = this._getMonth(date.year, date.month) //月份天数
    let getMonthDay = this._getWeekDay(); //几号
    date.countDay = 0;
    date.countDay += getMonth;
    calendar.appendChild(ol);
    //创建当月日子模块
    let dayIndex = this.createArray(42, this.options.startOfWeek).map((day, i) => {
      let li = dom.create(this.options.strings.templateDay);
      let span = li.querySelector('.dayLabel>.day');
      //判断日历起止,对本月日子进行赋值
      if (i >= getWeek && i <= (getMonth + getWeek)) {
        span.textContent = i - getWeek;
      }

      //判断是否为今天
      if (i == (getMonthDay + getWeek) && date.noMonth == date.month && date.noYear == date.year) {
        li.classList.add('today');
      }
      ol.appendChild(li);
    });
    document.querySelector('h1.date').appendChild(dom.create(`<p data-role="time">${date.year}-${date.month}-${date.day}</p>`));
    this._generatePrevMonth()._generateNextMonth();

  }

生成当前月份日期的逻辑如下:首先创建一个长度为42的数组(6行×7列=42,索引0至41)。获取当月总天数及当月1号是星期几,通过计算确定本月日期在数组中的索引范围,最后通过循环赋值,完成本月日期的生成。

随后处理上个月日期的生成。

具体代码实现如下。


_generatePrevMonth() {
    let date = this.options.days;
    let year = date.year;
    let month = date.month;
    let beginWeek = this._getWeekWeek(year,month-1,1);//本月开始星期
    let countMonth = this._getMonth(year,month-1);//上月月份天数
    let li = document.querySelectorAll('.dayLabel>.day');
    beginWeek == 0 ? beginWeek+= 7 : ''; //如果月份开头为星期日,会出bug,这是防止
    date.countDay += beginWeek;
    this.createArray(beginWeek,this.options.startOfWeek).map((day,i)=>{
      if(i<beginWeek) {
        //上月总天数-本月开始星期几+1+i
        li[i].textContent = countMonth - beginWeek + 1 + i;
      }
    }); 
    return this;
  }

生成上月日期的逻辑:首先获取本月1号是星期几(例如星期三),这意味着前面有星期日、星期一、星期二三个空位。因此创建一个长度为3的数组,计算上月的具体天数,并通过逻辑判断进行赋值。

最后处理下个月日期的生成。

相关代码实现如下。


//创建下个月日子
_generateNextMonth() {
    let date = this.options.days;
    let year = date.year;
    let month = date.month;
    let beginWeek = this._getWeekWeek(year,month,1);//开始星期
    let countMonth = this._getMonth(year,month+1);//下月月份天数
    let li = document.querySelectorAll('.dayLabel>.day');
    //data.countDay统计了上月和本月的日子数总量,直接减去即可
    this.createArray(42-date.countDay , this.options.startOfWeek).map((day,i)=>{
      li[date.countDay+i].textContent = i+1;
    });
  }

该部分逻辑较为简单:用总格子数(6×7=42)减去上月天数和本月天数,剩余的位置即为显示下个月日期的格子。

以下是封装好的辅助代码部分。


//dom.create()调用
let dom = {
  create(html) {
    let template = document.createElement('template');
    template.innerHTML = html;
    return template.content.firstChild;
  }
}

  //this.createArray()调用
  //创建数组节点
  createArray(length, fill) {
    let array = Array.apply(null, {
      length: length
    }).map(() => fill);
    return array;
  }

交互动作与总结

动作切换部分

日期切换功能的实现相对简单,直接参考代码即可理解。


   //上一个月
  previousMonth() {
    // this.options.days.month -= 1;
    this.changeMonth('prev');
  }

  //下一个月
  nextMonth() {
    // this.options.days.month += 1;
    this.changeMonth('next');
  }

  //回到今天
  resetMonth() {
    // this._generateTime();
    this.changeMonth('defalut');
  }

  //封装月份dom
  changeMonth(status) {
    let date = this.options.days;
    switch(status) {
      case 'prev': {
        --date.month < 1 ? date.year-- ? date.month = 12 : '' : '';
        break;
      }

      case 'next': {
        ++date.month > 12 ? date.year++ ? date.month = 1 : '' : '';
        break;
      }

      case 'defalut': {
        this._generateTime();
        break;
      }
    }
    //移除节点
    this._generateCalendar();
    //重新添加节点
    this._generateCurrentDay();
  }

整个日历组件的开发流程大致如此。通过完整编写该组件,笔者对前端思维逻辑有了更深的理解。当然,该组件的代码仍有封装和完善的空间。

由于组件功能较为简单,剩余的高级功能留给读者自行扩展。

这是笔者首次撰写技术文章,熬夜完成,灵感突至便未休息。预计次日上班可能会因疲劳而打瞌睡。

笔者从业经验近一年,属于初学者阶段。代码中若存在错误或不足,恳请各位前辈指正。

总结

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对本站的支持。

作者最新文章