前端面试题详解整理107|Vue-Router 垂直水平居中,浏览器发送请求缓存,从url到渲染页面的过程,继承,new操作符,数据类型,数组方法,数组去重,

57 阅读22分钟

锐明 前端 一面面经

3.7 一面

说说数据类型,引用数据类型的数组

数组常用方法有哪些

数组去重方式
数据类型是编程语言中用来表示数据的种类或类型。在 JavaScript 中,数据类型包括以下几种:

  1. 基本数据类型(Primitive Data Types): 存储简单数据值的类型。

    • 字符串(String)
    • 数字(Number)
    • 布尔值(Boolean)
    • 空(Null)
    • 未定义(Undefined)
    • 符号(Symbol)(ES6 新增)
  2. 引用数据类型(Reference Data Types): 存储对象的引用或复杂数据值的类型。

    • 对象(Object)
    • 数组(Array)
    • 函数(Function)
    • 日期(Date)
    • 正则表达式(RegExp)
    • 等等

引用数据类型的数组是存储引用类型数据的容器,可以同时存储多个不同类型的对象或其他数组。数组常用的方法包括:

  1. push(): 在数组末尾添加一个或多个元素,并返回新数组的长度。
  2. pop(): 删除并返回数组的最后一个元素。
  3. shift(): 删除并返回数组的第一个元素。
  4. unshift(): 在数组开头添加一个或多个元素,并返回新数组的长度。
  5. concat(): 连接两个或多个数组,并返回新数组。
  6. slice(): 返回数组的一部分,不修改原数组。
  7. splice(): 在指定位置添加或删除元素,并返回被删除的元素组成的数组。
  8. forEach(): 遍历数组的每个元素,并对其执行提供的函数。
  9. map(): 遍历数组的每个元素,并使用提供的函数创建一个新数组。
  10. filter(): 使用提供的函数测试数组的每个元素,并创建一个包含所有通过测试的元素的新数组。
  11. reduce(): 对数组中的每个元素执行一个提供的函数,将其结果汇总为单个值。

数组去重的常用方式包括:

  1. 使用 Set 数据结构:利用 Set 对象的特性,将数组转换为 Set,然后再转换回数组。
  2. 使用 filter 方法:遍历数组,通过 filter 方法筛选出不重复的元素。
  3. 使用 reduce 方法:利用 reduce 方法遍历数组,将不重复的元素添加到新数组中。
  4. 使用对象属性:遍历数组,利用对象属性的唯一性来进行去重。
  5. 使用 includes 或 indexOf 方法:遍历数组,利用数组方法检查元素是否已经存在于新数组中。

说说事件循环

说说原型和原型链
事件循环(Event Loop)是 JavaScript 中处理异步操作的机制。它负责处理事件队列中的事件,并将其分发到执行栈中执行。事件循环确保 JavaScript 在单线程环境下能够处理异步操作,而不会阻塞主线程。

事件循环的主要步骤如下:

  1. 执行同步任务: 首先,事件循环会执行执行栈中的同步任务,直到执行栈为空。

  2. 处理微任务(Microtask): 接着,事件循环会处理微任务队列中的所有任务,直到微任务队列为空。微任务通常包括 Promise 的回调函数、MutationObserver 和 process.nextTick 等。

  3. 处理宏任务(Macrotask): 最后,事件循环会处理宏任务队列中的一个任务。宏任务通常包括 setTimeout、setInterval、setImmediate、I/O 操作和 UI 渲染等。处理完一个宏任务后,事件循环会再次处理微任务队列,然后继续处理下一个宏任务,如此循环。

通过这种方式,事件循环确保 JavaScript 在执行异步操作时能够保持响应,并且不会阻塞主线程的执行。

原型和原型链是 JavaScript 中非常重要的概念,用于实现对象之间的继承关系。在 JavaScript 中,每个对象都有一个原型(prototype),原型是一个对象,其他对象可以通过原型继承其中的属性和方法。

当访问一个对象的属性或方法时,JavaScript 引擎会先在对象本身查找,如果找不到,则会沿着原型链向上查找,直到找到属性或方法为止。原型链的顶端是 Object.prototype,它是所有对象的原型。

