# JS 的 Math 与 Date 对象,手写活动倒计时、日历组件

TIP

从本节内容我们开始学习实际开发中常用的 Math 与 Date 对象,以及在项目实践中的应用开发等

# 一、Math 对象

TIP

  • Math 是一个内置对象,它拥有一些数学常数属性和数学函数方法。
  • 与其他全局对象不同的是,Math 不是一个构造器。即不能使用 new 关键字
  • Math 的所有属性与方法都是静态的

以下是 Math 对象上的一些常用的方法和属性

属性 说明
Math.PI Math.PI 表示圆周率π,即一个圆的周长与直径的比例,约为 3.14159
方法 说明
Math.pow(x,y) 返回xy次幂
Math.sqrt(x) 返回x的平方根
Math.ceil(x) 向上取整,返回大于等于x的最小整数
Math.floor(x) 向下取整,返回小于等于x的最大整数
Math.round(x) 返回x经过 4 舍 5 入后得到的最接近的整数
Math.max(a,b,c,[..]) 返回给定数值中的最大数
Math.min(a,b,c,[..]) 返回给定数值中的最小数
Math.abs() 返回一个数字的绝对值
Math.random() 生成一个(0-1)之间的随机数
Math.atan2 返回从原点(0,0)(x,y) 点的线段与 x 轴正方向之间的平面角度(弧度值)

# 1、Math.PI 属性

TIP

Math.PI 表示圆周率π,即一个圆的周长与直径的比例,约为 3.14159

console.log(Math.PI); // 3.141592653589793

# 2、Math.pow(x,y)

TIP

Math.pow(x,y) 返回 x 的 y 次幂

console.log(Math.pow(2, 3)); // 8  2的3次幂为8

# 3、Math.sqrt(x)

TIP

返回x的平方根

console.log(Math.sqrt(4)); // 2
console.log(Math.sqrt(9)); // 3

# 4、Math.ceil

TIP

向上取整,返回大于等于 x 的最小整数

console.log(Math.ceil(2.2)); // 3
console.log(Math.ceil(2.0001)); // 3
console.log(Math.ceil(-2.1)); // -2
console.log(Math.ceil(0.909)); // 1

# 5、Math.floor

TIP

向下取整,返回小于等于 x 的最大整数

console.log(Math.floor(2.2)); // 2
console.log(Math.floor(2.0001)); // 2
console.log(Math.floor(-2.1)); // -3
console.log(Math.floor(0.909)); // 0

# 6、Math.round

TIP

返回x经过四舍 5 入后得到的最接近的整数,只需要看 x 的小数部分与 0.5 的关系

特别注意:当x是负数是,如果小数部分是 0.5,则返回值为x的整数部分

console.log(Math.round(10.49)); // 10
console.log(Math.round(10.5)); // 11
console.log(Math.round(-10.49)); // -10
// 特殊情况,记下
console.log(Math.round(-10.5)); // -10
console.log(Math.round(-10.51)); // -11

# 7、Math.max

TIP

返回给定数值中的最大数,如果任一参数不能转换为数值,则返回 NaN

如果没有提供参数,返回 -Infinity

console.log(Math.max(1, 2, 3, 7)); // 7
console.log(Math.max()); // -Infinity
console.log(Math.max(1, 2, "a")); // NaN

求数组中的最大值

var arr = [1, 20, 5, 6, 3, 9, 10];
var maxValue = Math.max.apply(null, arr);
console.log(maxValue);

# 8、Math.min

TIP

返回给定数值中的最小数,如果任一参数不能转换为数值,则返回 NaN

如果没有提供参数,返回 Infinity

console.log(Math.min(1, 2, 3, 7)); // 1
console.log(Math.min()); // -Infinity
console.log(Math.min(1, 2, "a")); // NaN

Math.maxMath.min常用于裁剪值

比如

元素的 left 值只能在0 ~ 500之间,则我们可以利用Math.maxMath.min来对值做裁剪

var left = 300;
left = left < 0 ? 0 : left;
left = left > 500 ? 500 : left;
console.log(left);

// 利用 Math.max和Math.min来实现
left = Math.max(left, 0);
left = Math.min(500, left);
console.log(left);

# 9、Math.abs

TIP

返回一个数字的绝对值,如果不能转换的,则会转换为 NaN,能转的都会转换成对应的数字

console.log(Math.abs(-1)); // 1
console.log(Math.abs(-1.2)); // 1.2
console.log(Math.abs(null)); // 0
console.log(Math.abs(true)); // 1
console.log(Math.abs(false)); // 0
console.log(Math.abs(undefined)); // NaN
console.log(Math.abs([-3])); // 3
console.log(Math.abs([2, -3])); // NaN
console.log(Math.abs({})); // NaN

# 10、Math.random

TIP

  • 返回一个 0 ~ 1 之间的随机数,不包括 0 和 1
  • 返回指定范围[a,b]之间的随机整数公式,包括 a 和 b
Math.random() * (b - a + 1) + a;
// 随机生成 0-6之间的随机整数,包括0和6
var n = Math.floor(Math.random() * 7);

// 随机生成 [2,6]之间的随机整数
var n = Math.floor(Math.random() * (6 - 2 + 1) + 2);

// 随机生成[-5,5]之间的整数
var n = Math.floor(Math.random() * (5 - -5 + 1) - 5);

# 11、Math.atan2

