当前位置: 首页 > news >正文

微信小程序实现长按录音,点击播放等功能,CSS实现语音录制动画效果

有一个需求需要在微信小程序上实现一个长按时进行语音录制,录制时间最大为60秒,录制完成后,可点击播放,播放时再次点击停止播放,可以反复录制,新录制的语音把之前的语音覆盖掉,也可以主动长按删除

// index.js
const recorderManager = wx.getRecorderManager()
const innerAudioContext = wx.createInnerAudioContext()
let recordingTimerInterval = null // 录音时长计时器
let countdownTimerInterval = null // 倒计时计时器
let playbackCountdownInterval = null // 播放倒计时计时器Page({
/*** 页面的初始数据*/data: {// 语音输入部分inputType: 'input',count: null, // 录制倒计时longPress: '1', // 1显示 按住说话 2显示 说话中delShow: false, // 删除提示框显示隐藏time: 0, // 录音时长recordedDuration: 0, // 已录制音频的时长duration: 60000, // 录音最大值ms 60000/1分钟tempFilePath: '', //音频路径playStatus: 0, //录音播放状态 0:未播放 1:正在播放currentTime: 0, // 当前播放进度(秒)remain: 0, // 当前剩余时长(秒) = duration - currentTimewarningShown: false, // 是否已显示50秒提示minDuration: 2, // 录音最小时长秒数animationArray: Array.from({ length: 15 }, (_, index) => {// length 这个名字就不再需要,因为我们已经在这里写死了 15const centerIndex = Math.floor((15 - 1) / 2) // 7const distance = Math.abs(index - centerIndex)// 中心延迟为 0,向外越来越大const delay = distance * 0.2return { delay }})},/*** 开始录音倒计时* @param {number} val - 倒计时秒数*/startCountdown(val) {this.setData({count: Number(val)})countdownTimerInterval = setInterval(() => {if (this.data.count > 0) {this.setData({count: this.data.count - 1})} else {this.setData({longPress: '1'})clearInterval(countdownTimerInterval)countdownTimerInterval = null}}, 1000)},/*** 开始录音时长计时*/startRecordingTimer() {if (recordingTimerInterval) return // 防止重复启动计时器recordingTimerInterval = setInterval(() => {this.setData({time: this.data.time + 1})// 当录音时长达到50秒且未显示提示时,显示提示if (this.data.time === 50 && !this.data.warningShown) {wx.showToast({title: '录音即将结束',icon: 'none',duration: 2000})this.setData({warningShown: true})}// 如果录音时长达到最大值,自动停止录音if (this.data.time >= this.data.duration / 1000) {wx.showToast({title: '录音已达到最大时长',icon: 'none'})this.touchendBtn()}}, 1000)},/*** 停止录音时长计时* @param {string} newTempFilePath - 新录音的文件路径*/stopRecordingTimer(newTempFilePath) {if (recordingTimerInterval) {clearInterval(recordingTimerInterval)recordingTimerInterval = null}const duration = this.data.timeif (duration >= this.data.minDuration) {this.setData({recordedDuration: duration,tempFilePath: newTempFilePath},() => {console.log('录音已停止,时长:', this.data.recordedDuration, '秒')})} else {// 录音时长过短,提示用户wx.showToast({title: '录音时间太短',icon: 'none'})// 不覆盖之前的 tempFilePath,保留旧的录音// 仅重置 timethis.setData({time: 0},() => {console.log('录音时间太短,不保存此次录音。')})}},/*** 开始播放倒计时* @param {number} val - 播放倒计时秒数*/startPlaybackCountdown(val) {// 先停止可能存在的旧计时器if (playbackCountdownInterval) {clearInterval(playbackCountdownInterval)playbackCountdownInterval = null}this.setData({count: Number(val)})playbackCountdownInterval = setInterval(() => {if (this.data.count > 0) {this.setData({count: this.data.count - 1})} else {// 播放结束this.setData({playStatus: 0,count: null})innerAudioContext.stop()clearInterval(playbackCountdownInterval)playbackCountdownInterval = null}}, 1000)},/*** 停止播放倒计时*/stopPlaybackCountdown() {if (playbackCountdownInterval) {clearInterval(playbackCountdownInterval)playbackCountdownInterval = null}this.setData({count: null})},/*** 清除所有计时器*/clearAllTimers() {if (recordingTimerInterval) {clearInterval(recordingTimerInterval)recordingTimerInterval = null}if (countdownTimerInterval) {clearInterval(countdownTimerInterval)countdownTimerInterval = null}if (playbackCountdownInterval) {clearInterval(playbackCountdownInterval)playbackCountdownInterval = null}},/*** 重置录音状态*/resetRecordingState() {this.setData({longPress: '1',time: 0,recordedDuration: 0,count: null,warningShown: false // 重置警告提示})this.stopRecordingTimer()this.stopCountdownTimer()},/*** 处理输入类型变化* @param {object} e - 事件对象*/handleChangeInputType(e) {const { type } = e.currentTarget.datasetthis.setData({inputType: type})},/*** 检查录音权限*/checkRecordPermission() {wx.getSetting({success: res => {if (!res.authSetting['scope.record']) {// 没有录音权限,尝试授权wx.authorize({scope: 'scope.record',success: () => {// 授权成功,可以开始录音this.startRecording()},fail: () => {// 授权失败,提示用户前往设置授权wx.showModal({title: '授权提示',content: '录音权限未授权,请前往设置授权',success: res => {if (res.confirm) {wx.openSetting()}}})}})} else {// 已经授权,可以开始录音this.startRecording()}},fail: () => {// 获取设置失败,提示用户wx.showToast({title: '获取权限失败,请重试',icon: 'none'})}})},/*** 开始录音的封装函数*/startRecording() {this.setData({longPress: '2',time: 0, // 在开始录音前重置 timewarningShown: false // 重置警告提示})this.startCountdown(this.data.duration / 1000) // 录音倒计时60秒//recorderManager.stop() // 确保之前的录音已停止this.startRecordingTimer()const options = {duration: this.data.duration * 1000, // 指定录音的时长,单位 mssampleRate: 16000, // 采样率numberOfChannels: 1, // 录音通道数encodeBitRate: 96000, // 编码码率format: 'mp3', // 音频格式,有效值 aac/mp3frameSize: 10 // 指定帧大小,单位 KB}recorderManager.start(options)},/*** 长按录音事件*/longpressBtn() {this.checkRecordPermission()},/*** 长按松开录音事件*/touchendBtn() {this.setData({longPress: '1'})recorderManager.stop()this.stopCountdownTimer()},/*** 停止倒计时计时器*/stopCountdownTimer() {if (countdownTimerInterval) {clearInterval(countdownTimerInterval)countdownTimerInterval = null}this.setData({count: null})},/*** 播放录音*/playBtn() {if (!this.data.tempFilePath) {wx.showToast({title: '没有录音文件',icon: 'none'})return}// 如果已经在播放,就先停止if (this.data.playStatus === 1) {innerAudioContext.stop()// 重置状态this.setData({playStatus: 0,currentTime: 0,remain: 0})} else {// 重新开始播放console.log('开始播放', this.data.tempFilePath)innerAudioContext.src = this.data.tempFilePath// 在 iOS 下,即使系统静音,也能播放音频innerAudioContext.obeyMuteSwitch = false// 播放innerAudioContext.play()// playStatus 会在 onPlay 中置为 1// 如果想在点击之后就立即把 playStatus 置为 1 也行}},/*** 生命周期函数--监听页面加载*/onLoad(options) {// 绑定录音停止事件recorderManager.onStop(res => {// 将新录音的文件路径传递给 stopRecordingTimerthis.stopRecordingTimer(res.tempFilePath)console.log('录音已停止,文件路径:', res.tempFilePath)console.log('录音时长:', this.data.recordedDuration, '秒')})// 绑定录音开始事件recorderManager.onStart(res => {console.log('录音开始', res)})// 绑定录音错误事件recorderManager.onError(err => {console.error('录音错误:', err)wx.showToast({title: '录音失败,请重试',icon: 'none'})this.resetRecordingState()})// 当音频真正开始播放时innerAudioContext.onPlay(() => {console.log('onPlay 音频开始播放')// 设置为播放状态this.setData({playStatus: 1})})// 绑定音频播放结束事件innerAudioContext.onEnded(() => {console.log('onEnded 音频播放结束')// 停止播放并重置this.setData({playStatus: 0,currentTime: 0,remain: 0})// 如果想让界面上回到音频的总时长也可以手动 set remain = recordedDuration// 但通常播放结束,就显示 0 或不显示都行})innerAudioContext.onTimeUpdate(() => {const current = Math.floor(innerAudioContext.currentTime) // 取整或保留小数都可const total = Math.floor(innerAudioContext.duration)// 若 total 不准确(部分手机可能最初获取到是 0),可做一些保护if (total > 0) {const remain = total - currentthis.setData({currentTime: current,remain: remain > 0 ? remain : 0})}})// 绑定音频播放错误事件innerAudioContext.onError(err => {console.error('播放错误:', err)wx.showToast({title: '播放失败,请重试',icon: 'none'})this.setData({playStatus: 0,currentTime: 0,remain: 0})})},/*** 生命周期函数--监听页面卸载*/onUnload() {this.clearAllTimers()recorderManager.stop()innerAudioContext.stop()innerAudioContext.destroy()},
})
// index.wxml
<view wx:else class="voice-input"><view wx:if="{{tempFilePath !== ''}}" class="voice-msg" bind:tap="playBtn"><imagesrc="{{ playStatus === 0 ? '/sendingaudio.png' : '/voice.gif' }}"mode="aspectFill"style="transform: rotate(180deg); width: 22rpx; height: 32rpx"/><text class="voice-msg-text">{{ playStatus === 1 ? (remain + "''") : (recordedDuration + "''") }}</text></view><viewclass="voice-input-btn {{longPress == '1' ? '' : 'record-btn-2'}}"bind:longpress="longpressBtn"bind:touchend="touchendBtn"><!-- 语音音阶动画 --><view class="prompt-layer prompt-layer-1" wx:if="{{longPress == '2'}}"><!-- <view class="prompt-layer prompt-layer-1" wx:if="{{longPress == '2'}}"> --><view class="prompt-loader"><viewclass="em"wx:for="{{animationArray}}"wx:key="index"style="--delay: {{item.delay}}s;"></view></view><text class="p">{{'剩余:' + count + 's' + (warningShown ? ',即将结束录音' : '')}}</text><text class="span">松手结束录音</text></view><text class="voice-input-btn-text">{{longPress == '1' ? '按住 说话' : '说话中...'}}</text></view></view>
/*  index.wxss */
.voice-btn {box-sizing: border-box;padding: 6rpx 16rpx;background: #2197ee;border-radius: 28rpx;display: flex;align-items: center;justify-content: center;gap: 10rpx;
}.voice-text {line-height: 42rpx;color: #ffffff;font-size: 30rpx;
}.voice-input {box-sizing: border-box;display: flex;flex-direction: column;padding: 30rpx 76rpx;
}.voice-msg {width: 100%;height: 56rpx;background: #95ec69;border-radius: 10rpx;box-shadow: 0 3rpx 6rpx rgba(0, 0, 0, 0.13);margin-bottom: 26rpx;box-sizing: border-box;padding: 0 20rpx;display: flex;align-items: center;gap: 16rpx;
}.voice-msg-text {color: #000000;font-size: 30rpx;line-height: 56rpx;
}.voice-input-btn {width: 100%;box-sizing: border-box;padding: 12rpx 0;background: #ffffff;border: 2rpx solid;border-color: #1f75e3;border-radius: 8rpx;box-sizing: border-box;text-align: center;position: relative;
}.voice-input-btn-text {color: #1f75e3;font-size: 36rpx;line-height: 50rpx;
}/* 提示小弹窗 */
.prompt-layer {border-radius: 16rpx;background: #2197ee;padding: 16rpx 32rpx;box-sizing: border-box;position: absolute;left: 50%;transform: translateX(-50%);
}.prompt-layer::after {content: '';display: block;border: 12rpx solid rgba(0, 0, 0, 0);border-top-color: #2197ee;position: absolute;bottom: -20rpx;left: 50%;transform: translateX(-50%);
}.prompt-layer-1 {font-size: 32rpx;width: 80%;text-align: center;display: flex;flex-direction: column;align-items: center;justify-content: center;top: -178rpx;
}
.prompt-layer-1 .p {color: #ffffff;
}
.prompt-layer-1 .span {color: rgba(255, 255, 255, 0.6);
}/* 语音音阶------------- */
/* 容器样式 */
.prompt-loader {width: 250rpx;height: 40rpx;display: flex;align-items: center; /* 对齐到容器底部 */justify-content: space-between;margin-bottom: 12rpx;
}/* 音阶条样式 */
.prompt-loader .em {background: #ffffff;width: 6rpx;border-radius: 6rpx;height: 40rpx;margin-right: 5rpx;/* 通用动画属性 */animation: load 2.5s infinite linear;animation-delay: var(--delay);will-change: transform;transform-origin: center
}/* 移除最后一个元素的右边距 */
.prompt-loader .em:last-child {margin-right: 0;
}/* 动画关键帧 */
@keyframes load {0% {transform: scaleY(1);}50% {transform: scaleY(0.1);}100% {transform: scaleY(1);}
}
.record-btn-2 {background-color: rgba(33, 151, 238, 0.2);
}

