素材牛VIP会员
JavaScript中函数作为另一个函数的参数的时候它存在于哪个作用域
 12***om  分类:Node.js  人气:960  回帖:3  发布于6年前 收藏

一直对函数作为参数被传递进另外一个函数理解的不是很清除。先看下这段代码吧:

function test(fn){
    var bar = 1;
    fn();
}
var bar = 99;
test(function foo(){
    console.log(bar);
});
console.log(foo);

先说下结果为99和foo is not defined。在《你不知道的JavaScript》一书中有这么一句话:无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

所以我的问题是这样的:

  • 上面代码中test函数的参数foo函数是函数表达式对吧?也算是函数声明吧?

  • 代码中的foo函数到底存在于哪个作用域里面呢?我最开始以为console.log(bar)的结果是99,说明了这个foo函数存在于全局作用域内而不是test函数作用域内(如果在test函数的作用域内结果就该为1了吧),可是最后的console.log(foo)的结果又是foo is not defined。。

困惑了很久。。望解答~

 标签:node.jsjavascript

讨论这个帖子(3)垃圾回帖将一律封号处理……

Lv6 码匠
85***32 交互设计师 6年前#1

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 书中的说明是一样的:

无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

Lv4 码徒
间***p JAVA开发工程师 6年前#2

你的主要问题是,函数表达式函数声明是不一样的。

把你的代码改成这样,你运行看看结果是怎样的:

function test(fn){
    var bar = 1;
    fn();
}
var bar = 99;
function foo() {
    console.log(bar);
}
test(foo);
console.log(foo);

对于函数表达式,函数名字(标识符)的作用域只存在于它自身内部,在外面你是访问不到函数名字的。

Lv5 码农
卡***丢 软件测试工程师 6年前#3

foo在声明的时候并不在test的作用域内,所以它只能访问全局变量;foo作为表达式方法,他的名字只在自己的定义内有效:This name is then local only to the function body (scope).

 文明上网,理性发言!   😉 阿里云幸运券,戳我领取