每日几道面试题系列

103 阅读5分钟

(主要是方便自己复习啦,如有侵权请联系我删除~)

js防抖和节流

1:关于防抖和节流,学习到的相关知识,这里这位博主讲解的很好,我就直接引用过来啦

在进行窗口的resize、scroll,输入框内容校验等操作时,如果事件处理函数调用的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕。此时我们可以采用debounce(防抖)和throttle(节流)的方式来减少调用频率,同时又不影响实际效果。 

 

函数防抖

函数防抖(debounce):当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。如下图,持续触发scroll事件时,并不执行handle函数,当1000毫秒内没有触发scroll事件时,才会延时触发scroll事件。

一起来实现个简单的debounce~

防抖debounce代码:

复制代码

// 防抖
function debounce(func, wait=0) {    

   if (typeof func !== 'function') {

    throw new TypeError('need a function arguments')

   }

   let timeid = null;

     let result;

 

   return function() {

    let context = this;

    let args = arguments;

\

    if (timeid) {

      clearTimeout(timeid);

    }

    timeid = setTimeout(function() {

      result = func.apply(context, args);

    }, wait);

 

    return result;

   }

}
// 处理函数
function handle() {    
    console.log(Math.random()); 
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));

复制代码

当持续触发scroll事件时,事件处理函数handle只在停止滚动1000毫秒之后才会调用一次,也就是说在持续触发scroll事件的过程中,事件处理函数handle一直没有执行。

 

函数节流

 

函数节流(throttle):当持续触发事件时,保证一定时间段内只调用一次事件处理函数。节流通俗解释就比如我们水龙头放水,阀门一打开,水哗哗的往下流,秉着勤俭节约的优良传统美德,我们要把水龙头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的往下滴。如下图,持续触发scroll事件时,并不立即执行handle函数,每隔1000毫秒才会执行一次handle函数。

函数节流主要有两种实现方法:时间戳和定时器。接下来分别用两种方法实现throttle~

节流throttle代码(时间戳):

复制代码

var throttle = function(func, delay) {            
  var prev = Date.now();            
  return function() {                
    var context = this;                
    var args = arguments;                
    var now = Date.now();                
    if (now - prev >= delay) {                    
      func.apply(context, args);                    
      prev = Date.now();                
    }            
  }        
}        
function handle() {            
  console.log(Math.random());        
}        
window.addEventListener('scroll', throttle(handle, 1000));

复制代码

当高频事件触发时,第一次会立即执行(给scroll事件绑定函数与真正触发事件的间隔一般大于delay,如果你非要在网页加载1000毫秒以内就去滚动网页的话,我也没办法o(╥﹏╥)o),而后再怎么频繁地触发事件,也都是每delay时间才执行一次。而当最后一次事件触发完毕后,事件也不会再被执行了 (最后一次触发事件与倒数第二次触发事件的间隔小于delay,为什么小于呢?因为大于就不叫高频了呀(╹▽╹))。

节流throttle代码(定时器):

复制代码

// 节流throttle代码(定时器):
var throttle = function(func, delay) {            
    var timer = null;            
    return function() {                
        var context = this;               
        var args = arguments;                
        if (!timer) {                    
            timer = setTimeout(function() {                        
                func.apply(context, args);                        
                timer = null;                    
            }, delay);                
        }            
    }        
}        
function handle() {            
    console.log(Math.random());        
}        
window.addEventListener('scroll', throttle(handle, 1000));

复制代码

当触发事件的时候,我们设置一个定时器,再次触发事件的时候,如果定时器存在,就不执行,直到delay时间后,定时器执行执行函数,并且清空定时器,这样就可以设置下个定时器。当第一次触发事件时,不会立即执行函数,而是在delay秒后才执行。而后再怎么频繁触发事件,也都是每delay时间才执行一次。当最后一次停止触发后,由于定时器的delay延迟,可能还会执行一次函数。

节流中用时间戳或定时器都是可以的。更精确地,可以用时间戳+定时器,当第一次触发事件时马上执行事件处理函数,最后一次触发事件后也还会执行一次事件处理函数。

节流throttle代码(时间戳+定时器):

复制代码

// 节流throttle代码(时间戳+定时器):
var throttle = function(func, delay) {     
    var timer = null;     
    var startTime = Date.now();     
    return function() {             
        var curTime = Date.now();             
        var remaining = delay - (curTime - startTime);             
        var context = this;             
        var args = arguments;             
        clearTimeout(timer);              
        if (remaining <= 0) {                    
            func.apply(context, args);                    
            startTime = Date.now();              
        } else {                    
            timer = setTimeout(func, remaining);              
        }      
    }
}
function handle() {      
    console.log(Math.random());
} 
window.addEventListener('scroll', throttle(handle, 1000));

