1 回答
TA贡献1866条经验 获得超5个赞
JS中对于函数的声明语法在标准中就有两种不同的语法样式,其一称为函数声明
样式,另一个是函数表达式
样式,在标准中也写得很简略,像下面这样,上面的是函数声明
样式,下面则是函数表达式
,opt
记号代表的是可选的,也就是可有可无的意思:
FunctionDeclaration :function Identifier ( FormalParameterList opt ){ FunctionBody } FunctionExpression :function Identifier opt ( FormalParameterList opt ){ FunctionBody }
所以在函数表达式
这种样式,函数的识别名是可以不需要有的,但它因为是个表达式
,可以赋给另一个变量当值来看待,所以才会有像var f = function(){ return 23 }
的这种写法,真正在函数调用时是用前面的变量识别名称f,而不是额外的那个可有可无的函数识别名称。如果充分区分出函数声明
与函数表达式
的两种不同样式,在文中最好以函式创建
这字词来说明函数声明或函数表达式的这两种语句较佳。
那要分辨何者是函数声明
又何者是函式表达式
?最简单的方式就是看语句的开头,在单一个语句中以function
作为该语句开头的函数,应该就是函数声明
,函式表达式
不会以function开头。所以下面的几例都是函式表达式
而不是函式声明
样式:
var a = function() { return 3; } var a = function bar() { return 3; } (function sayHello() { alert("hello!"); })();
而为何会有两种不同的函数语法样式,主要是这两种样式的用处不同,这有很多不同的应用情况。
最明显的例子是函数声明
有特殊的提升(hoisting)特性,在同一作用域中的函数声明
会先被提升到此作用域的最上面,所以函数声明
可以写在代码文档的后面,但可以在代码文档的上面位置使用。此外,函数声明
只能在函数中块级或整个应用的全局区中使用,它没办法在其他的块级中使用,例如像if、for等的花括号({})中。而在某些情况下,当需要把函数整体块级作为一种值,用来当其他函数的传参或返回时,例如回调函数的语法结构,就是要使用函式表达式
的样式才可以达到。当然,基本上这两者看起来好像都是长得一样,实际上在执行阶段的运作并不相同。
带有名称的函数表达式,有个专用语称之为"具名函数表达式"(Named function expressions,NFE),这个函数的识别名,它的作用域到底是在什么地方,答案是在函数的主体(FunctionBody)内部。原因当然它只是个原本就可有可无的"代理"函数名,真正的这函数识别名称是被赋值的那个变量识别名。
正常情况下,你只能在函数表述式中的主体中使用这个"代理函数名",这也是符合标准的规定,如下面的例子:
var f = function foo(){ return typeof foo; };typeof foo; // "undefined"f(); // "function"
那么又为何要使用这个"代理函数名",不是可有可无的吗?
因为这个名称在调试时,可以明确地在呼叫堆叠中看到,如果是不加这名称,也就是"匿名函数表达式"在调试时是看不准是呼叫什么的。这使得调试时多了一些便利,所以它会被用在这个情况下。
比较特别的情况是在IE8以前的版本中,它里面的JS引擎并不是现在的标准ECMAScript规范,而是JScript 5.8。IE8并没有设计这个封闭作用域,来界定出函数表达式的作用域,而且,在IE8中认为这种"具名函数表达式",相等于函数声明
。
以上的资料主要参考Named function expressions demystified与Function Declarations vs. Function Expressions
后面针对题目本身提供一些解答:
上面代码中test函数的参数foo函数是函数表达式对吧?也算是函数声明吧?
是函数表达式
而不是函数声明,这是依照ECMAScript标准的说明。只是它有带函数名,是上面说的"具名函数表达式"(NFE)。
代码中的foo函数到底存在于哪个作用域里面呢?
在函数表达式
的函数定义块级作用域里,也就是函数的主体(FunctionBody)内部,上面有例子。但这有例外情况,在IE8以前的IE浏览器,这代码应该是会如同函数声明样式一样的执行结果。
另一个问题忘了说明,补充一下。
全域中有一个bar=99变量,在test函数中的区块中也有声明一个bar=1,那么如果在传参中是以函数类型传入test函数,有存取bar如console.log(bar)
,为何答案是99而不是1?
主要是因为传参的作用域是在全局作用域,并非函数的作用域之中。也就是说像下面的代码:
test(function foo(){ console.log(bar);});
相等于下面的代码:
var f = function foo(){ console.log(bar); }test(f);
也就是说在本例子中,test的传参在传入后,然后被调用,里面要作什么事,已经可以决定了结果,也就是相当于:
var bar = 99;var f = function foo(){ console.log(99); // bar在全局中已赋值为99} test(f);
上面说的概念虽然会有点怪异,在这个例子你可以把f这个函数,当成只是稍晚些打印到控制台的一种变量而已。
因为很特别的是,这个f函数,它并没有传参,也没有返回,单纯只是打印bar变量,这个bar变量如果要能有值,只能从函数所能存取得到的作用域而来,而且是以函数声明的主体上下文作用域为主。
例子中的传参的作用域是位于全局作用域,所以只能存取得到全局的那个var bar = 99
变量。
会让人误解的是这个test函数中的写法:
function test(fn){ var bar = 1; fn(); }
有可能会直觉得认定,应该是相当于下面的写法:
function test(function foo(){ console.log(bar); }){ var bar = 1; foo(); }
所以它的结果应该是:
console.log(1)
但结果不是上面那样,实际上这相当于下面的例子,因为f函数不需要传参,所以相等于直接在test函数中调用而已:
var f = function foo(){ console.log(99); // bar在全局中已赋值为99}function test(){ var bar = 1; f(); }
除非f函数的内容,是定义在test函数的区块之中时,才有可能得到1的打印结果,像下面这样:
function test(){ var bar = 1; var f = function foo(){ console.log(bar); // bar相当于1 } f(); }
JS中的作用域,是属于词法上的作用域,也就是"静态"的作用域,作用域中的变量绑定,在执行前就已经决定好了,按定义是在编译期间。在JavaScript: The Definitive Guide这本书中对作用域的一句说明如下:
函数在调用(执行)时,使用它们被定义(声明)时被影响的作用域链
这与题中所引用的 你不知道的JavaScript 书中的说明是一样的:
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
添加回答
举报