默认参数值未定义;这是JavaScript错误吗? [英] Default parameter value undefined; is this a JavaScript Bug?

查看:62
本文介绍了默认参数值未定义;这是JavaScript错误吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

下面是语法上有效的javascript程序–仅,它的行为与我们期望的不太一样.问题的标题应该可以帮助您放大问题区域

Below is a syntactically valid javascript program – only, it doesn't behave quite the way we're expecting. The title of the question should help your eyes zoom to The Problem Area

const recur = (...args) =>
  ({ type: recur, args })

const loop = f =>
  {
    let acc = f ()
    while (acc.type === recur)
      acc = f (...acc.args)
    return acc
  }

const repeat = n => f => x =>
  loop ((n = n, f = f, x = x) => // The Problem Area
    n === 0
      ? x
      : recur (n - 1, f, f (x)))

console.time ('loop/recur')
console.log (repeat (1e6) (x => x + 1) (0))
console.timeEnd ('loop/recur')
// Error: Uncaught ReferenceError: n is not defined

如果我改用 unique 标识符,则该程序可以完美运行

If instead I use unique identifiers, the program works perfectly

const recur = (...args) =>
  ({ type: recur, args })

const loop = f =>
  {
    let acc = f ()
    while (acc.type === recur)
      acc = f (...acc.args)
    return acc
  }

const repeat = $n => $f => $x =>
  loop ((n = $n, f = $f, x = $x) =>
    n === 0
      ? x
      : recur (n - 1, f, f (x)))

console.time ('loop/recur')
console.log (repeat (1e6) (x => x + 1) (0)) // 1000000
console.timeEnd ('loop/recur')              // 24 ms

仅这没有道理.让我们讨论一下现在不使用 $ 前缀的原始代码.

Only this doesn't make sense. Let's talk about the original code that doesn't use the $-prefixes now.

当评估 loop 的lambda时,由 repeat 接收的 n 在lambda的环境中可用.将内部 n 设置为外部 n 的值应有效

When the lambda for loop is being evaluated, n as received by repeat, is available in the lambda's environment. Setting the inner n to the outer n's value should effectively shadow the outer n. But instead, JavaScript sees this as some kind of problem and the inner n results in an assignment of undefined.

对我来说,这似乎是个错误,但是我很讨厌阅读规范,所以不确定.

This seems like a bug to me but I suck at reading the spec so I'm unsure.

这是一个错误吗?

推荐答案

我想您已经弄清楚了为什么您的代码不起作用.默认参数的行为类似于递归let绑定.因此,当您编写 n = n 时,您正在为其自身分配新声明的(但尚未 undefined )变量 n .我个人认为这很合理.

I guess you already figured out why your code doesn't work. Default arguments behave like recursive let bindings. Hence, when you write n = n you're assigning the newly declared (but yet undefined) variable n to itself. Personally, I think this makes perfect sense.

因此,您在评论中提到了Racket,并评论了Racket如何允许程序员在 let letrec 之间进行选择.我想将这些绑定与 Chomsky层次结构进行比较. let 绑定类似于常规语言.它不是很强大,但是允许可变阴影. letrec 绑定类似于递归可枚举的语言.它可以做所有事情,但不允许可变阴影.

So, you mentioned Racket in your comments and remarked on how Racket allows programmers to choose between let and letrec. I like to compare these bindings to the Chomsky hierarchy. The let binding is akin to regular languages. It isn't very powerful but allows variable shadowing. The letrec binding is akin to recursively enumerable languages. It can do everything but doesn't allow variable shadowing.

由于 letrec 可以完成 let 可以完成的所有工作,因此您根本不需要 let .Haskell就是一个很好的例子,它只有递归的let绑定(不幸的是 let 而不是 letrec ).现在的问题是,像Haskell这样的语言是否也应该具有 let 绑定.为了回答这个问题,让我们看下面的例子:

Since letrec can do everything that let can do, you don't really need let at all. A prime example of this is Haskell which only has recursive let bindings (unfortunately called let instead of letrec). The question now arises as to whether languages like Haskell should also have let bindings. To answer this question, let's look at the following example:

-- Inserts value into slot1 or slot2
insert :: (Bool, Bool, Bool) -> (Bool, Bool, Bool)
insert (slot1, slot2, value) =
    let (slot1', value')  = (slot1 || value,  slot1 && value)
        (slot2', value'') = (slot2 || value', slot2 && value')
    in  (slot1', slot2', value'')

如果Haskell中的 let 不是递归的,那么我们可以将此代码编写为:

If let in Haskell wasn't recursive then we could write this code as:

-- Inserts value into slot1 or slot2
insert :: (Bool, Bool, Bool) -> (Bool, Bool, Bool)
insert (slot1, slot2, value) =
    let (slot1, value) = (slot1 || value, slot1 && value)
        (slot2, value) = (slot2 || value, slot2 && value)
    in  (slot1, slot2, value)

那么Haskell为什么不具有非递归的let绑定?好吧,使用不同的名称绝对有一些好处.作为一名编译器作者,我注意到这种编程风格类似于单个静态分配表单其中每个变量名仅使用一次.通过仅使用一次变量名,该程序将使编译器更易于分析.

So why doesn't Haskell have non-recursive let bindings? Well, there's definitely some merit to using distinct names. As a compiler writer, I notice that this style of programming is similar to the single static assignment form in which every variable name is used exactly once. By using a variable name only once, the program becomes easier for a compiler to analyze.

我认为这也适用于人类.使用不同的名称可以帮助人们阅读您的代码以理解它.对于编写代码的人,可能更需要重用现有名称.但是,对于使用不同名称阅读代码的人来说,可以防止由于所有内容看起来相同而引起的任何混乱.实际上,道格拉斯·克罗福德(Douglas Crockford)(经常吹嘘的JavaScript大师)倡导者上下文着色以解决类似的问题.

I think this applies to humans as well. Using distinct names helps people reading your code to understand it. For a person writing the code it might be more desirable to reuse existing names. However, for a person reading the code using distinct names prevents any confusion that might arise due to everything looking the same. In fact, Douglas Crockford (oft-touted JavaScript guru) advocates context coloring to solve a similar problem.

无论如何,回到前面的问题.我可以想到两种解决您当前问题的方法.第一个解决方案是简单地使用不同的名称,这就是您所做的.第二种解决方案是模拟非递归 let 表达式.请注意,在Racket中, let 只是一个宏,它会扩展为left-left-left-lambda表达式.例如,考虑以下代码:

Anyway, back to the question at hand. There are two possible ways that I can think of to solve your immediate problem. The first solution is to simply use different names, which is what you did. The second solution is to emulate non-recursive let expressions. Note that in Racket, let is just a macro which expands to a left-left-lambda expression. For example, consider the following code:

(let ([x 5])
  (* x x))

let 表达式将被宏扩展为以下left-left-lambda表达式:

This let expression would be macro expanded to the following left-left-lambda expression:

((lambda (x) (* x x)) 5)

实际上,我们可以使用反向应用程序运算符

In fact, we can do the same thing in Haskell using the reverse application operator (&):

import Data.Function ((&))

-- Inserts value into slot1 or slot2
insert :: (Bool, Bool, Bool) -> (Bool, Bool, Bool)
insert (slot1, slot2, value) =
    (slot1 || value, slot1 && value) & \(slot1, value) ->
    (slot2 || value, slot2 && value) & \(slot2, value) ->
    (slot1, slot2, value)

本着同样的精神,我们可以通过手动宏扩展" let 表达式来解决您的问题:

In the same spirit, we can solve your problem by manually "macro expanding" the let expression:

const recur = (...args) => ({ type: recur, args });

const loop = (args, f) => {
    let acc = f(...args);
    while (acc.type === recur)
        acc = f(...acc.args);
    return acc;
};

const repeat = n => f => x =>
    loop([n, f, x], (n, f, x) =>
        n === 0 ? x : recur (n - 1, f, f(x)));

console.time('loop/recur');
console.log(repeat(1e6)(x => x + 1)(0)); // 1000000
console.timeEnd('loop/recur');

在这里,不是将默认参数用于初始循环状态,而是将它们直接传递给 loop .您可以将 loop 视为Haskell中的(&)运算符,该运算符也可以递归.实际上,此代码可以直接音译为Haskell:

Here, instead of using default parameters for the initial loop state I'm passing them directly to loop instead. You can think of loop as the (&) operator in Haskell which also does recursion. In fact, this code can be directly transliterated into Haskell:

import Prelude hiding (repeat)

data Recur r a = Recur r | Return a

loop :: r -> (r -> Recur r a) -> a
loop r f = case f r of
    Recur r  -> loop r f
    Return a -> a

repeat :: Int -> (a -> a) -> a -> a
repeat n f x = loop (n, f, x) (\(n, f, x) ->
    if n == 0 then Return x else Recur (n - 1, f, f x))

main :: IO ()
main = print $ repeat 1000000 (+1) 0

如您所见,您根本不需要 let . let 可以完成的所有操作也可以由 letrec 完成.如果您确实想要变量阴影,则可以手动执行宏扩展.在Haskell中,您甚至可以更进一步,并使用

As you can see you don't really need let at all. Everything that can be done by let can also be done by letrec and if you really want variable shadowing then you can just manually perform the macro expansion. In Haskell, you could even go one step further and make your code prettier using The Mother of all Monads.

这篇关于默认参数值未定义;这是JavaScript错误吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