前端入门09 -- JavaScript之函数,作用域链,闭包

函数

  • 函数也是一个对象;
  • 函数:把一个或者多功能通过函数的方式封装起来,对外只提供一个简单的函数接口;
  • 声明定义函数的三种方式:
    • 利用函数关键字function 自定义函数;
    • 函数表达式,用一个变量接收函数表达式;
    • 箭头函数,用一个变量接收定义的箭头函数;
    <script>
        ///1.function关键字
        ///addNum是一个函数
        function addNum(a,b) {
            return a + b
        }
        addNum(100,200)
        ///2.函数表达式 
        ///func是变量名 不是函数名 后面的表达式才是函数 属于匿名函数
        var func = function(x) {
            console.log(x)
        }
        func('yanzi')

        ///3.箭头函数
        var func2 = () => {
            console.log('我是一个箭头函数');
        }
        func2();
    </script>
    <script>
        //定义声明函数
        function getSum() {
            var sum = 0
            for (var i = 0; i < 100; i++) {
                sum += i
            }   
            console.log(sum);
        }
        //调用函数
        getSum()
    </script>

函数的参数

  • 行参:形式上的参数,函数定义的时候传递的参数 当前并不知道是什么;
  • 实参:实际上的参数,函数调用的时候传递的参数 实参是传递给行参的;
  • 函数的参数可以有,也可以没有个数不限;
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        //定义声明函数
        function getSum(num1,num2) {
            console.log(num1 + num2)
        }
        //调用函数
        getSum(100,200)
    </script>
</head>
<body>
</body>
</html>
  • 函数形参实参的个数匹配问题:
    • 如果实参的个数和形参的个数一致,则正常输出结果;
    • 如果实参的个数多于形参的个数,会取到形参的个数;
    • 如果实参的个数小于形参的个数,多于的形参定义为undefined,最终的结果就是NaN;
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        //定义声明函数
        function getSum(num1,num2) {
            console.log(num1 + num2)
        }
        getSum(100,200) //300
        getSum(100,200,300) //300
        getSum(100) //NaN
    </script>
</head>
<body>
</body>
</html>
箭头函数的参数与参数默认值
  • 箭头函数中只有一个参数时,可以省略();
  • 箭头函数中代码只有一行时,可以省略{};
  • 参数没有传入实参时,参数默认值才会生效;
  <script>

    const fn = (a,b) => {
      console.log('a =',a);
      console.log('b =',b);
    }

    //省略() 与 {}
    const fn1 = a => console.log('a =',a);

    //参数默认值
    const fn2 = (a,b,c = 30) => {
      console.log('a =',a);
      console.log('b =',b);
      console.log('c =',c);
    }

  </script>
Object对象作为函数参数
  • Object对象可以作为函数的参数;
  <script>

    function fn(a) {
      console.log('a =', a);
    }

    let obj = { name: 'yanzi' };
    fn(obj);

  </script>
  • Object对象字面量作为函数参数默认值,函数多次调用,会多次创建新的Object对象字面量;
  • 自定义Object对象作为函数参数默认值,函数多次调用,都使用的同一个自定义Object对象;
  <script>

    function func(a = { name: '大神' }) {
      console.log(a.name);
      a.name = '小神';
      console.log(a.name);
    }
    func(); // 大神 小神
    func(); // 大神 小神

    let obj = { name: '大神' }
    function func1(a = obj) {
      console.log(a.name);
      a.name = '小神';
      console.log(a.name);
    }
    func1(); // 大神 小神
    func1(); // 小神 小神

  </script>
函数作为函数参数
  • 自定义函数,匿名函数表达式,箭头函数作为函数的参数;
  <script>

    function func(a) {
      console.log('a =', a);
    }

    //自定义函数
    function func1() {
      console.log('自定义函数');
    }

    func(func1);

    //匿名函数表达式
    func(function () {
      console.log('匿名函数表达式');
    })

    //箭头函数
    func(() => {
      console.log('箭头函数');
    })

  </script>
arguments的使用
  • 当我们不确定有多少个参数传递的时候,可以用arguments来获取,在JavaScript中,arguments实际上它是 当前函数的一个内置对象,所有函数都内置了一个arguments对象,arguments对象中存储了传递的所有实参
  • arguments展示形式是一个伪数组,因此可以遍历,伪数组具有以下特点:
    • 具有length属性;
    • 按索引方式储存数据;
    • 不具有数组的push,pop等方法;
    <script>
        function fn() {
            console.log(arguments)
            console.log(arguments.length)
            console.log(arguments[2])
            for (var i = 0; i < arguments.length; i++) {
                console.log(arguments[i]); //10,2,33
            }
        }
        fn(10,2,33)
    </script>