以下是原型和原型链的主要特点:

  1. 每个对象都有一个原型(prototype): 原型是一个对象,包含着共享的属性和方法。

  2. 通过原型链实现继承: JavaScript 中的继承是通过原型链实现的,子对象可以继承父对象的属性和方法。

  3. 原型链的结构: 每个对象都有一个原型,原型又有自己的原型,形成一个链式结构,最终指向 Object.prototype。

  4. 原型链的查找: 当访问一个对象的属性或方法时,JavaScript 引擎会先在对象本身查找,如果找不到,则沿着原型链向上查找,直到找到为止。

原型和原型链是 JavaScript 中非常重要的概念,理解它们有助于更好地理解 JavaScript 中的对象和继承机制。

new操作符中干了什么

new 操作符用于创建一个新对象,并将构造函数(constructor)绑定到该对象。当使用 new 操作符调用构造函数时,会执行以下几个步骤:

  1. 创建一个新对象: 首先,创建一个新的空对象,该对象会成为构造函数的实例。

  2. 将构造函数的作用域赋给新对象: 在创建新对象后,将构造函数的作用域(即 this 关键字)绑定到新对象上,以便在构造函数中使用 this 来引用新对象。

  3. 执行构造函数: 在将构造函数的作用域绑定到新对象后,会执行构造函数内部的代码,其中可能会对新对象进行初始化操作。

  4. 返回新对象: 如果构造函数没有显式地返回一个对象,则 new 操作符会隐式地返回新创建的对象。如果构造函数显式地返回一个对象(非原始值),则 new 操作符会返回该对象而不是新创建的对象。

下面是一个简单的示例:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 使用 new 操作符创建一个 Person 对象
const person1 = new Person('John', 30);
console.log(person1); // { name: 'John', age: 30 }

在上面的示例中,使用 new 操作符调用了 Person 构造函数,并传入了参数 'John'30new 操作符创建了一个新的空对象,并将构造函数 Person 的作用域绑定到该对象上,然后执行构造函数内部的代码,最后返回新创建的对象 person1

从url到渲染页面的过程

从 URL 到渲染页面的过程涉及多个步骤,包括 DNS 解析、建立 TCP 连接、发送 HTTP 请求、接收 HTTP 响应、解析 HTML、构建 DOM 树、渲染页面等。以下是从 URL 到渲染页面的简要过程:

  1. DNS 解析: 浏览器首先需要将 URL 解析为服务器的 IP 地址,这个过程称为 DNS 解析。浏览器会先检查本地缓存,如果找不到,则向 DNS 服务器发送查询请求,获取服务器的 IP 地址。

  2. 建立 TCP 连接: 浏览器根据解析得到的服务器 IP 地址,通过 TCP/IP 协议向服务器发起连接请求。TCP 连接的建立包括三次握手,确保客户端和服务器之间建立可靠的通信连接。

  3. 发送 HTTP 请求: 一旦建立了 TCP 连接,浏览器就会向服务器发送 HTTP 请求。请求中包括请求方法(GET、POST 等)、URL、请求头(User-Agent、Cookie 等)、请求体(POST 请求时),服务器接收到请求后进行处理。

  4. 接收 HTTP 响应: 服务器接收到请求后,会根据请求内容生成相应的 HTTP 响应,并将响应内容发送回客户端。响应包括响应状态码(200、404 等)、响应头(Content-Type、Cache-Control 等)、响应体(HTML、CSS、JavaScript 等)。

  5. 解析 HTML: 浏览器接收到 HTTP 响应后,会开始解析 HTML 文件。解析过程包括词法分析和语法分析,将 HTML 文件转换为 DOM(文档对象模型)树。

  6. 构建 DOM 树: 解析 HTML 文件后,浏览器会根据解析得到的 DOM 元素构建 DOM 树。DOM 树表示 HTML 文档的层次结构,每个 HTML 元素都是树中的一个节点。

  7. 渲染页面: 构建完 DOM 树后,浏览器会根据 DOM 树和 CSS 样式信息计算出每个节点的渲染样式,并将页面内容渲染到屏幕上。这个过程包括布局(Layout)、绘制(Painting)和合成(Compositing)等步骤。

  8. 执行 JavaScript: 如果 HTML 中包含了 JavaScript 代码,浏览器会在渲染过程中解析和执行 JavaScript。JavaScript 的执行可能会改变 DOM 结构、样式或页面内容,从而触发重新渲染。

  9. 完成页面加载: 页面渲染完成后,浏览器会触发 load 事件,表示页面加载完成。此时页面已经可以与用户进行交互。

