JavaScript闭包系列之闭包进阶应用—惰性思想、函数柯里化、组合函数

359 阅读9分钟

个人笔记

对闭包的理解(以前的文章)

有这样的情况:函数的当前上下文EC(fn)中开辟的某个"堆内存"(函数或对象),被当前上下文以外的变量(或者其他事物)所占用(引用),此时当前上下文是不能被出栈释放的(不论这个堆内存中的被外部引用的这个函数或对象有没有用到当前上下文中的私有变量)。

里面的私有变量受到了私有上下文的保护,不受外界干扰.

有可能形成不被释放的上下文,里面的私有变量和一些值就会被保存起来,这些值可以供"下级"上下文读取使用

我们把函数的这种保存/保护机制称之为闭包

高级单例设计模式(早期模块化思想)

对象的另一个作用(单例设计模式)

对象的另外一个作用:把描述同一个事物的属性和方法,归纳到相同的空间中,也起到了防止全局污染的作用,这就是单例设计模式。

单例设计模式是一种思想。js中单例设计模式就是一个对象,用单独的实例来管理变量的存储

  • 每一个对象都是Object类的实例(单例)
  • 如下person1不在称之为对象名,而是叫做命名空间(起了一个名字的一块空间)
var name = "玉媛";//污染全局变量
var age = 18;

var name = "王琪";
var age = 81; 
//-------------
var person1 = {//使用命名空间避免污染全局变量
    name: '玉媛',
    age: 18
};

var person2 = {
    name: '王琪',
    age: 81
};

闭包+单利设计模式=高级单例设计模式

单利设计模式的应用:

let searchModule = (function () {
    let wd = "";

    function query() {
        // ...
    }

    function submit() {
        // ...
    }

    return {
        // submit:submit
        submit,
        query
    };
})();


let weatherModule = (function () {
    let city = "";

    function submit() {
        // ...
    }

    return {
        submit
    };
})();


let skinModule = (function () {
    let wd = "";

    function search() {
        // ...
    }

    searchModule.submit();

    return {};
})();

这就是早期的模块化方案

特点是:

  1. 基于闭包避免全局变量污染
  2. 实现各版块之间方法的相互调用:返回对象,把需要供别人调用的方法暴露到全局,window.xxx=xxx (暴露比较多的情况下,还是会产生全局污染)

基于闭包+单例设计思想就是高级单例设计模式 (早期的模块化思想)

惰性函数

惰性函数是js函数式编程的另一个应用,惰性函数表示函数执行的分支只会在函数第一次调用的时候执行

例1 获取元素样式

获取元素样式

  • 元素.style.xxx 获取行内样式
  • 盒子模型属性「外加:getBoundingClientRect(获取当前元素和当前可视窗口的交叉位信息)」
  • 获取所有经过浏览器计算过的样式(所有渲染的样式)
    • 标准:getComputedStyle
    • IE6~8:currentStyle
let getCss = function (ele, attr) {
    if (typeof getComputedStyle !== "undefined") {
       return window.getComputedStyle(ele)[attr];
    } 
    return ele.currentStyle[attr];
};

上面那样写,每次调用方法,都要进行浏览器是否兼容的判断,第二次执行没必要判断

let isCompatible = typeof getComputedStyle !== "undefined"
let getCss = function (ele, attr) {
    if (isCompatible) {
       return window.getComputedStyle(ele)[attr];
    } 
    return ele.currentStyle[attr];
};

将判断逻辑抽取,但上面每次还是需要逻辑判断

// 核心:函数重构「闭包」
let getCss = function (ele, attr) {
    if (typeof getComputedStyle !== "undefined") {
        getCss = function (ele, attr) {
            return window.getComputedStyle(ele)[attr];
        };
    } else {
        getCss = function (ele, attr) {
            return ele.currentStyle[attr];
        };
    }
    // 保证第一次也获取值
    return getCss(ele, attr);
};

根据是否兼容重构成小函数,当前函数执行(最外面的getCss)形成私有上下文,私有上下文中的声明的小函数,被全局上下文中的变量(最外面的getCss)占用,并且最外面的getCss执行的上下文是不销毁的,所以形成了闭包。

惰性函数出现的准则:出现函数重构,并且形成「闭包」,提高性能

例2 绑定事件

js惰性函数思想介绍 绑定事件:

function emit(element, type, func) {
    if (element.addEventListener) {
        element.addEventListener(type, func, false);
    } else if (element.attachEvent) {
        element.attachEvent('on' + type, func);
    } else { //如果不支持DOM2级事件
        element['on' + type] = func;
    }
}
function emit(element, type, func) {
    if (element.addEventListener) {
        emit = function (element, type, func) {
            element.addEventListener(type, func, false);
        };
    } else if (element.attachEvent) {
        emit = function (element, type, func) {
            element.attachEvent('on' + type, func);
        };
    } else {
        emit = function (element, type, func) {
            element['on' + type] = func;
        };
    }
    emit(element, type, func);
}

函数柯理化

核心:预先处理/预先存储「利用闭包的保存作用:凡是形成一个闭包,存储一些信息,供其下级上下文调取使用的,都是柯理化思想」

柯里化思想的概念理解和应用参考这里

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

核心思想:调用函数时只传递给函数一部分参数,对这部分参数进行处理(预先处理),然后让它返回一个新函数去处理剩下的别的参数。

目的:将先传进去的一部分参数进行复用.

柯里化例子

例1:实现fn

let total = fn(1, 2)(3);
console.log(total); //=>6

答案:

 const fn = (...params) => {
    // params->[1,2]
    return (...args) => {
        // args->[3]
        return params.concat(args).reduce((total, item) => {
            return total + item;
        });
    };
};

解析:

对象转化为原始值,或者字符串,会依次调用下面的方法:

Symbol.toPrimitive

Object.prototype.valueOf()

Object.prototype.toString()

function fn() {}
alert(fn); //会把fn变为String然后再输出(会调用fn.toString())
// console.log(fn); //基于console.log也会把fn转换为String处理,只不过在控制台的输出和alert输出的结果看起来不一样而已

以下重构toString方法就会输出ok

function fn() {}
//重构toString方法就会输出ok
fn.toString = function () {
    return 'ok';
}
alert(fn);

以下会输出11

function fn() {}
// toString/valueOf
fn[Symbol.toPrimitive] = function () {
    return 10;
}; 
console.log(fn+1)
fn.toString = function () {
    return 'ok';
}

例2:实现一个add方法,使计算结果能够满足如下预期:


add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

let add = curring();
let res = add(1)(2)(3);
console.log(res); //->6

add = curring();
res = add(1, 2, 3)(4);
console.log(res); //->10

add = curring();
res = add(1)(2)(3)(4)(5);
console.log(res); //->15

例1只是确定的执行两次,下面是可以执行任意次

答案1:

const curring = () => {
    let arr = [];//把每次add执行的时候传进来的params暂存到这里面
    const add = (...params) => {
        arr = arr.concat(params);
        return add;//每次都返回add
    };
    add.toString = () => {//输出add的时候相当于调用了add的toString方法
        return arr.reduce((total, item) => {
            return total + item;
        });
    };
    return add;
};
  1. 声明一个arr数组,把每次add执行的时候传进来的params暂存到这里面
  2. add每次执行完,每次都返回add本身,让他可以再次调用
  3. 最后结束的时候输出的还是add,输出add的时候相当于调用了addtoString方法,所以重写一下toString方法,把数组进行累加

例3:实现一个curring方法,使计算结果能够满足如下预期:

let add = curring(5);
res = add(1)(2)(3)(4)(5);
console.log(res); //->15 

规定curring(n),n是返回的函数执行的次数,比如n=5,那么后面add要执行5次

达到规定的次数,就返回处理后的结果,否则就返回函数,继续调用,上面题2用toString是因为不知道要执行几次,所以什么时候不执行函数了,就直接输出函数,就调用toString方法

const curring = n => {
    let arr = [],
        index = 0;
    const add = (...params) => {
        index++;
        arr = arr.concat(params);
        if (index >= n) {//达到规定的次数,就返回处理后的结果,否则就返回函数,继续调用,上面题2用toString是因为不知道要执行几次,所以什么时候不执行函数了,就直接输出函数,就调用toString方法
            return arr.reduce((total, item) => {
                return total + item;
            });
        }
        return add;
    };
    return add;
};

组合函数

组合函数也可以叫做函数内嵌调用的扁平化或函数的管道化

组合函数是函数式编程的一个重要概念. 思想:把处理数据的函数像管道一样连接起来, 然后让数据穿过管道得到最终的结果。 例如:

 const add1 = x => x + 1;
 const mul3 = x => x * 3;
 const div2 = x => x / 2;
 div2(mul3(add1(add1(0)))); //=>3

