前端面试 常见问题

264 阅读7分钟

1、VUE的生命周期

概述:

VUE的生命周期是指:一个组件 从 被创建被销毁 的全部过程;可以分为 四个阶段,八个函数

  • 创建阶段 --> 挂载阶段 --> 更新阶段 --> 销毁阶段

1.创建阶段:

beforeCreate:实例对象尚未被创建,无法获取data数据或methods方法

created:实例对象已经被创建,可以获取data数据或methods方法

2.挂载阶段:

beforeMounte:实例对象尚未挂载到指定节点成为真实DOM,不能操作真实DOM,通常请求数据

mounted:实例对象已经挂载到指定节点成为真实DOM,可以操作真实DOM

3.更新阶段:

beforeUpdate:虚拟DOM上的数据已经更新,但页面中的数据尚未更新

updated:页面中的数据已经完成更新

4.销毁阶段:

beforeDestory:实例对象尚未销毁,通常解绑方法

destoryed:实例对象已经销毁


2、父子组件的生命周期顺序

概述:

父组件执行完beforeMounte后,才会进入template标签;进而触发子组件beforeCreate函数

当标签内所有子组件都按顺序执行完mounted之后,父组件再执行mounted。(更新与销毁同理)

Snipaste_2024-01-06_17-08-51.png


3、v-if 和 v-show

概述:

两者都是用来控制元素的显示/隐藏,只是两者实现方式不同

v-if通过创建/销毁节点来实现,v-show通过控制display样式属性来实现。

因此,从性能方面考虑,对于经常切换展示或隐藏的,我们推荐使用v-show的方法来实现。

相同点不同点适用情况
v-if控制元素的展示/隐藏通过创建销毁实现效果不常切换
v-show控制元素的展示/隐藏通过控制display属性none/block实现效果经常切换

4、v-if 和 v-for 避免一起使用

概述:

在VUE中v-for优先级更高。如果我们在同一标签上即使用v-for又使用v-if,那么VUE会首先遍历出很多标签,然后对每一个标签进行v-if判断,性能消耗很大。

我们通常有两种方式来解决这个问题:

  • v-if的判断条件与v-for的遍历项有依赖关系时,可以通过computed计算属性进行判断
<!-- 不推荐写法 -->
<div v-for="item in inHouseList" v-if="item.is_expired === 1">{{item.bar_code}}</div>
<!-- 推荐写法 -->
<div v-for="item in activeinHouseList">{{item.bar_code}}</div>

computed: {
	activeinHouseList: function() {
	    return this.inHouseList.filter((item) => {
		return item.is_expired === 1 
	    })
	}
}
  • v-if的判断条件与v-for的遍历项有非依赖关系时,可以通过嵌套标签进行实现
<!-- 不推荐写法 -->
<div v-for="item in inHouseList" v-if="isExpired === 1 ">{{item.bar_code}}</div>
<!-- 推荐写法 -->
<template v-if="isExpired === 1">
	<div v-for="item in inHouseList"></div>
</template>

5、v-model 的作用

概述:

v-model的本质是一种语法糖写法,用来实现数据的双向绑定

  • 原生写法:在标签上通过v-bind进行赋值,实现数据向页面的数据流;再通过v-on进行事件绑定,配合$event.target.value将页面中的数据赋值给变量,从而实现页面向数据的数据流。
 <!-- 原生写法 -->
 <input v-bind:value="test" v-on:input="test= $event.target.value" />
 
  <!-- v-model写法 -->
 <input v-model="test" />
  • 通常用在:表单的input textare等标签上;也可以通过自定义解析,用在父子组件的通信中。

例如,在父页面的子组件标签上通过v-model进行赋值传递。在子组件中通过props属性获取传入值;再通过emit方法将数据传回父组件

同时,我们还可以进行自定义解析:在子组件中自定义一个model属性,model属性内的prop传入数据(默认为value值),event传出事件(默认为input事件)。

