[JavaScript]ECMA-262-3 深入解析.第四章.函数

概要

本文将给大家介绍ECMAScript中的一般对象之一——函数。我们将着重介绍不同类型的函数以及不同类型的函数是如何影响上下文的变量对象以及函数的作用域链的。 我们还会解释经常会问到的问题,诸如:“不同方式创建出来的函数会不一样吗?(如果会,那么到底有什么不一样呢?)”:

var foo = function () {
  ...
};

上述方式创建的函数和如下方式创建的有什么不同?

function foo() {
  ...
}

如下代码中,为啥一个函数要用括号包起来呢?

(function () {
  ...
})();

由于本文和此前几篇文章都是有关联的,因此,要想完全搞懂这部分内容,建议先去阅读 第二章-变量对象 以及 第三章-作用域链

下面,来我们先来介绍下函数类型。

函数类型

ECMAScript中包含三类函数,每一类都有各自的特性。

函数声明(Function Declaration)

函数声明(简称FD)是指这样的函数

  • 有函数名
  • 代码位置在:要么在程序级别或者直接在另外一个函数的函数体(FunctionBody)中
  • 在进入上下文时创建出来的
  • 会影响变量对象
  • 是以如下形式声明的
function exampleFunc() {
  ...
}

这类函数的主要特性是:只有它们可以影响变量对象(存储在上下文的VO中)。此特性同时也引出了非常重要的一点(变量对象的天生特性导致的) —— 它们在执行代码阶段就已经存在了(因为FD在进入上下文阶段就收集到了VO中)。

下面是例子(从代码位置上来看,函数调用在声明之前):

foo();

function foo() {
  alert('foo');
}

从定义中还提到了非常重要的一点 —— 函数声明在代码中的位置:

// 函数声明可以直接在程序级别的全局上下文中
function globalFD() {
  // 或者直接在另外一个函数的函数体中
  function innerFD() {}
}

除了上述提到了两个位置,其他位置均不能出现函数声明 —— 比方说,在表达式的位置或者是代码块中进行函数声明都是不可以的。

介绍完了函数声明,接下来介绍函数表达式(function expression)。

函数表达式

函数表达式(简称:FE)是指这样的函数:

  • 代码位置必须要在表达式的位置
  • 名字可有可无
  • 不会影响变量对象
  • 在执行代码阶段创建出来

    这类函数的主要特性是:它们的代码总是在表达式的位置。最简单的表达式的例子就是赋值表达式:

var foo = function () {
  ...
};

上述例子中将一个匿名FE赋值给了变量“foo”,之后该函数就可以通过“foo”来访问了—— foo()。

正如定义中提到的,FE也可以有名字:

var foo = function _foo() {
  ...
};

这里要注意的是,在FE的外部可以通过变量“foo”——foo()来访问,而在函数内部(比如递归调用),还可以用“_foo”(译者注:但在外部是无法使用“_foo”的)。

当FE有名字的时候,它很难和FD作区分。不过,如果仔细看这两者的定义的话,要区分它们还是很容易的: FE总是在表达式的位置。 如下例子展示的各类ECMAScript表达式都属于FE:

// 在括号中(grouping operator)只可能是表达式
(function foo() {});

// 在数组初始化中 —— 同样也只能是表达式
[function bar() {}];

// 逗号操作符也只能跟表达式
1, function baz() {};

定义中还提到FE是在执行代码阶段创建的,并且不是存储在变量对象上的。如下所示:

// 不论是在定义前还是定义后,FE都是无法访问的
// (因为它是在代码执行阶段创建出来的),

alert(foo); // "foo" is not defined

(function foo() {});

// 后面也没用,因为它根本就不在VO中

alert(foo);  // "foo" is not defined

问题来了,FE要来干嘛?其实答案是很明显的 —— 在表达式中使用,从而避免对变量对象造成“污染”。最简单的例子就是将函数作为参数传递给另外一个函数:

function foo(callback) {
  callback();
}

foo(function bar() {
  alert('foo.bar');
});

foo(function baz() {
  alert('foo.baz');
});

上述例子中,部分变量存储了对FE的引用,这样函数就会保留在内存中并在之后,可以通过变量来访问(因为变量是可以影响VO的):

var foo = function () {
  alert('foo');
};

foo();

如下例子是通过创建一个封装的作用域来对外部上下文隐藏辅助数据(例子中我们使用FE使得函数创建后就立马执行):

var foo = {};

(function initialize() {

  var x = 10;

  foo.bar = function () {
    alert(x);
  };

})();

foo.bar(); // 10;

alert(x); // "x" is not defined

我们看到函数“foo.bar”(通过其[[Scope]]属性)获得了对函数“initialize”内部变量“x”的访问。 而同样的“x”在外部就无法访问到。很多库都使用这种策略来创建“私有”数据以及隐藏辅助数据。通常,这样的情况下FE的名字都会省略掉:

(function () {

  // 初始化作用域

})();

还有一个FE的例子是:在执行代码阶段在条件语句中创建FE,这种方式也不会影响VO:

var foo = 10;

var bar = (foo % 2 == 0
  ? function () { alert(0); }
  : function () { alert(1); }
);

bar(); // 0
“有关括号”的问题

让我们回到本文之初,来回答下此前提到的问题 —— “为什么在函数创建之后立即进行函数调用时,需要用括号将其包起来?”。 要回答此问题,需要先介绍下关于表达式语句的限制。

