跟着underscore学习与函数有关的函数(偏函数、函数记忆、after、before、once等)

339 阅读3分钟

前言

学习的underscore.js 源码版本为1.13.1
这一节学习与函数有关的函数,涉及的函数包括bind、bindAll、partial、memoize、delay、defer、after、before、once、wrap、negate、compose、throttle、debounce、restArguments

bind

理解this绑定规则及模拟实现new操作符、call、apply、bind方法这篇文章里已经实现了bind,实现代码如下:

Function.prototype.myBind = function (context) {
  var self = this,  args = Array.prototype.slice.call(arguments, 1);
  // 作为中转
  var fNOP = function () {};
  var fBound = function () {
    // 获取的是bind返回的函数传入的参数  
    var bindArgs = Array.prototype.slice.call(arguments); 
    // 按照顺序拼接起来
    var finalArgs = args.concat(bindArgs); 
    // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
    return self.apply(this instanceof fNOP ? this : context, finalArgs);
  };
  // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
  fNOP.prototype = this.prototype;
  fBound.prototype = new fNOP();
  return fBound;
};

看下underscore中bind是如何实现的:

var bind = restArguments(function(func, context, args) {
  // 如果传入的参数 func 不是方法,则抛出错误
  if (!isFunction$1(func)) throw new TypeError('Bind must be called on a function');
  var bound = restArguments(function(callArgs) {
    return executeBound(func, bound, context, this, args.concat(callArgs));
  });
  return bound;
});

其中restArguments函数在之前内容中讲过。下面看下executeBound函数

executeBound


function executeBound(sourceFunc, boundFunc, context, callingContext, args) {
  if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
  // self 为 sourceFunc 的实例,继承了它的原型链
  // self 理论上是一个空对象,但是有原型链
  var self = baseCreate(sourceFunc.prototype);
  // 用 new 生成一个构造函数的实例
  // 正常情况下是没有返回值的,即 result 值为 undefined
  // 如果构造函数有返回值
  // 如果返回值是对象(非 null),则 new 的结果返回这个对象
  // 否则返回实例
  var result = sourceFunc.apply(self, args);
  // 如果构造函数返回了对象
  // 则 new 的结果是这个对象
  // 返回这个对象
  if (isObject(result)) return result;
  return self;
}

baseCreate

var nativeCreate = Object.create;

function ctor() {
  return function(){};
}

function baseCreate(prototype) {
  // 如果 prototype 参数不是对象
  if (!isObject(prototype)) return {};
  // 如果浏览器支持 ES5 Object.create
  if (nativeCreate) return nativeCreate(prototype);

  // 以下相当于Object.create()
  var Ctor = ctor();
  Ctor.prototype = prototype;
  var result = new Ctor;
  Ctor.prototype = null;

  return result;
}

bindAll

_.bindAll(object, *methodNames)

把methodNames参数指定的一些方法绑定到object上,这些方法就会在对象的上下文环境中执行。绑定函数用作事件处理函数时非常便利,否则函数被调用时this一点用也没有。methodNames参数是必须的。
使用:

var curly = {name: 'curly'};
var moe = {
  name: 'moe',
  getName: function() { return 'name: ' + this.name; },
  sayHi: function() { return 'hi: ' + this.name; }
};
curly.getName = moe.getName;
_.bindAll(moe, 'getName', 'sayHi');
curly.sayHi = moe.sayHi;

curly.getName();  // 'name: curly'
curly.sayHi();    // 'hi: moe'

源码如下:

var bindAll = restArguments(function(obj, keys) {
  keys = flatten$1(keys, false, false);
  var index = keys.length;
  // 如果只传入了一个参数(obj),没有传入 methodNames,则报错
  if (index < 1) throw new Error('bindAll must be passed function names');
  while (index--) {
    var key = keys[index];
    // 逐个绑定
    obj[key] = bind(obj[key], obj);
  }
  return obj;
});

偏函数/局部应用函数

维基百科上对partial application解释:

partial application可以被描述为一个函数,它接受一定数目的参数,绑定值到一个或多个这些参数,并返回一个新的函数,这个返回函数只接受剩余未绑定值的参数。 例如:

function add (a, b) {
  return a + b;
}
// 执行 add 函数,一次传入两个参数即可
add(1, 2) // 3

// 假设有一个 partial 函数可以做到局部应用
var addOne = partial(add, 1);
addOne(2);  // 3