TIP

  • 返回从原点(0,0)(x,y)点的线段与 x 轴正方向之间的平面角度(弧度值
  • 假设圆的中心点为原点坐标(0,0),即从原点到坐标(x,y) 的线段与 x 轴正方向之间的角 θ 是一个负角(逆时针方向)
  • 旋转角度的正负,是由 y 值决定的,y < 0得到负角,y > 0得到正角

image-20221215222607148

Math.atan2(y, x); // 第一个参数是y坐标,第二个参数是x坐标

Math.atan2(-1, 1); // -0.7853981633974483

因为

Math.atan2(y,x)得到的是对应的弧度值,所以我们还需要将弧度值转换为对应的角度值

弧度值转换成对应角度值的转换公式

  • 角度 = 弧度 * (180/π)
  • 角度 = 弧度 / (π/180)
var rad = Math.atan2(-1, 1);
var deg = rad * (180 / Math.PI);
console.log(deg); // -45

var rad2 = Math.atan2(1, -1);
var deg2 = rad2 * (180 / Math.PI);
console.log(deg2); // 135

# 12、案例:元素跟随鼠标旋转

当鼠标上旋转时,元素也会跟着一起旋转。

GIF2022-12-1522-37-19

# 12.1、实现原理

image-20221215223615956

我们以元素的中心点坐标为原点坐标(0,0)来计算鼠标旋转的角度

  • 我们需要获取到元素中心点与浏览器左边和上边的距离
var box = document.querySelector(".box");
// box中心点与浏览器左边和上边距离
var centerX = box.offsetLeft + box.offsetWidth / 2;
var centerY = box.offsetTop + box.offsetHeight / 2;
  • 然后获取到鼠标与浏览左边和上边的距离
document.onmousemove = function (e) {
  var clientY = e.clientY;
  var clientX = e.clientX;
};
  • 最后用鼠标对应位置减去元素中心点与浏览器对应位置,得到的 x,y 这样元素的中心点就相当于是(0,0)原点坐标,(x,y)就是Math.atan2(y,x)方法对应的 x,y
// 以元素的中心点为(0,0)坐标,来计算弧度值,最后转换为对应角度值
x = clientX - centerX;
y = clientY - centerY;

var rad = Math.atan2(y, x); // 弧度值
var deg = rad * (180 / Math.PI); // 将弧度值转换为对应角度制

# 12.2、完整版源码

<style>
  .box {
    width: 200px;
    height: 200px;
    background-image: linear-gradient(to right, khaki, skyblue);
    margin: 200px;
    /* transform-origin: top left; */
  }
</style>

<div class="box"></div>
<script>
  var box = document.querySelector(".box");
  // box中心点与浏览器左边和上边距离
  var centerX = box.offsetLeft + box.offsetWidth / 2;
  var centerY = box.offsetTop + box.offsetHeight / 2;

  // onmousemove事件
  document.onmousemove = function (e) {
    // 鼠标与浏览器左边和右边距离
    var clientY = e.clientY;
    var clientX = e.clientX;

    // 得到鼠标与元素中心点的x和y坐标
    x = clientX - centerX;
    y = clientY - centerY;

    // 以元素的中心点为(0,0)坐标,来计算弧度值,最后转换为对应角度值
    var rad = Math.atan2(y, x);
    var deg = rad * (180 / Math.PI);

    // 控制元素的旋转角度
    box.style.transform = "rotate(" + deg + "deg)";
  };
</script>

# 13、扩展知识:弧度与角度的关系

TIP

  • 角的度量单位通常有两种:一种是角度制,另一种是弧度制
  • 弧度制:我们把长度等于半径长的弧所对的圆心角叫1 弧度的角。 弧度常用rad表示

假设下图中圆的半径为 r ,弧 AB 的长为 L,如果L = r, 则< AOB = L/r = 1 弧度

image-20221215202105196

1 弧度 = ?角度呢

  • 1弧度 = 180deg/π 约为 57.2958279deg

推导过程

  • 周长 = 2πr周角 = 360deg
  • 1弧度/360deg = L/2πr -->> 1弧度/360deg = r/2πr --->> 1弧度 = 180deg/π

通过 1弧度 = 180deg/π 得到

  • 角度 = 弧度 * (180deg/π)
  • 角度 = 弧度 / (π/180)
  • π = 180deg2π = 360deg (弧度单位可以省略)

# 二、Date 日期对象

TIP

Date 日期对象主要是用来处理日期,接下来我们会从以下三个方面来展开讲解

  • 如何创建一个日期
  • 如何对日期格式化
  • 日期对象常见的方法
  • 如获得对应时间戳

以下是与星期相关的英文单词,可以记下来

星期 英文 缩写
星期一 Monday Mon
星期二 Tuesday Tue
星期三 Wednesday Wed
星期四 Thursday Thur
星期五 Friday Fri
星期六 Saturday Sat
星期七 Sunday Sun

以下是与月份相关的英文单词

月份 英文 简写
一月 January Jan
二月 February Feb
三月 March Mar
四月 April Apr
五月 May May
六月 June Jun
七月 July Jul
八月 August Aug
九月 September Sep
十月 October Oct
十一月 November Nov
十二月 December Dec

# 1、new Date() 创建日期对象

TIP

new Date() 可以用来创建一个日期对象

他的基本使用语法如下:

new Date();
new Date(value);
new Date(dateString);
new Date((year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]]);)
  • ①、new Date() 中没有提供任何参数,则返回结果为当前的日期和时间
console.log(new Date()); //  Thu Aug 08 2022 21:39:54 GMT+0800 (中国标准时间)
  • ②、value 是一个时间戳,他是一个整数值,表示从 1970 年 1 月 1 日 00:00:00 UTC(the Unix epoch)开始算起的一个毫秒数。
console.log(new Date(5000));
  • ③、dateString 表示一个日期字符串,其中 2022-07-02 这种格式的日期会被处理成 UTC,而不是本地时间
console.log(new Date("2022-7-1"));
console.log(new Date("2022/7/1"));
console.log(new Date("2022-07-1"));
console.log(new Date("2022-07-01")); // 会被处理成UTC时间

image-20221215234820121

UTC:

  • 也就是我们所说的格林威治时间,指的是 time 中的世界时间标准
  • JavaScript 的时间由世界标准时间(UTC)1970 年 1 月 1 日开始,用毫秒计时,一天由 86,400,000 毫秒组成

本地(当地)时间

是指执行 JavaScript 的客户端电脑所设置的时间

  • ④、new Date((year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]]);)这种形式

TIP

  • 分别提供日期和时间的每个成员,不过至少要提供年份月份
  • 如果没有提供的参数,日期默认值为 1,时,分,秒默认值为 0

注意事项

monthIndex 表示月份的整数值,从 0(1 月)到 11(12 月)

new Date((year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]]);)
// year 年份的整数值,0 到 99 会被映射至 1900 年至 1999 年,其它值代表实际年份
// monthIndex 表示月份的整数值,从 0(1 月)到 11(12 月)
// day  表示一个月中的第几天的整数值 ,1表示第1天
// hours  表示小时,24小时制
// minutes 表示分  0-59
// seconds  表示 秒 0-59
// milliseconds 毫秒数


var d = new Date("2022-9-9");
console.log(d);

var d1 = new Date(2022, 8, 9); // 这里的8表示9月份
console.log(d1);

image-20221215235334270

# 2、new Date() 方法注意事项

TIP

new Date((year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]]);)

这种写法,当传入多个参数时,如果数值大于合理范围时,相邻的数值会被调整

// 月超出范围
var date1 = new Date(2022, 14); // 表示2023年3月1日
var date2 = new Date(2022, 26); // 表示2024年3月1日
console.log(date1);
console.log(date2);

// 日超出范围
var date3 = new Date(2022, 1, 61); // 表示2023年4月2日
console.log(date3);

// 时间超出范围
var date4 = new Date(2022, 1, 2, 1, 80); // 表示2022年1月2日2时20分
console.log(date4);

实践应用

  • 获取上一个月共有多少天,比如获取 2022 年 9 月的上一个月有多少天
// 获取2022年9月的上一个月有多少天
var date = new Date(2022, 8, 0); // 2022年8月的最后一天
console.log(date); // Wed Aug 31 2022 00:00:00 GMT+0800
var prevMonthDayCount = date.getDate(); // 获取当前日期的天
console.log(prevMonthDayCount); // 31
  • 获取当月一共有多少天,如比获取 2022 年 9 月一共有多少天
// 获取2022年9月一共有多少天
var date = new Date(2022, 9, 0); // 2022年9月的最后一天
console.log(date); // Fri Sep 30 2022 00:00:00 GMT+0800 (中国标准时间)
var prevMonthDayCount = date.getDate(); // 获取当前日期的天
console.log(prevMonthDayCount); // 30

# 3、日期格式化方法

以下方法只需要了解即可

方法 说明
toString() 返回一个字符串,以本地的时间表示当前的时间
toLocaleString() 根据当地语言规定返回代表着时间的字符串
toLocaleDateString() 根据当地语言规定返回代表着日期的字符串
toLocaleTimeString() 根据当地语言规定返回代表着时分秒的字符串
var date = new Date();
console.log(date); // Thu Aug 08 2022 22:21:58 GMT+0800 (中国标准时间)
console.log(date.toString()); // Thu Aug 08 2022 22:21:58 GMT+0800 (中国标准时间)
console.log(date.toLocaleString()); // 2022/8/8 22:21:58
console.log(date.toLocaleDateString()); // 2022/8/8
console.log(date.toLocaleTimeString()); // 22:21:58

# 4、日期对象常见方法

TIP

如果我们想获取日期的指定部分,如:星期等中的某个部分

则可以使用下面提供的方法

方法 说明
getFullYear() 根据本地时间,返回具体时间中的 年份
getMonth() 根据本地时间,返回具体时间中的 月份 0-11 表示 1-12 月
getDate() 根据本地时间,返回具体时间中的日 (天)
getDay() 根据本地时间,返回具体时间中的星期,0-6 表示星期天,一,二,三,... 六
getHours() 根据本地时间,返回具体时间中的
getMinutes() 根据本地时间,返回具体时间中的 分钟数
getSeconds() 根据本地时间,返回具体时间中的 秒数
getMilliseconds() 根据本地时间,返回具体时间中的 毫秒数
var d = new Date();

