2022年第三次面试,含泪整理万字面试题。

vue2

谈谈vue原理,和对vue的理解?

Vue 响应系统,其核心有三点:observe、watcher、dep:

  • observe:遍历 data 中的属性,使用 Object.defineProperty 的 get/set 方法对其进行数据劫持;
  • dep:每个属性拥有自己的消息订阅器 dep,用于存放所有订阅了该属性的观察者对象;
  • watcher:观察者(对象),通过 dep 实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应。

1.vue是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调。

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项时,Vue 将遍历通过observer方法监听data数据,在observer中会循环遍历data中的数据执行defineProperty方法把这些属性全部转为 getter/setter,这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

2.Object.defineProperty()它可以接收三个参数,第一个是目标对象,第二个是需要定义的属性或方法名,第三个是属性的描述符。

3.vue是一个基于mvvm模型设计的数据驱动视图的响应式框架。mvvm模型分为m数据层、v视图层、vm作为连接数据层和视图层的桥梁实现数据驱动视图,它也是一个对象,负责监听 Model 中数据的改变并且控制View视图的更新,处理用户交互操作。

4.Object.defineProperty()缺点:
深度监听需要递归到底,一次性计算量大。无法监听新增删除属性(vue.set vue./delete)无法监听数组,需要对数组的一系列方法重写来实现劫持数组元素等特殊处理。

5.VDOM和diff算法:
用JS去模拟DOM结构,计算出最小的变更,然后再操作DOM。 用VDOM来模拟DOM之后,我们可以用diff算法计算出Virtual DOM中真正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面。

    // 数据准备
    const data = {
      name: 'omg'
    }

    // 更新视图
    function updateView() {
      console.log('更新视图');
    }

    // 深度监听
    function defineReactive(target, key, value) {
      observer(target)
      Object.defineProperty(target, key, {
        get() {
          console.log('get');
          return value
        },
        set(newValue) {
          if (newValue !== value) {
            observer(target)
            value = newValue
            console.log('set');
            updateView()  // 更新视图
          }
        }
      })
    }

    // 监听对象属性
    function observer(target) {
      if (typeof target !== 'object' || target === null){
        return target
      }
      for (let key in target) {
        defineReactive(target, key, target[key])
      }
    }

    observer(data)
    data.name = 'kkk'
   
vue2和vue3原理有哪些不同?

vue2的核心是defineProperty 来劫持各个属性的getter和setter,通过重写数组的一系列方法来实现劫持数组元素。Object.defineproperty是对所有的属性做的数据劫持不是目标对象,而且对数组是无法进行劫持的,也就是数组的变化监听实际上是在原有的数组方法上进行的改造实现的;vue2的问题是修改和删除对象的属性不会被劫持。

vue3的核心是通过Proxy和Reflect这两个api实现的响应式,这样就解决了vue2的问题。

1.vue3支持大多数vue2的特性,支持TS。

2.使用proxy代替了Object.definproperty()实现数据的响应式

3.vue3新提出了组合式api相比于选项式api复用性更强。

4.重写虚拟DOM的实现和Tree-Shaking

谈一谈对vuex的理解?

vuex是实现组件全局状态管理的一种机制,可是方便的实现组件之间的数据共享。

主要分为五部分:

1.state:储存全局状态

2.mutation:修改全局状态的且只能执行同步代码

3.active:通过执行mutation来修改全局状态,可以执行异步操作

4.getter:类似于computed计算属性,对全局状态的进一步修改处理

5.module:可以将store分为不同的模块

谈一谈对路由的理解?

路就是道路,由也有途径的意思,所以从字面上理解就是地址、路径的意思。那么在前端中路由就是路径和组件的映射关系,当然也是和后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源。前端把路由分为哈希(hash)路由和历史(history)路由。

hash(哈希)路由的特点是有#标志,是通过监听哈希值的变化来触发hashchange事件去匹配组件然后渲染。

history路由没有#也更符合url标准,它是通过pushState和popState两个api来实现页面不刷新跳转的,因为pushState和repalace都不会出发页面刷新但是去可以修改活动历史记录从而修改url,再通过监听popstate事件来实现无刷新跳转。

但是使用history模式还有一个问题就是,在访问二级页面的时候,做刷新操作,会出现404错误,那么就需要和后端人配合让他配置一下apache或是nginx的url重定向,重定向到你的首页路由上就ok啦。

路由传参方式有哪些?
this.$router.push({
    path:`/home/${id}`,
})

路由配置
{
    path:"/home/:id",
    name:"Home",
    component:Home
}
在Home组件中获取参数值
this.$route.params.id


name----params:
通过name来匹配路由,通过param来传递参数
this.$router.push({
    name:'Home',
    params:{
        id:id
    }
})
用params传递参数,不使用:/id
{
    path:'/home',
    name:Home,
    component:Home
}
Home组件中获取参数
this.$route.params.id


path----query
this.$router.push({
    path:'/home',
    query:{
        id:id
    }
})
路由配置
{
    path:'/home',
    name:Home,
    component:Home
}
获取参数的方法
this.$route.query.id


vue-router守卫:

vue-router全局有三个守卫:

router.beforeEach 全局前置守卫 进入路由之前

router.beforeResolve 全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用

router.afterEach 全局后置钩子 进入路由之后

    // main.js 入口文件
    import router from './router'; // 引入路由
    router.beforeEach((to, from, next) => { 
      next();
    });
    router.beforeResolve((to, from, next) => {
      next();
    });
    router.afterEach((to, from) => {
      console.log('afterEach 全局后置钩子');
    });

路由独享守卫:

    const router = new VueRouter({
      routes: [
        {
          path: '/foo',
          component: Foo,
          beforeEnter: (to, from, next) => { 
            // 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
            // ...
          }
        }
      ]
    })

路由组件内的守卫:

beforeRouteEnter 进入路由前

beforeRouteUpdate (2.2) 路由复用同一个组件时

beforeRouteLeave 离开当前路由时

  beforeRouteEnter (to, from, next) {
    // 在路由独享守卫后调用 不!能!获取组件实例 `this`,组件实例还没被创建
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用 可以访问组件实例 `this`
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用,可以访问组件实例 `this`
  }

简述Ajax原理?

它是一个可以发送异步请求的技术,可以做到无刷新发送请求。

发送一个请求分为四步:

1.实例化一个XMLHttpRequest对象

2.通过open方法与服务端建立连接,指定method和url

3.通过send方法传递请求体

4.通过onreadystatechange监听服务端状态readystate01234,然后获取响应体。

//  readystate01234:

0,表示 XMLHttpRequest 实例已经生成,但是实例的`open()`方法还没有被调用。
1,表示`open()`方法已经调用,但是实例的`send()`方法还没有调用,仍然可以使用实例的`setRequestHeader()`方法,设定 HTTP 请求的头信息。
2,表示实例的`send()`方法已经调用,并且服务器返回的头信息和状态码已经收到。
3,表示正在接收服务器传来的数据体(body 部分)。这时,如果实例的`responseType`属性等于`text`或者空字符串,`responseText`属性就会包含已经收到的部分信息。
4,表示服务器返回的数据已经完全接收请求已经完成。
在浏览器地址栏中输入地址后浏览器发生了什么?

要知道每一个服务器都有一个对应的ip地址,但是不方便记忆所以出现了域名,当我们在浏览器上输入了一个地址实际上就是一个与服务端对应的域名,然后找到对应的服务器获取资源的过程。

这一过程具体的是:

1.DNS解析:将域名解析为ip地址,查找过程是从本地域名服务器到根域名服务器再到顶级域名服务器。

2.IP解析出来后开始建立tcp连接,tcp三次握手建立连接,本质是确认双方的接收能力和发送能力是否正常

3.发送http请求

4.服务器处理http请求并返回报文

5.浏览器解析并渲染页面,浏览器是一个边解析边渲染的过程。首先浏览器的HTML解析器解析HTML文件构建DOM树,然后CSS解析器解析CSS文件构建渲染树,再结合HTML树和CSS树构建render树(渲染树),等到渲染树构建完成后,浏览器开始布局渲染树并将其绘制到屏幕上 。

6.http连接断开,tcp四次挥手断开连接

keep-alive的作用

组件缓存,用于那些频繁切换的页面,减少因页面频繁切换导致页面频繁的创建和销毁等损耗性能的情况。里面有两个生命周期钩子,创建actived和销毁deactived,你可以在这两个钩子里面做一些事情。

问题:
使用vue2.0中的keep-alive来缓存页面,导致每次编辑页面保存后返回到list页的数据不会实时更新:使用keep-alive后,mounted生命周期函数不会每次都触发,而vue新增了activated和deactivated函数,所以我们可以在activated函数里执行一次ajax请求,list页面就会实时更新了

v-if与v-show的区别

v-if通过创建删除节点来显示隐藏元素,v-show通过display:none来显示隐藏元素

vue3

vue3优点?

更好的支持TS,打包大小减少41%初次渲染快55%, 更新渲染快133%,内存减少54%,使用Proxy代替defineProperty实现数据响应式,重写虚拟DOM的实现和Tree-Shaking,以及组合式api。

vue3常用api有哪些?

setup,ref,reactive,computed ,watch ,watchEffect ,toRefs,toRef

ref,reactive的区别?

ref要用value才能获取到值,ref接收基本数据类型会返回一个ref引用对象,接收复杂数据类型会调用reactive处理成一个代理对象,其实是先有的reactive,由于reactive只能处理对象数组所以才有的ref。

计算属性和监听?

计算属性:只接收一个回调默认为get,

组合式api和选项式api区别?

选项式 API 以“组件实例”的概念为中心 ,简单项目使用较为合适,组合式api这种形式更加自由灵活,可以使得组织和重用逻辑的模式变得更加强大 。

TS中的类型?

在js基础上扩展了:元组,枚举,any,void

接口和类型别名区别?

接口可以用来定义对象类型,类型别名不仅可以定义对象类型还行可以定义基本类型元组等。

接口命名重复会合并,类型别名会报错。

接口通过extends继承,类型别名通过&继承。

什么是泛型?

泛型是指在定义函数、接口或类的时候不预先指定具体的类型,而是在使用的时候在指定的一种形式(专门存类型的变量)。

三件套

HTML&CSS:

H5新增特性

1.语义化标签:header,nav,footer ,aside, article, section,audio, video

2.阴影:文字阴影(text-shadow),盒子阴影 (box-shadow)

3.表单控件:calendar , date , time , email , url , search , tel , file , number

4.画布: Canvas,svg

5.浏览器存储:localstory,sessionstory

6.拖拽释放(Drap and drop) API ondrop

CSS3新特性

1、弹性布局 flex

2、阴影 :文字阴影(text-shadow)盒子阴影 : box-shadow

3、边框: 圆角(border-radius)

4、盒子模型: box-sizing

5、背景:background-size background-origin background-clip

6、渐变: linear-gradient , radial-gradient

7、过渡 : transition 可实现动画

8、自定义动画 animate @keyfrom

9、媒体查询 多栏布局 @media screen and (width:800px) { …}

10、2D3D 转换;transform: translate(x,y) rotate(x,y) skew(x,y) scale(x,y)

11、新增 RGBA , HSLA 模式

纯css手写大于号
盒子模型
如何使一个盒子水平垂直居中?

1.利用flex布局(设置在父元素)

display:flex;justify-content:center;align-items:center;

2.利用定位 + margin:auto

position: absolute;margin: auto;top: 0;left: 0;right: 0;bottom: 0;

3.利用定位 + 转换

position: absolute;top:50%;left:50%;transform:translate(-50%,-50%)

什么是BFC,如何创建BFC?

BFC:格式化上下文(formatting context) ,是有一定规则的环境,不同BFC区域下的盒子有不同的表现,BFC是一个完全独立的空间(布局环境),让空间里的子元素不会影响到外面的布局。那么怎么使用BFC呢,BFC可以看做是一个CSS元素属性。

BFC规则:

BFC就是一个块级元素,块级元素会在垂直方向一个接一个的排列

BFC就是页面中的一个隔离的独立容器,容器里的标签不会影响到外部标签

垂直方向的距离由margin决定, 属于同一个BFC的两个相邻的标签外边距会发生重叠

计算BFC的高度时,浮动元素也参与计算

触发BFC:

float值为left、right

overflow值不为 visible,为 auto、scroll、hidden

display值为 inline-block、table-cell、table-caption、table、inline-table、flex、inline-flex

position值为 absolute、fixed`

css动画js动画区别?

CSS 动画大多数都是补间动画(原因是只需要添加关键帧的位置,其他的未定义的帧会被自动生成),而 JS 动画是逐帧动画,CSS动画的渲染成本小,并且它的执行效率高于 JavaScript 动画。

JS 动画是逐帧动画,在时间帧上绘制内容,一帧一帧的,所以他的可再造性很高,几乎可以完成任何你想要的动画形式。但是由于逐帧动画的内容不一样,会增加制作的负担,占用比较大的资源空间。细腻的动画,可控性高,炫酷高级的动画。

display: none; visibility: hidden;opacity: 0;区别?

display: none,dom树中存在节点,页面不占位置

visibility: hidden,dom树中存在节点,页面占位置

opacity: 0,dom树中存在节点,页面占位置

JavaScript:

什么是闭包?

一个作用域可以访问另外一个函数内部的局部变量 ,或者说一个函数 (子函数)访问另一 个函数 (父函数)中的变量。此时就会有闭包产生 ,那么这个变量所在的函数我们就称之为闭包函数。

闭包的主要作用: 延伸了变量的作用范围, 因为闭包函数中的局部变量不会等着闭包函数 执行完就销毁, 因为还有别的函数要调用它 , 只有等着所有的函数都调用完了他才会销毁 闭包会造成内存泄漏, 如何解决: 用完之后手动释放。

应用场景:

1.原生的setTimeout传递的第一个函数不能带参数,通过闭包可以实现传参效果。
function f1(a) {
    function f2() {
        console.log(a);
    }
    return f2;
}
var fun = f1(1);
setTimeout(fun,2000);//一秒之后打印出1

2.vue中的计算属性


3.防抖节流
防抖:当一个动作连续触发,只执行最后一次。
智能提示搜索框,访问量大的页面。
节流:限制一个函数在一定时间内只能执行一次。
短信验证码
原型原型链的理解?

在JavaScript 中, 原型也是一个对象,通过原型可以实现对象的属性继承,JavaScript 的对象中都包含了一个” prototype” 内部属性,这个属性所对应的就是该对象的原型。

当访问一个对象的某个属性时, 会先在这个对象本身属性上查找, 如果没有找到, 则会去 它的__proto__隐式原型上查找, 即它的构造函数的 prototype, 如果还没有找到就会再 在构造函数的 prototype 的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。

谈谈ES6?

1.变量
var, let, const的区别?

var有变量提升,let, const没有。
var可以重复声明变量,let, const不能。
var没有块级作用域,let, const有。
var变量挂载在window对象中,let, const是挂载在和Global对象平级的Script对象中。
const为常量声明时要给初始值且声明后不可更改(引用类型可以,不可修改的是栈中地址)。
let为变量无论什么类型都可以修改。

2.箭头函数
什么是箭头函数,this指向?

3.模板字符串
可以在模板字符串中使用变量和调用函数

4.解构赋值
对象和数组的解构赋值,以及别名和初始值。

5.展开运算符

6.class类

7.Promise 对象

从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。
Promise 异步操作有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。
Promise 对象只有:从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ,状态就不会再变了即 resolved(已定型)。

const p1 = new Promise(function(resolve,reject){
    resolve('success1');
    resolve('success2');
}); 
const p2 = new Promise(function(resolve,reject){  
    resolve('success3'); 
    reject('reject');
});
p1.then(function(value){  
    console.log(value); // success1
});
p2.then(function(value){ 
    console.log(value); // success3
});

状态的缺点:

无法取消 Promise ,一旦新建它就会立即执行,无法中途取消。

如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。

当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

then 方法:

then 方法接收两个函数作为参数,第一个参数是 Promise 执行成功时的回调,第二个参数是 Promise 执行失败时的回调,两个函数只会有一个被调用。

方法的特点:

在 JavaScript 事件队列的当前运行完成之前,回调函数永远不会被调用。

const p = new Promise(function(resolve,reject){
  resolve('success');
});
 
p.then(function(value){
  console.log(value);
});
 
//先执行队列,才会执行回调函数
console.log('first');
// first
// success

通过 .then 形式添加的回调函数,不论什么时候,都会被调用。

通过多次调用,可以添加多个回调函数,它们会按照插入顺序并且独立运行。

const p = new Promise(function(resolve,reject){
  resolve(1);
}).then(function(value){ // 第一个then // 1
  console.log(value);
  return value * 2;
}).then(function(value){ // 第二个then // 2
  console.log(value);
}).then(function(value){ // 第三个then // undefined
  console.log(value);
  return Promise.resolve('resolve'); 
}).then(function(value){ // 第四个then // resolve
  console.log(value);
  return Promise.reject('reject'); 
}).then(function(value){ // 第五个then //reject:reject
  console.log('resolve:' + value);
}, function(err) {
  console.log('reject:' + err);
});

then 方法将返回一个 resolved 或 rejected 状态的 Promise 对象用于链式调用,且 Promise 对象的值就是这个返回值。

Pomise.all的使用:吗

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

let p1 = new Promise(function(resolve) {
  resolve(1);
});
let p2 = new Promise(function(resolve) {
  resolve(2);
});
let p3 = new Promise(function(resolve) {
  resolve(3);
});

let promiseAll = Promise.all([p1, p2, p3]);
promiseAll.then(function(res) {
  console.log(res);
});

Promse.all在处理多个异步处理时非常有用,比如说一个页面上需要等两个或多个ajax的数据回来以后才正常显示,在此之前只显示loading图标。

需要特别注意的是,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,即p1的结果在前,即便p1的结果获取的比p2要晚。这带来了一个绝大的好处:在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用Promise.all毫无疑问可以解决这个问题。

8.async、 await

await 命令后面是一个 Promise 对象,它也可以跟其他值,如字符串,布尔值,数值以及普通函数。
await针对所跟不同表达式的处理方式:

-   Promise 对象:await 会暂停执行,等待 Promise 对象 resolve,然后恢复 async 函数的执行并返回解析值。
-   非 Promise 对象:直接返回对应的值。

9.Set 数据结构

10.对象扩展:Object.keys()方法,Object.assign ()

11.for…of 循环

12.import 和 export

事件循环机制?

js是单线程非阻塞的脚本语言,但也需要执行异步任务,所以事件循环应运而生。

任务进入执行栈之后会分为同步任务和异步任务,同步任务进入主线程执行,异步任务会进入Event Table并注册回调函数,当指定的事情完成时(比如接口调用),Event Table会将这个函数移入任务队列(task Queue),所有的同步
任务执行完后会到任务队列中查找是否有可执行的任务,这样循环往复形成事件循环机制。

常见的微任务有:

  • Promise.then
  • MutaionObserver
  • Object.observe(已废弃;Proxy 对象替代)
  • process.nextTick(Node.js)

常见的宏任务有:

  • script (可以理解为外层同步代码)
  • setTimeout/setInterval
  • UI rendering/UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

当一个宏任务执行完后判断是否有可执行的微任务,有的话执行没有直接执行新的宏任务。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EQgVcbA6-1661385833040)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a3c127ab5016421cbb759797967946b7~tplv-k3u1fbpfcp-watermark.image?)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sxuhXSMu-1661385833041)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/37fb8f9b542f4aafb4e57a6caa334429~tplv-k3u1fbpfcp-watermark.image?)]

深浅拷贝?

1.赋值:基本数据类型相当于是"深拷贝",引用类型相当于是浅拷贝。

// 基本类型
let num=2;
let n=num;
n=222;
console.log(n); 222
console.log(num);2

// 引用类型
let obj1 = {
    name: 'joney',
    school: {
        address:"湖南"
    }
}
let obj2 = obj1
obj2.name = 'woody'
obj2.school.address='上海'

console.log(obj1); //woody, 上海
console.log(obj2); //woody, 上海

2.浅拷贝:
浅拷贝是不完全拷贝,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝,如果属性值是基本数据类型则是"深拷贝",如果是引用类型则是"浅拷贝"(拷贝的就是内存地址)。

Object.assign :
-   如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性;
-   因为`null` 和 `undefined` 不能转化为对象,所以第一个参数不能为`null`或 `undefined`,否则会报错;

实际上,Object.assign 会循环遍历原对象的可枚举属性,通过复制的方式将其赋值给目标对象的相应属性。

...扩展运算符:
扩展运算符 和 Object.assign 实现的浅拷贝的功能差不多,如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便,不调用set(),Object.assign()调用set()。

3.深拷贝:
当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝在拷贝的时候,需要将当前要拷贝的对象内的所有引用类型的属性进行完整的拷贝,也就是说拷贝出来的对象和原对象之间没有任何数据是共享的,所有的东西都是自己独占的一份。

1.JSON.parse(JSON.stringify())
2.手写递归
3.引用第三方库的`lodash`的`_.cloneDeep()`
取url参数
  let url = window.location.href
  function parseQuery(url) {
    url = url || '';
    const queryObj = {};
    const reg = /[?&]([^=&#]+)=([^&#]*)/g;
    const queryArr = url.match(reg) || [];
    for (const i in queryArr) {
        if (Object.hasOwnProperty.call(queryArr, i)) {
            const query = queryArr[i].split('=');
            const key = query[0].substr(1);
            const value = decodeURIComponent(query[1]);
            queryObj[key] ? queryObj[key] = [].concat(queryObj[key], value) : queryObj[key] = value;
        }
    }
    return queryObj;
  }
  console.log(parseQuery(url));
js执行上下文:

(49条消息) 一篇了解 JS的上下文(context)_卡乐C前端的博客-CSDN博客_js 上下文

js中this的指向问题:

1.普通函数

全局作用域中或者普通函数中this指向全局对象window

// 非严格模式this指向window
function hello() { 
   console.log(this);  // window 
}  
hello();

// 严格模式this是undefined
function hello() { 
   'use strict';
   console.log(this);  // undefined
}  
hello();

// setTimeout没有明确的调用者,所以指向window
const obj = {
    num: 10,
   hello: function () {
    console.log(this);    // obj
    setTimeout(function () {
      console.log(this);    // window
    });
   }    
}
obj.hello();

2.箭头函数

箭头函数本身没有this,与父级作用域this相同。

const obj = {
    num: 10,
   hello: function () {
    console.log(this);    // obj
    setTimeout(() => {
      console.log(this);    // obj
    });
   }    
}
obj.hello();

const obj = {
    num: 10,
   hello: function () {
    console.log(this);    // obj,方法声明中的this指向调用者。
    setTimeout(function () {
      console.log(this);    // window
    });
   }    
}
obj.hello();

3.事件中this指向事件源对象

let btn = document.querySelector('.btn');

btn.addEventListener('click', function() {
     console.log(this);// <div class='btn'>点击</div>
})

4.构造函数中this指向

构造函数中的this指向实例化出来的对象。

 function CreatPerson(){
    this.name = "李伟";
    console.log(this);
    console.log(this.name);
}

var per1 = new  CreatPerson();
js中 call()、apply()、bind() 的用法

call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象,第二个参数差别就来了:
call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 。
apply 的所有参数都必须放在一个数组里面传进去 。
bind 除了返回是函数必须调用它才会被执行,它的参数和 call 一样。

const obj = {
  name: '哈哈',
  objAge: this.age,
  myFun(fm, t) {
    console.log(`${this.name}年龄${this.age}来自${fm}去往${t}`);
  }
}

const db = {
  name: '姐姐',
  age: '11'
}

obj.myFun.call(db, '成都', '上海')
obj.myFun.apply(db, ['成都', '上海'])
obj.myFun.bind(db, '成都', '上海')()
obj.myFun.bind(db, ['成都', '上海'])()

基础算法:

手写数组去重:
 双重for循环
        let arr = [1, 2, 3, 4, 5, 5, 4, 6, 1]
        let newarr = [arr[0]]
        for (let i = 1; i < arr.length; i++) {
            let flag = true
            for (let j = 0; j < newarr.length; j++) {
                if (arr[i] === newarr[j]) {
                    flag = false;
                    break
                }
            }
            if (flag) {
                newarr.push(arr[i])
            }
        }
        console.log(newarr);

indexOf
        let arr = [1, 2, 3, 4, 5, 5, 4, 6, 1]
        let newarr = Array.prototype.filter.call(arr, function (item, index) {
            return arr.indexOf(item) === index;
        });

        console.log(newarr);


        let list = [1, 2, 3, 4, 5, 5, 4, 6, 1]
        let newlist = []
        for (let i = 0; i < list.length; i++) {
            if(newlist.indexOf(list[i]) === -1){
                newlist.push(list[i])
            }
        }
        console.log(newlist);

set
        let list = [1, 2, 3, 4, 5, 5, 4, 6, 1]
        let newlist =[...new Set(list)]
        console.log(newlist);
       
        let list = [1, 2, 3, 4, 5, 5, 4, 6, 1]
        let newlist = Array.from(new Set(list))
        console.log(newlist);
冒泡排序

冒泡排序原理是重复扫描待排序序列,并比较每一对相邻的元素,当该对元素顺序不正确时进行交换。一直重复这个过程,直到没有任何两个相邻元素可以交换,就表明完成了排序。

const arr = [5,3,6,9,8,5,3,2]

function bubbleSort(arr) {
  let temp;
  for (let i, i < arr.length -1, i++) {
    for(let j, j< arr.length -1 -i, j++) {
      if(arr[i]>arr[j]){
        temp = arr[j]
        arr[j] = arr[j+1]
        arr[j+1] = temp
      }
    }
  }
}

console.log(bubbleSort(arr))

性能优化:

减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化 和 页面渲染优化。

资源加载优化

1.减少入口文件体积: 常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加。

2.UI框架按需加载: 合并、压缩文件;按需加载代码,减少冗余代码。

3.静态资源使用(CDN): 解决用户与服务器物理距离对响应时间的影响,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。
浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。
本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到全局负载均衡系统(GSLB)的 IP 地址。
本地 DNS 再向 GSLB 发出请求,GSLB 的主要功能是根据本地 DNS 的 IP 地址判断用户的位置,筛选出距离用户较近的本地负载均衡系统(SLB),并将该 SLB 的 IP 地址作为结果返回给本地 DNS。
本地 DNS 将 SLB 的 IP 地址发回给浏览器,浏览器向 SLB 发出请求。
SLB 根据浏览器请求的资源和地址,选出最优的缓存服务器发回给浏览器。
浏览器再根据 SLB 发回的地址重定向到缓存服务器。
如果缓存服务器有浏览器需要的资源,就将资源发回给浏览器。如果没有,就向源服务器请求资源,再发给浏览器并缓存在本地。

4.资源缓存: 不重复加载相同的资源。

强缓存:

在浏览器加载资源的时候,首先会根据请求头的expirescache-control判断是否命中强缓存策略,判断是否向远程服务器请求资源还是去本地获取缓存资源。
在浏览器中,强缓存分为Expires(http1.0规范,绝对时间)、cache-control(http1.1规范,相对时间)两种。

在浏览器第一个请求资源时,服务器端的响应头会附上Expires这个响应字段,当浏览器在下一次请求这个资源时会根据上次的expires字段是否使用缓存资源(当请求时间小于服务端返回的到期时间,直接使用缓存数据)。expires是根据本地时间来判断的,假设客户端和服务器时间不同,会导致缓存命中误差。

Expires有个缺点,当客户端本地时间和服务器时间不一致时会产生误差,浏览器会直接向服务器请求新的资源,为了解决这个问题,在http1.1规范中,提出了cache-control字段,且这个字段优先级高于上面提到的Expires,值是相对时间。

协商缓存:

上面提到的强缓存都是由本地浏览器在确定是否使用缓存,当浏览器没有命中强缓存时就会向浏览器发送请求,验证协商缓存是否命中,如果缓存命中则返回304状态码,否则返回新的资源数据。

协商缓存(也叫对比缓存)是由服务器来确定资源是否可用,这将涉及到两组字段成对出现的,在浏览器第一次发出请求时会带上字段(Last-Modified或者Etag),则后续请求则会带上对于的请求字段(if-modified-since或者if-none-Match),若响应头没有Last-Modified或者Etag,则请求头也不会有对应的字段

缓存流程:

1.当浏览器发起一个资源请求时,浏览器会先判断本地是否有缓存记录,如果没有会向浏览器请求新的资源,并记录服务器返回的last-modified

2.如果有缓存记录,先判断强缓存是否存在(cache-control优先于expires,后面会说),如果强缓存的时间没有过期则返回本地缓存资源(状态码为200)

3.如果强缓存失效了,客户端会发起请求进行协商缓存策略,首先服务器判断Etag标识符,如果客户端传来标识符和当前服务器上的标识符是一致的,则返回状态码 304 not modified(不会返回资源内容)

4.如果没有Etag字段,服务器会对比客户端传过来的if-modified-match,如果这两个值是一致的,此时响应头不会带有last-modified字段(因为资源没有变化,last-modified的值也不会有变化)。客户端304状态码之后读取本地缓存。如果last-modified

5.如果Etag和服务器端上的不一致,重新获取新的资源,并进行协商缓存返回数据。

5.图片资源的压缩: 图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素 对于所有的图片资源,我们可以进行适当的压缩 对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力,字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便。并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小。

6.将CSS放在文件头部,JavaScript 文件放在底部:
如果这些 CSS、JS 标签放在 HEAD 标签里,并且需要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部(不阻止 DOM 解析,但会阻塞渲染),等 HTML 解析完了再加载 JS 文件,尽早向用户呈现页面的内容。
先加载 HTML 再加载 CSS,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了避免这种情况发生,就要将 CSS 文件放在头部了。

7.压缩文件: 压缩文件可以减少文件下载时间,让用户体验性更好。得益于 webpack 和 node 的发展,现在压缩文件已经非常方便了。
在 webpack 可以使用如下插件进行压缩:\

  • JavaScript:UglifyPlugin
  • CSS :MiniCssExtractPlugin
  • HTML:HtmlWebpackPlugin

其实,我们还可以做得更好。那就是使用 gzip 压缩。可以通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。当然,服务器也得支持这一功能。

gzip 是目前最流行和最有效的压缩方法。举个例子,我用 Vue 开发的项目构建后生成的 app.js 文件大小为 1.4MB,使用 gzip 压缩后只有 573KB,体积减少了将近 60%。

页面渲染优化

1.减少重绘重排

浏览器渲染过程:

  1. 解析HTML生成DOM树。
  2. 解析CSS生成CSSOM规则树。
  3. 解析JS,操作 DOM 树和 CSSOM 规则树。
  4. 将DOM树与CSSOM规则树合并在一起生成渲染树。
  5. 遍历渲染树开始布局,计算每个节点的位置大小信息。
  6. 浏览器将所有图层的数据发送给GPU,GPU将图层合成并显示在屏幕上。

重排和重绘这两个操作都是非常昂贵的,因为 JavaScript 引擎线程与 GUI 渲染线程是互斥,它们同时只能一个在工作。

什么操作会导致重排?

  • 添加或删除可见的 DOM 元素
  • 元素位置改变
  • 元素尺寸改变
  • 内容改变
  • 浏览器窗口尺寸改变

如何减少重排重绘?

  • 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。
  • 如果要对 DOM 元素执行一系列操作,可以将 DOM 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素(display:none)或文档碎片(DocumentFragement),都能很好的实现这个方案。

使用事件委托: 事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。

<ul>
  <li>苹果</li>
  <li>香蕉</li>
  <li>凤梨</li>
</ul>

// good
document.querySelector('ul').onclick = (event) => {
  const target = event.target
  if (target.nodeName === 'LI') {
    console.log(target.innerHTML)
  }
}

// bad
document.querySelectorAll('li').forEach((e) => {
  e.onclick = function() {
    console.log(this.innerHTML)
  }
}) 

前端工程化

谈谈对webpack的理解?

entry:入口,提示webpack从哪个文件开始打包,分析构建内部依赖图。

output:输出,提示webpack打包后的资源bundles输出到那个文件去,以及如何命名。

loader:翻译,由于webpack只认识js文件,所以需要loader将非js,json文件转化为webpack认识的文件。

plugins:插件,插件可以用于执行范围更广的任务,从打包优化和压缩,一直到重新定义环境中的变量等。

mode:模式,开发和生产(优化压缩)

前端模块化

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值,ES6 模块输出的是值的引用。

AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。

require和import的区别
  1. 导入require 导出 exports/module.exportsCommonJS 的标准,通常适用范围如 Node.js
  2. import/exportES6 的标准,通常适用范围如 React
  3. require赋值过程并且是运行时才执行,也就是同步加载
  4. require 可以理解为一个全局方法,因为它是一个方法所以意味着可以在任何地方执行。
  5. import解构过程并且是编译时执行,理解为异步加载
  6. import 会提升到整个模块的头部,具有置顶性,但是建议写在文件的顶部。

require 的性能相对于 import 稍低。
因为 require 是在运行时才引入模块并且还赋值给某个变量,而 import 只需要依据 import 中的接口在编译时引入指定模块所以性能稍高

网络:

http和https区别

1.http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

2.http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

3.https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。

4.https相比于http更有利于SEO

5.HTTP使用TCP三次握手建立连接,客户端和服务器需要交换3个包,HTTPS除了TCP的三个包,还要加上ssl握手需要的9个包,所以一共是12个包,更加占用服务器资源。

https缺点:

  • HTTPS协议多次握手,导致页面的加载时间延长近50%;
  • HTTPS连接缓存不如HTTP高效,会增加数据开销和功耗;
  • 申请SSL证书需要钱,功能越强大的证书费用越高。
  • SSL涉及到的安全算法会消耗 CPU 资源,对服务器资源消耗较大。
tcp三次握手四次挥手

三次握手其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。

三次握手:客户端发第一个包服务端接收,服务端可以判断客户端发送能力。

服务端接收后发第二个包给客户端,这时客户端可以判断服务端的接收和发送能力。

这时还服务端还不能判断客户端的接收能力,所以还需要客户端再发一个包服务端接收来确定。

四次挥手TCP 协议是全双工通信, 这意味着客户端和服务器端都可以向彼此发送数据,所以关闭连接是双方都需要确认的共同行为, 假设是三次挥手时, 首先释放了客户到服务器方向的连接,此时 TCP 连接处于半关闭状态, 这时客户不能向服务器发送数据, 而服务器还是可以向客户发送数据。 如果此时客户收到了服务器的确认报文段后, 就立即发送一个确认报文段,这会导致服务器向客户还在发送数据时连接就被关闭。 这样会导致客户没有完整收到服务器所发的报文段

简单介绍localStorage,sessionStorage,cookie,session以及它们之间的区别:

localStorage,sessionStorage,cookie三者都是浏览器端储存方式,由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份,所以使用cookie来保存用户登录状态区分不同用户。localStorage,sessionStorage主要用来存储数据。

不同点

cookie 数据始终在同源的 http 请求中携带(即使不需要),localStorage,sessionStorage仅本地储存

cookie能存大约4k的数据,localStorage,sessionStorage大约5M
cookie默认有效期20分钟(后端可修改),localStorage默认永久有效,sessionStorage窗口关闭即失效

//手动封装设置localStorage有效时间方法
// 存值函数
        // 接收三个参数:键、值、有效天数
        Storage.prototype.setCanExpireLocal = (key, value, expire) => {
            // 判断传入的有效期是否为数值或有效
            // isNaN是js的内置函数,用于判断一个值是否为NaN(非数值),
            // 非数值返回true,数值返回false
            // 还可以限制一下有效期为整数,这里就不做了
            if (isNaN(expire) || expire < 1) {
                throw new Error('有效期应为一个有效数值')
            }
            // 86_400_000一天时间的毫秒数,_是数值分隔符
            let time = expire * 86_400_000
            let obj = {
                data: value, //存储值
                time: Date.now(), //存值时间戳
                expire: time, //过期时间
            }
            // 注意,localStorage不能直接存储对象类型,sessionStorage也一样
            // 需要先用JSON.stringify()将其转换成字符串,取值时再通过JSON.parse()转换回来
            localStorage.setItem(key, JSON.stringify(obj))
        }

        // 取值函数
        // 接收一个参数,存值的键          
        Storage.prototype.getCanExpireLocal = key => {
            let val = localStorage.getItem(key)
            // 如果没有值就直接返回null
            if (!val) return val
            // 存的时候转换成了字符串,现在转回来
            val = JSON.parse(val)
            // 存值时间戳 +  有效时间 = 过期时间戳
            // 如果当前时间戳大于过期时间戳说明过期了,删除值并返回提示
            if (Date.now() > val.time + val.expire) {
                localStorage.removeItem(key)
                return '值已失效'
            }
            return val.data
        }
        // 存值
        Storage.prototype.setCanExpireLocal('测试', '一天后过期', 1)
        // 取值
        Storage.prototype.getCanExpireLocal('测试')
        console.log(Storage.prototype.getCanExpireLocal('测试'));
        
cookie,session,token

cookie:由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份,Cookie实际上是一小段的文本信息。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。

session:session保存在服务器,session需要使用Cookie作为识别标志,由于HTTP协议是无状态的,session不能依据HTTP连接来判断是否为同一客户,因此服务器向客户端浏览器发送一个名为JSESSIONID的Cookie,它的值为该session的id(也就是HttpSession.getId()的返回值)。session依据该Cookie来识别是否为同一用户。

token:token是目前多用户下处理认证的最佳方式,token=用户数据 + 签名(算法加密后的用户数据 + 秘钥),服务端生成token,客户端请求时携带token,之后服务端再对用户数据进行算法加密并且与签名中的数据对比,达到验证目的。

json理解

JSON(JavaScript Object Notation)它是一种轻量级纯文本数据格式,不是一种编程语言,可以简化表示复杂数据结构的工作量。虽然和JavaScript具有类似的语法形式,但 JSON 并不从属于 JavaScript。而且,并不是只有 JavaScript 才使用 JSON。

JSON 是存储和交换文本信息的语法,类似 XML,比 XML 更小、更快,更易解析。JSON 使用 JavaScript 语法的子集表示对象、数组、字符串、数值、布尔值和 null 。即使 XML 也能表示同样复杂的数据结果,但JSON 没有那么烦琐,而且在 JavaScript 中使用更便利。ECMAScript 5 定义了一个原生的 JSON 对象,JSON.parse(),JSON.stringify()。

跨域处理

协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。

JSONP

是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,兼容性好(兼容低版本IE),缺点是只支持get请求,不支持post请求。

// index.html 原生封装jsonp方法
function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')
    window[callback] = function(data) {
      resolve(data)
      document.body.removeChild(script)
    }
    params = { ...params, callback } // wd=b&callback=show
    let arrs = []
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${arrs.join('&')}`
    document.body.appendChild(script)
  })
}
jsonp({
  url: 'http://localhost:3000/say',
  params: { wd: 'Iloveyou' },
  callback: 'show'
}).then(data => {
  console.log(data)
})

Vue axios实现
this.$http = axios;
this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})


CORS

是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。 CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

node中间件实现跨域代理

原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

nginx反向代理跨域

通过Nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域访问。

兼容:

click 在 ios 上有 300ms 延迟,原因及如何解决?

原因:早期苹果为了判断移动端上的双击缩放事件而加的,在touchendclick事件之间加300ms的延迟来判断用户到底是点击还是双击。
解决方法:fastclick是专门为解决移动端浏览器300毫秒点击延迟问题所开发的一个轻量级的库,fastclick的原理是在检测到touchend事件的时候,会通过DOM自定义事件立即触发模拟一个click事件,并把浏览器在300ms之后的click事件阻止调。还有禁用缩放也可以解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值