emu
7/27/2017 - 3:50 PM

Advanced Tricks on JavaScript

Advanced Tricks on JavaScript

这里整理记录以下过去一段时间学习和收集的一些JavaScript高级技巧

  1. 安全类型检测
  2. 作用域安全的构造函数
  3. 惰性载入
  4. 函数绑定
  5. 函数柯理化
  6. 线程控制

安全类型检测

在多个全局作用域的情况下,instanceof会出现类型检测出错的情况,需要使用更安全的Object.prototype.toString.call(param)方案。

但是这个方案有其局限性,可以利用ES原生操作符或属性补充。

方案对比\检测对象原始值类型内置引用类型自定义引用类型描述
typeof可检测,除Null类型外Function类型可检测,其他检测为"object"检测为"object"常用于原始值和函检测
instanceof——可检测可检测
  • 因为构造函数是全局对象的属性,在跨全局作用域的情况下,instanceof可检测错误
  • instanceof运算符实际上是依赖构造函数的(prototype属性所指向的)原型对象与实例的__proto__属性比较,受限于构造函数的prototype属性
Object.prototype.toString.call可检测可检测检测为"Object"只能准确检测原生类型
实例的constructor属性可检测,除Undefined和Null类型外可检测可检测实例的constructor属性继承自构造函数对应的原型

结合Object.prototype.toString.call和实例的constructor属性,可较全面准确地检测数据类型

/**
 * 跨全局作用域的,全类型检测
 * @param  {any} param 待检测数据
 * @return {string}    被检测数据的类型
 */
function typeOf (param) {
    // 检测ES原生类型
    var type = Object.prototype.toString.call(param).slice(8, -1);

    // Object.create(null)生成的对象,以及宿主环境里的对象并未继承Object.prototype
    // 需要借用Object.prototype.hasOwnProperty,检测对象的自有属性
    var _hasOwn = Object.prototype.hasOwnProperty;

    // 检测自定义类型
    if (type == 'Object') {
        if (param.constructor &&
            // 排除Object类型
            !_hasOwn.call(param, 'constructor') &&
            !_hasOwn.call(param, 'isPrototypeOf')) {
            type = param.constructor.toString().match(/function\s*([^\(\s]*)/)[1];
        }
    }

    // 返回的数据类型跟数据的构造函数名相同,即类型名区分大小写
    return type;
}

作用域安全的构造函数

由于构造函数会操作this,必须要保证构造函数的正确使用,否则会污染全局变量window的命名空间

function Car (brand) {
    // 确保得到的是一个实例而不是window的属性
    if (this instanceof Person) {
        this.brand = brand;
    } else {
        return new Car(brand);
    }
}

惰性载入

惰性载入是一种提升加载和运算性能的技术。

前端主要存在两种形式的惰性载入:

  1. 函数的惰性初始化
  2. 页面内容的延迟加载

惰性初始化

函数惰性载入常用于浏览器兼容性判断的逻辑分支中替代原有函数,避免反复判断带来的性能损耗。

有两种实现方式,区分在于逻辑分支替换原有函数的时机:

  1. 当第一次调用该函数时进行替换,然后执行

    function createXHR(){
        if (typeof XMLHttpRequest != "undefined"){
            // 替换原函数
            createXHR = function(){
                return new XMLHttpRequest();
            };
        } else if (typeof ActiveXObject != "undefined"){
            // 替换原函数
            createXHR = function(){
                if (typeof arguments.callee.activeXString != "string"){
                    var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
                                    "MSXML2.XMLHttp"],
                        i, len;
            
                    for (i=0,len=versions.length; i < len; i++){
                        try {
                            new ActiveXObject(versions[i]);
                            arguments.callee.activeXString = versions[i];
                        } catch (ex){}
                    }
                }
            
                return new ActiveXObject(arguments.callee.activeXString);
            };
        } else {
            createXHR = function(){
                throw new Error("No XHR object available.");
            };
        }
        
        // 最后执行被替换后的函数
        return createXHR();
    }
    
  2. 在函数F声明的时候就执行一个匿名函数,匿名函数的返回值作为函数F的主体

    var createXHR = (function(){ // 立即执行函数的返回值做为声明的函数
        if (typeof XMLHttpRequest != "undefined"){
            return function(){
                return new XMLHttpRequest();
            };
        } else if (typeof ActiveXObject != "undefined"){
            return function(){                    
                if (typeof arguments.callee.activeXString != "string"){
                    var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
                                    "MSXML2.XMLHttp"],
                        i, len;
            
                    for (i=0,len=versions.length; i < len; i++){
                        try {
                            new ActiveXObject(versions[i]);
                            arguments.callee.activeXString = versions[i];
                            break;
                        } catch (ex){}
                    }
                }
            
                return new ActiveXObject(arguments.callee.activeXString);
            };
        } else {
            return function(){
                throw new Error("No XHR object available.");
            };
        }
    })(); // 加载即执行
    

页面延迟加载

延迟加载的目的是按需加载,提升页面初加载速度

  1. 瀑布流/无限卷动是较常见的延迟加载技术,下面是一个简单的图片延迟载入示例

    <head>
        <meta charset="UTF-8">
        <title>Infinite Scroll</title>
        <style>
            body {width: 100%;height:100%;}
            img {display: block;margin-bottom: 50px;height: 400px;}
        </style>
    </head>
    <body>
        <img src="loading.gif" data-src="../images/bg/01.jpg">
        <img src="loading.gif" data-src="../images/bg/02.jpg">
        <img src="loading.gif" data-src="../images/bg/03.jpg">
        <img src="loading.gif" data-src="../images/bg/04.jpg">
        <img src="loading.gif" data-src="../images/bg/05.jpg">
        <img src="loading.gif" data-src="../images/bg/06.jpg">
        <img src="loading.gif" data-src="../images/bg/07.jpg">
        <img src="loading.gif" data-src="../images/bg/08.jpg">
        <script type="text/javascript">
        function infiScroll () {
            var images = document.getElementsByTagName('img'),
                len = images.length,
                n = 0;
            return function () {
                var seeHeight = document.documentElement.clientHeight || document.body.clientHeight,
                    scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
                    img;
                for (var i = n; i < len; i++) {
                    img = images[i];
                    if (img.offsetTop <= seeHeight + scrollTop) {
                        if (img.getAttribute('src') === 'loading.gif') {
                            img.src = img.getAttribute('data-src');
                            n++;
                        }
                    }
                }
            };
        }
        var loadImages = infiScroll();
        window.addEventListener('load', loadImages, false);
        window.addEventListener('scroll', loadImages, false);
        </script>
    </body>
    

    目前Chrome 51+版本的浏览器实现了一个新的IntersectionObserver API,可以异步地自动“观察”元素是否进入了可视区域。上面页面代码的脚本部分可以全部替换成如下来实现:

    var io = new IntersectionObserver(function(items) {
        items.forEach(function(item) {
            var target = item.target;
            if (target.getAttribute('src') === 'loading.gif') {
                target.src = target.getAttribute('data-src');
            }
        });
    });
    
    Array.from(document.getElementsByTagName('img')).forEach(function(item) {
        // 插入观察队列
        io.observe(item);
    });
    
  2. 脚本和样式表,也可以动态加载。尤其是在页面需要加载的数据大且部分数据暂时用不上的时候,特别有用。

    // 动态加载脚本
    function loadScript(url) {
        var script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = url;
        document.body.appendChild(script);
    }
    
    // 动态加载样式表
    function loadStyles(url) {
        var link = document.createElement('link');
        link.rel = 'stylesheet';
        link.type = 'text/css';
        link.href = url;
        document.getElementsByTagName('head')[0].appendChild(link);
    }
    

    动态脚本技术还可以应用在跨站数据访问CORS上,即JSONP技术。

参考阅读

  1. JavaScript高级程序设计(第三版) P277 P600
  2. 延迟加载(Lazyload)三种实现方式
  3. IntersectionObserver API

函数绑定

函数绑定是JavaScript一大特性第一类函数(First-class Function)的展现 —— 函数可以作为参数传入另一个函数,并且返回别的函数,这个函数已经绑定了固定的参数或作用域。
研究函数绑定的内部实现原理,可以帮助我们加深对JavaScript这门语言的了解