// 父组件
<template>
 <div>
  <test-model v-model="inputValue"></test-model>
  <span>{{inputValue}}</span>
</div>
</template>
<script>
import testModel from 'src/components/testModel'
export default {
  data(){
    return{
      inputValue: "inputValue"
    }
  },
  components: {
    testModel,
  }
}
</script>
// 子组件
<template>
  <input ref="input" :value="value" @input="$emit('ababab', $event.target.value)" />
</template>
<script>
 
export default {
  data(){
    return{ }
  },
  props: ["value"],
  model: {
    prop: 'value',
    //这个事件名可以随意写,它实际上是规定了子组件要更新父组件值需要注册的方法
    event: 'ababab'
  }
}
</script>

6、 $router$route 的区别

概述:

$router全局路由对象,包含所有路由信息。我们通常使用$router.push()进行路由的跳转

41524653f1ed49dcb4d25187b9951a43~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.webp

$route局部路由对象,是当前跳转路由对象。我们通常使用$router.query获取路由跳转的参数

1.webp


7、 Data 为什么是个函数

概述:

因为在VUE中,每个组件调用的时候,都会通过组件的构造函数new出一个实例化对象

  • 定义为对象: 如果我们将data定义为这个构造函数上的一个对象属性,那么每次组件被调用(即被new)的时候,都会指向同一个data属性,这就会造成数据紊乱
function VueComponent(){}
VueComponent.prototype.$options = {
    data:{name:'three'} 
}
let vc1 = new VueComponent();
vc1.$options.data.name = 'six'; // 将vc1实例上的data修改为six
let vc2 = new VueComponent(); // 在new一个新的实例vc2
console.log(vc2.$options.data.name); six
// 输出vc2的data的值是six,这时候发现vc2中的data也被修改了,他们data相互影响
  • 定义为函数:为了避免这样的现象,我们将data定义为构造函数上的一个方法,并通过return返回一个新的对象,这样每次调用都会创建一个新的空间用来存储data的数据,避免数据的紊乱。
// 这样就可以保证每个组件调用data返回一个全新的对象,和外部没有关系
function VueComponent(){}
VueComponent.prototype.$options = {
    data: () => ({name:'three'}) 
}
let vc1 = new VueComponent();
let objData = vc1.$options.data()
objData.name = 'six'; // 调用data方法会返回一个对象,用这个对象作为它的属性
console.log(objData)
let vc2 = new VueComponent();
console.log(vc2.$options.data());

8、 组件的通信有哪些方式

概述:

  • $ref$children / $patent

我们可以在标签上添加ref属性,在代码中通过$refs.name的方式来获取指定组件的实例化对象,进而获取到指定组件中的datamethods,从而实现父子组件之间的通信。

$children / $patent则是分别在父组件子组件中使用。$children可以获取到所有子组件并返回一个数组,通过下角标找到指定组件的实例化对象进行通信。$patent则是获取到对应的父组件进行通信。

<!-- 父组件 -->
<template>
  <div class="outer">
    <child ref="ref-child"></child>
    <button @click="getChildName">获得子组件的name:{{ name }}</button>
    <button @click="getChildAge">获得子组件的age:{{ age }}</button>
  </div>
</template>
 
<script>
  ......
  methods: {
    // 读子组件节点
    getChildName: function () {
      this.name = this.$refs["ref-child"].name;
    },
    getChildAge: function () {
      this.name = this.$children[0].age;
    },
  }
</script>
<!-- 子组件 -->
<template>
  <div>name:{{ name }}</div>
  <div>age:{{ age }}</div>
</template>
 
<script>
  ......
  methods: {
    // 读父组件节点
    getFatherStr: function () {
      this.str = this.$parent.str;
    }
  }
</script>
  • props$emit

父组件的子组件标签中通过v-bind的方式向子组件传递数据,在子组件中通过props属性来接收数据props属性有两种形式数组对象,在对象形式中,我们可以通过request default来设置是否必传默认值

