JavaScript 高级篇-03 (this指向/上下文执行模式/递归/闭包)

1.this指向

  • 函数三种执行模式 : 全局函数 、 对象方法 、 构造函数
  • this : 谁 调用 我,我就指向谁 (与函数的声明无关 取决于函数的调用)
  1. 全局函数 :函数名( ) ------------------>this指向window
  2. 对象方法 :对象名.方法名( ) -------> this指向对象
  3. 构造函数 :new 函数名( )------------>this指向new创建的空对象

分析this指向心得 : this指向取决于函数调用,而函数有三种调用方式。所以this指向也会有三种情况
(1)优先用排除法分析, 如果有new关键字,则指向new创建的对象。 否则不是window就是对象
(2)如何判断this是window还是对象,就看调用是: 函数名() 还是 对象名.方法名()

        let obj = {
            name: 'ikun',
            sayHi: function () {
                function fn() {
                    console.log(this);
                }
                fn()// 普通函数调用 :函数名()  fn调用了this  所以this指向window 
            }
        }
        obj.sayHi()//window
        let obj1 = {
            name: 'ikun',
            sayHi: function () {
                console.log(this);
            }
        }
        obj1.sayHi() //对象方法调用 :对象名.方法名()  对象obj1调用了this  所以this指向obj1对象
        function Fn() {
            console.log(this);
        }
        new Fn()//构造函数调用 :new 函数名()  根据new执行流程/创建一个空对象-this指向这个对象-对象赋值-返回对象  这里的this指向new创建的空对象

共同的特点: this的指向无法动态修改

2.函数上下文执行模式(call/apply/bind)

2.1上下文模式注意点

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

2.2函数上下文作用:

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

2.3 异同点:

  • 相同之处:都可以修改函数中this指向
  • 不同点:
    1.传参方式不同 : call是一一传参,apply是数组/伪数组传参
    2.执行机制不同 : call、apply会立即执行函数, bind不会立即执行函数

2.3 语法

有3种写法,作用都是一样改this,应用场景不同

  • call()语法: 函数名.call(this修改后的指向,参数1,参数2,...)--------------适用于函数原本形参 <= 1
  • apply()语法:函数名.apply(this修改之后的指向,伪数组或者数组)-------------适用于函数原本形参 >= 2
  • bind()语法:函数名.bind(this修改后的指向)
    bind()语法并不会立即执行函数,而是返回一个修改指向后的新函数,常用于回调函数
    如果bind的时候传参,则参数也会绑定,之后无法传递实参
  	<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   30
        fn.call({ age: 18 }, 100, 200); //age:18  300

        //b. 函数名.apply(修改的this,[数组或伪数组])
        //应用场景: 适用于函数原本形参 >=2
        fn.apply({ age: 88 }, [20, 30]);

        //c. 函数名.bind(修改的this)
        //特点:这个函数不会立即执行,而是返回一个修改this之后的新函数
        //应用场景 : 事件处理函数,定时器
        let newFn = fn.bind({ name: '坤坤' });
        console.log(newFn);
        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>

函数.call() 应用场景

 /* 
        1. 函数上下问执行模式 : 动态修改this
            注意点 : 修改的this只能是引用类型

        2.如果写的是基本数据类型 
            string,number,boolean : 自定帮我们转成对应的基本装包类型 new String() Boolean() Number()
            undefined,null :
         */

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

        };

        fn.call('str');//String
        fn.call(1);//Number
        fn.call(true);//Boolean

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

Object.prototype.toString.call()万能检测数据类型

1.Object.prototype.toString() 得到固定格式字符串
2.检测数据类型固定格式语法:Object.prototype.toString.call()
得到一个固定格式字符串: [object 数据类型]

 /* call场景:数据类型检测 */

        //1. typeof 数据 : 检测数据类型 
        /* typeof有两种数据类型无法检测: null,array  都会得到object */

        console.log( typeof '123' )// 'string'
        console.log( typeof 123 )//'number'
        console.log( typeof true )//'boolean'
        console.log( typeof undefined )//'undefined'
        console.log( typeof null )//'object'
        console.log( typeof [10,20,30] )//'object'
        console.log( typeof function(){} )//'function'
        console.log( typeof {name:'123'} )//'object'

        //2. Object.prototype.toString() : 得到固定格式字符串 
        //"[object type]",其中 type 是对象的类型
        //检测数据类型固定格式语法:  Object.prototype.toString.call(数据)
        
        console.log( Object.prototype.toString.call('123') )//[object String]
        console.log( Object.prototype.toString.call(123) )//[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({name:'123'}) )//[object Object]
        

apply上下文调用

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

        fn(1,2)//普通函数  this->window

        //(1)函数名.call(修改的this,参数1,参数2,....)
        fn.call({name:'张三'},10,20)
        //(2)函数名.apply(修改的this, 数组/伪数组 )
        //apply传参的时候会自动的遍历这个数组,然后按照循序逐一传参  a = [30,40][0] b=[30,40][1]
        fn.apply({name:'李四'},[30,40])
    </script>

