【JavaScript】几个必须要会的高级编程技巧

4,057 阅读7分钟

写在前面

作为一名有追求的前端攻城狮,掌握几个高级编程技巧是必须的~学会这些技巧,会让我们的开发工作事半功倍,让我们在工作中更加游刃有余,本篇文章将介绍三个小技巧:

  • 惰性载入函数的应用

  • 函数柯里化的应用

  • compose调用函数扁平化

惰性载入函数

在我们的代码中,一定包含了大量的if语句,这些if语句的执行要花费一些时间。有一种情况是这样的,我们第一次进入到if分支中,执行了这部分代码,然后第二次同样执行进入了同一分支,所以会再次执行此部分代码。这样的情况下,代码执行肯定会慢一些,那么如果我们只让代码有一个if分支,代码就会执行的快一些。如果让代码执行的更快一些,这就是惰性载入函数的应用,接下来,看一个DOM事件绑定的例子:

function emit(element, type, func) {
    if(element.addEventListener) {
        element.addEventListener(type, func, false)
    } else if(element.attachEvent) {  // IE6、7、8
        element.attachEvent("on" + type, func)
    } else {
        element["on" + type] = func;
    }
}

上面的例子中,判断浏览器是否支持每个方法,进入到不同分支,从而用不同的方式绑定事件。如果多次绑定就会多次进入同一if分支执行代码,其实在同一浏览器环境下,多次绑定,只需要判断一次就可以完成目的。所以,这时需要应用惰性载入函数

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);
}

优化后的代码中,第一次执行进入到一个分支执行此部分代码后,函数emit会被重新赋值,这样就保证了多次调用时只有一次if执行,代码执行的就变得快了一些。

函数柯里化的应用

对于函数柯里化,最通俗的理解是:一个大函数返回一个小函数在《JavaScript高级程序设计》中,这样解释函数柯里化:与函数绑定紧密相关的主题时函数柯里化,它用于创建已经设置好了一个或多个参数的函数。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。 我们先来看一个例子:

function add(num1, num2) {
    return num1 + num2;
}

function curried(num2) {
    return add(5, num2);
}

console.log(curried(2));    // 7

这段代码有两个函数,add函数返回了两个参数的和,curried函数返回了调用add函数后5和接收的参数的和。这个curried函数就是咱们所说的一个大函数返回了一个小函数,但是它并不是一个柯里化函数。

柯里化函数通常动态创建:调用另一个函数并为它传入要柯里化的函数和必要参数。 下面,写一个柯里化函数的通用方式

function curry(fn) {
    var args = Array.prototype.slice.call(arguments, 1);  // 获取第一个参数之后的参数
    return function() {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return fn.apply(null, finalArgs);                // 执行传入函数fn
    }
}

function add(num1, num2) {
    return num1 + num2;
}

console.log(curry(add, 5, 12)());         // 17
console.log(curry(add, 6, 7)());          // 13

柯里化函数也可应用于构造出bind()函数中

/**
 * @params
 *      fn: 要执行的函数
 *      context: 需要改变的this指向
 */
function bind(fn, context) {
    context = context || window;                          // 如果没传context参数,就让其为window
    var args = Array.prototype.slice.call(arguments, 2);  // 获取第二个参数之后的参数
    return function() {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return fn.apply(context, finalArgs);
    }
}

来看一下效果:

var obj = {
    x: 1
}
function fn() {
    return this.x + 2;
}

btn.onclick = function() {
    console.log(bind(fn, obj)())      // 3
}

bind函数成功将this指向修改为了obj。但是我们为了调用时和Function原型上的bind一样,我们将自己写的bind写在原型里

(function(proto) {
    function bind(context) {
        context = context || window;                          // 如果没传context参数,就让其为window
        var args = Array.prototype.slice.call(arguments, 1);  // 获取第二个参数之后的参数
        return function() {
            var innerArgs = Array.prototype.slice.call(arguments);
            var finalArgs = args.concat(innerArgs);
            return fn.apply(context, finalArgs);
        }
    }
    proto.bind = bind;
})(Function.prototype)

再来看一下效果

var obj = {
    x: 1
}
function fn(y) {
    return this.x + y;
}

