前端面试提问(4)

2023-12-13 13:56:29

1、手撕防抖与节流、树与对象的转换、递归调用,链表头插法

1.1、防抖

防抖函数用于延迟执行某个函数,直到过了一定的间隔时间(例如等待用户停止输入)后再执行。

即后一次点击事件发生时间距离一次点击事件至少间隔一定时间。

function debounce(fn, wait) {
  let timer = null
  return function () {
    if (timer) {
      clearTimeout(timer)
      timer = null
    }
    timer = setTimeout(() => {
      fn.call(this, arguments)
    }, wait)
  }
}

1.2、节流

节流函数用于限制函数的执行频率,确保一定时间内只执行一次。

//时间戳版
function throttle(fn, wait) {
  let date = Date.now()
  return function () {
    let now = Date.now()
    if (now - date > wait) {
      fn.call(this, arguments)
      date = now
    }
  }
}
//定时器版
function throttle(fn, wait) {
  let timer = null
  return function () {
    if (!timer) {
      timer = setTimeout(() => {
        fn.call(this, arguments)
        timer = null
      }, wait)
    }
  }
}

?1.3、树与对象的转换

参考作者之前的文章

2、水平垂直居中方法

①flex布局,父元素display:flex; justify-content:center; align-items:center;

②父元素position:relative; 子元素position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);

③父元素position:relative; 子元素position:absolute; left:0; top:0; bottom:0; right:0; margin:auto;

④文字的话, text-align:center; line-height 和 height 相等

3、?手写ajax(使用promise封装)

function getJSON(url) {
  let promise = new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest()
    xhr.open("GET", url, true)
    xhr.onreadystatechange = function () {
      if (this.readyState !== 4) return
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.onerror = function () {
      reject(new Error(this.statusText))
    }
    xhr.responseType = "json"
    xhr.setRequestHeader("Accept", "application/json")
    xhr.send(null)
  })
  return promise
}

4、扁平数组

const flatten = (arr) => {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
  }, []);
};

arr1 = [1, [2, 3], [4, [5, [6, 7], 8]]];

5、setTimeout实现setInterval

function myInterval(func, time) {
  let ids = [];

  function fn() {
    let id = setTimeout(() => {
      func();
      fn();
    }, time);

    ids.push(id);
  }
  fn();
  return ids;
}

let id = myInterval(() => {
  console.log("Hello World");
}, 500);

function clearMyInterval(idList) {
  idList.forEach((id) => {
    clearTimeout(id);
  });
}

setTimeout(() => {
  clearMyInterval(id);
}, 3000);

6、输入url发生了什么?

  1. URL 解析: 浏览器解析输入的 URL。

  2. DNS 解析: 如果域名需要解析,进行 DNS 解析。如果已经有缓存的 DNS 记录,可以跳过此步骤。

  3. 检查缓存: 浏览器检查缓存,看是否已经有了之前请求过的资源的副本。这包括检查浏览器缓存和可能存在的代理服务器缓存。

  4. 有缓存: 如果资源已经存在于缓存中,并且没有过期,浏览器可以跳过后续的步骤,直接使用缓存中的资源渲染页面。

  5. 无缓存或缓存过期: 如果资源不存在于缓存中,或者缓存已经过期,浏览器将按照正常的流程发起网络请求,

  6. TCP 连接:拿到IP地址后,三次握手建立TCP连接,https的话还需要进行TLS加密协议的握手过程

  7. 发送请求,获取响应:连接建立成功之后,浏览器会构建请求行、cookie等数据附加到请求头中,发给服务器,服务器接受请求并解析,如果没有对应的资源就404;否则检查HTTP请求头有没有包含协商缓存信息(前面命中强缓存且已过期的话就会走这个步骤),如果验证缓存没有更新,过期的缓存依然可以使用,就返回304和空响应体;如果没有缓存或者资源更新了,就读取完整请求并准备http响应,进行查询数据库等操作,返回200和查询到的资源

  8. TCP 连接:??浏览器接收到响应数据之后,如果是http1.1以下则直接关闭连接,否则双方都可以根据情况选择关闭TCP连接或者保留重用,现在浏览器默认都会保持连接(keep-alive)

  9. 浏览器渲染: 浏览器使用获取到的资源渲染页面。

