2-22 函数

208 阅读14分钟

1.函数概念

函数(function),也叫作功能、方法,函数可以将一段代码一起封装起来,被封装起来的函数具备某一项特殊的功能,内部封装的一段代码作为一个完整的结构体,要执行就都执行,要不执行就都不执行。

函数的作用就是封装一段代码,将来可以重复使用。

2.函数声明

  • 函数声明又叫函数定义,函数必须先定义然后才能使用。 如果没有定义函数直接使用,会出现一个引用错误。

  • 函数声明语法:

    function 函数名(参数){
    		封装的结构体;
    	}
    
  • 特点:函数声明的时候,函数体并不会执行,只有当函数被调用的时候才会执行。

3.函数调用

  • 调用方法:函数名();
  • 函数调用也叫作函数执行,调用时会将函数内部封装的所有的结构体的代码立即执行。
  • 函数内部语句执行的位置,与函数定义的位置无关,与函数调用位置有关。
  • 函数可以一次定义,多次执行。

4.函数的参数

  • 函数执行结果不是一成不变的,可以根据自定义的内容发生一些变化。
  • 函数预留了一个接口,专门用于让用户自定义内容,使函数发生一些执行效果变化。
  • 接口:就是函数的参数,函数参数的本质就是变量,可以接收任意类型的数据,导致函数执行结果根据参数不同,结果也不同。
  • 一个函数可以设置 0 个或者多个参数,参数之间用逗号分隔
    // 定义一个求和函数,传入两个数据
    // 参数:传两个参数,数据类型为数字
    // 功能:得到两个数字之和
    function sum(a,b) {
      console.log(a + b);
    }
    // 调用函数中,给小括号内部添加数据
    sum(3,4);
    sum("3",4);
  • 形式参数:定义的 () 内部的参数,叫做形式参数,本质是变量,可以接收实际参数传递过来的数据。简称形参。
  • 实际参数:调用的 () 内部的参数,叫做实际参数,本质就是传递的各种类型的数据,传递给每个形参,简称实参。
  • 函数执行过程,伴随着传参的过程:

参数优点:

不论使用自己封装的函数,还是其他人封装的函数,只需要知道传递什么参数,执行什么功能,没必要知道内部的结构什么。 一般自己封装的函数或者其他人封装的函数需要有一个 API 接口说明,告诉用户参数需要传递什么类型的数据,实现什么功能。

5.函数的返回值

函数能够通过参数接收数据,也能够将函数执行结果返回一个值。 利用函数内部的一个return的关键字设置函数的返回值。

  • 作用 ①:函数内部如果结构体执行到一个 return 的关键字,会立即停止后面代码的执行。
  • 作用 ②:可以在 return 关键字后面添加空格,空格后面任意定义一个数据字面量或者表达式,函数在执行完自身功能之后,整体会被 return 矮化成一个表达式,表达式必须求出一个值继续可以参与程序,表达式的值就是 return 后面的数据。

函数返回值的应用

函数如果有返回值,执行结果可以当成普通数据参与程序。 函数如果有返回值,可以作为一个普通数据赋值给一个变量,甚至赋值给其他函数的实际参数。

注意

如果函数没有设置 return 语句 ,那么函数有默认的返回值 undefined;如果函数使用 return 语句,但是 return 后面没有任何值,那么函数的返回值也是 undefined。

    // 定义一个求和函数,传入两个数据
    // 参数:传两个参数,数据类型为数字
    // 功能:得到两个数字之和
    // 使用返回值,制作函数运行结束后的结果
    function sum(a,b) {
      return a + b;
    }
    // 调用函数
    // console.log(sum(1,2));
    // 将返回值赋值给变量
    var num = sum(3,4);
    console.log(num);
    // 将返回值赋值给函数的实参
    console.log(sum(2,sum(3,4)));
    // return 可以终止函数的执行
    // function fun() {
    //   console.log(1);
    //   console.log(2);
    //   console.log(3);
    //   return;
    //   console.log(4);
    //   console.log(5);
    //   console.log(6);
    // }
    // // 函数调用
    // fun();

6.函数表达式