由于props属性为只读,因此我们需要通过$emit方法来进行数据回传。并在父组件中绑定对应的事件方法来接收数据。

<!-- 父组件 -->
<template>
  <p>childMessage: {{childMessage}}</p>
  <children :sendmessage='childMessage' @showMsg="updataMsg"></children>
</template>
<!-- 子组件 -->
<template>
  // 通过按钮点击事件将子组件的值传给父组件
  <button @click="sendtoParent">Click this Button</button>
</template>

<script>
export default {
  props: ['sendmessage'],
  methods: {
    sendtoParent() {
      //$emit括号里的第一个参数为自定义事件
      this.$emit('showMsg','子组件通过$emit给父组件传值')
    }
  }
}
</script>
  • v-model

详见问题5、v-model 的作用

  • eventBus 订阅发布者模式:

这种方式是通过订阅/发布者模式来实现的,我们可以在src文件夹下定义一个JS文件作为eventBus的事件中间频道,并在文件中返回一个VUE对象

发布数据的文件中引入eventBus,并通过eventBus.$emit()的方法来进行数据的发布

订阅数据的文件中引入eventBus,并通过eventBus.$on()的方法来进行数据的接收

// eventBus文件中
import Vue from 'vue' 
const EventBus = new Vue()
export default EventBus

// main.js文件中全局挂载
import EventBus from './EventBus' 
Vue.prototype.$EventBus = EventBus

// 发布数据的文件中
this.$EventBus.$emit('getNum', num)

// 监听数据的文件中     
this.$EventBus.$on('getNum'(num) => {console.log('num', num)})
  • Vuex等状态管理工具:

可以通过Vuex等状态管理工具实现跨组件级的数据通信。Vuex中的state是存储的数据状态,我们通常通过mutationsations来对数据进行赋值操作,前者只能进行同步操作,后者可以进行异步操作;通过stategetters来对数据进行读取操作,前者直接读取,后者类似computed返回处理后的数据。同时Vuex还提供了四种映射函数

mapState mapGetters mapActions mapMutations

<!-- Vuex文件中 -->
import Vue from "vue"
import Vuex from "vuex"
Vue.use(Vuex)
const actions={
    odd_Num(comtext,value){
        // 可以通过上下文comtext找到commit方法,dispatch方法
        // 可以找到state对象,并找到其中的属性
        if (comtext.state.n%2===0){
            comtext.commit("add_Num",value)
        }
    },
    long_Time_Add(comtext,value){
        setTimeout(() => {
            comtext.commit("add_Num",value)
        }, 1000);
    }
}
const mutations={
    add_Num(state,value){
        // 这里的state被称为上下文,这里可以拿到state中的数据
        // console.log(state,value)
        state.n+=value
    },
    sub_Num(state,value){
        // console.log(state,value)
        state.n-=value
    },
}
//数据源,源数据
const state={
    n:0
}
//创建一个vuex对象
export default new Vuex.Store({
    actions,
    mutations,
    state
})
<!-- 功能组件中 -->
<script>
import {mapState,mapGetters,mapActions,mapMutations} from "vuex"
export default {
    name:"Counts",
    data(){
        return {
            step:1
        }
    },
    computed:{
        ...mapState(["n"]),
        ...mapGetters({bigNum:"numBig"})
    }
    ,
    methods:{
        // 传入对象是常规写法,当对象中的键值相等时,可以用传入列表的方式进行传参
        // 例如上面的mapState(["n"])
        ...mapMutations({addNum:"add_Num",subNum:"sub_Num"}),
        ...mapActions({oddNum:"odd_Num",longTimeAdd:"long_Time_Add"})
    }
}
</script>
  • provideinject注入依赖:

我们可以在先辈组件中通过provide属性注入依赖项,这个属性可以是对象或者返回对象的函数

在子孙组件中通过inject属性来接收依赖性,这个属性通常为字符串数组

// 先辈组件
provide: { message: 'provided by father' },

// 子孙组件
inject: [ "message" ],

9、 订阅发布者模式 / 观察者模式

概述:

  • 订阅发布者模式

一种一对多的数据关系,我们称被依赖数据项发布者,称数据依赖项观察者

在事件的中间频道中(eventBus)我们定义了$emit$on两种方法,以及一个依赖对象数组。当有数据被依赖的时候就会触发$on方法,将依赖项事件名作为key值、将依赖项回调函数作为value值,存储到定义好的依赖对象数组中。当被依赖的数据发生变化时,就会触发$emit方法,遍历依赖对象数组中对应的事件名称,执行回调函数,进行数据传递

它与观察者模式的区别在于,观察者模式观察者被观察之间知道彼此的存在,直接进行通信。订阅发布者模式订阅者发布者之间不知道彼此的存在,需要通过事件中间频道进行通信。

1.png

  • 观察者模式

一种一对多的数据关系,当被观察的数据发生变化的时候,会通知所有观察者进行数据更新

VUE的数据响应式原理就是:通过数据劫持+观察者模式实现的。VUE会将每一个组件实例创建一个Watcher对象,并将接触过data属性通过Object.defineProperty方法进行数据劫持,为其添加settergetter 的方法,使其成为响应式对象,并作为被观察者。当数据变化的时候,就会触发setter方法,通知观察者进行更新。


10、 数据响应式原理

概述:

数据响应式是指:当数据发生变化的时候,页面中的视图同步更新

我们可以通过Object.defineProperty的方法可以对一个对象的原型进行修改。我们可以借助其为一个对象的原型对象上添加setget的方法,当读取这个对象的属性时,就会触发get方法,并将get方法中return数据返回。当对对象中的属性值进行修改的时候,就会触发set方法,在修改数据的同时,通过document.getElementById等方法对页面中的视图进行同步,进而实现数据响应式。因此,我们可以通过Object.key.forEach配合递归深层次遍历对象中的每一个属性,为每一个属性添加setget的方法。

VUE的数据响应式原理就是:通过数据劫持+观察者模式实现的。VUE会将每一个组件实例创建一个Watcher对象,并将接触过data属性通过Object.defineProperty方法进行数据劫持,为其添加settergetter 的方法,使其成为响应式对象,并作为被观察者。当数据变化的时候,就会触发setter方法,通知观察者进行更新。

var obj={    
    name:Vue是响应式吗?
}

Object.defineProperty(obj,"name",{
    get(){        
        console.log("get方法被触发")
    },
    set(val){        
        console.log("set方法被触发")
    }
})

var str = obj.name  //get方法被触发
obj.name = "Vue是响应式的"  // set方法被触发


11、 VUE2中数据不响应的情况

概述:

  • VUE2中数据不响应有两种情况: 对象内对某一属性进行操作+数组中对某一项进行操作

对象内对某一属性进行操作不响应,是因为Object.defineProperty方法的缺陷,它只能对已经存在的数据进行劫持。当我们页面初始化完成之后,数据就已经被劫持过了,因此单独操作对象内的属性不能够被再次劫持到的。

数组对某一项进行操作不响应,是因为VUE的官方团队舍弃了这一功能,原因是数组的长度是不固定的,这样监听响应的性能消耗与用户体验不成正比

  • 解决方法有以下几种:

Vue.set Vue.delete Object.assign this.$forceUpdate array.splice() 或者重新整体赋值

// Vue.set(原对象/数组,需要设置的新属性, 需要设置的新值) 
Vue.set(this.user, 'age', 18)

// Vue.delete(原对象/数组,需要删除的属性, 需要删除的值) 
Vue.delete(this.user, 'age', 18)