相关文章:

微信小程序实现长按录音,点击播放等功能,CSS实现语音录制动画效果

有一个需求需要在微信小程序上实现一个长按时进行语音录制&#xff0c;录制时间最大为60秒&#xff0c;录制完成后&#xff0c;可点击播放&#xff0c;播放时再次点击停止播放&#xff0c;可以反复录制&#xff0c;新录制的语音把之前的语音覆盖掉&#xff0c;也可以主动长按删…...

校园跑腿小程序---轮播图,导航栏开发

hello hello~ &#xff0c;这里是 code袁~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的在校大学生…...

详细全面讲解C++中重载、隐藏、覆盖的区别

文章目录 总结1、重载示例代码特点1. 模板函数和非模板函数重载2. 重载示例与调用规则示例代码调用规则解释3. 特殊情况与注意事项二义性问题 函数特化与重载的交互 2. 函数隐藏&#xff08;Function Hiding&#xff09;概念示例代码特点 3. 函数覆盖&#xff08;重写&#xff…...

一文读懂单片机的串口

目录 串口通信的基本概念 串口通信的关键参数 单片机串口的硬件连接 单片机串口的工作原理 数据发送过程 数据接收过程 单片机串口的编程实现 以51单片机为例 硬件连接 初始化串口 发送数据 接收数据 串口中断服务函数 代码示例 单片机串口的应用实例 单片机与…...

