简体   繁体   English

默认参数值未定义; 这是一个JavaScript Bug吗?

[英]Default parameter value undefined; is this a JavaScript Bug?

Below is a syntactically valid javascript program – only, it doesn't behave quite the way we're expecting. 下面是一个语法上有效的javascript程序 - 只是,它的行为并不像我们期望的那样。 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 

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. 我们来谈谈现在不使用$ -prefix的原始代码。

When the lambda for loop is being evaluated, n as received by repeat , is available in the lambda's environment. 当正在评估lambda for loop ,在lambda的环境中可以获得由repeat接收的n Setting the inner n to the outer n 's value should effectively shadow the outer n . 将内部n设置为外部n的值应该有效地遮蔽外部n But instead, JavaScript sees this as some kind of problem and the inner n results in an assignment of undefined . 但相反,JavaScript将此视为某种问题,内部n导致undefined的赋值。

This seems like a bug to me but I suck at reading the spec so I'm unsure. 这对我来说似乎是一个错误,但我读取规范时很糟糕,所以我不确定。

Is this a bug? 这是一个错误吗?

I guess you already figured out why your code doesn't work. 我想你已经弄清楚为什么你的代码不起作用了。 Default arguments behave like recursive let bindings. 默认参数的行为类似于递归let绑定。 Hence, when you write n = n you're assigning the newly declared (but yet undefined ) variable n to itself. 因此,当您编写n = n您将新声明的(但尚未undefined )变量n分配给自身。 Personally, I think this makes perfect sense. 就个人而言,我认为这很有道理。

So, you mentioned Racket in your comments and remarked on how Racket allows programmers to choose between let and letrec . 所以,你在评论中提到了Racket,并评论了Racket如何让程序员在letletrec之间做出选择。 I like to compare these bindings to the Chomsky hierarchy . 我喜欢将这些绑定与Chomsky层次结构进行比较。 The let binding is akin to regular languages. let绑定类似于常规语言。 It isn't very powerful but allows variable shadowing. 它不是很强大但允许变量阴影。 The letrec binding is akin to recursively enumerable languages. letrec绑定类似于递归可枚举语言。 It can do everything but doesn't allow variable shadowing. 它可以做任何事情,但不允许变量阴影。

Since letrec can do everything that let can do, you don't really need let at all. 由于letrec能做的一切let能做到的,你并不真的需要let所有。 A prime example of this is Haskell which only has recursive let bindings (unfortunately called let instead of letrec ). 一个主要的例子是Haskell,它只有递归let绑定(不幸的是叫let而不是letrec )。 The question now arises as to whether languages like Haskell should also have let bindings. 现在的问题是,像Haskell这样的语言是否也应该let绑定。 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'')

If let in Haskell wasn't recursive then we could write this code as: 如果let Haskell不是递归的,那么我们可以将这段代码编写为:

-- 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)

So why doesn't Haskell have non-recursive let bindings? 那么为什么Haskell没有非递归的let绑定呢? 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. 通过仅使用变量名称一次,程序变得更容易编译器进行分析。

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. 事实上,Douglas Crockford(经常被吹捧的JavaScript大师) 主张上下文着色来解决类似的问题。


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. 第二种解决方案是模拟非递归let表达式。 Note that in Racket, let is just a macro which expands to a left-left-lambda expression. 请注意,在Racket中, let只是一个扩展为左左lambda表达式的宏。 For example, consider the following code: 例如,请考虑以下代码:

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

This let expression would be macro expanded to the following left-left-lambda expression: let表达是宏扩展到了下面的左左lambda表达式:

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

In fact, we can do the same thing in Haskell using the reverse application operator (&) : 实际上,我们可以使用反向应用程序运算符(&)在Haskell中执行相同的操作:

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)

In the same spirit, we can solve your problem by manually "macro expanding" the let expression: 本着同样的精神,我们可以通过手动“宏扩展” let表达式来解决您的问题:

 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'); 

Here, instead of using default parameters for the initial loop state I'm passing them directly to loop instead. 在这里,我没有使用默认参数作为初始循环状态,而是直接将它们传递给loop You can think of loop as the (&) operator in Haskell which also does recursion. 您可以将loop视为Haskell中的(&)运算符,它也会进行递归。 In fact, this code can be directly transliterated into Haskell: 实际上,这段代码可以直接音译成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

As you can see you don't really need let at all. 如你所见,你根本不需要let 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. let可以完成的所有事情也可以通过letrec完成,如果你真的想要变量阴影,那么你可以手动执行宏扩展。 In Haskell, you could even go one step further and make your code prettier using The Mother of all Monads . 在Haskell中,你甚至可以更进一步,使用所有Monads的母亲让你的代码更漂亮。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM