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。(更新与销毁同理)
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" />
- 通常用在:表单的
inputtextare等标签上;也可以通过自定义解析,用在父子组件的通信中。
例如,在父页面的子组件标签上通过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()进行路由的跳转
$route :局部路由对象,是当前跳转的路由对象。我们通常使用$router.query获取路由跳转的参数
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的方式来获取指定组件的实例化对象,进而获取到指定组件中的data和methods,从而实现父子组件之间的通信。
$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是存储的数据状态,我们通常通过mutations和ations来对数据进行赋值操作,前者只能进行同步操作,后者可以进行异步操作;通过state和getters来对数据进行读取操作,前者直接读取,后者类似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>
provide和inject注入依赖:
我们可以在先辈组件中通过provide属性注入依赖项,这个属性可以是对象或者返回对象的函数
在子孙组件中通过inject属性来接收依赖性,这个属性通常为字符串数组
// 先辈组件
provide: { message: 'provided by father' },
// 子孙组件
inject: [ "message" ],
9、 订阅发布者模式 / 观察者模式
概述:
- 订阅发布者模式:
一种一对多的数据关系,我们称被依赖数据项为发布者,称数据依赖项为观察者。
在事件的中间频道中(eventBus)我们定义了$emit 和 $on两种方法,以及一个依赖对象数组。当有数据被依赖的时候就会触发$on方法,将依赖项的事件名作为key值、将依赖项的回调函数作为value值,存储到定义好的依赖对象数组中。当被依赖的数据发生变化时,就会触发$emit方法,遍历依赖对象数组中对应的事件名称,执行回调函数,进行数据传递。
它与观察者模式的区别在于,观察者模式是观察者与被观察之间知道彼此的存在,直接进行通信。订阅发布者模式是订阅者与发布者之间不知道彼此的存在,需要通过事件中间频道进行通信。
- 观察者模式:
一种一对多的数据关系,当被观察的数据发生变化的时候,会通知所有观察者进行数据更新。
VUE的数据响应式原理就是:通过数据劫持+观察者模式实现的。VUE会将每一个组件实例创建一个Watcher对象,并将接触过的data属性通过Object.defineProperty方法进行数据劫持,为其添加setter 和 getter 的方法,使其成为响应式对象,并作为被观察者。当数据变化的时候,就会触发setter方法,通知观察者进行更新。
10、 数据响应式原理
概述:
数据响应式是指:当数据发生变化的时候,页面中的视图同步更新。
我们可以通过Object.defineProperty的方法可以对一个对象的原型进行修改。我们可以借助其为一个对象的原型对象上添加set和get的方法,当读取这个对象的属性时,就会触发get方法,并将get方法中return的数据返回。当对对象中的属性值进行修改的时候,就会触发set方法,在修改数据的同时,通过document.getElementById等方法对页面中的视图进行同步,进而实现数据响应式。因此,我们可以通过Object.key.forEach配合递归,深层次遍历对象中的每一个属性,为每一个属性添加set和get的方法。
VUE的数据响应式原理就是:通过数据劫持+观察者模式实现的。VUE会将每一个组件实例创建一个Watcher对象,并将接触过的data属性通过Object.defineProperty方法进行数据劫持,为其添加setter 和 getter 的方法,使其成为响应式对象,并作为被观察者。当数据变化的时候,就会触发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 Cache是HTTP2.0中的内容,他的存储空间小,缓存时间短,是一种会话级存储,在Session结束时会被销毁。
| 定义 | 特点 | |
|---|---|---|
Service Workers | 指定源和路径的事件驱动 | 拦截请求,灵活的操作存储及加载的数据,细颗粒度操作缓存 |
Memory Cache | 内存 | 速度快、容量小、持续化时间短 |
Disk Cache | 硬盘 | 速度相对慢、容量大、持续化时间久 |
Push Cache | HTTP2.0的内容 | 容量小、持续化时间短 |
- 缓存机制
浏览器第一次获取资源的时候,会将请求到的响应标头及响应数据等数据存储在浏览器缓存中。这其中就包含了Expires和Cache-Control,两者都表示响应数据的有效时间,Expires为绝对时间,Cache-Control为相对时间且优先级更高。
第二次请求的时候,首先访问浏览器缓存中有无对应的缓存资源与缓存标识,并检查Expires和Cache-Control是否过期,没有过期则不请求服务器,直接加载缓存。
如果过期,则将第一次响应头中的ETag和Last-Modified,分别赋值给第二次请求头中的If-None-Match和If-Modified-Since,并向服务器发送器请求。前者用于判断请求的唯一标识,后者用于判断当前数据是否真的过期。
服务器判断当前数据过期之后,返回状态码200及新数据,此时加载最新数据;判断当前数据没有过期之后,返回状态码304,表示无数据更新,可以加载缓存数据。
我们根据浏览器是否请求服务器资源,将缓存策略分为强缓存和协商缓存。从浏览器缓存中获取,不需要请求服务器的称为强缓存;需要请求服务器进行判断的称为协商缓存。
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 规则树。
在得到DOM TREE和CSS 规则树之后,会将两棵树合并形成Render Tree即渲染树。这时候将进入LAYOUT阶段即布局算法阶段,在这个阶段,会根据元素的大小、位置等信息进行判断,触发回流机制,返回一个盒子模型。
最后根据这个盒子模型执行重绘机制,进入painting阶段,将页面绘制出来。
- 回流:
页面中元素的大小、位置等发生变化,影响页面布局的时候,就需要重新计算页面整体布局,我们称这个过程为回流机制。回流的性能消耗较大,因为回流会对所有DOM节点进行计算。
- 重绘:
页面中元素样式发生变化,需要对页面进行重新绘制的时候,我们称这个过程为重绘。回流一定会导致重绘,但是重绘却定不一定都需要回流,例如当元素只有背景色发生变化时,此时并不影响页面的布局,只需要重绘就可以。
| 标题 | 触发时机 |
|---|---|
| 回流 | 元素的大小、位置,字体的大小,内容的图片等发生变化时 |
| 重绘 | 页面回流、阴影、颜色等属性发生变化时 |
15、 虚拟DOM树和Diff算法
概述:
虚拟DOM树的本质是一个JS对象,用来描述当前页面中的节点元素,其作用是抽离数据的渲染过程使其具备跨端开发的能力。因为我们知道直接操作真实DOM会导致页面的回流与重绘,性能消耗较大,因此通过虚拟DOM技术将多次真实DOM操作合并称一次,就可以减少开销。
其具体做法如下:当页面第一次创建的时候会生成一个虚拟DOM树,并将其挂载到指定节点,成为真实DOM树。当页面发生变化的时候会重新生成一个虚拟DOM树,此时并不会直接操作真实DOM树,而是先通过DIff算法进行比较,将多次操作合并为一次,之后在对真实DOM树进行操作。
Diff算法是一种广度优先、逐层比较的算法,它只会对同级比较,而不会跨级。在比较的过程中,如果有v-for和key,就以key为唯一标识,当key发生变化时,就重新绘制;当key没有发生变化时,就不重新创建节点,而是复用现有节点,仅对数据进行更新。
16、 同步与异步 宏任务与微任务
概述:
- 同步:按照顺序依次执行,同一时间只执行一个
- 异步:在同一时间内同时执行,往往伴随多线程
浏览器的JS引擎是单线程的执行机制,会按照执行队列中的顺序依次将任务放到主线程中执行,这些依次执行的任务我们称之为同步任务。
而有些任务耗时较大,如果也按照同步的方式执行,可能会出现等待时间很长甚至卡死的情况,这时候浏览器会开辟新的线程,将这些耗时较大的任务放在新的线程中同时执行,当这些任务执行完毕时,会被放入到一个任务队列。我们称这些同时执行的任务为异步任务。
当主线程将执行队列中的任务执行完毕后,通过Event Loop即事件循环机制查找任务队列中的异步任务。其实,在这个任务队列中,会根据异步任务的性质分为两个队列,宏任务队列与微任务队列。
- 宏任务:异步任务中严格按照时间进行压栈和执行的任务,如
setTimeOutsetInverter等 - 微任务:需要在当前任务执行完之后,立即执行的任务,如
Promise.thenawait后面的异步函数等
主线程会优先将微任务队列中的回调函数放到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、 深拷贝与浅拷贝
概述:
深浅拷贝是对于引用型数据而言的,简单数据类型不存在这个问题。因为简单数据类型在进行赋值的时候,数据直接存储在栈中,而引用类型数据在赋值的时候,只在栈中存储一个指针,这个指针指向堆的一个空间,真实数据是存储在堆空间中的。
当我们将数据直接赋值给引用类型的数据时,只是将指向堆的指针赋值给新数据的栈中,并没有重新开辟空间,新旧数据的栈中存储的指针指向同一个堆空间,这样的拷贝称为浅拷贝。
浅拷贝有一些缺陷。比如,当我们对新数据进行修改的时候,会同时修改原有数据,导致数据紊乱。为了避免这样的情况,就需要用到深拷贝。
深拷贝会重新开辟空间,将原有数据拷贝到重新开辟的空间内。再将重新开辟空间的指针赋值给新数据的栈中。
常见的深拷贝方法有: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添加的成员,这些成员只能通过实例对象进行访问
-
静态成员:在构造函数上添加的成员,这些成员只能通过构造函数来访问,在实例对象上没有办法访问
继承是指子类拥有父类的方法和属性,常见的继承方式有以下几种:
-
原型链继承:我们将子类的
__proto__指向父类的实例对象,再将子类的construct指向子类的构造函数,这样就可以继承父类的方法,但是不能继承属性。 -
构造函数继承:我们在子类构造函数中,通过
.call()的方式改变父类构造函数的this指向,从而继承父类的属性,但是无法继承方法。 -
组合式继承:通过原型链继承 + 构造函数结合的方法,即继承方法又继承属性的方式称为组合式继承。
-
寄生式组合继承:与组合式继承类似,但我们不再将子类的
__proto__指向父类的实例对象,而是将子类的__proto__赋值为Object.create()方法,并在方法中传入父类的实例,这样就得到一个新的对象。在通过组合式继承的方式继承属性与方法。 -
extend继承:这是一种语法糖的写法,我们通过
class和extend关键字来实现继承。例如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:float、overfloew:hidden、position:absolute
<!-- 防止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>
<!-- 清除浮动 -->
<!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>
<!-- 防止父盒子塌陷 -->
<!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>