前端面试题记录

讲一下css3动画

css3实现动画主要有3种方式,第一种是:transition实现渐变动画,第二种是:transform转变动画,第三种是:animation实现自定义动画。
transition的中文含义是过渡。过渡是CSS3中具有颠覆性的一个特征,可以实现元素不同状态间的平滑过渡(补间动画),并且设置过渡持续的时间。
transform属性应用于2D 或 3D转换。该属性允许我们对元素进行旋转(rotate)、缩放(scale)、倾斜(skew)、移动(translate)这四类操作。一般是配合transition的属性一起使用。
animation可通过设置多个节点来精确控制一个或一组动画,常用来实现复杂的动画效果。
定义动画的步骤
(1)通过@keyframes定义动画;
(2)将这段动画通过百分比,分割成多个节点;然后各节点中分别定义各属性;
(3)在指定元素里,通过 animation 属性调用动画。
定义动画:

  @keyframes 动画名{
        from{ 初始状态 }
        to{ 结束状态 }
    }
    调用:
  animation: 动画名称 持续时间;

怎样减少页面的回流和重绘

1、减少对 dom 的操作
(1)当需要添加多个dom时,应将添加的操作合并为一次,可以使用 createDocumentFragment 方法创建虚拟的 dom 对象,对创建的 dom 进行相应的修改,将新 dom 添加到dom对象中,最终再把dom对象添加到真实dom中,这样做对 dom 的多次修改合并为一次,大大减少了回流和重绘的次数。

let box = document.querySelector('#box')
let fragment= document.createDocumentFragment()
for (let i = 0; i < 5; i++) {
    let li = document.createElement("li")
    li.appendChild(document.createTextNode(i))
    fragment.appendChild(li)
}
box.appendChild(fragment)

(2)先使用 display: none把需要修改的 dom 隐藏,修改完成后再将 dom 重新显示。由于使用 display: none 后渲染树中将不再渲染当前 dom,所以多次操作也不会触发多次回流和重绘。

let box = document.querySelector('#box')
box.style.display = 'none';
for (let i = 0; i < 5; i++) {
    let li = document.createElement("li")
    li.appendChild(document.createTextNode(i))
    box.appendChild(li)
}
box.style.display = 'block';

(3)将原始元素复制到一个脱离文档的节点中,对该节点进行修改,然后再替换原始的元素。通过这种方式,我们可以在不影响主文档的情况下对元素进行操作,这样只会触发一次回流。

const el = document.querySelector('.el');
const clone = el.cloneNode(true);
//一系列修改样式、大小或添加删除子节点操作
...
el.parentNode.replaceChild(clone, ul);

2、使元素脱离文档流
当元素浮动( float )、元素的position属性为absolute或fixed时,会使元素脱离文档流,它们样式的修改不会对其他元素的布局产生影响,因此在进行样式修改时,只有该元素本身及其子元素会触发回流和重绘。这样可以减小回流的范围,提高页面的渲染性能。
3、尽量避免频繁读取布局信息
在读取元素的布局信息(如offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle、getBoundingClientRect)时,会触发强制同步布局导致回流重绘。应尽量避免频繁读取布局信息,可以通过缓存布局信息或一次性读取多个属性来减少回流和重绘。
4、使用节流和防抖
对于一些频繁触发的事件(如scroll和resize),可以使用节流(throttle) 或防抖(debounce) 来限制事件的触发频率,从而减少回流和重绘。
5、合并样式修改
以下操作会导致 3次重绘 1次回流:

const el = document.querySelector('.el');
el.style.color = 'blue'; // 导致重绘 
el.style.backgroundColor = '#96f2d7'; // 导致重绘 
el.style.margin = '10px'; // 导致回流(回流会引起重绘)

如果采用动态添加class或者使用cssText方式的话,只会导致1次回流,从而减少重绘次数:

.change{
    color: blue;
    background-color: #96f2d7;
    margin: 10px;
}
const el = document.querySelector('.el');
el.classList.add('change')

或者使用cssText:

el.style.cssText = "color: blue; background-color: #96f2d7; margin: 10px;";

vue2响应式原理

vue2主要是采用了数据劫持结合发布者-订阅者模式来实现数据的响应式,vue在初始化的时候,会遍历data中的数据,使用object.defineProperty为data中的每一个数据绑定setter和getter,当获取数据的时候会触发getter,在getter中会收集对应的依赖,即收集订阅者,将这些订阅者存储起来;当数据被赋值或者修改时,就会触发setter,在setter中会调用notify方法通知订阅者数据发生变化,订阅者收到消息后调用update方法更新视图。以上是model(数据)改变,view(视图)随之一起改变的原理,而要做到view改变,model也随之改变的话,主要就是监听dom事件,在事件回调函数中对model数据进行修改。

vue3响应式原理

其实vue2和vue3响应式的实现思路差别并不大,主要是将数据劫持的api换成了Proxy,proxy api的第二个参数是一个对象,在这个对象里就可以对数据的操作进行劫持,即setter和getter。get 函数主要做了四件事情:1、对特殊的 key 做了代理;2、通过 Reflect.get 方法返回源对象的值;3、执行 track 函数收集依赖(最核心);4、对计算的值 (Reflect.get返回的值)res 进行判断,如果它也是数组或对象,则递归执行 reactive 把 res 变成响应式对象。set 函数主要做两件事情:1、通过Reflect.set 设置源对象的值;2、通过 trigger 函数派发通知(最核心),并依据 key 是否存在于 target 上来确定通知类型,即新增还是修改。

Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?

在get函数中,判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理,把深层次的数据也变成响应式对象,这样就实现了深度观测。

Proxy监测数组的时候可能触发多次get/set,那么如何防止触发多次呢

判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有当key不是被代理对象target自身属性或者旧值与新值不相等时,才执行trigger函数。(trigger函数主要是执行更新逻辑等);
以下是示例代码:

function hasOwn(val, key) {
  const hasOwnProperty = Object.prototype.hasOwnProperty
  return hasOwnProperty.call(val, key)
}
function reactive(data) {
  let observed = new Proxy(data, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      return res
    },
    set(target, key, value, receiver) {
      console.log(target, key, value)
      const hadKey = hasOwn(target, key)
      const oldValue = target[key]

      const result = Reflect.set(target, key, value, receiver)
      if (!hadKey) {
        console.log('trigger add operation...')
      } else if(value !== oldValue) {
        console.log('trigger set operation...')
      }

      return result
    }
  })
  return observed
}

let data = ['a', 'b']
let state = reactive(data)
state.push('c')

// ["a", "b"] "2" "c"
// trigger add operation...
// ["a", "b", "c"] "length" 3

state.push(‘c’) 会触发两次 set ,一次是push的值 c,一次是 length 属性设置。

1、设置值 c 时,新增了索引 key 为 2,*target 是原始的代理对象 [‘a’, ‘c’] ,这是一个add 操作, 故 hasOwn(target, key) 返回的是false,此时执行 trigger add operation… 。注意在trigger 方法中, length 没有对应的 effect ,所以就没有执行相关的 effect 。

2、当传入 key 为 length 时, length 是自身属性,故 hasOwn(target, key) 返回 true , 此时 value 是 3, 而 oldValue 即为 target[‘length’] 也是 3,故 value !== oldValue 不成立,不执行 trigger 方法

故只有当 hasOwn(target, key) 返回false或者 value !== oldValue 的时候才执行 trigger 。

为什么 Proxy 一定要配合 Reflect 使用

1、为了触发代理对象拦截操作时,保持正确的 this 指向。

Proxy 中接受的 receiver 形参,表示代理对象本身 或者 继承了代理对象的对象,即正确的this指向。

Reflect 中传入的 recriver 实参,表示修改执行原始操作时的 this 指向。
在一些特殊情况下,this 指向在 get 方法中会被修改,如下所示:

let parent = {
  name: "哈哈哈",
  get value() {
    return this.name;
  },
};
 
let proxy = new Proxy(parent, {
  get(target, key, receiver) {
    return Reflect.get(target, key);
  },
});
 
let child = { name: "呵呵呵" };
// 设置 child 继承 代理对象 proxy
Object.setPrototypeOf(child, proxy);
 
console.log(child.value);

在这里插入图片描述
我们所期望的是打印“呵呵呵”,但打印的是“哈哈哈”。
当我们调用 child.value 的时候,child 本身并不存在 value 属性。但是它继承的 proxy 对象中存在 value 属性的访问方法。所以触发 proxy 上的 get value(),同时由于访问了 proxy 上的 value属性,所以触发 proxy 的 get 方法。get 方法的 target 参数就是 parent,key 参数 就是 value。然后方法中 return Reflect.get(target, key) 相当于 target[key]。此时,我们访问的 child 对象的 value 属性,return 的却是 parent 的 value 属性。不知不觉中 this 指向在 get 方法中被偷偷修改了,原本调用的 child 在 get 方法中 变成了 parent。所以打印出来的是 parent[value],也就是 “哈哈哈”。

因此,要使用Reflect 中传入的 recriver 实参 this 指向,如下所示:

let parent = {
  name: "哈哈哈",
  get value() {
    return this.name;
  },
};
 
let proxy = new Proxy(parent, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
});
 
let child = { name: "呵呵呵" };
// 设置 child 继承 代理对象 proxy
Object.setPrototypeOf(child, proxy);
 
console.log(child.value); 

在这里插入图片描述
2、代码健壮性
举个例子:使用 Object.defineProperty() 重复声明的属性会报错,因为 JavaScript 是单线程语言,一旦抛出异常,后边的任何逻辑都不会执行,所以为了避免这种情况,我们就要写 大量的 try catch 来避免,不够优雅。
在这里插入图片描述

而使用 Reflect.defineProperty() 是有返回值的,不会报错,所以可以通过返回值来判断你当前操作是否成功。
在这里插入图片描述

Vue2 的生命周期方法有哪些

beforeCreate: 在组件实例初始化完成之后立即调用,会在实例初始化完成、props 解析之后、data() 和 computed 等选项处理之前立即调用。在这个阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。
created: 在组件实例处理完所有与状态相关的选项后调用。在这个阶段 data、methods、computed 以及 watch 上的数据和方法都可以访问了。然而,此时挂载阶段还未开始,因此 $el 属性和跟dom相关的操作仍不可用。
beforeMount :在组件被挂载之前调用。组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。
mounted :在组件被挂载之后调用。在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以进行dom操作。
beforeUpdate :在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个阶段,数据和视图是不一致。
updated :在组件因为一个响应式状态变更而更新其 DOM 树之后调用。父组件的更新钩子将在其子组件的更新钩子之后调用。这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。
beforeDestroy :实例被卸载之前调用。在这一步,实例仍然完全可用。我们可以在这时进行 善后收尾工作,比如清除定时器。
destroyed :实例卸载后调用。调用后,Vue实例指示的东西都会卸载,所有的事件监听器会被移除,所有的子实例也会被卸载。
activated :若组件实例是缓存树的一部分,当页面显示的时候这个生命周期会被调用。
deactivated :若组件实例是缓存树的一部分,当页面隐藏的时候这个生命周期会被调用。

Vue3 的生命周期方法有哪些