从简单的开始,了解其原理

function bind(fn, context) { // fn是待绑定作用域的函数
    return function () {
        return fn.apply(context, arguments); // 只绑定了作用域
    }
}

ES5开始,function类型提供了原生的bind方法Function.prototype.bind(thisArg, arg1, arg2, ...)

var math = {
    a: 1,
    b: 2,
    add: function () {
        return this.a + this.b;
    }
};
math.add.bind({a: 10, b: -1})(); // output: 9

但是旧版本的浏览器不支持,时常需要兼容的代码封装

if (!Function.prototype.bind) {
    Function.prototype.bind = function (context) {
        // 由于arguments不是真Array类型,故借用数组原型的slice方法取带绑定参数
        var args = Array.prototype.slice.call(arguments, 1),
            _self = this;
        return function () {
            // 拼接绑定的参数和后接收的参数,最后一起传入绑定作用域的函数,执行
            return _self.apply(context, args.concat(Array.prototype.slice.call(arguments)));
        }
    }
}

完整版

if (!Function.prototype.bind) {
    Function.prototype.bind = function(oThis) {
        if (typeof this !== 'function') {
            // closest thing possible to the ECMAScript 5
            // internal IsCallable function
            throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
        }

        //建立“参数池”,并存储第一次绑定参数,this指向的对象除外
        var aArgs = Array.prototype.slice.call(arguments, 1),
            //第一次绑定的this指向的对象
            fToBind = this,
            fNOP = function() {},
            fBound = function() {
                // 如果fToBind是构造函数,保留指向实例的this,如果不是,将fToBind绑定到传入的oThis作用域上
                return fToBind.apply(this instanceof fNOP ? this : oThis,
                     // 返回的函数可以再次接受参数,并通过concat补充到“参数池”
                    aArgs.concat(Array.prototype.slice.call(arguments)));
            };

        if (this.prototype) {
            // 注意特殊点:js的function既是函数也是对象(有prototype属性),但是Function的原型属性prototype是没有prototype属性的
            // 当this == Function.prototype时,Function.prototype没有prototype
            // 当fToBind是一个构造函数时,fBound函数中的this是这个构造函数对应的原型的实例的实例
            fNOP.prototype = this.prototype;
        }
        //fBound->fBound.prototype==>someProto === fToBind.prototype, this === bind的调用者 === fToBind
        //                              ↑
        //                          fNOP.prototype
        // 构成原型链继承,fBound构建的实例继承了fNOP.prototype(即this.prototype或fToBind.prototype)
        // 这一段对应fBound的函数声明中“this instanceof fNOP”的条件操作
        fBound.prototype = new fNOP();

        return fBound;
    };
}

参考阅读

  1. Function.prototype.bind

函数柯理化

函数柯理化(currying),把可以接受多个参数的函数转变为一个接受部分参数的函数,这个函数会返回一个接受剩余参数并且返回结果的新函数的技术。
这个技术可以帮助我们把复杂的逻辑拆分成多个小的,进行逐步或独立的运算,便于测试与调整。应用面很广,在异步逻辑中很常见。
Javascript的函数柯理化,应用到了闭包和第一类函数的特性。在绑定参数方面,原理跟bind函数相似。

我们来看一个简单的应用

function add (n) {
    return function (m) {
        return n + m;
    }
}
add(1)(3); // 4

也可以传入数个参数,在函数声明时并未知道明确多少个

function add () {
    var _slice = Array.prototype.slice,
    args1 = _slice.call(arguments);
    return function () {
        var args2 = _slice.call(arguments);
        return args1.concat(args2).reduce(function (acc, val) {return acc + val});
    }
}
add(1, 3)(5, 7, 9);

当然为了代码复用,我们可以封装一个柯理化工具函数