console.log("年份", d.getFullYear()); // 年份 2022
// 月份 0 ,表示1月,d.getMonth()+1 表示当前月份
console.log("月份", d.getMonth() + 1); // 月份 9
console.log("日期", d.getDate()); // 日期 9
console.log("星期", d.getDay()); // 星期5
console.log("小时", d.getHours()); // 小时22
console.log("分钟", d.getMinutes()); // 分钟46
console.log("秒数", d.getSeconds()); // 秒数11
console.log("毫秒", d.getMilliseconds()); // 毫秒 952
  • 以 ”2023 年 09 月 08 日 12 时 04 分 05 秒 星期四“ 格式输出对应的日期
var d = new Date();
var year = d.getFullYear();
var month = d.getMonth() + 1;
var date = d.getDate();
var day = d.getDay();
var hours = d.getHours();
var minutes = d.getMinutes();
var seconds = d.getSeconds();

// 对日期不足两位的补0操作
month = padZero(month);
date = padZero(date);
hours = padZero(hours);
minutes = padZero(minutes);
seconds = padZero(seconds);

var week = [
  "星期天",
  "星期一",
  "星期二",
  "星期三",
  "星期四",
  "星期五",
  "星期六",
];

console.log(
  year +
    "年" +
    month +
    "月" +
    date +
    "天 " +
    hours +
    "时" +
    minutes +
    "分" +
    seconds +
    "秒 " +
    week[day]
);
// 输出结果格式: 2023年09月08天 23时12分16秒 星期四

// 补0
function padZero(str) {
  str = str + "";
  return str.padStart(2, "0");
}

# 5、时间戳

TIP

  • 时间戳:表示 1970 年 1 月 1 日零点整距离某时刻的毫秒数
  • getTime 方法:可以把对应的日期时间转换为对应的时间戳
var d = new Date();
console.log(d.getTime()); // 当前时间戳  1665244800000
  • 利用 getTime 方法,可以测试代码的执行时间
var start = new Date();
for (var i = 0; i < 1000; i++) {
  console.log(i);
}
var end = new Date();
// 代码执行时间,为两者时间之差(毫秒数)
var time = end.getTime() - start.getTime();
console.log("代码执行时间" + time + "毫秒"); // 代码执行时间20毫秒
  • 将一个日期转换为对应的时间戳 ,还有以下 4 种方法
时间戳方法 说明
日期对象.getTime() 可以把对应的日期时间转换为对应的时间戳
日期对象.valueOf方法 返回 Date 对象的原始值,其功能和 getTime 方法一样
+日期对象 这种是一种技巧,利用+号的特性,底层调用的是 valueOf 方法
Date.parse()方法 静态方法,返回传递的日期的时间戳
Date.now() 方法 静态方法 返回当前的日期的时间戳
  • valueOf 方法和 + 日期对象
var d = new Date(2022, 9, 9);
console.log(d.valueOf()); // 1665244800000
console.log(d.getTime()); // 1665244800000
console.log(+d); // 1665244800000
  • Date.now() 方法:只能用来返回当前日期的时间戳
console.log(Date.now()); // 1665244823500
  • Date.parse() 方法有一个很特别的地方,传入的日期相同,但因为格式不同,得到的结果不同。
console.log(Date.parse("2022-9-1")); // 本地时间  1661961600000
console.log(Date.parse("2022/9/1")); // 本地时间  1661961600000
console.log(Date.parse("2022-09-01")); // UTC 1661990400000

# 6、案例:活动倒计时

TIP

涉及知识点

  • 定时器
  • Date 对象、getTime() 方法
  • 倒计时原理: 时间戳如何转换为对应的天数小时分钟秒数

GIF2022-12-1617-16-05

# 6.1、布局思路

<style>
  .countdown {
    width: 190px;
    height: 260px;
    background: url(./images/countdown-bg.png);
    position: relative;
    display: none;
  }
  .countdown-title {
    font-size: 30px;
    color: #fff;
    text-align: center;
    padding-top: 30px;
    font-weight: bold;
  }
  .countdown-main {
    width: 100%;
    height: 35px;
    position: absolute;
    left: 0px;
    bottom: 20px;
    text-align: center;
    font-size: 0;
  }
  .countdown-main span {
    display: inline-block;
    width: 30px;
    height: 30px;
    background-color: #000;
    font-size: 14px;
    color: #fff;
    margin: 0 10px;
    color: #fff;
    line-height: 30px;
    font-weight: bold;
    position: relative;
  }
  .countdown-main span::after {
    content: ":";
    position: absolute;
    right: -11px;
    font-size: 16px;
  }
  .countdown-main span.second::after {
    content: "";
  }
</style>
<!-- 秒杀到计时 -->
<div class="countdown" id="J_countdown">
  <div class="countdown-title">京东秒杀</div>
  <div class="countdown-main">
    <span class="hour">00</span>
    <span class="minute">00</span>
    <span class="second">00</span>
  </div>
</div>

# 6.2、JS 实现思路

TIP

倒计时需要三个时间:当前时间活动开始时间活动结束时间

  • 活动没有开始前,活动版块不会出现在页面中;活动结束后,活动版块要在页面中删除
  • 当前时间 >= 活动开始时间,表示当前活动正在进行,开始倒计时功能

倒计时实现原理

  • 利用活动结束时间戳 - 当前时间戳 ,得到的剩余时间(毫秒)
  • 把剩余时间(毫秒)/ 1000 得到对应的秒数,然后将秒数转换为对应的 天、时、分、秒
  • 然后利用定时器,每过 1 分钟就更新下时间

秒数转换成天、时、分、秒公式

  • 我们知道 1天 = 24小时1小时 = 60分1分 = 60秒,如果当前为 n 天,则 总秒数 = n * 60 * 60 * 24
  • 天数:day = parseInt(总秒数 / 24 / 60 / 60)
  • 小时:hour = parseInt(总秒数 / 60 / 60 % 24 )
  • 如果不考虑天数,也就是所有天数的时间也按小时来算,hour=parseInt(总秒数 / 60 / 60 )
  • 分钟:minutes=parseInt(总秒数 / 60 % 60)
  • 秒数:secondes=parseInt(总秒数 % 60)

第一步:创建 countDown 函数

TIP

这个函数有两个参数,分别代表开始时间和结束时间,只要调用这个函数,传入对应实参,就能实现倒计时功能

function countDown(startDate, endDate) {}

第二步:完善 countDown 函数功能

TIP

  • 检测是否两个参数都有传,同时传的类型是否是时间对象,并且是否满足 endDate 大于 startDate
  • 如果不满足以上条件,则直接抛出对应的错误
function countDown(startDate, endDate) {
  // 判断是否两个参数都有传
  if (startDate === undefined || endDate === undefined)
    throw new Error("必需传入两个日期对象,作为活动的开始时间和结束时间");

  // 两个中只要有一个不是日期对象,则抛出错误
  if (!(startDate instanceof Date) || !(endDate instanceof Date))
    throw new Error("两个参数,都必需是日期对象");

  // 如果开始时间大于等于结束时间,则相当于没有活动,抛出错误
  if (startDate.getTime() >= endDate.getTime())
    throw new Error("开始时间不能大于等于结束时间");

  // ....... 活动开始与结束相关代码从这往下开始
}

// 测试代码
var startDate = new Date("2022-12-16 15:53:00"); // 活动开始时间
var endDate = new Date("2022-12-17 00:00:00"); // 活动结束时间
countDown(startDate, endDate);

