Skip to content

Instantly share code, notes, and snippets.

@uolcano
Last active May 8, 2018 06:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save uolcano/98d2525de5b9d4b8e8be155211c136b8 to your computer and use it in GitHub Desktop.
Save uolcano/98d2525de5b9d4b8e8be155211c136b8 to your computer and use it in GitHub Desktop.
Advanced Tricks on JavaScript

安全类型检测

在多个全局作用域的情况下,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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment