0%

JS入门:函数

函数定义

函数是执行特定任务的代码块。

函数是“可调用对象”,仅当有代码调用时其中的代码才被执行。

1
2
3
4
// 函数定义一(函数声明)
function funcName(param1, param2, ...) {
// 执行代码
}

以上就是函数声明语法。它包括:关键字 function、函数名、参数列表和函数体。

对于 Java 而言,是以方法签名来区分所调用的方法,所谓方法签名=方法名+参数列表。

而 JavaScript 与之不同,仅以函数名来区分所调用的函数,与参数列表无关。换言之,同一个函数可以传入任何数量的参数。即是说,由于没有签名,所以 JavaScript 不直接支持函数重载。

另外,还可以通过名为“函数表达式”的方式定义函数:

1
2
3
4
5

// 函数定义二(函数表达式)
const func = function funcName(param1, param2, ...) {
// 执行代码
}

本质上,这仅仅是声明了一个函数,并把它赋值给了一个变量(ES6 中通常赋值给常量)。当然,赋值号右边的函数名是可以省略的,除非需要在其函数体内递归调用自身。

将一个函数赋值给变量在 JavaScript 中是可行的,因为,在 JavaScript 中函数是“一等公民”,本质上也是一种数值。

除了这两种定义方式外,还可以使用 Function 构造函数来定义函数:

1
2
// 函数定义三(构造函数)
const func = new Function(param1, param2, ..., funcBody);

Function 构造函数的最后一个参数是要创建的函数对象的函数体,之前的参数都是函数对象的传入参数。

很少会使用构造函数来创建函数,因为其性能比较差。

函数调用

与大多数语言类似,JavaScript 也是在函数名后追加一对圆括号来进行函数调用,函数的参数置于括号之中。

1
functionName(param1, param2, ...);

与 Java 类语言不同的是,JavaScript 函数调用时参数传递限制更为宽松。虽然仍需要依序传入参数,但是,不必传入所有参数,也可以传入更多参数。

编写代码时,函数通常都是先定义后调用。但是,也可以在函数定义的同时调用它,通常这称为,立即执行函数表达式(IIFE,Immediately Invoked Function Expression),即在声明的同时执行该函数。

常见形式有:

1
2
3
4
// 形式一
(function(){...})();
// 形式二
(function(){...}());

显然,IIFE 中的函数只能被执行一次,后续代码将没有办法再引用到它,更别说执行了。

但是,使用 IIFE 的目的通常是为了创建一个局部的作用域,以免局部变量扩散到外层作用域(通常是全局)中。

使用 IIFE 的根本原因在于:起初,JavaScript 没有块级作用域的概念,只有全局作用域和函数作用域。

因此,IIFE 是借用函数作用域模仿块级作用域的一种常用方式。而 ES6 加入了块级作用域后,IIFE 该方面的使用应当会减少。

函数对象

本质上讲,函数还是一个对象,因此,函数可拥有属性,其 length 属性表示其参数个数。

注意,这里指的是函数参数列表中参数(形参)的个数,而不是调用时传入的参数(实参)个数。

函数对象还有 toString()valueOf() 两个方法,它们均返回函数的字符串表示。

注意区分“函数”与“方法”在概念上的差异,广义地说,“方法”是一个面向对象的概念,指的是与类或对象相关联的函数。

因此,在 Java 这类面向对象的语言中,总是说“方法”。而在 JavaScript 中,大部分是“函数”,仅当函数作为对象属性时才称为“方法”。

闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。闭包是对作用域的引用。闭包是一个关于如何在函数作为值按需传递的词法环境中书写代码的标准。

只要使用了回调函数,实际上就是在使用闭包。

闭包是词法作用域和作为值的函数两者的直接结果之一。

通常,除非函数被传播到其作用域之外执行,否则即使产生了闭包也并未使用,而仅仅使用了普通的变量查询而已。

附录

函数声明 vs. 函数表达式

从效果上讲,普通声明方式与函数表达式存在一些差异:

  • 函数声明不能省略函数名;函数表达式函数可以是匿名的。
  • 函数声明会提升(hoisting);函数表达式不会。

一个简单的区分方法是:在整个声明中,如果 function 是第一个词,则是函数声明,否则是函数表达式。

所谓“提升”,是指函数声明会在当前作用域任何代码被执行前首先被处理,就好像声明被移动到了作用域的最开始。但是只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。因此,函数表达式不会被提升,过早使用会引起 TypeError

所有声明(ES6 中的 letconst除外)都会被提升,但函数首先被提升,然后才是变量。因此,要注意重复声明可能引发的问题。

词法作用域

在 JavaScript 中,每个函数都有自己的作用域。作用域基本上是变量的一个集合以及如何通过名称访问这些变量的规则。只有函数内部的代码才能访问这个函数作用域中的变量。

作用域分为词法作用域和动态作用域,JavaScript 使用前者。JavaScript 的词法作用域是基于编译器语义(非解释器)。

作用域查找会在找到每一个匹配的标识符时停止。并且只会查找一级标识符(意即不会查找属性链形式访问的属性)。

可以通过 evalwith 欺骗词法作用域,但会导致性能下降,因为编译时将无法优化作用域查询。

非全局的变量如果被遮蔽了,将无法被访问到。

ES3+,catch 分句会创建一个块作用域。

let 定义循环变量时,变量在每次迭代都会声明。

杂项

Function.prototype 本身就是一个没有操作的空函数,因此,可以作为无操作的回调。