第三步:判断活动是否结束

TIP

如果当前时间 > 活动结束时间,则表明活动结束,将活动版块从页面移除

// 判断活动是否结束
if (endDate.getTime() < nowDate.getTime()) {
  // 活动结束,将元素从页面删除
  countdownEl.parentNode.removeChild(countdownEl);
  //  清除定时器
  clearInterval(timer);
}

注:

判断活动结束的代码,要写在判断活动开始的代码前,这样就会因为活动结束了,还要执行一次代码,然后才知道活动结束

第四步:判断活动是否开始

TIP

  • 如果活动开始时间 > 当前时间,则表示活动开始,在活动版块插入页面中(显示)
  • 同时开始定时器,每隔 1 秒更新下时间,实现倒计时功能。
  • 活动结束时间戳 - 当前时间戳 得到的毫秒数,转换成秒。
  • 最后将秒数,换算成对应时,分,秒
var countdownEl = document.getElementById("J_countdown");
var spans = document.querySelectorAll("#J_countdown .countdown-main span");

// 以下代码,写在countDown 内部
var timer = setInterval(function () {
  var nowDate = new Date();

  // .... 判断活动是否结束的代码,写在这里

  // 判断活动是否开始,如果未开始,则啥也不做,如果开始,则将活动版块显示
  if (nowDate.getTime() >= startDate.getTime()) {
    // 活动开始,显示活动版块
    countdownEl.style.display = "block";

    // 倒计时 时间差秒
    var timeDiff = (endDate.getTime() - nowDate.getTime()) / 1000;
    var hour = Math.floor(timeDiff / 60 / 60); // 计算时
    var minute = Math.floor((timeDiff / 60) % 60); // 计算分
    var second = Math.floor(timeDiff % 60); // 计时秒

    // 更新DOM元素,对于不足两位的数,前面补0
    spans[0].innerHTML = (hour + "").padStart(2, "0");
    spans[1].innerHTML = (minute + "").padStart(2, "0");
    spans[2].innerHTML = (second + "").padStart(2, "0");
  }
}, 1000);

优化细节

TIP

  • 如果 没有变化,则不需要再次更新 DOM,只有不相同时,才需要更新 DOM
  • 创建一个对象,用来前面的时,分,然后拿 对象中的时,分与现在的对比,如果不相同则更新 DOM,同时把自身的值也更新为最新的,否则啥也不做
// 用来保存前面的时,分
var time = {
  hour: "00",
  minute: "00",
};

// 如果时有变化,则更新DOM,同时更新自身的值
if (time.hour != hour) {
  spans[0].innerHTML = (hour + "").padStart(2, "0");
  time.hour = hour;
}
// 如果分没有变化,则更新DOM,同时更新自身的值
if (time.minute != minute) {
  spans[1].innerHTML = (minute + "").padStart(2, "0");
  time.minute = minute;
}

# 6.3、完整版代码

TIP

  • 用户刚打开页面,活动已经开始了,则需要立马显示倒计时,而不用等到 1 秒后,才开始显示元素,然后显示对应时间
  • 所以把定时器需要执行的代码封装到函数 updateTime 中,然后在定时器之前,先调用下 updateTime 函数
<style>
  .countdown {
    width: 190px;
    height: 260px;
    background: url(./images/countdown-bg.png);
    position: relative;
    display: none;
  }
  .countdown-title {
    font-size: 30px;
    color: #fff;
    text-align: center;
    padding-top: 30px;
    font-weight: bold;
  }
  .countdown-main {
    width: 100%;
    height: 35px;
    position: absolute;
    left: 0px;
    bottom: 20px;
    text-align: center;
    font-size: 0;
  }
  .countdown-main span {
    display: inline-block;
    width: 30px;
    height: 30px;
    background-color: #000;
    font-size: 16px;
    color: #fff;
    margin: 0 10px;
    color: #fff;
    line-height: 30px;
    font-weight: bold;
    position: relative;
  }
  .countdown-main span::after {
    content: ":";
    position: absolute;
    right: -11px;
    font-size: 16px;
  }
  .countdown-main span.second::after {
    content: "";
  }
</style>
<!-- 秒杀到计时 -->
<div class="countdown" id="J_countdown">
  <div class="countdown-title">京东秒杀</div>
  <div class="countdown-main">
    <span class="hour">00</span>
    <span class="minute">00</span>
    <span class="second">00</span>
  </div>
</div>

<script>
  // 获取DOM元素
  var countdownEl = document.getElementById("J_countdown");
  var spans = document.querySelectorAll("#J_countdown .countdown-main span");

  /**
   * countDown 倒计时函数
   * @param startDate 开始时间,日期对象
   * @param endDate 结束时间,日期对象
   */
  function countDown(startDate, endDate) {
    // 判断是否两个参数都有传
    if (startDate === undefined || endDate === undefined)
      throw new Error("必需传入两个日期对象,作为活动的开始时间和结束时间");

    // 两个中只要有一个不是日期对象,则抛出错误
    if (!(startDate instanceof Date) || !(endDate instanceof Date))
      throw new Error("两个参数,都必需是日期对象");

    // 如果开始时间大于等于结束时间,则相当于没有活动,抛出错误
    if (startDate.getTime() >= endDate.getTime())
      throw new Error("开始时间不能大于等于结束时间");

    // 用来记录前前面时,分
    var time = {
      hour: "00",
      minute: "00",
    };
    var timer = setInterval(updateTime, 1000);
    updateTime(); //  进来就调用更新时间的函数
    function updateTime() {
      var nowDate = new Date();
      // .... 判断活动是否结束的代码,写在这里
      // 判断活动是否结束
      if (endDate.getTime() < nowDate.getTime()) {
        // 活动结束,将元素从页面删除
        countdownEl.parentNode.removeChild(countdownEl);
        //  清除定时器
        clearInterval(timer);
      }

      // 判断活动是否开始,如果未开始,则啥也不做,如果开始,则将活动版块显示
      if (nowDate.getTime() >= startDate.getTime()) {
        // 活动开始,显示活动版块
        countdownEl.style.display = "block";

        // 倒计时 时间差秒
        var timeDiff = (endDate.getTime() - nowDate.getTime()) / 1000;
        var hour = parseInt(timeDiff / 60 / 60); // 计算时
        var minute = parseInt((timeDiff / 60) % 60); // 计算分
        var second = parseInt(timeDiff % 60); // 计时秒

        // 更新DOM元素,对于不足两位的数,前面补0,
        if (time.hour != hour) {
          spans[0].innerHTML = (hour + "").padStart(2, "0");
          time.hour = hour;
        }

        if (time.minute != minute) {
          spans[1].innerHTML = (minute + "").padStart(2, "0");
          time.minute = minute;
        }
        spans[2].innerHTML = (second + "").padStart(2, "0");
      }
    }
  }

  // 倒计时功能
  // 需要三个时间,当前时间,活动开始时间,活动结束时间
  var startDate = new Date("2022-12-15 15:53:00"); // 活动开始时间
  var endDate = new Date("2022-12-17 16:59:00"); // 活动结束时间

  countDown(startDate, endDate);
</script>

# 三、手写日历组件

GIF2022-12-2619-14-44

# 1、布局思路

其中calendar-main中的内容,是重点渲染的内容,后期通过 JS 动态生成