underscore中partail实现

/**
 * 局部应用一个函数填充在任意个数的 arguments,不改变其动态this值
 */
var partial = restArguments(function(func, boundArgs) {
  var placeholder = partial.placeholder;
  var bound = function() {
    var position = 0, length = boundArgs.length;
    var args = Array(length);
    for (var i = 0; i < length; i++) {
      // 如果该位置的参数为 _,则用 bound 方法的参数填充这个位置
      args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i];
    }
    while (position < arguments.length) args.push(arguments[position++]);
    return executeBound(func, bound, this, this, args);
  };
  return bound;
});

partial.placeholder = _$1;

实际用法

var toString = Object.prototype.toString;

var isString = function (obj) {
  return toString.call(obj) == '[object String]';
};
var isFunction = function (obj) {
  return toString.call(obj) == '[object Function]'; 
};

在JavaScript中进行类型判定时,我们通常会进行类似以上代码定义,如果有更多的isXXX(),就会重复去定义一些相似的函数,为了解决重复定义的问题,我们引入一个新函数,这个新函数可以如工厂一样批量创建一些类似的函数。underscore中有许多偏函数实例,如通过tagTester()函数预先指定type的值,然后返回一个新的函数:

function tagTester(name) {
  var tag = '[object ' + name + ']';
  return function(obj) {
    return toString.call(obj) === tag;
  };
}

var isString = tagTester('String');

var isNumber = tagTester('Number');

var isDate = tagTester('Date');

var isRegExp = tagTester('RegExp');

var isError = tagTester('Error');

var isSymbol = tagTester('Symbol');

var isArrayBuffer = tagTester('ArrayBuffer');

var isFunction = tagTester('Function');

可以看出,引入tagTester()函数,创建isString、isNumber等函数就很简单了,这种通过指定部分参数来产生一个新的定制函数的形式就是偏函数

函数记忆

函数记忆是指将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据。

underscore中memoize实现

_.memoize(function, [hashFunction])
Memoizes方法可以缓存某函数的计算结果。对于耗时较长的计算是很有帮助的。 如果传递了 hashFunction 参数,就用 hashFunction 的返回值作为key存储函数的计算结果。hashFunction 默认使用function的第一个参数作为key。memoized值的缓存可作为返回函数的cache属性。
实现如下:

var hasOwnProperty = Object.prototype.hasOwnProperty;
function has$1(obj, key) {
  return obj != null && hasOwnProperty.call(obj, key);
}
/**
 * 缓存某函数的计算结果
 * @param {*} func 
 * @param {?function} hasher 返回值作为key存储函数的计算结果
 */
function memoize(func, hasher) {
  var memoize = function(key) {
    var cache = memoize.cache;
    // 如果传入了 hasher,则用 hasher 函数来计算 key
    // 否则用 参数 key(即 memoize 方法传入的第一个参数)当 key
    var address = '' + (hasher ? hasher.apply(this, arguments) : key);
    if (!has$1(cache, address)) cache[address] = func.apply(this, arguments);
    return cache[address];
  };
  // cache 对象被当做 key-value 键值对缓存中间运算结果
  memoize.cache = {};
  return memoize;
}

函数记忆本质上是牺牲算法的空间复杂度以换取更优的时间复杂度,在客户端 JavaScript 中代码的执行时间复杂度往往成为瓶颈,因此在大多数场景下,这种牺牲空间换取时间的做法以提升程序执行效率的做法是非常可取的。
以斐波那契数列为例:

var times = 0;
var fibonacci = function(n) {
  times++;
  return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
};
fibonacci(10);
console.log(times);  // 55


// 使用函数记忆
fibonacci = memoize(fibonacci);
fibonacci(10);
console.log(times);  // 11

从这个例子中可以看出,如果需要大量重复的计算,或者大量计算又依赖于之前的结果,就可以考虑使用函数记忆。

delay

_.delay(function, wait, *arguments)
类似setTimeout,等待wait毫秒后调用function。如果传递可选的参数arguments,当函数function执行时, arguments 会作为参数传入。

/**
 * 类似setTimeout,等待wait毫秒后调用function。
 */
var delay = restArguments(function(func, wait, args) {
  return setTimeout(function() {
    return func.apply(null, args);
  }, wait);
});


// 使用
var log = _.bind(console.log, console);
_.delay(log, 1000, 'logged later');
// 1秒后输出 'logged later'

defer

