js/HTML
1、介绍JavaScript的数据类型
基本类型:字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol(独一无二的值)
引用类型:对象(Object)、数组(Array)、函数(Function)
注:Symbol 是 ES6 引入了一种新的原始数据类型,表示独一无二的值
2、基本数据类型与引用类型在存储上有什么区别?
1.存储位置不同:
基本数据类型:以栈的形式存储, 保存与赋值指向数据本身, 用typeof 来判断类型,存储空间固定。
引用类型:以堆的形式存储, 保存于赋值指向对象的一个指针, 用instanceof 来判断类型 , 存储空间不固定。
2.传值方式不同:
基本数据类型按值传递,无法改变一个基本数据类型的值
引用类型按引用传递,应用类型值可以改变
3、栈和堆的区别?
栈(stack):由编译器自动分配释放,存放函数的参数值,局部变量等;
堆(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统释放。
4、闭包的概念?优缺点?使用场景?
闭包的概念:闭包就是能读取其他函数内部变量的函数。
优点:
- 避免全局变量的污染
- 希望一个变量长期存储在内存中(缓存变量)
缺点: - 内存泄露(消耗)
- 常驻内存,增加内存使用量
为什么使用:
- 设计私有方法和变量
- 避免全局变量污染
- 希望变量长期驻扎在内存中 使用场景:封装功能时(需要使用私有的属性和方法),函数防抖、函数节流、函数柯里化、给元素伪数组添加事件需要使用元素的索引值。
5、垃圾回收机制。
垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为0(没有其他对象引用过该对象),或该对象的惟一引用是循环的,那么该对象的内存即可回收
6、JS的深、浅拷贝
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
- 浅拷贝:拷贝基本数据类型时,不受任何影响,当拷贝引用类型时,源对象也会被修改。
- 深拷贝:深拷贝就是完完全全拷贝一份新的对象,它会在内存的堆区域重新开辟空间,修改拷贝对象就不会影响到源对象
常见的浅拷贝方式: - 1、直接赋值
var user1 = {
name : '张三',
age : 18
}
var user2 = user1;
user2.name = "李四";
console.log(user1); // { name: '李四', age: 18 }
console.log(user2); // { name: '李四', age: 18 }
- 2、 Object.assign 方法
var user1 = {
name : "张三"
}
var user2 = Object.assign(user1, {age : 18}, {sex : 'male'});
console.log(user1); // { name: '张三', age: 18, sex: 'male' }
console.log(user2); // { name: '张三', age: 18, sex: 'male' }
这个方法可以实现拷贝,但是当属性的值是引用类型时,实现的也是浅拷贝
var user1 = {
name: "张三",
like: {
eat: "面条",
sport: "篮球",
},
};
var user2 = Object.assign({}, user1);
console.log(user1); //{name: '张三', like: {eat: '面条', sport: '篮球'}}
console.log(user2); //{name: '张三', like: {eat: '面条', sport: '篮球'}}
user1.name = "李四";
console.log(user1); //{name: '李四', like: {eat: '面条', sport: '篮球'}}
console.log(user2); //{name: '张三', like: {eat: '面条', sport: '篮球'}}
user1.like.eat = "米饭";
console.log(user1); //{name: '李四', like: {eat: '米饭', sport: '篮球'}
console.log(user2); //{name: '张三', like: {eat: '米饭', sport: '篮球'}}
- 3、ES6 扩展运算符
es6这个扩展运算符(...)在一层对象的话,就是深拷贝,多层的话就是浅拷贝
var user1 = {
name: "张三",
like: {
eat: "面条",
sport: "篮球",
},
};
var user2 = {...user1};
console.log(user1); //{name: '张三', like: {eat: '面条', sport: '篮球'}}
console.log(user2); //{name: '张三', like: {eat: '面条', sport: '篮球'}}
user1.name = "李四";
console.log(user1); //{name: '李四', like: {eat: '面条', sport: '篮球'}}
console.log(user2); //{name: '张三', like: {eat: '面条', sport: '篮球'}}
user1.like.eat = "米饭";
console.log(user1); //{name: '李四', like: {eat: '米饭', sport: '篮球'}
console.log(user2); //{name: '张三', like: {eat: '米饭', sport: '篮球'}}
通过上面的示例,我们发现 ES6 扩展运算符也是浅拷贝。
- 4、数组的 slice 和 concat 方法
这两个方法一个是截取,一个是拼接,这两个方法也可以用于拷贝数组
常见的深拷贝方式:
- 1、 JSON.parse(JSON.stringify(待拷贝对象))
var user = {
name: "张三",
age: 18,
like: {
eat: "面条",
sport: "篮球",
},
};
var target = JSON.parse(JSON.stringify(user));
target.like.eat = "米饭";
console.log(user); //{name: '张三', like: {eat: '面条', sport: '篮球'}}
console.log(target); //{name: '张三', like: {eat: '米饭', sport: '篮球'}}
但是这种方式有一个缺点,那就是里面的函数无法被拷贝。
-
2、 **jQuery 中的 *.extend(deep,target,object1,object2,....)` 通过这个方法就可以实现深浅拷贝。各个参数的说明如下:
-
deep:
true表示深拷贝,false表示浅拷贝 -
target:要拷贝的目标对象
-
object1:待拷贝的对象
-
3、 手写递归的方式来实现深拷贝
//origin表示待拷贝对象,target表示目标对象
function deepClone(origin, target) {
var target = target || {}, //容错处理,防止用户不传target值
toStr = Object.prototype.toString,
arrAtr = "[object Array]";
for (var prop in origin) {
//遍历对象
if (origin.hasOwnProperty(prop)) {
//防止拿到原型链属性
if (
origin[prop] !== "null" &&
typeof origin[prop] == "object"
) {
//判断是不是原始值
target[prop] =
toStr.call(origin[prop]) == arrAtr ? [] : {}; //建立相对应的数组或对象
deepClone(origin[prop], target[prop]); //递归,为了拿到引用值里面还有引用值
} else {
target[prop] = origin[prop]; //是原始值,直接拷贝
}
}
}
return target;
}
7、讲一下回流和重绘
在讨论回流与重绘之前,我们要知道:
- 浏览器使用流式布局模型 (Flow Based Layout)。
- 浏览器会把
HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了Render Tree。 - 有了
RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。 - 由于浏览器使用流式布局,对
Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一。
一句话:回流必将引起重绘,重绘不一定会引起回流。
回流(Reflow)
当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:
-
页面首次渲染
-
浏览器窗口大小发生改变
-
元素尺寸或位置发生改变
-
元素内容变化(文字数量或图片大小等等)
-
元素字体大小变化
-
添加或者删除可见的
DOM元素 -
激活
CSS伪类(例如::hover) -
查询某些属性或调用某些方法
一些常用且会导致回流的属性和方法: -
clientWidth、clientHeight、clientTop、clientLeft -
offsetWidth、offsetHeight、offsetTop、offsetLeft -
scrollWidth、scrollHeight、scrollTop、scrollLeft -
scrollIntoView()、scrollIntoViewIfNeeded() -
getComputedStyle() -
getBoundingClientRect() -
scrollTo()
重绘 (Repaint)
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
性能影响
回流比重绘的代价要更高。
如何减少重绘、避免重排?
本质上,就是合并修改。具体的措施有:
- DOM层面:DocumentFragment本质上是一个占位符,真正插入页面的是它的所有子孙节点,所以,将需要变动的DOM节点先汇总到DocumentFragment,然后一次性插入,可以减少DOM操作的次数。
- CSS层面:操作多个样式时,可以先汇总到一个类中,然后一次性修改
小总结
会引起元素位置变化的就会reflow,如博主上面介绍的,窗口大小改变、字体大小改变、以及元素位置改变,都会引起周围的元素改变他们以前的位置;不会引起位置变化的,只是在以前的位置进行改变背景颜色等,只会repaint;
8、js事件循环机制
js是单线程语言,但是实际运行中例如高清图片加载时间长等问题,为了解决这一问题,js又分为--同步任务和异步任务
-
同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数
-
当指定的事情完成时,Event Table会将这个函数移入Event Queue。
-
主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
-
上述过程会不断重复,也就是常说的Event Loop(事件循环) Event Loop(事件循环)中,每一次循环称为 tick, 每一次tick的任务如下:
-
执行栈选择最先进入队列的宏任务(通常是
script整体代码),如果有则执行 -
检查是否存在 Microtask,如果存在则不停的执行,直至清空 microtask 队列
-
更新render(每一次事件循环,浏览器都可能会去更新渲染)
-
重复以上步骤
宏任务与微任务:
异步任务分为 宏任务(macrotask) 与 微任务 (microtask),不同的API注册的任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。
宏任务(macrotask): :
script(整体代码)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 环境)
微任务(microtask):
Promise、 MutaionObserver、process.nextTick(Node.js环境)
宏任务 > 所有微任务 > 宏任务
浏览器渲染过程是怎样的?
- HTML被解析成DOM Tree,CSS被解析成CSS Rule Tree
- 在布局阶段,把DOM Tree和CSS Rule Tree经过整合生成Render Tree
- 元素按照算出来的规则,把元素放到它该出现的位置,通过显卡画到屏幕上
VUE
1. 父子组件生命周期顺序
Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:
-
加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
-
子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
-
父组件更新过程
父 beforeUpdate -> 父 updated
-
销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
2. 谈谈keep-alive
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:
- 一般结合路由和动态组件一起使用,用于缓存组件;
- 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
- 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。
3. 组件中的data为什么是一个函数而不是对象
因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
4.对nextTick的理解
Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新
使用场景
如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()
第一个参数为:回调函数(可以获取最近的DOM结构)
第二个参数为:执行函数上下文
实现原理小结
- 把回调函数放入callbacks等待执行
- 将执行函数放到微任务或者宏任务中
- 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调
5. diff算法
diff 算法是一种通过同层的树节点进行比较的高效算法
其有两个特点:
- 比较只会在同层级进行, 不会跨层级比较
- 在diff比较的过程中,循环从两边向中间比较
diff 算法在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较
diff整体策略为:深度优先,同层比较
- 比较只会在同层级进行, 不会跨层级比较
2. 比较的过程中,循环从两边向中间收拢
原理分析
当数据发生改变时,set方法会调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图
-
当数据发生改变时,订阅者
watcher就会调用patch给真实的DOM打补丁 -
通过
isSameVnode进行判断,相同则调用patchVnode方法 -
patchVnode做了以下操作:- 找到对应的真实
dom,称为el - 如果都有都有文本节点且不相等,将
el文本节点设置为Vnode的文本节点 - 如果
oldVnode有子节点而VNode没有,则删除el子节点 - 如果
oldVnode没有子节点而VNode有,则将VNode的子节点真实化后添加到el - 如果两者都有子节点,则执行
updateChildren函数比较子节点
- 找到对应的真实
-
updateChildren主要做了以下操作:- 设置新旧
VNode的头尾指针 - 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用
patchVnode进行patch重复流程、调用createElem创建一个新节点,从哈希表寻找key一致的VNode节点再分情况操作
- 设置新旧