<div class="calendar">
  <!-- 年月切换按扭开始-->
  <div class="calendar-menu">
    <span class="prev-year">&lt;&lt;</span>
    <span class="prev-month">&lt;</span>
    <span class="current-date">2022年12月</span>
    <span class="next-month">&gt;</span>
    <span class="next-year">&gt;&gt;</span>
  </div>
  <!-- 年月切换按扭结束 -->
  <!-- 周信息 开始 -->
  <div class="calendar-week table">
    <div class="tr">
      <div class="th"></div>
      <div class="th"></div>
      <div class="th"></div>
      <div class="th"></div>
      <div class="th"></div>
      <div class="th"></div>
      <div class="th"></div>
    </div>
  </div>
  <!-- 周信息 结束 -->

  <!-- 日历主体开始 -->
  <div class="calendar-main">
    <div class="table">
      <div class="tr">
        <div class="td prev-month">28</div>
        <div class="td prev-month">29</div>
        <div class="td prev-month">30</div>

        <div class="td">1</div>
        <div class="td">2</div>
        <div class="td">3</div>
        <div class="td">4</div>
      </div>
      <div class="tr">
        <div class="td">5</div>
        <div class="td">6</div>
        <div class="td">7</div>
        <div class="td">8</div>
        <div class="td">9</div>
        <div class="td">10</div>
        <div class="td">11</div>
      </div>
      <div class="tr">
        <div class="td">12</div>
        <div class="td">13</div>
        <div class="td">14</div>
        <div class="td">15</div>
        <div class="td">16</div>
        <div class="td">17</div>
        <div class="td">18</div>
      </div>
      <div class="tr">
        <div class="td">19</div>
        <div class="td">20</div>
        <div class="td">21</div>
        <div class="td">22</div>
        <div class="td">23</div>
        <div class="td">24</div>
        <div class="td">25</div>
      </div>
      <div class="tr">
        <div class="td current">26</div>
        <div class="td">27</div>
        <div class="td">28</div>
        <div class="td">29</div>
        <div class="td">30</div>
        <div class="td">31</div>
        <div class="td next-month">1</div>
      </div>
      <div class="tr">
        <div class="td next-month">2</div>
        <div class="td next-month">3</div>
        <div class="td next-month">4</div>
        <div class="td next-month">5</div>
        <div class="td next-month">6</div>
        <div class="td next-month">7</div>
        <div class="td next-month">8</div>
      </div>
    </div>
  </div>
  <!-- 日历主体结束 -->
</div>

CSS 样式

<style>
  .calendar {
    width: 300px;
    border: 1px solid #ddd;
    box-shadow: 0 0 5px #ddd;
    border-radius: 5px;
    padding: 20px;
    margin: 100px;
  }
  .calendar-menu {
    display: flex;
  }
  .calendar-menu .current-date {
    flex-grow: 1;
    text-align: center;
    color: #000;
  }
  .calendar-menu span {
    color: rgb(125, 124, 124);
    user-select: none;
    cursor: pointer;
  }
  .calendar-menu .prev-year {
    margin-right: 20px;
  }
  .calendar-menu .next-year {
    margin-left: 20px;
  }

  /* 表格布局样式 */
  .table {
    display: table;
    width: 100%;
  }
  .table .tr {
    display: table-row;
  }
  .table .tr .th,
  .table .tr .td {
    display: table-cell;
    text-align: center;
    height: 35px;
    vertical-align: middle;
    cursor: pointer;
    font-size: 12px;
  }
  .table .tr .th {
    padding-top: 20px;
    border-bottom: 1px solid #ddd;
  }
  .table .tr .td.prev-month,
  .table .tr .td.next-month {
    color: rgb(194, 194, 194);
  }
  .table .tr .td.current {
    background-color: rgb(244, 31, 102);
    color: #fff;
    font-weight: bold;
  }
  .table .tr .td:hover {
    outline: 2px solid #ddd;
  }
  .table .tr .td.active {
    outline: 2px solid rgb(244, 31, 102);
  }
</style>

# 2、JS 实现思路

TIP

关于日历的渲染,我们需要知道以下几方面信息

  • 1、当月有多少天,这样就可以知道当月从 1 号渲染到几号
  • 2、当月的第一天是星期几,这样就能知道当月 1 号的渲染位置
  • 3、上月需要渲染天数对应的日期,(知道了当月第一天是星期几,就可以知道上月需要渲染的天数,同时还要知道上月最后一天是几号,就要以知道对应的日期)
  • 4、下个月需要渲染的天数对应日期,保存在数组中。(下月需要渲染天数 = 42 - 当月总天数 - 上月需要渲染的天数,对应日期,从 1 号开始渲染到对应天数就可以)

# 3、书写相关的工具函数

  • 获取对应年份的月份的总天数
// year 年份,month 月份
function getMonthDayCount(year, month) {
  // 知道当月最后一天的是几号,就可以知道当月一共有多少天
  var date = new Date(year, month, 0);
  return date.getDate();
}
  • 获取当月第一天是星期几
// 获取当月第一天是星期几
function getMonthFirstWeekDay(year, month) {
  var date = new Date(year, month - 1, 1);
  return date.getDay(); // 0-6表示星期日和星期一....星期六
}
  • 上月需要渲染天数对应的日期,保存在数组中
// 上月需要渲染天数对应的日期,保存在一个数组中
function getPrevMonthRestDays(year, month) {
  // 当月第一天是星期几
  var week = getMonthFirstWeekDay(year, month);
  //对于星期做相关处理,如果week=0表示周日,则需要上个月需要渲染6天
  week = week === 0 ? 6 : week - 1;
  // 上月最后一天是几号
  var lastDate = getMonthDayCount(year, month - 1);
  // 用来记录上月需要渲染天数
  var restDays = [];

  while (week > 0) {
    restDays.push(lastDate--);
    week--;
  }
  return restDays.reverse(); // 将结果置反
}

console.log(getPrevMonthRestDays(2022, 12)); // [28, 29, 30]
  • 下个月需要渲染的天数对应的日期,保存在数组中
// 下个月需要渲染的天数
function getNextMonthRestDays(year, month) {
  // 当月的总天数
  var currentMonthDayCount = getMonthDayCount(year, month);
  // 上月渲染的天数
  var days = getMonthFirstWeekDay(year, month);
  days = days === 0 ? 6 : days - 1;
  // 计算得到下月需要渲染的天数
  var nextMonthRestDays = 42 - currentMonthDayCount - days;

  var result = [];
  for (var i = 1; i <= nextMonthRestDays; i++) {
    result.push(i);
  }
  return result;
}

# 4、根据日期创建 td 结构数组

根据上月需要渲染的日期,生成对应 td 标签和样式,添加到数组中

// 创建上月td结构
function createTd1(arr) {
  return arr.map(function (item) {
    var td = document.createElement("div");
    td.className = "td prev-month";
    td.innerText = item;
    return td;
  });
}

// 根据下月需要渲染的天数,来生成对应的td标签
function createTd2(arr) {
  return arr.map(function (item) {
    var td = document.createElement("div");
    td.className = "td next-month";
    td.innerText = item;
    return td;
  });
}

// 根据当月需要渲染的天数,来生成对应td标签
function createTd3(days) {
  var result = [];
  for (var i = 1; i <= days; i++) {
    var td = document.createElement("div");
    td.className = "td";
    td.innerText = i;
    result.push(td);
  }
  return result;
}

# 5、创建 render 渲染函数,根据对应年月来渲染出日历内容