_.defer(function, *arguments)
延迟调用function直到当前调用栈清空为止,类似使用延时为0的setTimeout方法。对于执行开销大的计算和无阻塞UI线程的HTML渲染时候非常有用。
如果传递arguments参数,当函数function执行时, arguments 会作为参数传入。

/**
 * 延迟调用function直到当前调用栈清空为止,类似使用延时为0的setTimeout方法
 */
var defer = partial(delay, _$1, 1);

// 使用
_.defer(function(){ console.log('deferred'); });

after

_.after(count, function)
偏函数应用。
创建一个函数, 只有在运行了 count 次之后才有效果. 在处理同组异步请求返回结果时, 如果你要确保同组里所有异步请求完成之后才 执行这个函数, 这将非常有用。

/**
 * 创建一个函数, 只有在运行了 count 次之后才有效果. 
 * 在处理同组异步请求返回结果时, 如果你要确保同组里所有异步请求完成之后才 执行这个函数,这将非常有用
 * @param {*} times 
 * @param {*} func 
 */
function after(times, func) {
  return function() {
    if (--times < 1) {
      return func.apply(this, arguments);
    }
  };
}

before

_.before(count, function)
偏函数应用
创建一个函数,调用不超过count 次。 当count已经达到时,最后一个函数调用的结果将被记住并返回。

/**
 * 创建一个函数,调用不超过count 次。 
 * 当count已经达到时,最后一个函数调用的结果将被记住并返回
 * @param {*} times 
 * @param {*} func 
 */
function before(times, func) {
  var memo;
  return function() {
    if (--times > 0) {
      memo = func.apply(this, arguments);
    }
    if (times <= 1) func = null;
    return memo;
  };
}

once

_.once(function)
创建一个只能调用一次的函数。重复调用改进的方法也没有效果,只会返回第一次执行时的结果。 作为初始化函数使用时非常有用, 不用再设一个boolean值来检查是否已经初始化完成.

/**
 * 创建一个只能调用一次的函数
 */
var once = partial(before, 2);

var num = 0;
var increment = _.once(function(){ return ++num; });
increment();
increment();
console.log(num);    // 1

wrap

_.wrap(function, wrapper)
将第一个函数 function 封装到函数 wrapper 里面, 并把函数 function 作为第一个参数传给 wrapper. 这样可以让 wrapper 在 function 运行之前和之后 执行代码, 调整参数然后附有条件地执行。

/**
 * 将第一个函数 function 封装到函数 wrapper 里面, 并把函数 function 作为第一个参数传给 wrapper. 
 * 这样可以让 wrapper 在 function 运行之前和之后 执行代码, 调整参数然后附有条件地执行
 * @param {*} func 
 * @param {*} wrapper 
 */
function wrap(func, wrapper) {
  return partial(wrapper, func);
}

// 使用
var hello = function(name) { return "hello: " + name; };
hello = _.wrap(hello, function(func) {
  return "before, " + func("xman") + ", after";
});
hello();    // 'before, hello: xman, after'

negate

_.negate(predicate)
返回一个新的predicate函数的否定版本

function negate(predicate) {
  return function() {
    return !predicate.apply(this, arguments);
  };
}


// 使用
var isFalsy = _.negate(Boolean);
_.find([-2, -1, 0, 1, 2], isFalsy);   // 0 

compose

_.compose(*functions)
返回函数集 functions 组合后的复合函数, 也就是一个函数执行完之后把返回的结果再作为参数赋给下一个函数来执行. 以此类推. 在数学里, 把函数 f(), g(), 和 h() 组合起来可以得到复合函数 f(g(h()))。

function compose() {
  var args = arguments;
  var start = args.length - 1;
  return function() {
    var i = start;
    var result = args[start].apply(this, arguments);
    while (i--) result = args[i].call(this, result);
    return result;
  };
}

// 使用
var greet = function(name){ return "hi: " + name; };
var exclaim = function(statement){ return statement.toUpperCase() + "!"; };
var welcome = _.compose(greet, exclaim);
welcome('xman');  // 'hi: XMAN!'

更多关于函数组合内容请移步理解函数组合(compose)及中间件实现

throttle、debounce

请移步学习underscore源码之函数去抖、节流

restArguments

请移步学习underscore源码之处理剩余参数函数restArguments

参考资料:
underscorejs
github.com/mqyqingfeng… github.com/mqyqingfeng…
github.com/lessfish/un…