JS高级03-this指向的三种情况,函数调用的上下文模式(动态修改函数中this的指向),上下文经典场景,闭包的解释

今日学习任务

  • 1.函数的四种调用方式
    • 重点掌握this指向的三种情况
    • a.普通函数
    • b.对象方法
    • c.构造函数
    • d.上下文模式
  • 2.函数调用的上下文模式
    • a.call()
    • b.apply()
    • c.bind()
    • 上下文模式经典场景

01-函数的三种调用方式(this指向)

1.复习函数三种调用方式:普通函数 对象方法 构造函数

  • 重点:理解this关键字作用:谁调用这个函数,this指向谁
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script>
        /* 
        函数三种执行模式 : 全局函数 、 对象方法 、 构造函数
            this : 谁 `调用` 我,我就指向谁
            1. 全局函数 : this指向window
            2. 对象方法 : this指向对象
            3. 构造函数 : this指向new创建的空对象
         */

         
        //1.全局函数
        function fn(){
            console.log('111111');
            console.log(this);
            
            
        };

        fn();//window.fn()


        //2.对象的方法
        let obj = {
            name:'班长',
            sayHi:function(){
                console.log('我是胖胖又可爱的得小又又');
                console.log(this);
                
            }
        };

        obj.sayHi();
        //将fn的地址赋值给sayHi
        obj.sayHi = fn;
        //此时this指向obj,this指向跟声明没有关系。取决于函数是如何调用的
        obj.sayHi();

        //3.构造函数
        function Person(name,age){
            //(1)创建一个空对象  (2)this指向这个对象 (3)执行赋值代码 (4)返回这个对象

            //this :指向new创建的哪个对象
            console.log(this);
            
            this.name = name;
            this.age = age;
        };

        let p1 = new Person();//构造函数

        //没有加new,以全局函数方式执行。此时this就是window,函数里面其实是给window添加属性(全局变量)
        Person('张三',18);//全局函数

        console.log(name);
        console.log(age);
        
    </script>
</body>
</html>

02-函数调用的上下文模式

了解上下文模式注意点

  • a.函数上下文三个方法:call() apply() bind(),它们定义在Function构造函数的原型中
  • b.如果将this指向修改为值类型:(number,boolean,string),则函数会自动帮我们包装成对应的引用类型(基本包装类型)
    • 值类型: '123',1,true
    • 基本包装类型:String('123'),Number(1),Boolean(true)

1.1-函数执行的上下文模式

作用:可以动态修改函数中的this指向

  • call() apply() bind()
    

    异同点

    • 相同之处:都可以修改函数中this指向
    • 不同点:传参方式不同
      • call()语法: 函数名.call(this修改后的指向,arg1,arg2…………)
      • apply()语法:函数名.apply(this修改之后的指向,伪数组或者数组)
        • bind()语法:函数名.bind(this修改后的指向,arg1,arg2....)
        • bind()语法并不会立即执行函数,而是返回一个修改指向后的新函数,常用于回调函数
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script>
        /* 
        1.函数三种执行方式 : 
            全局函数 : this指向window
            对象方法:  this指向对象
            构造函数 : this指向new创建的对象
                共同的特点: this的指向无法动态修改

        2.函数上下文模式 : 
            2.1 作用: 可以动态修改函数中的this
            2.2 语法: 有3种写法,作用都是一样改this,应用场景不同
                a. 函数名.call(修改的this,arg1,arg2…………) 
                    * 适用于函数原本形参 <= 1
                b. 函数名.apply(修改的this,[数组或伪数组])
                    * 适用于函数原本形参 >= 2
                c. 函数名.bind(修改的this,arg1,arg2…………)
                    * 特点:不会立即执行这个函数,而是返回修改this后的新函数
                    * 适用于不会立即执行的函数 : 事件处理函数,定时器函数

         */

        // function fn(){
        //     //三种执行模式this无法动态修改
        //     //this = {age:18};

        //     console.log(this);
            
        // };

        // fn();//this:window
        
        /* 上下文模式 */
        function fn(a,b){
            console.log(this);
            console.log(a+b);
        };

        //a. 函数名.call(修改的this,arg1,arg2…………) 
        //应用场景: 适用于函数原本形参 <= 1
        fn(10,20);//this:window
        fn.call({age:18},100,200);
        
        //b. 函数名.apply(修改的this,[数组或伪数组])
        //应用场景: 适用于函数原本形参 >=2
        fn.apply({age:88},[20,30]);

        //c. 函数名.bind(修改的this,arg1,arg2…………)
        //特点:这个函数不会立即执行,而是返回一个修改this之后的新函数
        //应用场景 : 事件处理函数,定时器
        let newFn =  fn.bind({name:'坤坤'});
        newFn(50,60);


        //4. 定时器中的this一定是指向window

        // 定时器:一段代码间隔事件执行 setTimeout(一段代码,间隔时间)

        //4.1 具名函数
        let test = function(){
            console.log('我是具名函数');
            console.log(this);    
        };

        let newTest = test.bind({name:'张三'})

        setTimeout(newTest,3000);

        //4.2 匿名函数
        setTimeout(function(){
            console.log('我是定时器中的函数');
            console.log(this);
        }.bind({name:'李四'}),2000);
    </script>