HTML5 网站模板

HTML5 网站模板 参考 HTML5 Website Templates...

mybatis分页插件:PageHelper、mybatis-plus-jsqlparser(解决SQL_SERVER2005连接分页查询OFFSET问题)

文章目录 引言I PageHelper坐标II mybatis-plus-jsqlparser坐标Spring Boot 添加分页插件自定义 Mapper 方法中使用分页注意事项解决SQL_SERVER2005连接分页查询OFFSET问题知识扩展MyBatis-Plus 框架结构mybatis-plus-jsqlparser的 Page 类引言 PageHelper import com.github.p…...

uniapp中rpx和upx的区别

在 UniApp 中&#xff0c;rpx 和 upx 是两种不同的单位&#xff0c;它们的主要区别在于适用的场景和计算方式。 ### rpx&#xff08;Responsive Pixel&#xff09; - **适用场景**&#xff1a;rpx 是一种响应式单位&#xff0c;主要用于小程序和移动端的布局。 - **计算方式**…...

什么是卷积网络中的平移不变性?平移shft在数据增强中的意义

今天来介绍一下数据增强中的平移shft操作和卷积网络中的平移不变性。 1、什么是平移 Shift 平移是指在数据增强&#xff08;data augmentation&#xff09;过程中&#xff0c;通过对输入图像或目标进行位置偏移&#xff08;平移&#xff09;&#xff0c;让目标在图像中呈现出…...