缓存是一种重要的性能优化手段,可以减少网络请求,加快页面加载速度。缓存策略通常由服务器端和浏览器端一起决定,可以通过 HTTP 头部信息来进行配置。例如,使用 Cache-Control 头部可以控制缓存的行为,而 ETagLast-Modified 头部可以用于验证缓存是否过期。

7、加载js和css会不会阻塞页面渲染

css用link和@import的情况

link标签引入css资源时在火狐浏览器中是异步加载的,在谷歌浏览器中是同步加载的。

但如果是通过style标签引入样式,则不论何种浏览器,均为同步加载。

@import是在网页完全载入后才加载,在关键路径上创造了更多的网络请求,阻塞渲染时间,影响浏览器的并行下载,多个@import导致下载顺序紊乱。

8、重排重绘

重排:当渲染树的一部分必须更新并且节点的尺寸发生了变化,浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。
①添加、删除可见的dom

②元素的位置改变

③元素的尺寸改变(外边距、内边距、边框厚度、宽高等几何属性)
④页面渲染初始化
⑤浏览器窗口尺寸改变
重绘:是在一个元素的外观被改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。
如何减少reflow、repaint?
①不要一条一条的修改DOM的样式,可以先定义好css的class,然后修改DOM的className。
②不要把DOM结点的属性值放在一个循环里当成循环里的变量。
③为动画的HTML元件适用fixed或absolute的position,那么修改他们的css是不会reflow

9、深浅拷贝

浅拷贝:基本数据类型、扩展运算符()、slice()、concat()、Object.assign()

深拷贝:JSON.parse(JSON.stringify())、手写深拷贝、lodash

手撕深拷贝

let obj = {
  lili: { name: "lili", person: ["lisan", "zhangsan"] },
  arr: [1, 2, 3, 4],
  fruit: "apple",
};
function deepclone(obj) {
  // 检查是否是基本数据类型,如果是则直接返回
  if (obj === null || typeof obj !== "object") {
    return obj;
  }
  if (Array.isArray(obj)) {
    let newArray = [];
    for (let i = 0; i < obj.length; i++) {
      newArray[i] = deepclone(obj[i]);
    }
    return newArray;
  } else {
    let newObj = {};
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        newObj[key] = deepclone(obj[key]);
      }
    }
    return newObj;
  }
}
console.log(deepclone(obj));

10、手撕合并函数

function deepMerge(target, source) {
  // 检查参数类型
  if (typeof target !== "object" || typeof source !== "object") {
    throw new Error("Both target and source must be objects");
  }

  // 遍历source对象的属性
  for (const key in source) {
    if (source.hasOwnProperty(key)) {
      // 如果属性是对象且存在于target中,递归深度合并
      if (
        typeof source[key] === "object" &&
        source[key] !== null &&
        target.hasOwnProperty(key) &&
        typeof target[key] === "object" &&
        target[key] !== null
      ) {
        if (Array.isArray(target[key])) {
          target[key] = [].concat(target[key], source[key]);
        } else {
          target[key] = deepMerge(target[key], source[key]);
        }
      } else {
        // 否则直接赋值
        // target[key] = source[key];
        target[key] = [].concat(target[key], source[key]);
      }
    }
  }

  return target;
}

// 使用例子
const targetObject = {
  name: "John",
  age: 30,
  address: {
    city: "New York",
    zip: "10001",
    people: { class: "1班" },
  },
  hobbies: ["shopping"],
};

const sourceObject = {
  age: 31,
  address: {
    zip: "10002",
  },
  hobbies: ["reading", "traveling"],
};

const resultObject = deepMerge(targetObject, sourceObject);
console.log(resultObject);

11、Object和map

共同点:键值对的动态集合,支持增删

不同点:

①构造方式不同

//map
const?map?=?new?Map()
const?map1?=?new?Map([['a',1],['b',2]])
//obj
const?obj?=?new?Object()
const?obj1?=?Object.create()

