为什么不允许在参数中声明变量,但是可以声明函数?

9
可能是个愚蠢的问题。我通过谷歌搜索但找不到答案。如下,变量声明不能作为函数的参数。
function t(a) {
    alert(a);
}

t(var x = 1); // Uncaught SyntaxError: Unexpected token var
t(let x = 1); // Uncaught SyntaxError: missing ) after argument list
t(x = 1); // working fine and later I am able to access x also
console.log(x); // printing 1

但是函数声明可以作为一个函数的参数,如下所示。
function callback(str, f1, f2) {
    if(str == "")
        f1();
    else
        f2();
};

callback("", function b1() { alert("empty") }, function b2() { alert("not empty") }); // working fine

b1(); // Throwing error Uncaught ReferenceError: b1 is not defined

请问有人能帮我理解以下问题吗?

  • 为什么变量声明不允许作为函数参数,但函数声明可以作为参数?
  • 为什么我们无法在函数调用外部访问声明为函数参数的函数?

1
function callback 是一个函数声明,但 function b1 不是;它是一个函数表达式,因为它在表达式上下文中使用而不是语句上下文中。 - Paul
1
console.log(x = 1)会输出1console.log(function() {...})会输出函数定义。然而,console.log(var x = 1)是语法错误。虽然答案会详细解释,但定义一个函数或执行x=1返回一个值,而声明一个var则不会。 - Tyler Roper
1
简单来说,声明语句永远不可能作为参数合法。无论是变量声明还是函数声明都不行。function关键字的问题在于,当它在期望表达式的上下文中使用时,它并不是声明一个函数,而是创建一个产生函数的函数表达式。顺便提一下,这里函数的名称除了帮助调试和错误报告外没有实际用途,这就是为什么在这些情况下通常使用匿名函数的原因。 - Lennholm
@Paul,函数声明和函数表达式非常相似,让人感到困惑。起初我以为你错了,但实际上调用它的上下文很重要。我试图在我的回答中深入探讨。 - icc97
2个回答

16

好问题!我会将其分为几个部分。由于您的问题涉及多个深层次的主题,因此在这里需要搜索大量材料。

1. 语句没有值

声明是语句。它们没有值,因此不能作为参数。这段代码...

let a = 1

...没有价值,这意味着这些都不会起作用:

doStuff(let a = 1)
let b = (let a = 1)
(let a = 1) + 5

名称a,或a + 5,或f(a)都是表达式,与语句不同的是,表达式具有值。但是a本身的声明并没有值。

请注意,你对此的直觉并不荒谬:在其他语言中,let a = 1是一个求值为1的表达式。但在Javascript中不是这样。

2. 函数是对象

然而function关键字确实有价值:它定义的Function对象。与变量不同,变量是为你方便而创建的语言构造,Functions是实际存在于运行程序中的对象。我们说函数是一等对象

可以做所有这些事情:

doStuff(function f() {})
let a = function f() {}
let b = (function f() {}) + 5 // the result of this is funny

回到你的例子:

callback(
  "", // a String object
  function b1() { alert("empty") }, // a Function object
  function b2() { alert("not empty") } // a Function object
);

类似于这样:

function b1() { alert("empty") }
function b2() { alert("not empty") }

callback("", b1, b2)

但并不完全如此。让我们谈谈作用域。

3. 名称在作用域内被定义

名称(例如变量或函数)的作用域是具有该定义的代码部分。

例如:

// Top-level scope:
let a = 1

if (a == 1) {
  // Inner block scope:
  let b = 2
  console.log(a, b) // 1, 2
}

console.log(a, b) // 1, undefined

作用域存在于更大的作用域内。内部作用域可以访问周围的作用域(a和b在块内可见),但反过来则不行(所以b在外部不可见)。

当你在调用中创建你的function对象时...

<code>f(function a() { })
</code>

...他们被困在一个内部范围内,无法从外部引用。

4. 赋值是表达式

在你的示例代码中,你注意到像这样声明a可以工作:

f(a = 5)

这真是不幸。这实际上是JavaScript历史的产物。在现代代码中,您应该始终使用letconst来定义变量。

那么它为什么起作用呢?有两个原因。首先,因为它是一个 赋值,而不是一个声明,就像这样:

let x = 1
f(x = 2)

赋值语句是表达式,它们的值是被赋的值。例如 x = 2 的值是 2,并且会导致 x 的值发生改变。

5. 存在全局作用域

第二个原因则很不幸。当你避免使用 letvarconst 关键字时,实际上隐式地使用了 global 作用域。

这是所有作用域的母体,其中存储的名称可以从代码的任何位置访问。所以,如果只是像这样写...

f(a = 5)

如果在当前作用域中没有声明a,那么它会被隐含地声明在全局作用域中,并进行赋值。可以将其视为如下伪代码:

... without having declared a anywhere in the current scope, it's implicitly declared in the global scope, and the assignment takes place. Think of it as this (pseudo-code):

global let a
f(a = 5)

当然,那并不是有效的 JavaScript。但你明白了重点。


0
我能想到的最简短的解释方式是,如果我们将命名函数表达式参数(它在调用时所在的上下文非常重要)转换为你的示例:
callback("", function b1() { console.log("empty"); }, ...)

作为变量声明的参数:

callback("", var b1 = function() { console.log("empty"); }, ...)

This gives an error:

function callback(str, f1, f2) {
    if(str == "")
        f1();
    else
        f2();
};

callback(
  "", 
  var b1 = function () { console.log("empty") }, 
  var b2 = function () { console.log("not empty") }
); // Error: expected expression, got keyword 'var' 

This works fine:

function callback(str, f1, f2) {
    if(str == "")
        f1();
    else
        f2();
};

callback(
  "", 
  function b1() { console.log("empty") }, 
  function b2() { console.log("not empty") }
); // logs 'empty'

函数声明与函数表达式

它们几乎相同,MDN的参考层次结构对它们的区别非常详细:

Operators/function (表达式):

function [name]([param1[, param2[, ..., paramN]]]) {
  [statements]
}

语句/函数(定义):

function name([param1[, param2,[..., paramN]]]) {
   [statements]
}

看看表达式是如何列在'运算符'下面,而声明则列在'语句'下面。

注意:我对MDN的语法进行了轻微调整,以显示它们有多么相似 - 它们的参数后面都有数字,并且对于声明和表达式,内部语句都是可选的。

重要的是你在哪里调用它们:

function b1() {console.log('empty');} // declaration

callback(
  ""
  function b1() {console.log('empty');} // expression
)

同样,三元运算符 (Operators/Conditional_Operator) str == "" ? f1 : f2 是一个操作符/表达式,而 if 语句 (Statements/if...else) if (str == "") { return f1 } else { return f2 } 则是一个语句/声明。

function callCallback(cb) {
  cb()
}

function callback(str, f1, f2) {
  callCallback(str == "" ? f1 : f2)
}

callback(
  "", 
  function b1() { console.log("empty") }, 
  function b2() { console.log("not empty") }
); // logs 'empty'

function callCallback(cb) {
  cb()
}

function callback(str, f1, f2) {
  callCallback(if (str == "") { return f1 } else { return f2 })
}

callback(
  "", 
  function b1() { console.log("empty") }, 
  function b2() { console.log("not empty") }
); // Error: expected expression, got keyword 'if'


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