前端面试总结(二)

201 阅读24分钟

本人近期遇到的面试题汇总:

image.png

1、说一下css盒模型

css盒模型分为标准盒模型和怪异盒模型,标准盒模型我们设置宽高就是内容content的宽高,怪异(IE)盒模型我们设置宽高是content+padding+border。 image.png

image.png

2、两个盒子,上面盒子margin-buttom是30px,下面盒子margin-top是20px,这两个盒子的间距是多少?

30px,因为l两个盒子在上下方向存在margin塌陷。解决方案就是让其形成块级格式化上下文(BFC),可以通过设置display:flex;overflow:hidden等等

3、说一下flex布局的属性和常用用法

flex布局就是弹性布局,具有以下几种属性。
作用于容器中 :

  • flex-direction:设置主轴的方向
  • flex-wrap:如果一条轴放不下,是否换行
  • justify-content:主轴内容的对齐方式
  • align-items:交叉轴的对齐方式
  • align-content:多条轴线的对齐方式

作用于某个项目中

  • flex-grow:放大比例,默认是0,即存在剩余空间也不会放大
  • flex-shrink:缩小比例,默认是1,即空间不足时,将缩小
  • flex-basis:是否自动分配剩余的空间,默认是auto
  • align-self:允许单个项目与其他项目不一样的对齐方式
  • flex属性默认是0 1 auto

4、如果有四个盒子,方向是横向布局,两端对齐,设置换行,最后一个实现左对齐怎么实现?

1、设置最后一个盒子margin:auto 2、利用伪元素,将flex设为1或者auto
详细实现可以参考这篇文章如何实现CSS中flex布局最后一行左对齐?

5、flex自适应实现的?

设置flex:1

6、flex:1的属性是什么?

felx是flex-grow、flex-shrink、flex-basis的缩写,默认值是0 1 auto ;flex:1就相当于1 1 0%

7、css样式的优先级?从高到低

!important>行内样式>id选择器>类选择器、伪类选择器、属性选择器>标签选择器、伪元素选择器

8、css里面的透明,rgba和opacity的区别?

rgbaopacity 都是用于设置 HTML 和 CSS 中元素的透明度的属性,但它们有不同的用法和效果。

rgba 是一种颜色表示方式,其中 rgb 分别代表红、绿、蓝三原色的值,a 则代表透明度(alpha 值),其取值范围为 0 到 1,表示颜色的不透明程度。例如,rgba(255, 0, 0, 0.5) 表示颜色为红色,不透明度为 50%。

opacity 则是设置元素整体透明度的属性,取值范围也为 0 到 1,表示元素的不透明程度。例如,opacity: 0.5; 表示元素的不透明度为 50%。

rgbaopacity 的主要区别在于它们设置透明度的方式不同。rgba 是设置元素的背景色或前景色的透明度,而 opacity 是设置整个元素的透明度,包括元素的内容和边框。此外,rgba 还可以单独设置某个元素的背景色或前景色的透明度,而 opacity 只能设置整个元素的透明度。其次opacity是有继承性的,而rgba没有继承性。

9、说一说http常见的状态码

1xx (信息性状态码) :指示请求已经接受,但需要进一步处理才能完成。

  • 100 Continue:客户端应继续发送请求。
  • 101 Switching Protocols:服务器已经理解请求,并将切换到新的协议。

2xx (成功状态码):表示请求已成功被服务器接收、理解、并接受。

  • 200 响应成功
  • 201 Created:请求已经被实现,新的资源已经被创建
  • 204 No Content:请求成功,但是没有返回任何内容

3xx

  • 301 Moved Permanently:请求的资源已被永久移动到新URI。
  • 302 Found:请求的资源已被临时移动到新URI
  • 304 Not Modified:客户端有缓冲的文档并发出了一个条件性的请求,服务器告诉客户,原来缓冲的文档还可以继续使用

4xx(客户端错误状态码):表示客户端可能出现了错误。

  • 400 Bad Request:请求参数有误,服务器无法处理。
  • 401 Unauthorized:请求未经授权,需要身份验证。
  • 403 Forbidden:服务器拒绝请求,权限不足。
  • 404 请求地址不存在

5xx(服务器错误状态码):表示服务器在处理请求的过程中出现了错误。

  • 500 Internal Server Error:服务器遇到了一个未曾预料的错误。
  • 503 Service Unavailable:服务器当前无法处理请求,一般用于服务器维护或者过载时。

10、项目中vue封装axios请求的时候怎么利用上http状态码的?

在Vue项目中封装axios请求时,可以通过拦截响应的http状态码来判断请求的结果。一般来说,http状态码用于指示服务器对请求的处理结果,例如200表示成功,400表示请求错误,404表示请求的资源不存在等。利用http状态码可以让我们更精确地处理请求结果,例如在发生错误时显示相应的错误信息或者重新尝试请求等。

下面是一个示例代码,演示了如何在Vue项目中利用http状态码处理请求结果:

import axios from 'axios'

const http = axios.create({
  baseURL: 'http://api.example.com/',
  timeout: 5000
})