// Object.assign(目标对象,原对象, 新属性) 
this.user = Object.assign({}, this.user, { skill: '铁碎牙', age: 18 })

// 对数据进行操作之后,强行刷新页面
this.$forceUpdate()

//items.splice(下标, 1, 新数组)
items.splice(indexOfItem, 1, newValue)

12、 浏览器的缓存机制

概述:

浏览器的缓存HTTP报文中的缓存标识有关,优秀的缓存策略可以减少请求资源的距离提高响应的速度减少网络的压力

  • 缓存的位置

Service Workers + Memory Cache + Disk Cache + Push Cache

Service Workers 是注册在指定源和路径下的事件驱动worker,他可以拦截并修改请求和资源访问,细颗粒度操作缓存Service Workers的使用需要首先注册,然后再install事件中调用。

Memory Cache内存,他的响应速度,存储空间,与硬盘相比不是同一量级;但是存储时间短,进程结束就会被释放。我们通常用于存储JS脚本、图片及样式等资源。

Disk Cache硬盘,他的响应时间相对较慢,但高于网络请求,存储空间,持久性。任何资源都能存储,因此在内存使用率高、大资源存储的时候会用到硬盘存储

Push CacheHTTP2.0中的内容,他的存储空间,缓存时间,是一种会话级存储,在Session结束时会被销毁

定义特点
Service Workers指定源和路径的事件驱动拦截请求,灵活的操作存储及加载的数据,细颗粒度操作缓存
Memory Cache内存速度快、容量小、持续化时间短
Disk Cache硬盘速度相对慢、容量大、持续化时间久
Push CacheHTTP2.0的内容容量小、持续化时间短
  • 缓存机制

浏览器第一次获取资源的时候,会将请求到的响应标头响应数据等数据存储在浏览器缓存中。这其中就包含ExpiresCache-Control,两者都表示响应数据有效时间Expires绝对时间Cache-Control相对时间优先级更高

第二次请求的时候,首先访问浏览器缓存中有无对应的缓存资源缓存标识并检查ExpiresCache-Control是否过期没有过期不请求服务器,直接加载缓存

如果过期,则将第一次响应头中的ETagLast-Modified,分别赋值给第二次请求头中的If-None-MatchIf-Modified-Since,并向服务器发送器请求。前者用于判断请求的唯一标识,后者用于判断当前数据是否真的过期

服务器判断当前数据过期之后,返回状态码200及新数据,此时加载最新数据;判断当前数据没有过期之后,返回状态码304,表示无数据更新,可以加载缓存数据

我们根据浏览器是否请求服务器资源,将缓存策略分为强缓存协商缓存。从浏览器缓存中获取,不需要请求服务器的称为强缓存需要请求服务器进行判断的称为协商缓存

3.png


13、 浏览器的本地存储

概述:

浏览器本地存储分为四类Cookie + Session Storage + Local Storage + indexed DB

特点应用场景存储位置
Cookie容量很小、私密性相对较好、存储时间很短最早不是用于本地存储,而是弥补浏览器状态管理的缺陷有时效的是时候存在硬盘没时效的时候存内存
Session Storage容量相对较小、安全性相对较好、会话级存储,浏览器关闭就没了用于存储用户相关敏感信息,如token等数据数据值存储在服务器端,本地只存session id
Local Storage容量相对较大、存储时间长久,浏览器关闭数据还在用于存储大数据或需要持久化的数据存储在硬盘
indexed DB容量最大,类似于浏览器的非线性数据库用于存储到达一定规模的大数据存储在硬盘

14、 浏览器的渲染机制

概述:

  • 渲染机制:浏览器在请求到数据之后,需要通过渲染机制数据转换为图形