java.net.SocketException: Connection reset 异常原因分析和解决方法

导致此异常的原因&#xff0c;总结下来有三种情况&#xff1a; 一、服务器端偶尔出现了异常&#xff0c;导致连接关闭 解决方法&#xff1a; 采用出错重试机制 二、 服务器端和客户端使用的连接方式不一致 解决方法&#xff1a; 服务器端和客户端使用相同的连接方式&#xff…...

Maven 仓库的分类

Maven 是一个广泛使用的项目构建和依赖管理工具&#xff0c;在 Java 开发生态中占据重要地位。作为 Maven 的核心概念之一&#xff0c;仓库&#xff08;Repository&#xff09;扮演着至关重要的角色&#xff0c;用于存储项目的依赖、插件以及构建所需的各种资源。 了解 Maven 仓…...

隧道网络:为数据传输开辟安全通道

什么是隧道网络&#xff1f; 想象一下&#xff0c;你正在一个陌生的城市旅行&#xff0c;并且想要访问家里的电脑。但是&#xff0c;直接连接是不可能的&#xff0c;因为家庭网络通常受到防火墙或路由器的保护&#xff0c;不允许外部直接访问。这时候&#xff0c;隧道网络&…...

CentOS 7 下 Nginx 的详细安装与配置

1、安装方式 1.1、通过编译方式安装 下载Nginx1.16.1的安装包 https://nginx.org/download/nginx-1.16.1.tar.gz 下载后上传至/home目录下。 1.2、通过yum方式安装 这种方式安装更简单。 2、通过编译源码包安装Nginx 2.1、安装必要依赖 sudo yum -y install gcc gcc-c sudo…...

