Skip to content

Latest commit

 

History

History
414 lines (331 loc) · 15.7 KB

js13.md

File metadata and controls

414 lines (331 loc) · 15.7 KB

常用编程技巧

这里会收录一些好玩的 js 技巧,写的不全,欢迎大家随时补充啊。

函数节流(throttle)与函数去抖(debounce)

产生原因

以下场景往往由于事件频繁被触发,因而频繁执行DOM操作、资源加载等重行为,导致UI停顿甚至浏览器崩溃。

  1. window对象的resize、scroll事件

  2. 拖拽时的mousemove事件

  3. 射击游戏中的mousedown、keydown事件

  4. 文字输入、自动完成的keyup事件

实际上对于window的resize事件,实际需求大多为停止改变大小n毫秒后执行后续处理;而其他事件大多的需求是以一定的频率执行后续处理。针对这两种需求就出现了debouncethrottle两种解决办法。

throttledebounce 是解决请求和响应速度不匹配问题的两个方案。二者的差异在于选择不同的策略。

  • throttle 等时间间隔执行函数。
  • debounce 时间间隔 t 内若再次触发事件,则重新计时,直到停止时间大于或等于 t 才执行函数。

debounce

当调用动作n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间。
接口定义如下:

/**
* 空闲控制 返回函数连续调用时,空闲时间必须大于或等于 idle,action 才会执行
* @param idle   {number}    空闲时间,单位毫秒
* @param action {function}  请求关联函数,实际应用需要调用的函数
* @return {function}    返回客户调用函数
*/
debounce(idle, action) 

undersorce.js 中实现方案如下:

_.debounce = function(func, wait, immediate) {
    // immediate默认为false
    var timeout, args, context, timestamp, result;
    var later = function() {
      // 当wait指定的时间间隔期间多次调用_.debounce返回的函数,则会不断更新timestamp的值,导致last < wait && last >= 0一直为true,从而不断启动新的计时器延时执行func
      var last = _.now() - timestamp;

      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last);
      } else {
        timeout = null;
        if (!immediate) {
          result = func.apply(context, args);
          if (!timeout) context = args = null;
        }
      }
    };

    return function() {
      context = this;
      args = arguments;
      timestamp = _.now();
      // 第一次调用该方法时,且immediate为true,则调用func函数
      var callNow = immediate && !timeout;
      // 在wait指定的时间间隔内首次调用该方法,则启动计时器定时调用func函数
      if (!timeout) timeout = setTimeout(later, wait);
      if (callNow) {
        result = func.apply(context, args);
        context = args = null;
      }

      return result;
    };
  };

throttle

预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。
接口定义如下:

/**
* 频率控制 返回函数连续调用时,action 执行频率限定为 次 / delay
* @param delay  {number}    延迟时间,单位毫秒
* @param action {function}  请求关联函数,实际应用需要调用的函数
* @return {function}    返回客户调用函数
*/
throttle(delay,action)

undersorce.js 中实现方案如下:

