函数相关知识

86 阅读4分钟

箭头函数

  • 如果形参只有一个,则小括号可以省略
  • 函数体如果只有一条语句,则花括号可以省略,函数的返回值为该条语句的执行结果,所以 return也要去掉
  • 箭头函数 this 指向声明时所在作用域下 this 的值,this是静态的
  • 箭头函数不能作为构造函数实例化
  • 不能使用 arguments

箭头函数不会更改 this 指向,用来指定回调函数会非常合适。
因为其this是静态的,指向声明时所在的作用域

函数柯里化

是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

但是问题来了费这么大劲封装一层,到底有什么用处呢?

来列一列柯里化有哪些好处呢?

1.参数复用

// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true

// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false

上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。

2.提前确认

var on = function(element, event, handler) {
  if (document.addEventListener) {
    if (element && event && handler) {
        element.addEventListener(event, handler, false);
    }
  } else {
    if (element && event && handler) {
        element.attachEvent('on' + event, handler);
    }
  }
}

var on = (function() {
  if (document.addEventListener) {
    return function(element, event, handler) {
      if (element && event && handler) {
        element.addEventListener(event, handler, false);
      }
    };
  } else {
    return function(element, event, handler) {
      if (element && event && handler) {
        element.attachEvent('on' + event, handler);
      }
    };
  }
})();

//换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了
var on = function(isSupport, element, event, handler) {
  isSupport = isSupport || document.addEventListener;
  if (isSupport) {
    return element.addEventListener(event, handler, false);
  } else {
    return element.attachEvent('on' + event, handler);
  }
}

我们在做项目的过程中,封装一些dom操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。

3.延迟运行

Function.prototype.bind = function (context) {
  var _this = this
  var args = Array.prototype.slice.call(arguments, 1)

  return function() {
    return _this.apply(context, args)
  }
}

像我们js中经常使用的bind,实现的机制就是Currying.

说了这几点好处之后,发现还有个问题,难道每次使用Currying都要对底层函数去做修改,

有没有什么通用的封装方法?

function currying(fn) {
  // args 获取第一个方法内的全部参数
  var args = Array.prototype.slice.call(arguments, 1);
  return function() {
    // 将后面方法里的全部参数和args进行合并
    var newArgs = args.concat(Array.prototype.slice.call(arguments));
    // 把合并后的参数通过apply作为fn的参数并执行
    return fn.apply(this, newArgs);
  }
}

这边首先是初步封装,通过闭包把初步参数给保存下来,然后通过获取剩下的arguments进行拼接,最后执行需要currying的函数。

但是好像还有些什么缺陷,这样返回的话其实只能多扩展一个参数,currying(a)(b)(c)这样的话,貌似就不支持了(不支持多参数调用),一般这种情况都会想到使用递归再进行封装一层。

function progressCurrying(fn, args) {
  let _this = this;
  let length = fn.length;
  let args = args || [];

  return function() {
    let _args = Array.prototype.slice.call(arguments);
    Array.prototype.push.apply(args, _args);

    // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
    if (_args.length < length) {
      return progressCurrying.call(_this, fn, args);
    }

    // 参数收集完毕,则执行fn
    return fn.apply(_this, args);
  }
}

这边其实是在初步的基础上,加上了递归的调用,只要参数个数小于最初的fn.length,就会继续执行递归。

curry的一些性能问题你只要知道下面四点就差不多了:

  • 存取arguments对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在arguments.length的实现上是相当慢的
  • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上

其实在大部分应用中,主要的性能瓶颈是在操作DOM节点上,这js的性能损耗基本是可以忽略不计的,所以curry是可以直接放心的使用。

最后再扩展一道经典面试题

// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

function add() {
  // 第一次执行时,定义一个数组专门用来存储所有的参数
  let _args = Array.prototype.slice.call(arguments);

  // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
  let _adder = function() {
    _args.push(...arguments);
    return _adder;
  }
  
  // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
  _adder.toString = function() {
    return _args.reduce(function(a, b) {
      return a + b;
    })
  }

  return _adder
}

add(1)(2)(3)                // 6
add(1, 2, 3)(4)             // 10
add(1)(2)(3)(4)(5)          // 15
add(2, 6)(1)                // 9