而这样的写法可读性明显太差了,我们可以构建一个compose函数,它接受任意多个函数作为参数(这些函数都只接受一个参数),然后compose返回的也是一个函数,达到以下的效果:

    const operate = compose(div2, mul3, add1, add1)
    operate(0) //=>相当于div2(mul3(add1(add1(0)))) 
    operate(2) //=>相当于div2(mul3(add1(add1(2))))

简而言之:compose可以把类似于f(g(h(x)))这种写法简化成compose(f, g, h)(x),请你完成 compose函数的编写 例1:实现函数compose

const add1 = x => x + 1;
const mul3 = x => x * 3;
const div2 = x => x / 2;
function compose() {
}
let operate = compose(div2, mul3, add1, add1);
console.log(operate(0));

答案1

const add1 = x => x + 1;
const mul3 = x => x * 3;
const div2 = x => x / 2;

function compose(...funcs) {
    // funcs -> [div2, mul3, add1, add1]
    return function operate(x) {
        let len = funcs.length;
        if (len === 0) return x;
        if (len === 1) return funcs[0](x);
        return funcs.reduceRight((result, item) => {
            return item(result);
        }, x);
    };
}
let operate = compose(div2, mul3, add1, add1);
console.log(operate(0));

compose执行,会把传进来的函数预先存起来,等到compose执行完了之后,再把返回的函数执行,才使用存起来的函数,所以这里也是闭包的保存作用

看一看redux中的compose源码

修改一下

function compose(...funcs) {
    if (funcs.length === 0) {
        return x => {
            return x;
        };
    }
    if (funcs.length === 1) {
        return funcs[0];
    }
    // funcs -> [div2, mul3, add1, add1]
    return funcs.reduce((a, b) => {
        // 第一次 每一次迭代,执行回调函数,都产生一个闭包,存储a/b,返回的小函数中后期使用的a/b就是这个闭包中的
        //   a -> div2
        //   b -> mul3
        //   return x=>a(b(x)) @1
        // 第二次
        //   a -> @1
        //   b -> add1
        //   return x=>a(b(x)) @2
        // 第三次
        //   a -> @2
        //   b -> add1
        //   return x=>a(b(x)) @3
        return x => {
            return a(b(x));
        };
    }); //=>return @3; 赋值给外面的operate
}
const operate = compose(div2, mul3, add1, add1);
console.log(operate(0)); 

这种redux的写法会在reduce时产生大量的闭包不被释放,最后operate(0)才会一层层释放,答案1只是把上一次执行产生的结果传给下一次,上一次执行完,内存就释放了。所以这这个角度来说,答案1性能会好一点

reduxfuncs的长度等于0 和1的判断放在迭代外面,不参与循环,性能要好一点

所以结合答案1和reduxcompose函数写法的新的答案:

const add1 = x => x + 1;
const mul3 = x => x * 3;
const div2 = x => x / 2;

function compose(...funcs) {
    let len = funcs.length;
    if (len === 0) return x => x;//将
    if (len === 1) return funcs[0];
    return function operate(x) {
        return funcs.reduceRight((result, item) => {
            return item(result);
        }, x);
    };
}
let operate = compose(div2, mul3, add1, add1);
console.log(operate(0));

延申:如何解决operate需要传多个参数?

const add1 = x => x + 1;
const mul3 = x => x * 3;
const div2 = x => x / 2;

function compose(...funcs) {
    let len = funcs.length;
    if (len === 0) return x => x;//将
    if (len === 1) return funcs[0];
    return function operate(...args) {
        return funcs.reduceRight((result, item) => {
            if (Array.isArray(result)) {//如果是数组,说明是多个参数
                return item(...result);
            }
            return item(result);
        }, args);
    };
}
let operate = compose(div2, mul3, add1, add1);
console.log(operate(0));

重写reduce

Array.prototype.reduce = function reduce(callback, initial) {
    let self = this, // this -> arr
        i = 0,
        len = self.length,
        item,
        result;
    if (typeof callback !== "function") throw new TypeError('callback must be an function!');
    if (typeof initial === "undefined") {
        // 初始值不设置,让初始值是数组第一项,并且从数组第二项开始遍历
        initial = self[0];
        i = 1;
    }
    result = initial;

    // 循环数组中的每一项
    for (; i < len; i++) {
        item = self[i];
        result = callback(result, item, i);
    }
    return result;
};

let arr = [10, 20, 30, 40];
console.log(arr.reduce((result, item, index) => {
    return result + item;
}));
console.log(arr.reduce((result, item) => {
    return result + item;
}, 0));