慎用全局变量
-
我们在日常声明变量时要尽量的使用块级域变量声明关键字来声明变量,尽可能少的使用全局作用域,全局变量定义在全局执行上下文里,且它还是所有作用域的最顶端。如果我们定义变量为全局变量, JavaScript 在查找时都要从块级往上扫一遍直到最顶端才可以找到该变量,所以这样查找的时间消耗就变大了。
-
全局变量在程序执行完毕前会始终存在于上下文执行栈中,直到程序退出。这就还会导致全局变量永远不会被当作垃圾被回收掉,会始终占据内存空间。
-
如果某个局部作用域出现了同名变量则会遮蔽或污染全局
因此我们如果使用全局变量就要考虑更多的情况,而且还总是会带给我们意想不到的问题。
- 变量做局部化:把一些变量做局部化的处理,可以提高代码的执行效率(减少了数据访问时需要查找的路径,就是作用域链查找的优化);
数据的存储和读取:对于数据的存储和读取来说,最好能够在作用域链的查找上减少它的访问层级,从而能够提升代码的执行速度,所以情况允许的话建议把变量数据直接放在当前执行上下文的同级作用域中;
var i,str = '';
function packageDom(){
for(i=0;i<1000;i++){
str+=i
};
console.log(str);
};
packageDom()
//变量局部化之后
function packageDom(){
let str = '';
for(let i=0;i<1000;i++){
str+=i
};
console.log(str)
}
packageDom()
缓存数据
对于需要多次使用的数据进行提前保存,后续进行使用。(目的还是为了减少访问查找它的层级,把路径做到最短)
我们可以将使用中无法避免的全局变量缓存到局部作用域,代码如下:
var oBox = document.getElementById('btn')
function hasClassName(ele,cls){
console.log(ele.className)
console.log(ele.className)
console.log(ele.className)
console.log(ele.className)
}
hasClassName(oBox,btn);
function hasClassName(ele,cls){
const name = ele.className
console.log(name)
console.log(name)
console.log(name)
console.log(name)
}
hasClassName(oBox,btn);
总之,变量局部化和缓存数据的目的还是:对数据进行访问时,尽量把它放到最快能够找到的地方,不需要把它藏得太深,可以减少读取时所消耗的时间。
防抖与节流
为什么需要防抖与节流:
- 在一些高频率事件触发的情况下,我们不希望对应的事件处理函数多次执行
- 场景:
- 滚动事件
- 轮播图切换按钮 高频率点击
- input 键盘事件 触发模糊查询
- 浏览器默认情况下都会有自己的监听事件 的时间间隔(谷歌 4~6ms)如果监测到事件的多次反复重复高频率执行,就会造成不必要的资源浪费
**防抖:**对于高频的操作来说,我们只希望识别一次点击,可以是第一次或者最后一次。
**节流:**对于高频操作,不是让它只执行一次;而是我们可以自己设置频率,让本来会执行多次的事件触发,按值我们定义的频流减少触发的次数。
防抖函数实现
初步实现一个防抖函数:
const btn = document.getElementById('btn');
/*
*handle 最终要执行的回调函数
*wait 等待时间
*immedite 控制第一次执行还是最后一次执行,false 最后一次
*/
function MyDebonce(handle,wait,immedite){
// 参数类型及默认值处理
if( typeof handle !== 'function' ) throw new Error('handle must be an function !')
if( typeof wait === 'boolean' ) {
immedite = wait
wait = 500
}
if(!wait) wait = 500
if( typeof immedite !== 'boolean' ){
immedite = false;
}
let timer = null;
// 所谓的“防抖”就是有一个"人" ,来管理handle函数的执行次数;
// 点击事件嘛,this指向和事件对象总要还原一下的,匿名函数接收点击对象的参数
return function(...args){
let init = immedite && timer==null;
let self = this;
clearTimeout(timer)
timer = setTimeout( ()=>{
timer = null
!immedite ? handle.call(self,...args) : null;
},wait );
init?handle.call(self,...args):null
}
}
// 定义事件执行函数
function btnClick(event){
console.log('点击了!',this,event)
}
btn.onclick = MyDebonce( btnClick,500,true )
可以看出来,MyDebonce函数只执行一次,btnClick回调函数作为参数和其他参数一起传入防抖函数MyDebonce,然后形成闭包。这也是函数柯里化的一个应用了。
节流
节流:我们这里的节流指的是在自定义的一段时间内 让事件只触发一次;
核心操作:执行代理函数,当执行onscroll事件的时候,不去执行scrollfn了,而是执行它的代理函数,然后在代理函数proxy中去控制scrollfn的执行频率。
const btn = document.getElementById('btn');
function MyThrottle(handle,wait){
// 参数类型及默认值处理
if( typeof handle !== 'function' ) throw new Error('handle must be an function !')
if( typeof wait === 'boolean' ) {
immedite = wait
wait = 500
}
if(!wait) wait = 500;
if( typeof immedite !== 'boolean' ){
immedite = false;
};
let previous = 0;
let timer = null;
return function proxy(e){
let now = new Date();
let interval = wait - (now - previous);
if( interval<=0 ){
// 此时就说明是一个非高频次操作,可以执行 handle
previous = now;
handle.call(this,e,"interval<=0");
}else if( !timer ){
// 如果系统中有一个延时器,就不需要再开启了
// 此时说明这次操作在我们定义的频次内不是第一次调用proxy,不应该执行 handle、
// 这时候可以定义一个延时器,让handle在 interval后执行
timer = setTimeout(()=>{
// clearTimeout(timer)
timer = null;
previous = new Date();
handle.call(this,e,"interval>0");
},interval)
}
}
}
function handle(e,aa){
console.log("点击啦!",this,aa)
}
btn.onclick = MyThrottle(handle,500);
减少 if 判断层级
我们的项目中经常出现判断条件嵌套的场景,我们可以提前 return 掉无用的、 无效的条件来达到嵌套优化的效果。
// 优化前
function doSomething (part, chapter) {
const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']
if (part){
if(parts.includes(part)){
console.log('属于当前课程')
if(chapter > 5){
console.log('您需要提供 VIP 身份')
}
}
}else{
console.log('请确认模块信息')
}
}
// 优化后
function doSomething (part, chapter) {
const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']
if (!part) return console.log('请确认模块信息')
if(!parts.includes(part)) return
console.log('属于当前课程')
if(chapter > 5){
console.log('您需要提供 VIP 身份')
}
}
总结:
- 每当出现 多层嵌套的 if else 的时候,就可以考虑是否可以通过提前return 无效代码的操作来减少判断层级;
- if else 一般用于区间的条件判断,
- 而如果明确了有几个枚举值的时候,就更建议使用switch case,这样的代码会更加的清晰,且易于维护。
- 当然易于维护的代码不一定执行速度就快。
减少 循环体活动
循环体里做的事情越多,循环就越慢。因此我们就可以把一些不需要放在循环内的代码放置在循环体外部来优化。把每次循环都要用到的,或者都要操作的数据值不变的都抽离到循环体外部(类似于数据缓存);
// 优化前
var test = () => {
let i;
let arr = ['zce', 18, '我为前端而活']
for (i = 0; i < arr.length; i++) {
console.log(arr[i])
}
}
// 优化后,实际上就是基础的 for 循环优化,然后减少循环体内不必要的逻辑
var test = () => {
let i;
let arr = ['zce', 18, '我为前端而活']
const len = arr.length
for (i = 0; i < len; i++) {
console.log(arr[i])
}
}
从后往前遍历的思路,会少做条件判断
var test = () => {
let arr = ['zce', 18, '我为前端而活'];
const len = arr.length
while(len--){
console.log(arr[len])
}
}
选择最优的循环方式
var arrList = new Array(1, 2, 3, 4, 5)
forEach循环
arrList.forEach(item => {
console.log(item)
})
for循环
var len = arrList.length;
for (let i = 0; i<len; i++) {
console.log(arrList[i])
}
for of 循环
var i;
for ( i of arrList ) {
console.log(arrList[i])
}
对比后发现 forEach循环的执行速度最优,依次往下越递减。并且foeEach的速度优于其他循环很多!
字面量与构造式
引用类型数据 :字面量创建 与 构造函数式创建相比
通过字面量来创建对象会比构造式的方式快许多。
// 优化前
let test = () => {
let obj = new Object()
obj.name = 'mn'
obj.age = 25
obj.slogan = '我为前端而活'
return obj
}
// 优化后,使用字面量代替构造式
let test = () => {
let obj = {
name: 'mn',
age: 25,
slogan: '我为前端而活'
}
return obj
}
console.log(test())
因通过new 关键字创建对象时 ,涉及到了函数的调用,做的事情多一些,消耗的的时间也会多一些;
而字面量创建对象就像是直接开辟一个空间,然后往里存东西就可以了。
基础数据类型:
var str1 = '我是字符串'
var str2 = new String('我是字符串')
再JSbench中进行测试后发现它们的差异是非常明显的,引用类型的差异在5%以内,而字符串类型这两种方式的差别是非常大的(90%以上);
str1是一个字符串,str2是一个对象,创建时它需要多余的空间来存放Object身上的一些属性和方法。
优化dom节点添加
dom 操作很频繁且性能消耗十分严重,而在 dom 种添加节点 必然会出现回流和重绘,那我们就要避免频繁的直接操作 dom 来优化性能损耗,示例如下:
// 优化前
for (let i = 0; i < 10; i++) {
let oP = document.createElement('p')
oP.innerHTML = i
document.body.appendChild(oP)
}
// 优化后,我们只需要直接操作 document 一次,避免了多次 dom 变化重绘
const fragEle = document.createDocumentFragment()
for (let i = 0; i < 10; i++) {
let oP = document.createElement('p')
oP.innerHTML = i
fragEle.appendChild(oP)
}
document.body.appendChild(fragEle)
优化dom节点操作
当我们向文档 document 中添加一个和文档中已存在元素的文本不同但标签和样式都相同的节点时,我们没有必要创建新的标签和样式的节点再插入,而是可以通过复制克隆的方式创建节点(会更快占存更少)并添加至文档中。
// 优化前
for (let i = 0; i < 3; i++) {
let oP = document.createElement('p')
oP.innerHTML = i
document.body.appendChild(oP)
}
// 优化后
const oldP = document.getElement('box1')
for (let i = 0; i < 3; i++) {
let newP = oldP.cloneNode(false)
newP.innerHTML = i
document.body.appendChild(newP)
}
总结
我们在项目里并不是一味的去提升 JS 运行速度或一味的去优化 JS 占据的内存空间,有时两者都是互补状态,我们要结合业务需要来具体优化其中符合我们需求的部分,而且我们在项目中经常会出现一种情况:性能和封装上去了但可读性反而变差了,我们就要究其利弊考虑好究竟实施哪一套方案,程序总会有不那么令人如意的地方。