18_闭包&继承

本文详细介绍了JavaScript中的闭包,包括函数的定义、执行空间、不销毁的执行空间和闭包的生成条件。此外,还探讨了闭包的特点如延长变量生命周期、访问私有变量,以及闭包在柯里化和节流防抖中的应用。接着,文章讨论了继承的概念,解释了构造函数、原型、继承的作用和常见继承方式,如原型继承、借用构造函数继承及组合继承,并引入了ES6中的类和继承机制。
摘要由CSDN通过智能技术生成

一、闭包

1.函数的两个阶段

1.1 函数定义阶段

  1. 开辟一个 存储空间
  2. 把函数体内的代码当作“字符串”一模一样的放在这个空间内
    • 碰到的所有变量都不进行解析
  3. 存储空间 的地址赋值给函数名(变量名)

1.2 函数调用阶段

  1. 每一个函数调用的时候都会开辟一个执行空间
  2. 你调用你一次,开辟一个执行空间
  3. 执行完毕,执行空间销毁
  4. 你再次调用的时候,再次开辟一个执行空间
  5. 执行完毕,执行空间销毁

1.3 函数调用阶段(重新定义)

  1. 按照函数名的地址找到函数的 存储空间

  2. 形参赋值

  3. 预解析

  4. 在内存中开辟一个 执行空间

  5. 将函数 存储空间 中的代码拿出来在刚刚开辟的 执行空间 中执行

  6. 执行完毕后,内存中开辟的 执行空间 销毁

    function fn() {
      console.log('我是 fn 函数')
    }
    
    fn() 
    
    • 函数执行的时候会开辟一个 执行空间 (我们暂且叫他 xxff00
    • console.log('我是 fn 函数') 这个代码就是在 xxff00 这个空间中执行
    • 代码执行完毕以后,这个 xxff00 空间就销毁了

2.函数执行空间

  • 每一个函数会有一个 存储空间
  • 但是每一次调用都会生成一个完全不一样的 执行空间
  • 并且 执行空间 会在函数执行完毕后就销毁了,但是 存储空间 不会
  • 那么这个函数空间执行完毕就销毁了,还有什么意义呢?
    • 我们可以有一些办法让这个空间 不销毁
    • 闭包,就是要利用这个 不销毁的执行空间

3.不销毁的函数执行空间

  • 函数的 执行空间 会在函数执行完毕之后销毁

  • 但是,一旦函数内部返回了一个 复杂数据类型,并且 在函数外部有变量接收这个 复杂数据类型的情况下

  • 那么这个函数 执行空间 就不会销毁了

    function fn() {
      const obj = {
          name: 'Jack',
          age: 18,
          gender: '男'
      }
      
      return obj
    }
    
    const o = fn()
    
    • 函数执行的时候,会生成一个函数 执行空间 (我们暂且叫他 xxff00
    • 代码在 xxff00 空间中执行
    • xxff00 这个空间中声名了一个 对象空间(xxff11
    • xxff00 这个执行空间把 xxff11 这个对象地址返回了
    • 函数外部 0 接受的是一个对象的地址没错
      • 但是是一个在 xxff00 函数执行空间中的 xxff11 对象地址
      • 因为 o 变量一直在和这个对象地址关联着,所以 xxff00 这个空间一直不会销毁
    • 等到什么时候,执行一句代码 o = null
      • 此时, o 变量比在关联在 xxff00 函数执行空间中的 xxff11 对象地址
      • 那么,这个时候函数执行空间 xxff00 就销毁了

4.闭包

  • 闭包就是利用了这个函数执行空间不销毁的逻辑
  • 有几个条件组成闭包

4.1 闭包生成的三个条件

  • 有一个 A 函数,再 A 函数内部返回一个 B 函数

  • 再 A 函数外部有变量引用这个 B 函数

  • B 函数内部访问着 A 函数内部的私有变量

以上三个条件缺一不可

4.2 不销毁的闭包空间

  • 闭包的第一个条件就是利用了不销毁空间的逻辑

  • 只不过不是返回一个 对象数据类型

  • 而是返回一个 函数数据类型

    function fn() {
        
      return function () {}
    }
    
    const f = fn()
    
    • f 变量接受的就是一个 fn的执行空间 中的 函数

4.3 内部函数引用外部函数中变量

  • 涉及到两个函数

  • 内部函数要查看或者使用着外部函数的变量

    function fn() {
      const num = 100
      
      // 这个函数给一个名字,方便写笔记
      return function a() {
        console.log(num)
      }
    }
    
    const f = fn()
    
    • fn() 的时候会生成一个 xxff00 的执行空间
    • xxff00 这个执行空间内部,定义了一个 a 函数的 存储空间 xxff11
    • 全局 f 变量接受的就是 xxff00 里面的 xxff11
    • 所以 xxff00 就是不会销毁的空间
    • 因为 xxff00 不会销毁,所以,定义再里面的变量 num 也不会销毁
    • 将来 f() 的时候,就能访问到 num 变量

4.4 间接返回一个函数

<script>
    /* 
        间接返回一个函数
            + 直接返回的一个函数: return function(){}
            + 间接返回一个函数:return 一个对象或者数组
                == 这个对象或者数组里面有一个函数
        使用
            + 当你只需要访问一个私有变量的时候,可以使用直接返回或者间接返回
            + 当你需要访问多个私有变量的时候
                == 我们就需要使用间接返回的方式
                == 返回一个对象内包含多个闭包函数
    */

    function fn(){
        let num = 100;
        let num2 = 200;
        return {
            // 对象的名称语义化比数组的索引强一些
            getNum:function(){
                console.log(num)
            },
            getNum2:function(){
                console.log(num2)
            }
        }
    }

    let res = fn();
    // res得到的是一个对象
    // 这个对象里面有一个函数是fn的闭包函数
    console.log(res)
    res.getNum()
    res.getNum2()
</script>

4.5 闭包的特点

  • 为什么要叫做特点,就是因为他的每一个点都是优点同时也是缺点
    1. 作用域空间不销毁
      • 优点: 因为不销毁,变量页不会销毁,增加了变量的生命周期
      • 缺点: 因为不销毁,会一直占用内存,多了以后就会导致内存溢出
    2. 可以利用闭包访问再一个函数外部访问函数内部的变量
      • 优点: 可以再函数外部访问内部数据
      • 缺点: 必须要时刻保持引用,导致函数执行栈不被销毁
    3. 保护私有变量
      • 优点: 可以把一些变量放在函数里面,不会污染全局
      • 缺点: 要利用闭包函数才能访问,不是很方便

4.6 柯里化函数

  • 柯里化(currying)又称部分求值
  • 柯里化的函数首先会接收一些参数
  • 接收了这些参数后,该函数并不会立即求值
  • 而是继续返回另外一个函数
  • 刚才传入的参数在函数形成的闭包中被保存起来
  • 待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值
// 打印一个班级学员的信息:我是 XX 学科 XX 班的XX,XX 岁
function printInfo(xueke,banji,name,age){
    console.log(`我是${xueke}学科 ${banji} 班的${name},${age}`)
}

printInfo('h5','2209','lucy',20)
printInfo('h5','2209','hanmeimei',30)

// 对于“学科”和“班级”,都是一样的,就没必要都作为参数传递了,可以把上面改造成
function printInfo(xueke,banji){
    return function(name,age){
        console.log(`我是${xueke}学科 ${banji} 班的${name},${age}`)
    }
}
let print = printInfo('h5','2209')
print('lucy',20)
print('hanmeimei',30)
  • 柯里化函数调用后,得到的是一个函数
  • 柯里化可以帮助我们把相同的参数固定下来
  • 把任意的多参函数通过固定参数的方式,变为单参函数
  • 这样就不用每次调用函数的时候都去反复传递这些一样的参数了

4.7 节流和防抖

    <style>
        body{
            height: 10000px;
        }
    </style>

<body>
    <button id="btn">不要一直点击我</button>
    <script>
        /* 
            节流
                + 假如一个用户一直触发某个函数,且每次触发时间小于1s
                + 每隔1s调用函数
                + 以按钮点击事件为例子,你一直点击,我每1s执行一次
            防抖
                + 假如一个用户一直触发某个函数,且每次触发时间小于1s
                + 只调用一次
                + 以onscroll事件为例,你停止滚动的时候才调用函数,也就是1s以内不出发scroll事件,就说明你停止了
                
            目的:防止多次调用
        */
        let btn = document.querySelector('#btn');

        // 没有做防抖和节流的时候,只要点击就会执行真正的事件处理函数
        // btn.addEventListener('click',function(){
        //     console.log('真正要执行的代码执行了')
        // })

        // 节流
        // 在1s以内,不管点击多少次,都只执行一次
        // 15:23:10-15:23:11: 点击100次只执行一次
        // 15:23:11-15:23:12: 点击10只执行一次
        
        // 把匿名函数执行以后的返回值函数,作为click的事件处理函数
        // 点击事件发生的时候,真正执行的返回的那个函数
        btn.addEventListener('click',(()=>{
            // 在这里定义的变量,函数b可以使用,可以不是全局变量
            let preTime = new Date()
            return ()=>{
                // 事件处理函数只要被点击,一定会执行
                // 条件是:点击事件发生的时间距离上次的时间超过1000ms
                let nowTime = new Date();  
                if(nowTime-preTime>1000){
                    // 真正有用的代码放在一个条件中,只有符合条件才执行真正的代码
                    console.log('真正要执行的代码执行了');
                    // 当执行完以后,下次的开始实际就是本次点击的时间
                    preTime = nowTime;
                }
            }
        })())

        
        
        // 不做防抖处理的页面滚动事件
        window.addEventListener('scroll',function(){
            console.log(1)
        })

    </script>
</body>

滚动事件防抖

<script>
        // 只要滚动就一定会触发scroll事件
        // 对应的事件处理函数也一定会发生
        // 但是我们可以在事件处理函数里面设置条件
        // 只有符合条件才执行代码,不符合条件就什么都不做
        // 条件是什么?1s以内不滚动就说明滚动停了,停了就是条件
        // 也就是说,代码不要在滚动的时候立即执行,而是倒计时1s以后执行
        // 如果在1s以内又发生了滚动事件,上次的倒计时定时器就清除不再发生,重新开启倒计时定时器1s后执行

        // 优化,减少全局变量timer
        // window.onscrll = function(){clearTimeout(timer);timer = setTimeout(function(){console.log('我是真正要执行的代码')},1000)}
        window.onscroll = (function(){
            let timer = null;
            return function(){
                // 每次滚动的时候,如果timer里面有记录定时器变量,就说明要先清除定时器
                clearTimeout(timer);
                // 不要立即执行代码,等待1s再执行代码
                timer = setTimeout(function(){
                    console.log('我是真正要执行的代码')
                },1000)
            }
        })()
    </script>

二、继承

  • 继承是和构造函数相关的一个应用
  • 是指,让一个构造函数去继承另一个构造函数的属性和方法
  • 所以继承一定出现在 两个构造函数之间
  • 当A构造函数的属性和方法被B构造函数的实例使用了
  • 那么我们就说B继承自A构造函数
  • A是B构造函数的父类
  • B是A构造函数的子类

1.构造函数

  • 一个构造函数可以使用 new 关键字来创造出若干的实例
  • 每一个实例都可以使用这个构造函数的属性和方法

2.构造函数的意义

  • 构造函数的意义就是为了创建一个对象
  • 当函数和new关键字连用的时候,就拥有了创建对象的能力
  • 需要和new关键字连用的函数,我们建议首字母大写

3.new关键字

  • new就是创造对象的过程
  • new也叫做实例化对象的过程
  • new创造出来的对象叫做构造函数的实例对象
  • new干了什么
    1. 在内存中开辟一个对象存储空间
    2. 把这个空间的地址赋值给this
    3. 我们通过代码给this添加属性
    4. 会自动return this

4.继承的作用

  • 在我们书写构造函数的时候,为了解决一个函数重复出现的问题
  • 我们把构造函数的 方法 写在了 prototype
  • 这样,每一个实例使用的方法就都是来自构造函数的 prototype
  • 就避免了函数重复出现占用内存得到情况
  • 那么,如果两个构造函数的 prototype 中有一样的方法呢,是不是也是一种浪费
  • 所以我们把构造函数䣌 prototype 中的公共的方法再次尽心提取
  • 我们准备一个更公共的构造函数,让构造函数的 __proto__ 指向这个公共的构造函数的 prototype

5.常见的继承方式

  • 我们有一些常见的继承方式来实现和达到继承的效果

  • 我们先准备一个父类(也就是要让别的构造函数使用我这个构造函数的属性和方法)

    function Person() {
        this.name = 'Jack'
    }
    
    Person.prototype.sayHi = function () {
        cosnole.log('hello')
    }
    
  • 这个 Person 构造函数为父类

  • 让其他的构造函数来继承他

  • 当别的构造函数能够使用他的属性和方法的时候,就达到了继承的效果

5.1 原型继承

继承

  • 涉及两个构造函数

  • 准备好的父类叫做Person

  • 准备好的子类叫做Student

  • 当完成继承以后, Student继承自Person

  • 那么s就可以使用name属性和sayHi方法

原型继承

  • 就是通过改变原型链的方式来达到继承
  • 子类.prototype = 父类的实例

原型继承缺点

  • 我继承下来的属性没有继承在自己身上

    • 而是在__proto__里面

    • 当我访问的时候就要求__proto__里面找

  • 我继承的目的是为了继承属性和方法

    • 我自己要使用的name属性和age属性的值

    • 自己要用的多个参数在多个位置传递

    • 对于代码的维护和书写阅读都不是很好

原型继承,就是在本身的原型链上加一层结构

```javascript
function Student() {}
Student.prototype = new Person()
```

5.2 借用构造函数继承

继承=>两个构造函数之间的关系

  • 为了让子类的实例使用父类的属性和方法

继承方案

  • 在子类的构造函数体内,借用构造函数执行一下
  • 并且强行让父类的构造函数的this指向子类的实例

借用构造函数继承的优缺点

  • 优点

    • 继承来 的属性写在了自己的身上
      • 就不需要去__proto__上找了
    • 自己需要的两个属性的值,在一个构造函数的时候传递
      • 不想原型继承需要在两个地方传递参数
  • 缺点

    • 只能继承父类的属性
      • 不能继承父类原型prototype上的方法
      • 写在构造函数体内的都可以继承下来

call是改变函数内部this指向的方法之一

  • 调用是跟在函数后面的
    • fn() => fn.call()
    • obj.fn() => obj.fn.call()
  • 目的是强行改变本次调用的this指向
    • 不管你本身函数内部的this指向谁
    • 我让你本次指向谁就指向谁
  • 语法:函数名.call(你要改变的函数this指向,给函数传递的实参1,给函数传递的实参2,…)

把父类构造函数体借用过来使用一下而已

function Student() {
  Person.call(this)
}

5.3 组合继承

继承=>两个构造函数之间的关系

  • 子类的实例使用父类的属性和方法

组合=>原型继承 + 借用构造函数继承

  • 利用借用构造函数继承,把属性继承在自己身上
  • 利用原型继承吧父类prototype上的方法继承下来

就是把 原型继承借用构造函数继承 两个方式组合在一起

function Student() {
  Person.call(this)
}
Student.prototype = new Person

六、extends+super ES6继承

  • es6 的继承很容易,而且是固定语法

    • es6有自己书写类的语法: class

    • es6也有自己的继承关键字: extends

  • 继承

    • 两个类的关系
    • 语法: class 子类 extends 父类
    • 创建一个继承自Person的子类Student
    // 下面表示创造一个 Student 类,继承自 Person 类
    class Student extends Person {
        constructor () {
            // 必须在 constructor 里面执行一下 super() 完成继承 
            super()
        }
    }
    
  • 这样就继承成功了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值