JAVA 使用apache poi实现EXCEL文件的输出;apache poi实现标题行的第一个字符为红色;EXCEL设置某几个字符为别的颜色

设置输出文件的列宽&#xff0c;防止文件过于丑陋 Sheet sheet workbook.createSheet(FileConstants.ERROR_FILE_SHEET_NAME); sheet.setColumnWidth(0, 40 * 256); sheet.setColumnWidth(1, 20 * 256); sheet.setColumnWidth(2, 20 * 256); sheet.setColumnWidth(3, 20 * 25…...

通过vba实现在PPT中添加计时器功能

目录 一、前言 二、具体实现步骤 1、准备 2、开启宏、打开开发工具 3、添加计时器显示控件 3.1、开启母版 3.2、插入计时器控件 4、vba代码实现 4.1、添加模块 4.2、添加代码 4.3、保存为pptm 5、效果展示 一、前言 要求/目标:在PPT中每一页上面增加一个计时器功能…...

检验统计量与p值笔记

一、背景 以雨量数据为例&#xff0c;当获得一个站点一年的日雨量数据后&#xff0c;我们需要估计该站点的雨量的概率分布情况&#xff0c;因此我们利用有参估计的方式如极大似然法估计得到了假定该随机变量服从某一分布的参数&#xff0c;从而得到该站点的概率密度函数&#x…...

【集成学习】Bagging、Boosting、Stacking算法详解