复制代码

在节流函数内部使用开始时间startTime、当前时间curTime与delay来计算剩余时间remaining,当remaining<=0时表示该执行事件处理函数了(保证了第一次触发事件就能立即执行事件处理函数和每隔delay时间执行一次事件处理函数)。如果还没到时间的话就设定在remaining时间后再触发 (保证了最后一次触发事件后还能再执行一次事件处理函数)。当然在remaining这段时间中如果又一次触发事件,那么会取消当前的计时器,并重新计算一个remaining来判断当前状态。

总结

函数防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

##### 原博主:www.cnblogs.com/momo798/p/9…

js对象的深度克隆

2:关于深度克隆和浅克隆,同上是一位博主的总结

 

在聊JavaScript(以下简称js)深度克隆之前,我们先来了解一下js中对象的组成。
在 js 中一切实例皆是对象,具体分为 原始类型 和 合成类型 :
原始类型 对象指的是 Undefined 、 Null 、Boolean 、Number 和 String ,按值传递。
合成类型 对象指的是 array 、 object 以及 function ,按址传递,传递的时候是内存中的地址。

克隆或者拷贝分为2种: 浅度克隆 、 深度克隆 。
浅度克隆 :基本类型为值传递,对象仍为引用传递。
深度克隆 :所有元素或属性均完全克隆,并于原引用类型完全独立,即,在后面修改对象的属性的时候,原对象不会被修改。

又或许你刚听说“深度克隆”这个词,简单来说,就是说有个变量a,a的值是个对象(包括基本数据类型),现在你要创建一个变量b,使得它拥有跟a一样的方法和属性等等。但是a和b之间不能相互影响,即a的值的改变不影响b值的变化。直接赋值可好?

复制代码

var a = 1;
var b = a;
a = 10;
console.log(b);            // 1
 
var a = 'hello';
var b = a;
a = 'world';
console.log(b);            // hello
 
var a = true;
var b = a;
a = false;
console.log(b);            // true

复制代码

实践证明某些 JavaScript 的原始数据类型,如果要克隆直接赋值即可。
关于 function 的深度复制:查阅了一些资料, function 的深度复制似乎和原始数据类型的深度复制一样。

复制代码

var a = function () {
    console.log(1);
};
var b = a;
a = function () {
    console.log(2);
};
b(); 

复制代码

本来我也是这么认为的,直到文章下出现了评论。思考后我觉得 function 和普通的对象一样,只是我们在平常应用中习惯了整体的重新赋值,导致它在深度复制中的表现和原始类型一致:

复制代码

var a = function () {
    console.log(1);
};
a.tmp = 10;
var b = a;
a.tmp = 20;
console.log(b.tmp);        // 20

复制代码

于是乎对于 function 类型的深度克隆,直接赋值似乎并不应该是一种最好的方法(尽管实际应用中足矣)。

但是对象呢?

var a = [0,1,2,3];
var b = a;
a.push(4);
console.log(b);            // [0, 1, 2, 3, 4]

显然与预期不符,为什么会这样?因为原始数据类型储存的是对象的实际数据,而对象类型存储的是对象的引用地址。上面的例子呢也就是说a和b对象引用了同一个地址,无论改变a还是改变b,其实根本操作是一样的,都是对那块空间地址中的值的改变。

于是我们知道了,对于基本的对象来说,不能只能用 “ = ” 赋值,思索后写下如下代码:

复制代码

// 判断arr是否为一个数组,返回一个bool值
function isArray (arr) {
    return Object.prototype.toString.call(arr) === '[object Array]';  
}
// 深度克隆
function deepClone (obj) {  
    if(typeof obj !== "object" && typeof obj !== 'function') {
        return obj;        //原始类型直接返回
    }
    var o = isArray(obj) ? [] : {}; 
    for(i in obj) {  
        if(obj.hasOwnProperty(i)){ 
            o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i]; 
        } 
    } 
    return o;
}

复制代码

注意代码中判断数组的时候用的不是 obj instanceof Array ,这是因为该方法存在一些小问题,详情见www.nowamagic.net/librarys/ve…

用一些代码来测试下:

复制代码

// 测试用例:
var srcObj = {
    a: 1,
    b: {
        b1: ["hello", "hi"],
        b2: "JavaScript"
    }
};
var abObj = srcObj;
var tarObj = cloneObject(srcObj);

srcObj.a = 2;
srcObj.b.b1[0] = "Hello";

console.log(abObj.a);
console.log(abObj.b.b1[0]);

console.log(tarObj.a);      // 1
console.log(tarObj.b.b1[0]);    // "hello"