当浏览器拿到数据之后,会通过渲染引擎中的HTML解析器HTML转换为DOM TREE,通过CSS解析器CSS转化为CSS 规则树;在这两个解析器转化的过程中,都是首先原始字节(Bytes 转化为字符串之后字符串转化为节点(Nodes最后节点转化为DOM TREE或者CSS 规则树

1.png

2.png

在得到DOM TREECSS 规则树之后,会将两棵树合并形成Render Tree即渲染树。这时候将进入LAYOUT阶段即布局算法阶段,在这个阶段,会根据元素的大小、位置等信息进行判断,触发回流机制,返回一个盒子模型

最后根据这个盒子模型执行重绘机制,进入painting阶段,将页面绘制出来。

3.png

  • 回流

页面中元素的大小、位置等发生变化,影响页面布局的时候,就需要重新计算页面整体布局,我们称这个过程为回流机制回流性能消耗较大,因为回流会对所有DOM节点进行计算。

  • 重绘

页面中元素样式发生变化,需要对页面进行重新绘制的时候,我们称这个过程为重绘回流一定会导致重绘,但是重绘却定不一定都需要回流,例如当元素只有背景色发生变化时,此时并不影响页面的布局,只需要重绘就可以。

标题触发时机
回流元素的大小、位置,字体的大小,内容的图片等发生变化时
重绘页面回流、阴影、颜色等属性发生变化时

15、 虚拟DOM树和Diff算法

概述:

虚拟DOM树本质是一个JS对象,用来描述当前页面中的节点元素,其作用是抽离数据的渲染过程使其具备跨端开发的能力。因为我们知道直接操作真实DOM会导致页面的回流与重绘性能消耗较大,因此通过虚拟DOM技术将多次真实DOM操作合并称一次,就可以减少开销

其具体做法如下:当页面第一次创建的时候会生成一个虚拟DOM树,并将其挂载指定节点,成为真实DOM树。当页面发生变化的时候会重新生成一个虚拟DOM树,此时并不会直接操作真实DOM树,而是先通过DIff算法进行比较,将多次操作合并为一次,之后在对真实DOM树进行操作。

1.png

Diff算法是一种广度优先、逐层比较的算法,它只会对同级比较,而不会跨级。在比较的过程中,如果有v-forkey,就以key唯一标识,当key发生变化时,就重新绘制;当key没有发生变化时,就重新创建节点,而是复用现有节点,仅对数据进行更新

2.png


16、 同步与异步 宏任务与微任务

概述:

  • 同步:按照顺序依次执行,同一时间只执行一个
  • 异步:在同一时间内同时执行,往往伴随多线程

浏览器JS引擎单线程的执行机制,会按照执行队列中的顺序依次将任务放到主线程中执行,这些依次执行的任务我们称之为同步任务

而有些任务耗时较大,如果也按照同步的方式执行,可能会出现等待时间很长甚至卡死的情况,这时候浏览器开辟新的线程,将这些耗时较大的任务放在新的线程中同时执行,当这些任务执行完毕时,会被放入到一个任务队列。我们称这些同时执行的任务为异步任务

3.png

主线程执行队列中的任务执行完毕后,通过Event Loop事件循环机制查找任务队列中的异步任务。其实,在这个任务队列中,会根据异步任务的性质分为两个队列宏任务队列微任务队列

  • 宏任务:异步任务中严格按照时间进行压栈和执行的任务,如setTimeOut setInverter
  • 微任务:需要在当前任务执行完之后,立即执行的任务,如Promise.then await后面的异步函数等

主线程优先微任务队列中的回调函数放到JS的主线程执行,然后再去执行宏任务队列中的任务,等这些任务队列执行完毕之后,回到执行队列中去同步任务。我们称这样的循环机制浏览器的事件循环机制


17、 关于Promise

概述:

Promise本质是一个JS对象,它的作用是将异步操作结果或者状态进行返回。当然它也解决了回调地狱的问题,让我们使用同步编程的思想来执行异步操作。

  • Promise三种状态待定态 + 成功态 + 失败态

Promise的异步操作尚未执行的时候,就处于待定态,因为我们不知道操作能否成功。当Promise的异步操作执行成功的时候,就处于成功态,返回的结果会进入resolve()中,并作为参数传入.then()中。当Promise的异步操作执行失败的时候,就处于失败态,返回的结果会进入reject()中,并作为参数传入.catch()中。

function request() { 
  const flag = Math.random() <= 0.5 ? true : false 
  return new Promise((resolve, reject) => { 
    // 模拟异步操作
    setTimeout(() => { 
      if (flag) { 
        // 成功态进入 resolve-->.then()
        resolve('成功的消息') 
        return 
      } 
      // 成功态进入 reject -->.catch()
      reject('失败的消息') 
    }, 2000) }) 
  } 
  
  request().then(msg => console.log(msg)).catch(err => console.log(err))

至于Promise如何解决回调地狱的问题,是因为在成功态中,.then()返回的也是一个Promise对象,这就可以让我们一直.then()下去。从而实现同步编程的思想实现异步操作,进而解决了回调地狱的问题。

  • Promise四种静态方法promise.all() + promise.allSettlesd() + promise.race() + promise.any()
标题描述
promise.all()只要有一个Promise失败就会进入失败态;只有全部成功才将结果全部返回
promise.allSettled()不管Promise成功与否返回一个数组,与Promise数组相对应
promise.race()只将第一个完成Promise返回,不管他是什么状态,第一个完成成功态就进入resolve,第一个完成失败态就进入reject
promise.any()第一个成功Promise返回,所有都处于失败态才进入reject
// 所有方法都是传入Promise数组
Promise.all([]).then(res => {}).catch(err => {})
Promise.any([]).then(res => {}).catch(err => {});
Promise.race([]).then(res => {}).catch(err => {});
Promise.allSettled([]).then(res => {}).catch(err => {});

18、 深拷贝与浅拷贝

概述:

深浅拷贝是对于引用型数据而言的,简单数据类型不存在这个问题。因为简单数据类型在进行赋值的时候,数据直接存储在中,而引用类型数据在赋值的时候,只在中存储一个指针,这个指针指向的一个空间,真实数据是存储在空间中的。

当我们将数据直接赋值引用类型的数据时,只是将指向指针赋值给新数据的中,并没有重新开辟空间新旧数据中存储的指针指向同一个堆空间,这样的拷贝称为浅拷贝

1.png

浅拷贝有一些缺陷。比如,当我们对新数据进行修改的时候,会同时修改原有数据,导致数据紊乱。为了避免这样的情况,就需要用到深拷贝

深拷贝重新开辟空间,将原有数据拷贝到重新开辟的空间内。再将重新开辟空间指针赋值给新数据的中。

2.png

常见的深拷贝方法有:loadsh库中的cloneDeep()方法、JSON序列化与发序列、手写遍历+递归函数等。

// JSON序列化与发序列
function cloneDeepJson(obj){ return JSON.parse(JSON.stringify(obj)) }

// 遍历+递归函数
function cloneDeepDi(obj){ 
  const newObj = {}; 
  let keys = Object.keys(obj); 
  let key = null; 
  let data = null; 
  for(let i = 0; i<keys.length;i++){ 
    key = keys[i]; 
    data = obj[key]; 
    if(data && typeof data === 'object'){ 
      newObj[key] = cloneDeepDi(data) 
    }else{ 
      newObj[key] = data; 
    } 
  } 
  return newObj 
}

19、 原型链与继承

概述:

每一个实例对象都是由构造函数new出来的,并且实例对象上都会有__proto__属性指向构造函数原型对象原型对象上又有一个construct属性执行构造函数。这样就形成一种闭环三角关系。而每一个原型对象的本质都是一个JS对象,也都有其自己的__proto__指向构造函数原型对象,这样就会形成一种链式关系,一直可以向上追溯Object原型对象__proto__属性指向null即空对象。我们称这样的链式关系原型链

  • 实例成员:指构造函数通过this添加的成员,这些成员只能通过实例对象进行访问

  • 静态成员:在构造函数上添加的成员,这些成员只能通过构造函数来访问,在实例对象上没有办法访问

3.png

继承是指子类拥有父类方法和属性,常见的继承方式有以下几种:

  • 原型链继承:我们将子类__proto__指向父类实例对象,再将子类construct指向子类的构造函数,这样就可以继承父类的方法,但是不能继承属性

  • 构造函数继承:我们在子类构造函数中,通过.call()的方式改变父类构造函数this指向,从而继承父类的属性,但是无法继承方法

  • 组合式继承:通过原型链继承 + 构造函数结合的方法,即继承方法又继承属性的方式称为组合式继承

  • 寄生式组合继承:与组合式继承类似,但我们不再将子类__proto__指向父类实例对象,而是将子类__proto__赋值为Object.create()方法,并在方法中传入父类的实例,这样就得到一个新的对象。在通过组合式继承的方式继承属性与方法

  • extend继承:这是一种语法糖的写法,我们通过classextend关键字来实现继承。例如class son extend father,就可以直接即继承方法又继承属性

// 寄生式组合继承
function Parent(name, actions) {
  this.name = name;
  this.actions = actions;
}

Parent.prototype.eat = function () {
  console.log(`${this.name} - eat`);
};

function Child(id) {
  Parent.apply(this, Array.from(arguments).slice(1));
  this.id = id;
}

// 模拟Object.create的效果
// 如果直接使用Object.create的话,可以写成Child.prototype = Object.create(Parent.prototype);
let TempFunction = function () {};
TempFunction.prototype = Parent.prototype;
Child.prototype = new TempFunction();

Child.prototype.constructor = Child;

const child1 = new Child(1, "c1", ["hahahahahhah"]);
const child2 = new Child(2, "c2", ["xixixixixixx"]);

20、 关于 BFC 模式

概述:

BFC是指:块级格式化上下文,是指给当前盒子开启一个独立的渲染空间,使其不受其他环境的影响BFC模式常用于解决清除浮动、父盒子塌陷、margin重叠等问题。

触发BFC的模式有:display:floatoverfloew:hiddenposition:absolute

4.png

<!-- 防止margin重叠 -->
<!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>防止margin重叠</title>
</head>
<style>
    *{
        margin: 0;
        padding: 0;
    }
    p {
        color: #f55;
        background: yellow;
        width: 200px;
        line-height: 100px;
        text-align:center;
        margin: 30px;
    }
    div{
        overflow: hidden;
    }
</style>
<body>
    <p>看看我的 margin是多少</p>
    <div>
        <p>看看我的 margin是多少</p>
    </div>
</body>
</html>

5.png

<!-- 清除浮动 -->
<!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>
</head>
<style>
    *{
        margin: 0;
        padding: 0;
    }
    body {
        width: 100%;
        position: relative;
    }
 
    .left {
        width: 100px;
        height: 150px;
        float: left;
        background: rgb(139, 214, 78);
        text-align: center;
        line-height: 150px;
        font-size: 20px;
    }
 
    .right {
        overflow: hidden;
        height: 300px;
        background: rgb(170, 54, 236);
        text-align: center;
        line-height: 300px;
        font-size: 40px;
    }
</style>
<body>
    <div class="left">LEFT</div>
    <div class="right">RIGHT</div>
</body>
</html>

6.png

<!-- 防止父盒子塌陷 -->
<!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>清除浮动</title>
</head>
<style>
    .par {
        border: 5px solid rgb(91, 243, 30);
        width: 300px;
        overflow: hidden;
    }
    
    .child {
        border: 5px solid rgb(233, 250, 84);
        width:100px;
        height: 100px;
        float: left;
    }
</style>
<body>
    <div class="par">
        <div class="child"></div>
        <div class="child"></div>
    </div>
</body>
</html>