2024.04.20 - 2024.05.09 更新前端面试问题总结(18 道题)
获取更多面试相关问题可以访问
github 地址: https://github.com/pro-collection/interview-question/issues
gitee 地址: https://gitee.com/yanleweb/interview-question/issues

目录:

  • 中级开发者相关问题【共计 7 道题】

    • 726.[React] 介绍一下 HOC【热度: 789】【web 框架】【出题公司: 百度】
    • 727.介绍一下 MutationObserver【热度: 632】【浏览器】【出题公司: 阿里巴巴】
    • 728.ts 项目中,如何使用 node_modules 里面定义的全局类型包到自己项目 src 下面使用?【热度: 377】【TypeScript】【出题公司: 阿里巴巴】
    • 730.mouseEnter、mouseLeave、mouseOver、mouseOut 有什么区别?【热度: 266】【JavaScript】【出题公司: 阿里巴巴】
    • 739.样式隔离方式有哪些【热度: 683】【CSS】【出题公司: 美团】
    • 742.单元测试中, TDD、BDD、DDD 分别指?【热度: 166】【工程化】【出题公司: 京东】
    • 743.用 JS 写一个 cookies 解析函数, 输出结果为一个对象【热度: 137】【web 应用场景】【出题公司: 网易】
  • 高级开发者相关问题【共计 11 道题】

    • 725.husky 作用是啥, 有哪些重要配置【热度: 192】【工程化】【出题公司: 腾讯】
    • 729.tsconfig 配置中 types 和 typeRoots 作用是什么, 有什么区别?【热度: 378】【TypeScript】【出题公司: 阿里巴巴】
    • 731.[React] Portals 作用是什么, 有哪些使用场景?【热度: 216】【web 框架】【出题公司: 腾讯】
    • 732.[React] react 和 react-dom 是什么关系?【热度: 197】【web 框架】【出题公司: 腾讯】
    • 733.什么是 DNS 劫持?【热度: 165】【网络】【出题公司: 百度】
    • 734.站点如何防止爬虫?【热度: 554】【web 应用场景】【出题公司: 百度】
    • 735.git pull 和 git fetch 有啥区别?【热度: 355】【web 应用场景】【出题公司: 百度】
    • 737.在 JS 中, 如何解决递归导致栈溢出问题?【热度: 269】【JavaScript】【出题公司: 小米】
    • 738.jsBridge 是什么?原理是啥?【热度: 220】【JavaScript】【出题公司: 小米】
    • 740.vue 中 Scoped Styles 是如何实现样式隔离的, 原理是啥?【热度: 244】【CSS】【出题公司: 美团】
    • 741.[React] forwardsRef 作用是啥, 有哪些使用场景?【热度: 336】【web 框架】【出题公司: PDD】

726.[React] 介绍一下 HOC【热度: 789】【web 框架】【出题公司: 百度】

关键词:React HOC

React 中的 HOC(高阶组件,Higher-Order Components)是一种基于 React 的组合特性而形成的设计模式,用于重用组件逻辑。一个高阶组件是一个函数,它接受一个组件并返回一个新组件。

HOC 允许你为组件添加额外的功能而无需更改组件自身的实现。这种模式可以帮助你在 React 应用程序中保持 DRY(不重复你自己),并且可以提升组件的可测试性和可维护性。

  1. 代码复用、逻辑和引导抽象: 可以将共享逻辑提取到 HOC 中,让不同的组件能够重用这段逻辑。
  2. 渲染劫持: 在 HOC 中可以修改传入组件的 JSX 结构。
  3. 状态抽象和操作: 可以将内部状态和相关方法从组件中抽离出来。
  4. Props 代理: 通过 HOC 可以添加、编辑或删除传入组件的 props。
 1function withSubscription(WrappedComponent, selectData) {
 2  // 返回一个 class 组件
 3  return class extends React.Component {
 4    constructor(props) {
 5      super(props);
 6      this.handleChange = this.handleChange.bind(this);
 7      this.state = {
 8        data: selectData(DataSource, props),
 9      };
10    }
11
12    componentDidMount() {
13      // ...负责订阅相关的操作...
14    }
15
16    componentWillUnmount() {
17      // ...取消订阅...
18    }
19
20    handleChange() {
21      this.setState({
22        data: selectData(DataSource, this.props),
23      });
24    }
25
26    render() {
27      // ... 并使用新数据渲染被包装的组件!
28      // 请注意,我们可能还会传递其他属性
29      return <WrappedComponent data={this.state.data} {...this.props} />;
30    }
31  };
32}

在这个例子中,withSubscription 是一个 HOC。它接受一个组件 WrappedComponent 和一个函数 selectData 作为参数,这个函数用于从数据源中选择需要的数据。返回一个新的组件,这个新组件通过 state 管理数据,并在挂载后订阅数据源,在卸载前取消订阅,并且在数据改变时通过 setState 更新数据。

  • HOC 不应该修改传入的组件,而是使用组合的方式将其包裹起来。
  • 传递不相关的 props 至被包裹的组件,可能会导致属性冲突。
  • HOC 应该传递不与高阶组件相关的 props 至被包裹的组件,这有助于保持组件的纯净和可复用性。
  • 对于 HOC,通常需要注意不要在 render 方法中创建 HOC,因为这会导致组件的不必要的重新挂载。

总而言之,HOC 是 React 中一个非常有用的模式,允许开发者以声明方式抽象组件逻辑,提高组件复用。

727.介绍一下 MutationObserver【热度: 632】【浏览器】【出题公司: 阿里巴巴】

关键词:MutationObserver api

MutationObserver 是一种能够响应 DOM 树变动的 Web API,它可以监听几乎所有类型的 DOM 变动,比如元素被添加、删除或修改。你可以通过它执行 callback 来应对这些变化。

下面是 MutationObserver 的基本用法:

 1var observer = new MutationObserver(callback);

你可以指定要观察的 DOM 变动的类型和具体的目标节点:

 1var config = {
 2  attributes: true, // 观察属性变动
 3  childList: true, // 观察子列表变动
 4  subtree: true, // 观察后代节点
 5};
 6
 7observer.observe(targetNode, config);

这里的 callback 是一个在观察到变动时执行的函数,它有两个参数:mutationsList 是一个变动列表,observer 是观察者实例。

MutationCallback 函数会被调用,它有两个参数:

  1. mutationsList:一个 MutationRecord 对象的数组,每个对象都描述了一个变动。
  2. observer:触发通知的 MutationObserver 实例。
 1function callback(mutationsList) {
 2  for (var mutation of mutationsList) {
 3    if (mutation.type === "childList") {
 4      console.log("A child node has been added or removed.");
 5    } else if (mutation.type === "attributes") {
 6      console.log(`The ${mutation.attributeName} attribute was modified.`);
 7    }
 8  }
 9}

你可以通过调用 disconnect 方法来停止观察:

这将停止观察并且清除之前的记录。

  • 使用 MutationObserver 应该谨慎,因为它可能对页面性能产生影响,尤其是在观察大型 DOM 树或频繁变动时。
  • 尽量不要过度使用 MutationObserver 或过度指定需要它观察的变动种类和节点。

比如,如果你只想监听某个特定属性的变动,那么就不应该打开 childList 或者 attributes(如果不需要观察它们)。

MutationObserver 非常适用于响应 DOM 的动态变动来执行特定的 JavaScript 代码,而且是现代前端开发中的一个重要工具。在使用它时,考虑使用最严格的选项来优化性能并减少不必要的性能损耗。

728.ts 项目中,如何使用 node_modules 里面定义的全局类型包到自己项目 src 下面使用?【热度: 377】【TypeScript】【出题公司: 阿里巴巴】

关键词:ts 类型配置

关键点在 types 属性配置

在 TypeScript 项目中导入 node_modules 中定义的全局包,并在你的 src 目录下使用它,通常遵循以下步骤:

  1. 安装包: 使用包管理器如 npm 或 yarn 来安装你需要的全局包。

     1npm install <package-name>
     2# 或者
     3yarn add <package-name>
  2. 类型声明: 确保该全局包具有类型声明。如果该全局包包含自己的类型声明,则 TypeScript 应该能够自动找到它们。如果不包含,则可能需要安装对应的 DefinitelyTyped 声明文件。

     1npm install @types/<package-name>
     2# 或者如果它是一个流行的库一些库可能已经带有自己的类型定义
  3. 导入包: 在 TypeScript 文件中,使用 import 语句导入全局包。

     1import * as PackageName from "<package-name>";
     2// 或者
     3import PackageName from "<package-name>";
  4. tsconfig.json 配置: 确保你的 tsconfig.json 文件配置得当,以便 TypeScript 能够找到 node_modules 中的声明文件。

    • 如果包是模块形式的,确保 "moduleResolution" 设置为 "node"
    • 确保 compilerOptions 中的 "types""typeRoots" 属性没有配置错误。
  5. 使用全局包: 现在你可以在你的 src 目录中的任何文件里使用这个全局包。

记住,最好的做法是不要把包当成全局包来使用,即使它们是全局的。通过显式地导入所需的模块,可以有助于工具如 linters 和 bundlers 更好地追踪依赖关系,并可以在以后的代码分析和维护中发挥重要作用。

此外,全局变量或全局模块通常指的是在项目的多个部分中无需导入就可以直接使用的变量或模块。如果你确实需要将某些模块定义为全局可用,并且无法通过导入来使用,你可能需要更新你的 TypeScript 配置文件(tsconfig.json)来包括这些全局声明。但这通常不是一个推荐的做法,因为它可能会导致命名冲突和代码可维护性问题。

730.mouseEnter、mouseLeave、mouseOver、mouseOut 有什么区别?【热度: 266】【JavaScript】【出题公司: 阿里巴巴】

关键词:mouseEnter、mouseLeave、mouseOver、mouseOut 区别

这四个事件都与鼠标指针与元素的交互有关,不过它们之间有一些关键的差异:

  1. mouseEnter 和 mouseLeave

    • mouseEnter 事件当鼠标指针进入元素时触发,但不冒泡,即只有指定的元素可以触发此事件,其子元素不能。
    • mouseLeave 事件则是当鼠标指针离开元素时触发,同样也不冒泡。
  2. mouseOver 和 mouseOut

    • mouseOver 事件当鼠标指针移动到元素或其子元素上时触发,该事件会冒泡,即如果鼠标指针移动到其子元素上,也会触发该元素的mouseOver事件。
    • mouseOut 事件则是当鼠标指针离开元素或其子元素时触发,也会冒泡。

总结一下它们的区别:

  • 冒泡: mouseOvermouseOut 事件会冒泡(父元素也会响应这个事件),而 mouseEntermouseLeave 不会冒泡。
  • 对子元素的响应mouseOvermouseOut 会在鼠标指针移动到子元素上时也被触发,而 mouseEntermouseLeave 在鼠标指针移动到子元素上时不会被触发。

在处理具有嵌套子元素的元素时,使用 mouseEntermouseLeave 可以避免多余的事件触发,因为它们不会在鼠标从父元素移动到子元素时触发事件。(即不会对内部子元素的进入和离开反应敏感)。而 mouseOvermouseOut 更适合需要监测鼠标指针是否有移动到子元素上的情况。

739.样式隔离方式有哪些【热度: 683】【CSS】【出题公司: 美团】

关键词:样式个例

样式隔离意味着在一个复杂的前端应用中保持组件的样式私有化,使得不同组件之间的样式不会互相影响。以下是一些在前端开发中实现样式隔离的常见方式:

CSS 模块是一种在构建时将 CSS 类名局部作用域化的技术。每个类名都是独一无二的,通常通过添加哈希值来实现。当你导入一个 CSS 模块,会得到一个包含生成的类名的对象。这样可以确保样式的唯一性,并防止样式冲突。

Shadow DOM 是 Web 组件规范的一部分,它允许将一段不受外界影响的 DOM 附加到元素上。在 Shadow DOM 中的样式是局部的,不会影响外部的文档样式。

CSS-in-JS 是一种技术,允许你用 JavaScript 编写 CSS,并在运行时生成唯一的类名。常见的库有 Styled-components、Emotion 等。这些库通常提供自动的样式隔离,并且还支持主题化和动态样式。

4. 使用 BEM(Block Element Modifier)命名约定

BEM 是一种 CSS 命名方法,通过使用严格的命名规则来保持样式的模块化。通过将样式绑定到特定的类名上,这种方法有助于防止样式泄露。

在 Vue.js 中,可以为 <style> 标签添加 scoped 属性,这将使用 Vue 的编译器来实现样式的作用域。虽然这不是一个标准的 Web 特性,但它在 Vue 生态系统中提供了很方便的样式隔离。

将组件或部分页面放在 iframe 中可以提供非常强的样式和脚本隔离。尽管如此,iframe 通常不是最佳选择,因为它们可能导致性能问题,而且使得组件间的沟通变得更加困难。

Web 组件利用了自定义元素和 Shadow DOM 来创建封装的、可复用的组件。在 Web 组件中,可以使用 Shadow DOM 实现真正的样式和脚本隔离。

准确使用 CSS 选择器,避免使用全局标签选择器或基础类,而是使用更具体的类选择器可以部分隔离样式。此外,可以设置严格的 CSS 命名策略,不同模块使用不同的命名前缀,以避免名称冲突。

使用 PostCSS 插件来处理 CSS,可以自动添加前缀、变量等,从而实现隔离。例如,PostCSS 前缀插件可以自动为 CSS 类添加唯一的前缀。

各种方法有各自的优点和限制,选择哪种方法取决于项目的技术栈、团队的熟悉程度以及特定的项目需求。

742.单元测试中, TDD、BDD、DDD 分别指?【热度: 166】【工程化】【出题公司: 京东】

关键词:TDD、BDD、DDD

TDD、BDD 和 DDD 这三个缩写在软件开发中分别代表以下概念:

  1. TDD(Test-Driven Development) - 测试驱动开发: TDD 是一种软件开发过程,其中开发人员首先编写一个小测试用例,然后编写足够的代码来使这个测试通过,最后重构新代码以满足所需的设计标准。这个过程就是一个循环,被成为“红-绿-重构”循环,其中测试先失败(红色),编写代码使其通过(绿色),然后优化代码(重构)。TDD 的焦点在于编写干净的代码和降低未来的缺陷。

  2. BDD(Behavior-Driven Development) - 行为驱动开发: BDD 将 TDD 的基本思想和原则扩展到软件的整个开发生命周期,但其着重点在于软件的行为——即软件应如何表现,而不仅仅是它应该完成什么功能。BDD 强调的是与利益相关者的交流与协作,通过使用通俗易懂的语言来写测试,让非技术人员也能理解测试内容。BDD 鼓励团队成员之间更好地沟通,确保所有人都对软件应有的行为有共同的理解。

  3. DDD(Domain-Driven Design) - 领域驱动设计: DDD 与 TDD 和 BDD 并不是同一类型的概念。DDD 是一种软件设计哲学,强调了在软件项目的设计与开发中应以业务领域(Domain)为中心。它主张将业务领域的专业知识嵌入到软件的设计中,从而使软件能更好地解决业务问题。DDD 通常涉及到丰富的领域模型以及分层的架构设计,以确保业务逻辑清晰和维护性高。

这三个概念在软件开发中都扮演着重要的角色。TDD 和 BDD 都与确保软件质量和满足用户需求有关,而 DDD 则是一种更宏观层面上对软件设计的方法论。

743.用 JS 写一个 cookies 解析函数, 输出结果为一个对象【热度: 137】【web 应用场景】【出题公司: 网易】

关键词:解析 cookies

当然可以,以下是一个简单的 JavaScript 函数,用于解析当前页面的 cookie 并将它们存储到一个对象中:

 1function parseCookies() {
 2  // 创建一个空对象来存储解析后的 cookie
 3  var cookiesObj = {};
 4
 5  // 获取 cookie 字符串,然后分割每个键值对
 6  var cookies = document.cookie.split(";");
 7
 8  // 遍历每个键值对
 9  cookies.forEach(function (cookie) {
10    // 去除键值对前后的空格
11    var cleanCookie = cookie.trim();
12    // 找到键和值之间的等号位置
13    var separatorIndex = cleanCookie.indexOf("=");
14
15    // 如果找不到等号,则不是有效的键值对,跳过当前循环
16    if (separatorIndex === -1) return;
17
18    // 获取键名
19    var key = cleanCookie.substring(0, separatorIndex);
20    // 获取值
21    var value = cleanCookie.substring(separatorIndex + 1);
22
23    // 解码因为 cookie 键和值是编码过的
24    key = decodeURIComponent(key);
25    value = decodeURIComponent(value);
26
27    // 将解析后的值存储到对象中
28    cookiesObj[key] = value;
29  });
30
31  // 返回解析后的 cookie 对象
32  return cookiesObj;
33}
34
35// 使用示例
36var cookies = parseCookies();
37console.log(cookies);

这个函数首先会以分号 ; 分割 document.cookie 字符串来得到各个 cookie 键值对,然后移除键值对前后的任何空格。接着寻找每个键值对中的等号 = 位置,以此来分割键和值。最后,它会使用 decodeURIComponent 函数来解码键名和键值,因为通过 document.cookie 读取的键名和键值通常是编码过的。

调用 parseCookies 函数将返回一个对象,其中包含了当前页面的所有 cookie,键名和值都已被解码。然后你可以像访问普通对象一样访问这些值,例如 cookies['username'] 来获取 ‘username’ 键对应的值。

725.husky 作用是啥, 有哪些重要配置【热度: 192】【工程化】【出题公司: 腾讯】

关键词:husky 作用、husky 配置

Husky 是一个基于 Node 的 Git 钩子管理工具,用于在你的工作流程中强制执行 Git 钩子。Husky 允许你定义脚本,这些脚本会在不同的 Git 生命周期事件触发时自运行,比如在提交、推送或合并前。

使用 Husky 可以:

  1. 保证提交质量:Husky 可以在你提交代码之前运行代码校验,确保代码符合项目规范,提高代码质量。
  2. 维护代码风格:可以在提交时检查代码风格,确保代码风格一致性。
  3. 自动化流程:支持在推送前执行代码部署、测试脚本,让整个开发流程自动化。
  4. 预防错误:例如在允许推送到远程仓库之前检查代码中是否有遗留的更改。

Husky 的一些重要配置如下:

  1. npm install husky@latest --save-dev: 安装 husky。
  2. npx husky install: 在新建的项目管理下生成 husky 的配置文件。
  3. npx husky add .husky/*.sh: 添加 Git 钩子脚本,这里的 *.sh 是你想触发的钩子点,例如:pre-commitcommit-msg 等。

Husky 支持的钩子包括:

  • apply-patch-msg: 应用一个补丁到暂存区并生成提交信息时。
  • pre-applypatch: 打补丁前。
  • post-applypatch: 打补丁后。
  • pre-commit: 提交前,常用于检查代码、分析代码风格等。
  • prepare-commit-msg: 提交准备工作完成后,修改提交信息之前运行。
  • commit-msg: 检查提交信息有效性。
  • post-commit: 提交后。
  • pre-rebase: 回滚操作开始前。
  • post-checkout: 检出操作后(如切换分支)。
  • post-merge: 合并和变基操作后。

记得在 .husky 文件夹里配置这些钩子脚本,你可以根据项目需求来写自己的 hook 脚本。比如,设置一个 .husky/pre-commit 脚本(可能是一个 shell 脚本和 Node.js 脚本的组合),当你尝试提交代码时,Husky 将会运行这个脚本作为 pre-commit 钩子。

在一些场景下的 .husky/pre-commit 脚本,你可以指定运行如下:

 1#!/bin/sh
 2. "$(dirname -- "$0")/_/husky.sh"
 3
 4npm run lint  # 运行 ESLint 检查代码
 5./node_modules/.bin/pretty-quick  # 格式化代码
 6./node_modules/.bin/tsc  # 检查 TypeScript

以上脚本将确保代码在提交前通过了 linter 检查,并通过 prettier 快速格式化以及 TypeScript 编译。

使用的时候,请确认你的项目已经有了 Node.js 环境,并且已经安装了 Husky 和相应的代码检查、格式化工具。

729.tsconfig 配置中 types 和 typeRoots 作用是什么, 有什么区别?【热度: 378】【TypeScript】【出题公司: 阿里巴巴】

关键词:ts 类型配置

作者备注 这个问题很冷门, 没有价值, 当做科普即可

在 TypeScript 的 tsconfig.json 配置文件中,typestypeRoots 是两个与类型声明相关的选项,它们用于控制 TypeScript 编译器如何处理类型声明文件。这两个选项的主要区别在于它们控制的范围:

typeRoots 选项指定了包含类型声明文件的目录列表。默认情况下,TypeScript 会查看所有以 node_modules/@types 结尾的目录。通过设置 typeRoots,你可以直接告诉 TypeScript 编译器去哪查找类型声明:

 1{
 2  "compilerOptions": {
 3    "typeRoots": ["./node_modules/@types", "./typings"]
 4  }
 5}

在这个例子中,我们指定了两个 typeRoots:默认的 node_modules/@types 和另外一个自定义的类型声明目录 ./typings

types 选项允许你设置在项目中所使用到的类型声明文件列表。这个列表会限制编译器在 typeRoots 下查找的声明文件,意味着 types 中列出的类型声明会是项目中唯一可以引用的声明。如果没有设置 types,你可以使用存在于 typeRoots 下面的任何类型声明:

 1{
 2  "compilerOptions": {
 3    "types": ["my-global-types"]
 4  }
 5}

在这个例子中,types 选项限制了项目只能使用名为 my-global-types 的类型声明。即使有其他的 .d.ts 文件在 typeRoots 指定的目录下,它们也无法在不修改这个列表的情况下被引用。

  • 当你有多个 d.ts 文件你想指定给 TypeScript 编译器,而不是每一个单独去处理时,使用 typeRoots 更为方便。
  • types 用于控制引用的类型声明集,如果你是在限制或精心策划的设定下工作,这会很有帮助。

在许多情况下,typeRootstypes 可以联合使用:

  1. typeRoots 列表包含了所有声明文件的位置。
  2. types 列表限制 TypeScript 可以引用特定集合的声明(其中未列出的声明则不可用)。

通过合理的配置这两个选项,你可以精确控制在 TypeScript 项目中使用的类型声明,帮助你避免类型定义的混乱。

731.[React] Portals 作用是什么, 有哪些使用场景?【热度: 216】【web 框架】【出题公司: 腾讯】

关键词:React Portals API

React Portals 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的方式。通常,组件的渲染输出会被插入到其在组件树中的父组件下,但是 Portals 提供了一种穿透组件层次结构直接渲染到任意 DOM 节点的方法。

  1. 父子结构逃逸:React Portals 允许你将子组件渲染到其父组件 DOM 结构之外的地方,这在视觉和位置上「逃逸」了它们的父组件。

  2. 样式继承独立:使用 Portal 的组件通常可以避免父组件样式的影响,易于控制和自定义样式。

  3. 事件冒泡正常:尽管 Portal 可以渲染到 DOM 树中的任何位置,但是事件冒泡会按照 React 组件树而不是 DOM 树来进行。所以,尽管组件可能被渲染到 DOM 树的不同部分,它的行为仍然像常规的 React 子组件一样。

  4. 模态框:最常见的场景之一就是模态对话框,这时候对话框需要覆盖应用程序的其余部分(包括可能存在的其他元素如遮罩层),而且往往模态框的样式不应该受到其它 DOM 元素的影响。

  5. 浮动菜单:对于那些需要覆盖其它元素的浮动菜单或下拉式组件,React Portal 可以使这些组件渲染在最外层,避免被其他 DOM 元素的样式或结构干扰。

  6. 提示/通知:用于在界面上创建提示信息,如 Toasts 或 Snackbars,这些通常会浮动在内容之上并在固定位置显示。

  7. 全屏组件:对于需要全屏显示而不受现有 DOM 层级影响的组件(如图片库的全屏视图、视频播放或者游戏界面)。

  8. 第三方库的集成:有时候需要将 React 组件嵌入由非 React 库管理的 DOM 结构中,此时 Portal 可以非常有用。

总之,Portals 提供了一种灵活的方式来逃离父组件的限制,帮助开发者更加自由和方便地进行 UI 布局,同时也有助于维护组件结构的整洁和一致性。

假设我们想创建一个模态框(Modal)组件,我们会希望这个模态框在 DOM 中是在最顶层的,但在 React 组件树中它应该在逻辑上保持在其父组件下。使用 React Portals 可以很容易地实现这一点。

首先,我们在 public/index.html 中,添加一个新的 DOM 节点,作为 Portal 的容器:

 1<!-- index.html -->
 2<div id="app-root"></div>
 3<!-- React App 将会挂载在这里 -->
 4<div id="modal-root"></div>
 5<!-- Modal 元素将会挂载在这里 -->

接着,我们创建一个 Modal 组件,它会使用 ReactDOM.createPortal 来渲染其子元素到 #modal-root

 1// Modal.js
 2import React from "react";
 3import ReactDOM from "react-dom";
 4
 5class Modal extends React.Component {
 6  render() {
 7    // 使用 ReactDOM.createPortal 将子元素渲染到 modal-root 中
 8    return ReactDOM.createPortal(
 9      // 任何有效的 React 孩子元素
10      this.props.children,
11      // 一个 DOM 元素
12      document.getElementById("modal-root")
13    );
14  }
15}
16
17export default Modal;

现在,我们可以在应用程序的任何其他组件中使用这个 Modal 组件了,不论它们在 DOM 树中的位置如何:

 1// App.js
 2import React from "react";
 3import Modal from "./Modal";
 4
 5class App extends React.Component {
 6  constructor(props) {
 7    super(props);
 8    this.state = { showModal: false };
 9  }
10
11  handleShow = () => {
12    this.setState({ showModal: true });
13  };
14
15  handleClose = () => {
16    this.setState({ showModal: false });
17  };
18
19  render() {
20    return (
21      <div className="App">
22        <button onClick={this.handleShow}>显示模态框</button>
23
24        {this.state.showModal ? (
25          <Modal>
26            <div className="modal">
27              <div className="modal-content">
28                <h2>我是一个模态框!</h2>
29                <button onClick={this.handleClose}>关闭</button>
30              </div>
31            </div>
32          </Modal>
33        ) : null}
34      </div>
35    );
36  }
37}
38
39export default App;

在以上代码中,无论 Modal 组件在 App 组件中的位置如何,模态框的渲染位置总是在 #modal-root 中,这是一个典型的使用 React Portals 的例子。上述代码中的模态框在视觉上会覆盖整个应用程序的位置,但在组件层次结构中它仍然是 App 组件的子组件。

732.[React] react 和 react-dom 是什么关系?【热度: 197】【web 框架】【出题公司: 腾讯】

关键词:react 和 react-dom 关系

reactreact-dom 是两个与 React 生态系统密切相关的 npm 包,它们在使用 React 构建用户界面时扮演不同的角色:

  • react 包含了构建 React 组件所必需的核心功能,例如创建组件类(如 React.Component),创建元素(如使用 React.createElement),还有新的 React 16+ 特性中的 Hooks(如 useStateuseEffect)。

  • 它提供了组件生命周期管理、组件状态管理以及 React 元素(用于描述 UI 长相的对象)的创建。

  • react 实现了 React 的核心算法,包括对组件状态的更新以及虚拟 DOM 的概念。

  • 简而言之,react 包对于任何使用 React 的应用程序都是一个必需的依赖,无论该应用程序是运行在浏览器还是其他环境中。

  • react-dom 提供了一些让 React 能够与 DOM 互动的方法。在浏览器中,它把 React 组件渲染到真实的 DOM 节点上,并且处理用户的交互(如点击、输入等事件)。

  • 主要的方法是 ReactDOM.render(),它将 React 组件或者元素渲染到指定的 DOM 容器中。在 React 18+ 中,这个角色由 ReactDOM.createRoot().render() 接手。

  • 如果你在使用服务端渲染(Server-Side Rendering, SSR),那么你会使用 react-dom/server 中的方法,如 ReactDOMServer.renderToString()ReactDOMServer.renderToStaticMarkup()。这些方法允许你把 React 组件渲染成初始的 HTML 字符串。

  • 当 React 组件需要被集成到现有的非 React 应用中,或者需要执行如测试和服务端渲染等操作时,通常需要使用 react-dom 包。

React 使用了所谓的“适配器模式”(Adapter Pattern),react 包提供平台独立的解决方案,而像 react-dom 这样的包则提供针对特定平台的方法。这允许 React 的核心能够被跨平台使用,例如在浏览器(通过 react-dom)、移动设备(通过 React Native 的 react-native)、VR 设备(通过 react-vr)等。

当你在浏览器中构建 React 应用程序时,你通常会同时安装并使用这两个包。在引导你的应用程序时,你将使用 react 包来定义你的组件,然后用 react-dom 包将你的顶层组件渲染到页面中的 DOM 元素上。这样的分离也为服务器端渲染或在其他渲染目标上使用 React 打下了基础。

733.什么是 DNS 劫持?【热度: 165】【网络】【出题公司: 百度】

关键词:DNS 劫持

DNS 劫持(DNS Hijacking),也称为 DNS 重定向,是一种通过篡改原本的 DNS 解析流程,使得用户在尝试访问特定网址时被非法重定向到其他(通常是恶意的、广告相关的或者钓鱼的)网站的行为。这种攻击可以发生在用户的个人电脑、网络设备、甚至是直接在 DNS 服务器上。

DNS 劫持可以通过以下几种方式实现:

  1. 恶意软件

    • 用户的计算机被感染了恶意软件,该软件修改了本地的 DNS 设置,例如更改本地的 hosts 文件或 DNS 配置,使得所有或特定域名的请求都会被发送到攻击者指定的服务器。
  2. 篡改路由器设置

    • 攻击者通过各种手段(如默认密码、漏洞利用等)获取路由器的管理权限,并修改其上的 DNS 服务器设置,使得连接到该路由器的所有设备的 DNS 请求都会被重定向。
  3. DNS 服务器劫持

    • 攻击者直接对 DNS 服务器进行攻击,将规范域名的正确解析地址更改为恶意地址。
  4. 中间人攻击(Man-in-the-Middle Attack, MiTM)

    • 在用户与 DNS 服务器之间截获和修改 DNS 查询和响应,将用户请求重定向到另一个服务器。
  5. 网络服务提供商干预

    • 部分网络服务商出于广告和监管的目的,可能会在 DNS 层面上进行重定向,将无效域名或特定关键字的域名请求导向他们自己的服务器。

DNS 劫持对用户的主要威胁是隐私泄露和安全风险,用户有可能无意中访问到含有恶意软件的网页,导致个人信息泄露或者计算机安全受到威胁。为了防范 DNS 劫持,用户可以采取以下措施:

  • 使用可信赖的 DNS 服务,如 Google 的 8.8.8.8、Cloudflare 的 1.1.1.1 等。
  • 保持操作系统和防病毒软件都更新至最新状态, regularly scan for malware。
  • 对家用路由器设置复杂的登录密码,并定期进行固件更新。
  • 使用 VPN 服务,在密封的隧道中完成所有网络通信。
  • 对于重要的网站,最好使用书签直接访问,防止输入错误的 URL。
  • 启用 DNSSEC(Domain Name System Security Extensions),增加额外的验证步骤来保证 DNS 查询的安全。

734.站点如何防止爬虫?【热度: 554】【web 应用场景】【出题公司: 百度】

关键词:反爬虫

站点防止爬虫通常涉及一系列技术和策略的组合。以下是一些常用的方法:

在站点的根目录下创建或修改 robots.txt 文件,用来告知遵守该协议的爬虫应该爬取哪些页面,哪些不应该爬取。例如:

 1User-agent: *
 2Disallow: /

然而,需要注意的是遵守 robots.txt 不是强制性的,恶意爬虫可以忽视这些规则。

对于表单提交、登录页面等,使用验证码(CAPTCHA)可以防止自动化脚本或机器人执行操作。

服务器可以根据请求的用户代理(User-Agent)字符串来决定是否屏蔽某些爬虫。但用户代理字符串可以伪造,所以这不是一个完全可靠的方法。

分析访问者的行为,比如访问频率、访问页数、访问时长,并与正常用户的行为进行对比,从而尝试检测和屏蔽爬虫。

许多 Web 应用防火墙提供自动化的爬虫和机器人检测功能,可以帮助防止爬虫。

一些网站使用 JavaScript 服务端渲染,或将关键内容(比如令牌)动态地插入到页面中,这可以使得非浏览器的自动化工具获取网站内容变得更加困难。

一些站点要求每个请求都包括特定的 HTTP 头,这些头信息不是常规爬虫会添加的,而是通过 JavaScript 动态添加的。

如果探测到某个 IP 地址的不正常行为,就可以将该 IP 地址加入黑名单,阻止其进一步的访问。

通过限制特定时间内允许的请求次数来禁止爬虫执行大量快速的页面抓取。

对 API 使用率进行限制,比如基于用户、IP 地址等实施限速和配额。

使用 HTTPS 加密您的网站,这可以避免中间人攻击,并增加爬虫的抓取难度。

定期更改网站的 URL 结构、内容排版等,使得爬虫开发人员需要不断更新爬虫程序来跟进网站的改动。

735.git pull 和 git fetch 有啥区别?【热度: 355】【web 应用场景】【出题公司: 百度】

关键词:git pull 和 git fetch

git pullgit fetch 是 Git 版本控制系统中的两个基本命令,它们都用于从远程仓库更新本地仓库的信息,但执行的具体操作不同。

  • git fetch 下载远程仓库最新的内容到你的本地仓库,但它并不自动合并或修改你当前的工作。它取回了远程仓库的所有分支和标签(tags)。

  • 运行 git fetch 后,你可以在需要时手动执行合并操作(使用 git merge)或者重新基于远程仓库的内容进行修改。

  • fetch 只是将远程变更下载到本地的远程分支跟踪副本中,例如 origin/master

  • git pull 实际上是 git fetch 操作之后紧跟一个 git merge 操作,它会自动拉取远程仓库的新变更,并尝试合并到当前所在的本地分支中。

  • 当你使用 git pull,Git 会尝试自动合并变更。这可能会引起冲突(conflicts),当然冲突需要手动解决。

  • git pull 等价于执行了 git fetchgit merge FETCH_HEAD 的组合。

  • 当你仅仅想要查看远程仓库的变动而不立即合并到你的工作,可以使用 git fetch

  • 而当你想要立即获取远程的最新变动并快速合并到你的工作中,则可以使用 git pull

总之,git pull 是一个更加「激进」的命令,因为它自动将远程变更合并到你的当前分支,而 git fetch 更加「谨慎」,它只下载变更到本地,不做任何合并操作。

737.在 JS 中, 如何解决递归导致栈溢出问题?【热度: 269】【JavaScript】【出题公司: 小米】

关键词:栈溢出问题

在 JavaScript 中,递归如果执行过深,确实有可能导致“栈溢出(stack overflow)”错误,因为每次函数调用都会向调用栈中添加一个新的帧,而每个线程的调用栈都有其最大容量限制。当这个容量被超过时,就会发生栈溢出。为了解决这个问题,你可以使用几种不同的方法:

尾调用优化(Tail Call Optimization)

在 ES6 中,引入了尾调用优化。这意味着如果函数的最后一个操作是返回另一个函数的调用(即尾调用),那么这个调用可以在不增加新栈帧的情况下执行。但是,截至我知识更新的时间,大多数 JavaScript 引擎还没有实现这项优化,或者它在默认情况下并未激活。

大多数递归函数都可以重写为循环,这样可以避免调用栈问题。这种方法需要手动维护一个栈来存储必要的状态信息,而这个栈通常是存储在堆(heap)中的数组,不受调用栈大小限制。

例如,下面递归计算阶乘的代码:

 1function factorial(n) {
 2  if (n === 1) return 1;
 3  return n * factorial(n - 1);
 4}

可以重写为循环形式:

 1function factorial(n) {
 2  let result = 1;
 3  for (let i = 2; i <= n; i++) {
 4    result *= i;
 5  }
 6  return result;
 7}

Trampoline 是一个高阶函数,使您可以在递归调用的情况下避免栈溢出。它通过在每个递归步骤返回一个函数而不是值,然后持续调用这些函数,直到获得最终结果为止。

 1function trampoline(fn) {
 2  return function (...args) {
 3    let result = fn.apply(this, args);
 4
 5    while (typeof result === "function") {
 6      result = result();
 7    }
 8
 9    return result;
10  };
11}

然后,将原始递归函数改写为每次递归调用返回一个函数:

 1function recursiveFunction(args) {
 2  if (baseCase) {
 3    return finalValue;
 4  } else {
 5    return function () {
 6      return recursiveFunction(newArgs);
 7    };
 8  }
 9}
10
11const trampolinedFunction = trampoline(recursiveFunction);

调用 trampolinedFunction 会避免栈溢出,因为它不是真正的递归调用,只是同步循环调用那些返回的函数。

使用 ES6 的生成器(generator)和/或 Promises 也可以用来避免递归调用过深。这些特性可以帮助您生成异步递归调用,其允许事件循环(event loop)介入,避免单次执行过多递归调用造成的栈溢出。

将递归函数改造成异步函数(async function),并确保每一次递归调用都有机会返回控制权给 JavaScript 事件循环,这可以通过setTimeoutsetImmediate或者process.nextTick(在 Node.js 环境下)实现。

例如,可以将一个同步递归函数改写为:

 1function recursiveAsyncFunction(i) {
 2  if (i < 0) return Promise.resolve();
 3  console.log("Recursion ", i);
 4  return new Promise((resolve) => {
 5    setImmediate(() => {
 6      resolve(recursiveAsyncFunction(i - 1));
 7    });
 8  });
 9}

记得确保递归终止条件是正确的,否则即便以上方法也可能导致无限循环或者内存泄漏。每一种方法都有其适用场景,具体使用哪一种方法取决于问题的具体需求。

738.jsBridge 是什么?原理是啥?【热度: 220】【JavaScript】【出题公司: 小米】

关键词:jsBridge 原理

jsBridge是一种在 Web 开发中常用的技术,通常指的是 JavaScript Bridge 的缩写,它是一种在 Web 视图(如 WebView)和原生应用之间进行通信的机制。jsBridge 使得原生代码(如 Android 的 Java/Kotlin 或 iOS 的 Objective-C/Swift)能够与嵌入到 WebView 中的 JavaScript 代码相互调用和通信。

在具体实现上,jsBridge 的原理可能因平台而异,但大致的原理如下:

  1. 从 JavaScript 调用原生代码

    • 注册原生函数:首先,原生应用会在 WebView 中注册一些可以供 JavaScript 调用的方法或函数。
    • 调用原生函数:然后,JavaScript 可以通过特定的接口调用这些注册的原生方法。这通常是通过注入对象(例如,在 Android 中可以使用addJavascriptInterface方法)或监听特定的 URL scheme。
    • 消息传递:当 JavaScript 需要与原生应用通信时,它会发送消息(或调用方法),这个消息包含必要的指令和数据。
    • 原生处理:原生代码接收到这个消息后,会执行对应的指令,并将结果返回给 JavaScript(如果需要)。
  2. 从原生代码调用 JavaScript

    • 执行 JavaScript 代码:原生应用可以执行 WebView 中的 JavaScript 代码。例如,通过 WebView 的evaluateJavaScript(iOS)或loadUrl("javascript:...")(Android)方法。
    • 回调 JavaScript:原生应用还可以通过执行回调函数的方式,将数据或结果传递回 JavaScript。

jsBridge 在移动应用开发中尤为重要,因为它提供了一种方式来整合 Web 技术和原生应用功能,让开发者能够利用 Web 技术来编写跨平台的应用,同时还能够访问设备的原生功能,如相机、GPS 等。

这种机制特别适合于混合应用的开发,在这些应用中,部分界面和逻辑使用 Web 技术实现,而另一部分则利用原生代码以获取更好的性能和更丰富的设备功能支持。通过 jsBridge,两种不同的代码和技术可以互相协作,提供统一的用户体验。

740.vue 中 Scoped Styles 是如何实现样式隔离的, 原理是啥?【热度: 244】【CSS】【出题公司: 美团】

关键词:Scoped Styles 样式隔离

在 Vue 中,.vue 单文件组件的 <style> 标签可以添加一个 scoped 属性来实现样式的隔离。通过这个 scoped 属性,Vue 会确保样式只应用到当前组件的模板中,而不会泄漏到外部的其他组件中。

这个效果是通过 PostCSS 在构建过程中对 CSS 进行转换来实现的。基本原理如下:

  1. 当你为 <style> 标签添加 scoped 属性时,Vue 的加载器(比如 vue-loader)会处理你的组件文件。

  2. vue-loader 使用 PostCSS 来处理 scoped 的 CSS。它为组件模板内的每个元素添加一个独特的属性(如 data-v-f3f3eg9)。这个属性是随机生成的,确保唯一性(是在 Vue 项目构建过程中的 hash 值)。

  3. 同时,所有的 CSS 规则都会被更新,以仅匹配带有相应属性选择器的元素。例如:如果你有一个 .button 类的样式规则,它会被转换成类似 .button[data-v-f3f3eg9] 的形式。这确保了样式只会被应用到拥有对应属性的 DOM 元素上。

假设你在组件 MyComponent.vue 内写了如下代码:

 1<template>
 2  <button class="btn">Click Me</button>
 3</template>
 4
 5<style scoped>
 6  .btn {
 7    background-color: blue;
 8  }
 9</style>

vue-loader 将处理上述代码,模板中的 <button> 可能会渲染成类似下面的 HTML:

 1<button class="btn" data-v-f3f3eg9>Click Me</button>

CSS 则会被转换成:

 1.btn[data-v-f3f3eg9] {
 2  background-color: blue;
 3}

因此,.btn 类的样式仅会应用于拥有 data-v-f3f3eg9 属性的 <button> 元素上。

  • Scoped styles 提供了样式封装,但不是绝对的隔离。子组件的根节点仍然会受到父组件的 scoped CSS 的影响。在子组件中使用 scoped 可以避免这种情况。
  • Scoped CSS 不防止全局样式影响组件。如果其他地方定义了全局样式,它们仍然会应用到组件中。
  • 当使用外部库的类名时,scoped 可能会导致样式不被应用,因为它会期望所有匹配规则的元素都带有特定的属性。

总的来说,Scoped Styles 是 Vue 单文件组件提供的一种方便且有效的样式封装方式,通过 PostCSS 转换和属性选择器来实现组件之间的样式隔离。

741.[React] forwardsRef 作用是啥, 有哪些使用场景?【热度: 336】【web 框架】【出题公司: PDD】

关键词:forwardsRef 作用、forwardsRef 使用场景

在 React 中,forwardRef 是一个用来传递 ref 引用给子组件的技术。通常情况下,refs 是不会透传给子组件的,因为 refs 并不是像 props 那样的属性。forwardRef 提供了一种机制,可以将 ref 自动地通过组件传递到它的子组件。

  • 访问子组件的 DOM 节点: 当需要直接访问子组件中的 DOM 元素(例如,需要管理焦点或测量尺寸)时,可以使用 forwardRef
  • 在高阶组件(HOC)中转发 refs: 封装组件时,通过 forwardRef 可以将 ref 属性透传给被封装的组件,这样父组件就能够通过 ref 访问到实际的子组件实例或 DOM 节点。
  • 在函数组件中使用 refs(React 16.8+): 在引入 Hook 之前,函数组件不能直接与 refs 交互。但是,引入了 forwardRefuseRef 之后,函数组件可以接受 ref 并将它透传给子节点。

假设你有一个 FancyButton 组件,你想从父组件中直接访问这个按钮的 DOM 节点。

 1const FancyButton = React.forwardRef((props, ref) => (
 2  <button ref={ref} className="FancyButton">
 3    {props.children}
 4  </button>
 5));
 6
 7// 现在你可以从父组件中直接获取DOM引用
 8const ref = React.createRef();
 9<FancyButton ref={ref}>Click me!</FancyButton>;

一个常见的模式是为了抽象或修改子组件行为的高阶组件(HOC)。forwardRef可以用来确保 ref 可以传递给包装组件:

 1function logProps(Component) {
 2  class LogProps extends React.Component {
 3    componentDidUpdate(prevProps) {
 4      console.log("old props:", prevProps);
 5      console.log("new props:", this.props);
 6    }
 7
 8    render() {
 9      const { forwardedRef, ...rest } = this.props;
10
11      // 将自定义的 prop 属性 "forwardedRef" 定义为 ref
12      return <Component ref={forwardedRef} {...rest} />;
13    }
14  }
15
16  // 注意:React.forwardRef 回调的第二个参数 "ref" 传递给了LogProps组件的props.forwardedRef
17  return React.forwardRef((props, ref) => {
18    return <LogProps {...props} forwardedRef={ref} />;
19  });
20}

在 Hook 出现之前,函数组件不能够直接与 ref 交云。现在可以这样做:

 1const MyFunctionalComponent = React.forwardRef((props, ref) => {
 2  return <input type="text" ref={ref} />;
 3});
 4
 5const ref = React.createRef();
 6<MyFunctionalComponent ref={ref} />;

当你需要在父组件中控制子组件中的 DOM 元素或组件实例的行为时,forwardRef 是非常有用的工具。不过,如果可行的话,通常最好通过状态提升或使用 context 来管理行为,只在没有其他替代的情况下才选择使用 refs。

个人笔记记录 2021 ~ 2025