函数表达式是函数定义的另外一种方式。

  • 定义方法:就是将函数的定义、匿名函数赋值给一个变量。
  • 函数定义赋值给一个变量,相当于将函数整体矮化成了一个表达式。
  • 匿名函数:函数没有函数名。
  • 调用函数表达式,方法是给变量名加()执行,不能使用函数名加()执行。
    // 定义一个函数表达式
    var foo = function fun() {
      console.log(1);
    };
    var foo2 = function () {
      console.log(2);
    };
    // 调用函数式,只能用变量名调用,函数名调用不成功
    // fun();
    foo();
    foo2();

7.函数的数据类型

  • 函数是一种单独的数据类型 Function,属于Obeject复杂类型之一
  • 由于函数是一种数据类型,可以参与其他程序。 例如,可以把函数作为另一个函数的参数,在另一个函数中调用。 或者,可以把函数可以作为返回值从函数内部返回。
    // 定义一个函数
    function fun() {
      console.log(1);
    }
    // 定义一个函数表达式
    var foo = function () {
      console.log(2);
    };
    // 检测函数的数据类型
    console.log(typeof(fun));
    console.log(typeof(foo));
    
    // 函数是一种数据类型,可以当成其他函数的参数
    // setInterval(function(){
    //   console.log(1);
    // },1000);

    // 将函数当成另一个函数的返回值
    function fn(b) {
      var a = 10;
      return function () {
        alert(a + b);
      };
    }

8.函数的arguments对象

JavaScript 中,arguments 对象是比较特别的一个对象,实际上是当前函数的一个内置属性。

也就是说所有函数都内置了一个 arguments 对象,arguments 对象中存储了传递的所有的实参。

arguments 是一个伪数组,因此及可以进行遍历。

函数的实参个数和形参个数可以不一致,所有的实参都会存储在函数内部的 arguments 类数组对象中。

    // 定义一个函数
    function sum(a,b) {
      return a + b;
    }
    // // 调用函数的时候,实参的个数可以与形参不同
    console.log(sum(1,2));
    console.log(sum(1));
    console.log(sum(1,2,3,4));

    // 函数内部有一个 arguments 对象,会接收所有的实参
    function fun() {
      console.log(arguments);
      console.log(arguments.length);
      // 使用数组的遍历方法可以获取每一项实参
      for (var i = 0 ; i <= arguments.length - 1 ; i++) {
        console.log(arguments[i]);
      }
    }
    // // 调用函数
    fun(1,2,3,4,5,6,7);

    // 案例:定义一个求和函数,如果传入 1 个参数,返回它自己,如果传入两个参数,返回他们的和,如果传入三个参数,先比较前两个的大小,大的与第三个参数求和返回,如果传入 4 个及以上,输出错误提示。
    function sum(a,b,c) {
      // 条件分支语句,根据实参个数走不同的分支
      switch (arguments.length) {
        case 1:
          return a;
          break;
        case 2:
          return a + b;
          break;
        case 3:
          return a > b ? a + c : b + c ;
          break;
        default:
          // 提示用户,实参个数传递错误
          // 模拟控制台报错
          throw new Error("参数个数不能超过 3 个");
      }
    }
    // 调用函数
    console.log(sum(1));
    console.log(sum(1,2));
    console.log(sum(1,2,3));
    console.log(sum(1,2,3,4,5));

9.函数递归

函数内部可以通过函数名调用函数自身的方式,就是函数递归现象。 递归的次数太多容易出现错误:超出计算机的计算最大能力。 更多时候,使用递归去解决一些数学中的现象。 例如可以输出斐波那契数列的某一项的值

    // 函数,如果 传入的参数是1,返回1,如果传入的是1以上的数字,让他返回参数 + 函数调用上一项
    function fun(a) {
      if (a === 1) {
        return 1;
      } else {
        return a + fun(a - 1);
      }
    }
    // 调用函数
    console.log(fun(1));
    console.log(fun(2));
    console.log(fun(3));
    // console.log(fun(1000000000));


    // 菲波那切数列
	// 输出斐波那契数列的某一项的值。
