如何在JavaScript中最好地实现参数输出(out params)?

45
我正在使用带有jQuery的Javascript。我想实现输出参数。在C#中,它看起来像这样:
/*
 * odp      the object to test
 * error    a string that will be filled with the error message if odp is illegal. Undefined otherwise.
 *
 * Returns  true if odp is legal.
 */
bool isLegal(odp, out error);

在JS中,像这样做最好的方法是什么?对象?

function isLegal(odp, errorObj)
{
    // ...
    errorObj.val = "ODP failed test foo";
    return false;
}

Firebug告诉我上述方法可以工作,但是否有更好的方法?


Nick,我知道这是一段时间以前的事情了,但我认为我终于有了一个答案。是的,你可以在JavaScript中使用输出参数。 - Matt
10个回答

51

@Felix Kling提到的回调函数方法可能是最好的想法,但我也发现有时候很容易利用JavaScript对象字面语法,在出错时只需让函数返回一个对象:

function mightFail(param) {
  // ...
  return didThisFail ? { error: true, msg: "Did not work" } : realResult;
}

然后当你调用这个函数时:

var result = mightFail("something");
if (result.error) alert("It failed: " + result.msg);

虽然不太花哨也不是特别可靠,但对于一些简单的情况来说肯定没问题。


+1,这种方法在JSON调用中很常见,但我很少在代码中看到它。 - gradbot
4
因为我对JavaScript如何处理“out参数”感兴趣,所以来到这个页面,但我对被接受的答案感到失望。当编写非常过程化的逻辑时,具有明确的返回类型和一个或多个明确类型的“out参数”能力是非常强大的。不幸的是,由于我声望很低,无法进行负面评价。 - Anthony Ruffino
3
好的,答案者并没有设计这种语言。在这种语言的限制下,这可能是一个很好的答案。如果JavaScript很糟糕,那也不是答案者的错。 - AaronLS

19

我认为这可能是唯一的方法(但我不是一个非常专业的JavaScript程序员;)。

另外,您还可以考虑使用回调函数:

function onError(data) {
    // do stuff
}


function isLegal(odp, cb) {
    //...
    if(error) cb(error);
    return false;
}

isLegal(value, onError);

19

是的,正如您自己提到的那样,在JavaScript中,对象是传递引用数据的最佳且唯一方式。我建议保留您的isLegal函数不变,并像这样简单地调用它:

var error = {};
isLegal("something", error);
alert(error.val);

8
我所看到的答案并没有在JavaScript中实现输出参数,就像在C#中使用的那样(out关键字)。它们只是一种在出现错误时返回对象的解决方法。
但是,如果您真的需要输出参数,该怎么办呢?
由于Javascript不直接支持它,您需要构建与C#的输出参数接近的东西。看看这个方法,我在JavaScript中模拟了C#的DateTime.TryParse函数。输出参数是结果,因为JavaScript不提供一个输出关键字,所以我在函数内部使用.value将值传递到函数外部(受MDN suggestion启发)。

// create a function similar to C#'s DateTime.TryParse
var DateTime = [];
DateTime.TryParse = function(str, result) {
  result.value = new Date(str); // out value
  return (result.value != "Invalid Date");
};

// now invoke it
var result = [];
if (DateTime.TryParse("05.01.2018", result)) {
  alert(result.value);
} else {
  alert("no date");
};