以上是从 URL 到渲染页面的简要过程,其中涉及了多个步骤和技术。

说说缓存

html,css,js解析流程,想让js先与页面解析,后于页面解析该怎么做
要让 JavaScript 在页面解析之前先执行,可以通过将 JavaScript 放置在 HTML 页面中的合适位置或者使用异步加载的方式来实现。

  1. 将 JavaScript 放置在 <head> 标签中: 将 JavaScript 放置在 <head> 标签中可以确保在页面其他内容加载和解析之前先加载和执行 JavaScript。但是要注意,这样可能会阻塞页面的渲染,因为浏览器会在执行 JavaScript 之前等待 JavaScript 文件的加载和执行完成。
<!DOCTYPE html>
<html>
<head>
  <title>页面标题</title>
  <script src="your-script.js"></script>
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>
  1. 使用 defer 属性: 将 JavaScript 放置在 <head> 标签中,并使用 defer 属性可以延迟脚本的执行,使其在页面解析完成后执行。使用 defer 属性可以确保 JavaScript 在页面解析过程中不会阻塞页面的渲染,但会按照顺序在 DOMContentLoaded 事件触发前依次执行。
<!DOCTYPE html>
<html>
<head>
  <title>页面标题</title>
  <script src="your-script.js" defer></script>
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>
  1. 使用 async 属性: 如果不需要保证 JavaScript 的执行顺序,可以使用 async 属性将 JavaScript 异步加载,这样 JavaScript 文件会在加载完成后立即执行,不会阻塞页面的解析和渲染。但是要注意,使用 async 属性加载的 JavaScript 文件执行顺序不确定。
<!DOCTYPE html>
<html>
<head>
  <title>页面标题</title>
  <script src="your-script.js" async></script>
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>

通过上述方式,可以控制 JavaScript 在页面解析之前或之后执行,根据实际需求选择合适的方式。

浏览器发送请求时有没有限制数量,大概是多少,怎么进行优化

浏览器发送请求时通常存在以下限制:

  1. 并发请求限制: 浏览器对同一域名下的并发请求数量有限制。不同浏览器的限制数量不同,一般为 6 到 8 个并发请求。这意味着当页面中有超过这个数量的资源需要加载时,部分资源会被阻塞,直到前面的请求完成。

  2. 同一域名下的并发连接限制: 浏览器对同一域名下的并发连接数量也有限制,这个数量通常为 6 到 8 个。这意味着当页面中需要加载大量资源时,即使这些资源来自不同的路径,也会受到同一域名下的并发连接限制。

  3. 总请求数量限制: 虽然没有硬性规定,但是过多的请求也会对性能产生负面影响。因此,通常建议尽量减少页面中的请求数量,合并和压缩静态资源,使用懒加载和分页加载等技术来优化页面加载性能。

针对以上限制,可以进行如下优化:

  • 资源合并和压缩: 将多个小文件合并为一个大文件,并对静态资源进行压缩,减少请求数量和资源大小。

  • 使用 CDN: 将静态资源部署到 CDN 上,利用 CDN 的分布式网络提供快速的资源访问速度,减少对同一域名下的并发连接限制的影响。

  • 延迟加载和懒加载: 将页面中不必要的资源延迟加载或懒加载,只在需要时再进行加载,减少初始加载时的请求数量。

  • 资源预加载: 对于即将需要的资源,可以通过预加载的方式提前加载到缓存中,以减少未来的请求延迟。

  • 使用 HTTP/2: HTTP/2 支持多路复用,可以在同一连接上同时发送多个请求,从而减少了并发请求和连接数量的限制。

通过以上优化措施,可以提高页面加载性能,减少页面加载时间,提升用户体验。

css中position的属性

css实现文本溢出…省略

CSS 中的 position 属性