// 创建渲染函数,用来渲染 xxxx年xx月的日历表
function render(year, month) {
  // 获取上月需要渲染的日期
  var prevMonthRestDays = getPrevMonthRestDays(year, month);
  // 当月需要渲染的天数
  var currentMonthCountDays = getMonthDayCount(year, month);
  // 获取下月需要渲染的日期
  var nextMonthRestDays = getNextMonthRestDays(year, month);

  // 根据对应日期和天数,创建出对应的td结构
  var prevTd = createTd(prevMonthRestDays);
  var nextTd = createTd(nextMonthRestDays);
  var currentTd = createTd2(currentMonthCountDays);

  // 三个数组合并到一个数组
  var tds = prevTd.concat(currentTd, nextTd);
  // 利用tds来创建对应的表格,然后将表格渲染到页面中
  var calendarMain = document.querySelector(".calendar-main");
  // 渲染前,要先将原有内容清空
  calendarMain.innerHTML = "";
  calendarMain.appendChild(createTabel(tds));

  function createTabel(tds) {
    var table = document.createElement("div");
    table.className = "table";
    var n = 0;
    // 行
    for (var i = 0; i < 6; i++) {
      var tr = document.createElement("div");
      tr.className = "tr";
      // 列
      for (var j = 0; j < 7; j++) {
        tr.appendChild(tds[n]);
        n++;
      }
      // 将tr添加到table
      table.appendChild(tr);
    }
    return table;
  }
}
  • 创建函数,用来获取当前的日期
// 获取当前日期函数
function getCurrentDate() {
  var nowDate = new Date();
  return {
    year: nowDate.getFullYear(),
    month: nowDate.getMonth() + 1,
    date: nowDate.getDate(),
  };
}
  • 调用 render 函数,来生成对应日历
render(getCurrentDate().year, getCurrentDate().month);

// 或

render(2022, 10);

# 6、当前日期,背景要变成粉色

对应 render 函数中,调用 createTd2() 时,传入的参数要改变

// 创建本月td结构
function createTd2(year, month) {
  var result = [];
  // 获取当前日期(年,月,日)
  var currentDate = getCurrentDate().date;
  var currentMonth = getCurrentDate().month;
  var currentYear = getCurrentDate().year;
  // 获取当月的总天数
  days = getMonthDayCount(year, month);
  // 遍历
  for (var i = 1; i <= days; i++) {
    var td = document.createElement("div");

    if (year === currentYear && month === currentMonth && i === currentDate) {
      td.className = "td current";
    } else {
      td.className = "td";
    }

    td.innerText = i;
    result.push(td);
  }
  return result;
}

# 7、完善 render 函数

当前日历表头的xxxx 年 xx 月日期,要更新为当前渲染的日期

function render(year, month) {
  // 日历头部日期显示为渲染日期
  var dateDom = document.querySelector(".calendar-menu .current-date");
  dateDom.innerText = dateDom.innerText = year + "年 " + month + "月";

  // .....
}

# 8、年月切换按扭事件处理

// 获取当前日期
var date = getCurrentDate();
var currentYear = date.year;
var currentMonth = date.month;
var currentDate = date.date;

// 渲染日历
render(currentYear, currentMonth);

// 年月切换按扭事件处理
var prevYearDom = document.querySelector(".prev-year");
var nextYearDom = document.querySelector(".next-year");
var prevMonthDom = document.querySelector(".prev-month");
var nextMonthDom = document.querySelector(".next-month");

// 上一年
prevYearDom.onclick = function () {
  currentYear--;
  render(currentYear, currentMonth);
};

// 下一年
nextYearDom.onclick = function () {
  currentYear++;
  render(currentYear, currentMonth);
};

// 上一个月
prevMonthDom.onclick = function () {
  currentMonth--;
  if (currentMonth === 0) {
    currentMonth = 12;
    currentYear--;
  }
  render(currentYear, currentMonth);
};

// 下一个月
nextMonthDom.onclick = function () {
  currentMonth++;
  if (currentMonth === 13) {
    currentMonth = 1;
    currentYear++;
  }
  render(currentYear, currentMonth);
};

# 9、选中对应日期

TIP

如果选中的日期是上个月或下个月的某天,则日历表更新到对应月份的天数

render 函数,需要再添加一个 selectDate 参数,如果有选中当前日期,则渲染时把当前日期标出来

// 选中对应的日期
// 事件代理
var calendarMain = document.querySelector(".calendar-main");
var prevEl = null; // 前一个被选中的元素
calendarMain.onclick = function (e) {
  var target = e.target;
  var bool = target.classList.contains("td");
  if (!bool) return;

  // 首先处理样式变化
  if (prevEl) prevEl.classList.remove("active");
  target.classList.add("active");
  prevEl = target;

  // 判断当前点的是上个月的日期,还是下个月的日期
  var prevBool = target.classList.contains("prev-month"); // 如果为true,表示点的是上个月日期
  var nextBool = target.classList.contains("next-month"); // 如果为true,表示点的是上个月日期

  // 点的是上个月日期 或下个月的日期时,才会重新触发渲染
  if (prevBool) {
    // 点了上个月日期
    currentMonth--;
    if (currentMonth === 0) {
      currentMonth = 12;
      currentYear--;
    }
    // 重新渲染
    var selectDate = target.innerText;
    render(currentYear, currentMonth, selectDate);
  } else if (nextBool) {
    // 点了下个月日期
    currentMonth++;
    if (currentMonth === 13) {
      currentMonth = 1;
      currentYear++;
    }
    // 重新渲染
    var selectDate = target.innerText;
    render(currentYear, currentMonth, selectDate);
  }

  // 得到当前选中的日期
  console.log(
    currentYear + "年" + currentMonth + "月" + target.innerText + "日"
  );
};
  • 更新 createTd3 函数
// 创建本月td结构,selectDate是当前选中的日期天数
function createTd3(year, month, selectDate) {
  var result = [];
  // 要判断当前的year,month,date日,是不是于当前的日期完全一样,如果是,则date的td结构上添加current样式
  var currentYear = getCurrentDate().year;
  var currentMonth = getCurrentDate().month;
  var currentDate = getCurrentDate().date;

  var days = getMonthDayCount(year, month);
  for (var i = 1; i <= days; i++) {
    var td = document.createElement("div");
    // 判断 渲染日期是不当前日期
    if (year === currentYear && month === currentMonth && i === currentDate) {
      td.className = "td current";
    } else {
      td.className = "td";
    }

    // 判断是否当前有选中的日期,如果有,则将期标红色边框
    if (selectDate && selectDate == i) {
      td.classList.add("active");
      prevEl = td;
    }

    td.innerText = i;
    result.push(td);
  }
  return result;
}
  • 把 render 函数中如下代码的注释部分更新为如下
// selectDate 表示当前选中天数
function render(year, month, selectDate) {
  // var currentTd=createTd3(currentMonthCountDays);
  if (selectDate) {
    var currentTd = createTd3(year, month, selectDate);
  } else {
    var currentTd = createTd3(year, month);
  }

  //....
}

# 10、封装完整版工具函数 data.js

// 日历的渲染,我们需要知道以下几个重要的信息
// 1、当月有多少天,有了这个数字,我们就可以知道当月从1号渲染到几号
// 2、当月的第一天是星期几,有了这个信息,我就知道当月1号的渲染位置
// 3、上个月需要渲染多少天和对应的日期
//    - 当月的第一天是星期几,就可以知道上个月需要渲染多少天,如果当月的第一天是星期日(0),要特别处理下
//    - 上个月的最后一天是多少要知道
// 4、下个月需要渲染的天数和日期  = 42-当月总天数 -上月需要渲染的天数    42-3-31=42-34=8

// 获取对应年份的月份的总天数  2022 8
function getMonthDayCount(year, month) {
  var date = new Date(year, month, 0);
  return date.getDate();
}
//   console.log(getMonthDayCount(2022,9))

// 获取当月的第一天是星期几  2022 9
function getMonthFirstWeekDay(year, month) {
  var date = new Date(year, month - 1, 1);
  return date.getDay(); // 0-6表示星期日和星期一....星期六
}
// console.log(getMonthFirstWeekDay(2022,11))

