• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2025-04-06 15:44 Aet 隐藏边栏 |   抢沙发  2 
文章评分 1 次,平均分 5.0

装饰器模式和转发,call/apply

概述

  1. JavaScript 在处理函数时提供了非凡的灵活性
  2. 它们可以被传递,用作对象,也可以在它们之间 转发(forward) 调用并 装饰(decorate) 它们

透明缓存

  1. 假设我们有一个 CPU 重负载的函数 slow(x),但它的结果是稳定的
    1. 换句话说,对于相同的 x,它总是返回相同的结果
    2. 如果经常调用该函数,我们可能希望将结果缓存(记住)下来,以避免在重新计算上花费额外的时间
    3. 但是我们不是将这个功能添加到 slow() 中,而是创建一个包装器(wrapper)函数,该函数增加了缓存功能

  1. 上面的代码中,cachingDecorator 是一个 装饰器(decorator):
    1. 一个特殊的函数,它接受另一个函数并改变它的行为
    2. 其思想是,我们可以为任何函数调用 cachingDecorator,它将返回缓存包装器
  2. 总而言之,使用分离的 cachingDecorator 而不是改变 slow 本身的代码有几个好处:
    1. cachingDecorator 是可重用的。我们可以将它应用于另一个函数
    2. 缓存逻辑是独立的,它没有增加 slow 本身的复杂性(如果有的话)
    3. 如果需要,我们可以组合多个装饰器(其他装饰器将遵循同样的逻辑)

使用 “func.call” 设定上下文

  1. 面提到的缓存装饰器不适用于对象方法
  2. 例如,在下面的代码中,worker.slow() 在装饰后停止工作:
    1. 错误发生在试图访问 this.someMethod 并失败了的 (*) 行中
    2. 原因是包装器将原始函数调用为 (**) 行中的 func(x)。并且,当这样调用时,函数将得到 this = undefined
    3. 因此,包装器将调用传递给原始方法,但没有上下文 this。因此,发生了错误

  1. 有一个特殊的内建函数方法 func.call(context, …args),它允许调用一个显式设置 this 的函数
    1. 它运行 func,提供的第一个参数作为 this,后面的作为参数(arguments

  1. 例如,在下面的代码中,我们在不同对象的上下文中调用 sayHi
    1. sayHi.call(user) 运行 sayHi 并提供了 this=user,然后下一行设置 this=admin

  1. 在这里我们用带有给定上下文和 phrase 的 call 调用 say

  1. 我们的例子中,我们可以在包装器中使用 call 将上下文传递给原始函数:
    1. 现在一切都正常工作了
    2. 在经过装饰之后,worker.slow 现在是包装器 function (x) { ... }
    3. 因此,当 worker.slow(2) 执行时,包装器将 2 作为参数,并且 this=worker(它是点符号 . 之前的对象)
    4. 在包装器内部,假设结果尚未缓存,func.call(this, x) 将当前的 this=worker)和当前的参数(=2)传递给原始方法

传递多个参数

  1. 现在让我们把 cachingDecorator 写得更加通用
  2. 如何缓存多参数 worker.slow 方法呢
    1. 之前,对于单个参数 x,我们可以只使用 cache.set(x, result) 来保存结果,并使用 cache.get(x) 来检索并获取结果
    2. 但是现在,我们需要记住 参数组合 (min,max) 的结果。原生的 Map 仅将单个值作为键(key)
    3. 有许多解决方案可以实现:
      1 实现一个新的(或使用第三方的)类似 map 的更通用并且允许多个键的数据结构
      2 使用嵌套 mapcache.set(min) 将是一个存储(键值)对 (max, result)Map。所以我们可以使用 cache.get(min).get(max) 来获取 result
      3 将两个值合并为一个。为了灵活性,我们可以允许为装饰器提供一个“哈希函数”,该函数知道如何将多个值合并为一个值

  1. 第三种方式
    1. 当然,我们需要传入的不仅是 x,还需要传入 func.call 的所有参数
    2. function() 中我们可以得到一个包含所有参数的伪数组(pseudo-array)arguments,那么 func.call(this, x) 应该被替换为 func.call(this, ...arguments)

  1. 现在这个包装器可以处理任意数量的参数了(尽管哈希函数还需要被进行调整以允许任意数量的参数)
    1. (*) 行中它调用 hash 来从 arguments 创建一个单独的键。这里我们使用一个简单的“连接”函数,将参数 (3, 5) 转换为键 "3,5"。更复杂的情况可能需要其他哈希函数
    2. 然后 (**) 行使用 func.call(this, ...arguments) 将包装器获得的上下文和所有参数(不仅仅是第一个参数)传递给原始函数