_.throttle = function(func, wait, options) {
    /* options的默认值
     *  表示首次调用返回值方法时,会马上调用func;否则仅会记录当前时刻,当第二次调用的时间间隔超过wait时,才调用func。
     *  options.leading = true;
     * 表示当调用方法时,未到达wait指定的时间间隔,则启动计时器延迟调用func函数,若后续在既未达到wait指定的时间间隔和func函数又未被调用的情况下调用返回值方法,则被调用请求将被丢弃。
     *  options.trailing = true; 
     * 注意:当options.trailing = false时,效果与上面的简单实现效果相同
     */
    var context, args, result;
    var timeout = null;
    var previous = 0;
    if (!options) options = {};
    var later = function() {
      previous = options.leading === false ? 0 : _.now();
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      var now = _.now();
      if (!previous && options.leading === false) previous = now;
      // 计算剩余时间
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 当到达wait指定的时间间隔,则调用func函数
      // 精彩之处:按理来说remaining <= 0已经足够证明已经到达wait的时间间隔,但这里还考虑到假如客户端修改了系统时间则马上执行func函数。
      if (remaining <= 0 || remaining > wait) {
        // 由于setTimeout存在最小时间精度问题,因此会存在到达wait的时间间隔,但之前设置的setTimeout操作还没被执行,因此为保险起见,这里先清理setTimeout操作
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // options.trailing=true时,延时执行func函数
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

递归计算优化 memoization

memoization适用于递归计算场景,递归操作会随着递归次数的增加,导致执行时间的成指数上升。 memoization的技巧在于将计算过的结果『缓存』下来,避免重复计算带来的成本,提高执行效率。

memozization 定义

Memoization 是一种将函数返回值缓存起来的方法,Memoization 原理非常简单,就是把函数的每次执行结果都放入一个键值对(数组也可以,视情况而定)中,在接下来的执行中,在键值对中查找是否已经有相应执行过的值,如果有,直接返回该值,没有才 真正执行函数体的求值部分。很明显,找值,尤其是在键值对中找值,比执行函数快多了。现代 JavaScript 的开发也已经大量使用这种技术。
例如计算斐波那契数列:

function fibonacci(n) {
  count++;
  if (n == 0 || n == 1) {
    return n;
  } else {
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
}

当n逐渐增大,运行时间越来越长。

这里我们写一个简单的 memoizer 函数来测试一下:

function memoizer(fundamental, cache) {   
  cachecache = cache || {};   
  var shell = function(arg) {   
      if (! (arg in cache)) {   
          cache[arg] = fundamental(shell, arg);   
      }   
      return cache[arg];   
  };   
  return shell;   
} 

重新包装 fibonacci 函数:

var fibonacci = memoizer(function(recur, n) {   
  return recur(n - 1) + recur(n - 2);   
}, { "0": 0, "1": 1} ); 
fibonacci(50);

这次会很快计算出想要的结果。关于 memozization 更多细节内容,请查看性能优化:memoization

js 种子随机数

试着想一下,如果在某一个场景,我们做一个游戏,用户玩到一半的时候退出了,这样用户下次进来可以选择继续上一次的进度继续玩,那么现在问题来了:用户玩的进度以及用户的积分等简单的描述数据,我们都可以记录下来,但是游戏里绘制的障碍物、飞行物以及很多装饰类的小玩意儿,他们甚至是每次用户点开始随机输出的,要把画布上所有的东西以及它们的大小,位置等都记录下来,实在是没必要。 于是种子随机数就闪亮登场了,我们如果在画布上元素随机绘制的时候,有一个种子值,页面上所有元素的位置、大小等都是根据这个种子来算的,那么等到第二次绘制的时候只需要传入这个种子,就可以重现之前未完成的画布元素。
这个时候我们用 Math.random 就不好用了,为此我们需要实现一个指定种子的随机数,如果种子不变,那么随机数不会产生变化。

方案1:

Math.random = function(seed){
    return ('0.'+Math.sin(seed).toString().substr(6));
} 

这也是一个比较常用的方案

方案2:

Math.seed = 5; 
Math.seededRandom = function(max, min) { 
    max = max || 1; 
    min = min || 0; 
    Math.seed = (Math.seed * 9301 + 49297) % 233280; 
    var rnd = Math.seed / 233280.0; 
    return min + rnd * (max - min); 
};

像Math.seededRandom这种伪随机数生成器叫做线性同余生成器(LCG, Linear Congruential Generator),几乎所有的运行库提供的rand都是采用的LCG,形如:

I n+1=aI n+c(mod m)

生成的伪随机数序列最大周期m,范围在0到m-1之间。要达到这个最大周期,必须满足:

  1. c与m互质
  2. a - 1可以被m的所有质因数整除
  3. 如果m是4的倍数,a - 1也必须是4的倍数

以上三条被称为Hull-Dobell定理。作为一个伪随机数生成器,周期不够大是不好意思混的,所以这是要求之一。因此才有了:a=9301, c = 49297, m = 233280这组参数,以上三条全部满足。

方案3:
我们可以采用seedRandom库来实现。

Navigation Timing 网页性能监控

Navigation Timing 是一个可以在web中精确测量性能的javascript API。这个API提供了一个简单的方法来获得页面导航、加载事件的精确而又详细的时间状态。目前在 IE9、Chrome、Firefox nightly builds 中可用。
该API通过window.performance对象的属性来报告页面被导航或被加载的时间及相关信息。

  • navigation:用户是怎样导航到当前页面的。
  • timing:页面被导航或被加载完毕时所耗费的时间。
  • 在Chrome浏览器中,同时提供performance.memory属性,用于报告JavaScript的内存耗费情况。

兼容性如下:

使用 performance.timing 信息简单计算出网页性能数据

function getPerformanceTiming () {  
    var performance = window.performance;
 
    if (!performance) {
        // 当前浏览器不支持
        console.log('你的浏览器不支持 performance 接口');
        return;
    }
 
    var t = performance.timing;
    var times = {};
 
    //【重要】页面加载完成的时间
    //【原因】这几乎代表了用户等待页面可用的时间
    times.loadPage = t.loadEventEnd - t.navigationStart;
 
    //【重要】解析 DOM 树结构的时间
    //【原因】反省下你的 DOM 树嵌套是不是太多了!
    times.domReady = t.domComplete - t.responseEnd;
 
    //【重要】重定向的时间
    //【原因】拒绝重定向!比如,http://example.com/ 就不该写成 http://example.com
    times.redirect = t.redirectEnd - t.redirectStart;
 
    //【重要】DNS 查询时间
    //【原因】DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长?
    // 可使用 HTML5 Prefetch 预查询 DNS ,见:[HTML5 prefetch](http://segmentfault.com/a/1190000000633364)            
    times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;
 
    //【重要】读取页面第一个字节的时间
    //【原因】这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么?
    // TTFB 即 Time To First Byte 的意思
    // 维基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte
    times.ttfb = t.responseStart - t.navigationStart;
 
    //【重要】内容加载完成的时间
    //【原因】页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么?
    times.request = t.responseEnd - t.requestStart;
 
    //【重要】执行 onload 回调函数的时间
    //【原因】是否太多不必要的操作都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么?
    times.loadEvent = t.loadEventEnd - t.loadEventStart;
 
    // DNS 缓存时间
    times.appcache = t.domainLookupStart - t.fetchStart;
 
    // 卸载页面的时间
    times.unloadEvent = t.unloadEventEnd - t.unloadEventStart;
 
    // TCP 建立连接完成握手的时间
    times.connect = t.connectEnd - t.connectStart;
 
    return times;
}

ES6 技巧

通过参数默认值强制要求传参

ES6 指定默认参数在它们被实际使用的时候才会被执行,这个特性让我们可以强制要求传参:

/**
* Called if a parameter is missing and
* the default value is evaluated.
*/
function mandatory() {
    throw new Error("Missing parameter");
}
function foo(mustBeProvided = mandatory()) {
    return mustBeProvided;
}

函数调用 mandatory() 只有在参数 mustBeProvided 缺失的时候才会被执行。

通过 for-of 循环来遍历数组元素和索引

方法 forEach() 允许你遍历一个数组的元素和索引:

var arr = ["a", "b", "c"];
arr.forEach(function (elem, index) {
    console.log("index = "+index+", elem = "+elem);
});
// Output:
// index = 0, elem = a
// index = 1, elem = b
// index = 2, elem = c

ES6 的 for-of 循环支持 ES6 迭代(通过 iterables 和 iterators)和解构。如果你通过数组的新方法 enteries() 再结合解构,可以达到上面 forEach 同样的效果:

const arr = ["a", "b", "c"];
for (const [index, elem] of arr.entries()) {
    console.log(`index = ${index}, elem = ${elem}`);
}

通过变量解构交换两个变量的值

如果你将一对变量放入一个数组,然后将数组解构赋值相同的变量(顺序不同),你就可以不依赖中间变量交换两个变量的值:

[a, b] = [b, a];

通过模板字面量(template literals)进行简单的模板解析

ES6 的模板字面量与文字模板相比,更接近于字符串字面量。但是,如果你将它们通过函数返回,你可以使用他们来做简单的模板渲染:

const tmpl = addrs => `
    <table>
    ${addrs.map(addr => `
        <tr><td>${addr.first}</td></tr>
        <tr><td>${addr.last}</td></tr>
    `).join("")}
    </table>
`;    

tmpl 函数将数组 addrs 用 map(通过箭头函数) join 拼成字符串。tmpl() 可以批量插入数据到表格中:

const data = [
    { first: "<Jane>", last: "Bond" },
    { first: "Lars", last: "<Croft>" },
];
console.log(tmpl(data));
// Output:
// <table>
//
//     <tr><td><Jane></td></tr>
//     <tr><td>Bond</td></tr>
//
//     <tr><td>Lars</td></tr>
//     <tr><td><Croft></td></tr>
//
// </table> 

通过子类工厂实现简单的合成器

当 ES6 类继承另一个类,被继承的类可以是通过任意表达式创建的动态类:

// Function id() simply returns its parameter
const id = x => x;

class Foo extends id(Object) {}

这个特性可以允许你实现一种合成器模式,用一个函数来将一个类 C 映射到一个新的继承了C的类。例如,下面的两个函数 Storage 和 Validation 是合成器:

const Storage = Sup => class extends Sup {
    save(database) { ··· }
};
const Validation = Sup => class extends Sup {
    validate(schema) { ··· }
};

你可以使用它们去组合生成一个如下的 Employee 类:

class Person { ··· }
class Employee extends Storage(Validation(Person)) { ··· }