</body>
</html>


    //2.3 bind();
    //语法:  函数名.bind(this修改后的指向,arg1,arg2....);
    let obj = {
        name:"文聪兄"
    };

    function getSum(a,b){
        console.log(this);
        console.log(a+b);
    }

    getSum(10,20);//this指向window
    let fn = getSum.bind(obj); //bind()不会执行这个函数,而是会返回一个修改了this的函数.
    fn(100,200); //此时这个fn,就相当于是修改了this之后的getSum.


    //3 回调函数(例如定时器),一般使用bind来修改this指向
       let obj = {
         name:"班长"
       };
        //3.1  定时器的回调函数是一个具名函数
       function test1(){
         console.log(this);
       }
       let test2 = test1.bind(obj);
       setInterval(test2,2000);
       //3.2 定时器的回调函数是一个匿名函数
       setInterval(function () {
           console.log ( this );
       }.bind(obj),2000);

1.2-函数调用上下文模式注意点

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script>
        /* 
        1. 函数上下文执行模式 : 动态修改this
            注意点 : 修改的this只能是引用类型

        2.如果写的是基本数据类型 
            string,number,boolean : 自定帮我们转成对应的引用类型(基本包装类型) new String() Boolean() Number()
            undefined,null :代码不会报错,也不会帮我们修改this,还是原来的this
         */

        function fn(){
            console.log(this);
            
        };

        fn.call('str');
        fn.call(1);
        fn.call(true);

        //如果传的是undefined和null,或者不传。代码不会报错,也不会帮我们修改this,还是原来的window
        fn.call(undefined);
        fn.call(null);
        fn.call();
        fn.call(window);
    </script>
</body>
</html>

1.3-案例01:伪数组转数组