文章目录 1. 相关算法详解&#xff1a;2. 算法详细解释&#xff1a;2.1 Bagging&#xff1a;2.2 Boosting&#xff1a;2.3 Stacking&#xff1a;2.4 K-fold Multi-level Stacking&#xff1a; 集成学习&#xff08;Ensemble Learning&#xff09;是一种通过结合多个模型的预测结…...

Rabbit Rocket kafka 怎么实现消息有序消费和延迟消费的

在消息队列系统中&#xff0c;像 RabbitMQ、RocketMQ 和 Kafka 这样的系统&#xff0c;都支持不同的方式来实现消息的有序消费和延迟消费。下面我们分别探讨这些系统中如何实现这两种需求&#xff1a; 1. RabbitMQ&#xff1a;实现消息有序消费和延迟消费 有序消费&#xff1…...

【Ubuntu与Linux操作系统:五、文件与目录管理】

第5章 磁盘存储管理 5.1 Linux磁盘存储概述 磁盘存储是Linux系统存储数据的重要组件&#xff0c;它通过分区和文件系统组织和管理数据。Linux支持多种文件系统&#xff0c;如ext4、xfs和btrfs&#xff0c;并以块的形式管理存储设备。 1. 分区与文件系统&#xff1a; 分区&am…...

32_Redis分片集群原理

1.Redis集群分片 1.1 Redis集群分片介绍 Redis集群没有使用一致性hash,而是引入了哈希槽的概念。Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。 用于将密钥映射到散列插槽的基本算法如下: HASH_SLOT = CRC16(key) mod 16384 集群的每…...

微信小程序mp3音频播放组件,仅需传入url即可

// index.js // packageChat/components/audio-player/index.js Component({/*** 组件的属性列表*/properties: {/*** MP3 文件的 URL*/src: {type: String,value: ,observer(newVal, oldVal) {if (newVal ! oldVal && newVal) {// 如果 InnerAudioContext 已存在&…...

网络六边形受到攻击

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 抽象 现代智能交通系统 &#xff08;ITS&#xff09; 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 &#xff08;…...

应用升级/灾备测试时使用guarantee 闪回点迅速回退

1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间&#xff0c; 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点&#xff0c;不需要开启数据库闪回。…...

<6>-MySQL表的增删查改

目录 一&#xff0c;create&#xff08;创建表&#xff09; 二&#xff0c;retrieve&#xff08;查询表&#xff09; 1&#xff0c;select列 2&#xff0c;where条件 三&#xff0c;update&#xff08;更新表&#xff09; 四&#xff0c;delete&#xff08;删除表&#xf…...

【WiFi帧结构】

文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成&#xff1a;MAC头部frame bodyFCS&#xff0c;其中MAC是固定格式的&#xff0c;frame body是可变长度。 MAC头部有frame control&#xff0c;duration&#xff0c;address1&#xff0c;address2&#xff0c;addre…...

Oracle查询表空间大小

1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...

Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例

使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件&#xff0c;常用于在两个集合之间进行数据转移&#xff0c;如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model&#xff1a;绑定右侧列表的值&…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路

进入2025年以来&#xff0c;尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断&#xff0c;但全球市场热度依然高涨&#xff0c;入局者持续增加。 以国内市场为例&#xff0c;天眼查专业版数据显示&#xff0c;截至5月底&#xff0c;我国现存在业、存续状态的机器人相关企…...

论文浅尝 | 基于判别指令微调生成式大语言模型的知识图谱补全方法(ISWC2024)

笔记整理&#xff1a;刘治强&#xff0c;浙江大学硕士生&#xff0c;研究方向为知识图谱表示学习&#xff0c;大语言模型 论文链接&#xff1a;http://arxiv.org/abs/2407.16127 发表会议&#xff1a;ISWC 2024 1. 动机 传统的知识图谱补全&#xff08;KGC&#xff09;模型通过…...

PL0语法,分析器实现!

简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...