10分钟入门~面试官问我这个问题,我自信的站了起来!一笑生花

本文详细讲解前端面试常考知识点——闭包和继承。通过案例解析了闭包的形成条件、应用场景及常见面试题,还探讨了不同继承方案的优缺点,包括原型继承、借用函数继承等。适合初学者巩固知识,也为面试做准备。
摘要由CSDN通过智能技术生成

如果你是一个入门初学者,对前端的重要知识掌握的不够扎实或间接遗忘,亦或你是现在正在寻找工作,背烂了面试题,却被面试官处处难为到,这篇博文一定对你有很大的帮助,这篇博文主要详细结合案例讲述前端的难点:闭包和继承。 是许多前端大佬拿来反复咀嚼的知识,也是面试官们必问的题了吧。下面结合案例来讲述,希望这篇博文对你能起到较大的帮助。


认识函数

函数在内存中执行原理

函数定义过程:

function fn(){
    var num = 10;
    num++;
	console.log(num)
}

在这里插入图片描述
函数调用过程:

函数调用的时候,会在内存中的另外一个空间(执行空间)中进行,当函数执行结束后,执行空间立马销毁:

fn()

在这里插入图片描述

执行空间不销毁的函数

如果一个函数中返回了一个复杂类型数据,这个函数中的执行空间就不会被销毁:

function fn(){
    return {
        
    }
}
var obj = fn()

执行的时候的过程:
在这里插入图片描述
因为栈中变量obj跟执行中间中返回的数据空间保持了引用关系,所以这个fn的执行空间不能销毁,因为一旦销毁后,就没有了obj引用的数据空间了。

闭包

作用域嵌套形成的一种js的高级应用场景。

闭包xmind图

在这里插入图片描述

形成条件

大函数中直接或间接的返回一个小函数,小函数访问大函数中的变量。此时大函数的执行空间不会被销毁了。小函数叫做大函数的闭包函数。

function fn(){
    var num = 10;
    function fun(){
        num++;
        return num
    }
    return fun;
}
var f = fn();

/当一个大函数中,返回一个小函数,小函数中使用了大函数中的变量 - 里面小函数叫做大函数的闭包


 function fn(){
     var num = 10;
     return function(){
         num++;
         console.log(num);
    }
 }

 var fun = fn()
 console.log(fun);

 fun()
 fun()
 fun()
 fun()
 fun()
 fun()

钱包系统 - 存钱、消费、查看余额

var money = 100;

money += 10;

console.log(money);

money -= 20;

定义在全局中的变量,容易被覆盖 - 会污染全局
function fn(){
    var money = 100;
    return money
}

var m = fn()
console.log(m);
没有办法操作局部的money,无法存钱和消费了

function fn(){
    var money = 100;
    function fun(num){
        money += num
        return money
    }
    return fun
}

var f = fn()
var m = f(30)
console.log(m);


var n = f(40)
console.log(n);

闭包的应用场景

闭包的应用场景 - 通常会使用闭包解决 在循环中执行异步代码的问题

for(var i=1;i<=3;i++){
    setTimeout(function(){
        console.log(i);
    },1000*i)
}

for(var i=1;i<=3;i++){
    function fn(i){
        // var i = 1/2/3
        setTimeout(function(){
            console.log(i);
        },1000*i)
    }
    fn(i)
}


for(var i=1;i<=3;i++){
    var fn = (function(i){
        return function(){
            setTimeout(function(){
                console.log(i);
            },i*1000)
        }
    })(i)
    fn()
}

tab切换

var oLis = document.querySelectorAll('li')
var oDivs = document.querySelectorAll('div')
for(var i=0;i<oLis.length;i++){
    (function(i){
        oLis[i].onclick = function(){
            // 将所有的li的active去掉
            for(var j=0;j<oLis.length;j++){
                oLis[j].className = '';
                oDivs[j].className = '';
            }
            this.className = 'active';
            // 让i能用
            console.log(i);
        }
    })(i)
}