var _slice = Array.prototype.slice;
function add() {
    return _slice.call(arguments).reduce(function(acc, val) {
        return acc + val;
    });
}
function curry(fn) {
    var args1 = _slice.call(arguments, 1);
    return function() {
        var args2 = _slice.call(arguments);
        return fn.apply(null, args1.concat(args2));
    }
}
function curryAdv(fn, len) {
    var len = len || fn.length;
    return function() {
        var args = _slice.call(arguments);
        if (args.length >= len) {
            return fn.apply(null, args);
        } else {
            // 作为传入curry的参数列表,第一个参数是需要柯理化的函数fn
            var argsToFill = [fn].concat(args);
            // 利用apply和curry将argsToFill绑定到返回的一个匿名函数
            // 因为这是递归,根据尾调优化,需要优化性能就最好放在逻辑分支最后
            // 且只传入数据而不需等待返回值再操作
            return curryAdv(curry.apply(null, argsToFill), len - args.length);
        }
    }
}

var sum = curry(add, 1, 3);
sum(5, 7, 9); // 25

var sumAdv = curryAdv(add, 5);
sumAdv(1, 3)(5)(7, 9); // 25

在异步事件处理时,柯理化可以帮助我们提高代码可读性

// 由于CORS跨域保护
// 这段代码需要在任何mozilla.org域名的页面下运行
// 可用chrome的devtools测试
function ajax(method, url) {
    var xhr = new XMLHttpRequest();
    xhr.open(method, url);
    xhr.onload = function() {
        pipe(xhr);
    }
    xhr.send(null);
    
    var pipe = function () {};
    return {
        getFn: function(fn) {
            pipe = fn;
        }
    };
}
ajax('get', 'https://developer.mozilla.org/')
.getFn(function(xhr) {
    console.log(succeed to get ajax, xhr.statusText);
});

一个关于柯理化的面试题,非常有趣。要求实现如下输出的sum函数

sum(1); // 1
sum(1)(2); // 3
sum(1)(2)(3); // 6

实现如下

// 返回值是Function类型的
function sum(n) {
    var fn = function(x) {
        if (x !=null) n+=x;
        return fn;
    };
    fn.valueOf = function() {
        return n;
    };
    return fn;
}
// type Function
sum(1); // 1
sum(1)(2); // 3
sum(1)(2)(3); // 6
// type Number
+sum(1)(2)(3); // 6

其实返回值fn是Function类型的,只不过在调试工具的console中或者通过window.console这类函数打印结果时是会隐式地、依次地调用函数fnvalueOftoString方法(先调用valueOf如果返回值不是Undefined/Boolean/Number/String类型的,就继续调用toString),返回函数对象本身或函数的实现依赖字符串(implementation-dependent string)源码。
但是由于这里将valueOf重写为返回参数的和,但是返回值又必须是Function类型的,所以就得到一个Function类型的数字。
不过,如果利用加号操作符可以将其转换为Number类型:+sum(1)(2)(3)

可以改进以下,得到正确的数值

// sum(1)(2)返回值是Function类型的3,sum(1)(2)()返回值是一个Number类型的3
function sum(n) {
    function fn (x) {
        return x == null ? n : (n+=x, fn);
    };
    fn.valueOf = function() {
        return n;
    };
    return fn;
}
// type Function
sum(1); // 1
sum(1)(2); // 3
sum(1)(2)(3); // 6
// type Number
sum(1)(2)(); // 3

这里由于对最后参数为空的情况做了分支处理,所以sum(1)(2)()得到的才是真正的数值

再改进以实现多态化

// 返回值是Function类型的
var _slice = Array.prototype.slice;
function sum() {
    var allArgs = _slice.call(arguments);
    function fn() {
        var args = _slice.call(arguments);
        allArgs = allArgs.concat(args);
        return fn;
    }
    fn.valueOf = function() {
        return allArgs.reduce(function(acc, val) {
            return acc + val;
        });
    }
    return fn;
}
// type Function
sum(1)(2); // 3
sum(1)()(2); // 3
sum()(1)(2); // 3
sum(1)(2, 3)(4)(); // 10

如前面所示,可以通过加号操作符将结果转换为Number类型:+sum(1)()(2)

参考阅读

  1. 掌握JavaScript函数的柯里化
  2. 一道面试题引发的对javascript类型转换的思考
  3. Variadic curried sum function
  4. JavaScript问题集锦
  5. Why is toString of JavaScript function implementation-dependent?

高级定时器

由于JavaScript是单线程运行的,定时器仅仅是将待执行代码推入一个计划队列,并不能确保某个确定时间点后一定执行。

