前端面试题详解整理34|二叉树最近公共父节点,说一下权重,promise,手写并发控制,关于分割 URL 的判断数组。typeof和instanceof的区别,

55 阅读9分钟

字节 飞书前端 一面 凉经

因为有个低代码平台的项目经验,一上来疯狂问项目
然后问了几个vue相关的,感觉就像是随便问的。
最后写了一道简单的算法,

二叉树最近公共父节点

找到二叉树中给定两个节点的最近公共祖先是一种常见的算法问题。下面是一个简单的 JavaScript 实现:

class TreeNode {
    constructor(val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}

function lowestCommonAncestor(root, p, q) {
    if (root === null || root === p || root === q) {
        return root;
    }

    const left = lowestCommonAncestor(root.left, p, q);
    const right = lowestCommonAncestor(root.right, p, q);

    if (left === null) {
        return right;
    } else if (right === null) {
        return left;
    } else {
        return root;
    }
}

// 示例用法
const root = new TreeNode(3);
root.left = new TreeNode(5);
root.right = new TreeNode(1);
root.left.left = new TreeNode(6);
root.left.right = new TreeNode(2);
root.right.left = new TreeNode(0);
root.right.right = new TreeNode(8);
root.left.right.left = new TreeNode(7);
root.left.right.right = new TreeNode(4);

const p = root.left;
const q = root.right;

const result = lowestCommonAncestor(root, p, q);
console.log(result.val); // 应该输出3,即最近公共祖先节点的值

这段代码首先定义了一个 TreeNode 类来表示树节点,然后实现了 lowestCommonAncestor 函数来找到给定两个节点的最近公共祖先。找到二叉树中给定两个节点的最近公共祖先是一种常见的算法问题。下面是一个简单的 JavaScript 实现:

class TreeNode {
    constructor(val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}

function lowestCommonAncestor(root, p, q) {
    if (root === null || root === p || root === q) {
        return root;
    }

    const left = lowestCommonAncestor(root.left, p, q);
    const right = lowestCommonAncestor(root.right, p, q);

    if (left === null) {
        return right;
    } else if (right === null) {
        return left;
    } else {
        return root;
    }
}

// 示例用法
const root = new TreeNode(3);
root.left = new TreeNode(5);
root.right = new TreeNode(1);
root.left.left = new TreeNode(6);
root.left.right = new TreeNode(2);
root.right.left = new TreeNode(0);
root.right.right = new TreeNode(8);
root.left.right.left = new TreeNode(7);
root.left.right.right = new TreeNode(4);

const p = root.left;
const q = root.right;

const result = lowestCommonAncestor(root, p, q);
console.log(result.val); // 应该输出3,即最近公共祖先节点的值

这段代码首先定义了一个 TreeNode 类来表示树节点,然后实现了 lowestCommonAncestor 函数来找到给定两个节点的最近公共祖先。

基本上全答出来了
然后面试完大概5min,显示流程终止,感觉明显就是不想要人,建议投飞书的避避坑

作者:马屁山山主
链接:www.nowcoder.com/feed/main/d…
来源:牛客网

字节前端一二三面面经

一面
说一下你理解的div
说一下flex

说一下权重,看代码(父子和子的权重问题)

在前端开发中,"权重"通常指的是 CSS 样式表中选择器的优先级。CSS 选择器的优先级决定了哪些样式将被应用到元素上。

通常,选择器的优先级由以下几个因素决定,按重要性递减排列:

  1. 内联样式:在 HTML 元素中直接使用 style 属性设置的样式,其优先级最高,权重为 1000。
  2. ID 选择器:通过 ID 选择器(如 #element-id)设置的样式具有较高的优先级,权重为 100。
  3. 类选择器、属性选择器和伪类选择器:类选择器(如 .class-name)、属性选择器(如 [attribute])和伪类选择器(如 :hover)具有中等优先级,权重为 10。
  4. 元素选择器和伪元素选择器:元素选择器(如 div)和伪元素选择器(如 ::before)具有最低的优先级,权重为 1。

如果多个样式规则应用于同一个元素,浏览器将根据选择器的权重确定最终应用的样式。具体来说,权重越高的样式将覆盖权重较低的样式。例如,ID 选择器的样式将覆盖类选择器的样式,类选择器的样式将覆盖元素选择器的样式,依此类推。

在某些情况下,可能会遇到父子元素之间的样式冲突。这时,可以通过以下方式解决:

  1. 使用更具体的选择器:如果父子元素之间存在样式冲突,可以尝试使用更具体的选择器来限制样式的范围,以确保样式应用于正确的元素。
  2. 使用 !important 关键字:可以在样式规则中使用 !important 关键字来提高样式的优先级,使其覆盖其他具有相同或更低权重的样式。但应慎重使用,因为它会增加样式维护的复杂性。

如果你有具体的代码示例或问题,我可以帮助你更详细地解决问题。

说一下promise,手写并发控制

Promise 是 JavaScript 中用于处理异步操作的对象,它代表了一个异步操作的最终完成或失败,并且可以获取其结果。

Promise 的特点包括:

  1. 状态:Promise 对象有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。状态一旦改变,就不会再变。
  2. 异步操作:Promise 可以处理异步操作,通过 then() 方法注册回调函数,在异步操作完成后执行。
  3. 错误处理:可以通过 catch() 方法捕获异步操作中的错误。
  4. 链式调用:可以通过 then() 方法链式调用多个异步操作。

下面是一个手写并发控制的示例:

// 创建一个包装异步任务的 Promise 函数
function asyncTask(taskName) {
  return new Promise((resolve, reject) => {
    // 模拟异步操作
    setTimeout(() => {
      console.log(`${taskName} 完成`);
      resolve();
    }, Math.random() * 1000); // 使用随机时间模拟不同的异步任务
  });
}

// 并发控制函数,接受任务数组和并发数量作为参数
function concurrentControl(tasks, concurrency) {
  let index = 0; // 记录当前执行的任务索引
  const results = []; // 保存任务执行结果的数组

  // 递归执行任务函数
  function executeTask() {
    // 如果所有任务都已执行完毕,则返回结果
    if (index >= tasks.length) {
      return Promise.resolve(results);
    }

    // 截取当前并发数量的任务进行执行
    const currentTasks = tasks.slice(index, index + concurrency);

    // 更新索引,准备下一次执行
    index += concurrency;

    // 使用 Promise.all 执行并发任务
    return Promise.all(
      currentTasks.map(task => {
        return task()
          .then(result => {
            results.push(result); // 保存任务执行结果
          });
      })
    ).then(() => executeTask()); // 递归执行下一批任务
  }

  // 返回执行任务的 Promise
  return executeTask();
}

// 定义多个异步任务
const tasks = [
  () => asyncTask('任务1'),
  () => asyncTask('任务2'),
  () => asyncTask('任务3'),
  () => asyncTask('任务4'),
  () => asyncTask('任务5')
];

// 控制并发数量为2
concurrentControl(tasks, 2)
  .then(results => {
    console.log('所有任务执行完毕');
    console.log('任务执行结果:', results);
  })
  .catch(error => {
    console.error('任务执行出错:', error);
  });

在上面的示例中,我们定义了一个并发控制函数 concurrentControl(),它接受任务数组和并发数量作为参数,然后递归执行任务并控制并发数量。

怎么判断数组

要判断一个变量是否是数组,可以使用 JavaScript 中的 Array.isArray() 方法。这个方法接受一个参数,并返回一个布尔值,指示该参数是否为数组。

示例代码如下:

const arr = [1, 2, 3];
const obj = { key: 'value' };

console.log(Array.isArray(arr)); // true,arr 是数组
console.log(Array.isArray(obj)); // false,obj 不是数组

在上面的示例中,Array.isArray(arr) 返回 true,因为 arr 是一个数组;而 Array.isArray(obj) 返回 false,因为 obj 不是一个数组。

typeof和instanceof的区别

写代码,分割url
typeofinstanceof 是 JavaScript 中用于类型检测的两种不同方法,它们之间有以下区别:

  1. typeof 是一个操作符,返回一个表示未经计算的操作数的类型的字符串。它通常用于确定变量的类型,可以用于检测 JavaScript 的基本数据类型(如字符串、数字、布尔值等),以及函数和未定义的值。但 typeof 对于数组和对象返回的都是 "object"。

  2. instanceof 是一个运算符,用于检测一个对象是否是某个构造函数的实例。它检测的是原型链,如果对象的原型链中包含指定构造函数的原型,则返回 true,否则返回 false。

示例代码如下:

// 使用 typeof 检测变量类型
console.log(typeof "Hello"); // 输出 "string"
console.log(typeof 42); // 输出 "number"
console.log(typeof true); // 输出 "boolean"
console.log(typeof undefined); // 输出 "undefined"
console.log(typeof null); // 输出 "object"
console.log(typeof []); // 输出 "object",但不是数组类型
console.log(typeof {}); // 输出 "object"

// 使用 instanceof 检测对象是否为某个构造函数的实例
console.log([] instanceof Array); // 输出 true,[] 是 Array 的实例
console.log({} instanceof Object); // 输出 true,{} 是 Object 的实例
console.log("Hello" instanceof String); // 输出 false,"Hello" 不是 String 的实例,因为它是一个原始值而不是对象

关于分割 URL 的代码,你想要分割 URL 的哪些部分呢?常见的包括协议、主机、路径、查询参数等。

要分割 URL,可以使用 JavaScript 中的 URL 对象或者正则表达式来实现。以下是两种方法的示例:

  1. 使用 URL 对象:
const url = new URL("https://www.example.com/path/to/resource?query=value");

console.log("Protocol:", url.protocol); // 输出 "https:"
console.log("Host:", url.host); // 输出 "www.example.com"
console.log("Path:", url.pathname); // 输出 "/path/to/resource"
console.log("Query:", url.search); // 输出 "?query=value"
  1. 使用正则表达式:
const url = "https://www.example.com/path/to/resource?query=value";

// 正则表达式匹配
const matches = url.match(/^(https?:\/\/[^\/]+)(\/[^?]*)(\?.*)?$/);

if (matches) {
  console.log("Protocol:", matches[1]); // 输出 "https:"
  console.log("Host:", matches[2]); // 输出 "/path/to/resource"
  console.log("Query:", matches[3]); // 输出 "?query=value"
} else {
  console.log("URL 格式不正确");
}

这两种方法都可以用于分割 URL,根据实际情况选择适合的方法。

二面
场景题react

手写自定义hook,实现登陆拦截

自定义 Hook 是一个函数,名称以 “use” 开头,它可以让你使用 React 的一些特性。要实现登陆拦截的自定义 Hook,你可以按照以下步骤操作:

  1. 创建一个名为 useAuth 的自定义 Hook。
  2. 在该 Hook 中,使用 React 的 useState 来存储用户的登陆状态。
  3. 实现一个登陆函数,用于设置用户的登陆状态。
  4. 实现一个登出函数,用于清除用户的登陆状态。
  5. 在自定义 Hook 中,编写逻辑以检查用户是否已经登陆,如果未登录则跳转到登陆页面。
  6. 在组件中使用 useAuth Hook,以实现登陆拦截功能。

下面是一个简单的示例代码:

import { useState } from 'react';

// 自定义 Hook
const useAuth = () => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  // 登陆函数
  const login = () => {
    setIsLoggedIn(true);
  };

  // 登出函数
  const logout = () => {
    setIsLoggedIn(false);
  };

  // 检查用户是否已经登陆
  const checkAuth = () => {
    if (!isLoggedIn) {
      // 未登录则跳转到登陆页面
      window.location.href = '/login'; // 这里假设登陆页面的路径是 '/login'
    }
  };

  return { isLoggedIn, login, logout, checkAuth };
};

export default useAuth;

然后,在你的组件中使用 useAuth Hook,调用 checkAuth 函数即可实现登陆拦截:

import React, { useEffect } from 'react';
import useAuth from './useAuth'; // 引入自定义 Hook

const ProtectedComponent = () => {
  const { isLoggedIn, checkAuth } = useAuth(); // 使用自定义 Hook

  // 在组件加载时检查用户登陆状态
  useEffect(() => {
    checkAuth();
  }, []); // 空数组表示仅在组件加载时执行一次

  return (
    <div>
      {isLoggedIn ? (
        <p>Welcome, user!</p>
      ) : (
        <p>Please log in to view this content.</p>
      )}
    </div>
  );
};

export default ProtectedComponent;

以上代码是一个简单的登陆拦截功能的实现,。 你可以根据自己的需求进行扩展和修改

手写千分位格式化

下面是一个 JavaScript 函数,可以实现对数字的千分位格式化:

function formatNumber(number) {
  // 先将数字转换为字符串
  let str = number.toString();
  
  // 正则表达式匹配数字的整数部分和小数部分
  let parts = str.split('.');
  let integerPart = parts[0];
  let decimalPart = parts.length > 1 ? '.' + parts[1] : '';
  
  // 对整数部分添加千分位分隔符
  let integerWithCommas = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  
  // 将整数部分和小数部分拼接起来
  return integerWithCommas + decimalPart;
}

// 示例用法
console.log(formatNumber(1234567.89)); // 输出 "1,234,567.89"
console.log(formatNumber(9876543210)); // 输出 "9,876,543,210"

该函数接受一个数字作为参数,并返回一个格式化后的字符串,其中整数部分有千分位分隔符。

三面

扫一扫登陆流程

扫码登录流程通常涉及以下步骤:

  1. 生成登录二维码: 服务器端生成一个唯一的登录标识,并生成对应的登录二维码,通常包含了登录标识和一些其他信息。

  2. 展示二维码: 前端页面展示生成的登录二维码供用户扫描。

  3. 用户扫描二维码: 用户使用移动设备上的扫码工具扫描二维码。

  4. 确认登录: 扫描成功后,用户需要确认是否进行登录操作。

  5. 向服务器发送请求: 扫描后确认登录,移动设备会向服务器发送一个包含了登录标识的请求。

  6. 服务器验证登录标识: 服务器接收到请求后,验证登录标识的有效性。

  7. 返回登录结果: 如果登录标识有效,服务器返回登录成功的信息,否则返回登录失败的信息。

  8. 前端处理结果: 前端页面根据服务器返回的登录结果,展示登录成功或者失败的提示。

  9. 登录成功后续操作: 如果登录成功,前端页面可能会跳转到登录成功后的页面或者进行其他相关操作。

整个流程中,前后端需要进行有效的通信和数据验证,以确保登录操作的安全性和可靠性。

react通信,单项数据流

React 中的单向数据流是指数据在应用中的传递方向是单向的,通常从父组件流向子组件。这种单向数据流有助于代码的可维护性和可预测性。

在 React 中,父组件可以通过 props 将数据传递给子组件,子组件则无法直接修改父组件传递过来的 props。如果子组件需要修改数据,通常会通过触发事件或调用父组件传递过来的回调函数来实现。

单向数据流的优点包括:

  1. 可维护性: 数据流的方向清晰明确,易于理解和维护。
  2. 可预测性: 数据的流向是可预测的,减少了程序的复杂度和出错的可能性。
  3. 组件解耦: 父组件和子组件之间的数据传递通过 props 进行,降低了组件之间的耦合度。
  4. 数据单一来源: 数据的修改只能通过特定的途径进行,避免了数据的混乱和不一致性。

在实际开发中,单向数据流是 React 组件之间通信的主要方式,有助于构建可维护和可扩展的应用程序。

看代码,变量问题,pomise问题

写代码,数组里的0移到最后

大部分都忘记了。。。 以下是将数组中的所有 0 移到数组末尾的 JavaScript 代码:

function moveZerosToEnd(arr) {
    let count = 0; // 记录非零元素的个数
    // 遍历数组,将非零元素依次向前移动
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] !== 0) {
            arr[count++] = arr[i];
        }
    }
    // 将剩余位置填充为 0
    while (count < arr.length) {
        arr[count++] = 0;
    }
    return arr;
}

// 示例用法
const array = [0, 1, 0, 3, 12];
console.log(moveZerosToEnd(array)); // 输出:[1, 3, 12, 0, 0]

这段代码首先遍历数组,将非零元素依次向前移动,然后将剩余位置填充为 0,从而实现了将数组中的所有 0 移到数组末尾的功能。

作者:夏目又三
链接:www.nowcoder.com/feed/main/d…
来源:牛客网