js面试准备


个人准备面试的复习记录,加深印象;

事件循环机制

浏览器里面主要有浏览器进程、渲染进程、网络进程等;他们分工不同;
比如:浏览器进程负责顶部的窗口标签栏的渲染,交互,不包括页面的内容;网络进程负责网络相关;
渲染进程负责网页的渲染,一般情况下一个标签页就会开启一个渲染进行;

渲染进程

渲染进程开启后会开启一个渲染主线程,该进程是同步的;很多任务都是交给主线程去实现的,也就是说它很忙,当然渲染进程除了渲染主线程外还会有其它的进程(计时线程、交互线程等);假设一个任务是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兼容性查询网站

caniuse


深拷贝

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() 最完美的解决方法

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值