②object键的类型必须是String或者Symbol、map键的类型可以是任意类型

③object中key是无序的,map中可以是有序的,按照插入的顺序返回

④object只能通过Object.key()方法或for in统计数量,map有map.size

⑤object可以通过点或中括号访问属性,map用map.get()

⑥object不具备Iterator特性,不能for of遍历,map的keys()、values()、entries()都具有迭代器

⑦object可以用JSON.stringify()进行序列化,map只能转化成JSON,不能被parse解析

⑧应用场景:object做数据存储,需要序列化时使用;map频繁更新键值对,key类型未知时使用

12、http和https的区别

①https协议需要到CA申请证书,一般免费证书较少,因而需要一定费用。
②http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl/tls加密传输协议。
③http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
④http的连接很简单,是无状态的;HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
?

13、堆和栈

13.1、区别

①堆栈空间分配区别(操作系统):

栈由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
堆 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
②堆栈缓存方式区别:
栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
③堆栈数据结构区别:
栈是一种先进后出(FILO)的数据结构;
堆可以被看成是一棵树,如:堆排序。

13.2、最大堆和最小堆?

最大堆:根结点的键值是所有堆结点键值中最大者,且每个结点的值都比其孩子的值大。
最小堆:根结点的键值是所有堆结点键值中最小者,且每个结点的值都比其孩子的值小。

13.3、 堆栈溢出

  • JavaScript 的函数调用栈有一定大小限制,当函数调用的嵌套层数过多时,会导致栈溢出错误。
function recursive() {// 递归出现栈溢出
    recursive();
}
recursive();
  • JavaScript 的堆也有大小限制。堆是用来存储变量和对象等数据的一段内存空间,当我们创建了大量数据或者数据太大而超过了堆的容量时,就会触发堆溢出错误。
let arr = [];
while (true) {// 堆溢出
  arr.push('a');
}

14、进程和线程

进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程。

线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

进程和线程的区别:

①进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位)
②进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。
而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
③线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC) 进行。
④但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

15、promise.all,promise.any,promise.race,promise allsettled

Promise.allSettled 不会在其中一个 Promise 失败时立即 reject,而是等待所有 Promise 完成后再返回结果。

const promise1 = Promise.resolve(42);
const promise2 = Promise.reject("Oops!");
const promise3 = new Promise((resolve) => setTimeout(() => resolve("Done!"), 1000));

Promise.allSettled([promise1, promise2, promise3])
  .then((results) => {
    console.log(results);
    // results 包含了每个 Promise 的状态和结果
    // [{ status: 'fulfilled', value: 42 }, { status: 'rejected', reason: 'Oops!' }, { status: 'fulfilled', value: 'Done!' }]
  })
  .catch((error) => {
    console.error("Error:", error);
  });

Promise.all 在所有 Promise 全部成功(resolved)时才会成功,但只要有一个 Promise 失败(rejected),整个 Promise.all 就会立即失败。这种行为被称为“一败俱败”。

传递给 Promise.all 的数组为空: 如果传递给 Promise.all 的 Promise 数组为空,返回的 Promise 会立即被解决为一个空数组。

如果在某一个promise中reject后,那一个函数捕捉catch,就不会报错。不然就会走all的catch

16、场景题:100000条数据渲染