在vue3中,除了beforecate和created(它们被setup方法本身所取代),我们可以在setup方法中访问的API生命周期钩子有9个:
onBeforeMount :在挂载开始之前被调用,还没有创建 DOM 节点,相关的 render 函数首次被调用。
onMounted :组件挂载时调用。
onBeforeUpdate : 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
onUpdated : 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
onBeforeUnmount : 在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
onUnmounted : 卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
onActivated : 被 keep-alive 缓存的组件激活时调用。
onDeactivated : 被 keep-alive 缓存的组件停用时调用。
onErrorCaptured : 当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。

Vue2和Vue3的区别是什么

1、vue2 源码使用flow进行类型检测;vue3 源码使用typescript进行重构,vue对typescript支持更加友好。
2、vue2 使用object.defineProperty来劫持数据的setter和getter方法,对象改变需要借助api去深度监听;vue3 使用Proxy来实现数据劫持,删除了一些api( o n , on, on,once,$off) fiter等,优化了Block tree,solt,diff 算法等。
3、vue2 是选项API(Options API),一个逻辑会散乱在文件不同位置(data、props、computed、watch、生命周期钩子等),导致代码的可读性变差。当需要修改某个逻辑时,需要上下来回跳转文件位置;vue3 组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性,不需要在多个options里查找。
4、vue2使用mixins进行代码逻辑共享,mixins也是由一大堆options组成,如果有多个mixins则可能造成命名冲突等问题;vue3可以通过hook函数 将一部分独立的逻辑抽离出去,并且也是响应式的。
5、vue3支持在template中写多个根,vue2只能有一个根。
6、vue2和vue3生命周期写法也有所不同。

Vue和React的区别是什么

核心思想不同
Vue的主要特点是灵活易用的渐进式框架,进行数据拦截/代理,对侦测数据的变化更敏感、更精确,一旦数据改变就去通知依赖者更新视图。
React 推崇函数式编程(纯组件),数据不可变以及单向数据流。
组件写法不同
Vue 推荐的做法是 template 的单文件组件格式(SFC,简单易懂,从传统前端转过来易于理解),即 html,css,JS 写在同一个文件(vue也支持JSX写法)。
React推荐的做法是JSX + inline style, 也就是把 HTML 和 CSS 全都写进 JavaScript 中,即 all in js。
diff算法不同
Vue的Diff算法采用了双端比较的算法,同时从新旧虚拟dom树的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。(相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。)
React的Diff算法首先对新集合进行遍历,是那种从上到下依次遍历,通过唯一key来判断老集合中是否存在相同的节点,再进行相关操作。
响应式原理不同
Vue数据劫持结合发布者订阅者模式实现响应式。
React需要通过setState方法让新的state替换老的state,然后内部自动调用render函数更新视图,具有数据不可变性。

什么是匿名函数

匿名函数是一种没有名称的函数定义,也被称为无名函数、拉姆达函数(lambda function)、闭包(closure)或函数表达式。与具名函数不同,匿名函数在定义时不需要指定函数名,而是将整个函数定义作为一个表达式赋值给一个变量或作为其他函数的参数传递。

匿名函数的语法形式如下:

const functionName = function(parameters) {
  // 函数体
};

在上述语法中,function(parameters)表示匿名函数的参数列表,{}内的代码块表示函数体,可以包含一系列语句和逻辑操作。

匿名函数的特点和用途包括:

可以将匿名函数赋值给一个变量,以便在后续使用中调用该函数。
可以将匿名函数作为其他函数的参数传递,用于回调函数、事件处理等场景。
可以在函数内部定义局部作用域,避免全局变量的污染。
可以作为立即执行函数(Immediately Invoked Function Expression,IIFE)使用,即定义后立即调用。
匿名函数的一个示例是使用匿名函数作为回调函数传递给setTimeout函数,实现延迟执行的效果:

setTimeout(function() {
  console.log('延迟执行');
}, 1000);

匿名函数在函数式编程、异步编程等场景中经常被使用,它提供了一种灵活的方式来定义和使用函数。

什么是restful接口

RESTful接口是一种基于HTTP协议和REST(Representational State Transfer,表述性状态转移)架构风格的API设计。RESTful接口基于标准的HTTP方法,如GET、POST、PUT、DELETE等,以提供对资源的创建、读取、更新和删除(CRUD)操作。
GET(SELECT):从服务器取出资源(一项或多项),不改变资源状态。
POST(CREATE):在服务器创建新资源或者执行某个动作。
PUT(UPDATE):在服务器更新资源(客户端提供完整资源数据)。
PATCH(UPDATE):在服务器更新资源(客户端提供需要修改的资源数据)。
DELETE(DELETE):从服务器删除资源。
RESTful接口的设计原则包括无状态性(每次请求都应包含所有信息,不依赖于服务器保存的上下文信息)、客户端-服务器架构(客户端负责用户界面和用户体验,服务器提供数据和服务)、可缓存(响应可以被标记为可缓存或不可缓存,以提高性能)等。

RESTful接口通常以JSON或XML格式返回数据,使得它们可以被各种不同的客户端(如网页、移动应用、其他服务器等)使用。

从浏览器输入url后都经历了什么?

  • 从浏览器输入网址后,首先要经过域名解析,因为浏览器并不能直接通过域名找到服务器,而是通过IP地址找到对应的服务器,DNS域名解析时,先查看本地hosts文件,查看有没有当前域名对应的ip地址,若有直接发起请求,没有的话会在本地域名服务器去查找,该查找属于递归查找,如果本地域名服务器没查找到,会从根域名服务器查找,该过程属于迭代查找,根域名会告诉你从哪个与服务器查找,最后查找到对应的ip地址后把对应规则保存到本地的hosts文件中。

如果想加速以上及之后的http请求过程的话可以使用缓存服务器CDN,CDN过程如下:
用户输入url地址后,本地DNS会解析url地址,不过会把最终解析权交给CNAME指向的CDN的DNS服务器,CDN的DNS服务器会返回给浏览器一个全局负载均衡IP,用户会根据全局负载均衡IP去请求全局负载均衡服务器,全局负载均衡服务器会根据用户的IP地址,url地址,会告诉用户一个区域负载均衡设备,让用户去请求它。区域负载均衡服务器会为用户选择一个离用户较近的最优的缓存服务器,并把ip地址给到用户,用户想缓存服务器发送请求,如果请求不到想要的资源的话,会一层层向上一级查找,直到查找到为止。

  • 浏览器通过IP地址找到对应的服务器后,建立TCP连接,通过三次握手以同步客户端和服务端的序列号和确认号,并交换TCP窗口大小的信息;
  • TCP三次握手结束后,开始发送HTTP请求;
  • 服务器处理请求,并返回HTTP响应报文,可能返回304也可能返回200

返回304说明客户端缓存可用,直接使用客户端缓存即可,该过程属于协商缓存 返回200的话会同时返回对应的数据

  • 客户端拿到响应文本HTML后,自上而下执行代码,开始解析渲染页面

其中遇到CSS加载的时候,CSS不会阻塞DOM树的解析,但是会阻塞DOM树的渲染,并且CSS会阻塞下面的JS的执行
然后是JS加载,JS加载会影响DOM的解析,之所以会影响,是因为JS可能会删除添加节点,如果先解析后加载的话,DOM树还得重新解析,性能比较差。如果不想阻塞DOM树的解析的话,可以给script添加一个defer或者async的标签。
defer:不会阻塞DOM解析,等DOM解析完之后在运行,在DOMContentloaed之前 async:
不会阻塞DOM解析,等该资源下载完成之后立刻运行 进行DOM渲染和Render树渲染 获取html并解析为Dom树
解析css并形成一个cssom(css树) 将cssom和dom合并成渲染树(render树) 进行布局(layout)
进行绘制(painting) 回流重绘 回流必将引起重绘,重绘不一定引起回流 当改变 width、height
等影响布局的属性时会引起回流,或者当获取 scroll、client、offset的值时,浏览器为获取这些值也会进行回流,getComputedStyle 也会引起回流

  • 当数据传送完毕后,断开TCP连接。

讲一下https相关知识?

HTTPS是在HTTP上建立SSL加密层,并对传输数据进行加密,是HTTP协议的安全版,具有不可否认性,可以保证对方身份的真实性,默认端口是443端口,而且会保证数据的完整性。采用 对称加密 和 非对称加密 结合的方式来保护浏览器和服务端之间的通信安全。
HTTPS实现原理:
首先客户端向服务端发送一个随机值和一个客户端支持的加密算法,并连接到443端口。
服务端收到以后,会返回另外一个随机值和一个协商好的加密算法,这个算法是刚才发送的那个算法的子集
随后服务端会再次发送一个 CA 证书,这个 CA 证书实际上就是一个公钥,包含了一些信息(比如颁发机构和有效时间等)
客户端收到以后会验证这个 CA 证书,比如验证是否过期,是否有效等等,如果验证未通过,会弹窗报错。
如果验证成功,会生成一个随机值作为预主密钥,客户端使用刚才两个随机值和这个预主密钥组装成会话密钥;再使用刚才服务端发来的公钥进行加密发送给服务端;这个过程是一个非对称加密(公钥加密,私钥解密)
服务端收到以后使用私钥解密,随后得到那两个随机值和预主密钥,随后再组装成会话密钥。
客户端在向服务端发起一条信息,这条信息使用会话秘钥加密,用来验证服务端时候能收到加密的信息
服务端收到以后使用刚才的会话密钥解密,在返回一个会话密钥加密的信息,双方收到以后 SSL 建立完成;这个过程是对称加密(加密和解密是同一个)。
若想更加深入了解https的加密原理的话可以阅读以下文章:
彻底搞懂https的加密原理

TCP的三次握手、四次挥手

建立连接的目的是为了可靠地传输数据,因此我们必须保证客户端和服务端都能正常的发送和接收数据,如果某一方不能正常的发送或者接收数据,那整个数据的传输就不能成功,也就不可靠。
三次握手:
第一次握手:第一次握手是客户端发送同步报文到服务端,这个时候客户端是知道自己具备发送数据的能力的,但是不知道服务端是否有接收和发送数据的能力;
第二次握手:当服务端接收到同步报文后,回复确认同步报文,此时服务端是知道客户端具有发送报文的能力,并且知道自己具有接收和发送数据的能力,但是并不知道客户端是否有接收数据的能力;
第三次握手:当客户端收到服务端的确认报文后,知道服务端具备接收和发送数据的能力,但是此时服务端并不知道客户端是否具有接收数据的能力,所以还需要发送一个确认报文,告知服务端客户端是具有接收数据能力的。最后,当整个三次握手结束过后,客户端和服务端都知道自己和对方具备发送和接收数据的能力,随后整个连接建立就完成了,可以进行后续数据的传输了。
四次挥手:
第一次挥手:客户端发起关闭连接的请求给服务端;
第二次挥手:服务端收到关闭请求的时候可能这个时候数据还没发送完,所以服务端会先回复一个确认报文,表示自己知道客户端想要关闭连接了,但是因为数据还没传输完,所以还需要等待;
第三次挥手:当数据传输完了,服务端会主动发送一个 FIN 报文,告诉客户端,表示数据已经发送完了,服务端这边准备关闭连接了。
第四次挥手:当客户端收到服务端的 FIN 报文过后,会回复一个 ACK 报文,告诉服务端自己知道了,再等待一会就关闭连接。

为什么握手要三次,挥手却要四次?

建立连接时,被动方服务器端进入“握手”阶段并不需要任何准备,可以将SYN和ACK报文一起发给客户端,开始建立连接。释放连接时,被动方服务器,突然收到主动方客户端释放连接的请求时并不能立即释放连接,因为还有必要的数据需要处理,所以服务器先返回ACK确认收到报文,经过CLOSE-WAIT阶段准备好释放连接之后,才能返回FIN释放连接报文,所以会比握手多一步。

为什么客户端在第四次挥手后还会等待 2MSL?

等待 2MSL 是因为要保证服务端接收到了 ACK 报文,因为网络是复杂的,很有可能 ACK 报文丢失了,如果服务端没接收到 ACK 报文的话,会重新发送 FIN 报文,只有当客户端等待了 2MSL 都没有收到重发的 FIN 报文时就表示服务端是正常收到了 ACK 报文,那么这个时候客户端就可以关闭了。

水平越权和垂直越权?

越权访问漏洞:越权漏洞是一种很常见的逻辑安全漏洞。是某应用在检查授权的时候存在纰漏问题,是由于服务器端对客户提出的数据操作请求过分信任,忽略了对该用户操作权限的判定,导致修改相关参数就可以拥有了其他账户的增、删、查、改功能,从而导致越权漏洞。
水平越权:同一权限下的不同用户可以互相访问。攻击者尝试访问与他拥有相同权限的用户的资源,怎么理解呢?比如某系统中有个人资料这个功能,A账号和B账号都可以访问这个功能,但是A账号的个人信息和B账号的个人信息不同,可以理解为A账号和B账号个人资料这个功能上具备水平权限的划分。此时, A账号通过攻击手段访问了B账号的个人资料,这就是水平越权漏洞。
水平越权常见场景:
1、基于用户身份的ID
在使用某个功能时通过用户提交的身份ID (用户ID、账号、手机号、证件号等用户唯一标识)来访问或操作对应的数据。
2、基于对象ID
在使用某个功能时通过用户提交的对象ID (如订单号、记录号)来访问或操作对应的数据。
3、基于文件名
在使用某个功能时通过文件名直接访问文件,最常见于用户上传文件的场景。
垂直越权:权限低的用户可以访问到权限高的用户。
垂直越权是不同级别之间或不同角色之间的越权,垂直越权还可以分为向上越权和向下越权。向上越权指的是一个低级别用户尝试访问高级别用户的资源,比如说某个系统分为普通用户和管理员用户,管理员有系统管理功能,而普通用户没有,那我们就可以理解成管理功能具备垂直权限划分,如果普通用户能利用某种攻击手段访问到管理功能,那我们就称之为向上越权(就是以下犯上)。向下越权是一个高级别用户访问低级别用户信息。
垂直越权常见场景:
1、未认证账户访问无需认证就能访问该功能;
2、不具备某个功能权限的账户认证后成功访问该功能。

什么情况下会抽取组件?

1、设计稿上样式相似,多个地方使用: 当多个地方需要类似的功能或UI样式相似时,可以将其抽离为一个组件,以便在多个地方重复使用。
2、复杂的UI结构: 如果某个页面或部分的UI结构设计得非常复杂,可以将其拆分为更小、更易管理的组件,以提高可维护性。
3、可复用的逻辑: 如果某个组件包含了复杂的业务逻辑,这些逻辑可能在其他地方也需要使用,可以将这些逻辑抽离为一个可复用的组件。
4、应用需要较高性能: 通过按需加载组件,可以在页面渲染时减少不必要的资源加载,从而提高应用性能。
5、团队协作: 在多人协作的项目中,拆分组件可以让团队成员更容易独立开发、测试和维护不同的部分。
6、考虑技术栈变化: 如果考虑项目技术栈发生变化的情况,组件的抽离可以使迁移工作更加容易,因为你只需要关注一个个独立的组件。
注意:过度抽离可能会导致组件层次过深,增加理解和调试的难度,因此需要在合理的范围内进行抽离。

在平时的开发工作中,做过哪些性能优化?

1、配置webpack,在生产环境中去除console.log的打印;
2、将项目中用到的图标制作成雪碧图,减少http 请求数,并且充分利用缓存来提升性能;
3、去除不必要的请求,将项目中残留的无效请求连接去除(废弃的后台接口等),避免重复的资源请求,合理设置HTTP缓存;;
4、图片懒加载,在图片即将进入可视区域的时候进行加载;
5、使用 webpack 插件 image-webpack-loader对图片进行压缩;
6、使用事件委托,防抖和节流,尽量不要使用JS动画,css3动画和canvas动画都比JS动画性能好;
7、将外部脚本置底(将脚本内容在页面信息内容加载后再加载);
8、在首页不需要使用的脚本文件,可以使用懒加载的方式对其进行加载(只有在需要加载的时候加载,在一般情况下并不加载脚本内容。)使用import(“…/…/xxxx.js”)方式加载文件即懒加载,webpack 的懒加载实现在打包时会将懒加载的代码切割出去单独打包,然后在主包中进行按需加载,最后执行调用;
9、精简javaScript和css;
10、减少不必要的 HTTP跳转 。
【关于性能优化这个问题也可以用具体的实例说明,回答的模板:
性能问题的出现:在今年的xx月,测试同事发现在这个项目的xx页面加载的时候出现卡顿。(这点其实能编,注意 对于产品、测试、用户 而言,能直观感受到的就是卡顿、慢)
问题复现:随后我打开页面,通过工具测试发现(这里的工具可以是performance、lighthouse、前端埋点SDK亦或者其他第三方的监测工具,你要说你直接调用浏览器的performance API估计也能行但不推荐,容易被面试官反问为什么不封装个性能检测工具…orz)几个性能指标存在问题:FCP、TTI这两个性能指标都过长,FCP达到了3.x秒,TTI更是长达5.x秒(不要选太多性能指标,很多性能指标可以不纳入你们公司的衡量范围,or你编的衡量范围~~)。
问题分析:(分析过程相信大家都有,这段大家可以自己想想,在此我以FCP为例)我发现在xx页面加载的时候会先获取几张比较大的图片,导致FCP指标过长。
优化方案:采取了图片优化策略xxx执行优化。(下文提及哪些优化策略)
量化优化效果:在经过上述的优化方案后,我们最终将FCP优化到了1.8秒,TTI优化到了3.8秒。(量化你的优化成果)
(非必要)优化是否达标:如果同学们的公司对性能指标的数据有强要求,比如FCP必须在2秒以内诸如此类…,可以提一下,可以代表你在之前的公司是有完善的性能优化流程的。】

以下代码执行后会输出什么?

在这里插入图片描述

new操作符具体都干了什么?

1、创建一个新对象 obj
2、使新对象obj的__proto__指向该构造函数的prototype
3、改变this的指向(指向新对象 obj)并执行该构造函数,执行结果保存起来作为 result
4、判断构造函数的执行结果,执行结果如果是值类型则返回新对象 obj,执行结果如果是对象类型则返回这个对象 result

导入模块import、import()和require()的区别

import 和 import() 都是 ES6 中用于导入模块的语句,而 require() 则是 Node.js 中用于导入模块的函数。
使用 import 语句导入模块时,模块会被静态加载,也就是在编译时就已经确定了导入的模块;import() 和 require() 都是动态加载模块的方式。它们都允许在代码运行时根据需要加载模块,而不是在编译时就将所有模块都加载进来。不过两者的实现方式略有不同:import() 是基于 Promise 的异步加载,而 require() 是同步加载模块。

在整个应用程序中,使用 import 和 import() 语句导入的模块都是动态只读引用:
动态,即原始值发生变化,import加载的值也会发生变化,不论是基本数据类型还是复杂数据类型。
只读,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型,若对import的变量进行修改则会报错(但复杂数据类型可以修改其内部的属性,因为并没有将复杂数据类型的指向修改)。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
导入的数据是复杂数据类型:
在这里插入图片描述
在这里插入图片描述
运行index.js, 结果如下:
在这里插入图片描述
导入的数据是基本数据类型:
在这里插入图片描述
在这里插入图片描述
运行index.js, 结果如下:
在这里插入图片描述
修改index.js文件:
在这里插入图片描述
运行结果:
在这里插入图片描述

import()异步加载的证明:

在这里插入图片描述
import()加载数据的结果与import是一样的:
在这里插入图片描述
运行结果:
在这里插入图片描述

使用 require() 导入的模块,属于浅拷贝。如果导入的数据是复杂数据类型,由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。如果导入的数据是基本数据类型,则属于复制,是两个单独的模块,因此对该模块的值做修改时不会影响另一个模块。同时,在另一个模块可以对该模块输出的变量重新赋值。当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块(require)无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
导入的数据是复杂数据类型:
在这里插入图片描述

在这里插入图片描述

运行index.js, 结果如下:
在这里插入图片描述
导入的数据是基本数据类型:
在这里插入图片描述

在这里插入图片描述
再次运行index.js, 结果如下:
在这里插入图片描述

import 和 import() 语句支持模块的默认导出和命名导出,而 require() 只支持模块的默认导出 (module.exports) 导出。

触底加载下一页

判断页面是否触底主要是通过监听页面滚动事件(scroll),判断页面被卷去的高度(scrollTop)加可视区域的高度(clientHeight)是否大于等于整个文档的高度(scrollHeight),如果是则代表页面已经触底了

// 浏览器触底加载功能的实现
    /* 1.视口的高度+页面被卷去的高度=滚动条的长度 */
    window.addEventListener("scroll",function(){
        //页面被卷去的高度: window.scrollY 
        //页面被卷去的高度: window.pageYOffset
        //页面被卷去的高度: document.documentElement.scrollTop
        //页面被卷去的高度: document.body.scrollTop
        //document.body.scrollTop与document.documentElement.scrollTop两者有个特点: 同时只会有一个值生效。比如
        //document.body.scrollTop能取到值的时候,document.documentElement.scrollTop就会始终为0;反之亦然。
        //所以,如果要得到网页的真正的scrollTop值,可以这样:
        //scrollTop=document.body.scrollTop+document.documentElement.scrollTop;
        //这两个值总会有一个恒为0,所以不用担心会对真正的scrollTop造成影响。
        // console.log("页面被卷去的高度:",window.scrollY,window.pageYOffset,document.documentElement.scrollTop,document.body.scrollTop);
       
        // body页面的滚动条高度(整个文档的高度): document.body.scrollHeight
        // 整个页面你的滚动条高度(整个文档的高度): document.documentElement.scrollHeight
        // console.log(document.body.scrollHeight,document.documentElement.scrollHeight);

        // 可视区域的高度: document.documentElement.clientHeight
        // console.log(document.documentElement.clientHeight);

        if(document.documentElement.clientHeight+document.documentElement.scrollTop>=document.documentElement.scrollHeight){
            console.log("触底了!!!!");
        }

    })

在这里插入图片描述
局部滚动的触底也是类似的原理,直接上代码吧(vue3):

<template>
  <div style="color: red; width: 100%;">
    <div class="scroll" style="width: 600px; height: 500px; overflow-y: scroll;">
        <p style="padding: 10px;" v-for="item in 100">数据大屏自适应函数</p>
    </div>
  </div>
</template>
  <script >
import { defineComponent, ref, onMounted } from 'vue';
export default defineComponent({
  name: 'Wisdom',
  setup(props) {
    onMounted(() => {
        document.querySelector('.scroll').addEventListener('scroll', (e) => {
            const target = e.target;
            if(target.scrollTop + target.clientHeight >= target.scrollHeight) {
                console.log('触底了')
            }
        })
    });
  },
});

判断元素进入可视区域