利用重复定时,可以实现自定义动画

<head>
    <meta charset="UTF-8">
    <title>定时器动画</title>
    <style type="text/css">
        * {margin: 0;padding: 0;}
        body {height: 100%;}
        #box {width:50px;height:50px;background: orange;}
    </style>
</head>
<body>
    <div id="box"></div>
    <script type="text/javascript">
        function slide(el, pos, delay) {
            var style = el.style,
                step = 10;
            style.position = 'absolute';
            setTimeout(function() {
                var left = parseFloat(style.left || 0);
                if (pos - left > step) {
                    style.left = left + step + 'px';
                    setTimeout(arguments.callee, delay);
                } else {
                    style.left = pos + 'px';
                    style = null;
                }
            }, delay);
        }

        slide(document.getElementById('box'), 200, 100);
    </script>
</body>

除了自定义动画,函数间断执行、节流控制等需要定时控制的的代码都需要用到定时器

利用setTimeout,可以将执行逻辑安全的推入执行队列中,而不会出现乱序或跳动,保证代码在某段时间内的独占运行

function doSomething(){
    // your codes
}
setTimeout(doSomething,  0); // 注意超时间隔是0

分割处理

在大量处理的循环或过深嵌套的函数调用的时候,一次性处理完对所有操作,会阻塞浏览器处理其他事务的线程,比如:用户交互操作。所以一般需要分块、定时进行。
对大量处理的循环,可以进行数组分块处理(Array chunking)。
但同时要注意的是数据分块的处理结果需要异步操作。

function chunk(arr, proc, ctx, fn) {
    var DELAY = 100;
    var result = [];
    setTimeout(function() {
        var itm = arr.shift();
        result.push(proc.call(ctx, itm));
        if (arr.length > 0) {
            setTimeout(arguments.callee, DELAY);
        } else {
            fn(result); // 执行异步回调
            result = null;
        }
    }, 0);
}
function x2(n) {
    console.log('get data: ', n);;
    return n * 2;
}
chunk([1,3,5,7,9], x2, null, function(arr) {
    console.log(arr);
});

当然除了利用定时器分割处理,有些大的数据循环本身可以优化逻辑
简单的优化循环

function loop(arr, proc) {
    var i = arr.length - 1;
    if (i < 0) return; // 配合后测试循环
    do {
        proc(arr[i]);
    } while (--i >= 0); // 后测试循环,减值迭代,精简终止条件
}

Duff技术优化循环

function duffLoop(arr, proc) {
    var len = arr.length,
        // 分割成8个一组,不够8个的是最后一组
        repeats = Math.ceil(len / 8),
        startAt = len % 8,
        i = 0;
    do { // 从最后一组开始
        switch (startAt) { // 
            case 0: proc(arr[i++]);
            case 7: proc(arr[i++]);
            case 6: proc(arr[i++]);
            case 5: proc(arr[i++]);
            case 4: proc(arr[i++]);
            case 3: proc(arr[i++]);
            case 2: proc(arr[i++]);
            case 1: proc(arr[i++]);
        }
        startAt = 0;
    } while (--repeats > 0);
}

进阶版Duff

function duffLoopAdv(arr, proc) {
    var len = arr.length,
        // 分割成8个一组,不够8个的一组先行运算
        repeats = Math.floor(len / 8),
        leftNum = len % 8,
        i = 0;
    if (leftNum > 0) {
        do {
            proc(arr[i++]);
        } while (--leftNum > 0);
    }
    if (repeats <= 0) return;
    do {
        proc(arr[i++]);
        proc(arr[i++]);
        proc(arr[i++]);
        proc(arr[i++]);
        proc(arr[i++]);
        proc(arr[i++]);
        proc(arr[i++]);
        proc(arr[i++]);
    } while (--repeats > 0);
}

结合定时器和Duff

/**
 * 控制数组分块循环处理
 * @param  {array}    arr   待处理的数组
 * @param  {function} proc  处理数组项的函数
 * @param  {object}   ctx   proc的调用对象
 * @param  {function} fn    执行完成后的异步回调
 * @param  {number}   delay 分割处理延时
 */