星星评分:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>document</title>
</head>
<body>
<img src="./images/rank_3.gif" alt=""><img src="./images/rank_4.gif" alt="">
<img src="./images/rank_3.gif" alt=""><img src="./images/rank_4.gif" alt="">
<img src="./images/rank_3.gif" alt=""><img src="./images/rank_4.gif" alt="">
<img src="./images/rank_3.gif" alt=""><img src="./images/rank_4.gif" alt="">
<img src="./images/rank_3.gif" alt=""><img src="./images/rank_4.gif" alt="">
</body>
<script>
var oImgs = document.querySelectorAll('img')
for(var i=0;i<oImgs.length;i++){
    oImgs[i].onmouseover = fn(i)
}

function fn(i){

    return function(){
        for(var j=0;j<=i;j++){
            if(j%2){
                oImgs[j].src = './images/rank_2.gif';
            }else{
                oImgs[j].src = './images/rank_1.gif';
            }
        }
        for(var j=i+1;j<oImgs.length;j++){
            if(j%2){
                oImgs[j].src = './images/rank_4.gif';
            }else{
                oImgs[j].src = './images/rank_3.gif';
            }
        }
    }
}
</script>
</html>

闭包的作用:

  1. 保护变量私有化 - 定义在函数内部的变量就是私有变量
  2. 使用函数外部访问到函数内部的私有变量
  3. 延长了变量的生命周期

缺点:有一个不会被销毁的内存空间,容易造成内存泄漏。

闭包的应用案例:循环中绑定事件 - 星星评分

闭包的语法糖

语法糖:就是利用这个语法的一种场景,且使用起来方便, 但是看起来不舒服。

闭包有一个语法糖是获取器和设置器,分别是get和set。

正常的写法和用法:

function fn(){
    var num = 100;
    return {
        getNum(){
			return num;
        },
        setNum(val){
            num = val
        }
    }
}

var res = fn();
console.log(res.getNum()) // 100
res.setNum(500)
console.log(res.getNum()) // 500

语法糖写法:

function fn(){
    var num = 100;
    return {
        get num(){
			return num;
        },
        set num(val){
            num = val
        }
    }
}

var res = fn();
console.log(res.num) // 100
res.num = 500
console.log(res.num) // 500

get方法想要做的事情就是访问num的值,set方法想要做的事情就是设置num的值

换语法糖写之后,num作为了res对象的属性,直接可以访问,当访问num值的时候,其实就是在调用get方法,当给num直接赋值的时候,其实就是在调用set方法。

函数柯里化

函数柯里化其实就是利用闭包将函数的多个参数拆分成多个函数使用,方便模块化开发,例:

function fn(reg,username){
	return reg.test(username)
}

这是一个验证用户名是否正确的函数,调用方式如下:

var reg = /^[a-zA-Z]\dw{3,11}$/;
var res = fn(reg,'cuihua')

var res1 = fn(reg,'ruhua')

从上面重复使用的代码中可以看出来,每次使用的reg都是一样的,且在模块化开发中,不建议将变量暴露出来,暴露的都是一个一个的方法,也就是函数,所以讲上面的函数改写:

function fn(reg){
    return function(username){
        return reg.test(username)
    }
}

使用方式如下:

var test = fn(/^[a-zA-Z]\dw{3,11}$/);
var res = test('cuihua')

var res1 = test('ruhua')

这种使用方式,没有将变量暴露在外,而是将函数内部的函数暴露在外面,更加符合模块化的要求。

这就是函数的柯里化。

案例:正则验证的模块化封装

闭包面试题

function fn(i){
    return function(n){
        console.log(n + (--i))
    }
}
var f = fn(2)
f(3)
fn(4)(5)
fn(6)(7)
f(8)  ---结果是  8

继承

继承xmind图

在这里插入图片描述

概念

如下三个构造函数:

function Animal(name,age){
    this.name = name;
    this.age = age
}

Animal.prototype.say = function(){
    console.log('各种声音')
}

function Cat(eye) {
    this.eye = eye
}

Cat.prototype.xingwei = function () {
    console.log('抓老鼠') 
}

function Bird(body) {
    this.body = body
}

Bird.prototype.fly = function () { 
    console.log('飞') 
}

const c = new Cat('蓝黄')
console.log(c)
const b = new Bird('翅膀')
console.log(b)