// 请求拦截器
http.interceptors.request.use(
  config => {
    // 在请求头中添加 token 等信息
    config.headers.Authorization = 'Bearer ' + localStorage.getItem('token')
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
http.interceptors.response.use(
  response => {
    if (response.status === 200) {
      // 请求成功,返回数据
      return response.data
    } else {
      // 请求失败,返回错误信息
      return Promise.reject(response.statusText)
    }
  },
  error => {
    if (error.response) {
      // 请求失败,返回错误信息
      return Promise.reject(error.response.statusText)
    } else if (error.message === 'timeout') { // 请求超时,返回错误信息 
              return Promise.reject('请求超时,请稍后再试') 
    }else {
      // 网络错误,返回错误信息
      return Promise.reject(error.message)
    }
  }
)

export default http

在上述代码中,我们定义了一个名为http的axios实例,并设置了其基本的配置信息。接着,我们定义了请求拦截器和响应拦截器。在请求拦截器中,我们可以在请求头中添加一些信息,例如认证token等。在响应拦截器中,我们根据http状态码来判断请求结果,如果状态码为200,则表示请求成功,返回响应数据;否则,返回相应的错误信息。需要注意的是,在响应拦截器中还要处理网络错误,例如请求超时等情况。

在Vue组件中使用http实例发送请求时,可以直接调用该实例的方法,例如:

import http from '@/api/http'

export default {
  methods: {
    getUserInfo(id) {
      http.get('/users/' + id).then(response => {
        console.log(response)
      }).catch(error => {
        console.log(error)
      })
    }
  }
}

在上述代码中,我们通过http.get方法发送一个GET请求,获取用户信息。在响应中,如果http状态码为200,则输出响应数据;否则,输出错误信息。

11、响应体除了状态码还有什么返回?

在HTTP请求的响应中,除了状态码(status code)之外,还有以下常见的返回值:

  • 响应头(headers):HTTP响应头包含了一些元数据信息,比如响应的Content-Type、Content-Length等。可以通过访问响应对象(response object)的headers属性获取响应头信息。
  • 响应体(response body):HTTP响应体包含了响应内容的主体部分,可以是文本、HTML、XML、JSON等格式的数据。可以通过访问响应对象的data属性获取响应体信息。
  • 状态消息(status message):状态消息是与状态码对应的文本信息,通常为HTTP协议的规范定义的标准文本信息。可以通过访问响应对象的statusText属性获取状态消息。 -错误信息(error message):如果请求发生了错误,比如网络错误、请求超时等,HTTP响应对象也可能包含一些错误信息,可以通过捕获错误对象来获取。通常在响应拦截器中进行错误处理。

综上所述,HTTP响应对象除了状态码之外还包含了响应头、响应体、状态消息和错误信息等其他信息,这些信息都可以在响应拦截器中进行处理和利用。

12、请求标头怎么设置的?

在HTTP请求中,请求头(request header)用于告知服务器有关请求的额外信息,比如请求的数据格式、授权信息等。axios库提供了多种设置请求头的方式,以下是其中几种常见的方式:

  • 使用headers配置项:可以在axios的配置对象中通过headers属性设置请求头信息,例如:
axios.get('https://example.com/api/userinfo', {
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer xxxxxxx'
  }
})

在上述代码中,我们在请求中设置了两个请求头信息,分别是Content-TypeAuthorizationContent-Type指定了请求的数据格式为JSON格式,Authorization指定了请求的授权信息为Bearer Token,具体的值需要根据API接口的要求进行设置。

  • 使用全局默认配置:如果需要在所有请求中设置相同的请求头信息,可以使用axios的全局默认配置,在创建axios实例时进行设置,例如:
const instance = axios.create({
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer xxxxxxx'
  }
});

instance.get('https://example.com/api/userinfo');

在上述代码中,我们通过axios.create方法创建了一个axios实例,并在该实例中设置了默认的请求头信息。在发送请求时,只需要使用该实例的方法即可,axios会自动带上默认的请求头信息。

  • 使用拦截器:如果需要动态设置请求头信息,比如在每个请求中加入一些固定的信息,可以使用axios的请求拦截器,在请求发送之前对请求进行处理,例如:
axios.interceptors.request.use(config => {
  // 在请求头中添加固定的信息
  config.headers['X-Custom-Header'] = 'xxxxxx';
  return config;
});

axios.get('https://example.com/api/userinfo');

在上述代码中,我们使用axios的请求拦截器,在请求发送之前对请求进行处理,将请求头中添加了一个名为X-Custom-Header的信息。在实际请求中,axios会自动将这个请求头信息添加到请求中。

13、说一下this

在JavaScript中,this是一个关键字,用于指代当前函数所在的上下文对象。它的具体指向取决于函数的调用方式,包括以下几种情况:

  • 全局环境下的this:在全局环境中,this指向全局对象(浏览器中为window对象,Node.js中为global对象)。
console.log(this === window); // 在浏览器中输出true,在Node.js中输出false
  • 函数中的this:在函数中,this的指向取决于函数的调用方式。如果函数是作为对象的方法调用的,则this指向该对象,否则this指向全局对象。例如:
const obj = {
  name: 'Tom',
  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

obj.greet(); // 输出:Hello, my name is Tom

const func = obj.greet;
func(); // 输出:Hello, my name is undefined(在浏览器中可能是Hello, my name is window,取决于实现)

在上述代码中,我们定义了一个对象obj,其中包含一个方法greet,在该方法中使用this引用了当前对象的name属性。当我们通过obj.greet()调用方法时,this指向obj对象,因此输出的是Hello, my name is Tom。但是当我们将方法赋值给变量func并单独调用时,this指向全局对象,因此输出的是Hello, my name is undefined(或者Hello, my name is window)。

  • 构造函数中的this:当我们使用new关键字创建一个对象时,构造函数中的this指向即将创建的新对象。例如:
function Person(name, age) {
  this.name = name;
  this.age = age;
}

const p1 = new Person('Tom', 20);
console.log(p1.name); // 输出:Tom

在上述代码中,我们定义了一个构造函数Person,在该函数中使用this引用了即将创建的新对象的nameage属性。当我们使用new关键字创建一个新的Person对象时,构造函数中的this指向即将创建的新对象,因此在函数内部的赋值操作会作用于新对象。

除了上述情况外,this还可以在JavaScript中的其他特殊场合下使用,例如使用applycall方法调用函数时指定函数中的this指向。需要注意的是,在JavaScript中使用this要格外小心,因为它的指向非常容易被误解和混淆,导致代码中的错误和问题。

14、防抖和节流

防抖和节流都是前端开发中常用的优化性能的技术,它们的作用是减少一些频繁触发的事件的处理次数,从而提高页面的性能和响应速度。

  • 防抖 (Debouncing)

在实际应用中,我们常常需要对一些事件(如resize、scroll、input等)进行响应。但是由于事件可能会在短时间内被频繁触发,导致处理函数也被频繁执行,从而导致页面性能下降。

防抖的基本思路是:在事件被触发n毫秒后再执行回调函数,如果在这n毫秒内事件再次被触发,则重新计时。这样可以有效地减少事件处理的次数,提高性能。

例如,我们可以实现一个简单的防抖函数:

function debounce(fn, delay) {
  let timer = null;
  return function() {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, delay);
  };
}