// 上月需要渲染天数对应的日期,保存在数组中  2022 9
function getPrevMonthRestDays(year, month) {
  // 当月第一天是星期几
  var week = getMonthFirstWeekDay(year, month);
  week = week === 0 ? 6 : week - 1; // 处理特殊的0,表示星期天
  // 上月最后一天是几号
  var lastDate = getMonthDayCount(year, month - 1);
  // 创建一个数组,用来保存上月需要渲染的日期
  var restDays = [];
  while (week > 0) {
    restDays.push(lastDate--);
    week--;
  }
  return restDays.reverse(); // 置反
}
// console.log(getPrevMonthRestDays(2022,9))

// 下个月需要渲染的天数和对应的日期,保存在数组中  2022 9
function getNextMonthRestDays(year, month) {
  // 当月的总天数
  var currentMonthDayCount = getMonthDayCount(year, month);
  // 上月需要渲染的天数
  var days = getMonthFirstWeekDay(year, month);
  days = days === 0 ? 6 : days - 1; // 上个月需要渲染的天数
  // 下月需要渲染的天数
  var nextMonthRestDays = 42 - currentMonthDayCount - days;

  // 如果要把需要渲染的日期保存在数组中
  var restDays = [];
  for (var i = 1; i <= nextMonthRestDays; i++) {
    restDays.push(i);
  }
  // return nextMonthRestDays;
  return restDays;
}

// console.log(getNextMonthRestDays(2022,9))

// 根据对应日期来创建td结构
// 根据上月需要渲染的日期,生成对应的td标签和样式,添加到数组中
function createTd1(year, month) {
  //  return  arr.map(function(item){
  //         var td=document.createElement('div');
  //         td.className='td prev-month'
  //         td.innerText=item;
  //         return td;
  //    })

  // 获取前一个月份对应的年和月
  var prevMonth,
    prevYear = year;
  prevMonth = month - 1;
  if (prevMonth === 0) {
    prevYear = year - 1;
    prevMonth = 12;
  }
  // 获取当前日期
  var currentYear = getCurrentDate().year;
  var currentMonth = getCurrentDate().month;
  var currentDate = getCurrentDate().date;

  var arr = getPrevMonthRestDays(year, month);
  var result = [];
  for (var i = 0; i < arr.length; i++) {
    var td = document.createElement("div");
    td.className = "td prev-month";
    // 判断当前月的前一个月中的某个日期为当前日期时
    if (
      currentYear === prevYear &&
      currentMonth === prevMonth &&
      currentDate === arr[i]
    ) {
      td.classList.add("current");
    }

    td.innerText = arr[i];
    result.push(td);
  }
  return result;
}
//  console.log( createTd1(getPrevMonthRestDays(2022,12)))

// 根据下月需要渲染的天数,来生成对应的td标签
function createTd2(year, month) {
  // 考虑当前日历中的下月渲染中的某天为当前日期,则需要将背景变红色

  // 获取下一个月份对应的年和月
  var nextMonth = month + 1,
    nextYear = year;
  if (nextMonth === 13) {
    nextYear = year + 1;
    nextMonth = 1;
  }

  // 获取当前日期
  var currentYear = getCurrentDate().year;
  var currentMonth = getCurrentDate().month;
  var currentDate = getCurrentDate().date;

  var arr = getNextMonthRestDays(year, month);
  return arr.map(function (item) {
    var td = document.createElement("div");
    td.className = "td next-month";
    // 判断条件,下月渲染中的某天是否为当前日期
    if (
      currentYear === nextYear &&
      currentMonth === nextMonth &&
      currentDate === item
    ) {
      td.classList.add("current");
    }
    td.innerText = item;
    return td;
  });
}

//  console.log(createTd2(getNextMonthRestDays(2022,12)))

// 根据当月需要渲染的天数,来生成对应td标签
// function createTd3(days){
//     var result=[];
//     for(var i=1;i<=days;i++ ){
//         var td=document.createElement('div');
//         td.className='td';
//         td.innerText=i;
//         result.push(td);
//     }
//     return result;
// }

function createTd3(year, month, selectDate) {
  var result = [];
  // 要判断当前的year,month,date日,是不是于当前的日期完全一样,如果是,则date的td结构上添加current样式
  var currentYear = getCurrentDate().year;
  var currentMonth = getCurrentDate().month;
  var currentDate = getCurrentDate().date;

  var days = getMonthDayCount(year, month);
  for (var i = 1; i <= days; i++) {
    var td = document.createElement("div");
    // 判断 渲染日期是不当前日期
    if (year === currentYear && month === currentMonth && i === currentDate) {
      td.className = "td current";
    } else {
      td.className = "td";
    }

    // 判断是否当前有选中的日期,如果有,则将期标红色边框
    if (selectDate && selectDate == i) {
      td.classList.add("active");
      prevEl = td;
    }

    td.innerText = i;
    result.push(td);
  }
  return result;
}

// console.log(createTd3(getMonthDayCount(2022,12)))

//  创建render渲染函数,根据对应的年月渲染出日历内容
// selectDate 表示当前选中天数
function render(year, month, selectDate) {
  // 更新日历头部的日期
  var dateDom = document.querySelector(".calendar-menu .current-date");
  dateDom.innerText = year + "年" + month + "月";

  // 获取上月需要渲染的日期,返回的是一个数组
  //  var prevMonthRestDays=getPrevMonthRestDays(year,month);
  // 当前月需要渲染的天数,返回的是一个整数
  // var currentMonthCountDays=getMonthDayCount(year,month)
  // 下个月需要渲染的天数 ,返回的是一个数组
  // var nextMonthRestDays=getNextMonthRestDays(year,month);

  // 根据对应的日期创建对应的td结构,三者都是一个数组
  // var prevTd=createTd1(prevMonthRestDays);
  var prevTd = createTd1(year, month);
  // var nextTd=createTd2(nextMonthRestDays);
  var nextTd = createTd2(year, month);
  // var currentTd=createTd3(currentMonthCountDays);
  if (selectDate) {
    var currentTd = createTd3(year, month, selectDate);
  } else {
    var currentTd = createTd3(year, month);
  }

  // 将三个数组,合并成一个数组
  var tds = prevTd.concat(currentTd, nextTd);
  // 将创建好的表格,插入到页面中
  var calendarMain = document.querySelector(".calendar-main");
  // 每次插入新的内容前,要将原来的内容清空
  calendarMain.innerHTML = "";
  calendarMain.appendChild(createTable(tds));

  // 根据tds来创建对应6行7列的表格,然后插入到页面中去
  function createTable(tds) {
    var table = document.createElement("div");
    table.className = "table";

    // 创建对应的行和列
    var n = 0; // 用来记录当前的个数
    // 行
    for (var i = 0; i < 6; i++) {
      var tr = document.createElement("div");
      tr.className = "tr";
      for (var j = 0; j < 7; j++) {
        // 列
        tr.appendChild(tds[n]);
        n++;
      }
      table.appendChild(tr);
    }
    return table;
  }
}

// 根据当前的日期,来渲染出对应的日历
function getCurrentDate() {
  var nowDate = new Date();
  return {
    year: nowDate.getFullYear(),
    month: nowDate.getMonth() + 1,
    date: nowDate.getDate(),
  };

  // return {
  //     year:2023,
  //     month:1,
  //     date:3
  // }
}

// render(getCurrentDate().year,getCurrentDate().month)

# 11、完整版日历组件开发