position 属性用于指定元素在文档流中的定位方式。常用的属性值包括:

  1. static(默认值): 元素在文档流中的位置由元素在 HTML 中的位置决定,不会被偏移或浮动。

  2. relative: 元素在文档流中的位置保持不变,但可以通过 toprightbottomleft 属性进行偏移,偏移不会影响其他元素的布局。

  3. absolute: 元素的位置相对于其最近的非 static 定位的祖先元素进行定位,如果不存在这样的祖先元素,则相对于最初的包含块进行定位。元素脱离了文档流,不会影响其他元素的布局。

  4. fixed: 元素相对于浏览器窗口进行定位,即使页面滚动,元素也会保持固定的位置。元素脱离了文档流,不会影响其他元素的布局。

  5. sticky: 元素先在正常文档流中占据位置,然后在特定的阈值滚动容器(例如,窗口或滚动容器)的视口中变为固定定位。它会在其父元素内垂直滚动时始终保持位置不变,直到达到指定的偏移量。

CSS 实现文本溢出省略

当文本内容超出容器的尺寸时,可以通过 CSS 实现文本溢出省略,常用的属性包括:

  1. white-space: 用于控制文本的空白处理方式。

    • normal:默认值,合并连续的空白字符,并折叠换行符。
    • nowrap:不换行。
    • pre:保留空白字符序列,但合并换行符。
    • pre-wrap:保留空白字符序列,保留换行符。
    • pre-line:合并空白字符序列,保留换行符。
  2. overflow: 用于控制元素内容溢出时的处理方式。

    • visible:默认值,内容不会被修剪,会呈现在元素框之外。
    • hidden:内容会被修剪,超出部分不可见。
    • scroll:显示滚动条,即使内容未溢出。
    • auto:只有当内容溢出时才显示滚动条。
  3. text-overflow: 用于控制文本溢出时的处理方式,只能在 overflow 属性为 hiddenscroll 时生效。

    • clip:默认值,内容被修剪,不显示省略号。
    • ellipsis:显示省略号(...)来代表被修剪的文本。

例如,可以通过设置 white-space: nowrap; overflow: hidden; text-overflow: ellipsis; 来实现文本内容超出容器时显示省略号。

css实现垂直水平居中

实现元素在父容器中垂直水平居中有多种方法,以下是其中一些常用的方法:

1. 使用 Flexbox(弹性盒子布局)

.parent {
  display: flex;
  justify-content: center; /* 水平居中 */
  align-items: center; /* 垂直居中 */
}

2. 使用 Grid(网格布局)

.parent {
  display: grid;
  place-items: center; /* 水平垂直居中 */
}

3. 使用绝对定位和偏移量(知道元素的宽高)

.parent {
  position: relative;
}

.child {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

4. 使用绝对定位和负边距(不需要知道元素的宽高)

.parent {
  position: relative;
}

.child {
  position: absolute;
  top: 50%;
  left: 50%;
  margin-top: -50%;
  margin-left: -50%;
}

5. 使用 Grid 和 Auto Margins(不需要知道元素的宽高)

.parent {
  display: grid;
}

.child {
  justify-self: center;
  align-self: center;
}

6. 使用 Flexbox 和 Auto Margins(不需要知道元素的宽高)

.parent {
  display: flex;
}

.child {
  margin: auto; /* 水平垂直居中 */
}

7. 使用 table-cell(不推荐,语义不够清晰)

.parent {
  display: table-cell;
  text-align: center;
  vertical-align: middle;
}

以上是一些常用的方法,根据具体情况选择合适的方法来实现元素的垂直水平居中。

vue中的reactive如何实现

Vue.js 中的响应式数据是通过 Object.defineProperty 方法来实现的。Vue 会在初始化时递归地将所有的属性转换为 getter/setter,并且在适当的时候触发更新。

下面是一个简单的示例,演示了如何使用 Object.defineProperty 来实现响应式数据:

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get', key);
      return val;
    },
    set: function reactiveSetter(newVal) {
      console.log('set', key, newVal);
      if (newVal !== val) {
        val = newVal;
        // 触发更新
        // 例如,可以在这里通知 Vue 更新视图
      }
    }
  });
}