const renderList = async () => {
    const list = await getList()
    const total = list.length
    const page = 0
    const limit = 200
    const totalPage = Math.ceil(total / limit)
    const render = (page) => {
        if (page >= totalPage) return
        setTimeout(() => {
            for (let i = page * limit; i < page * limit + limit; i++) {
                const item = list[i]
                const div = document.createElement('div')
                div.className = 'sunshine'
                div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
                container.appendChild(div)
            }
            render(page + 1)
        }, 0)
    }
    render(page)

17、你做的项目如何部署到服务器

function myNew(constructor, ...args) {
  // 步骤 1:创建一个新的空对象
  const obj = {};

  // 步骤 2:将新对象的 __proto__ 指向构造函数的 prototype 属性
  obj.__proto__ = constructor.prototype;

  // 步骤 3:将构造函数的上下文传递给构造函数,并执行构造函数
  const result = constructor.apply(obj, args);

  // 步骤 4:如果构造函数返回一个对象,则返回该对象;否则,返回新创建的对象
  return result instanceof Object ? result : obj;
}

18、路由鉴权

18.1、请求数据

  1. 用户登录: 用户在登录时,通过用户名和密码等方式向后端发起登录请求。后端验证用户身份,并在登录成功后生成一个 Token。

  2. Token 存储: 将生成的 Token 存储在前端,通常是通过 localStorage。axios请求拦截,如果存在token,在每次请求时自动附加到请求头。

  3. 请求时携带 Token: 在每次请求后端受保护资源时,将 Token 携带在请求头中,通常是通过 Authorization 头或自定义头。

  4. 后端验证 Token: 后端接收到请求后,通过解析 Token 验证用户身份和权限。如果 Token 有效且权限足够,则返回相应资源;否则,返回错误状态。

18.2、路由守卫

根据权限决定跳转: 如果用户拥有访问权限,正常放行,让用户访问受保护页面。如果用户没有权限,可以将其重定向到登录页或其他提示页面,或者显示相应的提示信息。

// 路由表
const routes = [
  { path: '/', component: Home, meta: { requiresAuth: true } },
  { path: '/admin', component: Admin, meta: { requiresAuth: true, requiresAdmin: true } },
  { path: '/login', component: Login }
];

// 创建路由实例
const router = new VueRouter({
  routes
});

// 路由守卫
router.beforeEach((to, from, next) => {
  const isAuthenticated = /* 根据用户身份信息判断用户是否已登录 */;
  const isAdmin = /* 根据用户身份信息判断用户是否是管理员 */;

  if (to.meta.requiresAuth && !isAuthenticated) {
    // 用户未登录,重定向到登录页
    next('/login');
  } else if (to.meta.requiresAdmin && !isAdmin) {
    // 用户不是管理员,可以根据需要进行处理,例如重定向到首页
    next('/');
  } else {
    // 用户有权限,放行
    next();
  }
});

// 在 Vue 实例中使用路由
new Vue({
  el: '#app',
  router,
  render: h => h(App)
});

18.3、不同等级的权限控制

  1. 获取用户权限信息: 在用户登录成功后,获取token与用户的权限信息,存储在Vuex或者localStorage

  2. 定义菜单权限配置: 在前端定义一个菜单权限配置,包含不同权限下允许访问的菜单项。这可以是一个简单的 JSON 对象或数组,其中每个菜单项都包含一个权限属性,表示需要的用户权限。

  3. 根据用户权限过滤菜单项: 根据用户拥有的权限,从菜单权限配置中筛选出符合条件的菜单项。

  4. 动态生成菜单: 使用框架或库的路由功能,根据过滤后的菜单项动态生成菜单。这可以在页面加载时或用户登录成功后执行。

// 菜单权限配置
const menuConfig = [
  { path: '/dashboard', name: 'Dashboard', permission: 'view_dashboard' },
  { path: '/users', name: 'Users', permission: 'manage_users' },
  // 其他菜单项
];

// 获取用户权限信息(模拟)
const userPermissions = ['view_dashboard', 'manage_users']; // 实际中应该根据登录成功后的用户信息获取权限

// 根据用户权限过滤菜单项
const userMenu = menuConfig.filter(item => userPermissions.includes(item.permission));

// 在 Vue 实例中使用路由
const router = new VueRouter({
  routes: userMenu,
});

// 示例组件中动态生成菜单
Vue.component('Sidebar', {
  template: `
    <div>
      <router-link v-for="item in userMenu" :to="item.path" :key="item.path">
        {{ item.name }}
      </router-link>
    </div>
  `,
  data() {
    return {
      userMenu,
    };
  },
});

// 在 Vue 实例中使用路由和菜单组件
new Vue({
  el: '#app',
  router,
  render: h => h(App),
});

文章来源:https://blog.csdn.net/m0_56770663/article/details/134840809
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。