本文深入解析JavaScript中函数定义的四种常见方式,包括Function构造函数、函数声明、匿名函数表达式及命名函数表达式。重点对比了函数名与函数变量的关系、作用域继承差异以及函数声明提升机制,并探讨了函数在嵌套语句中作为表达式处理的特殊情况,帮助开发者理解JS引擎对代码的解析逻辑。
本文详细解析JavaScript中几种主流的函数定义方式,旨在为开发者提供清晰的参考。
JavaScript函数的四种定义方式
我们将首先介绍JavaScript中最基础的四种函数定义形式。
第一种方式是利用Function构造函数来创建函数,具体实现代码如下:
var multiply = new Function('x', 'y', 'return x * y;');
第二种方式是函数声明,这是日常开发中使用频率最高的一种定义方法。
function multiply(x, y) {
return x * y;
}
第三种方式是函数表达式,通常表现为将匿名函数赋值给一个变量,这也是非常普遍的写法。
var multiply = function(x, y) {
return x * y;
}
第四种方式同样是函数表达式,区别在于这里定义的是命名函数,随后将其赋值给变量,其外观与前一种方式极为相似。
var multiply = function multi(x, y) {
return x * y;
}
函数名与函数变量的关系辨析
接下来,我们需要厘清函数名与函数变量之间的关联,这往往令人困惑。以例4为例,重点在于理解函数变量multiply与函数名multi之间的关系。
函数名具有不可变性,而函数变量则可以重新赋值。这一点不难理解,例如在例4中定义的multiply变量,如果对其重新赋值为:
multiply = function(x, y) {
return x + y;
}
此时函数行为从乘法变为加法。然而,函数名multi却无法改变,因为函数定义一旦确定,只要引用存在,其本质不变。这一点初看可能难以理解,请继续阅读后续内容,随着深入会逐渐清晰。
此外,函数名在函数外部是不可见的,它仅在函数体内部有效。一个简单的验证示例如下:
var foo = function bar() {
alert('hello');
}
foo(); // 提示“hello”字符串
bar(); // 执行报错,bar未定义
显然,bar虽然是一个函数名,但无法在外部被调用。读者可能会疑惑,为何此例与例4结构相似,却不采用例2的方式?这涉及到底层机制,请继续往下看。
回到例4,函数名multi与函数变量multiply看似相关,实则毫无关联,因此无需保持一致。基于此,前四个例子本质上可以精简为三种,例2与例4在底层逻辑上是一致的。不信?请继续阅读验证。
对比例2与例4,区别仅在于例2缺少了var定义的函数变量;而例3与例4的区别仅在于缺少函数名。从现象看,例2与例4本质相同,证据如下:
function foo() {}
alert(foo); // 提示包含“foo”的函数名
var bar = foo;
alert(bar); // 提示依然只包含“foo”的函数名,和bar半毛钱关系都没有
这确实是铁证。将例2的代码合并书写,即可得到例4的形式。这正是两者本质相同的体现:在使用例2方式定义函数时,JavaScript引擎会执行额外操作,例如声明一个名为multiply的函数,并悄悄定义一个同名的变量,最后将该变量赋值给这个函数。这两个同名实体中,我们自以为在使用函数名,实际上调用的是函数变量。由于函数名无法在外部调用,因此得出上述推断。
函数声明提升与作用域差异
这里存在一个关键差异:通过函数声明方式定义的函数,可以在定义之前就被调用。这与构造函数或函数表达式声明的函数不同,具体代码示例如下:
foo(); // 提示Foo
function foo() {
alert('Foo');
}
bar(); // 哥们,和上面确实不一样,就不要逞能,这不报错了?提示bar未定义
var bar = function() {
alert('Bar');
}
关于构造函数声明的函数,其不会继承当前声明位置的作用域,而是默认仅拥有全局作用域。这是其他几种声明方式所不具备的特性,示例如下:
function foo() {
var hi = 'hello';
//return function() {
// alert(hi);
//};
return Function('return hi;');
}
foo()(); // 执行效果大家自己跑一下看看
由此可见,使用构造函数声明并返回的函数在执行时会报错,因为其全局作用域中并不存在变量hi。
构造函数方式的性能开销
此外,通常认为构造函数方式声明的函数效率较低。原因在于,其他三种方式定义的函数只会被解析一次(它们存在于闭包中,仅与作用域链有关,函数体只解析一次)。而构造函数方式在每次执行时,其函数体都会被重新解析。由于每次执行都需要先解析参数和函数体再执行,这必然导致效率低下。具体实验方法暂略。
源元素与函数声明的特殊情况
最后讨论一个容易被忽视的情况:何时看似函数声明的方式实际上并非函数声明(即例2的方式在何种情况下转化为其他方式)?
当成为表达式的一部分,就如同例3和例4。 不再是脚本本身或者函数的“源元素”(source element)。什么是源元素呢?即在脚本中的非嵌套语句或者函数体(A "source element" is a non-nested statement in the script or a function body),例如:
var x = 0; // source element
if (x == 0) { // source element
x = 10; // not a source element, 因为嵌套在了if语句里
function boo() {} // not a source element, 因为嵌套在了if语句里
}
function foo() { // source element
var y = 20; // source element
function bar() {} // source element
while (y == 10) { // source element
function blah() {} // not a source element, 因为嵌套在了while语句里
y++; // not a source element, 因为嵌套在了while语句里
}
}
理解了源元素的概念后,继续讨论函数声明。请看以下示例:
// 函数声明
function foo() {}
// 函数表达式
(function bar() {})
// 函数表达式
x = function hello() {}
if (x) {
// 函数表达式
function world() {}
}
// function statement
function a() {
// function statement
function b() {}
if (0) {
// 函数表达式
function c() {}
}
}
最后阐述个人理解:区分函数声明与非函数声明的原因在于,JS解析引擎在执行函数声明时,会将其提前声明(即可以在定义前使用),这是解析引擎预先处理的结果。而对于非函数声明(如表达式函数),JS解析引擎仅提前定义var声明的变量,此时变量值为undefined。真正的赋值发生在代码实际执行位置,因此上述报错显示为undefined。此时变量已定义但未赋值,JS引擎无法识别其为函数。
希望本文对JavaScript WEB程序设计的读者具有借鉴价值。