首页 技术 正文
技术 2022年11月15日
0 收藏 571 点赞 4,154 浏览 6235 个字

本文原链接:https://cloud.tencent.com/developer/article/1326958

https://cloud.tencent.com/developer/article/1356699

https://cloud.tencent.com/developer/article/1431398

https://cloud.tencent.com/developer/article/1123269

  • 这个系列

    2. 闭包的特性

    • 函数嵌套函数
    • 函数内部可以引用外部的参数和变量
    • 参数和变量不会被垃圾回收机制回收

    一般来说,闭包形式上来说有嵌套的函数,其可引用外部的参数和变量(自由变量),且在其上下文销毁之后,仍然存在(不会被垃圾回收机制回收)

    3. 闭包的优点

    • 使一个变量长期驻扎在内存中
    • 避免全局变量的污染
    • 作为私有成员的存在

    按照特性,闭包有着对应的优点

    比如创建一个计数器,常规来说我们可以使用类

    function Couter() {
    this.num = 0;
    }Couter.prototype = {
    constructor: Couter, // 增
    up: function() {
    this.num++;
    }, // 减
    down: function() {
    this.num--;
    }, // 获取
    getNum: function() {
    console.log(this.num);
    }
    };var c1 = new Couter();
    c1.up();
    c1.up();
    c1.getNum(); // 2var c2 = new Couter();
    c2.down();
    c2.down();
    c2.getNum(); // -2

    这挺好的,我们也可以用闭包的方式来实现

    function couter() {
    var num = 0; return {
    // 增
    up: function() {
    num++;
    },
    // 减
    down: function() {
    num--;
    },
    // 获取
    getNum: function() {
    console.log(num);
    }
    };
    }var c1 = couter();
    c1.up();
    c1.up();
    c1.getNum(); // 2var c2 = couter();
    c2.down();
    c2.down();
    c2.getNum(); // -2

    可以看到,虽然couter函数的上下文被销毁了,num仍保存在内存中

    在很多设计模式中,闭包都充当着很重要的角色,

    4. 闭包的缺点

    闭包的缺点,更多地是在内存性能的方面。

    由于变量长期驻扎在内存中,在复杂程序中可能会出现内存不足,但这也不算非常严重,我们需要在内存使用与开发方式上做好取舍。在不需要的时候清理掉变量

    在某些时候(对象与DOM存在互相引用,GC使用引用计数法)会造成内存泄漏,要记得在退出函数前清理变量

    window.onload = function() {
    var elem = document.querySelector('.txt'); // elem的onclick指向了匿名函数,匿名函数的闭包也引用着elem
    elem.onclick = function() {
    console.log(this.innerHTML);
    }; // 清理
    elem = null;
    };

    内存泄漏相关的东西,这里就不多说了,之后再整理一篇

    除此之外,由于闭包中的变量可以在函数外部进行修改(通过暴露出去的接口方法),所有不经意间也内部的变量会被修改,所以也要注意

    5. 闭包的运用

    闭包有很广泛的使用场景

    常见的一个问题是,这段代码输出什么

    var func = [];for (var i = 0; i < 5; ++i) {
    func[i] = function() {
    console.log(i);
    }
    }func[3](); // 5

    由于作用域的关系,最终输出了5

    稍作修改,可以使用匿名函数立即执行与闭包的方式,可输出正确的结果

    for (var i = 0; i < 5; ++i) {
    (function(i) {
    func[i] = function() {
    console.log(i);
    }
    })(i);
    }func[3](); // 3for (var i = 0; i < 5; ++i) {
    (function() {
    var n = i;
    func[i] = function() {
    console.log(n);
    }
    })();
    }func[3](); // 3for (var i = 0; i < 5; ++i) {
    func[i] = (function(i) {
    return function() {
    console.log(i);
    }
    })(i);
    }func[3](); // 3

    二、高阶函数

    高阶函数(high-order function 简称:HOF),咋一听起来那么高级,满足了以下两点就可以称作高阶函数了

    • 函数可以作为参数被传递
    • 函数可以作为返回值输出

    在维基中的定义是

    • 接受一个或多个函数作为输入
    • 输出一个函数

    可以将高阶函数理解为函数之上的函数,它很常用,比如常见的

    var getData = function(url, callback) {
    $.get(url, function(data){
    callback(data);
    });
    }

    或者在众多闭包的场景中都使用到

    比如 防抖函数(debounce)与节流函数(throttle)

    Debounce

    防抖,指的是无论某个动作被连续触发多少次,直到这个连续动作停止后,才会被当作一次来执行

    比如一个输入框接受用户不断输入,输入结束后才开始搜索

    以页面滚动作为例子,可以定义一个防抖函数,接受一个自定义的 delay值,作为判断停止的时间标识

    // 函数防抖,频繁操作中不处理,直到操作完成之后(再过 delay 的时间)才一次性处理
    function debounce(fn, delay) {
    delay = delay || 200; var timer = null; return function() {
    var arg = arguments; // 每次操作时,清除上次的定时器
    clearTimeout(timer);
    timer = null; // 定义新的定时器,一段时间后进行操作
    timer = setTimeout(function() {
    fn.apply(this, arg);
    }, delay);
    }
    };var count = 0;window.onscroll = debounce(function(e) {
    console.log(e.type, ++count); // scroll
    }, 500);

    滚动页面,可以看到只有在滚动结束后才执行

    JS的闭包、高阶函数、柯里化

    Throttle

    节流,指的是无论某个动作被连续触发多少次,在定义的一段时间之内,它仅能够触发一次

    比如resize和scroll时间频繁触发的操作,如果都接受了处理,可能会影响性能,需要进行节流控制

    以页面滚动作为例子,可以定义一个节流函数,接受一个自定义的 delay值,作为判断停止的时间标识

    需要注意的两点

    要设置一个初始的标识,防止一开始处理就被执行了,同时在最后一次处理之后,也需要重新置位

    也要设置定时器处理,防止两次动作未到delay值,最后一组动作触发不了

    // 函数节流,频繁操作中间隔 delay 的时间才处理一次
    function throttle(fn, delay) {
    delay = delay || 200; var timer = null;
    // 每次滚动初始的标识
    var timestamp = 0; return function() {
    var arg = arguments;
    var now = Date.now(); // 设置开始时间
    if (timestamp === 0) {
    timestamp = now;
    } clearTimeout(timer);
    timer = null; // 已经到了delay的一段时间,进行处理
    if (now - timestamp >= delay) {
    fn.apply(this, arg);
    timestamp = now;
    }
    // 添加定时器,确保最后一次的操作也能处理
    else {
    timer = setTimeout(function() {
    fn.apply(this, arg);
    // 恢复标识
    timestamp = 0;
    }, delay);
    }
    }
    };var count = 0;window.onscroll = throttle(function(e) {
    console.log(e.type, ++count); // scroll
    }, 500);

    JS的闭包、高阶函数、柯里化

    三、柯里化

    柯里化(Currying),又称为部分求值,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回一个新的函数的技术,新函数接受余下参数并返回运算结果。

    比较经典的例子是

    实现累加  add(1)(2)(3)(4)

    第一种方法即是使用回调嵌套

    function add(a) {
    // 疯狂的回调
    return function(b) {
    return function(c) {
    return function(d) {
    // return a + b + c + d;
    return [a, b, c, d].reduce(function(v1, v2) {
    return v1 + v2;
    });
    }
    }
    }
    }console.log(add(1)(2)(3)(4)); // 10

    既不优雅也不好扩展

    修改两下,让它支持不定的参数数量

    function add() {
    var args = [].slice.call(arguments); // 用以存储更新参数数组
    function adder() {
    var arg = [].slice.call(arguments); args = args.concat(arg); // 每次调用,都返回自身,取值时可以通过内部的toString取到值
    return adder;
    } // 指定 toString的值,用以隐示取值计算
    adder.toString = function() {
    return args.reduce(function(v1, v2) {
    return v1 + v2;
    });
    }; return adder;
    }console.log(add(1, 2), add(1, 2)(3), add(1)(2)(3)(4)); // 3 6 10

    上面这段代码,就能够实现了这个“柯里化”

    需要注意的两个点是

    • arguments并不是真正的数组,所以不能使用数组的原生方法(如 slice)
    • 在取值时,会进行隐示的求值,即先通过内部的toString()进行取值,再通过 valueOf()进行取值,valueOf优先级更高,我们可以进行覆盖初始的方法

    当然,并不是所有类型的toString和toValue都一样,Number、String、Date、Function 各种类型是不完全相同的,本文不展开

    上面用到了call 方法,它的作用主要是更改执行的上下文,类似的还有apply,bind 等

    我们可以试着自定义一个函数的 bind方法,比如

    var obj = {
    num: 10,
    getNum: function(num) {
    console.log(num || this.num);
    }
    };var o = {
    num: 20
    };obj.getNum(); // 10
    obj.getNum.call(o, 1000); // 1000
    obj.getNum.bind(o)(20); // 20// 自定义的 bind 绑定
    Function.prototype.binder = function(context) {
    var fn = this;
    var args = [].slice.call(arguments, 1); return function() {
    return fn.apply(context, args);
    };
    };obj.getNum.binder(o, 100)(); // 100

    上面的柯里化还不够完善,假如要定义一个乘法的函数,就得再写一遍长长的代码

    需要定义一个通用currying函数,作为包装

    // 柯里化
    function curry(fn) {
    var args = [].slice.call(arguments, 1); function inner() {
    var arg = [].slice.call(arguments); args = args.concat(arg);
    return inner;
    } inner.toString = function() {
    return fn.apply(this, args);
    }; return inner;
    }function add() {
    return [].slice.call(arguments).reduce(function(v1, v2) {
    return v1 + v2;
    });
    }function mul() {
    return [].slice.call(arguments).reduce(function(v1, v2) {
    return v1 * v2;
    });
    }var curryAdd = curry(add);
    console.log(curryAdd(1)(2)(3)(4)(5)); // 15var curryMul = curry(mul, 1);
    console.log(curryMul(2, 3)(4)(5)); // 120

    看起来就好多了,便于扩展

    不过实际上,柯里化的应用中,不定数量的参数场景比较少,更多的情况下的参数是固定的(常见的一般也就两三个)

    // 柯里化
    function curry(fn) {
    var args = [].slice.call(arguments, 1),
    // 函数fn的参数长度
    fnLen = fn.length; // 存储参数数组,直到参数足够多了,就调用
    function inner() {
    var arg = [].slice.call(arguments); args = args.concat(arg); if (args.length >= fnLen) {
    return fn.apply(this, args);
    } else {
    return inner;
    }
    } return inner;
    }function add(a, b, c, d) {
    return a + b + c + d;
    }function mul(a, b, c, d) {
    return a * b * c * d;
    }var curryAdd = curry(add);
    console.log(curryAdd(1)(2)(3)(4)); // 10var curryMul = curry(mul, 1);
    console.log(curryMul(2, 3)(4)); // 24

    上面定义的 add方法中,接受4个参数

    在我们currying函数中,接受这个add方法,并记住这个方法需要接受的参数数量,存储传入的参数,直到符合数量要求时,便进行调用处理。

    反柯里化

    反柯里化,将柯里化过后的函数反转回来,由原先的接受单个参数的几个调用转变为接受多个参数的单个调用

    一种简单的实现方法是:将多个参数一次性传给柯里化的函数,因为我们的柯里化函数本身就支持多个参数的传入处理,反柯里化调用时,仅使用“一次调用”即可。

    结合上方的柯里化代码,反柯里化代码如下

    // 反柯里化
    function uncurry(fn) {
    var args = [].slice.call(arguments, 1); return function() {
    var arg = [].slice.call(arguments); args = args.concat(arg); return fn.apply(this, args);
    }
    }var uncurryAdd = uncurry(curryAdd);
    console.log(uncurryAdd(1, 2, 3, 4)); // 10var uncurryMul = uncurry(curryMul, 2);
    console.log(uncurryMul(3, 4)); // 24
相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:9,105
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,582
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,429
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,200
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,836
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,919