// 斐波那契数列:后面的一项数据是前两项数据之和。1,1,2,3,5,8,13,21,34,55……
    // 参数:正整数
    // 返回值:对应的整数位置的菲波那切数列的值
    function fibo(a) {
      if (a === 1 || a === 2) {
        return 1;
      } else {
        return fibo(a - 1) + fibo(a - 2);
      }
    }
    // 调用函数
    console.log(fibo(1));
    console.log(fibo(2));
    console.log(fibo(3)); // 2
    console.log(fibo(4)); // 3
    console.log(fibo(5)); // 5
    console.log(fibo(6)); // 8

10.作用域

作用域:变量可以起作用的范围。

  • 如果变量定义在一个函数内部,只能在函数内部被访问到,在函数外部不能使用这个变量,函数就是变量定义的作用域。
  • 任何一对花括号 {} 中的结构体都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
  • 在es6之前没有块级作用域的的概念,只有函数作用域,现阶段可以认为 JavaScript 没有块级作用域.
    var a = 0;
    // // 定义函数
    function fun() {
      var a = 1;
      console.log(a);
    }
    // // 执行函数
    fun();
    // 函数外部调用 a
    console.log(a);

    // 1. 函数的参数也是局部变量
    function fun(a) {
      a = 2;
      console.log(a);
    }
    // // 调用函数
    fun(1);
    console.log(a);

    // 2. 函数也有自己的作用域
    function outer() {
      var a = 1;
      function inner() {
        console.log(2);
      }
      // 函数内部调用子函数才能成功
      inner();
    }
    // 调用函数
    outer();
    // inner();

1.全局变量、局部变量

  • 局部变量:定义在函数内部的变量,只能在函数作用域内部被访问到,在外面没有定义的。
  • 全局变量:从广义上来说,也是一种局部变量,定义在全局的变量,作用域范围是全局,在整个 js 程序任意位置都能够被访问到。
  • 变量退出作用域之后会销毁,全局变量关闭网页或浏览器才会销毁。
  • 函数的参数也是局部变量
  • 函数也有自己的作用域,函数a内部的另一个函数b的调用,只能写在函数a里,才能调用成功。

2.作用域链

  • 只有函数可以制造作用域结构, 只要是代码,就至少有一个作用域, 即全局作用域。
  • 将这样的所有的作用域列出来,可以有一个结构: 函数内指向函数外的链式结构。就称作作用域链。

    // 全局作用域
    var a = 1;
    // 创建函数
    function outer() {
      var a = 2;
      // 内部函数
      function inner() {
        var a = 3;
        console.log(a);
      }
      inner();
      console.log(a);
    }
    // 调用
    outer();

3.遮蔽效应

程序在遇到一个变量时,使用时作用域查找顺序,不同层次的函数内都有可能定义相同名字的变量,一个变量在使用时,会优先从自己所在层作用域查找变量,如果当前层没有变量定义会按照顺序从本层往外依次查找,直到找到第一个变量定义。

整个过程中会发生内层变量遮蔽外层变量的效果,叫做“遮蔽效应”。

11.不写var关键字的影响

  • 在函数内部想要定义新的变量,如果不加关键字 var,相当于定义的全局变量。如果全局也有相同的标识符,会被函数内部的变量影响,局部变量污染全局变量。

  • 注意:

    每次定义变量时都必须写 var 关键字,否则就会定义在全局,可能污染全局。

    // 全局作用域
    var a = 1;
    // console.log(a);
    // 创建函数
    function outer() {
      a = 2;
      // 内部函数
      function inner() {
        var a = 3;
        console.log(a);
      }
      inner();
      console.log(a);
    }
    // 调用
    outer();
    console.log(a);

12.预解析和声明提升

1.预解析

JavaScript 代码的执行是由浏览器中的 JavaScript 解析器来执行的。JavaScript 解析器执行 JavaScript 代码的时候,分为两个过程:预解析过程代码执行过程预解析过程

1. 把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值。
2. 把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用。
3. 先提升 var,再提升 function

JavaScript 的执行过程:在预解析之后,根据新的代码顺序,从上往下按照既定规律执行 js 代码。

2.变量声明提升

在预解析过程中,所有定义的变量,都会将声明的过程提升到所在的作用域最上面,在将来的代码执行过程中,按照先后顺序会先执行被提升的声明变量过程。