func.apply

  1. 可以使用 func.apply(this, arguments) 代替 func.call(this, ...arguments)
  2. 语法
    1. 它运行 func 设置 this=context,并使用类数组对象 args 作为参数列表(arguments

  1. callapply 之间唯一的语法区别是,call 期望一个参数列表,而 apply 期望一个包含这些参数的类数组对象
    1. 因此,这两个调用几乎是等效的:
    2. 它们使用给定的上下文和参数执行相同的 func 调用

  1. 只有一个关于 args 的细微的差别:
    1. Spread 语法 ... 允许将 可迭代对象 args 作为列表传递给 call
    2. apply 只接受 类数组 args
  2. 对于即可迭代又是类数组的对象,例如一个真正的数组,我们使用 callapply 均可,但是 apply 可能会更快,因为大多数 JavaScript 引擎在内部对其进行了优化

呼叫转移

  1. 将所有参数连同上下文一起传递给另一个函数被称为“呼叫转移(call forwarding)”

修改上面的hash函数

  1. 目前,它仅适用于两个参数

  1. 如果它可以适用于任何数量的 args 就更好了
    1. 不幸的是,这不行。因为我们正在调用 hash(arguments)arguments 对象既是可迭代对象又是类数组对象,但它并不是真正的数组
    2. 所以在它上面调用 join 会失败,我们可以在下面看到:

  1. 不过,有一种简单的方法可以使用数组的 join 方法:
    1. 这个技巧被称为 方法借用(method borrowing
    2. 我们从常规数组 [].join 中获取(借用)join 方法,并使用 [].join.callarguments 的上下文中运行它

装饰器和函数属性

  1. 通常,用装饰的函数替换一个函数或一个方法是安全的,除了一件小东西
    1. 如果原始函数有属性,例如 func.calledCount 或其他,则装饰后的函数将不再提供这些属性
    2. 例如,在上面的示例中,如果 slow 函数具有任何属性,而 cachingDecorator(slow) 则是一个没有这些属性的包装器
  2. 一些包装器可能会提供自己的属性
    1. 例如,装饰器会计算一个函数被调用了多少次以及花费了多少时间,并通过包装器属性公开(expose)这些信息
  3. 存在一种创建装饰器的方法,该装饰器可保留对函数属性的访问权限,但这需要使用特殊的 Proxy 对象来包装函数

函数绑定

概述

  1. 当将对象方法作为回调进行传递,例如传递给 setTimeout,这儿会存在一个常见的问题:“丢失 this

丢失 this

  1. 我们已经看到了丢失 this 的例子
    1. 一旦方法被传递到与对象分开的某个地方 —— this 就丢失
  2. 下面是使用 setTimeoutthis 是如何丢失的:
    1. 正如我们所看到的,输出没有像 this.firstName 那样显示 “John”,而显示了 undefined
    2. 这是因为 setTimeout 获取到了函数 user.sayHi,但它和对象分离开了

  1. 浏览器中的 setTimeout 方法有些特殊:它为函数调用设定了 this=window
    1. 对于 Node.jsthis 则会变为计时器(timer)对象
    2. 所以对于 this.firstName,它其实试图获取的是 window.firstName,这个变量并不存在
    3. 在其他类似的情况下,通常 this 会变为 undefined
  2. 解决这个问题的方法1:包装器
    1. 现在它可以正常工作了,因为它从外部词法环境中获取到了 user,就可以正常地调用方法了

  1. 解决这个问题的方法2:bind

bind

  1. 它可以绑定 this
  2. func.bind(context) 的结果是一个特殊的类似于函数的“外来对象(exotic object
  3. 它可以像函数一样被调用,并且透明地(transparently)将调用传递给 func 并设定 this=context
  4. 换句话说,boundFunc 调用就像绑定了 thisfunc
  5. 这里的 func.bind(user) 作为 func 的“绑定的(bound)变体”,绑定了 this=user

  1. 所有的参数(arguments)都被“原样”传递给了初始的 func,例如:
    1. (*) 行,我们取了方法 user.sayHi 并将其绑定到 user</li> <li>sayHi 是一个“绑定后(bound)”的方法,它可以被单独调用,也可以被传递给 setTimeout —— 都没关系,函数上下文都会是正确的

bindAll

  1. 如果一个对象有很多方法,并且我们都打算将它们都传递出去,那么我们可以在一个循环中完成所有方法的绑定:

部分(应用)函数(Partial functions

  1. 我们不仅可以绑定 this,还可以绑定参数(arguments
    1. 虽然很少这么做,但有时它可以派上用场
  2. 它允许将上下文绑定为 this,以及绑定函数的部分参数

  1. 例如,我们有一个乘法函数 mul(a, b)
    1. 让我们使用 bind 在该函数基础上创建一个 double 函数:
    2. mul.bind(null, 2) 的调用创建了一个新函数 double,它将调用传递到 mul,将 null 绑定为上下文,并将 2 绑定为第一个参数。并且,参数(arguments)均被“原样”传递
    3. 注意,这里我们实际上没有用到 this。但是 bind 需要它,所以我们必须传入 null 之类的东西

  1. 为什么我们通常会创建一个部分应用函数?
    1. 好处是我们可以创建一个具有可读性高的名字(doubletriple)的独立函数。我们可以使用它,并且不必每次都提供一个参数,因为参数是被绑定了的
    2. 另一方面,当我们有一个非常灵活的函数,并希望有一个不那么灵活的变型时,部分应用函数会非常有用
    3. 例如,我们有一个函数 send(from, to, text)。然后,在一个 user 对象的内部,我们可能希望对它使用 send 的部分应用函数变型:从当前 user 发送 sendTo(to, text)

在没有上下文情况下的 partial

  1. 当我们想绑定一些参数(arguments),但是不想绑定上下文 this,应该怎么办
    1. 例如,对于一个对象方法
    2. 原生的 bind 不允许这种情况。我们不可以省略上下文直接跳到参数(arguments
    3. 幸运的是,仅绑定参数(arguments)的函数 partial 比较容易实现

  1. partial(func[, arg1, arg2...]) 调用的结果是一个包装器 (*),它调用 func 并具有以下内容:
    1. 与它获得的函数具有相同的 this(对于 user.sayNow 调用来说,它是 user
    2. 然后给它 ...argsBound —— 来自于 partial 调用的参数("10:00"
    3. 然后给它 ...args —— 给包装器的参数("Hello"

深入理解箭头函数

概述

  1. 箭头函数不仅仅是编写简洁代码的“捷径”。它还具有非常特殊且有用的特性
  2. JavaScript 充满了我们需要编写在其他地方执行的小函数的情况
    1. arr.forEach(func) —— forEach 对每个数组元素都执行 func
    2. setTimeout(func) —— func 由内建调度器执行
    3. 等等
  3. JavaScript 的精髓在于创建一个函数并将其传递到某个地方
  4. 在这样的函数中,我们通常不想离开当前上下文。这就是箭头函数的主战场啦

箭头函数没有 “this

  1. 箭头函数没有 this。如果访问 this,则会从外部获取
  2. 例如,我们可以使用它在对象方法内部进行迭代:
    1. 这里 forEach 中使用了箭头函数,所以其中的 this.title 其实和外部方法 showList 的完全一样。那就是:group.title

注意

  1. 不能对箭头函数进行 new 操作
    1. 不具有 this 自然也就意味着另一个限制:箭头函数不能用作构造器(constructor)。不能用 new 调用它们

bind相比

  1. 箭头函数 => 和使用 .bind(this) 调用的常规函数之间有细微的差别:
    1. .bind(this) 创建了一个该函数的“绑定版本”
    2. 箭头函数 => 没有创建任何绑定。箭头函数只是没有 thisthis 的查找与常规变量的搜索方式完全相同:在外部词法环境中查找

箭头函数没有 “arguments

  1. 箭头函数也没有 arguments 变量
  2. 当我们需要使用当前的 thisarguments 转发一个调用时,这对装饰器(decorators)来说非常有用
  3. 例如,defer(f, ms) 获得了一个函数,并返回一个包装器,该包装器将调用延迟 ms 毫秒:

  1. 不用箭头函数的话,可以这么写:
    1. 在这里,我们必须创建额外的变量 argsctx,以便 setTimeout 内部的函数可以获取它们

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享