JavaScript 前端代码优化实战:从语法到性能

作者:极速慢跑时间:2026-10-24 09:57:33浏览:437

本文深入解析 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 优化的资料请关注本站其它相关文章!

作者最新文章