项目概述DataImputer 是一个基于 Web 的Water Supply Systems数据修补系统。采用图傅里叶变换原理，针对供水管网的流量、压力时间序列进行数据修补工作。方法论部分请参考论文：ZHOU X, MAN Y, LIU S, et al. 2024. Leveraging multi-level correlations for imputing monitoring data in water supply systems using graph signal sampling theory. Water Research X [J], 25: 100274.项目地址为：https://group.zhouxiao.tech/imputation.html主要功能为允许用户上传含有缺失值的供水管网流量、压力数据文件，进行自动化的数据修补工作。系统能够对缺失的数据进行插补，并在处理完成后生成图表以可视化结果，支持下载处理后的文件。该系统设计成全自动化流程，简化用户操作，提高数据预处理效率。系统架构项目实现过程中，核心包括前端页面设计、后端数据处理逻辑、跨域处理以及服务器配置优化。主要技术栈为：前端页面和交互逻辑主要由 HTML、CSS、JavaScript 和 jQuery 组成；后端由 Flask 提供 API 支持，包括文件上传、数据处理、图表生成和文件下载；数据处理基于 Pandas 和 NumPy 实现；图表使用 pyecharts 生成；通过 AJAX 技术进行前后端通信。前端设计前端核心框架结构为： 1. 上传文件 2. 运行算法 3. 保存文件 主要分为三部分：上部显示区域：用于显示可交互图表；操作区域：主要是三个按钮（1. 上传文件、2. 运行算法、3. 保存文件）；底部消息框：为用户展示前后端反馈消息。主要样式如下所示：成品展示前端部分技术不需要详细阐述，有问题直接 F12 控制台查看源代码。有一个设计点需要注意：考虑到用户上传的时间序列可能比较长，且大部分研究者日常工作都会使用PC进行访问本网站，因此 的宽度要尽量宽，而不是局限于父类元素的100%，这里采用的是绝对定位的方法将其与父元素居中的同时，宽度可以达到：@media (min-width: 800px) { #display-area { width: 90vw; } }这样就完美地同时顾及了页面设计美学和用户体验。JS工具函数COOKIE设置相关函数首先介绍几个主要的COOKIE工具函数，首先是设置Cookie和读取Cookie的函数：// 设置 Cookie 的函数 function setCookie(name, value, hours) { const date = new Date(); date.setTime(date.getTime() + (hours * 60 * 60 * 1000)); // 设置过期时间 document.cookie = `${name}=${value}; expires=${date.toUTCString()}; path=/`; } // 获取 Cookie 的函数 function getCookie(name) { const cookieArr = document.cookie.split("; "); for (let i = 0; i < cookieArr.length; i++) { const cookiePair = cookieArr[i].split("="); if (name === cookiePair[0]) { return decodeURIComponent(cookiePair[1]); } } return null; }然后是每次刷新页面或者第一次进入页面的恢复函数：function restorePreviousUpload() { const savedFilename = getCookie('uploadedFilename'); if (savedFilename) { $('#control-buttons').attr('data-filename', savedFilename); // 发送 AJAX 请求，让后端检查是否存在该文件并处理 $.ajax({ url: 'https://impute.zhouxiao.tech/upload', // 与后端的上传和检查接口一致 type: 'POST', data: { filename: savedFilename }, // 仅发送文件名，后端将根据文件名查找文件 success: function (response) { const message = ` ${response.message}. Start: [${response.start_time}], End: [${response.end_time}], Time interval: [${response.frequency}], Nan Info: [${response.missing_summary}]. `; displayMessage(message, "SUCCESS"); // 显示图表并启用“运行算法”按钮 $('#chart-frame').attr('srcdoc', response.chart_html); $('#run-btn').prop('disabled', false); }, error: function (error) { displayMessage(`无法找到文件 ${savedFilename}, 请重新上传.`, "ERROR"); $('#run-btn').prop('disabled', true); // 禁用“运行算法”按钮 } }); } }该函数的主要作用是实现了：当用户不小心刷新或者关闭了页面，不需要重新进行文件上传，而是直接读取COOKIE中的文件名传递给Server端进行处理。因此Server端文件是每六个小时清理一次，如果距离用户最近一次上传文件不到6个小时，那么不需要其重新上传，可以直接读取 Server 端保存的文件。文件时间戳重命名函数function generateUniqueFilename(originalName) { const timestamp = Date.now(); // 生成时间戳 const extension = originalName.split('.').pop(); // 获取文件扩展名 return `file_${timestamp}.${extension}`; }消息通知函数该函数的作用是添加一条信息到底部消息框用以告诉用户必要的信息，根据不同的样式将消息以不同颜色进行区分：level = "INFO"：灰色；level = "SUCCESS"：绿色；level = "WARNING"：黄色；level = "ERROR"：红色。function displayMessage(message, level = "INFO") { const $messageList = $('#message-list'); // 使用jQuery选择器 const timestamp = new Date().toLocaleTimeString(); // 根据等级确定消息前缀和样式 let prefix; let color; switch (level.toUpperCase()) { case 'ERROR': prefix = "[ERROR]"; color = "red"; break; case 'WARNING': prefix = "[WARNING]"; color = "orange"; break; case 'SUCCESS': prefix = "[SUCCESS]"; color = "green"; break; case 'INFO': default: prefix = "[INFO]"; color = "#ccc"; break; } prefix += " - "; // 移除之前消息的加粗样式 $messageList.find('li').css("font-weight", "normal"); // 创建并添加新消息项，并设置加粗样式 const $newMessageItem = $(` ${timestamp} ${prefix} ${message} `); $messageList.append($newMessageItem); // 平滑滚动至底部 $('#message-box').animate({ scrollTop: $messageList.prop("scrollHeight") }, 300); }文件输入由于HTML默认的文件输入框很难看，因此大部分开发者都会将其直接隐藏，通过JS来实现点击按钮上传文件：// 点击按钮触发文件输入框 $('#upload-btn').on('click', function () { $('#file-input').click(); });上传文件与GDI运行时间统计为了缓解1G1核服务器在网速和运行速度带给用户的压力，在用户上传文件或者 Server 端运行计算服务期间，给用户来点动画和消息提示——让用户知道我在努力工作，只是服务器配置太低，运行速度慢！！！// 动态图标显示，带耗时 function updateLastMessageIcon(status, startTime) { const $lastMessage = $('#message-list').find('li').last(); const icons = { start: ` `, success: ` `, error: ` ` }; const iconHtml = icons[status.toLowerCase()]; if (status.toLowerCase() === 'start') { $lastMessage.find('.gdi-icon').remove(); $lastMessage.append(`已耗时 [0分0秒].`); $lastMessage.append(iconHtml); startTimer($lastMessage, startTime); } else { clearInterval(timerInterval); const totalTime = getElapsedTime(startTime); const $loadingMessage = $('#message-list').find('li:has(.gdi-loading-icon)'); // 移除之前的start图标和耗时显示 $loadingMessage.find('.gdi-icon').remove(); $loadingMessage.find('.elapsed-time').remove(); // 添加结束耗时文本和新的图标 $loadingMessage.append(`耗时 [${totalTime}] 已结束.`); $loadingMessage.append(iconHtml); } } // 启动动态计时器 function startTimer($lastMessage, startTime) { timerInterval = setInterval(() => { const elapsedTime = getElapsedTime(startTime); $lastMessage.find('.elapsed-time').text(`已耗时 [${elapsedTime}].`); }, 1000); } // 获取耗时 function getElapsedTime(startTime) { const now = Date.now(); const elapsed = Math.floor((now - startTime) / 1000); const minutes = Math.floor(elapsed / 60); const seconds = elapsed % 60; return `${minutes}分${seconds}秒`; }算法参数有效性检查在提交表单到 Server 前，对算法参数进行检查：function validateParameters() { const fillMethod = $('#fill-method').val(); // 获取填充方式 const nStd = parseFloat($('#n-std').val()); // 获取N倍标准差 const f_p = parseFloat($('#sor-0').val()); // 低频项数量 const sigma_v = parseFloat($('#sor-1').val()); // 重构权重 if (['li', 'cv', 'svr', 'gru'].includes(fillMethod)) { displayMessage(`填充方式 '${fillMethod}' 暂未上线，请选择其他填充方式。`, "ERROR"); return false; } if (isNaN(nStd) || nStd > 10) { displayMessage(`N倍标准差应为数值，且不能大于 10，请重新输入。`, "ERROR"); return false; } if (isNaN(f_p) || f_p <= 0) { displayMessage(`参数 \\(F_p\\) 不能为空且必须大于 0，请重新输入。`, "ERROR"); return false; } if (isNaN(sigma_v) || sigma_v <= 0) { displayMessage(`参数 \\(\\sigma_v\\) 不能为空且必须大于 0，请重新输入。`, "ERROR"); return false; } // 所有检查通过 return true; }一键复制系统日志$('#copy-log-btn').on('click', function () { let logText = ''; $('#message-list li').each(function () { logText += ' ➤ ' + $(this).text() + '\n'; // 添加箭头符号 }); // 创建临时 textarea 用于复制 const tempInput = $(' '); $('body').append(tempInput); tempInput.val(logText).select(); document.execCommand('copy'); tempInput.remove(); // 显示提示信息 VOID.alert("日志信息已复制到剪贴板！"); });操作区域操作区域的第一个按钮是1. 上传文件，其点击事件的主要逻辑为：向底部消息框输入一条消息，告诉用户正在上传文件，displayMessage()函数实现；以当前时间戳创建一个文件名，对上传文件进行重命名并构建上传表单；与 Server 进行AJAX通信：如果上传并处理文件成功，返回message举例为：3:12:42 PM [SUCCESS] - Found the file on GDI Server. Start: [2020-03-01 00:00:00], End: [2020-09-30 23:45:00], Time interval: [0 days 00:15:00], Nan Info: [Volume: 7795, Volume_02: 7795, Volume_03: 7795]，主要介绍了数据的时间序列范围、采样频率以及各类数据的缺失情况。同时，response.chart_html为绘制的可视化JS交互图形，需要将其写入到$('#chart-frame')中，然后并开始2. 运行算法按钮。需要注意的是，此时还要将文件名写入到COOKIE中，方便用户后续操作。如果上传失败或者 Server 对文件检测估过程中出现问题，那么就会返回错误信息，具体的错误视情况而定。$('#file-input').on('change', function (event) { const startTime = Date.now(); // 记录任务开始时间 $('#save-btn').prop('disabled', true); const file = event.target.files[0]; if (file) { displayMessage("正在向服务器端发送AJAX请求, "); updateLastMessageIcon('start', startTime); const uniqueFilename = generateUniqueFilename(file.name); $('#control-buttons').attr('data-filename', uniqueFilename); const formData = new FormData(); formData.append('file', new File([file], uniqueFilename)); formData.append('filename', uniqueFilename); $.ajax({ url: 'https://impute.zhouxiao.tech/upload', type: 'POST', data: formData, processData: false, contentType: false, success: function (response) { updateLastMessageIcon('success', startTime); // 传递开始时间 const message = ` ${response.message}, Start: [${response.start_time}], End: [${response.end_time}], Time interval: [${response.frequency}], Nan Info: [${response.missing_summary}]. `; displayMessage(message, "SUCCESS"); $('#chart-frame').attr('srcdoc', response.chart_html); $('#run-btn').prop('disabled', false); setCookie('uploadedFilename', uniqueFilename, 6); }, error: function (xhr) { updateLastMessageIcon('error', startTime); // 传递开始时间 let errorMessage; if (xhr.status === 413) { errorMessage = "文件大小超出限制，请选择小于50MB的文件。"; } else if (xhr.responseJSON && xhr.responseJSON.error) { errorMessage = `文件上传失败: ${xhr.responseJSON.error}`; } else { errorMessage = `文件上传失败: 未知错误。`; } displayMessage(errorMessage, "ERROR"); } }); } });当上传文件并且文件通过检测时，2. 运行算法 按钮将被开启。$('#run-btn').on('click', function () { if (!validateParameters()) { return; } const startTime = Date.now(); // 记录任务开始时间 $('#display-area').addClass('border-animating'); const params = { fill_method: $('#fill-method').val(), n_std: $('#n-std').val(), use_sor: $('input[name="use-sor"]:checked').val(), sor_0: $('#sor-0').val(), sor_1: $('#sor-1').val(), filename: $('#control-buttons').attr('data-filename') }; // 开始任务请求 $.ajax({ url: 'https://impute.zhouxiao.tech/process', type: 'POST', data: params, success: function (response) { const taskId = response.task_id; displayMessage(`Task ID: [${taskId}], GDI Server is running, `); updateLastMessageIcon('start', startTime); // 传递开始时间 pollTaskStatus(taskId, startTime); }, error: function () { updateLastMessageIcon('error'); displayMessage("任务启动失败", "ERROR"); } }); });轮询任务状态：function pollTaskStatus(taskId, startTime) { const intervalId = setInterval(function () { $.get(`https://impute.zhouxiao.tech/check_task/${taskId}`, function (response) { const status = response.status; if (status === "completed") { clearInterval(intervalId); updateLastMessageIcon('success', startTime); $('#chart-frame').attr('srcdoc', response.result.chart_html); $('#save-btn').prop('disabled', false).attr('data-url', response.result.download_url.replace(/^http:/, 'https:')); $('#display-area').removeClass('border-animating'); // 完成 displayMessage('任务完成.', "SUCCESS"); } else if (status === "failed") { clearInterval(intervalId); updateLastMessageIcon('error', startTime); $('#display-area').removeClass('border-animating'); displayMessage(`任务失败: [${response.result}].`, "ERROR"); } else { console.log('GDI Running...') } }).fail(function () { displayMessage("无法获取任务状态，可能任务已失效", "ERROR"); clearInterval(intervalId); }); }, 4000); }当GDI算法运行完毕结束，3. 文件下载 按钮将被开启。// 下载打包文件 document.getElementById('save-btn').addEventListener('click', function () { const url = this.getAttribute('data-url'); window.location.href = url; });环境配置Python项目管理创建虚拟环境：cd /www/wwwroot/DataImputer python3 -m venv venv项目启动命令：/www/wwwroot/DataImputer/venv/bin/python /www/wwwroot/DataImputer/app.py激活虚拟环境：source /www/wwwroot/DataImputer/venv/bin/activate开启Flask Web服务：python app.pyAPACHE环境配置Server端下载文件夹的访问APACHE默认禁止用户端下载服务器端文件，用户浏览器直接访问服务器端文件显示502 Bad Gateway，开启网站下载文件夹的访问：# 下载设置 location /Downloads/ { alias /www/wwwroot/DataImputer/Downloads/; # 请替换为实际文件夹路径 autoindex off; # 可选：允许目录浏览 }Flask获取网站HTTPS域名后端Flask中默认使用from flask import request print(request.host_url)获取到的是服务器 IP + 端口号，例如：https://126.1.0.1:86/。直接把IP地址暴露在互联网是一种裸奔行为，为了文明起见，最好还是将其更换为网站域名：屏蔽APACHE配置文件的第65行：# proxy_set_header Host 127.0.0.1:$server_port;更换为：proxy_set_header Host $host;
每次更改或新增代码后，一定要及时查看控制台是否有报错，使用PC和移动端多次访问确保无误！！！网站设置网站后台设置基于宝塔面板。备份设置需要备份的东西都直接在宝塔后台备份就行，备份网站程序和数据库两个压缩包文件就行了。如果重装网站后更改了数据库账号密码，那么就直接在Typecho程序根目录下的config.inc.php文件中第56行更改数据库连接设置就行了：/** 定义数据库参数 */ $db->addServer(array ( 'host' => 'localhost', 'user' => '****', 'password' => '******', 'charset' => 'utf8', 'port' => '*******', 'database' => '******', ), Typecho_Db::READ | Typecho_Db::WRITE);网站所有都调整完毕后，可以在宝塔后台安装七牛云官方备份程序：可以自动将程序备份至七牛云账户。伪静态设置在宝塔网站设置中添加伪静态规则：if (!-e $request_filename) { rewrite ^(.*)$ /index.php$1 last; }然后在Typecho后台设置地址重写：CDN加速将主题下的assets文件夹全部复制至七牛云CDN，使用CDN加速全部主题资源。本站主题资源assets资源已全部放入七牛云CDN，如果需要进行文件修改，修改后需要在七牛云CDN中的刷新预取中刷新文件链接才会完成文件更新。使用自制404页面在宝塔后台打开网站配置文件，取消注释第28行：#ERROR-PAGE-START 错误页配置，可以注释、删除或修改 error_page 404 /404.html;然后在站点根目录重写404.html。流量限制后台登陆加密SQL语句学习将cid=1651的文章评论全部转移至cid=1561的文章下面：update typecho_comments set cid=1561 where cid=1651;在typecho_contents表中选择cid=1390的文章：SELECT * FROM `typecho_contents` WHERE cid=1390;插件设置ExSearch第一次使用该插件需要重建索引。该插件中不能引入JQuery，因为在主题中已经引入，如果再次引入，博客主页会出现空白。Meting问题：网易云歌单只能解析第一首歌曲，网易云无论是自己收藏的歌单还是公开歌单，均只能获取第一首，歌手与专辑正常。解决方案：插件后台填写网易云音乐Cookie。获取方法：打开 https://music.163.com/ ，F12-网络-再次刷新找到 music.163.com项，点击进去找到 cookie的值复制。如果网站开启PJAX局部刷新，需要设置重载函数：loadMeting(); // 跳转页面后重新解析播放器 if (typeof aplayers !== 'undefined') { // 跳转页面后暂停播放 for (var i = 0; i < aplayers.length; i++) { try { aplayers[i].destroy() } catch (e) {} } }解决（APlayer-Typecho）插件失效问题：可以通过替换插件中的include/Meting.php解决。访问地址：https://github.com/metowolf/Meting，然后复制src/Meting.php代码到插件中QQ音乐即可恢复。原因是插件中的Meting版本为（1.5.2）太过老旧，更换为最新的Meting（1.5.11）即可。音乐播放提示：// Meting 音乐播放提示 $('.aplayer-body').each(function () { // 单曲模块播放 $(this).find('div.aplayer-pic').click(function () { let songTitle = $(this).next().find('.aplayer-title').text(), songAuthor = $(this).next().find('.aplayer-author').text(); if (!$(this).find("*").eq(0).hasClass('aplayer-play')) { VOID.alert('开始播放' + songAuthor + '的《' + songTitle + '》~') } else { VOID.alert('音乐已暂停~') } }) // 歌单列表播放模块 $(this).next().find('ol>li').click(function () { let songTitle = $(this).find('.aplayer-list-title').text(), songAuthor = $(this).next().find('.aplayer-list-author').text(); VOID.alert('开始播放' + songAuthor + '的《' + songTitle + '》~') }) })Sticky美化代码： [置顶] 网站美化与优化前期也写过一些关于网站前端美化的文章，加载了自己以前的调整代码：网站前端修改网站前端美化OwO表情按钮及弹窗#comments form .comment-buttons .OwO .OwO-body { min-width: 294px } .OwO-body { z-index: 24!important } .OwO .OwO-body .OwO-items .OwO-item { padding: 0; } #comments form .comment-buttons .OwO .OwO-body .OwO-items-image .OwO-item { max-width: calc(25% - 10px)!important } #comments form .comment-buttons .OwO { left: 12px; width: auto; top: -60px }设置小齿轮位置调整@media screen and (max-width: 767.5px) { .setting-panel-show #setting-panel { top: 133px } }评论区调整如果文章禁止评论，那么直接评论区什么也不输出：数据库支持Emoji表情原生Typecho的数据库编码是utf8，不支持直接插入Emoji表情。要想数据库支持Emoji表情，就得使用utf8mb4编码来支持，于是我们需要修改已有数据库的编码格式，好消息是utf8mb4是utf-8的超集，完全兼容utf-8，修改后，不会影响现有数据。直接插入Emoji表情写入数据库报错首先修改MySQL数据库编码，连接数据库：mysql -u root -p切换到Typecho数据库：use 数据库名;然后执行以下SQL语句：alter table typecho_comments convert to character set utf8mb4 collate utf8mb4_general_ci; alter table typecho_contents convert to character set utf8mb4 collate utf8mb4_general_ci; alter table typecho_fields convert to character set utf8mb4 collate utf8mb4_general_ci; alter table typecho_metas convert to character set utf8mb4 collate utf8mb4_general_ci; alter table typecho_options convert to character set utf8mb4 collate utf8mb4_general_ci; alter table typecho_relationships convert to character set utf8mb4 collate utf8mb4_general_ci; alter table typecho_users convert to character set utf8mb4 collate utf8mb4_general_ci;然后修改网站配置文件修改config.inc.php：'charset' => 'utf8mb4', # 修改编码为 utf8mb4测试：?️??????????❤️。不要忘了每次使用phpMyAdmin操作数据的时候，也要把服务器连接排序规则设置为utf8mb4。经过实际操作，发现虽然这样可以使得数据库中存入Emoji表情，但是当数据库文件导出后，里面的Emoji表情就全部变成了问号……，暂时还没有找到解决办法~词义化时间在Typecho的后台，很多地方都是用这种 “XX前” 的词义化时间显示的，简单明了。从这里我们就可以得出，后台是有相关函数，我们只要找到，然后直接使用就可以了—— var/Typecho/I18n.php 。但是官方提供的代码没有“周”和“月”，所以动手完善了一下代码，添加了“周”和“月”，只有1年以及1年以上的时间才会显示完整时间。这样可能比较好点吧。/** * 词义化时间 * * @access public * @param string $from 起始时间 * @param string $now 终止时间 * @return string */ public static function dateWord($from, $now) { $between = $now - $from; /** 如果是一天 */ if ($between >= 0 && $between < 86400 && date('d', $from) == date('d', $now)) { /** 如果是一小时 */ if ($between < 3600) { /** 如果是一分钟 */ if ($between < 60) { if (0 == $between) { return _t('刚刚'); } else { return str_replace('%d', $between, _n('刚刚', '%d秒前', $between)); } } $min = floor($between / 60); return str_replace('%d', $min, _n('1分钟前', '%d分钟前', $min)); } $hour = floor($between / 3600); return str_replace('%d', $hour, _n('1小时前', '%d小时前', $hour)); } /** 如果是昨天 */ if ($between > 0 && $between < 172800 && (date('z', $from) + 1 == date('z', $now) // 在同一年的情况 || date('z', $from) + 1 == date('L') + 365 + date('z', $now))) { // 跨年的情况 return _t('昨天 %s', date('H:i', $from)); } /** 如果是一个星期以内 */ if ($between > 0 && $between < 604800) { $day = floor($between / 86400); return str_replace('%d', $day, _n('1天前', '%d天前', $day)); } /** 如果是一个星期以上 */ if ($between > 0 && $between < 2592000){ $week = floor($between / 648000); return str_replace('%d', $week, _n('1周前', '%d周前', $week)); } /** 如果是一年以内 */ if ($between > 0 && $between < 31557600){ $month = floor($between / 2629800); return str_replace('%d', $month, _n('1个月前', '%d个月前', $month)); } /** 如果是一年以上 */ if (date('Y', $from) == date('Y', $now)) { return date(_t('Y年n月j日 H:i'), $from); } return date(_t('Y年m月d日 H:i'), $from); }然后在需要输出的地方调用即可，例如：<?php $comments->dateWord(); ?>VOID主题在libs/Comments.php文件中第150行，将原来的：<timedatetime="<?php $this->date('c'); ?>"> <?php $singleCommentOptions->beforeDate(); echo date('Y-m-d H:i', $this->created); $singleCommentOptions->afterDate(); ?> 修改为：<?php $this->dateWord(); ?>即可实现评论时间的词义化。网站用户标识在评论区区分回头客、老顾客、忠实用户、站长等，站长logo学习(zhao chao)了百度网盘会员网站logo的样式?。在主题functions.php文件中增加查询数据库并返回顾客等级的函数：function get_user_level($mail = '') { $db = Typecho_Db::get(); if ($mail) { $count = $db->fetchRow( $db->select('COUNT(*)') ->from('table.comments') ->where('status = ?', 'approved') ->where('mail = ?', $mail) ); $commentnum = $count['COUNT(*)']; if ($commentnum <= 1) { return ''; } elseif ($commentnum <= 2) { return '二进宫'; } elseif ($commentnum <= 3) { return '三回头'; } elseif ($commentnum <= 5) { return '老油条'; } else { return 'OnlyFans'; } } else { // 如果没有输入Email，那么返回评论数据表中所有评论数 $count = $db->fetchRow( $db->select('COUNT(*)') ->from('table.comments') ->where('status = ?', 'approved') ); return $count['COUNT(*)']; } }在主题comments.php适当位置插入： <?php echo 'authorId) { if ($this->authorId == $this->ownerId) { echo ' webmaster">站长'; // 站长增加一个class类名 } } else { if (get_user_level($this->mail)) { echo '">' . get_user_level($this->mail); // 普通用户仅有一个user-logo类名 } else { echo '" style="display:none;">'; // php函数返回值为空，说明该用户仅有一条评论，那么久不输出标识 } } echo ''; ?>然后根据设置的类名，写出对应的css即可：.webmaster { background: linear-gradient(90deg,#ffeccc,#ffd080); color: #64360d !important; } .user-logo { font-size: 80%; display: inline-block; background-color: #20b8d4; padding: 1px 4px; color: white; border-radius: 3px; }快捷键提交评论为评论框添加onkeydown事件，当按下快捷键Ctrl+Enter的时候，JS模拟点击评论表单的提交按钮。 评论框背景评论框样式来源于Handsome主题。实现效果添加css属性即可：#textarea { background-image: #ffffff url('背景图片url地址') no-repeat right; transition: all 0.25s ease-in-out 0s; background-size: contain; } #textarea:focus { background-position-y: 150px; border-color: #448bff; box-shadow: 0 0 0 .2rem rgba(68,139,255,.25); }评论框随机昵称JS部分：// 随机昵称实现 var a = null, b = ["大名鼎鼎", "躲闪", "骄傲", "挺胸", "不知名", "知名", "刚下飞机", "看透一切", "小有名气", "潜心学习"], c = ["大黄", "匿名人士", "高质量男性", "女士", "人士", "男孩", "女孩", "贫僧", "道士", "学生", "打工人", "路人甲", "炮灰已", "流氓丙", "土匪丁", "龙套戊"]; $("#get-nickname").on("click", function () { const d = $(this); $(this).removeClass("shake"), $(this).addClass("shake"), null != a && (clearTimeout(a), a = null), a = setTimeout(function () { d.removeClass("shake") }, 500), _tmp = b[Math.floor(Math.random() * b.length)] + "的" + c[Math.floor(Math.random() * c.length)], $(this).prev().val(_tmp) } )前端css动画：.shake { animation-name: shake; animation-duration: .1s; animation-timing-function: ease-in-out; animation-iteration-count: infinite; display: inherit; transform-origin: center; } @keyframes shake { 2% { transform: translate(-1.5px, calc(-50% + .5px)) rotate(1.5deg); } 4% { transform: translate(2.5px, calc(-50% + .5px)) rotate(-.5deg); } 6% { transform: translate(1.5px, calc(-50% + 1.5px)) rotate(1.5deg); } 8% { transform: translate(1.5px, calc(-50% + .5px)) rotate(1.5deg); } 10% { transform: translate(-.5px, calc(-50% - .5px)) rotate(.5deg); } 12% { transform: translate(-1.5px, calc(-50% - 1.5px)) rotate(-.5deg); } 14% { transform: translate(2.5px, calc(-50% + 1.5px)) rotate(1.5deg); } 16% { transform: translate(-.5px, calc(-50% - .5px)) rotate(1.5deg); } 18% { transform: translate(-1.5px, calc(-50% - .5px)) rotate(.5deg); } 20% { transform: translate(-.5px, calc(-50% + 1.5px)) rotate(.5deg); } 22% { transform: translate(2.5px, calc(-50% + 2.5px)) rotate(1.5deg); } 24% { transform: translate(-.5px, calc(-50% + 1.5px)) rotate(-.5deg); } 26% { transform: translate(.5px, calc(-50% - .5px)) rotate(.5deg); } 28% { transform: translate(1.5px, calc(-50% + .5px)) rotate(.5deg); } 30% { transform: translate(1.5px, calc(-50% + 2.5px)) rotate(1.5deg); } 32% { transform: translate(-1.5px, calc(-50% - .5px)) rotate(.5deg); } 34% { transform: translate(.5px, calc(-50% + 1.5px)) rotate(.5deg); } 36% { transform: translate(-1.5px, calc(-50% + .5px)) rotate(-.5deg); } 38% { transform: translate(1.5px, calc(-50% - .5px)) rotate(-.5deg); } 40% { transform: translate(2.5px, calc(-50% - .5px)) rotate(.5deg); } 42% { transform: translate(2.5px, calc(-50% + 1.5px)) rotate(1.5deg); } 44% { transform: translate(.5px, calc(-50% + 1.5px)) rotate(1.5deg); } 46% { transform: translate(.5px, calc(-50% + 1.5px)) rotate(.5deg); } 48% { transform: translate(-1.5px, calc(-50% - 1.5px)) rotate(-.5deg); } 50% { transform: translate(-.5px, calc(-50% - .5px)) rotate(-.5deg); } 52% { transform: translate(-1.5px, calc(-50% - .5px)) rotate(-.5deg); } 54% { transform: translate(1.5px, calc(-50% + 1.5px)) rotate(.5deg); } 56% { transform: translate(.5px, calc(-50% - 1.5px)) rotate(-.5deg); } 58% { transform: translate(-.5px, calc(-50% - 2.5px)) rotate(1.5deg); } 60% { transform: translate(2.5px, calc(-50% + 1.5px)) rotate(-.5deg); } 62% { transform: translate(2.5px, calc(-50% - 1.5px)) rotate(-.5deg); } 64% { transform: translate(-.5px, calc(-50% - 2.5px)) rotate(1.5deg); } 66% { transform: translate(1.5px, calc(-50% - .5px)) rotate(1.5deg); } 68% { transform: translate(.5px, calc(-50% - 1.5px)) rotate(1.5deg); } 70% { transform: translate(.5px, calc(-50% - .5px)) rotate(1.5deg); } 72% { transform: translate(2.5px, calc(-50% - .5px)) rotate(.5deg); } 74% { transform: translate(-.5px, calc(-50% + 2.5px)) rotate(1.5deg); } 76% { transform: translate(1.5px, calc(-50% - .5px)) rotate(.5deg); } 78% { transform: translate(2.5px, calc(-50% - .5px)) rotate(1.5deg); } 80% { transform: translate(1.5px, calc(-50% + 2.5px)) rotate(-.5deg); } 82% { transform: translate(-1.5px, calc(-50% + .5px)) rotate(-.5deg); } 84% { transform: translate(.5px, calc(-50% - .5px)) rotate(-.5deg); } 86% { transform: translate(-1.5px, calc(-50% + .5px)) rotate(.5deg); } 88% { transform: translate(2.5px, calc(-50% + .5px)) rotate(.5deg); } 90% { transform: translate(1.5px, calc(-50% + 2.5px)) rotate(.5deg); } 92% { transform: translate(-1.5px, calc(-50% + 1.5px)) rotate(-.5deg); } 94% { transform: translate(-.5px, calc(-50% + 2.5px)) rotate(1.5deg); } 96% { transform: translate(1.5px, calc(-50% + .5px)) rotate(.5deg); } 98% { transform: translate(1.5px, calc(-50% - .5px)) rotate(-.5deg); } 0%, 100% { transform: translate(0, -50%) rotate(0); } }注意，这里使用了一种特征的CSS写法，即百分比与px单位同时出现，需要使用语法例如calc(10% - 5px)。之所以这样搞是因为div定位使用了transform: translateY(-50%);，如果你的div没有使用这个属性，那你不需要使用这种写法。前端html： ? 实现效果评论框placeholder动态变化// placehold变化 $("#textarea").focus(function () { $(this).attr("placeholder", "说点什么吧~") }); $("#textarea").blur(function () { $(this).attr("placeholder", "居然什么也不说，哼~") });评论框随机头像在主题functions.php文件中增加返回随机图片地址的函数：function local_random_avatar() { $options = Typecho_Widget::widget('Widget_Options'); $thumb = 'https://cdn.manyacan.com/blog/random_avatar/' . rand(1, 60) . '.png'; // $avatar = " "; echo $thumb; }由函数可得，其实就是产生一个随机数，然后把文件路径中的图片全部按顺序命名为数字即可，在合适的地方调用：<img class="author-avatar" src="<?php local_random_avatar();?>">替换Gravatar头像为Cravatar头像Cravatar是Gravatar在中国的完美替代方案，你可以在https://cravatar.cn更新你的头像。define('__TYPECHO_GRAVATAR_PREFIX__', 'https://cravatar.cn/avatar/');Cravatar可以默认直接对QQ邮箱解析QQ头像，YYDS！然后对var/Typecho/Common.php文件中的函数进行修改（932行），增加一个请求参数，当评论用户既没有Gravatar头像也没用QQ邮箱时，解析为随机头像：public static function gravatarUrl($mail, $size, $rating, $default, $isSecure = false) { if (defined('__TYPECHO_GRAVATAR_PREFIX__')) { $url = __TYPECHO_GRAVATAR_PREFIX__; } else { $url = $isSecure ? 'https://secure.gravatar.com' : 'http://www.gravatar.com'; $url .= '/avatar/'; } if (!empty($mail)) { $url .= md5(strtolower(trim($mail))); } $url .= '?s=' . $size; $url .= '&r=' . $rating; $url .= '&d=' . $default; // return $url; return $url .='retro'; // 增加参数 }参数设置显示评论IP归属地方法一：直接引入JS文件解析IP地址。来自<span id="ip-<?php $this->theId(); ?>"> <script referrerpolicy="no-referrer" src="https://whois.pconline.com.cn/jsDom.jsp?level=3&domId=ip-<?php $this->theId(); ?>&ip=<?php echo $this->ip; ?>"> 不支持Pjax刷新，不知道怎么改代码。方法二：在网站根目录下上传qqwry.dat文件，然后在主题functions.php文件中添加函数：function convertip($ip) { $ip1num = 0; $ip2num = 0; $ipAddr1 = ""; $ipAddr2 = ""; $dat_path = './qqwry.dat'; if (!preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/", $ip)) { return 'IP 数据库路径不对'; } if (!$fd = @fopen($dat_path, 'rb')) { return 'IP 数据库路径不正确'; } $ip = explode('.', $ip); $ipNum = $ip[0] * 16777216 + $ip[1] * 65536 + $ip[2] * 256 + $ip[3]; $DataBegin = fread($fd, 4); $DataEnd = fread($fd, 4); $ipbegin = implode('', unpack('L', $DataBegin)); if ($ipbegin < 0) $ipbegin += pow(2, 32); $ipend = implode('', unpack('L', $DataEnd)); if ($ipend < 0) $ipend += pow(2, 32); $ipAllNum = ($ipend - $ipbegin) / 7 + 1; $BeginNum = 0; $EndNum = $ipAllNum; while ($ip1num > $ipNum || $ip2num < $ipNum) { $Middle = intval(($EndNum + $BeginNum) / 2); fseek($fd, $ipbegin + 7 * $Middle); $ipData1 = fread($fd, 4); if (strlen($ipData1) < 4) { fclose($fd); return 'System Error'; } $ip1num = implode('', unpack('L', $ipData1)); if ($ip1num < 0) $ip1num += pow(2, 32); if ($ip1num > $ipNum) { $EndNum = $Middle; continue; } $DataSeek = fread($fd, 3); if (strlen($DataSeek) < 3) { fclose($fd); return 'System Error'; } $DataSeek = implode('', unpack('L', $DataSeek . chr(0))); fseek($fd, $DataSeek); $ipData2 = fread($fd, 4); if (strlen($ipData2) < 4) { fclose($fd); return 'System Error'; } $ip2num = implode('', unpack('L', $ipData2)); if ($ip2num < 0) $ip2num += pow(2, 32); if ($ip2num < $ipNum) { if ($Middle == $BeginNum) { fclose($fd); return 'Unknown'; } $BeginNum = $Middle; } } $ipFlag = fread($fd, 1); if ($ipFlag == chr(1)) { $ipSeek = fread($fd, 3); if (strlen($ipSeek) < 3) { fclose($fd); return 'System Error'; } $ipSeek = implode('', unpack('L', $ipSeek . chr(0))); fseek($fd, $ipSeek); $ipFlag = fread($fd, 1); } if ($ipFlag == chr(2)) { $AddrSeek = fread($fd, 3); if (strlen($AddrSeek) < 3) { fclose($fd); return 'System Error'; } $ipFlag = fread($fd, 1); if ($ipFlag == chr(2)) { $AddrSeek2 = fread($fd, 3); if (strlen($AddrSeek2) < 3) { fclose($fd); return 'System Error'; } $AddrSeek2 = implode('', unpack('L', $AddrSeek2 . chr(0))); fseek($fd, $AddrSeek2); } else { fseek($fd, -1, SEEK_CUR); } while (($char = fread($fd, 1)) != chr(0)) $ipAddr2 .= $char; $AddrSeek = implode('', unpack('L', $AddrSeek . chr(0))); fseek($fd, $AddrSeek); while (($char = fread($fd, 1)) != chr(0)) $ipAddr1 .= $char; } else { fseek($fd, -1, SEEK_CUR); while (($char = fread($fd, 1)) != chr(0)) $ipAddr1 .= $char; $ipFlag = fread($fd, 1); if ($ipFlag == chr(2)) { $AddrSeek2 = fread($fd, 3); if (strlen($AddrSeek2) < 3) { fclose($fd); return 'System Error'; } $AddrSeek2 = implode('', unpack('L', $AddrSeek2 . chr(0))); fseek($fd, $AddrSeek2); } else { fseek($fd, -1, SEEK_CUR); } while (($char = fread($fd, 1)) != chr(0)) { $ipAddr2 .= $char; } } fclose($fd); if (preg_match('/http/i', $ipAddr2)) { $ipAddr2 = ''; } $ipaddr = "$ipAddr1 $ipAddr2"; $ipaddr = preg_replace('/CZ88.NET/is', '', $ipaddr); $ipaddr = preg_replace('/^s*/is', '', $ipaddr); $ipaddr = preg_replace('/s*$/is', '', $ipaddr); if (preg_match('/http/i', $ipaddr) || $ipaddr == '') { $ipaddr = '可能来自火星'; } $ipaddr = iconv('gbk', 'utf-8//IGNORE', $ipaddr); return $ipaddr; }然后在需要comments.php文件中需要输出的位置使用：<?php echo convertip($this->ip); ?>或者：<?php echo convertip($comments->ip); ?>即可将评论者IP地址转化为物理位置。获取站长最后更新时间根据文章数据表table.contents、评论数据表table.comments查询站长最后修改文章、新建文章、评论的最新评论，然后获取最后离开的时间。在主题文件functions.php中添加函数（需要手动填入站长邮箱）：// 获取站长最后来网站的时间（创建文章、修改文章、发表说说） function get_last_update() { $now = time(); $db = Typecho_Db::get(); $prefix = $db->getPrefix(); $create = $db->fetchRow($db->select('created')->from('table.contents')->limit(1)->order('created', Typecho_Db::SORT_DESC)); // 最后创建文章的时间 $update = $db->fetchRow($db->select('modified')->from('table.contents')->limit(1)->order('modified', Typecho_Db::SORT_DESC)); // 最后修改文章的时间 $comment_create = $db->fetchRow($db->select('created')->from('table.comments')->limit(1)->order('created', Typecho_Db::SORT_DESC)->where('author = ?', 'Yacan Man')); // 站长最后评论的时间 $max_timer = max($create['created'], $update['modified'], $comment_create['created']); echo Typecho_I18n::dateWord($max_timer, $now); }然后在需要输出的地方调用即可：<?php echo get_last_update(); ?>文章热度排行无意间从百度热搜看到的热度排行样式，感觉还不错，偷过来学习(chao xi)一下。效果图还不错在主题下的funtions.php中写入函数：function getHotComments($limit = 10) { $db = Typecho_Db::get(); $result = $db->fetchAll( $db->select()->from('table.contents') ->where('status = ?', 'publish') ->where('type = ?', 'post') ->where('created <= unix_timestamp(now())', 'post') //添加这一句避免未达到时间的文章提前曝光 ->limit($limit) ->order('viewsNum', Typecho_Db::SORT_DESC) ); if ($result) { foreach ($result as $val) { $val = Typecho_Widget::widget('Widget_Abstract_Contents')->push($val); $post_title = htmlspecialchars($val['title']); $permalink = $val['permalink']; if ($val['commentsNum'] >= 10) { echo ' '; } else { echo ' '; } // 为什么最后要多输出一个标签？为了换行时伪元素能够被挤下去到第二行 echo '' . $val['title'] . ' 阅读' . $val['viewsNum'] . ' / 评论' . $val['commentsNum'] . ' '; } } }然后再在主题文件夹下的Archives.php文件中的合适地方调用该函数： 热门文章排行 <?php getHotComments('20');?> 最后增加css美化：.popular-articles ol>li:first-child { color: #FE2D46; } .popular-articles ol>li:nth-of-type(2) { color: #F60; } .popular-articles ol>li:nth-of-type(3) { color: #FAA90E; } .popular-articles ol>li.hot i::after { content: '热'; color: #fafafa; position: absolute; background-color: #F60; display: inline-block; border-radius: 5px; padding: 0 3px; right: -1.7em; transform: translateY(-50%); top: 50%; font-style: normal; font-size: 80%; } .popular-articles ol>li.hot i { position: relative; }统计全部文章阅读数public static function getViewNum() { $db= Typecho_Db::get(); $query= $db->select('sum(viewsNum)')->from('table.contents'); $result = $db->fetchAll($query); return $result[0]['sum(`viewsNum`)']; }然后在合适的地方输出即可：<?php echo get_sum_view_num(); ?>文章页末尾调整调整效果首先是PHP判断，只有文章是post类型的时候输出，独立页面不输出：<?php if ($this->is('post')) : ?> // ... <?php endif; ?>文章结束END提示： ----- END ----- 版权信息： 本文作者：<a href="<?php $this->author->permalink(); ?>" rel="author"> <?php $this->author(); ?> 本文链接：<a href="<?php $this->permalink(); ?>"><?php $this->permalink(); ?> 版权声明：本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 。 标签云和点赞调整（使用flex布局）：.tag-and-reward { flex: 2; display: flex; justify-content: space-around; border-top: 1px solid #ccc; padding: 1rem 0; border-bottom: 1px solid #ccc; margin: 1rem 0; } @media screen and (max-width:767px) { .tag-and-reward { flex-direction: column; } .tag-and-reward>div:nth-of-type(1) { margin: 0 auto 1rem; } .tag-and-reward>div:nth-of-type(2) { display: none; } } .tag-and-reward>div:nth-of-type(1) { border: 1px dotted #ccc; height: 70%; padding: .3rem .2rem; width: fit-content } .tag-and-reward>div:nth-of-type(1)>a { margin-bottom: unset; } .tag-and-reward>div:nth-of-type(1)>a:nth-last-of-type(1) { margin-right: unset; } .tag-and-reward>div:nth-of-type(2) { width: 1px; background-color: #ccc; } .tag-and-reward>div:nth-of-type(3) { padding: unset; border: unset; text-align: center; } .tag-and-reward>div:nth-of-type(3)>a { margin-bottom: unset } .tag-and-reward>div:nth-of-type(3)>a:nth-of-type(3) { margin-right: unset }上一篇文章与下一篇文章更改：<?php if ($prev) : ?> <a href="<?php $prev->permalink(); ?>"> <?php $prev->title(); ?> <?php if ($prev->fields->excerpt != '') { echo " {$prev->fields->excerpt} "; } else { // 移动端少输出一点字 if (Utils::isMobile()) $prev->excerpt(60); else $prev->excerpt(80); } ?> <?php else : ?> 考古结束~ <?php endif; ?>移动端展示首页归档页电影页关于页实时搜索文章页
ES6相关概念（★★）什么是ES6ES 的全称是 ECMAScript , 它是由 ECMA 国际标准化组织,制定的一项脚本语言的标准化规范。版本更新为什么使用 ES6 ?每一次标准的诞生都意味着语言的完善，功能的加强。JavaScript语言本身也有一些令人不满意的地方。变量提升特性增加了程序运行时的不可预测性；语法过于松散，实现相同的功能，不同的人可能会写出不同的代码。ES6新增语法let（★★★）ES6中新增了用于声明变量的关键字let声明的变量只在所处于的块级有效 if (true) { let a = 10; } console.log(a) // a is not defined注意：使用let关键字声明的变量才具有块级作用域，使用var声明的变量不具备块级作用域特性。不存在变量提升console.log(a); // a is not defined let a = 20;暂时性死区利用let声明的变量会绑定在这个块级作用域，不会受外界的影响 var tmp = 123; if (true) { tmp = 'abc'; let tmp; } console.log(tmp)经典面试题 var arr = []; for (var i = 0; i < 2; i++) { arr[i] = function () { console.log(i); } } arr[0](); // 2 arr[1](); // 2经典面试题图解：此题的关键点在于变量i是全局的，函数执行时输出的都是全局作用域下的i值。 let arr = []; for (let i = 0; i < 2; i++) { arr[i] = function () { console.log(i); } } arr[0](); // 0 arr[1](); // 1经典面试题图解：此题的关键点在于每次循环都会产生一个块级作用域，每个块级作用域中的变量都是不同的，函数执行时输出的是自己上一级（循环产生的块级作用域）作用域下的i值.小结let关键字就是用来声明变量的使用let关键字声明的变量具有块级作用域在一个大括号中 使用let关键字声明的变量才具有块级作用域 var关键字是不具备这个特点的防止循环变量变成全局变量使用let关键字声明的变量没有变量提升使用let关键字声明的变量具有暂时性死区特性代码：/* --------let关键字就是用来声明变量的-------- */ // let a = 10; // console.log(a); /* --------使用let关键字声明的变量具有块级作用域-------- */ // if (true) { // let b = 20; // console.log(b) // if (true) { // let c = 30; // } // console.log(c); // } // console.log(b) /* -------在一个大括号中 使用let关键字声明的变量才具有块级作用域 var关键字是不具备这个特点的--------- */ if (true) { let num = 100; var abc = 200; } console.log(abc); console.log(num) /* -------防止循环变量变成全局变量--------- */ for (let i = 0; i < 2; i++) {} console.log(i); /*-----使用let关键字声明的变量没有变量提升------*/ console.log(a); // a is not defined let a = 100; /* -------使用let关键字声明的变量具有暂时性死区特性------- */ var num = 10 if (true) { console.log(num); // num不会对外部进行查找 // num is not defined let num = 20; }const（★★★）声明常量，常量就是值（内存地址）不能变化的量具有块级作用域 if (true) { const a = 10; if (true) { const a = 20; console.log(a); // 20 } console.log(a); // 10 } console.log(a) // a is not defined声明常量时必须赋值const PI; // Missing initializer in const declaration常量赋值后，值不能修改const PI = 3.14; PI = 100; // Assignment to constant variable. const ary = [100, 200]; ary[0] = 'a'; // 并没有改变ary的地址 ary[1] = 'b'; console.log(ary); // ['a', 'b']; ary = ['a', 'b']; // Assignment to constant variable. 改变了ary的地址小结const声明的变量是一个常量既然是常量不能重新进行赋值，如果是基本数据类型，不能更改值，如果是复杂数据类型，不能更改地址值声明const时候必须要给定值let、const、var 的区别使用var声明的变量，其作用域为该语句所在的函数内，且存在变量提升现象使用let声明的变量，其作用域为该语句所在的代码块内，不存在变量提升使用const声明的是常量，在后面出现的代码中不能再修改该常量的值区别解构赋值（★★★）ES6中允许从数组中提取值，按照对应位置，对变量赋值，对象也可以实现解构数组解构 let [a, b, c, d, e] = [1, 2, 3]; console.log(a)//1 console.log(b)//2 console.log(c)//3 console.log(d)//undefined console.log(e)//undefined //如果解构不成功，变量的值为undefined对象解构 let person = { name: 'zhangsan', age: 20 }; let { name, age } = person; // 定义名称必须与属性名一致 console.log(name); // 'zhangsan' console.log(age); // 20 let {name: myName, age: myAge} = person; // myName myAge 属于别名 console.log(myName); // 'zhangsan' console.log(myAge); // 20小结解构赋值就是把数据结构分解，然后给变量进行赋值如果结构不成功，变量跟数值个数不匹配的时候，变量的值为undefined数组解构用中括号包裹，多个变量用逗号隔开，对象解构用花括号包裹，多个变量用逗号隔开利用解构赋值能够让我们方便的去取对象中的属性跟方法箭头函数（★★★）ES6中新增的定义函数的方式。() => {} // ()：代表是函数； =>：必须要的符号，指向哪一个代码块；{}：函数体 const fn = () => {} //代表把一个函数赋值给fn fn(); // 调用函数函数体中只有一句代码，且代码的执行结果就是返回值，可以省略大括号 function sum(num1, num2) { return num1 + num2; } //es6写法 const sum = (num1, num2) => num1 + num2; // 在箭头函数中如果函数体中只有一句代码，并且代码的执行结果就是函数的返回值函数体大括号可以省略，等于下面这样定义 // const sum = (num1, num2) => { // return num1 + num2; // };如果形参只有一个，可以省略小括号 function fn (v) { return v; } //es6写法 const fn = v => v;箭头函数不绑定this关键字，箭头函数中的this，指向的是函数定义位置的上下文thisconst obj = { name: '张三'} function fn () { console.log(this); //this 指向 是obj对象 return () => { console.log(this); //this 指向 的是箭头函数定义的位置，那么这个箭头函数定义在fn里面，而这个fn指向是的obj对象，所以这个this也指向是obj对象 } } const resFn = fn.call(obj); resFn();小结箭头函数中不绑定this，箭头函数中的this指向是它所定义的位置，可以简单理解成，定义箭头函数中的作用域的this指向谁，它就指向谁箭头函数的优点在于解决了this执行环境所造成的一些问题。比如：解决了匿名函数this指向的问题（匿名函数的执行环境具有全局性），包括setTimeout和setInterval中使用this所造成的问题面试题var age = 100; var obj = { age: 20, say: () => { alert(this.age) } } obj.say();//箭头函数this指向的是被声明的作用域里面，而对象没有作用域的，所以箭头函数虽然在对象中被定义，但是this指向的是全局作用域剩余参数（★★）剩余参数语法允许我们将一个不定数量的参数表示为一个数组，不定参数定义方式，这种方式很方便的去声明不知道参数情况下的一个函数function sum (first, ...args) { console.log(first); // 10 console.log(args); // [20, 30] } sum(10, 20, 30) 剩余参数和解构配合使用let students = ['wangwu', 'zhangsan', 'lisi']; let [s1, ...s2] = students; console.log(s1); // 'wangwu' console.log(s2); // ['zhangsan', 'lisi'] ES6 的内置对象扩展Array 的扩展方法（★★）扩展运算符（展开语法）扩展运算符可以将数组或者对象转为用逗号分隔的参数序列 let ary = [1, 2, 3]; // ...ary // 1, 2, 3 console.log(...ary); // 1 2 3,相当于下面的代码 console.log(1,2,3);扩展运算符可以应用于合并数组 let ary1 = [1, 2, 3]; let ary2 = [3, 4, 5]; // 方法一 let ary3 = [...ary1, ...ary2]; // 方法二 ary1.push(...ary2);将类数组或可遍历对象转换为真正的数组let oDivs = document.getElementsByTagName('div'); oDivs = [...oDivs];构造函数方法：Array.from()将伪数组或可遍历对象转换为真正的数组//定义一个集合 let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 }; //转成数组 let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']方法还可以接受第二个参数，作用类似于数组的map方法，用来对每个元素进行处理，将处理后的值放入返回的数组 let arrayLike = { "0": 1, "1": 2, "length": 2 } let newAry = Array.from(arrayLike, item => item *2) //[2,4]注意：如果是对象，那么属性需要写对应的索引实例方法：find()用于找出第一个符合条件的数组成员，如果没有找到返回undefinedlet ary = [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]; let target = ary.find((item, index) => item.id == 2); //找数组里面符合条件的值，当数组中元素id等于2的查找出来，注意，只会匹配第一个实例方法：findIndex()用于找出第一个符合条件的数组成员的位置，如果没有找到返回-1let ary = [1, 5, 10, 15]; let index = ary.findIndex((value, index) => value > 9); console.log(index); // 2实例方法：includes()判断某个数组是否包含给定的值，返回布尔值。[1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // falseString 的扩展方法模板字符串（★★★）ES6新增的创建字符串的方式，使用反引号定义let name = `zhangsan`;模板字符串中可以解析变量let name = '张三'; let sayHello = `hello,my name is ${name}`; // hello, my name is zhangsan模板字符串中可以换行 let result = { name: 'zhangsan', age: 20, sex: '男' } let html = ` ${result.name} ${result.age} ${result.sex} `; 在模板字符串中可以调用函数const sayHello = function () { return '哈哈哈哈 追不到我吧 我就是这么强大'; }; let greet = `${sayHello()} 哈哈哈哈`; console.log(greet); // 哈哈哈哈 追不到我吧 我就是这么强大 哈哈哈哈 实例方法：startsWith() 和 endsWith()startsWith()：表示参数字符串是否在原字符串的头部，返回布尔值endsWith()：表示参数字符串是否在原字符串的尾部，返回布尔值let str = 'Hello world!'; str.startsWith('Hello') // true str.endsWith('!') // true实例方法：repeat()repeat方法表示将原字符串重复n次，返回一个新字符串'x'.repeat(3) // "xxx" 'hello'.repeat(2) // "hellohello"Set 数据结构（★★）ES6 提供了新的数据结构 Set。它类似于数组，但是成员的值都是唯一的，没有重复的值。Set本身是一个构造函数，用来生成 Set 数据结构const s = new Set();Set函数可以接受一个数组作为参数，用来初始化。const set = new Set([1, 2, 3, 4, 4]); //{1, 2, 3, 4}const s1 = new Set(); console.log(s1.size) const s2 = new Set(["a", "b"]); console.log(s2.size) // 使用set对数组去重 const s3 = new Set(["a","a","b","b"]); console.log(s3.size) const ary = [...s3]; console.log(ary) const s4 = new Set(); // 向set结构中添加值 使用add方法 s4.add('a').add('b'); console.log(s4.size) // 从set结构中删除值 用到的方法是delete const r1 = s4.delete('c'); console.log(s4.size) console.log(r1); // 判断某一个值是否是set数据结构中的成员 使用has const r2 = s4.has('d'); console.log(r2) // 清空set数据结构中的值 使用clear方法 s4.clear(); console.log(s4.size); // 遍历set数据结构 从中取值 const s5 = new Set(['a', 'b', 'c']); s5.forEach(value => { console.log(value) })实例方法add(value)：添加某个值，返回 Set 结构本身delete(value)：删除某个值，返回一个布尔值，表示删除是否成功has(value)：返回一个布尔值，表示该值是否为 Set 的成员clear()：清除所有成员，没有返回值 const s = new Set(); s.add(1).add(2).add(3); // 向 set 结构中添加值 s.delete(2) // 删除 set 结构中的2值 s.has(1) // 表示 set 结构中是否有1这个值 返回布尔值 s.clear() // 清除 set 结构中的所有值 //注意：删除的是元素的值，不是代表的索引遍历Set 结构的实例与数组一样，也拥有forEach方法，用于对每个成员执行某种操作，没有返回值。s.forEach(value => console.log(value))
1.构造函数和原型1.1对象的三种创建方式--复习1、字面量方式：var obj = {};2、new关键字：var obj = new Object();3、构造函数方式function Person(name,age){ this.name = name; this.age = age; } var obj = new Person('zs',12);1.2静态成员和实例成员1.2.1实例成员实例成员就是构造函数内部通过this添加的成员，如下列代码中uname、age、sing就是实例成员，实例成员只能通过实例化的对象来访问： function Star(uname, age) { this.uname = uname; this.age = age; this.sing = function() { console.log('我会唱歌'); } } var ldh = new Star('刘德华', 18); console.log(ldh.uname);//实例成员只能通过实例化的对象来访问1.2.2静态成员静态成员是指在构造函数本身上添加的成员，如下列代码中sex就是静态成员，静态成员只能通过构造函数来访问： function Star(uname, age) { this.uname = uname; //实例成员就是构造函数内部通过this添加的成员 uname age sing 都是实例化成员 this.age = age; this.sing = function() { console.log('我会唱歌'); } } Star.sex = '男'; var ldh = new Star('刘德华', 18); console.log(ldh.sex); // Undefined console.log(Star.sex);//静态成员只能通过构造函数实例化对象来访问1.3构造函数的问题构造函数方法很好用，但是存在浪费内存的问题。console.log(ldh.sing === zxy.sing); //返回 false1.4构造函数原型prototype构造函数通过原型分配的函数是所有对象所共享的。JavaScript 规定，每一个构造函数都有一个prototype属性，指向另一个对象。注意这个prototype就是一个对象，这个对象的所有属性和方法，都会被构造函数所拥有。我们可以把那些不变的方法，直接定义在 prototype 对象上，这样所有对象的实例就可以共享这些方法。function Star(uname, age) { this.uname = uname; this.age = age; } Star.prototype.sing = function() { // 一般情况下，我们的公共属性定义到构造函数里面，公共的方法我们放到原型对象身上 console.log('我会唱歌'); } var ldh = new Star('刘德华', 18); var zxy = new Star('张学友', 19); ldh.sing();//我会唱歌 zxy.sing();//我会唱歌 console.log(ldh.sing === zxy.sing); //返回 true1.5对象原型对象都会有一个属性 __proto__ 指向构造函数的 prototype 原型对象，之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法，就是因为对象有 __proto__ 原型的存在。__proto__对象原型和原型对象 prototype 是等价的，__proto__对象原型的意义就在于为对象的查找机制提供一个方向，或者说一条路线，但是它是一个非标准属性，因此实际开发中，不可以使用这个属性，它只是内部指向原型对象 prototype。function Star(uname, age) { this.uname = uname; this.age = age; } Star.prototype.sing = function() { console.log('我会唱歌'); } var ldh = new Star('刘德华', 18); var zxy = new Star('张学友', 19); ldh.sing(); console.log(ldh); // 对象身上系统自己添加一个 __proto__ 指向我们构造函数的原型对象 prototype console.log(ldh.__proto__ === Star.prototype); // 方法的查找规则: 首先先看ldh 对象身上是否有 sing 方法,如果有就执行这个对象上的sing // 如果么有sing 这个方法,因为有__proto__ 的存在,就去构造函数原型对象prototype身上去查找sing这个方法1.6constructor构造函数对象原型（ __proto__）和构造函数（prototype）原型对象里面都有一个属性 constructor 属性 ，constructor 我们称为构造函数，因为它指回构造函数本身。constructor 主要用于记录该对象引用于哪个构造函数，它可以让原型对象重新指向原来的构造函数。一般情况下，对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法，我们可以给原型对象采取对象形式赋值，但是这样就会覆盖构造函数原型对象原来的内容，这样修改后的原型对象 constructor 就不再指向当前构造函数了。此时，我们可以在修改后的原型对象中，添加一个 constructor 指向原来的构造函数。如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数如: function Star(uname, age) { this.uname = uname; this.age = age; } // 很多情况下,我们需要手动的利用constructor 这个属性指回 原来的构造函数 Star.prototype = { // 如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数 constructor: Star, // 手动设置指回原来的构造函数 sing: function() { console.log('我会唱歌'); }, movie: function() { console.log('我会演电影'); } } var zxy = new Star('张学友', 19); console.log(zxy)以上代码运行结果，设置constructor属性如图：如果未设置constructor属性,如图：1.7原型链每一个实例对象又有一个__proto__属性，指向的构造函数的原型对象，构造函数的原型对象也是一个对象，也有__proto__属性，这样一层一层往上找就形成了原型链。1.8构造函数实例和原型对象三角关系构造函数的prototype属性指向了构造函数原型对象；实例对象是由构造函数创建的,实例对象的__proto__属性指向了构造函数的原型对象；构造函数的原型对象的constructor属性指向了构造函数,实例对象的原型的constructor属性也指向了构造函数。1.9原型链和成员的查找机制任何对象都有原型对象，也就是prototype属性，任何原型对象也是一个对象，该对象就有__proto__属性，这样一层一层往上找，就形成了一条链，我们称此为原型链：当访问一个对象的属性（包括方法）时，首先查找这个对象自身有没有该属性；如果没有就查找它的原型（也就是__proto__指向的 prototype 原型对象）；如果还没有就查找原型对象的原型（Object的原型对象）；依此类推一直找到 Object 为止（null）；__proto__对象原型的意义就在于为对象成员查找机制提供一个方向，或者说一条路线。1.10原型对象中this指向构造函数中的this和原型对象的this，都指向我们new出来的实例对象：function Star(uname, age) { this.uname = uname; this.age = age; } var that; Star.prototype.sing = function() { console.log('我会唱歌'); that = this; } var ldh = new Star('刘德华', 18); // 1. 在构造函数中,里面this指向的是对象实例 ldh console.log(that === ldh);//true // 2.原型对象函数里面的this 指向的是 实例对象 ldh1.11通过原型为数组扩展内置方法Array.prototype = { // 为何不能采用此种方法赋值？因为这种方法会把默认属性、方法全部覆盖掉. sum: function() { var sum = 0; for (var i = 0; i < this.length; i++) { sum += this[i]; } return sum; } } Array.prototype.sum = function() { var sum = 0; for (var i = 0; i < this.length; i++) { sum += this[i]; } return sum; }; //此时数组对象中已经存在sum()方法了 可以始终 数组.sum()进行数据的求 // 进行使用 var arr = [1, 2, 3, 4, 5]; console.log(arr.sum()); // 输出15. var arr02 = new Array(1, 2, 3); console.log(arr02.sum()); // 输出6.2.继承2.1call()call()可以调用函数call()可以修改this的指向,使用call()的时候 参数一是修改后的this指向,参数2,参数3..使用逗号隔开连接 function fn(x, y) { console.log(this); console.log(x + y); } var o = { name: 'andy' }; fn.call(o, 1, 2);//调用了函数此时的this指向了对象o,2.2子构造函数继承父构造函数中的属性先定义一个父构造函数再定义一个子构造函数子构造函数继承父构造函数的属性(使用call方法) // 1. 父构造函数 function Father(uname, age) { // this 指向父构造函数的对象实例 this.uname = uname; this.age = age; } // 2 .子构造函数 function Son(uname, age, score) { // this 指向子构造函数的对象实例 // 3.使用call方式实现子继承父的属性 Father.call(this, uname, age); this.score = score; } var son = new Son('刘德华', 18, 100); console.log(son);2.3借用原型对象继承方法先定义一个父构造函数再定义一个子构造函数子构造函数继承父构造函数的属性(使用call方法)// 1. 父构造函数 function Father(uname, age) { // this 指向父构造函数的对象实例 this.uname = uname; this.age = age; } Father.prototype.money = function() { console.log(100000); }; // 2 .子构造函数 function Son(uname, age, score) { // this 指向子构造函数的对象实例 Father.call(this, uname, age); this.score = score; } // Son.prototype = Father.prototype; 这样直接赋值会有问题,如果修改了子原型对象,父原型对象也会跟着一起变化 Son.prototype = new Father(); // 如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的构造函数 Son.prototype.constructor = Son; // 这个是子构造函数专门的方法 Son.prototype.exam = function() { console.log('孩子要考试'); } var son = new Son('刘德华', 18, 100); console.log(son);如上代码结果如图:3.ES5新增方法3.1数组方法forEach遍历数组 arr.forEach(function(value, index, array) { //参数一是:数组元素 //参数二是:数组元素的索引 //参数三是:当前的数组 }) //相当于数组遍历的 for循环 没有返回值 // forEach 迭代(遍历) 数组 var arr = [1, 2, 3]; var sum = 0; arr.forEach(function(value, index, array) { console.log('每个数组元素' + value); console.log('每个数组元素的索引号' + index); console.log('数组本身' + array); sum += value; }) console.log(sum);3.2数组方法filter过滤数组 var arr = [12, 66, 4, 88, 3, 7]; var newArr = arr.filter(function(value, index,array) { //参数一是:数组元素 //参数二是:数组元素的索引 //参数三是:当前的数组 return value >= 20; }); console.log(newArr);//[66,88] //返回值是一个新数组3.3数组方法some// some 查找数组中是否有满足条件的元素 var arr = [10, 30, 4]; var flag = arr.some(function(value,index,array) { //参数一是:数组元素 //参数二是:数组元素的索引 //参数三是:当前的数组 return value < 3; }); console.log(flag);//false返回值是布尔值,只要查找到满足条件的一个元素就立马终止循环3.4筛选商品案例定义数组对象数据var data = [{ id: 1, pname: '小米', price: 3999 }, { id: 2, pname: 'oppo', price: 999 }, { id: 3, pname: '荣耀', price: 1299 }, { id: 4, pname: '华为', price: 1999 }, ];使用forEach遍历数据并渲染到页面中data.forEach(function(value) { var tr = document.createElement('tr'); tr.innerHTML = ' ' + value.id + ' ' + value.pname + ' ' + value.price + ' '; tbody.appendChild(tr); });根据价格筛选数据获取到搜索按钮并为其绑定点击事件search_price.addEventListener('click', function() { });使用filter将用户输入的价格信息筛选出来search_price.addEventListener('click', function() { var newDate = data.filter(function(value) { //start.value是开始区间 //end.value是结束的区间 return value.price >= start.value && value.price <= end.value; }); console.log(newDate); });将筛选出来的数据重新渲染到表格中将渲染数据的逻辑封装到一个函数中function setDate(mydata) { // 先清空原来tbody 里面的数据 tbody.innerHTML = ''; mydata.forEach(function(value) { var tr = document.createElement('tr'); tr.innerHTML = ' ' + value.id + ' ' + value.pname + ' ' + value.price + ' '; tbody.appendChild(tr); }); }将筛选之后的数据重新渲染 search_price.addEventListener('click', function() { var newDate = data.filter(function(value) { return value.price >= start.value && value.price <= end.value; }); console.log(newDate); // 把筛选完之后的对象渲染到页面中 setDate(newDate); });根据商品名称筛选获取用户输入的商品名称为查询按钮绑定点击事件,将输入的商品名称与这个数据进行筛选 search_pro.addEventListener('click', function() { var arr = []; data.some(function(value) { if (value.pname === product.value) { // console.log(value); arr.push(value); return true; // return 后面必须写true } }); // 把拿到的数据渲染到页面中 setDate(arr); })案例汇总： Document 按照价格查询: - 搜索 按照商品名称查询: 查询 id 产品名称 价格 3.5some和forEach区别如果查询数组中唯一的元素, 用some方法更合适,在some 里面 遇到 return true 就是终止遍历 迭代效率更高在forEach 里面 return 不会终止迭代// 1. forEach迭代 遍历 arr.forEach(function(value) { if (value == 'green') { console.log('找到了该元素'); return true; // 在forEach 里面 return 不会终止迭代 } console.log(11); }) // 如果查询数组中唯一的元素, 用some方法更合适, arr.some(function(value) { if (value == 'green') { console.log('找到了该元素'); return true; // 在some 里面 遇到 return true 就是终止遍历 迭代效率更高 } console.log(11); }); arr.filter(function(value) { if (value == 'green') { console.log('找到了该元素'); return true; // // filter 里面 return 不会终止迭代 } console.log(11); });3.6trim方法去除字符串两端的空格var str = ' hello ' console.log(str.trim()) //hello 去除两端空格 var str1 = ' he l l o ' console.log(str.trim()) //he l l o 去除两端空格// trim 方法去除字符串两侧空格 var str = ' an dy '; console.log(str); var str1 = str.trim(); console.log(str1); var input = document.querySelector('input'); var btn = document.querySelector('button'); var div = document.querySelector('div'); btn.onclick = function() { var str = input.value.trim(); if (str === '') { alert('请输入内容'); } else { console.log(str); console.log(str.length); div.innerHTML = str; } }3.7获取对象的属性名Object.keys(对象) 获取到当前对象中的属性名 ，返回值是一个数组 var obj = { id: 1, pname: '小米', price: 1999, num: 2000 }; var result = Object.keys(obj) console.log(result)//[id，pname,price,num]3.8Object.definePropertyObject.defineProperty设置或修改对象中的属性Object.defineProperty(对象，修改或新增的属性名，{ value:修改或新增的属性的值, writable:true/false,//如果值为false 不允许修改这个属性值 enumerable: false,//enumerable 如果值为false 则不允许遍历 configurable: false //configurable 如果为false 则不允许删除这个属性 属性是否可以被删除或是否可以再次修改特性 }) // Object.defineProperty() 定义新属性或修改原有的属性 var obj = { id: 1, pname: '小米', price: 1999 }; // 1. 以前的对象添加和修改属性的方式 // obj.num = 1000; // obj.price = 99; // console.log(obj); // 2. Object.defineProperty() 定义新属性或修改原有的属性 Object.defineProperty(obj, 'num', { value: 1000, enumerable: true }); console.log(obj); Object.defineProperty(obj, 'price', { value: 9.9 }); console.log(obj); Object.defineProperty(obj, 'id', { // 如果值为false 不允许修改这个属性值 默认值也是false writable: false, }); obj.id = 2; console.log(obj); Object.defineProperty(obj, 'address', { value: '中国山东蓝翔技校xx单元', // 如果只为false 不允许修改这个属性值 默认值也是false writable: false, // enumerable 如果值为false 则不允许遍历, 默认的值是 false enumerable: false, // configurable 如果为false 则不允许删除这个属性 不允许在修改第三个参数里面的特性 默认为false configurable: false }); console.log(obj); console.log(Object.keys(obj)); delete obj.address; console.log(obj); delete obj.pname; console.log(obj); Object.defineProperty(obj, 'address', { value: '中国山东蓝翔技校xx单元', // 如果只为false 不允许修改这个属性值 默认值也是false writable: true, // enumerable 如果值为false 则不允许遍历, 默认的值是 false enumerable: true, // configurable 如果为false 则不允许删除这个属性 默认为false configurable: true }); console.log(obj.address);