本小节知识点:伪数组转数组

  • 1.伪数组:只有数组的三要素(元素、下标、长度),没有数组的api
  • 2.转数组目的:让伪数组也可以调用数组的api
  • 3.方式很多种,掌握任何一种即可
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <script>
        /* 
        伪数组 : 拥有数组三要素,但是没有数组的api
            伪数组本质是一个对象
         */

        let weiArr = {
            0: '林绿裙',
            1: '保健坤',
            2: '泰拳邹',
            3: '炮王周',
            length: 4
        };
        console.log(weiArr);
        console.log(weiArr[0]);

        //需求:伪数组转成真数组 (希望伪数组也可以调用数组的api)

        //1.遍历伪数组添加到真数组中
        // let arr = [];
        // for (let i = 0; i < weiArr.length; i++) {
        //     console.log(weiArr[i]);
        //     arr.push(weiArr[i]);
        // };
        // console.log(arr);

        //2. ** arr.push.apply()
        /* 
        arr.push() : 支持多个参数
         */
        let arr = [];
        //这里使用apply不是为了修改this,而是利用applay传参会自动遍历数组/伪数组每一个元素作为实参传递
        arr.push.apply(arr, weiArr);
        console.log(arr);


        //3. ** slice
        /* 
       (1) 数组arr.slice() 作用是查询数组元素。 如果传0 或者 不传,则会自动返回数组本身
       (2) 如果 伪数组 可以调用slice(), 则会自动返回数组本身(真数组)
       (3) 但是 伪数组 不能调用slice(), 因为伪数组的原型 不是Array.prototype
       (4) 思考: 伪数组如何可以调用slice()?
       (5) 突破点:  slice() 方法在数组的原型中Array.prototype
           原型中的成员:两种对象可以访问
               * a.构造函数自身可以访问
               * b.每一个实例对象
       (6) 越过原型链查找机制,直接访问构造函数原型中的slice() : Array.prototype.slice()
       (7) 如果使用构造函数原型来调用slice(), 此时this是谁呢? 变成原型: Array.prototype
       (8) 使用 Array.prototype.slice.call(weiArr)
        */

        //weiArr.slice();//报错 为什么报错? weiArr的原型不是Array.prototype
        //slice方法存储在构造函数原型中,谁可以访问原型?
        //1.实例化对象
        //arr.sclie();
        //2.构造函数自身
        //Array.prototype.slice()

        //越过原型链,直接从构造函数原型中调用slice方法
        let arr = Array.prototype.slice.call(weiArr);
        console.log(arr);




        /* 手动模拟伪数组可以调用slice()原理 */

        //1.有一个构造函数Person  (相当于Array)
        function Person(name, age) {
            this.name = name;
            this.age = age;
        };

        //Person原型添加成员(相当于slice)
        Person.prototype.sayHi = function () {
            //this : 谁调用我就指向谁  (正常情况下:this指向调用这个方法person实例对象)
            console.log(this.name);
        };

        //2. p1 和 p2都是Person的实例对象, 所以可以访问 Person原型中的sayHi()方法
        let p1 = new Person('困困', 20);
        p1.sayHi();

        let p2 = new Person('琪琪', 30);
        p2.sayHi();

        //3. 有一个obj(属性名和p1一样,但是原型指向的不是Person,相当于伪数组)
        let obj = {
            name: '班长',
            age: 88
        };

        //4. obj不能直接调用sayHi,会报错
        // obj.sayHi();

        //5.   解决方案: 越过原型链,直接访问Person.prototype中的sayHi
        // 但是这样调用的话, sayHi中的this就变成了  Person.prototype
        // 所以需要使用call,把this修改成obj.  就相当于是obj在调用sayHi()
        Person.prototype.sayHi.call(obj);

    </script>
</body>

</html>

1.5-案例02:求数组最大值

本小节知识点

  • 通过apply方式,让数组对象可以调用Math对象的方法
    • 原因:apply方法传参的时候,会自动将数组的每一个元素取出来作为实参
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script>
        let arr = [100,20,66,88,90];

        //1.擂台思想
        let max = -Infinity;
        for(let i = 0;i<arr.length;i++){
            if(arr[i] > max){
                max = arr[i]
            };
        };
        console.log(max);

        //2.Math.max(数字1,数字2,数字3,…………)
        // let max = Math.max(arr[0],arr[1],arr[2],arr[3],arr[4]);
        //这里没有必要修改this,借助apply可以自动遍历数组传参
        let max1 =  Math.max.apply(Math,arr);
        console.log(max1);
        
        
    </script>
</body>
</html>

1.6-案例03:万能检测数据类型

  • 思考题:为什么数组调用toString和对象调用toString得结果不一样
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script>
        /* 
        1. 检测数据类型 : typeof 数据
            特点:两种数据类型无法检测 null与array
         */
        console.log(typeof null);//object
        console.log(typeof [10,20,30]);//object

        //3. 万能检测数据类型法 :
        // Object.prototype.toString.call(数据)
/* 
        检测数据类型需要使用Object原型(Object.prototype)中的toString()来检测
            返回一个固定值: [object 数据类型]
         */

        console.log( Object.prototype.toString.call('123'));//[object String]
        console.log( Object.prototype.toString.call(666));//[object Number]
        console.log( Object.prototype.toString.call(true));//[object Boolean]
        console.log( Object.prototype.toString.call(undefined));//[object Undefined]
        console.log( Object.prototype.toString.call(null));//[object Null]
        console.log( Object.prototype.toString.call([10,20,30]));//[object Array]
        console.log( Object.prototype.toString.call(function(){}));//[object Function]
        console.log( Object.prototype.toString.call({}));//[object Object]
        
        
        
        /* 
        请说出下列代码的结果,如果不是Null,如何得到Null
         */
        console.log( typeof null );
        
        
        
        
    </script>
</body>
</html>

03-递归