判断元素是否进入可视区域主要是通过监听页面滚动事件(scroll)然后判断:

竖向滚动:元素距文档顶部距离 - 页面被卷去的高度 < 视口高度 且 元素距文档顶部距离 - 页面被卷去的高度 + 元素高度 > 0
横向滚动:元素距文档左侧距离 - 页面被卷去的高度 < 视口宽度 且 元素距文档左侧距离 - 页面被卷去的高度 + 元素宽度 > 0

这种方法的优点是兼容性好,可以支持 IE8 及以上浏览器。缺点是需要考虑滚动条的影响,也需要获取元素的尺寸和位置,比较繁琐。

function isInViewport(element) {
  const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  const offsetTop = element.offsetTop;
  const windowHeight = window.innerHeight;
  const elementHeight = element.offsetHeight;

  return (
    offsetTop - scrollTop < windowHeight &&
    offsetTop - scrollTop + elementHeight > 0
  );
}

在这里插入图片描述

静态资源放在哪里?assets和static的区别?

vue项目的目录结构通常在src目录下有个assets文件夹,和src同级的地方有个static文件夹。
相同点:两个文件夹下都可以用于存储项目中所需的静态资源,像图片,样式文件等等。
区别:assets下存放的静态资源文件在项目打包时,也就是执行 npm run build 指令时,会走webpack的打包流程,做压缩代码体积、代码格式化这种操作;放static中存放的资源文件不会走打包流程,而是直接复制进最终的dist目录里。如果我们把所有资源都放进static下,由于该文件夹下的资源不会走打包流程,在项目打包时会提高一定的效率。但是同时也有一个问题,就是由于不会进行压缩等操作,项目打包后的体积会比把资源都放进assets下来得大。
总结:我们通过npm run build打包项目后,会生成一个dist文件夹,放在assets里面的资源会被webpack打包后放进dist文件夹中,而static里面的资源是直接复制进dist中,由于第三方类库资源一般都已经经过处理了,所以我们可以在static里放一些外部的第三方资源文件,assets放我们自己项目中的图片等资源,让这些资源走打包流程,减少最终包的体积。但是实际开发中情况肯定是多变的,还是要根据实际情况来看把静态资源文件放在哪里更合适。

mvc和mvvm的区别

具体可查阅以下这篇文章:
mvc和mvvm的区别和应用场景

PC端登录时手机二维码扫码登录,如何实现?

首先,在扫码前,手机端必然是已登陆状态,PC端登录的账号肯定与手机端是同一个账号。不可能手机端登录的是账号A,而扫码登录以后,PC端登录的是账号B。那如何实现呢?有些同学会想到,是不是扫码过程中,把密码传到了PC端呢?但这是不可能的。因为那样太不安全了,客户端也根本不会去存储密码。
大概步骤如下:
1、用户打开PC端,进入二维码登录界面,PC端向服务端发起请求,告诉服务端,我要生成用户登录的二维码,并且把PC端设备信息也传递给服务端,服务端收到请求后,生成二维码ID,并将二维码ID与PC端设备信息进行绑定,然后把二维码ID返回给PC端;
2、PC端收到二维码ID后,生成二维码(二维码中肯定包含了ID),此后,为了及时知道二维码的状态,客户端在展现二维码后,PC端不断的轮询服务端,比如每隔一秒就轮询一次,请求服务端告诉当前二维码的状态及相关信息(或者使用Websocket,Websocket是指前端在生成二维码后,会与后端建立连接,一旦后端发现二维码状态变化,可直接通过建立的连接主动推送信息给前端)。
3、用户用手机去扫描PC端的二维码,通过二维码内容取到其中的二维码ID,再调用服务端接口将移动端的身份信息与二维码ID一起发送给服务端,服务端接收到后,将身份信息与二维码ID进行绑定,生成临时token,返回给手机端。因为PC端一直在轮询二维码状态,所以这时候二维码状态发生了改变,在界面上把二维码状态更新为已扫描。

为什么需要返回给手机端一个临时token呢?临时token与token一样,它也是一种身份凭证,不同的地方在于它只能用一次,用过就失效。在第三步骤中返回临时token,为的就是手机端在下一步操作时,可以用它作为凭证。以此确保扫码,登录两步操作是同一部手机端发出的。

4、手机端在接收到临时token后会弹出确认登录界面,用户点击确认时,手机端携带临时token用来调用服务端的接口,告诉服务端,我已经确认要在PC端登录了,服务端收到确认后,根据二维码ID绑定的设备信息与账号信息,生成PC端登录的token,这时候PC端轮询接口,它就可以得知二维码的状态已经变成了"已确认"。并且从服务端可以获取到PC端登录的token,到这里,登录就成功了,后面PC端就可以用这个token去访问服务端的资源了。

什么是vNode,什么是vDom?

vNode,或称虚拟节点(Virtual Node),是虚拟 DOM 中的一个概念。它是一个用来描述真实 DOM 结构的 JavaScript 对象,用于描述标签或组件具体是怎样的。vNode 包含了元素的类型、属性、子元素等信息,但它只存在于内存中,并不直接映射到实际的浏览器 DOM。
vNode 主要有以下几个属性:
类型(Type): 表示元素的类型,如标签名、组件名等。
属性(Props): 包含元素的属性,例如样式、事件处理程序等。
子元素(Children): 表示当前元素包含的子元素,可以是其他 vNode 对象或者文本节点。
键(Key): 用于在更新时识别 vNode,帮助虚拟 DOM diff 算法更准确地比较新旧虚拟 DOM 树。

虚拟 DOM(vDOM)通常是由多个虚拟节点(vNode)组成的。每个 vNode 对象代表着虚拟 DOM 树中的一个节点,多个 vNode 对象组合在一起形成了完整的虚拟 DOM。它是一个在内存中存在的树形结构,用来表示真实 DOM 的层次结构和状态。虚拟 DOM 的主要目的是为了优化 DOM 操作的效率。

为什么需要虚拟DOM?

1、由于js需要借助浏览器提供的DOM接口(document对象)才能操作真实dom,所以操作真实dom的代价是比较大的,而有了虚拟dom的话,新的vDom会与旧的vDom进行对比,如果内容没变化的话,就直接复用原先的真实dom,当内容发生改变时才会另外生成新的真实dom替换掉旧的真实dom,这样就减少了操作真实dom的次数,提高了性能;新旧vDom的对比是在内存中进行的,不涉及实际的浏览器渲染,(新旧真实 DOM 元素的对比是在浏览器的渲染引擎中进行的,具体来说是在Reflow和Repaint阶段。当有新的真实 DOM 元素插入、删除或更新时,浏览器会触发重新布局(Reflow)和重绘(Repaint)操作。在这个过程中,浏览器会比较新旧 DOM 结构,确定需要进行怎样的布局和绘制变化。Reflow(或Layout)是指浏览器重新计算元素的位置和尺寸,确保它们正确显示在页面上。Repaint 是在元素样式没有改变但需要重新绘制的情况下触发,例如滚动条的移动。这两个过程的开销相对较大,因此优化库和框架通常会尽量减少对真实 DOM 的直接操作,采用一些策略来最小化布局和绘制的次数,提高性能。)性能和效率都更好。
2、 虚拟 DOM 的抽象层可以帮助实现跨平台开发,因为它使得可以在不同平台上使用相同的虚拟 DOM 结构,而在具体渲染时适配各个平台。
3、 虚拟 DOM 简化了复杂的 DOM 操作,使得开发者更专注于应用的逻辑和结构,提高了开发效率。

说出继承的几种实现方式及其优缺点

1、原型链继承
让子类的原型对象指向父类的实例,当子类的实例找不到对应的属性和方法时,就会沿着原型链往上查找。

function Parent() {
  this.name = 'Parent';
}

function Child() {
  this.childName = 'Child';
}

Child.prototype = new Parent();

优点:简单易懂。
缺点:子类原型对象指向同一个父类的实例,当有两个子类实例对象时,修改其中一个就会影响其它子类实例;没有实现super功能,无法传递参数给父类构造函数。

2、构造函数继承(借用构造函数)
在子类的构造函数中执行父类的构造函数,并且为其绑定子类的this。

function Parent(x) {
  this.name = 'Parent';
}

function Child() {
  Parent.call(this, 'aa');
  this.childName = 'Child';
}

优点:避免了属性共享问题(即子类实例互相影响),可以传递参数给父类构造函数。
缺点:无法继承父类原型上的方法和属性,每个实例都有一份父类构造函数的副本。
3、组合式继承
结合原型链继承和构造函数继承的继承方法。

function Parent() {
  this.name = 'Parent';
}

function Child() {
  Parent.call(this);
  this.childName = 'Child';
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

优点:兼顾了原型链继承和构造函数继承的优点。
缺点:调用两次父类构造函数,存在一份多余的属性,影响性能。
4、寄生组合式继承
解决组合式继承中对父类构造函数的不必要调用问题,避免在子类原型上创建多余的父类实例。

// 寄生式继承,只继承父类的原型
function inheritPrototype(child, parent) {
  var prototype = Object.create(parent.prototype);
  prototype.constructor = child; // 修复构造函数指向
  child.prototype = prototype;
}

function Parent(name) {
  this.name = name;
}

Parent.prototype.sayHello = function() {
  console.log("Hello, " + this.name);
};

function Child(name, childName) {
  Parent.call(this, name); // 借用构造函数,继承属性
  this.childName = childName;
}

// 寄生组合式继承核心
inheritPrototype(Child, Parent);

Child.prototype.sayChildHello = function() {
  console.log("Hello, " + this.childName);
};

var childInstance = new Child("ParentName", "ChildName");
childInstance.sayHello(); // 输出: Hello, ParentName
childInstance.sayChildHello(); // 输出: Hello, ChildName

与组合式继承方式对比可知,寄生组合式继承主要是将Child.prototype = new Parent()换成了Child.prototype = Object.create(Parent.prototype),Object.create 用于创建一个新对象,创建的新对象是一个浅拷贝。
优点:避免了多余的父类构造函数调用,保持原型链完整性,支持构造函数传参(es6的类继承原理是寄生组合式继承)。
缺点:相较于其他的继承方式,寄生组合式继承的实现相对复杂,对于初学者来说,寄生组合式继承可能不够直观,理解起来需要一些额外的学习成本。

Vue2中 $set()方法实现原理

Vue2 中使用的是基于 Object.defineProperty来实现响应式的。当一个对象被定义为响应式时,Vue 会为对象的每个属性都创建一个 getter 和 setter,这样当属性被访问或修改时,Vue 就能够进行侦测并触发相应的更新。但是对于新增的属性,由于在对象创建时并没有对应的 getter 和 setter,因此默认情况下是非响应式的。为了使新增的属性也能够具有响应性,$set 方法被引入。

使用:
Vue.set( target, key, value ) / this.$set( target, key, value )
target:要更改的数据源(可以是对象或者数组)
key:要更改的具体数据,或者新增的属性名
value :重新赋的值

原理:
set 方法会对参数中的 target 进行类型判断
如果是 undefined 、null 、基本数据类型,直接报错。
如果为数组,取当前数组长度与 key 这两者的最大值作为数组的新长度,然后使用数组的 splice 方法将传入的索引 key 对应的 val 值添加进数组。target 在 observe 的时候,原型链被修改了, splice 方法也已经被重写了,触发之后会再次遍历数组,进行数据劫持,也就是说当使用 splice 方法向数组内添加元素时,该元素会自动被变成响应式的
如果为对象,会先判断 key 值是否存在于对象中,如果在,则直接替换 value(因为这说明key这个属性已经是响应式的了,那么就直接将value赋值给这个属性)。如果不在,就判断 target 是不是响应式对象(其实就是判断它是否有 ob 属性),接着判断它是不是 Vue 实例,或者是 Vue 实例的根数据对象,如果是则抛出警告并退出程序。如果 target 不是响应式对象,就直接给 target 的 key 赋值(我一开始在这里有个疑惑,不是响应式对象的话,那不就应该重新给他绑定setter和getter吗?怎么就直接赋值了呢?其实这里是我想岔了,target并不是那个要新增的属性,而是那个要新增属性的源对象,如果这个源对象都不是响应式数据了,那么就代表它是不需要响应式的,换句话说就是这个源对象并不是vue的data下的属性,直接给它赋值就完事了),如果 target 是响应式对象,就调用 defineReactive(就是调用 Object.defineProperty进行setter和getter的绑定) 将新属性的值添加到 target 上,并进行依赖收集,更新视图。

简化的代码:

function set(target, key, value) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, value);
    return value;
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = value;
    return value;
  }
  const ob = target.__ob__;
  if (!ob) {
    target[key] = value;
    return value;
  }
  defineReactive(ob.value, key, value);
  ob.dep.notify();
  return value;
}

function defineReactive(obj, key, val) {
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      if (Dep.target) {
        dep.depend();
      }
      return val;
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
      dep.notify();
    },
  });
}

赋值、浅拷贝、深拷贝的区别(针对于Object来说,不考虑基本数据类型)

赋值(Assignment):
当把一个对象赋值给一个新的变量时,赋的是该对象在栈中的地址,而不是堆中的数据。两个变量指向同一个存储空间,无论哪个变量修改了指向的对象的属性值,其实都是改变存储空间的内容,因此两个对象是互相影响的。(修改对象里的基本数据也是会互相影响)

let obj1 = { name: 'John' };
let obj2 = obj1; // 赋值
obj2.name = 'Jane';
console.log(obj1.name); // 输出: Jane

浅拷贝(Shallow Copy):

浅拷贝创建一个新的对象,是重新在堆中创建内存,但仅复制原始对象的一层属性。拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存会互相影响。

let obj1 = { name: 'John', hobbies: ['reading', 'coding'] };
let obj2 = Object.assign({}, obj1); // 浅拷贝
obj2.hobbies.push('gaming');
console.log(obj1.hobbies); // 输出: ['reading', 'coding', 'gaming']

深拷贝(Deep Copy):

深拷贝创建一个新的对象,是重新在堆中创建内存,并递归地拷贝原始对象的所有嵌套属性。新对象与原始对象完全独立,修改其中一个不会影响另一个。
深拷贝更消耗内存和性能,特别是在处理大型对象或对象之间存在循环引用时。

let obj1 = { name: 'John', hobbies: ['reading', 'coding'] };
let obj2 = JSON.parse(JSON.stringify(obj1)); // 深拷贝(简单方式,不适用于包含函数或循环引用的对象)
obj2.hobbies.push('gaming');
console.log(obj1.hobbies); // 输出: ['reading', 'coding']

Object对象是不能产生作用域的

在这里插入图片描述
obj.hasOwnProperty(‘xx’):判断某个属性是否属于自身对象的,不会查找原型链。

vue2为什么不用Object.defineProperty劫持数组?

在vue2中,数据的响应式是基于Object.defineProperty实现的,但对于数组却不是使用Object.defineProperty来实现响应式的,而是通过改写数组的方法来实现对数组的监听(例如 push、pop、shift、unshift、splice 等)。主要原因有:
1、Object.defineProperty无法劫持数组长度length属性的变化,而数组length属性会影响数组的变动。
在这里插入图片描述
2、Object.defineProperty只能劫持已有属性,要监听数组变化,必须预设数组长度,遍历劫持,但数组长度在实际引用中是不可预料的。
3、数组删除或者新增会导致索引发生变动,每次变动都需要重新遍历,添加劫持,数据量大时非常影响性能。(数组新增的元素都不会被劫持,当数组的长度小于某个被劫持的元素的下标+1时,该索引位置的劫持也会失效)
在这里插入图片描述

什么是泛型?

泛型允许我们在定义的时候不具体指定类型,而是泛泛地说一种类型,并在函数调用的时候再指定具体的参数类型,也就是说泛型也是一种类型,只不过不同于 string, number 等具体的类型,它是一种抽象的类型,我们不能直接定义一个变量类型为泛型。简单来说,区别于平时我们对「值」进行编程,泛型是对「类型」进行编程。

在ts中, interface和type有哪些区别

1、interface(接口) 是 TS 设计出来用于定义对象和函数类型的,可以对对象和函数的形状进行描述,无法定义其它类型;type(类型别名)会给一个类型起个新名字, 类型别名有时和interface(接口)很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
interface定义对象:

interface Person {
    name: string
    age: number
}

interface定义函数:

interface MyFunction {
  (param1: number, param2: string): void;
}

type定义任何类型:

type MyNumber = number; //基本类型
type MyUnion = number | string; //联合类型
type MyIntersection = { prop1: number } & { prop2: string }; //交叉类型
type MyObject = { prop1: number; prop2: string }; //对象类型
type MyArray = number[]; //数组类型
type MyTuple = [number, string]; //元组类型
type MyFunction = (param1: number, param2: string) => void; //函数类型
type MyGeneric<T> = Array<T>; //泛型类型
type MyConditionalType<T> = T extends string ? string : number; //条件类型

type MyMappedType = {
  [K in 'prop1' | 'prop2']: number;
}; //映射类型

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; sideLength: number };//可辨识联合类型
  

2、interface 支持声明合并,这意味着你可以定义同名的多个接口,它们会被自动合并为一个接口;type 不支持声明合并,对于同名的 type,会产生冲突,导致报错。
interface 的声明合并:

interface Person {
  firstName: string;
}

interface Person {
  lastName: string;
}

// 合并后的 Person 接口
/*
interface Person {
  firstName: string;
  lastName: string;
}
*/

type 不支持声明合并:

type MyType = {
  property1: string;
};

type MyType = {
  property2: number; // Error: 不能重新定义属性“property2”。
};

3、interface 使用 extends 关键字来实现继承,而 type 使用交叉类型来实现继承。(type使用联合类型进行类型组合并不直接称为“继承”,而是叫做联合类型)
interface的继承:

interface Shape {
  color: string;
}

interface Dimensions {
  width: number;
  height: number;
}

interface RectangularShape extends Shape, Dimensions {
  // 继承了 Shape 和 Dimensions 接口的属性
}

type的继承:

type Person = {
  name: string;
};

type Employee = {
  role: string;
};

type EmployeePerson = Person & Employee;

const employee: EmployeePerson = {
  name: 'John',
  role: 'Developer',
};

补充:
条件类型的实际用例:
在这里插入图片描述
映射类型的实际用例:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可辨识联合类型的实际用例:
在这里插入图片描述

在ts中,any和unknown 区别

1、unknown 和 any 的主要区别是 unknown 类型检查会更加严格,在对 unknown 类型的值执行大多数操作之前,我们必须进行某种形式的检查。而在对 any 类型的值执行操作之前,我们不必进行任何检查。(可以将任何东西赋给 unknown 类型,但在进行类型检查或类型断言之前,不能对 unknown 进行操作;可以把任何东西分配给any类型,也可以对any类型进行任何操作。)
举例说明:

function test(callback: any) {
  callback();
}

test(1); 

因为 callback 是any类型的,所以 callback()语句不会触发类型错误。我们可以用any 类型的变量做任何事情。但是运行时会抛出一个运行时错误:TypeError: callback is not a function。1 是一个数字,不能作为函数调用,TypeScript并没有保护代码避免这个错误。
将callback 参数的类型换为unknown,unknown 变量接受任何值。但是当尝试使用 unknown 变量时,TypeScript 会强制执行类型检查:

function test(callback: unknown) {
  callback();
}

test(1); 

如果执行以上代码,因为 callback 的类型是 unknown,所以callback() 语句在代码编译阶段就会报一个类型错误 :Object is of type ‘unknown’。 与 any 相反,TypeScript会保护我们不调用可能不是函数的东西。要想以上代码正确通过编译,那么在使用 unknown 类型的变量之前,必须进行类型检查:

function test(callback: unknown) {
  if (typeof callback === 'function') {
    callback();
  }
}

test(1);

2、any类型的值可以赋值给其他任意类型的变量,其他任意类型(number、string等)的值可以赋值给any类型的变量。
在这里插入图片描述

unknown 类型的值只能赋值给 any 和 unkown 类型的变量,其他任意类型(number、string等)的值都可以赋值给unknown 类型的变量。
在这里插入图片描述
如果将注释打开的话就会报错:
在这里插入图片描述

补充:
1、在联合类型中,如果任一组成类型是 unknown,那么这一联合类型就是unknown类型( any类型除外)。

type UnionType1 = unknown | null;       // unknown
type UnionType2 = unknown | undefined;  // unknown
type UnionType3 = unknown | string;     // unknown
type UnionType4 = unknown | number[];   // unknown

如果有一种组成类型是 any,那么这一联合类型就是 any类型。

type UnionType5 = unknown | any;  // any

2、由于任何类型都可以赋值给 unknown 类型,所以在交叉类型中包含 unknown 不会改变结果。

type IntersectionType1 = unknown & null;       // null
type IntersectionType2 = unknown & undefined;  // undefined
type IntersectionType3 = unknown & string;     // string
type IntersectionType4 = unknown & number[];   // number[]
type IntersectionType5 = unknown & any;        // any

在ts中,如何解决引入第三方库没有声明文件的报错问题

1、安装官方的类型声明库
一般来说,如果你用的是一些比较大型、常用的第三方库,那么官方已经帮你写好类型声明了,只要按照这个库的官方文档,安装 @types/库名 这个库就行了。比如,使用 lodash 库的时候,只需要安装它的类型声明库:

npm install --save-dev @types/lodash

2、自己给第三方库写声明文件
并不是所有的第三方库都会写好类型声明库,此时就需要自己给第三方库写声明文件了。在项目的根目录下创建types文件夹(与src文件夹同级),编辑 tsconfig.json 文件,告诉 typescirpt 去哪里找我们自己定义的声明文件:

"baseUrl": "./",
"paths": {
    "*": [ "types/*" ]
}, 

在types文件夹下写对应的第三方库的类型声明,方法就是在types 下新建文件夹,文件夹的名字一定要和第三方库的名字一模一样,然后在新建的文件夹下再新建一个index.d.ts文件,在index.d.ts文件里写类型声明,比如说要给jQuery写类型声明,那么我们就要创建以下目录结构:

|-- test-project                  //项目名字
    |-- ...
    |-- src
    |-- types
        |-- jQuery
            |-- index.d.ts       // 第三方库的声明文件
    |-- ....

在index.d.ts文件中书写的类型,对于不同的库有不同的写法,这里就不展开描述了。但要注意的是声明文件中只是对类型的定义,不能进行赋值。

3、使用 declare module 语法
使用这种方式会把环境包当作 any 类型引入,只能解决代码报错的问题,但是丧失了ts的类型检查功能。
可以在项目根目录下新建 global.d.ts,内容写上

 declare module 'xxxx' //  'xxxx'为第三方库的名字

