• 欢迎访问天天编码网站,Java技术、技术书单、开发工具,欢迎加入天天编码
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏天天编码吧
  • 我们的淘宝店铺已经开张了哦,传送门:https://shop145764801.taobao.com/

6.11 高级特性

JS 教程 tiantian 287次浏览 0个评论 扫描二维码

截至目前,我们只讨论了绑定this的部分。现在,让我们来更加深入讨论绑定。

我们不仅可以绑定this,也可以绑定参数。这个情况很少使用,但在某些情况下非常实用。

绑定bind的完整语法为:

let bound = func.bind(context, arg1, arg2, ...);

它允许绑定上下文为this,而且可以绑定函数的参数。

举例,我们有一个多参数函数mul(a, b):

function mul(a, b) {
  return a * b;
}

让我们来使用bind来基于它创建函数double

let double = mul.bind(null, 2);

alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10

那个调用mul.bind(null, 2)创建了一个新函数double,并且传递调用给mul,将null赋值给上下文,并且2作为其第一个参数。其余的参数就原样传递。

这个就被称作为partial function application——通过固定已存在函数的某些参数来创建一个新函数。

请注意,实际上我们此处并没有使用this。但是bind需要此参数,所以我们可以使用null来代替一个可用值。

下列函数triple的作用是三倍参数值:

let triple = mul.bind(null, 3);

alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( triple(5) ); // = mul(3, 5) = 15

为什么我们经常会 partial 函数。

此处,我们创建了一个函数名称更加清晰的独立函数(double, triple)。我们可以直接使用该函数,而省略了原函数的第一个固定参数,因为它bind了固定值。

其他情况下,partial 函数在我们具有一个非常通用函数的情况下爱非常实用,而且希望获得一个该函数的特定版本。

举例,我们拥有一个send(from, to, text)函数。然后,在user对象的内部,我们也许希望使用一个 partial 变种:sendTo(to, text),其from固定为当前用户。

不带 context 的 partial

如果我们只希望固定某些参数,而不希望绑定this

那个原生的bind并不支持这样的场景。我们无法直接忽略context,直接跳到参数的绑定。

幸运地是,一个只绑定参数的partial函数可以轻易地实现。

function partial(func, ...argsBound) {
  return function(...args) { // (*)
    return func.call(this, ...argsBound, ...args);
  }
}

// Usage:
let user = {
  firstName: "John",
  say(time, phrase) {
    alert(`[${time}] ${this.firstName}: ${phrase}!`);
  }
};

// add a partial method that says something now by fixing the first argument
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());

user.sayNow("Hello");
// Something like:
// [10:00] John: Hello!

那个partial(func[, arg1, arg2...])调用是一个包装器(*),它会使用如下信息调用func

  • 它所获得的this(对于user.sayNow调用,它是user
  • 然后传递参数...argsBound——参数来自于partial调用(“10:00”)
  • 然后传递参数...args——参数来自于包装器(“Hello”)

所以,利用 Rest 参数很容易就完成了目的,对吧?

当然,在 lodash 库中已经存在一个 _.partial 实现。

Currying

有时,开发者会将上述讲述的 partial 函数与另一个名为currying的概念相混淆。那是另一个有趣的,关于函数的技术,我们将马上学习该技术。

Currying 就是将一个如f(a, b, c)这样的可调用函数转换为另一个可调用函数f(a)(b)(c)

现在,让我们来创建curry函数,执行两个参数的 currying 转换。换句话说,它转换f(a, b)f(a)(b)

function curry(func) {
  return function(a) {
    return function(b) {
      return func(a, b);
    };
  };
}

// usage
function sum(a, b) {
  return a + b;
}

let carriedSum = curry(sum);

alert( carriedSum(1)(2) ); // 3

正如你所见,那个实现就是一系列的包装器。

  • 那个curry(func)的结果是一个包装器function(a)
  • 当它被像sum(1)如此调用时,那个参数被保存在词法环境中,然后一个新的包装器被function(b)返回。
  • 然后,那个sum(1)(2)最终调用function(b)并提供参数2,然后它传递调用给原始的多参数函数sum

关于 currying 的更高级实现,比如来自 lodash 库的_.curry 可以完成一些更加复杂的功能。它们返回的包装器可以允许函数在所有参数都提供的情况下进行正常调用,或者返回一个 partial 函数。

function curry(f) {
  return function(...args) {
    // if args.length == f.length (as many arguments as f has),
    //   then pass the call to f
    // otherwise return a partial function that fixes args as first arguments
  };
}

Currying?为什么?

高级的 currying 允许函数可以被正常地调用,也可以轻易地获得 partial 函数。为了理解 currying 的好处,我们必须来考察一个实际编程中的实例。

举例,我们具有一个日志函数log(date, importance, message),它可以格式化并输出信息。在实际的项目中,这样的函数还具有许多其他的实用特性,比如在网络上进行传输或者过滤:

function log(date, importance, message) {
  alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}

现在,我们来 curry 它!

log = _.curry(log);

在那个log被 curry 之后,它仍然可以正常工作:

log(new Date(), "DEBUG", "some debug");

但是,它也可以以 curry 之后的方式来工作:

log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)