运行代码片段,您会发现它可以解析 str 参数并将其返回为日期格式,存储在 result 参数中。请注意,在调用函数之前必须将 result 初始化为空数组[]或对象{}(根据您的需求)。这是必须的,因为在函数内部您需要“注入”.value属性。
现在,您可以使用上述模式编写一个类似于您问题中提出的函数(这也向您展示了如何模拟C#中已知的新discard参数,称为 out _ :在JavaScript中,我们传递 [] ,如下所示:)

// create a function similar to C#'s DateTime.TryParse
var DateTime = [];
DateTime.TryParse = function(str, result) {
  result.value = new Date(str); // out value
  return (result.value != "Invalid Date");
};

// returns false, if odb is no date, otherwise true
function isLegal(odp, errorObj) {
  if (DateTime.TryParse(odp, [])) { // discard result here by passing []
    // all OK: leave errorObj.value undefined and return true
    return true;
  } else {
    errorObj.value = "ODP failed test foo"; // return error
    return false;
  }
}

// now test the function
var odp = "xxx01.12.2018xx"; // invalid date
var errorObj = [];
if (!isLegal(odp, errorObj)) alert(errorObj.value); else alert("OK!");

这个例子使用result参数传递错误信息,代码如下:

errorObj.value = "ODP failed test foo"; // 返回错误

如果运行该示例,将在弹出对话框中显示此消息。 注意:与上面所示的舍弃参数不同,在JavaScript中,您还可以使用检查undefined的方法,即在函数内部进行检查。
if (result === undefined) { 
   // do the check without passing back a value, i.e. just return true or false 
};

如果不需要,完全可以省略result作为参数,这样你可以这样调用
if (DateTime.TryParse(odp)) { 
    // ... same code as in the snippet above ...
};

1
为什么要使用数组而不是对象? - Kyle Delaney
@KyleDelaney - 你有什么解决方案? - Matt
2
var DateTime = {}; and var errorObj = {}; - Kyle Delaney
@KyleDelaney - 抱歉,回答晚了。我认为{}(对象)也可以使用。 - Matt

4

我正在使用回调方法(类似于Felix Kling的方法),来模拟out参数的行为。我的答案与Kling的不同之处在于回调函数充当引用捕获闭包,而不是处理程序。

这种方法受到JavaScript冗长的匿名函数语法的影响,但密切地模仿了其他语言的out参数语义。

function isLegal(odp, out_error) {
    //...
    out_error("ODP failed test foo"); // Assign to out parameter.
    return false;
}

var error;
var success = isLegal(null, function (e) { error = e; });

// Invariant: error === "ODP failed test foo".

3

JS还有一种传递“out”参数的方法,但我相信对于您的情况来说,最好的方法已经提到了。

数组也是按引用而非值传递的。因此,就像您可以将对象传递给函数,然后在函数中设置对象的属性,然后返回并访问该对象的属性一样,您也可以将数组传递给函数,在函数内部设置数组的某些值,然后返回并在数组外部访问这些值。

所以在每个情况下,您都可以问自己,“数组还是对象更好?”


虽然这是可能的,但为了调用代码的清晰度,我建议使用@Pointy的答案:使用重载返回值,因为它稍微更清晰一些。 - cmroanirgo

1

我不会发布任何代码,但这些答案中未能做到的是让事情有条理。我正在原生JS领域工作,问题出现在一些本地API调用需要转换,因为我们不能在没有丑陋可耻的黑客攻击的情况下写入参数。

这是我的解决方案:

    // Functions that return parameter data should be modified to return
    // an array whose zeroeth member is the return value, all other values
    // are their respective 1-based parameter index.

这并不意味着要定义和返回每个参数,只需要返回接收输出的参数即可。
采用这种方法的原因是:任何数量的过程可能需要多个返回值。这会导致具有命名值的对象(最终将与所有操作的词法上下文不同步)需要不断地被记忆以便适当地处理过程。
使用预定方法,您只需要知道您所调用的内容以及您应该查找的位置,而不必知道您正在寻找什么。
还有一个优点是可以编写“强大而愚蠢”的算法来包装所需的过程调用,使此操作“更加透明”。
最好使用对象、函数或数组(它们都是对象)作为“写回输出”参数,但我认为如果必须进行任何额外的工作,应该由编写工具包的人来完成,以使事情更容易或扩展功能。

这是一个适用于各种场合的通用答案,它使得 APIs 看起来像应该看起来的样子,而不是像一堆混乱的意大利面代码拼凑成的图案,无法确定它是一个定义还是数据

恭喜,并祝你好运。

我正在使用 webkitgtk3 并与一些本地 C 库进行接口交互。因此,这个经过验证的代码示例至少可以用作说明的目的。

// ssize_t read(int filedes, void *buf, size_t nbyte)
SeedValue libc_native_io_read (SeedContext ctx, SeedObject function, SeedObject this_object, gsize argument_count, const SeedValue arguments[], SeedException *exception) {


    // NOTE: caller is completely responsible for buffering!

                    /* C CODING LOOK AND FEEL */


    if (argument_count != 3) {
        seed_make_exception (ctx, exception, xXx_native_params_invalid,
            "read expects 3 arguments: filedes, buffer, nbyte: see `man 3 read' for details",
            argument_count
        );  return seed_make_undefined (ctx);
    }

    gint filedes = seed_value_to_int(ctx, arguments[0], exception);
    void *buf = seed_value_to_string(ctx, arguments[1], exception);
    size_t nbyte = seed_value_to_ulong(ctx, arguments[2], exception);

    SeedValue result[3];

    result[0] = seed_value_from_long(ctx, read(filedes, buf, nbyte), exception);
    result[2] = seed_value_from_binary_string(ctx, buf, nbyte, exception);

    g_free(buf);
    return  seed_make_array(ctx, result, 3, exception);

}

这是在JavaScript中返回输出参数的绝佳方法。对我来说最有意义。 - Sunil

1
以下是我正在使用的方法。这是对这个问题的答案。但是代码还没有经过测试。
function mineCoords( an_x1, an_y1 ) {
  this.x1 = an_x1;
  this.y1 = an_y1;
}

function mineTest( an_in_param1, an_in_param2 ) {

  // local variables
  var lo1 = an_in_param1;
  var lo2 = an_in_param2;

  // process here lo1 and lo2 and 
  // store result in lo1, lo2

  // set result object
  var lo_result = new mineCoords( lo1, lo2 );
  return lo_result;
}

var lo_test = mineTest( 16.7, 22.4 );
alert( 'x1 = ' + lo_test.x1.toString() + ', y1 = ' + lo_test.y1.toString() );

0

在Javascript中,以及大多数高级语言中,处理你所描述的特定用例的常规方法是依赖于错误(也称为异常),以让你知道是否发生了异常情况。在Javascript中,没有办法通过引用传递值类型(例如字符串、数字等)。

我建议你这样做。如果你真的需要将自定义数据反馈给调用函数,你可以子类化Error。

var MyError = function (message, some_other_param)
{
    this.message = message;
    this.some_other_param = some_other_param;
}
//I don't think you even need to do this, but it makes it nice and official
MyError.prototype = Error; 
...
if (something_is_wrong)
    throw new MyError('It failed', /* here's a number I made up */ 150); 

捕获异常很烦人,我知道,但跟踪引用也是如此。
如果你真的非常需要像“out”变量一样的行为,对象默认会按引用传递,并且可以轻松地从其他作用域中捕获数据。
function use_out (outvar)
{
    outvar.message = 'This is my failure';
    return false;
}

var container = { message : '' };
var result = use_out(container );
console.log(container.message); ///gives the string above
console.log(result); //false

我认为这在一定程度上回答了你的问题,但我认为你的整个方法从一开始就是错误的。JavaScript 支持更加优美和强大的方式来从函数中获取多个值。阅读一些有关生成器、闭包的内容,甚至在某些情况下回调也可以很好用——查找过程传递样式。

我想表达的重点是鼓励任何阅读此文的人将他们的编程风格调整到所使用语言的局限性和能力上,而不是试图将从其他语言学到的方法强加在它上面。

(顺便说一句,有些人强烈反对闭包,因为它们会引起恶意副作用,但我不会听他们的。他们是纯粹主义者。在许多应用程序中,副作用几乎是不可避免的,如果不进行大量的繁琐回溯和绕过无法到达的障碍,那么将所有副作用都保持在一个整洁的词法范围内,而不是散布在一片黑暗的指针和引用的地狱中,听起来对我来说更好)


0

真实的输出参数的主要优点是可以直接修改调用者作用域内一个或多个标量变量。在其他答案中提出的方法中,只有回调函数满足此要求:

function tryparse_int_1(s, cb)
{   var res = parseInt(s);
    cb(res);
    return !isNaN( res );
}

function test_1(s)
{   var /* inreger */ i;
    if( tryparse_int_1( s, x=>i=x ) )
        console.log(`String "${s}" is parsed as integer ${i}.`); else
        console.log(`String "${s}" does not start with an integer.`);
}

test_1("47");
test_1("forty-seven");

在这种情况下,传递每个输出参数需要额外的五个字符来将其标识符包装到匿名设置函数中。它既不易读也不易频繁输入,因此可以利用脚本语言最有趣的特性之一——执行字符串作为代码的能力。
以下示例实现了上面整数解析函数的扩展版本,现在具有两个输出参数:结果整数和指示它是否为正的标志:
/* ------------ General emulator of output parameters ------------ */

function out_lit(v)
{   var res;
    if( typeof(v) === "string" )
        res = '"' + v.split('\"').join('\\\"') + '"'; else
        res = `${v}`;
    return res;
}

function out_setpar(col, name, value)
{   if( col.outs == undefined ) col.outs = [];
    col.outs[name] = value;
}

function out_setret(col, value)
{   col.ret = value;  }

function out_ret( col )
{   var s;
    for(e in col.outs)
    {   s = s + "," + e + "=" + out_lit( col.outs[e] );  }
    
    if( col.ret != undefined )
    {   s = s + "," + out_lit( col.ret );  }
    
    return s;
}

/* -------- An intger-parsing function using the emulator -------- */

function tryparse_int_2 // parse the prefix of a string as an integer
(   /* string  */ s,    // in:  input string
    /* integer */ int,  // out: parsed integer value
    /* boolean */ pos   // out: whether the result is positive
)
{   var /* integer */ res; // function result
    var /* array   */ col; // collection of out parameters
    
    res = parseInt(s);
    col = [];    
    out_setpar( col, int, res           );
    out_setpar( col, pos, res > 0       );
    out_setret( col,          !isNaN( res ) );
    return out_ret( col );
}

在这个版本中,传递每个输出参数需要在其标识符周围添加两个额外的字符以将其嵌入字符串文字中,每次调用需要六个字符来评估结果。
function test_2(s)
{   var /* integer */ int;
    var /* boolean */ pos;
    
    if( !eval( tryparse_int_2( s, "int", "pos" ) ) )
    {   console.log(`String "${s}" does not start with an integer.`);  }
    else
    {   if( pos ) adj = "positive";
        else      adj = "non-positive";
        console.log(`String "${s}" is parsed as a ${adj} integer ${int}.`);
    }
}

test_2( "55 parrots"    );
test_2( "-7 thoughts"   );
test_2( "several balls" );

上面测试代码的输出为:
String "55 parrots" is parsed as a positive integer 55.
String "-7 thoughts" is parsed as a non-positive integer -7.
String "several balls" does not start with an integer.

然而,这个解决方案有一个缺陷:它无法处理非基本类型的返回。

也许更清晰的方法是模拟指针:

// Returns JavaScript for the defintion of a "pointer" to a variable named `v':
// The identifier of the pointer is that of the variable prepended by a $.
function makeref(v)
{   return `var $${v} = {set _(val){${v}=val;},get _() {return ${v};}}`;  }

// Calcualtes the square root of `value` and puts it into `$root`.
// Returns whether the operation has succeeded.
// In case of an error, stores error message in `$errmsg`.
function sqrt2
(   /* in  number */  value, /*  value to take the root of  */
    /* out number */ $root , /* "pointer" to result         */
    /* out string */ $errmsg /* "pointer" to error message  */
)
{   if( typeof( value ) !== "number" )
    {   $errmsg._ = "value is not a number.";
        return false;
    }
    if( value < 0 )
    {   $errmsg._ = "value is negative.";
        return false;
    }
    $root._ = Math.sqrt(value);
    return true;
}

以下是测试代码:

function test(v)
{   var /* string */ resmsg;
    var /* number */ root  ; eval( makeref( "root"   ) );
    var /* string */ errmsg; eval( makeref( "errmsg" ) );
    
    if( sqrt2(v, $root, $errmsg) ) resmsg = `Success: ${root}`;
    else                           resmsg = `Error: ${errmsg}`;
    console.log(`Square root of ${v}: ` + resmsg );
}

test("s"  );
test(-5   );
test( 1.44);

打印:

Square root of s: Error: value is not a number.
Square root of -5: Error: value is negative.
Square root of 1.44: Success: 1.2

通过这种方法创建的“指针”可以在其他函数中重复使用,并且可以在同一函数的后续调用中重复使用。例如,您可以定义一个附加字符串的函数:
// Append string `sep' to a string pointed to by $s, using `sep` as separator:
// $s shall not point to an undefined value.
function append($s, sep, val)
{   if( $s._ != '' ) $s._ += sep;
    $s._ += val;
}

并且这样使用:

const sep = ", "
var s; eval( makeref("s") );

s = '';
append( $s, sep, "one"   );
append( $s, sep, "two"   );
append( $s, sep, "three" );
console.log( s );

它将打印:

one, two, three

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接