文章目录
个人准备面试的复习记录,加深印象;
事件循环机制
浏览器里面主要有浏览器进程、渲染进程、网络进程等;他们分工不同;
比如:浏览器进程负责顶部的窗口标签栏的渲染,交互,不包括页面的内容;网络进程负责网络相关;
渲染进程负责网页的渲染,一般情况下一个标签页就会开启一个渲染进行;
渲染进程
渲染进程开启后会开启一个渲染主线程
,该进程是同步
的;很多任务都是交给主线程去实现的,也就是说它很忙
,当然渲染进程除了渲染主线程外还会有其它的进程(计时线程、交互线程等);假设一个任务是setTimeout10分钟后在执行一个函数,由于主进程是同步的它就会一直等10分钟,就会阻塞进程(js阻塞
),那么它是怎么去优化这一点的呢?首先你要知道消息队列
以前是叫宏队列、微队列,微队列的优先级高于宏任务,根据最新的w3c给出的说明,不再有宏队列的叫法了,以为简单的宏队列已经不能满足当下复杂的业务了,于是有了延时队列、交互队列、微队列等,当一个延时函数先会由计时线程处理,然后放到延时队列里面,然后主线程根据队列的优先级同步
处理各种队列里面的任务,从而尽可能的避免js堵塞的问题;
解析html
遇到css
渲染主线程是从上往下解析html了,同时也会开启预解析器来处理css相关的内容,也就是css不会阻塞html的原因;
遇到js
渲染主进程解析html遇到js的时候,会暂停
;
重排、重绘
重排
改动了
几何属性
需要重新计算layout布局树
重绘
减少重绘重排
- 脱离文档流
- 使用transform
- 减少逐个递增,使用文档碎片
- 减少逐个样式的修改,可以加个class,一次性重排,避免多次重排
强制渲染
有些场景下需要重现渲染,但是由于渲染线程的机制可能不会渲染,这个时候你就需要强制渲染
了;
如下图所示的案例中,css中已经设置了过渡时间,希望两次设置div.style.transform,起到动画的效果;
要起到强制渲染,只需要达到重排
就行,可以在两次设置div.style.transform的中间加上div.clientWidth即可
js原生强制渲染
只要满足重排
的条件即可,也就是改变dom的几何属性
vue的强制渲染
- 简单粗暴的方式:重新加载整个页面
- 不妥的方式:使用 v-if
- 较好的方法:使用Vue的内置forceUpdate方法
- 最好的方法:在组件上进行 key 更改,任何标签和组件都可以加上key(
推荐
)
<template>
<div class="box_search" :key='show'>
</div>
</template>
<script lang="ts" setup>
const show=ref(1)
// 触发函数
const FN=()=>{
show.value += 1
}
// 当页面缩放时重新渲染
window.onresize = function () {
FN()
};
</script>
AO对象(执行期上下文)
函数调用前创建的一个对象,用来保存函数内部的执行环境,也叫执行期上下文,函数执行完后销毁,函数里面定义的变量,将会作为AO对象的属性,这也就解释了局部变量的概念;
预编译
无论全局预编译还是函数预编译,都是先查找变量声明,然后再查找函数声明,如果属性名称相同,那么函数声明将会替换变量声明,这就是为什么
函数声明优先级高
的原因;
全局预编译
GO对象(全局对象)的创建的过程,也就是全局预编译的过程;先执行全局预编译,然后再执行函数预编译;
流程
- 查找变量声明,作为GO对象的属性名,值为
undefined
- 查找函数声明,作为GO对象的属性名,值为
function
函数的预编译
流程
- 在函数被调用时,为当前函数产生AO对象
- 查找形参和变量声明作为AO对象的属性名称,值为undefined;
- 使用实参的值改变形参的值
- 查找函数声明,作为AO对象的属性名,值为function
作用域链[[scopes]]
全局作用域:GO对象(全局对象window)
局部作用域:AO对象(执行期上下文)
如下所示,函数执行的时候会创建AO对象,[[scopes]]索引0,然后GO对象的索引为1,AO的索引较小,也就是先会读取局部变量的原因
闭包
- 如果在内部函数使用了外部函数的变量, 就会形成闭包. 闭包保留了外部环境的引用;
- 如果内部函数被返回到了外部函数的外面, 在外部函数执行完后, 依然可以使用闭包里的值;
从代码的角度上来,闭包其实就是一个对象,里面的数组就是用到的外部变量,如下图所示的aa
闭包的形成
如果在内部函数使用了外部函数的变量, 就会形成闭包. 闭包保留了外部环境的引用;
下面的列子,形成了闭包,但是当内部函数执行完成后,闭包就失效了,闭包没有保持住
function a() {//外部函数
var aa = 100
function b() {//内部函数
console.log(aa)
}
b()
}
a()
闭包的保持
如果希望在函数调用后, 闭包依然保持, 就需要将内部函数返回到外部函数的外部;
调用 demo 函数, 实质上是调用内部函数, 在函数 b 的[[scopes]]属性中可以找到闭包对象, 从而访问到里面的值
function a() {
var num = 0
function b() {
console.log(num++)
}
return b
}
var demo = a()
console.dir(demo) //打印的是一个fun
demo() //打印的是0
demo() //打印的是1
- 全局解析器产生GO(全局对象):{demo:undefined,a:function}
- 执行代码,也就是a函数的预编译;创建a函数的AO对象aAO:{num:undefined,b:function}
- 执行var num=0的代码,aAO变成:{num:0,b:undefined}
- 执行return b的代码,同时GO对象变成:{demo:function,a:function}
- 执行第一个demo()方法,也就是b函数的预编译,创建b函数的bAO对象:{}
- 此时,b函数的作用域链[[scopes]]:
0:bAO
1:aAo
3:Global
- 于是bAO没有找到num,再aAo里面找到num,所以值为0,打印出来,然后++,aAo变成:{num:1,a:function}
- 第二次执行demo()也是入参,打印的值1。然后++,aAo变成:{num:2,a:function}
var和let的区别
- let声明的变量不能变量提升
- var是函数作用域,let是块状作用域
- 使用let声明的变量不属于顶层对象,在浏览器中指的是window,在node环境中指的是global对象
this指向
- 全局作用域下的话指向window
- 在方法体中,谁调用它,它就指向谁
- 构造函数中的this指向的是创建出来的对象
- 定时器的定时器,如果是普通形式指向的window,因为setTime是浏览器调用的,箭头函数除外
- 数组的方法中,forEach,map等,都是指向的window
- 事件触发方法,j普通形式,谁调用就是谁,箭头函数除外
//非箭头函数,dom dom.onClick(function(){ console.log(this) }) //箭头函数,window dom.onClick=()=>{ console.log(this) }
原型&&原型链&&继承
1.原型&&原型链
- 对象的隐式原型(proto)指向创造这个对象的构造函数的显示原型(prototype),其中浏览器输出的[[prototype]]实际上就是__proto__
function Foo(){}
const foo=new Foo()
console.log(foo.__proto__==Foo.prototype)
console.log(Foo==Foo.prototype.constructor)
- proto__是对象才有的,prototype是函数才有的,函数也是对象,所以函数既有__proto,也有prototype
2.继承
2.1原型链继承(引发引用类型数据共享问题
)
基础数据不影响,两个实例之间不会相互影响,引用类型的数据,两个实例之间会相互影响
function Father() {
this.arr = [1, 2, 3];
this.name = '张三';
}
Father.prototype.say = () => {
console.log('Father.prototype.say');
};
function Son() {}
Son.prototype = new Father(); //原型链继承。下方的Son实例将会继承Father的属性和方法
const son1 = new Son();
const son2 = new Son();
console.log('🚀 ~ file: index.vue:21 ~ son1:', son1);
console.log('son1.__proto__ === Son.prototype', son1.__proto__ === Son.prototype); //true
console.log(' son1.__proto__.__proto__ === Father.prototype', son1.__proto__.__proto__ === Father.prototype); //true
console.log(' Father.prototype.constructor === Fathere', Father.prototype.constructor === Father); //true
console.log(' son1.__proto__.__proto__.constructor === Father', son1.__proto__.__proto__.constructor === Father); //true
son1.name = '李四';
son2.name = '王五';
//基础数据不影响,两个实例之间不会相互影响
console.log('🚀 ~ file: index.vue:24 ~ son1:', son1.name); //李四
console.log('🚀 ~ file: index.vue:25 ~ son2:', son2.name); //王五
// 引用类型的数据,两个实例之间会相互影响
son1.arr.push(4);
console.log('🚀 ~ file: index.vue:29 ~ son1.arr:', son1.arr); //[1,2,3,4]
console.log('🚀 ~ file: index.vue:30 ~ son2.arr:', son2.arr); //[1,2,3,4]
2.1构造函数继承(无法获取到父类原型上的方法
)
function Father() {
// console.log('🚀 ~ file: index.vue:45 ~ Father ~ this:', this);
this.arr = [1, 2, 3];
this.name = '张三';
}
Father.prototype.say = () => {
console.log('Father.prototype.say');
};
function Son() {
// 如果不加apply(this),那么上面第44行打印的就是undefined,无法将arr绑定到undefined上,会报错
//加上apply(this)会指向到当前Son
Father.apply(this);
}
const son1 = new Son();
const son2 = new Son();
// 此时引用类型和基本类型的数据都不会相互影响,
son1.arr.push(5);
console.log('🚀 ~ file: index.vue:59 ~ son1.arr:', son1.arr);
console.log('🚀 ~ file: index.vue:59 ~ son2.arr:', son2.arr);
son1.say(); //但是无法访问原型上的方法say,会报错
2.3 组合继承(会触发两次父类的构造函数
)
- 也就是原型链继承+构造函数继承
- 也就是说构造函数继承(第二次)会覆盖原型链继承(第一次),但是不会覆盖原型上的属性和方法
- 能解决引用类型和基础数据共享的问题,但是会触发
2次
父类的构造函数
function Father() {
this.arr = [1, 2, 3];
this.name = '张三';
}
Father.prototype.say = () => {
console.log('Father.prototype.say');
};
function Son() {
Father.apply(this); //构造函数继承,当实例化的时候,会再次调用一次Father(),第二次调用
}
Son.prototype = new Father(); //原型链继承,代码走到这行的时候,即使你不实例化也会调用一次Father()。第一次调用Father
//也就是说构造函数继承(第二次)会覆盖原型链继承(第一次),但是不会覆盖原型上的属性和方法
const son1 = new Son();
const son2 = new Son();
// 此时引用类型和基本类型的数据都不会相互影响,
son1.arr.push(5);
console.log('🚀 ~ file: index.vue:59 ~ son1.arr:', son1.arr);
console.log('🚀 ~ file: index.vue:59 ~ son2.arr:', son2.arr);
son1.say(); //原型上的方法也能正常使用
2.4寄生组合继承
主要是用了ES5里面的Object.create()将Father.prototype复制到Son.protoyp
function Father() {
this.arr = [1, 2, 3];
this.name = '张三';
}
Father.prototype.say = () => {
console.log('Father.prototype.say');
};
function Son() {
Father.apply(this);
}
Son.prototype.subSay = () => {
console.log('Son.prototype.subSay');
};
//Object.create是Es5才有的,下面是兼容写法
if (!Object.create) {
Object.create = function (proto) {
function F() {}
F.prototype = proto;
return new F();
};
}
Son.prototype = Object.create(Father.prototype);
const son1 = new Son();
const son2 = new Son();
// 此时引用类型和基本类型的数据都不会相互影响,
son1.arr.push(5);
console.log('🚀 ~ file: index.vue:59 ~ son1.arr:', son1.arr);
console.log('🚀 ~ file: index.vue:59 ~ son2.arr:', son2.arr);
son1.say(); //原型上的方法也能正常使用
son1.subSay(); //如果 Son.prototype.subSay 的定义在Object.create(Father.prototype)之前则会报错,之后则没有这个问题
浏览器缓存
强制缓存
不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network专用可以看到该请求赶回200的状态码;
Expires
Expires是HTTP/1.0控制网页缓存的字段,其值为服务器返回该请求结果缓存的到期时间,即再次发起该请求时,如果客户端的时间小于Expires的值时,直接使用缓存结果;
缺点:
Expires控制缓存的原理是使用客户端的时间与服务端返回的时间做对比,那么如果客户端与服务端的时间因为某些原因(例如时区不同;客户端和服务端有一方的时间不准确)发生误差,那么强制缓存则会直接失效,这样的话强制缓存的存在则毫无意义;
Cache-Control
- public:所有内容都将被缓存(客户端和代理服务器都可缓存)
- private:所有内容只有客户端可以缓存,Cache-Control的默认取值
- no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定
- no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
- max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效
协商缓存
在使用本地缓存之前,需要向服务器发送请求,咨询是否命中缓存;
Last-Modified / If-Modified-Since
Last-Modified是服务器响应请求时,返回该资源文件在服务器最后被修改的时间;If-Modified-Since则是客户端再次发起该请求时,携带上次请求返回的Last-Modified值,通过此字段值告诉服务器该资源上次请求返回的最后被修改时间。服务器收到该请求,发现请求头含有If-Modified-Since字段,则会根据If-Modified-Since的字段值与该资源在服务器的最后被修改时间做对比,若服务器的资源最后被修改时间大于If-Modified-Since的字段值,则重新返回资源,状态码为200;否则则返回304,代表资源无更新,可继续使用缓存文件;
Etag / If-None-Match
Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),If-None-Match是客户端再次发起该请求时,携带上次请求返回的唯一标识Etag值,通过此字段值告诉服务器该资源上次请求返回的唯一标识值。服务器收到该请求后,发现该请求头中含有If-None-Match,则会根据If-None-Match的字段值与该资源在服务器的Etag值做对比,一致则返回304,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为200;
API兼容性查询网站
深拷贝
1. 解构赋值
值得注意的是,解构赋值实现出来的深拷贝,只能拷贝第一层结构的数据,如果对象里面再嵌套对象或者数组,则第二层的变量将是浅拷贝; 当值为undefined、function、symbol会在转换过程中被忽略
const a={name:'张三'}
const b=[...a] //解构赋值
const c={
project:{
name:'基础工程',
price:12
}
}
const d=[...c] //project属性将是浅拷贝
2. Object.assign()
局限性也是只能深拷贝第一层
var obj = {name:'123',age:13};
var obj2 = Object.assign({},obj);
3. JSON.parse(JSON.stringify(source))
function被忽略
var obj = {name:'123'};
var obj2 = JSON.parse(JSON.stringify(obj))
4. 手写深拷贝方法
/**
* 深拷贝
* @param source 需要拷贝的数据
* @returns target 目标数据
*/
export const deepCopy = (source: any) => {
//如果不是对象类型直接返回
if (typeof source !== 'object') return source;
//创建一个区分数组和对象的目标变量target
const target = source.constructor === Array ? [] : {};
//循环
Object.keys(source).forEach((key) => {
if (source.hasOwnProperty(key)) {
if (source[key] && typeof source[key] === 'object') {
//循环-如果是数组或者对象,则接着递归
target[key] = source[key].construtor === Array ? [] : {};
target[key] = deepCopy(source[key]);
} else {
//循环-如果不是对象,赋值到target
target[key] = source[key];
}
}
});
return target;
};
防抖节流(简单手写)
快速记忆:防抖也就是会终止之前的操作,而节流就是不会终止之前的操作,终止的是后面进来排队的操作
防抖
function debounce(fn, delay){
let timerId = null
return function(){
const context = this
if(timerId){window.clearTimeout(timerId)}
timerId = setTimeout(()=>{
fn.apply(context, arguments)
timerId = null
},delay)
}
}
节流
function throttle(fn, delay){
let timerId = null
return function(){
const context = this
if(!timerId){
timerId = setTimeout(()=>{
fn.apply(context, arguments)
timerId = null
},delay)
}
}
}
new操作符具体做了什么?
script标签里面的async和defer有什么区别?
- 如果没有上面这个两个属性的时候,浏览器会立刻加载并执行脚本(
同步
); - async,加载和渲染后面元素的过程中和script的加载是
异步
的,加载完成后立即执行
; - defer ,加载和渲染后面元素的过程中和script的加载是
异步
的,但是它要等待所有的元素解析完成后才会执行
;
对数据类型的检测方式有哪些?
- typeof() 适用于基本数据类型,遇到引用类型就不管用了
- instanceof() 适用于引用数据类型,不能判断基本数据类型
- constructor 基本上可以判断基本和引用类型数据
- Object.prototype.toString.call() 最完美的解决方法