函数返回值
  • 函数的返回值:只要函数遇到return 就把后面的结果 返回给函数的调用者,且终止之后的代码执行,且只能返回一个值,是没有返回值类型的;
  • return什么都不返回 或者 不写return,那么函数的返回值为undefined;
    <script>
        //定义声明函数
        function getArrMax(arr) {
            var max = arr[0]
            for (var i = 1; i < arr.length; i++) {
                if (arr[i] > max) {
                    max = arr[i]
                }
            }
            return max
        }

        var max = getArrMax([100,200,30,40,50,333,234])
        console.log(max);
    </script>
    <script>
        function getResult(num1,num2) {
            //返回值为数组
            return [num1+num2,num1-num2]
        }
        var result = getResult(100,30)
        console.log(result);
    </script>
  • 箭头函数只有一行代码,且有返回值,可省略return关键字 和 {};
  • 箭头函数只有一行代码,且返回值为Object对象字面量,那么需要加上(Object对象字面量);
  <script>

    const func = (a, b) => {
      return a + b;
    }

    const func1 = (a, b) => a + b;

    const func2 = (a, b) => ({ name: 'yanzi' });

  </script>

JavaScript作用域

  • 全局作用域:整个script标签 或者是一个单独的js文件;
  • 局部作用域:存在两种分别为块级作用域和函数作用域;
    • 块级作用域:在代码块执行时创建,在代码块执行完毕时销毁;
    • 函数作用域:在函数调用时创建,在函数调用完成时销毁;
  • 全局变量:在全局作用域中的变量,注意在函数内部 没有声明直接赋值的变量 也属于全局变量,全局变量只有在浏览器关闭的时候才会被销毁,比较占内存资源;
  • 局部变量:在局部作用域中的变量,当函数执行完成时就会被销毁,比较节约内存资源;
    <script>
        //全局作用域 全局变量
        var num = 100

        function addNum(a,b) {
            //局部作用域 局部变量
            var sum = 0
            sum = a + b
            //sum1没有声明直接赋值 属于全局变量
            sum1 = 1000
            return sum
        }
        addNum(100,200)
    </script>
  • 作用域链:内部函数访问外部函数变量,JS解释器会优先在当前对象中寻找目标变量,若找到直接使用,若没有找到,则去上一层作用域中去寻找,依次类推,若找到全局作用域都没有找到目标变量,就会报错xxx is not defined,在作用域中寻找目标变量,遵循就近原则
  <script>

    var num = 10
    function func() { //外部函数
      var num = 20
      function func1() { //内部函数
        console.log(num); // 20
      }
    }

    let a = '全局作用域中的a';
    {
      let a = '第一个块级作用域中的a';
      {
        let a = '第二个块级作用域中的a';
        console.log('a =', a);
      }
    }

  </script>

JavaScript预解析

  • JavaScript代码是由浏览器中的JavaScript解析器来执行的,JavaScript解析器在运行JavaScript代码的时候分为两个步骤,分别为:预解析代码执行
  • 预解析:js引擎会将js里面所有的var和function 提升到当前作用域的最前面
  • 代码执行:按照代码书写的顺序从上到下依次执行;
  • 变量var的提升:将所有变量var声明提升到当前作用域的最前面,不会提升赋值操作
  • 函数的提升:将所有函数声明(定义)提升到当前作用域的最前面,不调用函数;
  • 变量let的提升:会将所有变量let声明提升到当前作用域的最前面,但是在赋值之前解释器禁止对该变量的访问;
  <script>

    console.log(a); //a is not defined
    a = 100;

    console.log(b); //undefined
    var b = 100;

    console.log(c); //Cannot access 'c' before initialization
    let c = 100;

  </script>
    <script>
        //第一种:
        console.log(num) //报错

        //第二种:
        console.log(num) //undefine
        var num = 10

        //第三种
        func() //111
        function func() {
            console.log(111)
        }

        //第四种
        fun() //报错
        var fun = function() {
            console.log(222)
        }
    </script>
  • 案例一:
    <script>
        var a = 18
        f1()

        function f1() {
            var b = 9
            console.log(a)
            console.log(b)
            var a = '123'
        }

        //上述代码等价于
        var a
        function f1() {
            var b 
            var a
            b = 9
            console.log(a) //undefine 
            console.log(b) //9
            a = '123'
        }
        a = 18
        f1()
    </script>
  • 案例二:
    <script>
        f1()
        console.log(c)
        console.log(b)
        console.log(a)

        function f1() {
            // var a = 9; b = 9;c = 9 b与c没有声明 直接赋值 属于全局变量
            // var a = b = c = 9 与 var a = 9,b = 9, c = 9 是不同的
            var a = b = c = 9
            console.log(a)
            console.log(b)
            console.log(c)
        }
        
        //以上代码等价于
        function f1() {
            var a
            a = b = c = 9
            console.log(a) //9
            console.log(b) //9
            console.log(c) //9
        }

        f1()
        console.log(c) //9
        console.log(b) //9
        console.log(a) //报错 a是局部变量
    </script>

