一、闭包
1.函数的两个阶段
1.1 函数定义阶段
- 开辟一个 存储空间
- 把函数体内的代码当作“字符串”一模一样的放在这个空间内
- 碰到的所有变量都不进行解析
- 把 存储空间 的地址赋值给函数名(变量名)
1.2 函数调用阶段
- 每一个函数调用的时候都会开辟一个执行空间
- 你调用你一次,开辟一个执行空间
- 执行完毕,执行空间销毁
- 你再次调用的时候,再次开辟一个执行空间
- 执行完毕,执行空间销毁
1.3 函数调用阶段(重新定义)
-
按照函数名的地址找到函数的 存储空间
-
形参赋值
-
预解析
-
在内存中开辟一个 执行空间
-
将函数 存储空间 中的代码拿出来在刚刚开辟的 执行空间 中执行
-
执行完毕后,内存中开辟的 执行空间 销毁
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 闭包的特点
- 为什么要叫做特点,就是因为他的每一个点都是优点同时也是缺点
- 作用域空间不销毁
- 优点: 因为不销毁,变量页不会销毁,增加了变量的生命周期
- 缺点: 因为不销毁,会一直占用内存,多了以后就会导致内存溢出
- 可以利用闭包访问再一个函数外部访问函数内部的变量
- 优点: 可以再函数外部访问内部数据
- 缺点: 必须要时刻保持引用,导致函数执行栈不被销毁
- 保护私有变量
- 优点: 可以把一些变量放在函数里面,不会污染全局
- 缺点: 要利用闭包函数才能访问,不是很方便
- 作用域空间不销毁
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干了什么
- 在内存中开辟一个对象存储空间
- 把这个空间的地址赋值给this
- 我们通过代码给this添加属性
- 会自动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() } }
-
这样就继承成功了