<style>
  .calendar {
    width: 300px;
    border: 1px solid #ddd;
    box-shadow: 0 0 5px #ddd;
    border-radius: 5px;
    padding: 20px;
    margin: 100px;
  }
  .calendar-menu {
    display: flex;
  }
  .calendar-menu span {
    user-select: none;
    cursor: pointer;
    color: rgb(125, 124, 124);
  }
  .calendar-menu .current-date {
    flex-grow: 1;
    text-align: center;
    color: #000;
  }
  .calendar-menu .prev-year {
    margin-right: 20px;
  }
  .calendar-menu .next-year {
    margin-left: 20px;
  }

  /* display:table 布局样式 */
  .table {
    display: table;
    width: 100%;
  }
  .table .tr {
    display: table-row;
  }
  .table .tr .th,
  .table .tr .td {
    display: table-cell;
    font-size: 12px;
    text-align: center;
    vertical-align: middle;
    height: 35px;
    cursor: pointer;
  }
  .table .tr .th {
    border-bottom: 1px solid #ddd;
    padding-top: 20px;
  }
  .table .tr .td.next-month,
  .table .tr .td.prev-month {
    color: rgb(194, 194, 194);
  }
  .table .tr .td.current {
    background-color: rgb(244, 31, 102);
    color: #fff;
    font-weight: bold;
  }
  .table .tr .td:hover {
    outline: 2px solid #ddd;
  }
  .table .tr .td.active {
    outline: 2px solid tomato;
  }
</style>

<div class="calendar">
  <!-- 年月切换按扭开始 -->
  <div class="calendar-menu">
    <span class="prev-year">&lt;&lt;</span>
    <span class="prev-month">&lt;</span>
    <span class="current-date">2022 年 12月</span>
    <span class="next-month">&gt;</span>
    <span class="next-year">&gt;&gt;</span>
  </div>
  <!-- 年月切换按扭结束 -->

  <!-- 周信息开始 -->
  <div class="calendar-week table">
    <div class="tr">
      <div class="th"></div>
      <div class="th"></div>
      <div class="th"></div>
      <div class="th"></div>
      <div class="th"></div>
      <div class="th"></div>
      <div class="th"></div>
    </div>
  </div>
  <!-- 周信息结束 -->
  <!-- 日历主体开始 -->
  <div class="calendar-main">
    <div class="table">
      <div class="tr">
        <div class="td prev-month">28</div>
        <div class="td prev-month">29</div>
        <div class="td prev-month">30</div>
        <div class="td">1</div>
        <div class="td">2</div>
        <div class="td">3</div>
        <div class="td">4</div>
      </div>
      <div class="tr">
        <div class="td">5</div>
        <div class="td">6</div>
        <div class="td">7</div>
        <div class="td">8</div>
        <div class="td">9</div>
        <div class="td">10</div>
        <div class="td">11</div>
      </div>
      <div class="tr">
        <div class="td">12</div>
        <div class="td">13</div>
        <div class="td">14</div>
        <div class="td">15</div>
        <div class="td">16</div>
        <div class="td">17</div>
        <div class="td">18</div>
      </div>
      <div class="tr">
        <div class="td">19</div>
        <div class="td">20</div>
        <div class="td">21</div>
        <div class="td">22</div>
        <div class="td">23</div>
        <div class="td">24</div>
        <div class="td">25</div>
      </div>
      <div class="tr">
        <div class="td current">26</div>
        <div class="td">27</div>
        <div class="td">28</div>
        <div class="td">29</div>
        <div class="td">30</div>
        <div class="td">31</div>
        <div class="td rest">1</div>
      </div>
      <div class="tr">
        <div class="td next-month">2</div>
        <div class="td next-month">3</div>
        <div class="td next-month">4</div>
        <div class="td next-month">5</div>
        <div class="td next-month">6</div>
        <div class="td next-month">7</div>
        <div class="td next-month">8</div>
      </div>
    </div>
  </div>
  <!-- 日历主体结束 -->
</div>
<script src="./date.js"></script>
<script>
  // 日历的渲染,我们需要知道以下几个重要的信息
  // 1、当月有多少天,有了这个数字,我们就可以知道当月从1号渲染到几号
  // 2、当月的第一天是星期几,有了这个信息,我就知道当月1号的渲染位置
  // 3、上个月需要渲染多少天和对应的日期
  //    - 当月的第一天是星期几,就可以知道上个月需要渲染多少天,如果当月的第一天是星期日(0),要特别处理下
  //    - 上个月的最后一天是多少要知道
  // 4、下个月需要渲染的天数和日期  = 42-当月总天数 -上月需要渲染的天数    42-3-31=42-34=8

  // 根据当前的日期(年,月来渲染出对应日历)
  var date = getCurrentDate();
  var currentYear = date.year;
  var currentMonth = date.month;
  var currentDate = date.date;

  // 渲染日历
  //   render(currentYear, currentMonth);
  render(2022, 12);

  // 优化的第一个方向:当前所在的日期背景色要标红
  // 化化的第二个方向,对应的标题中的日期要更新为最新渲染的日期

  // 获取对应的切换按扭,然后添加点击事件
  var prevYearDom = document.querySelector(".prev-year");
  var nextYearDom = document.querySelector(".next-year");
  var prevMonthDom = document.querySelector(".prev-month");
  var nextMonthDom = document.querySelector(".next-month");

  // 添加点击事件
  // 上一年
  prevYearDom.onclick = function () {
    currentYear--;
    render(currentYear, currentMonth);
  };

  // 下一年
  nextYearDom.onclick = function () {
    currentYear++;
    render(currentYear, currentMonth);
  };

  // 上一个月
  prevMonthDom.onclick = function () {
    currentMonth--;
    if (currentMonth === 0) {
      currentMonth = 12;
      currentYear--;
    }
    render(currentYear, currentMonth);
  };

  // 下一个月
  nextMonthDom.onclick = function () {
    currentMonth++;
    if (currentMonth === 13) {
      currentMonth = 1;
      currentYear++;
    }
    render(currentYear, currentMonth);
  };

  // 选中对应的日期
  // 事件代理
  var calendarMain = document.querySelector(".calendar-main");
  var prevEl = null; // 前一个被选中的元素
  calendarMain.onclick = function (e) {
    var target = e.target;
    var bool = target.classList.contains("td");
    if (!bool) return;

    // 首先处理样式变化
    if (prevEl) prevEl.classList.remove("active");
    target.classList.add("active");
    prevEl = target;

    // 判断当前点的是上个月的日期,还是下个月的日期
    var prevBool = target.classList.contains("prev-month"); // 如果为true,表示点的是上个月日期
    var nextBool = target.classList.contains("next-month"); // 如果为true,表示点的是上个月日期

    // 点的是上个月日期 或下个月的日期时,才会重新触发渲染
    if (prevBool) {
      // 点了上个月日期
      currentMonth--;
      if (currentMonth === 0) {
        currentMonth = 12;
        currentYear--;
      }
      // 重新渲染
      var selectDate = target.innerText;
      render(currentYear, currentMonth, selectDate);
    } else if (nextBool) {
      currentMonth++;
      if (currentMonth === 13) {
        currentMonth = 1;
        currentYear++;
      }
      // 重新渲染
      var selectDate = target.innerText;
      render(currentYear, currentMonth, selectDate);
    }

    // 得到当前选中的日期
    console.log(
      currentYear + "年" + currentMonth + "月" + target.innerText + "日"
    );
  };
</script>

# 四、重难点总结

TIP

总结本章重难点知识,理清思路,把握重难点。并能轻松回答以下问题,说明自己就真正的掌握了。

用于故而知新,快速复习。

# 1、重点

TIP

  • 掌握Math.PIMath.powMath.sqrtMath.floorMath.roundMath.ceilMath.randomMath.minMath.max方法及应用
  • 创建日期的四种形式
  • 日期对象的掌见方法:getFullYeargetMonth()getDate()、getDay、getHours、getMinutes、getSeconds、getMilliseconds
  • 掌握时间戳 getTime()方法

# 2、难点

TIP

  • 掌握Math.atan2方法及应用
  • 手写元素跟随鼠标旋转特效
  • 手写活动倒计时功能
  • 手写日历表
上次更新时间: 6/8/2023, 9:23:17 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X