const obj = {};
defineReactive(obj, 'foo', 'bar');

console.log(obj.foo); // 输出: 'bar'
obj.foo = 'baz'; // 输出: 'set foo baz'
console.log(obj.foo); // 输出: 'baz'

在上面的示例中,我们定义了一个 defineReactive 函数,用于将对象的属性转换为响应式属性。该函数通过 Object.defineProperty 来定义属性的 getter 和 setter 方法,从而实现对属性的监听和更新。当属性被访问时,会调用 getter 方法,当属性被修改时,会调用 setter 方法,并在 setter 方法中进行新值的更新和触发更新的操作。

在 Vue.js 内部,它会递归地对对象的所有属性进行这样的操作,以实现整个组件树的响应式数据更新。这样,在修改数据时,Vue 就能够监听到数据的变化,并在适当的时候更新视图,实现了数据驱动视图的机制。

vue-router的生命周期

vue中自定义组件

vue组件通信

Vue-Router 的生命周期

Vue Router 是 Vue.js 的官方路由管理器,它提供了在单页面应用中管理路由的能力。Vue Router 中的生命周期钩子与 Vue 组件的生命周期钩子有些类似,但又有一些不同之处。

  1. beforeEach(to, from, next):在导航触发之前调用,可以用于全局的路由守卫,用来拦截导航,进行一些权限验证或其他操作。

  2. beforeResolve(to, from, next):在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用。

  3. afterEach(to, from):导航成功完成后被调用,没有 next 函数,不能改变导航。

  4. beforeEnter(to, from, next):在路由独享的守卫中(即单个路由配置中)生效,在路由进入该组件前触发。

  5. beforeRouteEnter(to, from, next):在路由进入该组件前触发,此时该组件尚未创建,因此无法访问组件实例 this,需要通过 next 回调函数访问组件实例。

  6. beforeRouteUpdate(to, from, next):在当前路由改变,但是该组件被复用时触发,比如从 A 组件跳转到 B 组件,再从 B 组件跳转回到 A 组件。

  7. beforeRouteLeave(to, from, next):导航离开该组件的对应路由时触发,可以用来确认是否离开当前页面,常用于防止用户在还未保存修改的情况下离开页面。

  8. scrollBehavior(to, from, savedPosition):此函数会在每次导航后被调用,可以用来控制页面滚动行为。

Vue 中自定义组件

在 Vue 中,组件是可复用的 Vue 实例,可以扩展 HTML 元素,封装可重用的代码。自定义组件可以通过全局或局部注册,然后在模板中使用。

  1. 全局注册自定义组件:
Vue.component('my-component', {
  // 组件选项
})
  1. 局部注册自定义组件:
var MyComponent = {
  // 组件选项
}

new Vue({
  components: {
    'my-component': MyComponent
  }
})

自定义组件的选项包括数据、模板、方法、生命周期钩子等,可以根据实际需求定义组件的行为和外观。在模板中使用自定义组件时,可以像使用普通 HTML 元素一样,以标签的形式引入。

Vue 组件通信

在 Vue 中,组件之间的通信主要有以下几种方式:

  1. **Props / emit父组件通过Props向子组件传递数据,子组件通过emit:** 父组件通过 Props 向子组件传递数据,子组件通过 emit 触发事件,向父组件发送消息。

  2. parent/parent / children: 可以通过 $parent$children 直接访问父组件和子组件的实例,但这种方式不够灵活,不推荐使用。

  3. 事件总线: 可以创建一个空的 Vue 实例作为事件总线,用于组件之间的通信,任何组件都可以通过事件总线来发送和接收事件。

  4. Vuex: 对于大型应用或者需要跨组件通信的场景,推荐使用 Vuex 进行状态管理。Vuex 是一个专门为 Vue.js 应用开发的状态管理库,提供了集中式存储管理应用的所有组件的状态。

  5. Provide / Inject: 祖先组件通过 provide 来提供数据,后代组件通过 inject 来注入数据,这种方式适用于祖先组件向后代组件传递数据的场景。

以上是 Vue 组件通信的常见方式,根据具体场景和需求选择合适的通信方式。

v-model如何实现