arr.push.apply(arr,伪数组)的应用场景:伪数组转真数组

<script>

 /* 
        伪数组: 有数组三要素(下标、元素、长度),但是不能使用数组的方法
            伪数组 本质是对象
            伪数组不能使用数组方法: 伪数组的原型指向对象的原型,而不是Array的原型
*/
        let obj = {
            0: 10,
            1: 20,
            2: 30,
            3: 40,
            length: 4,//length 省略不了
        }
        console.log(obj[0]);

        //将伪数组转成真数组 arr.push 往数组后面新增元素 返回的是数组的长度
        let newArr = []
        let a = newArr.push(obj[0], obj[1], obj[2], obj[3])
        console.log(newArr)//[10, 20, 30, 40]
        console.log(a)//数组长度 4

        // ES5:上下文
        let newArr1 = []
        newArr1.push.apply(newArr1, obj)
        //第一个参数:newArr1 本来this就是newArr 这里不需要修改this(相当于this不变)
        //第二个参数:obj 借助apply特点:自动遍历伪数组/数组,逐一传参(自己省去了for循环)
        console.log(newArr1);

        // ES6推荐: Arr.from(伪数组)   伪数组转成真数组
        let arr = Array.from(obj)
        console.log(arr);
    </script>

Math.max.apply(Math, arr)应用场景:求数组最大值

 <script>
        //求数组最大值
        let arr = [10, 13, 12, 3, 14, 23, 32]

        // 1.arr.sort() 数组排序 参数:回调函数 固定语法死记硬背
        //     arr.sort(function (a, b) {
        //         return a - b//从小到大排
        //         return b - a//从大到小排序
        //     })
        arr.sort(function (a, b) {
            return b - a
        })
        console.log(arr[0]);

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

        // ES5:  Math.max.apply(Math, 数组名)
        //函数名.apply(修改的this,数组/伪数组)
        //第一个参数 Math: Math调用了方法 this指向的就是Math,这里就不需要修改this(传Math,相当于this不变)
        //第二个参数 arr :借助apply的特点.自动遍历数组/伪数组  逐一传参
        let max3 = Math.max.apply(Math, arr)
        console.log(max3);

        //ES6(推荐):
        let max4 = Math.max(...arr)
        console.log(max4)
    </script>

bind上下文调用

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

        fn(1,2)//普通函数  this->window

        //(1)函数名.call(修改的this,参数1,参数2,....)
        fn.call({name:'张三'},10,20)
        //(2)函数名.apply(修改的this, 数组/伪数组 )
        //apply传参的时候会自动的遍历这个数组,然后按照循序逐一传参  a = [30,40][0] b=[30,40][1]
        fn.apply({name:'李四'},[30,40])
        //(3)函数名.bind(修改this)
        //bind不会立即调用函数,而是得到一个修改this之后的新函数
        let newFn = fn.bind({name:'王五'})
        newFn(25,66)

bind()应用场景

bind场景主要是修改 '不需要立即执行的函数
//事件处理函数、定时器函数

 //bind场景主要是修改 '不需要立即执行的函数'
        //   事件处理函数、定时器函数

        //1.定时器中的this一定是window
        let fn = function(){
            console.log( this )
        }
        //使用bind修改this
        let newFn = fn.bind({name:'666'})
        // fn() : 调用函数, 运行结果是函数返回值
        // fn : 变量取值, 取出fn中存储的堆地址
        setTimeout( newFn ,3000)

        //下面这个写法和上面写法是一样的 :   函数是数据类型,也可以像其他数据一样直接使用语法
        setTimeout(function(){
            console.log(this)
        }.bind({name:'干饭'}),3000)

3.递归函数 :

/* 高阶函数: 自调用函数、回调函数、闭包、递归 */

什么是递归函数? : 函数内部调用自己
* 注意点: 需要满足条件才会递归,否则会导致死循环
* 递归函数和循环功能类似

        //一个函数递归
        // 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();

递归应用场景:

2.1 浅拷贝与深拷贝

1.浅拷贝: 拷贝的是地址, 修改拷贝后的数据对原数据有影响
       //声明一个对象
        //拷贝: 把对象中存储的数据 拷贝一份赋值给其他对象
        let obj = {
            name:'ikun',
            age:30,
            hobby:['讲课','学生','学习'],
            friend:{
                name:'朋友',
                sex:'男'
            }
        }

        //浅拷贝: 拷贝的是地址
        let obj1 = obj
        //由于浅拷贝,拷贝的是地址。 所以修改拷贝后的数据,原来的数据也会变化
        obj1.hobby = '美食'
        console.log(obj1,obj)