1.1-递归函数介绍

本小节知识点

  • 1.递归函数:一个函数自己调用自己
  • 2.递归函数特点
    • a.一定要有结束条件,否则会导致死循环
    • b.能用递归函数实现的需求,就一定可以用循环调用函数来解决,只是代码简洁与性能不同而已
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script>
        /* 
        1. 递归函数 : 在函数中自己调用自己

        2. 递归特点
            a. 能用递归实现的功能一定可以用循环,只是语法不同
            b. 递归一定要有结束的条件,否则会导致死循环
         */

        //一个函数递归
        // function fn(){
        //     console.log('哈哈');
        //     fn();
            
        // };

        // fn();

        //两个函数递归
        // function fn1(){
        //     console.log('哈哈');
        //     fn2();
            
        // };

        // function fn2(){
        //     console.log('呵呵');
        //     fn1();
            
        // };
        // fn2();


        //需求:写一个函数,打印三次 班长爱坤哥

        let i = 1;
        function fn(){
            console.log('班长爱坤哥');
            i++;
            if(i <= 3){
                fn();
            };
            
            //循环实现
            // for(let i = 1;i<=3;i++){
            //     console.log('班长爱坤哥');
                
            // };
        };

        fn();
    </script>
</body>
</html>

1.2-递归应用场景:浅拷贝与深拷贝

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <script>
      /*浅拷贝与深拷贝概念主要针对于对象这种数据类型 
        
        1.浅拷贝: 拷贝的是地址
            * 特点:修改拷贝后的数据,原数据也会随之修改
        2.深拷贝:拷贝的是数据
            * 特点:修改拷贝后的数据,对原数据没有影响
         */

      let obj = {
        name: 'ikun',
        age: 32,
        hobby: ['讲课', '敲代码', '黑马程序员'],
        class: {
          name: '武汉大前端',
          salary: [18888, 12000, 10000]
        }
      }

      //1.浅拷贝: 拷贝的是地址
      let obj1 = obj
      obj1.name = '黑马李宗盛'
      console.log(obj, obj1)

      //2.深拷贝:拷贝的是数据
      //核心原理:使用递归。 只要遇到属性值是引用类型,则遍历。

      function kaobei (newObj, obj) {
        // 遍历
        for (let key in obj) {
          if (obj[key] instanceof Array) {
            // obj[key] 是数组
            // obj[key]是数组
            newObj[key] = []

            kaobei(newObj[key], obj[key])
          } else if (obj[key] instanceof Object) {
            // obj[key] 是对象
            // obj[key]再遍历拷贝
            newObj[key] = {}

            kaobei(newObj[key], obj[key])
          } else {
            newObj[key] = obj[key]
          }
        }
      }

      let obj2 = {}
      //深拷贝
      kaobei(obj2, obj)
      //修改拷贝后的数据
      obj2.name = '黑马颜值担当'
      obj2.hobby = '唱歌'
      console.log(obj,obj2)
    </script>
  </body>
</html>

1.3-递归应用场景:遍历dom树

在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <style>
      * {
        padding: 0;
        margin: 0;
      }

      .menu p {
        width: 100px;
        border: 3px solid;
        margin: 5px;
      }

      .menu > div p {
        margin-left: 10px;
        border-color: red;
      }

      .menu > div > div p {
        margin-left: 20px;
        border-color: green;
      }

      .menu > div > div > div p {
        margin-left: 30px;
        border-color: yellow;
      }
    </style>
  </head>
  <body>
    <div class="menu">
      <!-- <div>
        <p>第一级菜单</p>
        <div>
          <p>第二级菜单</p>
          <div>
            <p>第三级菜单</p>
          </div>
        </div>
      </div> -->
    </div>
    <script>
      //服务器返回一个不确定的数据结构,涉及到多重数组嵌套
      let arr = [
        {
          type: '电子产品',
          data: [
            {
              type: '手机',
              data: ['iPhone手机', '小米手机', '华为手机']
            },
            {
              type: '平板',
              data: ['iPad', '平板小米', '平板华为']
            },
            {
              type: '智能手表',
              data: []
            }
          ]
        },
        {
          type: '生活家居',
          data: [
            {
              type: '沙发',
              data: ['真皮沙发', '布沙发']
            },
            {
              type: '椅子',
              data: ['餐椅', '电脑椅', '办公椅', '休闲椅']
            },
            {
              type: '桌子',
              data: ['办公桌']
            }
          ]
        },
        {
          type: '零食',
          data: [
            {
              type: '水果',
              data: []
            },
            {
              type: '咖啡',
              data: ['雀巢咖啡']
            }
          ]
        }
      ]

      /* 使用递归遍历数组 */
      function addElement (arr, father) {
        for (let i = 0; i < arr.length; i++) {
          let div = document.createElement('div')
          div.innerHTML = `<p>${arr[i].type || arr[i] }</p>`
          father.appendChild(div)
          if( arr[i].data ){
            addElement(arr[i].data,div)
          }
        }
      }

      //调用递归函数
      addElement(arr,document.querySelector('.menu'))
    </script>
  </body>