在上述代码中,我们定义了一个debounce函数,该函数接受一个回调函数fn和一个延迟时间delay作为参数,并返回一个新的函数。该新函数使用了setTimeout方法来实现延迟执行回调函数的效果,并在每次调用时清除之前的定时器。

使用该防抖函数,我们可以对某些频繁触发的事件进行优化,例如:

window.addEventListener('resize', debounce(() => {
  console.log('Window resized!');
}, 250));

在上述代码中,我们使用了addEventListener方法监听resize事件,并通过debounce函数对回调函数进行了防抖处理,从而减少了事件处理的次数。

  • 节流 (Throttling)

节流的基本思路是:在一段时间内只触发一次事件。例如,我们可以实现一个简单的节流函数:

function throttle(fn, delay) {
  let timer = null;
  let lastTime = 0;
  return function() {
    const now = new Date().getTime();
    if (now - lastTime >= delay) {
      fn.apply(this, arguments);
      lastTime = now;
    }
  };
}

在上述代码中,我们定义了一个throttle函数,该函数接受一个回调函数fn和一个时间间隔delay作为参数,并返回一个新的函数。该新函数使用了当前时间与上一次执行时间的差值来判断是否需要执行回调函数,并在每次调用时更新上一次执行时间。

使用该节流函数,我们可以对某些频繁触发的事件进行优化,例如:

window.addEventListener('scroll', throttle(() => {
  console.log('Window scrolled!');
}, 250));

在上述代码中,我们使用了addEventListener方法监听scroll事件,并通过throttle函数对回调函数进行了节流处理

15、防抖函数怎么写?

参考上一个问题

16、es6中数组常用方法?

ES6 中为数组新增了一些非常实用的方法,下面是一些常用的数组方法:

  • Array.from():将类似数组的对象(比如 DOM 对象、Set 和 Map 等)转换为真正的数组。

    const str = 'hello';
    const arr = Array.from(str);
    console.log(arr); // ['h', 'e', 'l', 'l', 'o']
    
  • Array.of():将一组值转换为数组。

    const arr = Array.of(1, 2, 3);
    console.log(arr); // [1, 2, 3]
    
  • Array.prototype.includes():判断数组中是否包含指定元素。

    const arr = [1, 2, 3];
    console.log(arr.includes(2)); // true
    console.log(arr.includes(4)); // false
    
  • Array.prototype.find():查找数组中符合条件的第一个元素,并返回该元素。

    const arr = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
    const item = arr.find(item => item.id === 2);
    console.log(item); // { id: 2, name: 'Bob' }
    
  • Array.prototype.findIndex():查找数组中符合条件的第一个元素的索引,并返回该索引。

    const arr = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
    const index = arr.findIndex(item => item.id === 2);
    console.log(index); // 1
    
  • Array.prototype.fill():用指定的值填充数组。

    const arr = new Array(3);
    arr.fill(0);
    console.log(arr); // [0, 0, 0]
    
  • Array.prototype.keys():返回数组中每个元素的索引。

    const arr = ['a', 'b', 'c'];
    for (const index of arr.keys()) {
      console.log(index); // 0, 1, 2
    }
    
  • Array.prototype.values():返回数组中每个元素的值。

    const arr = ['a', 'b', 'c'];
    for (const value of arr.values()) {
      console.log(value); // 'a', 'b', 'c'
    }
    
  • Array.prototype.entries():返回数组中每个元素的键值对(由索引和值组成的数组)。

    const arr = ['a', 'b', 'c'];
    for (const [index, value] of arr.entries()) {
      console.log(index, value); // 0 'a', 1 'b', 2 'c'
    }
    
  • Array.prototype.flat():将嵌套的数组扁平化为一维数组。

    const arr = [1, [2, 3], [4, [5, 6]]];
    const flatArr = arr.flat();
    console.log(flatArr); // [1, 2, 3, 4, 5, 6]
    

17、说一下map、fifter、forEach的区别

forEach()map()filter() 都是 JavaScript 中数组的方法,它们用于遍历数组中的每个元素,并对其执行给定的函数,但它们之间的区别在于返回值和对原数组的修改方式。

  • forEach() 方法:不返回任何值,仅遍历数组中的每个元素,并对每个元素执行给定的函数。它不会创建一个新数组,也不会修改原数组的值。如果需要修改原数组的值,可以在函数体内修改每个元素的值。

以下是一个示例:

const array = [1, 2, 3];
array.forEach((item) => console.log(item)); // 1 2 3
  • map() 方法:返回一个新数组,该数组由对原数组中的每个元素应用给定函数的结果组成。它不会修改原数组的值,而是返回一个新的、基于原数组的结果数组。

以下是一个示例:

const array = [1, 2, 3];
const newArray = array.map((item) => item * 2);
console.log(newArray); // [2, 4, 6]
  • filter() 方法:返回一个新数组,该数组由原数组中符合给定条件的元素组成。它不会修改原数组的值,而是返回一个新的、基于原数组的结果数组。