2.深拷贝:拷贝的是数据, 修改拷贝后的数据对原数据没有影响
  let obj = {
            name:'ikun',
            age:30,
            hobby:['讲课','学生','学习'],
            friend:{
                name:'朋友',
                sex:'男'
            }
        }  
        
        /* 
        (1)遍历obj,把所有的属性添加给newObj
        (2)如果obj[key]是引用类型(数组、对象),则不能直接拷贝地址
            (2.1)数组:给newObj声明一个空数组,然后遍历obj[key],把里面元素添加给newObj[key]
            (2.2)对象:给newObj声明一个空对象,然后遍历obj[key],把里面元素添加给newObj[key]
        (3)如果obj[key]不是引用类型,则直接赋值。结束递归
        */

        function kaobei(newObj,obj){
            for(let key in obj){
                //判断 obj[key] 是不是数组类型
                if( obj[key] instanceof Array ){
                    //声明一个空数组,然后继续拷贝数组里面的数据
                    newObj[key] = []
                    kaobei(newObj[key],obj[key])
                }else if(obj[key] instanceof Object){
                    //声明一个空对象,然后继续拷贝数组里面的数据
                    newObj[key] = {}
                    kaobei(newObj[key],obj[key])
                }else{
                    newObj[key] = obj[key]
                }
            }
        }

        //调用深拷贝函数
        let newObj = {}
        kaobei(newObj,obj)
        //深拷贝:修改拷贝的数据,对原数据没有影响
        newObj.hobby[0] = '111'
        console.log( newObj,obj)
3.用JSON转换实现深拷贝
/*   2.深拷贝有两种实现方式
            2.1 推荐使用:json转换
            2.2 递归 :
*/
  let obj = {
            name:'ikun',
            age:30,
            hobby:['讲课','学生','学习'],
            friend:{
                name:'朋友',
                sex:'男'
            }
        }    

        //(1)先把js对象转成json格式字符串 :  JSON.stringify(js对象)
        //json在把js转成json格式字符串的时候,底层会自动帮你深拷贝
        // let json = JSON.stringify(obj)
        // console.log( json )
        //(2)再把刚才的json字符串,转成js对象 : JSON.parse( json格式 )
        // let js = JSON.parse( json )

        // js.hobby = '学习'
        // console.log( js,obj )

        //以上两个流程,可以简写成一行代码
        let newObj = JSON.parse( JSON.stringify( obj ) )
        newObj.friend = '小狗'
        console.log(newObj,obj)
4.遍历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: ['雀巢咖啡']
            }
          ]
        }
      ]
      
      //arr:数据   father:父盒子
      function addElement(arr,father){
        //遍历数组,生成div>p添加到父元素中
        for(let i = 0;i<arr.length;i++){
            //(1)创建空标签
            let div = document.createElement('div')
            //(2)设置内容
            div.innerHTML = `<p>${arr[i].type || arr[i]}</p>`
            //(3)添加到父盒子
            father.appendChild(div)
            //如果菜单还有data,说明还有子菜单,则需要继续遍历添加
            if( arr[i].data ){
                addElement(arr[i].data,div)
            }
        }
      }

      let menu = document.querySelector('.menu')
      //调用函数
      addElement(arr,menu)

    </script>
  </body>
</html>

4.闭包:

  • 1.闭包是什么 : 闭包是一个可以访问其他函数内部变量的函数
  • 2.闭包的作用 : 解决变量污染
        function fn(){
            let a = 1
            //在fn1函数中, 访问了其他函数fn内部的变量。  fn1 + a 形成闭包
            function fn1(){
                console.log(a)
            }
            fn1()
        }
        fn()
        
        function test(){
            let num = 1
           
            setTimeout( function(){
                console.log(num)  
            },2000)
        }

        test()

闭包场景:

    <script>
        /* 
        1.闭包(closure)是什么?
            1.1 闭包是一个访问其他函数内部变量的函数
            1.2 闭包 = 函数 + 上下文引用   的组合
            总结:形成闭包要有两个条件,缺一不可。 (1)函数  (2)访问其他函数内部变量

        2.闭包作用 : 变量污染
            * 后期闭包出现较多的一般都会在回调函数里面
        */    
       
        //获取元素
        let input = document.querySelector('input')
        let button = document.querySelector('button')

        
        button.onclick = function(){
            //有一个变量存储搜索结果
            let str = '6666'
            //模拟网络请求:2秒钟之后获取搜索结果
            setTimeout(function(){
                alert(`搜索结束,本次搜索结果是${str}`)
            },2000)
        }
    </script>

5.总结梳理:

5.1this三种指向

  1. 全局函数 :函数名( ) ------------------>this指向window
  1. 对象方法 :对象名.方法名( ) -------> this指向对象
  2. 构造函数 :new 函数名( )------------>this指向new创建的空对象

5.2call、apply、bind区别

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

5.3闭包

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

5.4递归

  • 什么是递归:函数内部调用自己
  • 递归场景
    • 深拷贝
    • 遍历dom树
上一篇:Go语言基础03(程序流程控制)


下一篇:迭代器和生成器