</html>

04-闭包(closure)

  • 传送门:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures

在这里插入图片描述

学习目标:能够认识闭包结构,知道闭包在开发中的作用。

一. 闭包的定义

  • 1.闭包 : 是一个可以访问其他函数作用域的函数
    • 闭包 = 函数 + 上下文的引用,闭包不等于函数。以下代码就构成了闭包:
function fn(){
    let a = 1
    function fn1() {
        console.log(a)
    }
    fn1()
}

提示:执行函数 fn1 用到了另一个函数fn中的 a 这个变量,所以 fn1 + a 构成了闭包。

二. 闭包的作用

直接作用:解决变量污染问题,让变量被函数保护起来

示例代码如下:

let count = 0
setInterval(function () {
  console.log(count++)
}, 1000)

以上代码中的 count 是一个使用频率很高的变量名,为了避免和其他位置的代码冲突,可以再使用一个函数把以上代码包裹起来,起到保护作用。

function fn() {
  let count = 0

  setInterval(function () {
    console.log(count++)
  }, 1000)
}

以上代码中,setInterval 第一个参数的匿名函数count 构成了闭包。

将以上代码改写如下:

function fn() {
  let count = 0
  function add() {
    console.log(count++)
  }
  
  setInterval(add, 1000)
}

以上代码中,add + count 构成了闭包。

结论:一个函数内使用了外部的变量,那这个函数和被使用的外部变量一起被称为闭包结构,在实际开发中,通常会再使用一个函数包裹住闭包结构,以起到对变量保护的作用。

三. 闭包的案例

  1. 案例需求:在输入框输入搜索文字,点击百度一下按钮,用定时器模拟网络请求,1 秒之后显示搜索结果;

  2. 页面结构如下

    <div class="box">
      <input type="search" name="" id="">
      <button>百度一下</button>
    </div>
    
  3. 代码如下:

    // 1. 获取元素
    let search = document.querySelector('.box input')
    let btn = document.querySelector('.box button')
    
    // 2. 添加点击事件
    btn.onclick = function () {
      // 获取搜索的文字
      let text = search.value
    
      // 模拟发送网络请求
      setTimeout(function () {
        alert(`您搜索的内容是 ${text} 共搜索到 12345 条结果`)
      }, 1000)
    }
    

闭包小结

  1. 闭包 = 函数 + 上下文的引用
  2. 闭包的作用:解决变量污染问题,让变量被函数保护起来。
    • 在 ES5 时代,闭包可以解决一些其他 JavaScript 的小 BUG,但随着 ES6 let 等新语法的诞生,之前一些闭包的使用场景已经不再需要.

今天学习重点梳理(高频面试题)

this三种指向

this : 谁 调用 我,我就指向谁
1.全局函数 : this指向window

2.对象方法 : this指向对象

3.构造函数 : this指向new创建的空对象

call、apply、bind区别

  • 相同点:都是修改函数this指向
  • 不同点
    • 传参方式不同: call用于单个参数,apply用于多个参数
    • 执行机制不同: call与apply会立即执行, bind不会立即执行
      • call、apply用一次修改一次
      • bind;一次修改,终生有效

闭包

  • 什么是闭包:以下两种回答都可以
    • 闭包是一个访问其他函数内部变量的函数
    • 闭包是 函数 + 上下文代码组合
  • 闭包作用:解决变量污染

递归

  • 什么是递归:函数内部调用自己
  • 递归场景
    • 深拷贝
    • 遍历dom树
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值