什么是函数重载

用于实现不同参数输入并且对应不同参数输出的函数,在前面定义多个重载签名,一个实现签名,一个函数体构造,重载签名主要是精确显示函数的输入输出,实现签名主要是将所有的输入输出类型做一个全量定义,防止TS编译报错,函数体就是整个函数实现的全部逻辑。在函数调用的时候根据参数的类型执行不同的函数。

TypeScript 是如何进行类型检查的?

TypeScript 是一种静态类型语言,它在编译阶段执行严格的类型检查以确保代码的正确性和可靠性。

  1. 声明和推断:变量、函数参数、函数返回值等可以通过明确的类型注解来指定其类型。如果没有显式地指明类型,TypeScript 会尝试根据变量或表达式的初始赋值或其他上下文信息进行类型推断。
  2. 结构化类型系统:TypeScript 使用结构化类型系统,这意味着类型兼容性基于对象形状(成员的存在与否和类型)而非命名。
  3. 接口与类:TypeScript 支持接口(Interfaces),用于描述对象结构,类型检查会确保实现该接口的对象具有所有必需的属性和方法。同样支持类(Class),并且有严格的类继承和实现关系的类型检查。
  4. 泛型:泛型允许定义可重用的组件,这些组件可以操作多种类型的值,同时保持类型安全。
  5. 类型保护:使用 typeof、instanceof 和类型守卫(如 in 关键字、用户自定义的类型保护函数等)可以在运行时确定变量的具体类型,并据此做出条件分支内的类型细化。
  6. 枚举、元组 和 联合类型:枚举提供了一种方式来定义一组有限的常量集合。元组允许定义固定数量且不同类型元素组成的数组。联合类型允许一个值为多种类型之一,TypeScript 的控制流分析可以区分不同类型的路径。
  7. 严格模式与可选参数:TypeScript 提供了 --strict 标志启用严格模式,这会引入更多严格类型检查规则,例如禁止隐式any类型、禁用未使用的变量等。函数参数可以标记为可选,并可以指定默认值,类型检查器会验证这些规则。
  8. 编译时错误:当编译器检测到类型不匹配、未定义的变量引用、非法的操作(如对非函数类型的调用)等情况时,它会在编译阶段生成错误消息,阻止错误代码被编译为 JavaScript。
    通过上述机制,TypeScript 编译器在编译时能够全面地检查代码中所有的类型交互,从而提前捕获潜在的类型错误,提高软件质量并增强开发工具的智能提示功能。

时钟的整点数为1-12点,实现一种场景,当时针从12点跳转到3点时,需要顺时针旋转90度(+90),当时针从9点跳转到5点时,需要逆时针旋转120度(-120),请实现一个函数,给定入参为startTime、endTime,返回结果为时针需要旋转的最小角度。

function calculateClockHandRotation(startTime, endTime) {
    const FULL_CIRCLE_DEGREES = 360;
    const DEGREES_PER_HOUR = 30;

    // 处理边界情况,使startTime和endTime始终在1-12范围内
    startTime %= 12;
    endTime %= 12;

    // 计算两点间的实际小时差,取最短距离(考虑时钟是圆形)
    const diff = endTime - startTime;
    let hourDifference = Math.abs(endTime - startTime);
    let rotationDegrees;
    
    // 如果超过6小时,选择通过12点的更短路径
    if (hourDifference > 6 && diff < 0) {
        hourDifference = 12 - hourDifference;
        rotationDegrees = hourDifference * DEGREES_PER_HOUR;
    }else if(hourDifference > 6 && diff >= 0){
        rotationDegrees = -hourDifference * DEGREES_PER_HOUR;
    }else if(diff < 0) {
        rotationDegrees = -hourDifference * DEGREES_PER_HOUR;
    }else {
        rotationDegrees = hourDifference * DEGREES_PER_HOUR;
    }

    return rotationDegrees;
}

为什么存在localStorage里的数据还要再存在vuex里面?

1、localStorage里的数据是以文件的方式存储在本地的,永久保存(不主动删除,则一直存在),所以localStorage数据的读取相当于一次磁盘读取、文件读取,因此,localStorage数据的读取性能较差;vuex里的数据是存储在内存里的,数据的读取速度快,并且会对每一次数据变化进行跟踪,但是如果刷新页面的话,vuex里的数据就会丢失。虽然localStorage的性能不好,但是我们又不得不用localStorage,所以我们只能减少localStorage的读取次数,即刷新或初次打开页面的时候读取一次localStorage的数据到vuex,之后要用直接去vuex里面获取就可以了。
2、localStorage只支持存储字符串,如果是复杂数据类型的话都得使用JSON.parse和JSON.stringify对其进行转化,使用起来较为繁琐。
3、localStorage存储的数据不是响应式的,vuex存储的数据是响应式的(结合computed)。

如何解决移动端1px的问题

在CSS中我们一般使用px作为单位,需要注意的是,CSS样式里面的px和物理像素并不是相等的。CSS中的像素只是一个抽象的单位,在不同的设备或不同的环境中,CSS中的1px所代表的物理像素是不同的。在PC端,CSS的1px一般对应着电脑屏幕的1个物理像素,但在移动端,CSS的1px等于几个物理像素。因此,在移动端1px的线看起来会比较粗一些。
解决办法:
1、伪类+transform:把原先元素的border去掉,然后利用:before或者:after重做border,并 transform的scale缩小一半,原先的元素相对定位,新做的border绝对定位
2、viewport + rem:通过设置对应viewport的rem基准值,这种方式就可以像以前一样轻松愉快的写1px了。
在devicePixelRatio=2 时,设置meta:

<meta name="viewport" content="width=device-width,initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">

在devicePixelRatio=3 时,设置meta:

<meta name="viewport" content="width=device-width,initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no">

实例验证:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>移动端1px问题</title>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
    <meta name="viewport" id="WebViewport"
        content="width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
    <style>
        html {
            font-size: 11px;
        }
        body {
            padding: 1rem;
        }
        * {
            padding: 0;
            margin: 0;
        }
        .item {
            padding: 1rem;
            border-bottom: 1px solid gray;
            font-size: 1.2rem;
        }
    </style>
    <script>
        var viewport = document.querySelector("meta[name=viewport]");
        var dpr = window.devicePixelRatio || 1;
        var scale = 1 / dpr;
        //下面是根据设备dpr设置viewport
        viewport.setAttribute(
            "content", +
            "width=device-width," +
            "initial-scale=" +
            scale +
            ", maximum-scale=" +
            scale +
            ", minimum-scale=" +
            scale +
            ", user-scalable=no"
        );
 
        var docEl = document.documentElement;
        var fontsize = 10 * (docEl.clientWidth / 320) + "px";
        docEl.style.fontSize = fontsize;
    </script>
</head>
<body>
    <div class="item">border-bottom: 1px solid gray;</div>
    <div class="item">border-bottom: 1px solid gray;</div>
</body>
</html>

什么是CI/CD,以及CI/CD工具都有哪些?

CI/CD 是持续集成和持续交付/部署的意思,旨在简化并加快软件开发生命周期。
持续集成(CI)是指自动且频繁地将代码更改集成到共享源代码存储库中的做法。持续交付/持续部署(CD)是一个由两部分组成的过程,涉及代码更改的集成、测试和交付。持续交付不会自动部署到生产环境,持续部署则会自动将更新发布到生产环境。
CI/CD 有时也可理解为进行 CI/CD 的构建服务器,而提供 CI/CD 的服务,如以下产品,将会提供构建服务与 GitHub/GitLab 集成在一起:
Jenkins
GitLab CI/CD
Travis CI

要成为一个前端架构师需要做的事情

我认为一个好的架构师,不仅技术要好,还要懂业务;能从整体设计架构,也能在局部实现功能。首先,技术好是成为架构师的基础条件。在平常的开发中,需要多注意让你的代码容易阅读和扩展,多想想是否有更好的实现方式,多参与代码审查工作。这样通过大量的编码实践,可以逐步地培养出好的架构思维。成为架构师后也要多写代码,如果不写代码,是不能体会出开发的痛点和设计不好带来的问题,无法及时地对架构中的问题做出调整,所设计的架构可能不实用,甚至甚脱离现实。
了解业务:全面调研当前产品的现状,确认当前存在的问题;
寻找方案:预估未来发展的方向,尽可能多的了解相关解决方案或创新自己的方案;
评估方案:和相关同学讨论或开会,评估所有可行的方案及其合适度、复杂度、前瞻性,选出至少一个候选方案;
Demo 开发:基于现有开发能力为所有候选方案开发对应 Demo,提前探路并验证风险和可行性,帮助产出更合适的方案设计;
方案设计:多写、多画、多思考、多讨论相关架构和设计,深入细节产出文档;
RFC 评审:充分评审设计、实现和产物细节,可多次评审直至所有成员达成共识。确定相关开发和团队分工,保证方案完善可执行;
落到实处:推进项目开发,多与开发团队沟通,并至少参与一部分编码工作,打通所有相关开发和运维链路,保障产物简单好用;

前端架构师是否和你想象的有所不同?不仅仅要产出架构图,保证架构的正确执行,深入实现并至少要参与一部分编码工作,落实一个一个解决方案同时,前端架构师也要能阅读代码并经常与各个开发团队交流。可以说整个用户体验链路都有前端架构师的影子,他们了解用户体验;不设限,有审美能力,优雅;能看到其他人看不到的问题,也能解决一些其他人解决不了的问题;能够把复杂的系统想得清楚和透彻,也能够了解各个模块和环节;对未来发展有自己的思考和判断,并不断解决 DX 和 UX 相关问题。

架构师不同于高级开发可以只追求技术的深度,还需要有一定的技术广度。因为技术的选型,通常不能局限于一种技术,需要根据业务特点和团队特点灵活地选择,是 “T”字形的成长模型:

广度:做技术方案时,要有多种选择,最好可以熟悉各个链路的关系
深度:要能解决一些别人搞不定的问题,至少也能指导从某个方向入手排查
其次,要充分理解业务并时刻关注业务变化,使架构不仅能够很好地支持业务特点,并具有一定的前瞻性。架构师需要站在推进业务发展的角度上合理地改进和优化架构设计,为业务的快速发展做好保障。做“合适”的架构非常重要,避免拿着锤子找钉子。