v-model 是 Vue.js 中常用的指令,用于在表单元素(如输入框、复选框、单选框等)和 Vue 实例的数据之间创建双向绑定关系。

在 Vue 中,要使用 v-model 指令,需要满足以下两个条件:

  1. 表单元素的值需要绑定到 Vue 实例中的一个数据属性。
  2. 当用户输入时,表单元素的值会自动更新到 Vue 实例的数据属性中;同时,当数据属性的值发生变化时,表单元素的值也会相应地更新。

下面是一个简单的示例,演示了如何使用 v-model

<div id="app">
  <input type="text" v-model="message">
  <p>{{ message }}</p>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    message: ''
  }
})
</script>

在上面的示例中,我们创建了一个 Vue 实例,并将其挂载到了 id 为 app 的 DOM 元素上。在模板中,我们使用 v-model 指令将输入框的值绑定到了 Vue 实例中的 message 数据属性上。这样,当用户在输入框中输入内容时,message 的值会自动更新为输入框中的内容,并且在下方的 <p> 元素中实时显示出来。

总的来说,v-model 实现了表单元素和 Vue 实例数据之间的双向绑定,简化了开发者处理表单输入的逻辑。

ts中interface和type的应用场景,区别是什么

在 TypeScript 中,interfacetype 都用于定义自定义类型,它们有着一些相似之处,但也有一些区别。

应用场景:

  1. interface 的应用场景:

    • 定义对象的形状(Shape):interface 主要用于描述对象的结构,包括属性名、属性类型以及方法。
    • 对象的扩展:interface 可以被继承和扩展,用于组织和管理代码中的对象结构。
  2. type 的应用场景:

    • 定义联合类型、交叉类型、元组和其他更复杂的类型。
    • 创建类型别名:type 关键字可以用来创建自定义类型别名,方便重复使用复杂的类型。
    • 限制允许的值:type 可以用于创建字符串字面量类型、联合类型和交叉类型等,用于限制变量的可能取值。

区别:

  1. 语法不同:

    • interface 使用 interface 关键字进行定义,通常用于描述对象的形状。
    • type 使用 type 关键字进行定义,通常用于创建自定义类型别名。
  2. 可被扩展:

    • interface 可以被继承和扩展,允许在已有接口基础上添加新成员。
    • type 不支持继承,但是可以通过联合类型、交叉类型等方式实现相似的功能。
  3. 兼容性不同:

    • 对于扩展方面,interface 之间可以相互合并,而 type 不能合并。
    • 对于描述对象的形状,interface 在检查属性是否兼容时更加严格,而 type 更加灵活。
  4. 适用场景不同:

    • 如果需要描述对象的形状,特别是在定义类或者面向对象编程时,更倾向于使用 interface
    • 如果需要创建复杂的类型别名,或者对现有类型进行扩展和限制,更倾向于使用 type

总的来说,interfacetype 在功能上有一些重叠,但也存在一些不同之处,根据具体情况选择合适的工具更有利于代码的编写和维护。

ts中一个接口有a,b,c三个属性,定义一个新类,要求只有a,b两个属性
在 TypeScript 中,你可以使用接口继承来实现这样的需求。首先定义一个拥有 abc 三个属性的接口,然后定义一个新类,只包含 ab 两个属性,同时继承原始接口。

下面是示例代码:

// 定义原始接口
interface OriginalInterface {
  a: number;
  b: string;
  c: boolean;
}

// 定义新类,只包含 a 和 b 属性,并继承原始接口
class NewClass implements Pick<OriginalInterface, 'a' | 'b'> {
  constructor(public a: number, public b: string) {}
}

// 创建新类实例
const newObj = new NewClass(1, 'hello');

// 输出新类实例的属性
console.log(newObj.a); // 1
console.log(newObj.b); // "hello"

在上面的示例中,我们使用了 TypeScript 提供的 Pick 泛型工具类型,将原始接口中的 ab 属性挑选出来,然后新类实现了这个挑选出来的子集。这样就实现了新类只包含 ab 两个属性的需求。

两个非技术问题:怎么学习,遇到问题如何排查

最后没有反问,感觉没戏

作者:KeyError44
链接:www.nowcoder.com/feed/main/d…
来源:牛客网