一、闭包
函数的两个阶段:
1、定义阶段
1)开辟一个存储空间
2)把函数体内的代码一模一样的放在这个空间内(不解析变量)
3)把存储空间的地址给函数名
2、调用阶段
1)按照函数名的地址找到函数的存储空间
2)形参赋值
3)预解析
4)在内存中开辟一个执行空间
5)将函数存储空间中的代码拿出来在刚刚开辟的执行空间中执行
6)执行完毕后,内存中开辟的执行空间销毁
函数的执行空间:
每一个函数会有一个存储空间
但是每一次调用都会生成一个完全不一样的执行空间
并且执行空间会在函数执行完毕后就销毁了,但是 存储空间 不会
我们可以有一些办法让这个空间 不销毁
闭包,就是要利用这个不销毁的执行空间
闭包介绍
函数作用域嵌套,造成变量执行完不被销毁的场景就叫闭包。闭包不是新的语法,是函数嵌套后产生一种神奇的场景。函数内嵌套函数,并返回内函数的引用地址,在外部定义变量来接收。
总结:局部的引用类型数据跟全局的变量,产生了引用的关系。全局变量一直能被使用,局部的引用类型就不能被销毁,局部的执行空间一直存在内存中,没有被销毁。
闭包的优缺点
优点:
1)保护私有变量不被全局污染
2)间接的让函数外可以访问函数内的变量
3)延迟了变量的生命周期
缺点:
外面函数每调用一次,就会在调用栈保留一个执行空间,调用多了话,可能会造成内存溢出/内存泄漏
闭包的应用
(1)在循环中绑定事件,事件函数中需要使用循环的变量
(2)在循环中执行异步代码,在异步代码中使用循环的变量
(3)防抖:当某些事件在触发的时候,行为执行了一点,就会触发多次事件(鼠标移动事件、键盘按下事件、浏览器滚动事件、浏览器大小改变事件、文本框内容即时改变等)的时候,很多情况下,我们都只是需要最后一次触发的结果,中间触发的多次事件都是多余的、浪费的,此时就需要防抖。
防抖:简单地说,就是 当一个事件连续触发,只执行最后一次。
一般使用场景:
登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖
调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
//1.封装防抖函数
function debounce(fn, time) {
//4.创建一个标记用来存放定时器的返回值
let timer = null
return function () {
//5.每当用户触发input事件,把前一个setTimeout清除掉
clearTimeout(timer)
//6.又创建一个新的setTimeout,这样就能保证输入字符后等待的间隔内,还有字符输入的话,就不会执行setTimeout
timer = setTimeout(() => {
//7.这里进行防抖的内容
fn()
}, time)
}
}
//8.测试防抖临时使用函数
function sayHi() {
console.log("防抖成功")
}
//2.获取操作元素
var inp = document.querySelector("input")
//3.给inp绑定input事件,调用封装的防抖函数,传入要执行的内容与间隔时间
inp.addEventListener('input', debounce(sayHi, 5000))
(4)节流:一个事件会在很短的时间内触发多次的时候,其中很多事件在触发后,我们无法把握其中的每个事件,所以就造成了多余和浪费,我们可以让事件在一段我们能把握到、反应过来的时间段执行一次,其实是减少触发率。
节流:简单地说,就是限制一个事件在一段时间内只能执行一次
一般使用场景:
scroll 滚动事件,每隔一秒计算一次位置信息等
input 框实时搜索并发送请求展示下拉列表,每隔一秒发送一次请求
//使用定时器和开关节流
//1.封装节流函数
function throttle(fn, time) {
//3.通过闭包保存一个“节流阀” 默认为true
let flag = true
return function () {
//8.触发事件被调用,判断节流阀是否为false。如果为false就直接return出去不做任何操作
if (!flag) {
return
}
//4.如果节流阀为true,立即将节流阀设置为false
flag = false
//5.开启定时器
setTimeout(() => {
//6.将外部传入的函数的执行放在setTimeout中
fn.apply(this, arguments)
//7.最后在setTimeout执行完毕后再把标记节流阀为true,表示可以进行下次循环了
flag = true
}, time)
}
}
//8.测试防抖临时使用函数
function sayHi(e) {
//打印当前document的宽高
console.log(e.target.innerWidth, e.target.innerHeight)
}
//2.绑定事件,绑定时就调用节流函数
//绑定事件要调用一下封装的节流函数,触发事件时触发封装函数内部的函数
window.addEventListener('resize', throttle(sayHi, 2000))
//利用时间差节流
document.onmousemove = throttling(fn, 1000)
function throttling(handler, time) {
var startTime = +new Date()
return function() {
var now = +new Date()
if(now - startTime >= time) {
handler.call(this, ...arguments)
startTime = now
}
}
}
function fn(e) {
console.log( e.pageX );
}
总结:防抖是控制次数,节流是控制频率。
(5)函数柯里化:如果一个函数调用传递了多个实参,函数定义就需要多个形参来接收。函数柯里化,就是让函数不接收全部的实参,只接收部分参数,然后在函数内再次返回一个小函数,来接收剩余部分的参数,让函数整个运行流程,可以分多个步骤执行。
function add(a){
return function(b){
console.log(a+b)
}
}
add(2)(3)
二、继承
继承介绍
继承是让一个对象可以拥有另一个对象的属性和方法,而不用自己去添加,类似于原型和实例对象的关系。面向对象编程,有一个特性就是继承。
原型继承
我们可以通过修改对象的原型,让对象能拥有其他对象的属性和方法。
弊端:继承来的属性在原型上,不在自己上,当给自己添加同名属性时,就无法使用原型的属性了。
优点:可以继承父类构造函数和原型空间中的所有内容
缺点:不能给父类构造函数中传递参数
function animal(){
this.name="动物"
}
animal.prototype.sport=function(){
console.log("运动")
}
let a=new animal()
console.log(a)
function Birds(){
this.wing="翅膀"
}
// 为了能让bird实例对象能拥有animal对象的属性和方法 - 将animal对象作为bird实例对象的原型
Birds.prototype = a
let b=new Birds()
console.log(b)
Birds.prototype.fly=function(){
console.log("飞翔")
}
console.log(b.name) //动物
b.sport() //运动
//不能给父类构造函数中传递参数
借用函数继承
通过借用函数,可以在子构造函数中,执行父构造函数中的代码,将父构造函数中的属性添加在子构造函数中。
优点:可以继承父类构造函数中的属性和方法,也可以给父类构造函数中传递参数
缺点:无法继承父类原型空间中的内容
function animal() {
this.name = "动物"
}
animal.prototype.sport = function () {
console.log("运动")
}
let a = new animal()
console.log(a)
function Birds() {
//借用函数继承
//在这里执行父构造函数中的代码并将其中的this改成子构造函数中的this
animal.call(this)
this.wing = "翅膀"
}
let b = new Birds()
console.log(b)
Birds.prototype.fly = function () {
console.log("飞翔")
}
console.log(b.name) //动物
b.sport() //报错
//无法继承父类原型空间中的内容
组合继承
组合继承:为了解决原型继承和借用函数继承的弊端,可以将这两种继承方式都使用上。
优点:既可以继承父类构造函数中的内容,又可以继承父类原型空间中的内容,还可以给父类构造函数中传递参数
function animal() {
this.name = "动物"
}
animal.prototype.sport = function () {
console.log("运动")
}
let a = new animal()
console.log(a)
function Birds() {
//借用函数继承
// 在这里执行父构造函数中的代码并将其中的this改成子构造函数中的this
animal.call(this)
this.wing = "翅膀"
}
//原型继承
Birds.prototype = a
let b = new Birds()
console.log(b)
Birds.prototype.fly = function () {
console.log("飞翔")
}
console.log(b.name) //动物
b.sport() //运动
ES6的类
类是抽象的对象,对象是实例化的类
用class定义一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。简单地说,constructor内定义的方法和属性是实例对象自己的,而constructor外定义的方法和属性则是所有实例对象可以共享的。(除静态属性和静态方法以外)
//定义类
class 类的名字{}
//类只有一个作用,就是定义对象,定义对象
var 对象 = new 类
//给对象中添加属性,就要在类中定义
class 类的名字{
// 方法1
属性名 = 值
// 方法2
constructor() {
this.属性名 = 值
}
//给对象添加方法
方法名(){
代码段
}
}
class Animal{
name = '动物'
constructor(age) {
this.age = age
this.say = '叫'
}
sport() {
console.log('运动');
}
}
var a = new Animal(5)
console.log(a)
ES6的继承
class之间可以通过extends关键字实现继承,这比ES5通过修改原型链实现继承,要清晰和方便很多。
在ES6的类中定义的对象,实现继承有继承的语法:
class 子类 extends 父类{ }
//跟组合继承的效果是一样的
super关键字,它指向父类的实例(即父类的this对象)。
子类必须在constructor方法中调用super方法,否则新建实例时会报错。
这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
class Animal{
name = '动物'
sport() {
console.log('运动');
}
}
class Bird extends Animal{
constructor(age) {
//如果父类中有constructor,super就相当于在调用父类的constructor
super()
this.age = age
}
}
var b = new Bird(12)
console.log(b);
ES6的继承机制,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
同一个父类,可以被多个子类继承,但每个子类都可以在同样的属性和方法中有不同的表示显示。用专业术语来讲,叫做多态。
注意:
1、当子类继承父类时,子类中什么内容都没有时,创建的子类实例对象,实际上表示父类实例对象
2、当父类和子类中都没有constructor方法时,可以不使用super来进行修改this执行,同样可以继承父类中的属性和方法
3、当父类和子类中都有constructor方法时,那么需要在子类的constructor方法的第一行使用super方法来修改this指向
4、当父类中的属性跟子类中的属性同名时,子类属性会覆盖父类属性或方法