以下是一个示例:

const array = [1, 2, 3, 4, 5];
const newArray = array.filter((item) => item % 2 === 0);
console.log(newArray); // [2, 4]

因此,如果需要对每个元素执行某些操作,并将操作的结果存储在新数组中,可以使用 map() 方法。如果需要从原数组中筛选出符合某些条件的元素,可以使用 filter() 方法。如果只需要遍历数组中的每个元素,可以使用 forEach() 方法。需要注意的是,在 forEach()map() 中都不能使用 breakreturn 语句跳出循环,如果需要跳出循环,可以使用 some()every() 方法代替。

18、说一下prmoise

Promise 是 JavaScript 中处理异步操作的一种机制,它使异步操作更加容易管理和控制,避免了回调地狱的问题。Promise 代表了一个异步操作的最终结果,可以是一个值、一个错误或者一个还未确定的结果。

一个 Promise 可以处于以下三种状态之一:

  • pending(进行中):初始状态,既不是成功,也不是失败状态。
  • fulfilled(已成功):意味着操作成功完成。
  • rejected(已失败):意味着操作失败。

Promise 对象的语法如下:

const promise = new Promise((resolve, reject) => {
  // 异步操作代码
  if (异步操作成功) {
    resolve(成功结果);
  } else {
    reject(失败原因);
  }
});

Promise 中,异步操作代码应该在回调函数中执行,即在 Promise 构造函数的参数中传入一个回调函数,该回调函数接收两个参数:resolvereject。当异步操作成功时,应该调用 resolve 方法并传递成功的结果,如果异步操作失败,应该调用 reject 方法并传递失败的原因。

Promise 对象可以使用 then() 方法获取异步操作的结果,该方法接收两个参数:onFulfilledonRejected,它们分别在异步操作成功和失败时执行。当异步操作成功时,onFulfilled 函数将会被调用,其接收异步操作的结果作为参数;当异步操作失败时,onRejected 函数将会被调用,其接收异步操作的错误作为参数。

以下是一个示例:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("成功结果");
  }, 1000);
});

promise.then(
  (result) => console.log(result), // 输出 "成功结果"
  (error) => console.log(error)
);

在上面的示例中,Promise 对象表示一个异步操作,该操作在 1 秒后成功并返回一个字符串。then() 方法接收两个函数,第一个函数在异步操作成功时被调用,并接收异步操作的结果作为参数,第二个函数在异步操作失败时被调用,并接收异步操作的错误作为参数。在此示例中,then() 方法将输出异步操作的成功结果。

19、如何设置promise超时?

下面是一种基本的实现方式:

function promiseTimeout(ms, promise) {
  // 创建一个超时 Promise 对象
  const timeout = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error(`Promise timed out after ${ms} ms`));
    }, ms);
  });

  // Promise.race() 方法返回一个 Promise 对象,该对象的状态由第一个解决或拒绝的 Promise 对象的状态决定
  return Promise.race([promise, timeout]);
}

这个 promiseTimeout() 函数接受两个参数,分别是超时时间(以毫秒为单位)和要执行的 Promise 对象。它首先创建一个超时 Promise 对象,并使用 setTimeout() 方法在指定时间后将其拒绝。然后使用 Promise.race() 方法将超时 Promise 对象与传入的 Promise 对象合并成一个新的 Promise 对象,该新的 Promise 对象将与传入的 Promise 对象中最先解决或拒绝的 Promise 对象相同。因此,如果传入的 Promise 对象在超时前解决或拒绝,返回的 Promise 对象将与该 Promise 对象相同;否则,返回的 Promise 对象将与超时 Promise 对象相同。

以下是一个使用示例:

const promise = new Promise((resolve) => {
  setTimeout(() => {
    resolve("Promise resolved");
  }, 2000);
});

promiseTimeout(1000, promise)
  .then((result) => console.log(result))
  .catch((error) => console.error(error)); // 输出 "Promise timed out after 1000 ms"

在上面的示例中,我们创建了一个 Promise 对象,它将在 2 秒后解决。然后我们将该 Promise 对象和一个超时时间(1 秒)传递给 promiseTimeout() 函数。由于超时时间比解决时间短,因此返回的 Promise 对象将拒绝并返回一个超时错误。

20、promise是属于微任务还是宏任务?

在浏览器中,Promise 是一个微任务(microtask)。

在 JavaScript 中,任务被分为两种类型:宏任务(macrotask)和微任务(microtask)。宏任务包括在主线程上执行的任务,如 setTimeout() 和事件处理程序;而微任务包括在主线程上执行的较小的任务,如 Promiseprocess.nextTick()

当一个 Promise 对象被创建并添加到事件队列中时,它会在当前宏任务完成后立即执行。这使得 Promise 成为非常有用的异步编程工具,可以帮助我们在不阻塞主线程的情况下执行异步操作。此外,由于微任务在宏任务之间执行,因此它们通常比宏任务更快。

下面是一个示例,展示了 Promise 是如何作为微任务执行的:

console.log("start");

// 创建一个 Promise 对象并添加到当前微任务队列中
Promise.resolve()
  .then(() => console.log("Promise"));

console.log("end");

// 输出结果为:
// "start"
// "end"
// "Promise"

在上面的示例中,我们创建了一个 Promise 对象并将其添加到当前微任务队列中。尽管 Promise 对象是在代码的后面创建的,但它仍然在 "end" 之后立即执行,因为它是作为微任务被调度的。

21、setTimeout打印1、控制台打印2、promise回调打印3,控制台的打印顺序是什么?

打印结果是2、3、1

  • 宏任务和微任务的执行顺序 image.png

22、vue里面怎么进行样式穿透?