btn.onclick = function() {
    console.log(fn.bind(obj, 3)())     // 4
}

以上,就是应用函数柯里化手写实现的bind。另外,有一些源码也运用了函数柯里化,比如redux等。

下面,我们来看一道面试题:实现add函数

add(1);       //1
add(1)(2);    //3
add(1)(2)(3); //6
add(1)(2,3);  //6
add(1,2)(3);  //6
add(1,2,3);   //6
function currying(anonymous, length) {
    return function add(...args) {         // 返回add,接收参数...args
        // 如果接收的参数长度大于函数参数的个数,则直接执行接收函数
        if (args.length >= length) {      
            return anonymous(...args);
        }
        // 否则,递归调用currying,返回新的anonymous函数和新的length
        return currying(anonymous.bind(null, ...args), length - args.length);
    }
}

// count参数为传进参数的总个数,每次调用此函数时都需要修改
let add = currying(function anonymous(...args) {
	return args.reduce((x, y) => x + y);
}, count);

我们来看一下效果:

console.log(add(1));           // 修改count参数为1, 输出结果为1
console.log(add(1)(2));        // 修改count参数为2, 输出结果为3
console.log(add(1)(2)(3));     // 修改count参数为3, 输出结果为6
console.log(add(1)(2,3));      // 修改count参数为3, 输出结果为6
console.log(add(1,2)(3));      // 修改count参数为3, 输出结果为6
console.log(add(1,2,3));       // 修改count参数为3, 输出结果为6
console.log(add(5, 6, 7, 8))   // 修改count参数为4, 输出结果为26

综上,我们完成了add函数的编写,一道题的解法不一定只有一种,这道题也是一样,还有很多解法,欢迎大家在评论区讨论~

compose调用函数扁平化

在之前,我们听说过数组扁平化,数组扁平化就是将多层次的数组变为一层,那么同样的,调用函数扁平化就是将深层次的调用函数变为一层,我们来看一个例子:

var fn1 = function(x) {
    return x + 5;
}

var fn2 = function(x) {
    return x + 6;
}

var fn3 = function(x) {
    return x + 7;
}

console.log(fn3(fn2(fn1(5))));     // 23    

上面的例子中,将函数fn1的返回结果传给fn2,再将fn2的返回结果传给fn3,最终输出fn3的结果。这样层层嵌套的调用,看起来不是那么舒服,用起来也没有那么方便,所以我们想要实现这样的compose函数:

compose(fn1, fn2, fn3)(5)   // 等价于fn3(fn2(fn1(5)))

下面开始实现:

function compose() {
    var funcs = Array.prototype.slice.call(arguments);
    return function() {
        var args = Array.prototype.slice.call(arguments);
        var len = funcs.length;
        if(len === 0){
            return args[0];
        } 
        if(len === 1) {
            return funcs[0](...args)
        }
        return funcs.reduce(function(x, y) {
            return typeof x === "function" ? y(x(...args)) : y(x)
        })
    }
    
}

来看一下效果:

console.log(compose(fn1, fn2, fn3)(5));   // 23
console.log(compose(fn1)(5));             // 10
console.log(compose()(5));                // 5

输出的结果与我们预期的结果是一致的,说明我们封装的compose函数是正确的。但是compose函数封装的方式并不是只有这一种,我们来看一下redux中的compose函数

export default function compose(...funcs) {
    if (funcs.length === 0) {     // 当传入的函数个数为0时,直接返回参数
        return arg => arg
    }
    
    if (funcs.length === 1) {     // 当传入函数个数为1时,直接执行函数
        return funcs[0]
    }
    // 当传入函数个数不为0和1时,按函数传入从后向前的顺序依次执行函数
    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

这两种方式都可以实现compose函数,第一种方式看起来很好理解~第二种方式看起来更简单一些,这也需要我们认真的思考一下它的执行逻辑,增强自己的思维能力,让自己也可以写出更好的代码~

最后

高级编程技巧是开发中的必备,熟练掌握更是好处多多~觉得文章对你有帮助,可以给本篇文章点个赞呀~如果文章有不正确的地方,还希望大家指出~我们共同学习,共同进步~

最后,分享一下我的公众号「web前端日记」~大家可以关注一波~