提升过程中,只提升声明过程,不提升变量赋值,相当于变量定义未赋值,变量内存储 undefined 值。

因此,在 js 中会出现一种现象,在前面调用后定义的变量,不会报错,只会使用 undefined 值。

3.函数声明提升

在预解析过程中,所有定义的函数,都会将声明的过程提升到所在的作用域最上面,在将来的代码执行过程中,按照先后顺序会先执行被提升的函数声明过程。

在预解析之后的代码执行过程中,函数定义过程已经在最开始就会执行,一旦函数定义成功,后续就可以直接调用函数。

因此,在 js 中会出现一种现象,在前面调用后定义的函数,不会报错,而且能正常执行函数内部的代码。

    // 预解析,把变量,函数声明部分提升到了作用域最顶部
    // 模拟提升
    var a;  //相当于存了一个undefined的值
    var fun;
    function fun() {
      console.log(2);
    }
    // 调用一个变量
    console.log(a);
    console.log(fun);
    // 后定义变量
    var a = 1;
    a = 1;
    var fun = "haha";
    fun = "haha";
    // 先调用函数
    fun();
    // 定义函数
    function fun() {
      console.log(2);
    }
    // 调用
    fun();

    console.log(foo);
    foo();
    // 函数表达式进行的是变量声明提升
    var foo = function () {
      console.log(3);
    };

4.提升顺序

  • 预解析过程中,先提升 var 变量声明,再提升 function 函数声明。
  • 假设出现变量名和函数名相同,那么后提升的函数名标识符会覆盖先提升的变量名,那么在后续代码中出现调用标识符时,内部是函数的定义过程,而不是 undefined。
  • 如果调用标识符的过程在源代码函数和变量定义后面,相当于函数名覆盖了一次变量名,结果在执行到变量赋值时,又被新值覆盖了函数的值,那么在后面再次调用标识符,用的就是变量存的新值。
  • 建议:不要书写相同的标识符给变量名或函数名,避免出现覆盖。

5.函数表达式的提升

  • 在预解析过程中,函数表达式进行的是变量声明提升,而不是函数声明提升。提升后变量内部存的是一个 undefined。在前面进行函数方法调用,数据类型会提示错误。

  • 建议:定义函数时,最好使用 function 关键字定义方式,这样函数声明提升可以永远生效。

6.函数声明提升的应用

函数声明提升可以用于调整代码的顺序,将大段的定义过程放到代码最后,但是不影响代码执行效果。

13.IIFE 自调用函数

IIFE:immediately-invoked function expression,叫做即时调用的函数表达式,也叫做自调用函数,表示函数在定义时就立即调用。

函数调用方式:函数名或函数表达式的变量名后面加 () 运算符。

函数名定义的形式不能实现立即执行自调用,函数使用函数表达式形式可以实现立即执行,原因是因为函数表达式定义过程中,将一个函数矮化成了一个表达式,后面加()运算符就可以立即执行。

启发:如果想实现 IIFE,可以想办法将函数矮化成表达式。

  • 函数矮化成表达式,就可以实现自调用。
  • 函数矮化成表达式的方法,可以让函数参与一些运算,也就是说给函数前面加一些运算符。 数学运算符:+ - () 逻辑运算符:!非运算
  • IIFE 结构可以关住函数的作用域,在结构外面是不能调用函数的。
  • IIFE 最常用的是 () 运算符,而且函数可以不写函数名,使用匿名函数。
    // 关键字定义的方式,不能立即执行
    function fun() {
      console.log(1);
    }();

    // 函数表达式方式,可以在定义时被立即执行
    var foo = function fun() {
      console.log(2);
    }();

    // 通过在函数前面添加操作符,可以将函数矮化成表达式
    + function fun() {
      console.log(1);
    }();
    - function fun() {
      console.log(1);
    }();
    (function fun() {
      console.log(1);
    })();
    !function fun() {
      console.log(1);
    }();
    // *function fun() {
    //   console.log(1);
    // }();
    // IIFE 关住了函数的作用域,在外面是调用不了函数的
    // fun();

    // 常用的 IIFE 结构
    (function (a) {
      console.log(a);
    })(4);
    // console.log(a);