在 Vue 中,有时候我们需要修改组件的子元素的样式,但是又不想污染全局的样式,这时候就可以使用样式穿透(也叫深度作用选择器)。

样式穿透可以通过在样式选择器前面加上 /deep/>>> 或者 ::v-deep 来实现。这样就可以让样式选择器穿透组件的边界,作用到组件内部的子元素上。

下面是一个示例代码,演示了如何使用样式穿透:

<template>
  <div class="wrapper">
    <h1 class="title">Hello World</h1>
    <p class="content">Lorem ipsum dolor sit amet.</p>
  </div>
</template>

<style scoped>
.wrapper /deep/ .title {
  font-size: 24px;
}

.wrapper /deep/ .content {
  color: red;
}
</style>

在这个示例代码中,我们定义了一个名为 wrapper 的组件,其中包含了一个标题元素和一个内容元素。在组件的样式中,我们使用 /deep/ 操作符来让选择器穿透组件的边界,作用到子元素上。这样,标题元素的字体大小就会变成 24px,内容元素的文字颜色就会变成红色。

需要注意的是,样式穿透只能在有作用域的样式中使用,也就是 style 标签中带有 scoped 属性的样式中。如果没有作用域,样式穿透就会失效。

23、v-if和v-show的区别?

在 Vue 中,v-ifv-show 都是用来控制元素的显示和隐藏的指令,但它们的实现方式不同,因此使用的场景也不同。

  • v-if:是“条件渲染”指令,根据表达式的值来决定是否渲染元素。当表达式的值为 false 时,元素会被移除 DOM 树,当表达式的值为 true 时,元素会被添加到 DOM 树中。因为 v-if 是根据条件动态创建或销毁元素,所以在切换时,它的过渡效果比 v-show 更好。但是,因为它要重新创建元素,所以对于频繁切换的场景,会有性能问题。
  • v-show:是“条件显示”指令,根据表达式的值来决定元素是否显示。当表达式的值为 false 时,元素会被隐藏,但并没有被移除 DOM 树,当表达式的值为 true 时,元素会显示出来。因为 v-show 只是简单地切换元素的 CSS 属性 display,所以在切换时,它的过渡效果不如 v-if。但是,因为它不会重新创建元素,所以对于频繁切换的场景,会更加高效。

因此,如果需要在显示和隐藏之间频繁切换的元素,应该使用 v-show,如果需要根据条件动态创建或销毁元素的场景,应该使用 v-if

24、如何获取dom元素?

在 Vue 中,可以使用以下几种方式获取 DOM 元素:

  • 使用 $refs:在模板中,可以使用 ref 属性给元素指定一个唯一的引用标识,在组件实例中,可以通过 $refs 访问到这个元素的 DOM 对象。例如:

    <template>
      <div ref="myDiv">Hello, World!</div>
    </template>
    
    javascriptCopy code
    export default {
      mounted() {
        const div = this.$refs.myDiv
        // 在这里可以访问和操作 div 元素
      }
    }
    
  • 使用 document.getElementById() 等原生 DOM API:在 Vue 中,可以直接使用原生的 DOM API 获取元素,例如:

    javascriptCopy code
    export default {
      mounted() {
        const div = document.getElementById('myDiv')
        // 在这里可以访问和操作 div 元素
      }
    }
    
  • 使用 $el:在组件实例中,可以通过 $el 属性访问到组件实例对应的根 DOM 元素。例如:

    export default {
      mounted() {
        const div = this.$el
        // 在这里可以访问和操作组件的根元素
      }
    }
    

其中,使用 $refs 可以更好地组织和管理组件内部的 DOM 元素,但是需要注意的是,$refs 只在组件渲染完成后才能访问到,因此不能在组件的生命周期钩子 createdbeforeMount 中使用 $refs 访问元素。而使用原生 DOM API 或 $el 则不受此限制,可以在任何时候访问元素。

25、组件下的方法可以拿到吗?

获取到 DOM 元素后,组件下的方法可以通过 this 访问到。

在 Vue 组件中,模板中绑定的事件处理函数、计算属性、生命周期钩子等方法都可以通过 this 访问到。因此,如果你在组件的方法中需要访问 DOM 元素,可以通过 this.$refs 访问到元素,然后在方法中操作元素。例如:

<template>
  <div ref="myDiv" @click="handleClick">Hello, World!</div>
</template>
javascriptCopy code
export default {
  methods: {
    handleClick() {
      const div = this.$refs.myDiv
      // 在这里可以访问和操作 div 元素
    }
  }
}

需要注意的是,在 Vue 中,推荐使用数据驱动的方式来操作 DOM,而不是直接操作 DOM 元素。直接操作 DOM 元素会破坏 Vue 的响应式机制,可能会导致一些不可预测的问题。因此,只有在必要的情况下才应该直接操作 DOM 元素。

26、组件内通信的方式?

在 Vue 中,组件内通信的方式有以下几种:

  • Props 和 Events:父组件通过 props 把数据传递给子组件,子组件通过 events 把数据传递给父组件。这种方式适用于父子组件之间的通信,特别是父组件向子组件传递数据的情况。
  • $emit 和 $on:父组件通过 $emit 触发一个自定义事件,并传递数据给子组件,子组件通过 $on 监听该自定义事件,并接收传递过来的数据。这种方式适用于父子组件之间的双向通信,以及任意两个组件之间的通信。
  • $refs:父组件通过 ref 属性给子组件命名,然后通过 this.$refs 访问子组件,以及子组件的属性和方法。这种方式适用于父组件需要直接访问子组件的属性和方法的情况。
  • Vuex:Vuex 是 Vue.js 官方提供的状态管理库,可以用于管理应用程序的状态。通过 Vuex,可以在组件之间共享状态,并实现组件之间的通信。
  • Event Bus:Event Bus 是一种基于事件的通信机制,可以用于在任意两个组件之间传递数据。通过事件总线,组件可以发布事件并传递数据,其他组件可以订阅事件并接收数据。
  • $attrs和$listeners:假设我们有一个自定义组件 MyComponent,它的父组件传递了一个属性 title 和一个事件监听器 click,而这些属性和事件监听器在 MyComponent 中并没有声明为 props 和事件。那么在 MyComponent 中,我们可以通过$attrs 和 $listeners来访问它们,例如:
<template>
  <div>
    <h1>{{ $attrs.title }}</h1>
    <button @click="$listeners.click">Click Me</button>
  </div>
</template>

在上面的代码中,我们使用了 $attrs.title 来访问父组件传递的属性 title,并使用 $listeners.click 来访问父组件传递的事件监听器 click。通过这种方式,MyComponent 就可以接收来自父组件的属性和事件,而无需在组件中声明具体的 props 和事件。

当然,如果父组件传递的属性和事件已经在组件中声明为 props 和事件了,那么可以直接在组件中使用它们,而不需要通过$attrs 和 $listeners来访问。例如:

<template>
  <div>
    <h1>{{ title }}</h1>
    <button @click="$emit('click')">Click Me</button>
  </div>
</template>

<script>
export default {
  props: {
    title: String
  },
  methods: {
    handleClick() {
      this.$emit('click')
    }
  }
}
</script>

在上面的代码中,我们通过 props 来声明了属性 title,并通过 $emit 来声明了事件 click。这样,在组件中就可以直接使用 this.title 和 this.handleClick来访问和处理它们,而无需通过 $attrs 和 $listeners

  • 依赖注入(provide/inject):在 Vue 中,依赖注入的实现是通过 provide 和 inject 两个选项来实现的。其中,provide 选项用于在父组件中注册需要共享的数据,而 inject 选项用于在子组件中接收这些数据。例如:
// 父组件
export default {
  provide: {
    title: 'Welcome to My App'
  }
}

// 子组件
export default {
  inject: ['title'],
  mounted() {
    console.log(this.title) // 输出:Welcome to My App
  }
}

在上面的代码中,我们在父组件中使用 provide 选项注册了一个属性 title,它的值为字符串 'Welcome to My App'。而在子组件中,我们使用 inject 选项来接收这个属性,并在 mounted 钩子函数中输出它的值。

需要注意的是,inject 选项只能接收通过 provide 注册的属性,而不能接收父组件中声明的 prop 或 data 等属性。另外,由于 provide 和 inject 是基于上下文的,因此只有在当前组件的父组件中提供了相应的属性,才能通过 inject 选项获取到这些属性。

  • $parent和$children:在 Vue.js 中,每个组件实例都有一个指向其父组件实例的引用,可以通过 $parent 访问。同时,每个组件实例也可以拥有多个子组件,可以通过 $children 访问,得到的是一个数组。$parent$children 可以用于访问组件树中的其它组件实例,但它们是非响应式的,并且在组件树更新时不会自动更新。因此,如果需要在父子组件之间进行通信,更好的做法是使用 props 和 events。而对于组件树较为复杂的场景,可以使用 Vuex 或者其他状态管理库进行状态管理。 需要根据实际情况选择适合的通信方式。如果只是简单的父子组件之间的通信,使用 Props 和 Events 就可以了。如果涉及到跨层级、多个组件之间的通信,可以考虑使用 emitemit 和 on、Vuex 或 Event Bus 等高级通信方式。

27、有个父子组件,子组件一挂载后,有一个接口数据返回时间比较长,子组件如何拿到最新的数据?

当子组件挂载后,如果需要等待接口返回数据再渲染组件,可以使用 v-if 控制子组件的渲染。这样可以确保子组件拿到的是最新的数据。

具体实现可以在父组件中发起接口请求,将返回的数据通过 props 传递给子组件,然后在子组件中使用 v-if 控制渲染。在父组件中可以使用 v-if 或者 v-show 来判断接口数据是否已经返回,如果返回则将子组件的 show 属性设置为 true,否则设置为 false

例如:

<template>
  <div>
    <button @click="fetchData">获取数据</button>
    <Child v-if="showChild" :data="data" />
  </div>
</template>

<script>
import Child from './Child.vue'

export default {
  components: {
    Child
  },
  data() {
    return {
      data: null,
      showChild: false
    }
  },
  methods: {
    fetchData() {
      // 发起接口请求
      fetch('/api/data')
        .then(response => response.json())
        .then(data => {
          // 将返回的数据保存到父组件的 data 属性中
          this.data = data
          // 将子组件的 show 属性设置为 true,触发子组件的渲染
          this.showChild = true
        })
    }
  }
}
</script>

子组件中可以通过 props 接收父组件传递的数据,并使用这些数据进行渲染:

<template>
  <div>
    <h2>{{ data.title }}</h2>
    <p>{{ data.content }}</p>
  </div>
</template>

<script>
export default {
  props: {
    data: {
      type: Object,
      required: true
    }
  }
}
</script>

28、知道虚拟dom吗?

虚拟 DOM 本质上是一个 JavaScript 对象,它是对真实 DOM 的一种抽象描述,包括节点类型、属性、子节点等信息。当数据发生变化时,Vue.js 会先生成一颗新的虚拟 DOM 树,然后通过对新旧虚拟 DOM 树进行比较,计算出需要更新的部分,并且只更新这些部分的真实 DOM。

虚拟 DOM 的优势在于可以避免频繁操作真实 DOM 带来的性能损耗,同时可以将更新操作批量执行,减少更新的次数,提高应用性能。虽然虚拟 DOM 会带来一定的性能损耗,但是在大多数应用场景下,其性能优势还是明显的。

29、说一下watch

