1. 函数声明和函数表达式

1.1 函数声明

函数声明的语法如下:

  1. function functionName(arg0,arg1,arg2){
  2. //...
  3. }

首先是function关键字,然后是函数的名字,firefox safari chrome opera都给函数定义了一个非标准的name属性,通过这个属性可以访问到给函数指定的名字.这个属性的值永远等于跟在function关键字后边的标识符.关于函数声明,它的一个主要特征就是函数声明提升,意思是在执行代码之前会先读取函数声明.这就意味着可以把函数声明放在调用它的语句后边.

  1. sayHi();
  2. function sayHi(){
  3. alert("Hi!");
  4. }

1.2 函数表达式

函数表达式的语法如下:

  1. var functionName = function(arg0,arg1,arg2){
  2. //函数体
  3. }

这种情况下创建的函数叫做匿名函数,因为function关键字后面没有标识符.匿名函数的name属性为空字符串.函数表达式不会函数声明提升

2. 函数参数

2.1 arguments

2.1.1 定义

ECMAScript中函数的参数在内部用一个类数组来表示,在函数体内通过arguments对象来访问这个参数列表。
非严格模式下,传入的参数,形参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享,如果是在严格模式下,实参和 arguments 是不会共享的。

  1. function foo(name, age, sex, hobbit) {
  2. console.log(name, arguments[0]); // name name
  3. // 改变形参
  4. name = 'new name';
  5. console.log(name, arguments[0]); // new name new name
  6. // 改变arguments
  7. arguments[1] = 'new age';
  8. console.log(age, arguments[1]); // new age new age
  9. // 测试未传入的是否会绑定
  10. console.log(sex); // undefined
  11. sex = 'new sex';
  12. console.log(sex, arguments[2]); // new sex undefined
  13. arguments[3] = 'new hobbit';
  14. console.log(hobbit, arguments[3]); // undefined new hobbit
  15. }
  16. foo('name', 'age')

2.1.2 arguments.length

Arguments对象的length属性,表示实参的长度,举个例子:

  1. function foo(a,b,c) {
  2. console.log(foo.length) //形参长度
  3. console.log(arguments.length) //实参长度
  4. }

2.1.3 arguments.callee

通过arguments.callee属性可以调用arguments所属的函数.

2.1.4 箭头函数的arguments

如果函数是使用箭头函数定义的,那么传给函数的参数将不能使用arguments关键字访问,而只能通过定义的明明参数访问。


2.2 默认参数值

ECMAScript 5.1及以前,实现默认参数的常用方式是检测某个参数是否等于undefined,如下:

  1. function makeKing(name) {
  2. name = (typeof name !== 'undefined') ? name: 'Henry'
  3. return `King ${name} VIII`
  4. }

ES6支持显式定义默认参数,可以通过以下方式完全替代上述方案

  1. function makeKing(name = 'Henry') {
  2. return `King ${name} VIII`
  3. }

注意事项:

  • 使用默认参数时,arguments对象的值不反映参数的默认值,只反映传给函数的参数
  • 默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值,计算默认值的函数只有在函数调用但未传入相应参数时才会调用
  • 箭头函数同样也可以使用默认参数,只不过在只有一个参数时,必须使用括号而不能省略
  • 参数初始化顺序遵循暂时性死区规则,前面定义的参数不能引用后边定义的
  • 参数也存在于自己的作用域中,它们不能引用函数体的作用域

2.3 参数的扩展与收集

2.3.1 扩展参数

在给函数传参时,有时可能不需要传入一个数组,而是分别传入数组的每个元素,如下所示:

  1. let values = [1, 2, 3, ,4]
  2. function getSum() {
  3. let sum = 0
  4. for(let i = 0; i < arguments.length; i++) {
  5. sum += arguments[i]
  6. }
  7. return sum;
  8. }

ES5通过apply解决:

  1. getSum.apply(null,values)

ES6可以通过扩展操作符更为简洁的实现这种操作

  1. getSum(...values)