从下面两个实例对象中可以看出,每个对象都有自己的属性和方法,不带有Animal的属性和方法,但Cat和Bird又希望能使用到Animal的属性和方法。

当多个构造函数需要使用一些共同的属性和方法时候,就需要将将共同的属性和方法单独封装在一个构造函数中,方便多个构造函数继承使用。

继承:就是让子构造函数的实例对象能使用父构造函数的属性和方法。

父构造函数叫父类,子构造函数叫子类。继承的目的,就是让子类拥有父类的属性和方法。

继承方案

原型继承

通过概念对象的原型链结构来实现继承,因为对象默认能访问到原型对象的属性和方法

function Animal(name,age){
    this.name = name;
    this.age = age
}

Animal.prototype.say = function(){
    console.log('各种声音')
}

function Cat(eye) {
    this.eye = eye
}

// 改变Cat的原型
Cat.prototype = new Animal('猫',5)

Cat.prototype.xingwei = function () {
    console.log('抓老鼠') 
}

var c = new Cat('眼睛')
console.log(c)

优点:只要处在原型上的方法和属性都能使用

缺点:一个构造函数的实例创建需要在两个地方传递参数,且同样是属性,不在同一个地方显示

借用函数继承

利用借用函数,将父构造函数中的this改成子构造函数中的this,让子构造函数具备跟父构造函数同样的属性。

function Animal(name,age){
    this.name = name;
    this.age = age
}

Animal.prototype.say = function(){
    console.log('各种声音')
}

function Cat(name,age,eye) {
    // 使用借用函数调用父构造函数
    Animal.call(name,age)
    this.eye = eye
}

Cat.prototype.xingwei = function () {
    console.log('抓老鼠') 
}

var c = new Cat('猫',5,'眼睛')
console.log(c)

优点:在同一个地方传递参数了,且属性都放在了一起。

缺点:父构造函数原型上的方法继承不了。

组合继承

组合继承是将原型继承和借用函数继承同时使用。

function Animal(name,age){
    this.name = name;
    this.age = age
}

Animal.prototype.say = function(){
    console.log('各种声音')
}

function Cat(name,age,eye) {
    // 使用借用函数调用父构造函数
    Animal.call(name,age)
    this.eye = eye
}

// 改变Cat的原型
Cat.prototype = new Animal()

Cat.prototype.xingwei = function () {
    console.log('抓老鼠') 
}

var c = new Cat('猫',5,'眼睛')
console.log(c)

优点:

  1. 父类构造函数体内和原型上的内容都能继承
  2. 继承下来的属性放在自己身上
  3. 在一个位置传递所有参数

缺点:当给子类添加方法的时候, 实际上是添加在了父类的实例身上

拷贝继承

拷贝继承是利用for in循环能将原型中的属性和方法也遍历出来的特性,将父对象中属性和方法遍历绑定到子对象的原型上。

function Animal(name,age){
    this.name = name;
    this.age = age
}

Animal.prototype.say = function(){
    console.log('各种声音')
}

function Cat(name,age,eye) {
    // 在子构造函数中实例化父构造函数
    var a = new Animal(name,age)
    // 遍历父对象
    for(var attr in a){
        // 给子对象的原型上添加父对象中的属性和方法
        Cat.prototype[attr] = a[attr]
    }
    this.eye = eye
}

Cat.prototype.xingwei = function () {
    console.log('抓老鼠') 
}

var c = new Cat('猫',5,'眼睛')
console.log(c)

优点:

  1. 父类的构造函数体内的和原型上的都可以继承
  2. constructor 能正常配套
  3. 添加自己的方法的时候, 确实是在自己的原型身上

缺点:

  1. for in 循环: for in 循环需要一直遍历到 Object.prototype - 比较消耗性能
  2. 不能继承 不可枚举 的属性 - 颜色比较暗的属性和方法不能遍历到
  3. 继承来的属性不再自己身上

寄生继承

将父构造函数在子构造函数中实例化,然后在子构造函数中返回实例出来的对象。

function Animal(name,age){
    this.name = name;
    this.age = age
}