在 Vue.js 中,watch 是一个属性,用于观测 Vue 实例中的数据变化。当某个属性发生变化时,watch 可以触发一些自定义的操作,比如调用一个方法、发起一个异步请求等等。

watch 有两种定义方式:

  • 监听一个属性:

    watch: {
      propertyName(newVal, oldVal) {
        // do something
      }
    }
    

    这种方式会在 propertyName 发生变化时触发回调函数。

  • 监听一个表达式:

    `` watch: { 'expression': { handler(newVal, oldVal) { // do something }, deep: true, immediate: true } }

    
    这种方式会在 `expression` 的值发生变化时触发回调函数,`deep` 属性用于监听对象内部的属性变化,`immediate` 属性可以在组件挂载时立即执行一次回调函数。
    

watch 可以用于监听单个属性的变化,也可以用于监听多个属性的变化,还可以结合 computed 使用。但是需要注意的是,过度使用 watch 会带来性能问题,因为每个 watch 都需要消耗一定的性能,因此需要合理使用。

30、怎么开启深度监视?

通过将 watch 对象中的 deep 属性设置为 true 来开启深度监视。深度监视可以用于监听对象和数组的变化,包括对象和数组内部的属性和元素的变化。 当 deep 属性为 true 时,watch 会递归遍历整个对象或数组,并对每个属性或元素都进行监视,这样可以保证即使对象或数组内部的属性或元素发生变化,也可以及时触发回调函数。 例如,可以使用以下代码来开启一个深度监视:

watch: {
  obj: {
    handler(newVal, oldVal) {
      // do something
    },
    deep: true
  }
}

这样,在 obj 对象或对象内部的属性发生变化时,handler 回调函数都会被触发。需要注意的是,深度监视会消耗一定的性能,因此需要谨慎使用,仅在需要时才开启深度监视。

31、computed和watch有什么区别?

在 Vue.js 中,computedwatch 都可以用于监听数据变化并触发相应的响应式操作,但它们有一些不同点。

  • 计算属性和侦听属性的用途不同: computed 用于根据已有的属性计算并返回一个新的属性值,而 watch 用于监听某个属性的变化并在变化时执行一些特定的操作。
  • 计算属性是具有缓存机制的:Vue 会对计算属性进行缓存,只有在计算属性的相关依赖发生变化时才会重新计算值,而不是在每次访问计算属性时都进行计算。而 watch 则不具有缓存机制,每次属性变化时都会立即执行回调函数。
  • 计算属性可以作为响应式数据进行监听:由于计算属性是具有缓存机制的,所以 Vue 可以监测到计算属性的依赖关系,从而在依赖发生变化时触发相应的更新操作。而 watch 不会自动监测依赖关系,需要手动指定要监听的属性。
  • 计算属性适合用于多个属性之间的计算:当多个属性之间有复杂的计算关系时,可以使用计算属性来简化代码逻辑。而 watch 适合用于单个属性的变化监听,例如表单输入框的值变化等。

32、computed和watch哪一个可以用于异步操作?

在Vue中,computedwatch都可以用于异步操作,但是它们处理异步操作的方式不同。

computed主要用于处理基于响应式数据的复杂计算,当计算所依赖的响应式数据发生变化时,会自动重新计算结果。因此,如果在计算属性中使用异步操作,最好使用async/await或者返回一个Promise,确保计算属性能够正确地处理异步结果。

例如,下面的代码展示了一个基于axios实现异步请求的计算属性:

computed: {
  async userData() {
    const response = await axios.get('/api/user')
    return response.data
  }
}

watch则主要用于监听数据的变化,并在变化时执行一些逻辑。它的回调函数可以接受两个参数,第一个参数是新值,第二个参数是旧值。如果在watch的回调函数中使用异步操作,需要注意回调函数不能是异步函数。这是因为Vue无法正确处理异步函数的返回值。可以使用async/await或者.then()来处理异步操作。

例如,下面的代码展示了一个基于axios实现异步请求的watch

watch: {
  userId(newVal, oldVal) {
    axios.get(`/api/user/${newVal}`).then(response => {
      this.userData = response.data
    })
  }
}

33、vuex有什么缺点?怎么解决?

Vuex是一个用于Vue.js应用程序的状态管理库,它集中管理了应用程序中的所有组件的状态。虽然Vuex有很多优点,例如使数据流更加明确,方便状态共享和管理等,但也存在一些缺点。下面是一些常见的Vuex缺点和解决方法:

  • 复杂性增加:Vuex的引入会增加应用程序的复杂性,增加了学习成本和开发成本。

解决方法:可以通过良好的代码结构和设计来降低Vuex的复杂性,尽可能将代码拆分成小而简单的部分,并采用面向对象的编程方式。

  • 过度使用:在某些情况下,使用Vuex可能会过度,导致代码变得冗长和难以维护。

解决方法:在使用Vuex之前,需要慎重考虑其实用性,并在必要时使用。可以通过将组件状态保存在本地组件中来减少对Vuex的依赖性,只在必要时使用Vuex来处理跨组件的状态共享。

  • 性能问题:当应用程序变得更加复杂时,Vuex可能会影响应用程序的性能,因为它会导致大量的状态计算和更新。

解决方法:可以使用Vue的性能工具来分析应用程序的性能问题,并使用Vuex的优化技巧来解决性能问题。例如,可以使用Vuex的辅助函数来减少状态更新的次数,或者使用Vue的计算属性缓存结果,以减少不必要的计算。同时,需要注意避免在计算属性和方法中使用复杂的逻辑,以减少状态更新的次数。

34、说一下父子组件的生命周期(挂载、更新)

image.png

image.png

35、数据请求放在哪个生命周期?

通常数据请求放在 mounted 生命周期中。在 mounted 生命周期中,组件已经被挂载到 DOM 树上,这时候可以获取到组件的 DOM 元素,并且可以进行数据请求和初始化操作。

如果将数据请求放在 created 生命周期中,此时组件的 DOM 元素还未被创建,如果数据请求需要根据 DOM 元素的大小或位置来计算参数,则可能会出现问题。

如果将数据请求放在 beforeMount 生命周期中,此时组件的 DOM 元素已经被创建但还未挂载到 DOM 树上,如果数据请求返回的数据需要渲染到组件中,则此时会出现渲染不及时的问题。

当然,具体的实现还要根据具体的业务场景来定。

36、放在beforeumut有什么问题?

参考上一问题

37、路由拦截

路由拦截是指在访问某个路由时,通过对路由进行判断和处理,决定是否进行跳转或其他操作的过程。

在Vue.js中,可以通过路由守卫(Route Guard)来实现路由拦截。路由守卫是在路由跳转过程中被调用的钩子函数,可以在这些函数中对路由进行拦截和处理。

Vue.js提供了全局守卫(beforeEach、beforeResolve、afterEach)和路由级别守卫(beforeEnter、beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave)。

在全局守卫中,可以对所有路由进行拦截和处理;在路由级别守卫中,可以对特定的路由进行拦截和处理。

例如,在全局守卫的beforeEach函数中,可以判断用户是否已经登录,如果没有登录,则跳转到登录页面;在路由级别守卫的beforeRouteEnter函数中,可以获取路由参数,并根据参数来加载相应的数据。

路由拦截可以用于实现权限控制、页面访问限制等功能。

38、路由钩子怎么用的?

在Vue Router中,路由钩子是一些特定的生命周期钩子函数,它们在路由导航过程中被触发,可以用于实现路由拦截、鉴权等功能。

在Vue Router中,有三种类型的路由钩子:全局前置钩子、全局解析守卫和全局后置钩子。它们分别是:

  • 全局前置钩子:使用router.beforeEach()来注册,当导航被触发时,全局前置钩子会在所有路由守卫之前被调用。可以用它来实现路由拦截、鉴权等功能。
  • 全局解析守卫:使用router.beforeResolve()来注册,当导航被触发时,全局解析守卫会在所有路由守卫之前被调用,但是在全局前置钩子之后被调用。可以用它来处理异步路由组件的加载过程。
  • 全局后置钩子:使用router.afterEach()来注册,当导航成功完成时,全局后置钩子会被调用。可以用它来实现路由跟踪等功能。

除了全局钩子之外,还有路由独享的钩子和组件内的钩子。路由独享的钩子可以在路由配置中单独定义,组件内的钩子则是常规的Vue生命周期钩子函数。 下面是一个使用全局前置钩子实现路由拦截的例子:

router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !auth.isAuthenticated()) {
    next('/login')
  } else {
    next()
  }
})