现在,我们可以获取一个打印今日日志的便利函数:

// todayLog will be the partial of log with fixed first argument
let todayLog = log(new Date());

// use it
todayLog("INFO", "message"); // [HH:mm] INFO message

同样,我们可以获得一个打印今日调试信息的便利函数:

let todayDebug = todayLog("DEBUG");

todayDebug("message"); // [HH:mm] DEBUG message

所以:

  1. 在 currying 之后,我们的函数没有损失任何功能和特性,仍然可以正常调用。
  2. 在 currying 之后,我们可以产生出 partial 函数,可以便利使用。

高级 curry 实现

如果你感兴趣,下面是一个可用于上述示例代码的高级 curry 实现代码。

function curry(func) {

  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  };

}

function sum(a, b, c) {
  return a + b + c;
}

let curriedSum = curry(sum);

// still callable normally
alert( curriedSum(1, 2, 3) ); // 6

// get the partial with curried(1) and call it with 2 other arguments
alert( curriedSum(1)(2,3) ); // 6

// full curried form
alert( curriedSum(1)(2)(3) ); // 6

这个新的curry可以初看起来非常复杂,但是它非常容易理解。

那个curry(func)的结果是一个包装器curried,其看一来如下所示:

function curried(...args) {
  if (args.length >= func.length) { // (1)
    return func.apply(this, args);
  } else {
    return function pass(...args2) { // (2)
      return curried.apply(this, args.concat(args2));
    }
  }
};

当我们运行该函数,存在两个分支:

  1. 立即调用:如果传递的args数量与原始函数的参数数量一致或者更多,那么就直接调用原函数。
  2. 获取partial:func并不调用。同时,另一个新的包装器pass被返回,它会将已提供的所有参数连接起来并且再次应用 curried函数。所以,在一个新调用上,我们将获取到一个新的 partial,或者,在最终一定获取到原始调用。

举例,我们来看看sum(a, b, c)调用的内部情况。三个参数,所以sum.length = 3

对于调用curried(1)(2)(3)而言:

  1. 第一个调用curried(1)会在其词法环境中记录1,并返回一个包装器对象pass
  2. 那个包装器对象pass被使用(2)进行调用:它获取前一次调用的参数(1),将它与本次调用参数(2)联合起来,并调用curried(1, 2)。此时,参数个数仍然小于3,所以curry返回新的pass
  3. 那个新的pass被使用(3)进行调用:它获取前一次调用的参数(1, 2),将它与本地调用参数(3)联合起来,并调用curried(1, 2, 3)——现在,总共存在3个参数,它就会直接调用原始函数。

如果上述的讲解还不是很清晰的话,你可以在大脑中或者纸上追踪这个调用过程。

只适用于固定长度的函数

上述的那个 currying 函数要求函数必须具有一个已知的参数个数。

currying 扩展

根据定义,currying 应该将sum(a, b, c)转变为sum(a)(b)(c)

但是,JavaScript 的很多currying 实现都不止于此,正如所述:它们同样保持了函数在多参数情况下的可调用性。

总结

  • 当我们需要固定某些已有函数的参数时,那个结果函数(不那么通用)就被称为是一个 partial。我们可以使用bind来获取一个 partial,但是也存在其他的方法来实现该功能。

当我们不想一次次地重复代码时,使用 Partial 非常实用。比如,我们具有一个send(from, to)函数,然后对于我们的任务而言,那个from总是相同值,那么我们就应该获取一个 partial 并使用它。

  • Curring 是一种转换,它使得f(a,b,c)调用可以转换为f(a)(b)(c)。JavaScript 实现通常会保持函数原有调用方式,同时在参数不够的情况下返回 partial。

在我们期待轻松的 partial 时,Currying 非常实用。正如我们在日志函数示例:那个通用函数log(date, importance, message)在 currying 之后,当只使用一个参数时,比如 log(date),返回了一个 partial,或者两个参数时,log(date, importance),返回另一个 partial。

任务


Partial application for login

这个任务是Ask losing this 的一个更加复杂的变种。

那个user对象被修改。现在,不是两个函数loginOk/loginFail,它仅有一个函数user.login(true/false)

下列代码中传递给askPassword的参数是什么?所以,它在ok时调用user.login(true),在fail时调用user.login(false)

function askPassword(ok, fail) {
  let password = prompt("Password?", '');
  if (password == "rockstar") ok();
  else fail();
}

let user = {
  name: 'John',

  login(result) {
    alert( this.name + (result ? ' logged in' : ' failed to log in') );
  }
};

askPassword(?, ?); // ?

P.S. 你应该只修改最后一行代码。


天天编码 , 版权所有丨本文标题:6.11 高级特性
转载请保留页面地址:http://www.tiantianbianma.com/advanced-features.html/
喜欢 (1)
支付宝[多谢打赏]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址