立即执行函数

  • 在开发中应该尽量减少直接在全局作用域中编写代码,尽量在局部作用域中编写代码;
  • 立即执行函数:(function(){})(),是一个匿名函数,直接执行,且只会执行一次;
  <script>

    (function(){
      console.log('立即执行函数');
    })()

  <script>

函数中的this关键字

  • 函数在执行时,JS解析器每次都会传递一个隐含参数,此参数叫做this;
  • this会指向一个对象,其所指的对象会根据函数调用方式的不同而不同;
    • 以函数形式调用时,this指向的是window;
    • 以方法形式调用时,this指向的是调用方法的对象;
  <script>

    function func() {
      console.log(this);
    }
    //以函数的形式调用时,this指向window
    func(); //window

    //以方法的形式调用时,this指向调用方法的对象
    const obj = { name: '小神' };
    obj.test = func;
    obj.test(); //obj

    const obj2 = { name: '大神', test: func };
    obj2.test(); //obj2

  </script>
  • 箭头函数没有自己的this,内部打印的this是由外层作用域决定的,等于外层作用域的this;
  • 箭头函数的this和它的调用方式无关;
<script>

    function func() {
      console.log(this);
    }
    //以函数的形式调用时,this指向window
    func(); //window

    //箭头函数
    const func3 = (a, b) => {
      var num = a + b;
      console.log(this);
      return num;
    }
    func3(); //外层作用域为全局作用域 this指向window

    const obj3 = {
      name: '如来',
      fn: func,
      fn2: func3,
      sayHello() {
        console.log(this.name);
        function t() {
          console.log('t -->', this);
        }
        t(); //window

        const t2 = () => {
          console.log('t2 -->', this);
        }
        t2(); //obj3
      }
    }
    obj3.fn();  //obj3
    obj3.fn2(); //window

    obj3.sayHello();

  </script>

高阶函数

  • 高阶函数:若一个函数的参数或者返回值为函数,那么这个函数就是高阶函数;
  • 将函数作为参数,就意味着可以对另一个函数动态的传递代码;
  <script>

    //高阶函数 -- 参数为一个函数
    function filter(arr, cb) {
      const newArr = [];
      for (let index = 0; index < arr.length; index++) {
        const element = arr[index];
        if (cb(arr[index])) {
          newArr.push(arr[index]);
        }
      }
      return newArr;
    }

    const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    var result = filter(arr, a => a % 2 === 0);
    console.log(result);

  </script>

闭包

  • 闭包:能访问到外部函数作用域中变量的函数;
  • 构成闭包的必要条件:
    • 存在函数的嵌套;
    • 内部函数要引用外部函数的变量;
    • 内部函数要作为返回值;
  <script>

    function outer() {
      let num = 0;
      return () => {
        num++;
        console.log(num);
      }
    }

    const newFn = outer;
    newFn();

  </script>
  • 函数的外层作用域(词法作用域),在函数创建时就已经确定了;
  • 闭包的实现原理就是利用了词法作用域;
  <script>

    let a = '全局变量a';
    function fn() {
      console.log(a);
    }

    function fn2() {
      let a = 'fn2中的a';
      fn();
    }
    fn2();  //fn() --> 打印的是全局变量a 

    function fn3() {
      let a = 'fn3中的a';
      function fn4() {
        console.log(a);
      }
      return fn4;
    }

    let result = fn3();
    fn4(); //fn4() --> 打印的是fn3中的a
    
  </script>
  • 闭包的生命周期:
    • 闭包在外部函数调用时产生,外部函数每次调用都会产生一个全新的闭包;
    • 闭包在内部函数回收时销毁;
  • 闭包的注意事项:
    • 闭包住要用来隐藏一些不希望外部访问的内容,这就意味着闭包需要占用一定的内存空间;
    • 相比较于类Class来说,闭包比较浪费内存空间,因为类可以使用原型而闭包不可以;
  <script>

    function outer() {
      let someValue = 'some';
      return function () {
        console.log(someValue);
      }
    }

  </script>

推荐阅读更多精彩内容