在这个例子中,router.beforeEach()函数注册了一个全局前置钩子,当路由导航被触发时,如果目标路由需要鉴权并且当前用户未认证,就会跳转到登录页面,否则就允许导航继续进行。

需要注意的是,在全局前置钩子中,如果调用了next(false),则当前的导航会被中断,不会进入下一个钩子或目标路由。如果调用了next()next(true),则当前的导航会继续进行,进入下一个钩子或目标路由。如果调用了next('/path'),则当前的导航会被中断,跳转到指定的路径。

39、在拦截器里面怎么拿数据?

在Vue路由中,可以使用导航守卫来拦截路由并执行一些操作,比如在拦截器里面拿到数据。可以在路由配置时使用 beforeEach 方法来添加全局前置守卫,如下所示:

router.beforeEach((to, from, next) => {
  // 在这里可以拿到路由的相关信息,包括参数和元数据等
  const token = localStorage.getItem('token')
  if (to.meta.requiresAuth && !token) {
    // 如果需要验证,但是没有token,跳转到登录页
    next('/login')
  } else {
    // 否则继续访问
    next()
  }
})

在这个例子中,通过 to 参数可以拿到要跳转的路由信息,通过 from 参数可以拿到当前路由信息,通过 next 参数可以控制路由跳转。在这里,我们可以从本地存储中获取到token,来判断是否需要验证。如果需要验证,但是没有token,则跳转到登录页;否则继续访问。

需要注意的是,在使用 next 方法时,如果不传入参数,则会继续访问当前路由;如果传入一个参数,则会跳转到指定路由;如果传入一个错误对象,则会取消当前的导航。

如果需要在特定的路由或组件中使用路由钩子,则可以在路由配置或组件选项中添加相应的钩子函数。例如,在组件中可以通过添加 beforeRouteEnterbeforeRouteUpdate 钩子来处理路由拦截器相关的逻辑。

40、代码管理用什么?说一下git的常用命令?

  • git init:初始化本地仓库
  • git pull:从远程仓库拉取代码到本地
  • git clone:克隆远程仓库到本地
  • git merge:合并分支
  • git status:查看仓库状态
  • git branch:查看分支
  • git add:将文件添加到Git暂存区
  • git commit:将文件从暂存区提交到本地仓库
  • git push:将本地仓库提交到远程仓库
  • git diff:查看文件差异
  • git reset:回退版本

41、实习公司后台管理系统用的什么ui框架?

饿了么

42、虚拟列表怎么实现的?

因为每一数据项高度itemHeight是固定的

  • 通过网页卷去的高度scolltop/itemHeight得到起始索引startIndex
  • 通过屏幕高度screenHeight/itemHeight得到屏幕显示的数据项个数number
  • 通过起始索引startIndex加上数据项个数number得到结束索引endIndex
  • 通过起始索引和结束索引,用数组的slice方法去截取数据进行渲染
  • 最后设置起始索引所在的数据项在整个列表的偏移量startIndex*itemHeight设置到列表上。

43、微前端你有了解过吗?