本文深入解析 JavaScript 前端代码优化策略,涵盖条件判断重构、循环效率对比、数组去重技巧、DOM 操作优化及内存泄漏排查。重点讲解防抖与节流的应用场景,以及 defer 与 async 异步加载的区别,帮助开发者提升代码质量与页面性能。
if 判断的优化
在 JavaScript 日常开发中,条件语句的使用频率极高。然而,冗长的 if-else 链或深层嵌套的判断逻辑往往导致代码结构臃肿,降低可读性与可维护性。下文将通过具体案例展示如何对这些场景进行优化。
假设场景:现有四种产品——手机、电脑、电视机和游戏机,需要针对每种产品显示不同的价格信息。
1、最基础的方法:使用 if 判断
let commodity = {
phone: '手机',
computer: '电脑',
television: '电视',
gameBoy: '游戏机',
}
function price(name) {
if (name === commodity.phone) {
console.log(1999)
} else if (name === commodity.computer) {
console.log(9999)
} else if (name === commodity.television) {
console.log(2999)
} else if (name === commodity.gameBoy) {
console.log(3999)
}
}
price('手机') // 9999
该方法的缺陷在于代码行数过多,不仅难以阅读,后续维护成本也较高。
2、改进方案:使用 Switch 语句
let commodity = {
phone: '手机',
computer: '电脑',
television: '电视',
gameBoy: '游戏机',
}
const price = (name) => {
switch (name) {
case commodity.phone:
console.log(1999)
break
case commodity.computer:
console.log(9999)
break
case commodity.television:
console.log(2999)
break
case commodity.gameBoy:
console.log(3999)
break
}
}
price('手机') // 9999
3、更优方案:采用策略模式
策略模式通过组合、委托和多态等机制,有效避免了多重条件选择语句的复杂性。它严格遵循开放-封闭原则,将具体算法封装在独立的 strategy 中,从而实现算法的易于切换、理解与扩展。
const commodity = new Map([
['phone', 1999],
['computer', 9999],
['television', 2999],
['gameBoy', 3999],
])
const price = (name) => {
return commodity.get(name)
}
price('phone') // 1999
includes 方法的优化
includes 是 ES7 新增的 API。与返回索引值的 indexOf 不同,includes 直接返回布尔值(Boolean)。该方法同时适用于数组和字符串的操作。
需求场景:实现一个身份认证方法,通过传入身份 ID 来返回对应的验证结果。
传统实现方式
function verifyIdentity(identityId) {
if (identityId == 1 || identityId == 2 || identityId == 3 || identityId == 4) {
return '你的身份合法,请通行!'
} else {
return '你的身份不合法'
}
}
使用 includes 进行优化后的实现
function verifyIdentity(identityId) {
if ([1, 2, 3, 4].includes(identityId)) {
return '你的身份合法,请通行!'
} else {
return '你的身份不合法'
}
}
for 循环的性能考量
JavaScript 提供了 for()、while()、for-in 和 for-of 等多种循环结构。其中,for-in 的效率极低,因为它需要查询散列键,因此在实际开发中应尽量避免使用。
for 循环是最传统的迭代语句,它通过变量 i 作为索引来跟踪访问位置,从而对数组进行操作。
var arr = ['a', 'b', 'c']
for (var i = 0; i < arr.length; i++) {
console.log(arr[i]) //结果依次a,b,c
}
上述常规方法存在一个性能瓶颈:当数组长度达到百万级时,每次循环都会重新计算 arr.length,这将消耗大量性能。因此,可以采用以下改良方案。
var arr = ['a', 'b', 'c']
for (var i = 0, length = arr.length; i < length; i++) {
console.log(arr[i]) //结果依次a,b,c
}
改良后,arr.length 仅需计算一次,从而显著提升了执行性能。
for-in 通常用于遍历对象的属性,但前提是属性必须为 enumerable(可枚举)状态才能被读取。此外,for-in 也可用于遍历数组,但此时遍历的是数组的下标值(索引)。
var obj = { 0: 'a', 1: 'b', 2: 'c' }
for (var key in obj) {
console.log(key) //结果为依次为0,1,2
}
var arr = ['a', 'b', 'c']
for (var key in a) {
console.log(key) //结果为依次为0,1,2
}
for-of 语句在语法上与 for-in 相似,但关键区别在于它不能直接用于遍历普通对象,仅适用于数组等可迭代对象。
var arr = ['a', 'b', 'c']
for (var value of arr) {
console.log(value) // 结果依次为a,b,c
}
在遍历数组时,for-of 比 for-in 表现更佳。只要数据结构具备 Iterator 接口,for-of 即可用于迭代其成员,并直接读取键值。相比之下,for-in 需要穷举对象的所有属性(包括自定义添加的属性),且其 key 为 String 类型,涉及类型转换,开销较大。
因此,在开发过程中遍历数组时,应尽量避免使用 for-in。
数组去重技巧
数组去重是数据处理中的常见需求,实现方法多样,此处仅列举典型方案。
1、最传统的方法:利用数组的 indexOf 属性进行查询判断。
function unique4(arr) {
var newArr = []
for (var i = 0; i < arr.length; i++) {
if (newArr.indexOf(arr[i]) === -1) {
newArr.push(arr[i])
}
}
return newArr
}
console.log(unique4([1, 1, 2, 3, 5, 3, 1, 5, 6, 7, 4]))
// [1, 2, 3, 5, 6, 7, 4]
2、优化方案:利用 ES6 的 Set 数据结构。
Set 是一个构造函数,用于生成 Set 数据结构。它可以接受一个数组(或具有 iterable 接口的其他数据结构)作为参数进行初始化。Set 对象允许存储任何类型的值(原始值或对象引用),类似于数组,但其成员值唯一,不存在重复项。
function unique4(arr) {
return Array.from(new Set(arr)) // 利用Array.from将Set结构转换成数组
}
console.log(unique4([1, 1, 2, 3, 5, 3, 1, 5, 6, 7, 4]))
// [1, 2, 3, 5, 6, 7, 4]
箭头函数应用
箭头函数的语法比传统函数表达式更为简洁,因此在开发中更推荐使用。特别是在 Vue 项目中,使用箭头函数可以避免重新绑定 this 指向的问题。
// 使用functions
var arr = [5, 3, 2, 9, 1]
var arrFunc = arr.map(function (x) {
return x * x
})
console.log(arrFunc)
// 使用箭头函数
var arr = [5, 3, 2, 9, 1]
var arrFunc = arr.map((x) => x * x)
需要注意的是,箭头函数不绑定 arguments 对象,如需获取参数列表,应使用 rest 参数(…)来解决。
// 不能使用 arguments
let fun1 = (b) => {
console.log(arguments)
}
fun1(2, 92, 32, 32) // Uncaught ReferenceError: arguments is not defined
// 使用rest 参数
let fun2 = (...c) => {
console.log(c)
}
fun2(3, 82, 32, 11323) // [3, 82, 32, 11323]
DOM 元素的创建优化
在创建多个 DOM 元素时,建议先将元素 append 到 DocumentFragment 中,最后再将 DocumentFragment 一次性添加到页面中,以减少重排和重绘。
常规创建方法
for (var i = 0; i < 1000; i++) {
var el = document.createElement('p')
el.innerhtml = i
document.body.appendChild(el)
}
使用 DocumentFragment 优化多次 append 操作
var frag = document.createDocumentFragment()
for (var i = 0; i < 1000; i++) {
var el = document.createElement('p')
el.innerhtml = i
frag.appendChild(el)
}
document.body.appendChild(frag)
更优方案:使用一次 innerHTML 赋值来替代逐个构建 DOM 元素,以提升效率。
var html = []
for (var i = 0; i < 1000; i++) {
html.push('<p>' + i + '</p>')
}
document.body.innerHTML = html.join('')
内存泄漏排查
当系统进程不再使用的内存未及时释放时,即发生内存泄漏(memory leak)。随着内存占用持续增加,轻则影响系统性能,重则导致进程崩溃。
引起内存泄漏的常见原因
全局变量滥用
1、未声明的变量,或者通过 this 创建的变量(此时 this 指向 window 对象)都会导致内存泄漏。
function fn() {
a = "Actually, I'm a global variable"
}
fn()
function fn() {
this.a = "Actually, I'm a global variable"
}
fn()
解决方法:
- 避免创建全局变量。
- 启用严格模式,在 JavaScript 文件头部或函数顶部添加 'use strict'。
2、在 Vue 单页面应用(SPA)中,声明的全局变量在页面切换时若未清空,也会引发泄漏。
<template>
<div id="home">
这里是首页
</div>
</template>
<script>
export default {
mounted() {
window.test = {
// 此处在全局window对象中引用了本页面的dom对象
name: 'home',
node: document.getElementById('home')
}
}
}
</script>
解决方案:在页面卸载(销毁)生命周期钩子中,手动处理并清除相关引用。
destroyed () {
window.test = null // 页面卸载的时候解除引用
}
闭包导致的泄漏
闭包引起内存泄漏的原因是:闭包可以维持函数内的局部变量,使其无法被垃圾回收机制释放。
function fn() {
var a = "I'm a"
return function () {
console.log(a)
}
}
解决策略:将事件处理函数定义在外部以解除闭包,或者在定义事件处理函数的外部函数中,显式删除对 DOM 的引用。
定时器或事件监听未清理
项目中难免会使用定时器或事件监听。但在离开当前页面时,若未及时清除定时器或移除事件监听,会导致业务逻辑混乱甚至应用卡死。因此,必须在页面卸载(关闭)的生命周期函数中,清除定时器及事件监听。
methods:{
resizeFun () {
this.tableHeight = window.innerHeight - document.getElementById('table').offsetTop - 128
},
setTimer() {
this.timer = setInterval(() => { })
},
clearTimer() {//清除定时器
clearInterval(this.timer)
this.timer = null
}
},
mounted() {
this.setTimer()
window.addEventListener('resize', this.resizeFun)
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeFun)
this.clearTimer()
}
防抖与节流技术
在前端开发中,常需绑定持续触发的事件(如 resize、scroll、mousemove 等)。若每次触发都执行函数,会导致性能浪费。此时,防抖(Debounce)与节流(Throttle)技术便派上用场。
案例 1:远程搜索场景下,若用户每次输入都发起接口请求,将浪费带宽和性能。
<Select :remote-method="remoteMethod">
<Option v-for="item in temoteList" :value="item.value" :key="item.id">{{item.label}}</Option>
</Select>
<script>
function debounce(fn, wait) {
let timeout = null
return function () {
if (timeout !== null) clearTimeout(timeout)
timeout = setTimeout(fn, wait)
}
}
export default {
methods:{
remoteMethod:debounce(function (query) {
// to do ...
}, 200),
}
}
<script>
案例 2:在持续触发 scroll 事件时,不立即执行 handle 函数,而是等待 1000 毫秒内无新触发时,才延时执行一次 handle 函数。
function debounce(fn, wait) {
let timeout = null
return function () {
if (timeout !== null) clearTimeout(timeout)
timeout = setTimeout(fn, wait)
}
}
function handle() {
console.log(Math.random())
}
window.addEventListener('scroll', debounce(handle, 1000))
JavaScript 异步加载策略
默认情况下,浏览器同步加载 JS 脚本。在解析 HTML 过程中,遇到 <script> 标签会暂停解析,等待脚本下载、解析及执行完毕后,再继续向下解析渲染。
若 JS 文件体积较大,下载耗时较长,容易造成浏览器阻塞,页面呈现“白屏”状态,用户会感觉浏览器“卡死”无响应。此时,可采用异步加载与执行 JS 脚本的策略。
<script src="path/to/home.js" defer></script>
<script src="path/to/home.js" async></script>
在上述代码中,<script> 标签分别设置了 defer 和 async 属性。浏览器识别到这两个属性后,将以异步方式加载 JS 脚本,即浏览器无需等待脚本下载和执行完毕,即可继续向后解析 HTML。
defer 与 async 的区别:
- defer:DOM 结构完全生成,且其他脚本执行完毕后,才会执行(即渲染完成后执行)。若存在多个 defer 脚本,将严格按照页面出现的顺序依次加载和执行。
- async:一旦脚本下载完成,渲染引擎便会中断渲染,立即执行该脚本,随后再继续渲染。若存在多个 async 脚本,不保证按照页面出现的顺序加载和执行。
以上就是 JavaScript 前端代码优化的详细内容,更多关于 JavaScript 优化的资料请关注本站其它相关文章!