标准中提到,表达式语句(ExpressionStatement)不能以左大括号{开始 —— 因为这样一来就和代码块冲突了, 也不能以function关键字开始,因为这样一来又和函数声明冲突了。比方说,以如下所示的方式来定义一个立马要执行的函数:

function () {
  ...
}();

// or with a name

function foo() {
  ...
}();

对于这两种情况,解释器都会抛出错误,只是原因不同。

如果我们是在全局代码(程序级别)中这样定义函数,解释器会以函数声明来处理,因为它看到了是以function开始的。 在第一个例子中,会抛出语法错误,原因是既然是个函数声明,则缺少函数名了(一个函数声明其名字是必须的)。

而在第二个例子中,看上去已经有了名字了(foo),应该会正确执行。然而,这里还是会抛出语法错误 —— 组操作符内部缺少表达式。 这里要注意的是,这个例子中,函数声明后面的()会被当组操作符来处理,而非函数调用的()。因此,如果我们有如下代码:

// "foo" 是函数声明
// 并且是在进入上下文的时候创建的

alert(foo); // function

function foo(x) {
  alert(x);
}(1); // 这里只是组操作符,并非调用!

foo(10); // 这里就是调用了, 10

上述代码其实就是如下代码:

// function declaration
function foo(x) {
  alert(x);
}

// 含表达式的组操作符
(1);

// 另外一个组操作符
// 包含一个函数表达式
(function () {});

// 这里面也是表达式
("foo");

// etc

当这样的定义出现在语句位置时,也会发生冲突并产生语法错误:

if (true) function foo() {alert(1)}

上述结构根据标准规定是不合法的。(表达式是不能以function关键字开始的),然而,正如我们在后面要看到的,没有一种实现对其抛出错误, 它们各自按照自己的方式在处理。

讲了这么多,那究竟要怎么写才能达到创建一个函数后立马就进行调用的目的呢? 答案是很明显的。它必须要是个函数表达式,而不能是函数声明。而创建表达式最简单的方式就是使用上述提到的组操作符。因为在组操作符中只可能是表达式。 这样一来解释器也不会纠结了,会果断将其以FE的方式来处理。这样的函数将在执行阶段创建出来,然后立马执行,随后被移除(如果有没有对其的引用的话):

(function foo(x) {
  alert(x);
})(1); // 好了,这样就是函数调用了,而不再是组操作符了,1是参数

要注意的是,在下面的例子中,函数调用,其括号就不再是必须的了,因为函数本来就在表达式的位置了,解释器自然会以FE来处理,并且会在执行代码阶段创建该函数:

var foo = {

  bar: function (x) {
    return x % 2 != 0 ? 'yes' : 'no';
  }(1)

};

alert(foo.bar); // 'yes'

因此,对“括号有关”问题的完整的回答则如下所示:

如果要在函数创建后立马进行函数调用,并且函数不在表达式的位置时,括号就是必须的 —— 这样情况下,其实是手动的将其转换成了FE。 而当解释器直接将其以FE的方式处理的时候,说明FE本身就在函数表达式的位置 —— 这个时候括号就不是必须的了。

另外,除了使用括号的方式将函数转换成为FE之外,还有其他的方式,如下所示:

1, function () {
  alert('anonymous function is called');
}();

// 或者这样
!function () {
  alert('ECMAScript');
}();

// 当然,还有其他很多方式

不过,括号是最通用也是最优雅的方式。

通过Function构造器创建的函数

这类函数有别于FD和FE,有自己的专属特性: 它们的[[Scope]]属性中只包含全局对象:

var x = 10;

function foo() {

  var x = 20;
  var y = 30;

  var bar = new Function('alert(x); alert(y);');

  bar(); // 10, "y" is not defined

}

我们看到bar函数的[[Scope]]属性并未包含foo上下文的AO —— 变量“y”是无法访问的,并且变量“x”是来自全局上下文。 顺便提下,这里要注意的是,Function构造器可以通过new关键字和省略new关键字两种用法。上述例子中,这两种用法都是一样的。

函数创建的算法

如下所示使用伪代码表示的函数创建的算法(不包含联合对象的步骤)。有助于理解ECMAScript中的函数对象。此算法对所有函数类型都是一样的。

F = new NativeObject();

// 属性 [[Class]] is "Function"
F.[[Class]] = "Function"

// 函数对象的原型
F.[[Prototype]] = Function.prototype

// 对函数自身引用
// [[Call]] 在函数调用时F()激活
// 同时创建一个新的执行上下文
F.[[Call]] = <reference to function>

// 内置的构造器
// [[Construct]] 会在使用“new”关键字的时候激活
// 事实上,它会为新对象申请内存
// 然后调用 F.[[Call]]来初始化创建的对象,将this值设置为新创建的对象
F.[[Construct]] = internalConstructor

// 当前上下文(创建函数F的上下文)的作用域名链
F.[[Scope]] = activeContext.Scope
// 如果是通过new Function(...)来创建的,则
F.[[Scope]] = globalContext.Scope

// 形参的个数
F.length = countParameters

// 通过F创建出来的对象的原型
__objectPrototype = new Object();
__objectPrototype.constructor = F // {DontEnum}, 在遍历中不能枚举
F.prototype = __objectPrototype

return F

要注意的是,F.[[Prototype]]是函数(构造器)的原型,而F.prototype是通过该函数创建出来的对象的原型(因为通常对这两个概念都会混淆,在有些文章中会将F.prototype叫做“构造器的原型”,这是错误的)。

总结

本文介绍了很多关于函数的内容,下一章将会介绍有关闭包的内容。

参考

此系列文章为翻译原作者Dmitry Soshnikov的系列文章[ECMA-262-3 in detail]。

原文参考地址:ECMA-262-3 in detail. Chapter 5. Functions.


同系列文章下一篇:[JavaScript]ECMA-262-3 深入解析.第五章.闭包

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值