2.3.2 收集参数

在函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组,收集参数的前面如果还有命名参数,则只会收集其余的参数,如果没有则会得到空数组,因为收集参数的结果可变,所以只能把它作为最后一个参数
注意:箭头函数不支持arguments,但支持收集参数的定义方式,因此也可以实现与使用arguments一样的逻辑:

  1. let getSum = (...values) => {
  2. return values.reduce((x,y) => x + y, 0)
  3. }

3. 参数传递

ECMAScript中所有函数的参数都是按值传递的。分两种情况讨论。
1.参数为值类型,直接把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样.
2.当值为一个引用类型时,传递的是对象引用的副本,也就是说,虽然函数参数接受到的数据和外部变量指向同一块堆内存,但是两者本身不是同一个指针,这和引用传递有本质的区别,引用传递会将指针本身进行传递。示例如下:

Example1

  1. let obj = {
  2. "name":"zhangsan"
  3. }
  4. function changeObj(arg) {
  5. arg.name = "lisi"
  6. }
  7. changeObj(obj)
  8. console.log(obj.name) //lisi

Example2

  1. let obj = {
  2. "name":"zhangsan"
  3. }
  4. function changeObj(arg) {
  5. arg = 2
  6. }
  7. changeObj(obj)
  8. console.log(obj.name) //zhangsan

图解说明:

4. 没有重载

由于不存在函数签名的特性,ECMAScript函数不能重载。如果在ECMAScript中定义了两个名字相同的函数,则该名字只属于后定义的函数。

5. 内部属性

5.1 callee

指向arguments对象所在函数的指针,一般用于递归解耦

  1. function factorial(num) {
  2. if(num <= 1) {
  3. return 1
  4. }else{
  5. return num * arguments.callee(num - 1)
  6. }
  7. }

5.1 caller

这个属性保存着调用当前函数的函数的引用。如果在全局作用域中调用当前函数,它的值为null。

  1. function outer(){
  2. inner()
  3. }
  4. function inner(){
  5. console.log(arguments.callee.caller) //outer function
  6. }
  7. outer()

5.2 length

函数的length属性表示该函数的形参数量。arguments.length表示实参个数

5.3 new.target

js中的函数始终可以作为构造函数实例化为一个新对象,也可以作为普通函数调用,ES6新增了检测函数是否使用new关键字调用的new.target属性,如果函数是正常调用的,则new.target的值是undefined,如果使用new关键字调用,则new.target将引用被调用的构造函数。

6. 尾调用优化

ES6新增的内存管理优化机制,让js引擎在满足条件时可以重用栈帧。具体来说就是外部函数的返回值是一个内部函数的返回值,尾调用优化的条件就是确定外部栈帧没有必要存在而及时将其销毁,避免了多层函数嵌套导致执行栈上过多的堆积,造成资源浪费。
尾调用优化的条件如下:

  1. 函数使用严格模式
  2. 外部函数的返回值是对尾调用函数的调用
  3. 尾调用函数返回时不需要执行额外的逻辑
  4. 尾调用函数不是引用外部函数作用域中自由变量的闭包。

6.1 递归尾调用

无论是递归尾调用还是非递归尾调用,都可以应用尾调用优化,不过这个优化在递归场景下的效果是最明显的,因为递归代码最容易在栈内存中迅速产生大量栈帧。
以下举例说明将一个斐波那契数列递归调用转换为递归尾调用的代码

未使用尾递归优化的代码

  1. function fib(n) {
  2. if(n < 2) {
  3. return n
  4. }else{
  5. return fib(n-1) + fib(n-2)
  6. }
  7. }

更改为尾递归优化后的代码

  1. function fib(n) {
  2. return fibImp(0, 1, n)
  3. }
  4. function fibImp(a, b, n) {
  5. if(n === 0){
  6. return a
  7. }else{
  8. return fibImp(b, a + b, n-1)
  9. }
  10. }