复制代码

 

对于上面的方法再进行测试下,如下:

这个没有区分具体的对象,在此问下大家js的对象有哪些呢?相信一般人答不出来4个
[object Object][object Array][object Null][object RegExp][object Date][object HTMLXXElement][object Map],[object Set],... 等等一系列

检测类型使用 Object.prototype.toString.call(xxx) 和 typeof

我们分析下上面对象中哪些是引用类型需要特殊处理呢?相信大家都不陌生了。[object Object] 和 [object Array]

好!详细大家思路有了,咋们用递归来实现一把吧!

 

复制代码

const deepClone = function(obj) {
  // 先检测是不是数组和Object
  // let isMap = Object.prototype.toString.call(obj) === '[object Map];
  // let isSet = Object.prototype.toString.call(obj) === '[object Set];
  // let isArr = Object.prototype.toString.call(obj) === '[object Array]';
  let isArr = Array.isArray(obj);
  let isJson = Object.prototype.toString.call(obj) === '[object Object]';
  if (isArr) {
    // 克隆数组
    let newObj = [];
    for (let i = 0; i < obj.length; i++) {
      newObj[i] = deepClone(obj[i]);
    }
    return newObj;
  } else if (isJson) {
    // 克隆Object
    let newObj = {};
    for (let i in obj) {
      newObj[i] = deepClone(obj[i]);
    }
    return newObj;
  }
  // 不是引用类型直接返回
  return obj;
};

Object.prototype.deepClone = function() {
  return deepClone(this);
};

注:先不考虑Map Set Arguments [object XXArrayBuffer] 对象了原理都是一样

复制代码

各种情况分析完了才说算是真克隆
我们在控制台看下

    • 注意先要把方法在控制台输进去,在调试

 

是不是解决了? 在此并没有结束。 专注的伙伴们相信发现了对象中包含了个 deepClone 方法,具体细节我们在此就不多说了,我们给 Object 添加了个 Object.prototype.deepClone方法导致了每个对象都有了此方法。

原则上我们不允许在原型链上添加方法的,因为在循环中 for inObject.entriesObject.valuesObject.keys等方法会出现自定义的方法。

相信熟悉 Object 文档的伙伴人已经知道解决方案了,

Object.defineProperty 这个方法给大家带来了福音 具体参考 Object 文档。我们使用一个enumerable (不可枚举)属性就可以解决了。

在原来基础上添加以下代码即可。

Object.defineProperty(Object.prototype, 'deepClone', {enumerable: false});

再看控制台

同样上面方法中也是无法克隆一个不可枚举的属性。

完整代码如下:

复制代码

const deepClone = function(obj) {
  // 先检测是不是数组和Object
  // let isArr = Object.prototype.toString.call(obj) === '[object Array]';
  let isArr = Array.isArray(obj);
  let isJson = Object.prototype.toString.call(obj) === '[object Object]';
  if (isArr) {
    // 克隆数组
    let newObj = [];
    for (let i = 0; i < obj.length; i++) {
      newObj[i] = deepClone(obj[i]);
    }
    return newObj;
  } else if (isJson) {
    // 克隆Object
    let newObj = {};
    for (let i in obj) {
      newObj[i] = deepClone(obj[i]);
    }
    return newObj;
  }
  // 不是引用类型直接返回
  return obj;
};

Object.prototype.deepClone = function() {
  return deepClone(this);
};
Object.defineProperty(Object.prototype, 'deepClone', {enumerable: false});

复制代码

注: 为了兼容低版本浏览器需要借助 babel-polyfill;

 附: 其他深拷贝方式选择:blog.csdn.net/ios99999/ar…

一维数据结构的深拷贝方法建议使用:Object.assign();

二维数据结构及以上的深拷贝方法建议使用:JSON.parse(JSON.stringify());

特别复杂的数据结构的深拷贝方法建议使用:Loadsh.cloneDeep();

 

JSON.parse(JSON.stringify(obj))是最简单粗暴的深拷贝,能够处理JSON格式的所有数据类型,但是对于正则表达式类型、函数类型等无法进行深拷贝,而且会直接丢失相应的值,还有就是它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。同时如果对象中存在循环引用的情况也无法正确处理:

复制代码

var obj = { a: {a: "hello"}, b: 33 };
var newObj = JSON.parse(JSON.stringify(obj));
newObj.b = "hello world";
console.log(obj);    //  { a: "hello", b: 33 };
console.log(newObj);    //  { a: "hello world", b: 33};
console.log(obj==newObj);  //  false
console.log(obj===newObj);  //  false

复制代码

原博主:www.cnblogs.com/momo798/p/9…