function chunkAdv(arr, proc, ctx, fn, delay) {
    var len = arr.length,
    repeats = Math.floor(len/8),
    leftNum = len%8,
    i = 0,
    result = [];
    if (leftNum > 0) {
        do {
            result.push(proc.call(ctx, arr[i++]));
        }while(--leftNum > 0);
    }
    if (repeats <= 0) {
        fn(result);
        return;
    }
    setTimeout(function() {
        result.push(proc.call(ctx, arr[i++]));
        result.push(proc.call(ctx, arr[i++]));
        result.push(proc.call(ctx, arr[i++]));
        result.push(proc.call(ctx, arr[i++]));
        result.push(proc.call(ctx, arr[i++]));
        result.push(proc.call(ctx, arr[i++]));
        result.push(proc.call(ctx, arr[i++]));
        result.push(proc.call(ctx, arr[i++]));
        if (--repeats > 0) {
            setTimeout(arguments.callee, delay);
        } else {
            fn(result);
            result = null;
        }
    }, delay);
}

function x2(n) {
    console.log('get data: ', n);;
    return n * 2;
}
chunkAdv([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22], x2, null, function(arr) {
    console.log(arr);
}, 500);

函数执行控制

在频繁触发事件中,抑制一定的处理逻辑,可以有效优化页面,避免崩溃。
常见于UI事件的优化处理。

  1. debounce用于控制在连续调用完成后再过一定时间的函数执行,如同:压住的弹簧,松开以后过一小会儿才能弹起来

    function debounce(fn, wait) {
        var timeout = null;
        return function() {
            var args = arguments,
                that = this;
            // 再次调用时重置定时器
            clearTimeout(timeout);
            timeout = setTimeout(function() {
                fn.apply(that, args);
            }, wait);
        };
    }
    
  2. throttle用于控制在连续调用的一定时间段内函数执行的密度,如同:控制水龙头的出水量,拧到足够小,水会间隔一段时间滴出来,而不是连续流出

    function throttle(fn, wait) {
        var prev = 0;
        return function() {
            var args = arguments,
            that = this,
            now = +new Date();
            if (!prev) prev = now;
            // 首次调用,不执行
            // 且最末尾调用,也可能不执行
            if (now - prev >= wait) {
                fn.apply(that, args);
                prev = 0;
            }
        }
    }
    

    不过由于以上两个实现比较简单,控制程度不高,没有把复杂情况和多用性解决。连续调用触发时,往往会屏蔽掉首次函数执行,也就是后文underscore.js中提到的开始边界的执行。

  3. underscore.js对上述两个节流函数有更好的实现

    _.now = Date.now || function() {
        return new Date().getTime();
    };
    
    /**
     * 执行空闲控制 返回的匿名函数被连续调用时,空闲时间不小于wait毫秒,func才会执行
     * @param  {function} func      调用空闲时执行的函数
     * @param  {number}   wait      调用空闲的时间间隔
     * @param  {boolean}  immediate 如需设置在开始边界执行func,则设置为true
     *                              否则默认为末尾边界执行
     * @return {function}           返回待被调用的函数,控制传入的func的执行
     */
    _.debounce = function(func, wait, immediate) {
        var timeout, args, context, timestamp, result;
    
        var later = function() {
            // 定时器生效时,求得前后时间差
            var last = _.now() - timestamp;
            // 按理说,定时器生效时,时间差last应该是大于或等于超时时间wait的
            // 但是考虑到在代码运行时系统时间有可能调整,等等特殊情况
            if (last < wait && last >= 0) {
                // 重启定时器,设置新超时时间为
                // 旧超时时间wait与旧定时器生效时前后时间差last的差值
                timeout = setTimeout(later, wait - last);
            } else {
                // 定时器准确生效,重置timeout
                timeout = null;
                // 如果没有开启开始边界执行
                // 那么在定时器生效时调用func
                if (!immediate) {
                    result = func.apply(context, args);
                    if (!timeout) context = args = null;
                }
            }
        };
        return function() {
            context = this;
            args = arguments;
            // 参考起始时间
            timestamp = _.now();
            var callNow = immediate && !timeout;
            // 当首次调用时,开启定时器
            if (!timeout) timeout = setTimeout(later, wait);
            // 若设置了立即调用immediate,当首次调用时,立即执行func
            if (callNow) {
                result = func.apply(context, args);
                // func执行完成后,释放参数
                context = args = null;
            }
            // 如若不是首次调用,在定时器生效前,直接返回,而不再设置定时器
            return result;
        };
    };
    
    /**
     * 执行频率控制 返回的匿名函数被连续调用时,func的执行频率控制在1次/wait毫秒
     * @param  {function} func    需要控制执行频率的函数
     * @param  {number}   wait    控制执行的时间间隔
     * @param  {object}   options 若想设置开始边界不执行,则单独传入{leading: false}
     *                            若想设置末尾边界不执行,则单独传入{trailing: false}
     *                            若想双边界都执行,则无需传入此参数
     * @return {function}         返回待被调用的函数,控制传入的func的执行
     */
    _.throttle = function(func, wait, options) {
        var context, args, result;
        var timeout = null;
        // 参考起始时间
        var previous = 0;
        if (!options) options = {};
        var later = function() {
            // 若设置了开始边界不执行,则定时器生效时,重置起始时间,与#1契合
            previous = options.leading === false ? 0 : _.now();
            // 清除定时器,优化性能
            timeout = null;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
        };
        return function() {
            var now = _.now();
            // #1 首次执行时,若设置了开始边界不执行,则将起始时间设置为当前时间
            if (!previous && options.leading === false) previous = now;
            // 达到要求时间差wait的剩余差值
            var remaining = wait - (now - previous);
            context = this;
            args = arguments;
            // 若再次调用时已经满足要求时间差wait
            // 或者在代码运行时系统时间变更导致now - previous小于0
            // 则执行func
            if (remaining <= 0 || remaining > wait) {
                if (timeout) {
                    clearTimeout(timeout);
                    // 清除定时器,优化性能
                    timeout = null;
                }
                // 保留当前时间作为下一次调用的参考起始时间
                previous = now;
                result = func.apply(context, args);
                if (!timeout) context = args = null;
            // 若定时器未设定,且未设置末尾边界不执行,则开启定时器
            } else if (!timeout && options.trailing !== false) {
                timeout = setTimeout(later, remaining);
            }
            // 若设置了开始边界不执行,又设置了末尾别介不执行
            // 则保留参考起始时间,直接跳过,等待下次执行
            return result;
        };
    };
    
  4. 较新浏览器原生提供的requestanimationframe API可以帮助实现节流控制,而且比setIntervalsetTimeout这两个定时器控制更加精准

    <head>
        <meta charset="UTF-8">
        <title>rAF api</title>
        <style type="text/css">
            #box {width: 50px;height: 50px;background: orange;}
        </style>
    </head>
    <body>
        <div id="box"></div>
        <script type="text/javascript">
            var el = document.getElementById('box'),
                style = el.style,
                start = null;
            style.position = 'absolute';
            function update(timestamp) {
                // 首次调用时,初始化动画参考起始时间
                if (!start) start = timestamp;
                // 当前时间与动画起始时间的差值
                var diff = timestamp - start;
                // 每次移动时,每10ms向右移动1px
                // 最大移动右移为200px
                style.left = Math.min(diff / 10, 200) + 'px';
                // 时间差值小于2000ms,也就是右移未达到200px时
                // 持续动画效果
                if (diff < 2000) {
                    requestAnimationFrame(update);
                }
            }
            // 开始结束时间以及延迟间隔时间全由rAF自动决定
            requestAnimationFrame(update);
        </script>
    </body>
    

参考阅读

  1. JavaScript高级程序设计(第三版) P612 P615 P669
  2. Why is setTimeout(fn, 0) sometimes useful?
  3. JS魔法堂:函数节流(throttle)与函数去抖(debounce)
  4. underscore.js
  5. Debounce & Throtte JavaScript demo in different implementations
  6. 浅谈 Underscore.js 中 _.throttle 和 _.debounce 的差异
  7. JavaScript 节流函数 throttle 详解
  8. 浅谈javascript的函数节流
  9. jQuery throttle / debounce: Sometimes, less is more!
  10. Debouncing and Throttling Explained Through Examples
  11. requestAnimationFrame API