再者,要做一个靠谱并有良好的沟通和协调能力的人。架构师往往要面临着跨组、跨团队甚至跨 BU 的一些技术方案,需要在互相信任的前提下沟通和协调各方的诉求和冲突。好的架构师也可以让业务、技术、团队一起变得更好。
要想成为好的架构师,没有什么捷径可以走。持续的学习,不停的思考,多问为什么,多想想还能不能更好。愿大家都可以成为一个优秀靠谱的程序员、架构师。
此外,还有一些需要思考的问题:
技术选型
技术选型或许是大家最熟悉也最容易开始工作的部分。
你喜欢angular、react、vue还是其他框架?路由库、网络请求库、数据层用什么工具管理?使用或是定制化哪一套UI库?
你会使用less、sass还是postcss?是否需要为多语言和换肤功能引入额外的工具?
你和你的小伙伴是否是ts铁粉?项目中是否需要引入wasm?兼容性预期是什么,应该如何配置babel、是否需要引入特定的polyfill?
你将面对的是一个PC网站的开发,移动web应用,或是一个electron桌面项目?响应式和自适应如何选择?如果你开发的是小程序,你会选择taro、uniapp/mpvue或是选择不使用这些工具?
你的应用是否需要与APP整合?直接使用webview还是选择使用cordova?如果你开发的是一个跨平台APP,你将选择RN、weex还是flutter?如果是一个web应用,是否需要通过PWA来支持离线使用?
技术选型只是第一步。请切记,我们需要考虑的不仅仅是技术本身的优劣(很多时候不同技术本身就难以分出胜负),你还要考虑你和你的团队对于技术的熟悉程度和学习成本,并带领他们掌握你所选择的技术栈。
代码规范
你需要制定一系列规则来使团队的代码风格尽可能一致,当面对大型和长期维护的项目时,这点往往比想象得重要。
你可能需要借助一些工具,例如jslint、tslint、csslint、prettier等等。你还需要考虑使用BME命名、OOCSS、scoped css、css in js还是其他什么方法来避免CSS冲突。
当然这只是问题的一部分,还有许多问题或许无法通过工具来限制。例如,对于数组命名,使用arr、list还是复数单词(sheep这样的单词是否会让人感到困扰?)?文件目录如何组织?点击事件是否统一以on开头?
作为架构师或是Team Leader,你需要保持定期code review的习惯来保证团队正在以好的习惯书写代码。
以及,你是否需要统一团队的工具配置,例如IDE配置、git配置(例如使用LF还是CRLF作为换行符)、是否需要使用nrm来管理npm源并使用nvm来确保统一的node/npm版本?
UI规范
除了代码规范之外,如果有机会和设计师一起来制定一系列UI/UX规范,这会令你的项目长期受益。
确定主题与配色,typography(排印)规范(包括字体、字号、行高)。
你还要与设计师一通设计一套常用的组件以及交互方式,以确保你的界面在设计上遵循亲密性、对比、对其、重复的规则(《写给大家看的设计书》),同时给用户带来及时、一致、可控可预期的交互体验,例如”确定“统一使用绿色按钮,”取消“统一使用灰色按钮并在弹窗的左侧,蓝色文字代表连接与跳转,”删除“按钮使用红色并且每次点击会弹出二次确认框。
品质,来源于对细枝末节的把控。
公共代码
你需要管理你的公共代码,无论是UI组件还是非UI功能。
即使是只考虑单独的项目,也应该将具有复用可能意义的代码放到单独的目录下管理。
为了使用公共代码库,你需要搭建私有npm仓库,还是使用git submodule的形式?你的库是针对web还是nodejs设计的,对于web,你将通过webpack还是cdn的形式引入代码?如果不同形式都需要兼容,那以下形式的代码你应当非常熟悉了:

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global = global || self, global.VueRouter = factory());
}(this, function () {
  //
}));

文档
正如之前所言,你需要带领你的团队掌握新技术并高效高质量的开发项目,并提炼可复用的代码与组件,许多内容都需要文档化。
对于非UI组件,JSDoc是一个不错的文档工具。对于UI组件,storybook绝对是一个神奇,你可以通过它创建一系列可展示、可交互的组件示例,并且让其他人可以直接拷贝实例代码。
对于非来源于代码的文档,你可能需要借助静态网站生成器(static site generator)或是博客工具来书写文档,vuepress、wordpress、conflurence、石墨文档等等都是不错的选择。
为你的团队提供可参考的代码模板是个好主意,对此你可以使用例如hygen(jondot/hygen)这样的工具。当我需要添加一个列表页面时,我会用模板生成初始代码,其中包含了分页加载、弹窗交互、筛选查询等等代码的模板,保证我添加的页面有一致的代码风格和交互行为。
分支管理
你需要为团队设立一套分支管理规范,如果对此你还不了解,搜索”gitflow“你可以找到需要有用的资料。
除此之外,是否所有提交到主分支的代码合并都需要提交pull request并通过code review?如何与CI/CD流程整合?commit message和提交频率是否要加以规范?merge还是rebase?
演进能力
对于架构而言,演进能力同样重要。将webpack从3.x升级到4.x,将vue从2.x升级到3.x,或许每次升级都会成为阵痛,但我们不能停滞不前,否则项目会慢慢变成一个难以维护的老顽固。
好在微前端理念能帮助我们处理升级和重构所带来的麻烦。single-spa是目前最流行的微前端框架之一,qiankun(umijs/qiankun)在其之上为多项目结构提供了可行的实施方案。
通过微前端架构,我们可以逐步替代项目中的代码,引入最近的技术,而不必因为畏惧而停滞不前,或是每次都翻江倒海一翻。
自动化:部署 & 测试
作为架构师,你需要认真了解你所使用/搭建的脚手架和webpack,熟悉每一项配置(module、optimization、plugins…)的意义与使用。
除此之外,你需要了解git hooks、docker,懂一些shell script,以便和运维团队一起整合CI/CD流程。
你需要通过自动化测试以及代码覆盖率检测来保证项目稳定前进,并制定合适的适应度函数来观察项目的健康程度。可选的工具包括jest、instanbul等等,如果你需要编写e2e测试,那你可能还需要熟悉headless chrome。
如果你决定添加自动化测试,或是遵循TDD/BDD的开发理念,这很好,不过请做好准备,这可是一项会持续牵扯团队开发经历的大工程。

为什么小程序性能比h5好

1、传统 Web 的架构模型是单线程架构,其渲染线程和脚本线程是互斥的,这也就是说为什么长时间的脚本运行可能会使页面失去响应。
而小程序能够具备更卓越用户体验的关键在于其架构模型有别于传统 Web,小程序为双线程架构的,其渲染线程和脚本线程是分开运行的。渲染层的界面使用了 WebView 进行渲染,逻辑层采用 JsCore 线程运行 JS 脚本。
逻辑层:创建一个单独的线程去执行 JavaScript,在这个环境下执行的都是有关小程序业务逻辑的代码,只能够运行 JavaScript 的沙箱环境,不提供 DOM 操作相关的 API,所以不能直接操作 UI,只能够通过 setData更新数据的方式异步更新 UI 。

渲染层:界面渲染相关的任务全都在 WebView 线程里执行,通过逻辑层代码去控制渲染哪些界面。一个小程序存在多个界面,所以渲染层存在多个 WebView 线程。

渲染层和逻辑层具体如何通信?
两个线程的通信会经过微信的客户端【Native】 做中转,逻辑层发送网络请求也经由 Native 转发
逻辑层,通过调用 setData 函数将数据传递到渲染层
两个线程之间的通信基于微信提供的 WeiXinJsBridge ,
其实很好理解,渲染层是 webview,逻辑层是【原生】,所以二者的通信原理和开发原生 app 和 h5 的通信是一样的
每次调用 setData 都需要进行一次线程间的通信,所以在小程序的优化手段中有一个就是,尽量减少 setData 的调用
关于 setData 请看官网文档

2、小程序的运行环境是基于浏览器内核完全重构的一个内置解析器,针对性做了优化,配合自己定义的开发语言标准,提升了小程序的性能。 脚本内无法使用浏览器中常用的window对象和document对象,从源头上避免了DOM的内存泄漏。
对于H5,H5无法控制开发人员对DOM的操作,容易出现内存泄漏,在SPA单页应用还存在页面加载慢的问题。
3、当HTML5应用面对复杂的业务逻辑或者丰富的页面交互时,它的体验总是不尽人意,需要不断的对项目优化来提升用户体验。但是由于微信小程序运行环境独立,尽管同样用html+css+js去开发,但配合微信的解析器最终渲染出来的是原生组件的效果,自然体验上将会更进一步。

nginx反向代理需要做哪些配置?

Nginx的反向代理配置主要在nginx.conf或包含的子配置文件(如sites-enabled/*.conf)中进行。配置主要包括以下部分:
HTTP服务器块 (server): 定义监听的IP地址、端口以及与之关联的域名(server_name)。
位置块 (location): 根据请求URI进行匹配,并定义在此范围内应执行的操作,如反向代理。
反向代理指令 (proxy_pass): 指定请求应被转发到的后端服务器的URL。Nginx将替换匹配到的位置块中的URI,并将其发送到指定的后端服务器。

server {	
		server_name promotion.cdel.local;
        charset utf-8;
		listen 80;
        access_log  logs/access.promotion.cdeledu.com.log;
		resolver 127.0.0.1;

	 location /service-trade-favourable/ {  
     proxy_pass http://localhost:33012/;  
}
}

代码详解:

server 块:这是 Nginx 配置文件中的主要块,用于定义一个虚拟服务器。
server_name promotion.cdel.local;:指定了要匹配的服务器名。当请求的 Host头和该值匹配时,Nginx 将使用当前的 server 块进行处理。
charset utf-8;:设置字符集为 UTF-8,以确保正确的编码和解析请求。
listen 80;:指定 Nginx 监听的端口号为 80,这是 HTTP 请求的默认端口。
access_log:logs/access.promotion.cdeledu.com.log;:定义了访问日志的位置和文件名。通过此配置,Nginx将记录访问日志到指定的文件中,便于后续分析和监控。
resolver 127.0.0.1;:指定用于解析 DNS 的 IP 地址。在这个例子中,使用本地的 DNS 解析器(即127.0.0.1)进行域名解析。
location /service-trade-favourable/ {}:这是一个 location 块,用于匹配特定的请求路径。
proxy_pass http://localhost:33012/;:设置代理规则,将匹配到的请求转发到本地的端口 33012上。http://localhost:33012/ 是代理目标的地址。
这段配置的作用是,当请求的 Host 头与 promotion.cdel.local 匹配,并且请求路径为 /service-trade-favourable/ 时,Nginx 将代理该请求到本地的端口 33012 上。

你可以根据自己的实际需求,修改 server_name、listen 端口、location 路径和 proxy_pass 地址来满足你的代理需求。确保在使用此配置之前,已经安装并正确配置了 Nginx,并将该配置文件保存为有效的 Nginx 配置文件(通常以 .conf 结尾)。

不用脚手架,如何从零搭建一个前端项目

  1. 创建根目录
    创建 ManuallyReact 文件夹,通过编辑器(webstorm 或者 vcode )打开。
    在控制台执行以下命令初始化项目,安装完成之后会自动创建 package.json 文件。
npm init

注意:在执行 npm init 初始化项目时会有一系列配置提示,直接 enter 进入下一步即可。

  1. 安装 react
    使用 React 框架必须安装 react 和 react-dom 这两个包。

npm i react react-dom

其中 react 是实现虚拟 DOM,react-dom 是实现与原生 DOM 的交互。

  1. 安装 webpack
    工程化项目必须安装 webpack 打包工具。
    在控制台执行以下命令安装 webpack,安装成功后会出现 node_modules 文件夹和 package-lock.json。
npm i webpack@4.32.2 webpack-cli@2.0.9 webpack-dev-server@3.0.0

其中 node_modules 文件夹是 webpack 相关的包依赖,package-lock.json 文件是对包依赖的一些描述(名称、版本、依赖包等)。

注意:在安装 webpack、webpack-cli、webpack-dev-server 这三个包时一定要按照指定的版本安装,如果随意安装,这三个包之间可能会出现版本不兼容的情况。

  1. 安装 loaders 和 plugins
    webpack 本身只能打包 JS(ES5) 文件,所以如果涉及到 JS ES6 语法和 CSS 样式。就必须安装相关的 loader,以及一些必要的 plugin。

安装 babel 编译 ES6 语法

npm i @babel/core@7.12.3 babel-loader@8.1.0 @babel/preset-react@7.12.1

其中 @babel/core 是核心功能包,babel 从版本 7 开始用 @开头的 scope 包,老版本是babel-core,其他的包都是一样的规律。
babel 中带 preset 的包指的是预设的一些 plugin 集合。比如使用 React 框架就需要安装 @babel/preset-react,其作用是实现 jsx 语法。

安装 loader 编译 CSS 文件

npm i css-loader@5.0.0 style-loader@2.0.0

其中 css-loader 是编译 CSS 文件,style-loader 是将编译好的 CSS 文件插入到 style 中。

安装 plugin 打包 HTML 模板

npm i html-webpack-plugin@4.5.0

注意:在安装 @babel/core 和 @babel/preset-react 这两个包时一定要安装相同大版本的包,比如要么都安装 版本 7,要么都降级到版本 6,否则会报各种错,大概率就是版本不兼容导致。

安装完成之后 package.json 文件,内容如下:

{
  "name": "manuallyreact",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server --open",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/core": "^7.12.3",
    "@babel/preset-react": "^7.12.1",
    "babel-loader": "^8.1.0",
    "css-loader": "^5.0.0",
    "html-webpack-plugin": "^4.5.0",
    "react": "^16.14.0",
    "react-dom": "^16.14.0",
    "style-loader": "^2.0.0",
    "webpack": "^4.44.2",
    "webpack-cli": "^2.0.9",
    "webpack-dev-server": "^3.0.0"
  }
}
  1. 配置 webpack
    安装了相关的依赖之后需要配置 webpack。在根目录下新建 webpack.config.js 文件,内容如下。
const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry:'./src/index.js',
    output:{
        path:path.resolve(__dirname,'dist'),
        filename:"main.js"
    },
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true,//实时刷新
    },
    module:{
        rules:[
            {
                test: /\.js$/,
                exclude: path.resolve(__dirname, 'node_modules'),
                include: path.resolve(__dirname, 'src'),
                loader:'babel-loader',
                options:{
                    presets: [
                        "@babel/react"
                    ]
                }
            },
            {
                test: /\.css$/,
                use:['style-loader','css-loader']  // 从右到左执行,所以注意顺序
            }
        ]
    },
    plugins:[new htmlWebpackPlugin({
        template:path.join(__dirname,'./public/index.html'),
        filename: 'index.html'
    })
    ],
}
  1. 创建项目
    在根目录下新建 public 文件夹,在该文件夹下新建 index.html,内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>Manually React</title>
</head>
<body>
<div id="root"></div>
</body>
</html>\

在根目录下新建 src 文件夹,在 src 文件夹下:
新建 index.js 作为入口文件。
新建 index.css 用于编写样式。

index.js


```javascript
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
const App = (
    <div className="border">
        <h1>Manually React</h1>
    </div>
)
ReactDOM.render(App,document.getElementById("root"));

index.css


```css
.border{
    border: 1px solid red;
    border-radius: 10px;
    margin: 10px;
    text-align: center;
}

执行 npm start

以下代码会输出什么结果

let map1 = new Map([['A', '1'],['B', '1'],['C', '1']])
let map2 = new Map([['A', '2'],['B', '3']])
const m = new Map([...map1, ...map2])
console.log(m)

咱们直接看运行结果:
在这里插入图片描述
介绍下Map的相关知识点吧!
map是v8解析器里内置的一个对象,在初始化的时候,可以直接使用构造函数:

let map=new Map();

map赋值有两种方式,一种是直接用map自带的set方法来设置,一种是初始化时,直接在构造函数中传入。

1、直接set方法

map.set("a","1");
map.get("a"); //"1"

map的key值可以是字符串或者数字,但是1和’1’并不是同一个key。

map.set("1","test1")
map.get("1")//"test1"
map.get(1)//undefined

let let m="",n=false;
map.set(m,"m1")
map.get(m)//m1
map.get(n)//undefined

set的key只有严格相等,才属于同一个key,比如字符串“1”和数字1虽然用 == 比较是相同的,但是用===比较是false,不是同一个key。只有通过严格比较===是相同的,才属于同一个key。

map的key值可以是boolean

map.set(true,"true1");

map的key值可以是对象
如果是对象,要注意,这里set对象时,map里保存的是这个对象的引用地址,所以,a={a:1};b={a:1};此时map.set(a)是无法通过map.get(b)来获取的:

let a={a:1},b={a:1};
map.set(a,222);
map.get(b)//undefined,a和b值一样,但是引用地址不同,所以属于不同的key值
map.get(a)//222
let c=a;
map.get(c)//222

如上代码,只有对象的引用地址一致,key值才是相等的。也可以通过和字符串一样通过严格比较===相等的,才属于同一个key。

map的key可以是函数
当map的key值是函数时,如果函数是执行的,那么key值是这个函数里的返回值,如果没有设置返回值,那么默认为undefined。如果函数没有执行,key为这个函数本身

function fn1(){
	console.log(2);
}
function fn2(){
	return 23
}
map.set(fn1,"fn1");//fn1函数的内容=>fn1
map.set(fn1(),"fn1-1");//undefined=>fn1-1
map.set(fn2(),"fn2");//23=>fn2

2、初始化时,直接在构造函数中传入
以数组作为参数传入构造函数

let map=new Map([['key1','1'],['key2','2']]);
map.get('key1')//1
map.get('key2')//2

map构造函数接受数组作为参数时,实际执行的如下操作

let map=new Map();
let str=[['key1','1'],['key2','2']];
str.forEach(([key,value])=>{
	map.set(key,value)
})

从以上代码可以看出,构造函数传入数组时,传入的值:

(1)必须是二维数组,一维数组时会报错。
在这里插入图片描述

因为forEach第一层循环之后,调用之前函数([key,value])参数必须是可以遍历的iterator。如果是一维数组就会报错。

(2)如果二维数组最里层的数组长度大于2,那么后边的值会被忽略,只取前两位

let map=new Map([['key1','1',2,4],['key2','2']]);
map//{"key1" => "1", "key2" => "2"}

(3)如果传入的是对象数组,那么不会报错,但也不能正常赋值。只会默认赋值为undifened
在这里插入图片描述
map的key值可以是任意类型的变量,这是它优于对象的一点。

注意:map里不会有重复元素,在给map赋值时,如果值已经存在,则会覆盖此值。这也是以下代码这样输出的原因
在这里插入图片描述
对map数据使用展开运算符的话,会输出由key、value组成的数组,有多个属性就会输出多个数组,如下所示:
在这里插入图片描述

有关浅拷贝的题目

在这里插入图片描述

|| 和 && 操作符的返回值

注意:|| 和 && 操作符的返回值不是布尔值!!!不是布尔值!!!不是布尔值!!!
当使用 ||(逻辑或)和 &&(逻辑与)操作符时,首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。

逻辑或(||)操作符: 返回第一个为真的操作数,如果所有操作数都为假,则返回最后一个假值。
在这里插入图片描述
如果有任何一个操作数为真,那么整个表达式就为真,此时返回第一个为真的操作数的值。
如果所有操作数都为假,那么整个表达式就为假,此时返回最后一个假值。

逻辑与(&&)操作符: 返回第一个为假的操作数,如果所有操作数都为真,则返回最后一个真值。
在这里插入图片描述

如果有任何一个操作数为假,那么整个表达式就为假,此时返回第一个为假的操作数的值。
如果所有操作数都为真,那么整个表达式就为真,此时返回最后一个真值。

[“1”, “2”, “3”].map(parseInt)的结果是什么

在这里插入图片描述

  1. map函数

将数组的每个元素传递给指定的函数处理,并返回处理后的数组,所以 [‘1’,‘2’,‘3’].map(parseInt) 就是将字符串1,2,3作为元素;0,1,2作为下标分别调用 parseInt 函数。即分别求出 parseInt(‘1’,0), parseInt(‘2’,1), parseInt(‘3’,2)的结果。

  1. parseInt函数

概念:以第二个参数为基数来解析第一个参数字符串,通常用来做十进制的向上取整(省略小数)如:parseInt(2.7) //结果为2

特点:接收两个参数parseInt(string,radix)

string:字母(大小写均可)、数组、特殊字符(不可放在开头,特殊字符及特殊字符后面的内容不做解析)的任意字符串,如 ‘2’、‘2w’、‘2!’

radix:解析字符串的基数,基数规则如下:

1) 区间范围介于2~36之间;

2 ) 当参数为 0,parseInt() 会根据十进制来解析;

3 ) 如果没有第二个参数,默认的基数规则:

       如果 string 以 "0x" 开头,parseInt() 会把 string 的其余部分解析为十六进制的整数;parseInt("0xf")   //15 

       如果 string 以 0 开头,其后的字符解析为八进制或十六进制的数字;parseInt("08")   //8

       如果 string 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数;parseInt("88.99f")   //88

       只有字符串中的第一个数字会被返回。parseInt("10.33")   //返回10;

       开头和结尾的空格是允许的。parseInt(" 69 10 ")   //返回69

       如果字符串的第一个字符不能被转换为数字,返回 NaN。parseInt("f")  //返回NaN  而parseInt("f",16)  //返回15
  1. 代码解析:
    [‘1’,‘2’,‘3’].map(parseInt)即

parseInt(‘1’,0);radix 为 0,parseInt() 会根据十进制来解析,所以结果为 1;

parseInt(‘2’,1);radix 为 1,超出区间范围,所以结果为 NaN;

parseInt(‘3’,2);radix 为 2,用2进制来解析,第一个参数中的数据应该只存在 0 和 1,所以结果为 NaN。

  1. parseInt方法解析的运算过程:
    parseInt(‘101.55’,10); //以十进制解析,运算过程:向上取整数(不做四舍五入,省略小数),结果为 101。

parseInt(‘101’,2); //以二进制解析,运算过程:12的2次方+02的1次方+1*2的0次方=4+0+1=5,结果为 5。

parseInt(‘101’,8); //以八进制解析,运算过程:18的2次方+08的1次方+1*8的0次方=64+0+1=65,结果为 65。

parseInt(‘101’,16); //以十六进制解析,运算过程:116的2次方+016的1次方+1*16的0次方=256+0+1=257,结果为 257。

如何能够将字符串作为可执行的程序代码

通常我们有好几种方法来实现这个需求,最常见的方法是eval操作符,但我们常常在刚学习JavaScript的时候被告知,使用eval是一个非常不明智的决策,因为使用它会导致潜在的安全问题,因此我们放弃这个方法。
一个更好的方法是使用Function构造器,它需要两个参数:使用逗号分隔的参数列表字符串,和函数体字符串。例如:

const plusOne = new Function("n", "return n+1");
console.log(plusOne(5)); // 6

在这里插入图片描述

函数柯里化

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

function curry(fn, args) {
    var length = fn.length;

    args = args || [];

    return function() {

        var _args = args.slice(0),

            arg, i;

        for (i = 0; i < arguments.length; i++) {

            arg = arguments[i];

            _args.push(arg);

        }
        if (_args.length < length) {
            return curry.call(this, fn, _args);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}


var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

其实说白了,以上代码用一句话就可以概括:用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数

当接收的参数数量与原函数的形参数量相同时,执行原函数;
当接收的参数数量小于原函数的形参数量时,返回一个函数用于接收剩余的参数,直至接收的参数数量与形参数量一致,执行原函数。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值