Animal.prototype.say = function(){
    console.log('各种声音')
}

function Cat(name,age,eye) {
    // 在子构造函数中实例化父构造函数
    var a = new Animal(name,age)
    // 给这个对象中添加属性
    a.eye = eye;
    return a;
}

Cat.prototype.xingwei = function () {
    console.log('抓老鼠') 
}

var c = new Cat('猫',5,'眼睛')
console.log(c)

这种继承没有了自己的原型,原型完全是父构造函数的继承,改变自己原型后,自己原本原型上的方法又无法使用。

function Cat(name,age,eye) {
    // 在子构造函数中实例化父构造函数
    var a = new Animal(name,age)
    // 改原型
    Cat.prototype = a.__proto__;
    // 给这个对象中添加属性
    a.eye = eye;
    return a;
}
Cat.prototype.xingwei = function () {
    console.log('抓老鼠') 
}

var c = new Cat('猫',5,'眼睛')
console.log(c) // 对象中就没有了xingwei这个方法了

寄生继承的另一种方式修改:

function Cat(name,age,eye) {
    // 在子构造函数中实例化父构造函数
    var a = new Animal(name,age)
    // 改原型
    Cat.prototype = a.__proto__;
    // 给这个对象中添加属性
    a.eye = eye;
    return a;
}
Animal.prototype.xingwei = function () {
    console.log('抓老鼠') 
}

var c = new Cat('猫',5,'眼睛')
console.log(c) // 对象中又有了xingwei这个方法了

但这种继承方式,将父构造函数的原型修改了,以后的继承,都会带有这个方法。

优点:

  1. 原型和构造函数体内的都能继承下来
  2. 寄生原型的话, 自己的属性和方法依旧可以添加和使用

缺点:

  1. 寄生实例的时候, 没有自己的任何内容
  2. 寄生原型的时候, 一旦修改原型上, 父类的实例也会有这些方法

寄生组合继承(完美继承)

寄生组合继承是合并了寄生继承和组合继承以及借助了第三方独立的构造函数而完成。

function Animal(name,age){
    this.name = name;
    this.age = age
}

Animal.prototype.say = function(){
    console.log('各种声音')
}

function Cat(eye) {
    this.eye = eye;
}

Cat.prototype.xingwei = function () {
    console.log('抓老鼠') 
}

// 为了保护全局变量不被污染,使用自调用函数
(function(){
    // 借助第三方构造函数
    function Temp(name,age){
        // 再使用借用函数继承
        Animal.call(this,name,age)
    }
    // 先原型继承
    Temp.prototype = new Animal()
    // 原型继承后的实例对象,原型上有父构造函数的属性,原型的原型上有父构造函数的方法
    // 再加借用函数继承后,实例对象中拥有父类的属性,方法在原型的原型上
    var t = new Temp('猫',5);
    console.log(t)
    // 将得到的实例对象作为子类的原型
    Cat.prototype = t;
})()
// 然后Cat的实例对象中,需要有的就全有了
var c = new Cat('眼睛')
console.log(c)

Animal经过借用函数继承后,属性已经有了,需要的只剩下原型中的方法了,所以对原型进行寄生即可,不用实例化Animal了,简化如下:

(function(){
    // 借助第三方构造函数
    function Temp(name,age){
        // 借用函数继承
        Animal.call(this,name,age)
    }
    // 原型寄生
    Temp.prototype = Person.prototype
    // 寄生原型
    Cat.prototype = new Temp('猫',5)
})()

var c = new Cat('眼睛')
console.log(c)

ES6的继承

es6提供了类的语法和继承的语法:

class Animal{
    constructor(name,age){
        this.name = name
        this.age = age
    }
    
    say(){
        console.log('各种声音')
    }
}

class Cat extends Animal(){
    constructor(name,age,eye){
        super(name,age) // 相当于在调用父类的constructor
        this.eye = eye
    }
    xingwei () {
        console.log('抓老鼠') 
    }
}

var c = new Cat('猫',5,'眼睛')
console.log(c)

喜欢的欢迎收藏,记得点赞关注哦!

  • 18
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 17
    评论
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贪吃ღ大魔王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值