字节前端面试通关

react

1. React里面为什么不能写class,而一定要写成className,有探究过原因吗?

在React中,class是JavaScript的关键字,因此不能直接使用它来定义HTML元素的类名。为了避免冲突,React使用了className作为HTML元素的类名属性。

其实,这个问题不仅仅在React中存在。在原生JavaScript中,也不能使用class作为HTML元素的类名属性。解决方法也是一样的,需要使用className作为替代方案。

总之,这个问题的本质是JavaScript语言设计上的限制,并不是React框架特有的问题。

2. React组件里面,有哪几种操作会导致整个组件重新渲染?

1 组件的props属性发生了变化:当父组件传递给子组件的props发生改变时,子组件会重新渲染。
2 组件的state状态发生了变化:当组件内部调用setState()方法更新state时,组件会重新渲染。
3 父组件重新渲染:当父组件重新渲染时,所有子组件也会重新渲染。
4 强制更新:通过调用组件实例的forceUpdate()方法可以强制组件进行重新渲染。
5 上下文(Context)发生了变化:当使用React的上下文特性时,当上下文发生变化时,相关的组件会重新渲染。

需要注意的是,React使用了一种称为"虚拟DOM"的机制来优化渲染过程,只有真正需要更新的部分才会被重新渲染,而不是整个组件。这样可以提高性能并减少不必要的重渲染。

3. React的行内样式style,为什么会有2层花括号{{}}

在React的行内样式(inline style)中,有两层花括号的原因是因为它需要嵌套的 JavaScript 对象。
第一层花括号用于表示在 JSX 中插入 JavaScript 表达式。通过这种方式,我们可以在 JSX 中使用 JavaScript
变量和表达式。 第二层花括号用于表示一个 JavaScript 对象字面量,即表示行内样式的具体样式属性和值。在这个对象中,属性名是 CSS
属性名称,属性值可以是字符串或者其他 JavaScript 表达式(比如变量、函数调用等)。
例如,如果要设置一个元素的行内样式,可以这样写: jsx <div style={{ color: 'red', fontSize: '16px' }}>Hello, React!</div> 在上面的例子中,外层的花括号表示在 JSX 中插入 JavaScript
表达式,内层的花括号表示 JavaScript 对象字面量,其中包含了两个 CSS 属性(color 和
fontSize)及其对应的样式值。

这种双重花括号的语法是将 JavaScript 和 CSS 样式结合在一起的一种约定。它使得我们可以方便地在 JSX 中定义动态的行内样式。

4. 组件输出为什么要用key?

react 官方文档是这样描述 key 的:
keys 可以在 DOM 中的某些元素被增加或删除的时候帮助 React 识别哪些元素发生了变化。因此你应当给数组中的每一个子元素赋予一个缺点的标识。 
react 的 diff 算法是把 key 当成唯一 id 后对比组件的 value 来确定是否需要更新的,所以说如果没有 key, react 将不会知道如何更新组件。

虚拟 DOM 和 diff 算法
React 最为核心的就是 Virtual DOM 和 Diff 算法。React 在内存中维护一颗虚拟 DOM 树,当数据发生变化时(state & props),会自动地更新虚拟 DOM ,获得一个新的虚拟 DOM 树,然后通过 Diff 算法,比较新旧虚拟 DOM 树,找出最小的有变化的部分,将这个变化的部分(patch)加入队列 ,最终批量的更新这些 Patch 到实际的 DOM 中。

React diff 原理(大厂必问)
1.把树形结构按照层级分解,只比较同级元素;
2.给列表结构的每个单元添加唯一的 key 属性,方便比较;
3.React 只会匹配相同的 class 的 component (这里的 class 指的是组件的名字);
4.合并操作,调用 component 的 setState 方法的时候,React 将其标记为 dirty 到每一个事件循环结束。React 检查所有标记 dirty 的 component 重新绘制;
5.选择性子树渲染。

5. 简单说下props和state的作用和区别?

  1. props(属性)

props是一个组件的只读属性,是从父组件传递给子组件的值。它们可以用于定制组件的行为,例如设置样式、传递事件处理程序和参数等。
props是不可变的,只能从父组件传递给子组件,在子组件内部不能修改。这种限制使得React更容易跟踪数据的变化,并且可以更好地控制组件的行为。

  1. state(状态) state是一个组件的可变状态,它存储了组件内部的数据和状态。当状态发生变化时,React会自动重新渲染组件。 通过使用setState()方法来更新状态,React会在必要时更新组件的UI界面以反映新的状态。这种方式使得React非常高效地管理组件的状态和更新UI。

注意:尽量避免在组件中直接修改state,因为这可能会导致不正确的UI状态和性能下降。

  1. 区别 props和state的主要区别在于它们的可变性。props是只读的,它们从父组件传递给子组件,而state是可变的,它存储在组件内部,并且可以通过setState()方法进行修改。

另外,props通常用于定制组件的行为和样式,而state主要用于管理组件内部的数据和状态。因此,在设计React组件时,应该尽可能地将props和state分离,以便更好地组织和管理组件的代码。

6. getSnapshotBeforeUpdate是干什么的?它主要做了什么事?

getSnapshotBeforeUpdate是React组件生命周期中的一个方法,它通常用于在组件更新(render之后,DOM更新之前)时获取DOM的截屏,并将其传递给componentDidUpdate方法。
getSnapshotBeforeUpdate方法主要做了以下两件事情:

  1. 计算当前组件的属性和状态的变化 在组件更新之前,React会调用getSnapshotBeforeUpdate方法来计算当前组件的属性和状态的变化。这样可以使得我们在进行组件更新时能够更加精确地控制DOM的变化。

  2. 获取组件更新前的DOM快照 getSnapshotBeforeUpdate方法的另一个主要作用是获取组件更新前的DOM快照。通过对DOM快照的比较,我们可以确定哪些DOM节点已经更新,哪些仍然保持不变,从而避免不必要的DOM操作和提高性能。

需要注意的是,getSnapshotBeforeUpdate方法必须返回一个值或null。如果返回一个值,则该值将传递给componentDidUpdate方法作为第三个参数。当组件更新完成后,React会在componentDidUpdate方法中使用这个值来更新DOM,以确保组件的状态和UI是同步的。
总之,getSnapshotBeforeUpdate方法是React组件更新过程中的重要方法之一。它可以帮助我们在更新DOM之前获取快照并计算组件属性和状态的变化,从而更加精确地控制DOM的变化。

7. 为什么不能在componentDidUpdate中更新state?

在componentDidUpdate方法中更新state是不推荐的,因为这可能导致无限循环的更新。

componentDidUpdate是React组件生命周期中的一个方法,用于在组件完成更新后进行操作。当组件的state或props发生变化,并且组件重新渲染后,componentDidUpdate会被调用。

如果在componentDidUpdate方法中更新state,会导致再次触发组件的更新过程,从而再次调用componentDidUpdate方法,形成一个循环。这可能会导致性能问题和无限循环的错误。

通常情况下,componentDidUpdate方法用于处理DOM操作、网络请求、订阅事件等副作用操作,而不是用来更新state。如果需要根据state或props的变化来更新state,应该使用componentDidUpdate方法中的条件判断来避免无限循环的问题。

一种常见的做法是在componentDidUpdate方法中检查前后state或props的变化,并根据条件来更新state。同时,需要确保在更新state之前先进行条件判断,以防止陷入无限循环。

另外,如果需要根据props的变化来更新state,可以考虑使用componentDidUpdate方法之前的生命周期方法(例如componentDidMount或componentDidUpdate)来处理props的变化,并在这些方法中更新state。

总而言之,为了避免无限循环和性能问题,应该谨慎使用componentDidUpdate方法来更新state,而是将其用于处理副作用操作和非state的更新。

8.为什么不推荐在componentWillUnmount中销毁操作?

在React中,componentWillUnmount是组件生命周期中的一个方法,它在组件即将被卸载和销毁之前调用。通常情况下,我们可以在componentWillUnmount中执行一些清理操作,例如取消订阅、清除定时器、释放资源等。

然而,不推荐在componentWillUnmount中进行复杂的销毁操作的原因有以下几点:

  1. 异步操作可能未完成:如果在componentWillUnmount中执行一些异步操作(例如发送网络请求),由于组件即将被销毁,这些操作可能无法完成或得到正确的处理。这可能导致内存泄漏或其他错误。

  2. 可能会影响性能:在componentWillUnmount中执行耗时的销毁操作可能会影响组件的卸载速度,从而降低应用程序的性能。在用户界面中,我们通常希望快速地卸载组件以提供良好的用户体验。

  3. 不符合最佳实践:根据React的最佳实践,应该尽量将复杂的销毁操作放在组件生命周期的早期阶段,例如在componentWillUnmount之前的生命周期方法中执行。这样可以确保在componentWillUnmount中只进行简单且必要的清理操作。

为了避免上述问题,可以考虑在组件的早期生命周期方法(例如componentDidMount)中进行初始化操作,并在componentWillUnmount中进行简单的清理操作。对于复杂的销毁操作,可以考虑使用专门的管理工具或库,例如React的Context
API或Redux来处理。

总结而言,尽量避免在componentWillUnmount中执行复杂的销毁操作,以防止异步操作未完成、影响性能和不符合最佳实践。将复杂的销毁操作放在组件生命周期的早期阶段,并在componentWillUnmount中进行简单的清理操作是更好的做法。

9. react中的ref和Vue中的ref有什么不同?

React中的ref和Vue中的ref在概念和用法上有一些不同之处。

在React中,ref是一个属性,用于引用组件或DOM元素。它可以被用于访问组件或DOM元素的实例,从而进行一些操作,例如获取元素的值、调用组件的方法等。ref通常通过使用React.createRef()函数创建,并通过ref属性将其绑定到组件或DOM元素上。

示例:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }

  componentDidMount() {
    console.log(this.myRef.current); // 访问组件或DOM元素的实例
    this.myRef.current.focus(); // 调用组件或DOM元素的方法
  }

  render() {
    return <input ref={this.myRef} />;
  }
}

在Vue中,ref也是一个特殊的属性,但其用法更为灵活。在Vue模板中,ref可以直接在DOM元素上使用,或者在组件上使用。在DOM元素上使用ref时,ref会引用该元素的DOM节点。在组件上使用ref时,ref引用的是组件实例。

示例:

<template>
  <div>
    <input ref="myRef" />
    <my-component ref="myComponentRef"></my-component>
  </div>
</template>

<script>
import MyComponent from './MyComponent.vue';

export default {
  mounted() {
    console.log(this.$refs.myRef); // 访问DOM元素的引用
    console.log(this.$refs.myComponentRef); // 访问组件实例的引用
    this.$refs.myRef.focus(); // 在DOM元素上调用方法
    this.$refs.myComponentRef.doSomething(); // 在组件上调用方法
  },
  components: {
    MyComponent,
  },
};
</script>

需要注意的是,Vue中的ref是在组件实例或DOM元素上创建的引用,而React中的ref是在组件实例或DOM元素上创建的引用的容器。因此,在访问Vue中的ref时,可以直接使用this.$refs来获取引用;而在React中,需要使用this.myRef.current来获取引用。

另外,Vue还提供了$refs属性的响应式能力,当ref所引用的组件或DOM元素发生变化时,$refs会自动更新。而React中的ref不具有响应式能力,需要手动更新。

总结而言,React中的ref和Vue中的ref都是用于引用组件或DOM元素的特殊属性,但在概念和用法上有一些差别。

10. 字符串的ref和createRef的区别?

字符串形式的ref和createRef()形式的ref的区别如下:

字符串形式的ref不安全:字符串形式的ref存在安全隐患,因为它可以使用任意字符串作为引用名称。如果使用了非法字符或重复的引用名称,则可能会出现不可预料的错误。而createRef()形式的ref是类型安全的,因为它是通过调用函数来创建引用的,不需要手动输入引用名称。

字符串形式的ref不建议使用:尽管字符串形式的ref在React早期版本中可以使用,但在React
16.3及以后的版本中已被弃用。官方推荐使用createRef()形式的ref。

综上所述,createRef()形式的ref比字符串形式的ref更安全、更易于使用,且是官方推荐的方式。因此,在新开发的React项目中,应该尽量避免使用字符串形式的ref。

11. Context的作用是什么,简单说下contextType、useContext、Consumer三种方式

Context是React中用于在组件之间共享数据的一种机制。它可以避免将数据通过多层组件传递,使得组件之间可以直接访问共享的数据。

contextType是一种在类组件中使用Context的方式。通过在类组件中定义静态属性contextType,并设置为对应的Context对象,可以在组件的实例中访问该Context的值。这样,组件就可以直接通过this.context来获取共享的数据。

示例:

const MyContext = React.createContext();

class MyClassComponent extends React.Component {
  static contextType = MyContext;

  componentDidMount() {
    console.log(this.context); // 访问共享的数据
  }

  render() {
    return <div>{this.context}</div>;
  }
}

useContext是一种在函数组件中使用Context的方式。它是React Hooks提供的一个钩子函数,用于获取指定Context的值。通过调用useContext,并传入对应的Context对象,可以在函数组件中获取共享的数据。

示例:

const MyContext = React.createContext();

function MyFunctionComponent() {
  const contextValue = useContext(MyContext);

  useEffect(() => {
    console.log(contextValue); // 访问共享的数据
  }, []);

  return <div>{contextValue}</div>;
}

Consumer是一种在类组件或函数组件中使用Context的方式。通过Context.Consumer组件,可以在其内部使用一个函数,该函数接收共享的数据作为参数,并返回需要渲染的内容。Consumer可以嵌套使用,以实现在组件树中的任何地方获取共享的数据。

示例:

const MyContext = React.createContext();

class MyClassComponent extends React.Component {
  render() {
    return (
      <MyContext.Consumer>
        {(contextValue) => (
          <div>{contextValue}</div> // 访问共享的数据并渲染内容
        )}
      </MyContext.Consumer>
    );
  }
}

function MyFunctionComponent() {
  return (
    <MyContext.Consumer>
      {(contextValue) => (
        <div>{contextValue}</div> // 访问共享的数据并渲染内容
      )}
    </MyContext.Consumer>
  );
}

以上是使用contextType、useContext和Consumer三种方式在组件中获取共享数据的简单介绍。它们都可以用于在组件之间传递共享的数据,具体使用哪种方式取决于你的项目需求和个人喜好。

12. 什么是fiber,为什么需要fiber?

Fiber是React
16中引入的一种协调机制,用于实现React的异步渲染和增量更新。它是一种算法和数据结构的组合,用于管理组件的调度、渲染和协调过程。

在React早期版本中,更新组件时是同步进行的,即一旦开始更新,就必须完成所有的工作,包括递归遍历整个组件树、计算差异并更新DOM。这种同步的更新方式在大型应用中可能会导致阻塞主线程,造成用户界面不流畅,甚至出现卡顿的情况。

为了解决这个问题,React团队引入了Fiber架构。Fiber将组件的更新过程切割成多个优先级较低的小任务(称为Fiber节点),并使用调度器来决定哪些任务应该执行,以及何时执行。Fiber的设计目标是实现可中断和可恢复的渲染过程,使得React能够在渲染过程中优先处理高优先级的任务,同时允许中断和恢复渲染过程,以便处理更紧急的任务,例如用户交互或动画。

Fiber的好处有:

  1. 更好的用户体验:通过将渲染过程拆分成多个任务,React可以更好地响应用户的输入和操作,提供更流畅的用户体验。

  2. 可中断和可恢复的渲染过程:Fiber架构允许React在渲染过程中中断当前任务,并优先处理更紧急的任务。这使得React能够更好地适应不同的设备和网络环境。

  3. 资源分配和任务调度:Fiber使用优先级调度算法来决定哪些任务应该在何时执行。通过合理地分配资源和调度任务,可以提高应用程序的性能和响应能力。

总而言之,Fiber是React引入的一种新的协调机制,通过实现异步渲染和增量更新,提供更好的用户体验和性能。它是为了解决同步更新导致的阻塞和卡顿问题而引入的,使得React能够更好地适应各种场景和优化应用程序的性能。

13. 什么是bailing out 机制?

Bailing out机制是React在进行渲染时的一种优化策略,用于避免不必要的组件渲染和更新。

在React中,当父组件的状态或属性发生变化时,通常会重新渲染整个子组件树,即使某些子组件的属性或状态没有变化。这样做可能会导致不必要的性能损耗,因为React需要递归遍历整个子组件树,并计算出每个组件的差异,以便更新DOM。

为了避免这种性能损耗,React引入了Bailing
out机制。该机制会在渲染过程中对组件进行判断,如果组件的属性或状态没有发生改变,则跳过其渲染和更新过程,直接使用之前的结果。这种方式称为“bail
out”,即“退出”或“跳过”。

具体来说,React会通过比较前后两次渲染的props和state,来判断是否需要更新组件。如果props和state都没有发生变化,则React会认为组件没有必要更新,并跳过其渲染和更新过程。这样可以大大提高应用程序的性能和响应能力。

需要注意的是,Bailing
out机制并不是所有情况下都适用。例如,当组件的props和state包含引用类型的数据结构时,即使其属性值没有发生变化,也可能存在内部状态的改变。在这种情况下,React仍需要进行渲染和更新操作。

总而言之,Bailing
out机制是React用于优化组件渲染和更新过程的一种策略,通过避免不必要的组件渲染和更新,提高应用程序的性能和响应能力。

14. 为什么所有的hook,一定要写在顶层,而不能放在if、for、while中?

这是因为Hook的设计和实现机制决定了它们必须在组件的顶层进行调用。将所有的Hook放在函数组件的顶层使用是为了保证React能够正确管理和维护Hook的状态,并确保组件的行为一致性和可预测性。这是基于JavaScript闭包和调用顺序的规则所决定的。

15. useLayoutEffect和useEffect的区别?

useLayoutEffectuseEffect是React中两个常用的Hook函数,它们的主要区别在于触发时机。

  1. useEffect: useEffect是在组件渲染完成后(包括首次渲染和每次更新)执行的副作用函数。它在浏览器完成绘制之后才会执行,不会阻塞页面的渲染,因此适用于处理异步操作、数据获取、订阅或取消订阅等不需要立即执行的副作用。由于useEffect的执行是异步的,所以可能会导致视觉上的闪烁或延迟。

  2. useLayoutEffect: useLayoutEffectuseEffect非常相似,但它会在所有DOM变更(即浏览器执行布局和绘制之前)完成后同步执行。这意味着useLayoutEffect会阻塞页面的渲染,直到副作用函数执行完毕。由于useLayoutEffect的执行是同步的,所以可以在其中直接操作DOM,获取布局信息并立即更新,以避免不必要的布局计算和回流,从而提供更好的用户体验。

需要注意的是,使用useLayoutEffect可能会导致性能问题,特别是在涉及大量计算或操作DOM的情况下。因为它会阻塞页面渲染,所以应该谨慎使用,并确保副作用函数的执行时间尽量短。

综上所述,useEffect适用于大多数情况下的副作用处理,而useLayoutEffect适用于需要在DOM变更之后立即执行的副作用操作,特别是涉及到DOM操作和布局相关的场景。在选择使用哪个Hook时,根据具体的需求和副作用的触发时机来进行选择。

16. react-router中有哪些路由,简单的说下他们的作用和场景?

React Router是React官方推荐的一款路由库,提供了多种路由实现方式。

  1. BrowserRouter: BrowserRouter使用HTML5的History API来实现前端路由,支持浏览器刷新、后退、前进等操作,并且能够保持URL与当前页面的同步。适用于需要在前端实现完整的路由功能的场景。

  2. HashRouter: HashRouter使用URL hash值来实现前端路由,它不需要使用HTML5的History API,因此可以在所有现代浏览器中使用,并且不会受到浏览器历史记录的限制。适用于需要兼容旧版浏览器或无法控制服务器配置的场景。

  3. MemoryRouter: MemoryRouter将路由信息存储在内存中,不会改变浏览器URL,适用于需要在内存中管理路由状态的场景,如测试和非浏览器环境下的渲染。

  4. StaticRouter: StaticRouter用于服务器端渲染,它将路由信息作为props传递给组件,而不是依赖于浏览器URL。适用于需要在服务器端生成静态HTML的场景。

  5. NavLink: NavLink是一个特殊的链接组件,当它被点击时,它会自动添加active类名,以高亮当前活动的链接。适用于需要实现导航栏、标签页等UI组件的场景。

  6. Route: Route是最基本的路由组件,它用于匹配对应的URL,并渲染对应的组件。Route可以嵌套使用,支持动态路由和嵌套路由等高级功能。适用于需要动态加载组件、控制组件渲染、实现复杂路由逻辑的场景。

综上所述,React
Router提供了多种路由实现方式和相关组件,可以满足不同的需求和场景。开发者可以根据具体的项目需求和技术栈选择适合自己的路由实现方式和组件。

Javascript

1. 简单说下js中的变量提升

JavaScript中的变量提升(Hoisting)是指在代码执行过程中,变量和函数的声明会被提升到当前作用域的顶部,而不论它们实际上是在哪里被声明的。

具体来说,JavaScript引擎在执行代码之前会进行两个步骤:

  1. 变量声明提升:JavaScript引擎会将所有使用var关键字和函数声明的变量提升到当前作用域的顶部,并赋予默认值undefined。这意味着你可以在变量声明之前访问它们。
  2. 函数声明提升:JavaScript引擎会将所有函数声明提升到当前作用域的顶部。这意味着你可以在函数声明之前调用它们。

例如,以下代码中的变量提升示例:

console.log(a); // undefined
var a = 10;

console.log(b); // ReferenceError: b is not defined
let b = 20;

foo(); // "Hello, foo!"
function foo() {
  console.log("Hello, foo!");
}

在第一个例子中,变量a被提升到作用域的顶部,但它的值在声明之前为undefined,因此第一行输出结果为undefined

在第二个例子中,使用let关键字声明的变量b不会被提升,所以在使用之前访问它会抛出ReferenceError错误。

在第三个例子中,函数声明foo()被提升到作用域的顶部,因此在调用foo()之前就可以访问它。

需要注意的是,只有变量声明(使用varletconst)和函数声明会被提升,而变量赋值和函数表达式不会被提升。因此,在开发中建议始终在作用域的顶部声明变量和函数,以避免可能的混乱和错误。

2. 作用域链和闭包又有什么关系?

作用域链和闭包在 JavaScript 中是密切相关的概念。


1 作用域链是指变量和函数在嵌套的作用域中查找的机制。当在一个作用域中访问一个变量或函数时,JavaScript
引擎首先在当前作用域中查找,如果找不到,则会向上一级作用域继续查找,直到找到该变量或函数或达到全局作用域。这个 由多个作用域形成的链路就是作用域链。
-----------------------

2 闭包是指函数能够访问并持有其词法作用域中的变量,即使在函数定义之后,它仍然可以访问那些变量。闭包通过将函数及其相关的词法环境(即作用域)包裹在一起实现,使得函数内部的变量在函数执行完毕后依然存在。
-----------------------
闭包的实现依赖于作用域链的特性。当一个函数被定义时,它会创建一个闭包,其中包含了函数自身及其词法环境中的变量。当该函数在其定义的作用域之外被调用时,它仍然可以访问和操作定义时的作用域中的变量,因为作用域链会保留对这些变量的引用。

下面是一个使用闭包的示例:

function outerFunction() {
  var outerVar = "Hello, ";

  function innerFunction(name) {
    console.log(outerVar + name);
  }

  return innerFunction;
}

var greeting = outerFunction();
greeting("Alice"); // 输出:Hello, Alice

在这个例子中,outerFunction 定义了一个内部函数 innerFunction,内部函数可以访问外部函数的变量 outerVar。当 outerFunction 被调用时,它返回了 innerFunction,并赋值给变量 greeting。此时,greeting 成为了一个闭包,它持有了 outerFunction 定义时的作用域,包括其中的变量 outerVar。当后续调用 greeting 时,它依然能够访问和使用 outerVar 变量。

因此,作用域链提供了闭包的实现基础,允许函数在其定义的作用域之外访问和操作变量。闭包使得 JavaScript 具备了强大的能力,例如实现私有变量、模块化等。

3. new的原理是什么?

new 是 JavaScript 中用于创建对象实例的关键字。它的原理可以分为以下几个步骤:

  1. 创建一个空对象:new 关键字会创建一个空对象,作为要创建的对象实例。

  2. 将空对象的原型指向构造函数的原型:新创建的对象实例会继承构造函数的原型上的属性和方法。使用 Object.setPrototypeOf() 将新对象的原型指向构造函数的 prototype 属性。

  3. 将构造函数的 this 绑定到新对象:通过使用 Function.prototype.call()Function.prototype.apply() 方法来将构造函数内部的 this 指向新创建的对象。

  4. 执行构造函数的代码:在新创建的对象上执行构造函数内部的代码。这样,构造函数就可以对新创建的对象进行初始化操作,并添加属性和方法。

  5. 返回新对象:如果构造函数没有显式地返回一个对象,则 new 表达式会隐式地返回新创建的对象实例。如果构造函数显式地返回一个对象,则返回该对象,而不是新创建的对象。

下面是一个简单的示例来说明 new 的原理:

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

Person.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.name);
};

var john = new Person("John", 25);
john.sayHello(); // 输出:Hello, I'm John

在这个例子中,new Person("John", 25) 创建了一个新的对象实例 johnnew
关键字会执行上述的步骤,首先创建一个空对象 john,然后将 john 的原型指向 Person.prototype,接着将
this 绑定到新对象 john 上,并执行构造函数 Person 的代码进行初始化,最后返回新对象 john

通过使用 new 关键字,我们可以创建多个具有相同属性和方法的对象实例,这是面向对象编程中的常见需求。

4. this的原理是什么,怎么控制this的指向?

在 JavaScript 中,this 关键字用于指向当前执行上下文的对象。它的值在函数调用时确定,可以通过不同的方式来控制
this 的指向。

this 的指向规则如下:

  1. 默认绑定:在独立函数调用时(非严格模式下),this 默认指向全局对象(浏览器环境中为 window 对象,Node.js 环境中为 global 对象);在严格模式下,默认绑定会将 this 设置为 undefined

  2. 隐式绑定:当函数作为对象的方法被调用时,this 会指向调用该方法的对象。

  3. 显式绑定:通过 call()apply()bind() 方法,可以显式地指定函数的 this 值。

  4. new 绑定:使用 new 关键字创建对象实例时,this 会指向新创建的对象。

  5. 箭头函数绑定:箭头函数没有自己的 this 值,它会捕获上层作用域中的 this 值,即词法作用域。

以下是一些示例来说明如何控制 this 的指向:

// 默认绑定
function sayName() {
  console.log(this.name);
}

var person = {
  name: "John",
  sayName: sayName
};

person.sayName(); // 输出:John

// 显式绑定
function sayAge() {
  console.log(this.age);
}

var person2 = {
  age: 25
};

sayAge.call(person2); // 输出:25

// new 绑定
function Person(name) {
  this.name = name;
}

var john = new Person("John");
console.log(john.name); // 输出:John

// 箭头函数绑定
var obj = {
  name: "Alice",
  sayHello: () => {
    console.log("Hello, " + this.name);
  }
};

obj.sayHello(); // 输出:Hello, undefined

在这些示例中,我们使用了不同的方式来控制 this 的指向。通过选择合适的绑定方式,我们可以灵活地控制函数内部 this
的值,以满足不同的需求。

5. bind和call、apply最大的区别是什么?

bind()call()apply() 都是用于显式绑定函数的 this 值,但它们之间有几个重要的区别:

  1. 参数传递方式:

    • call(thisArg, arg1, arg2, ...)call() 方法接受一个指定的 this 值和可选的参数列表,参数以逗号分隔。
    • apply(thisArg, [arg1, arg2, ...])apply() 方法接受一个指定的 this 值和一个数组(或类数组对象),数组中的元素作为参数传递给函数。

    例如: ```javascript function greet(name) {
    console.log("Hello, " + name); }

    greet.call(null, “John”); // 使用 call() 调用函数并传递参数
    greet.apply(null, [“John”]); // 使用 apply() 调用函数并传递参数 ```

    call()apply() 的主要区别在于参数的传递方式,call() 是逐个传入参数,而 apply()
    则是通过数组传递参数。

  2. 返回值:

    • bind(thisArg, arg1, arg2, ...)bind() 方法返回一个新函数,该函数的 this 值被绑定到指定的 thisArg,并且可以传入可选的参数列表。
      例如:
function greet(name) {
  console.log("Hello, " + name);
}

var boundFunc = greet.bind(null, "John"); // 使用 bind() 绑定函数和参数
boundFunc(); // 调用绑定后的函数

bind() 方法会创建一个新函数,并返回这个新函数,而不是立即调用它。通过这样做,我们可以在稍后的时候调用新函数。

  1. 立即调用与延迟调用:
    • call()apply() 方法在调用时会立即执行函数,并传递指定的 this 值和参数。
    • bind() 方法不会立即执行函数,而是返回一个新函数,我们可以在需要的时候再进行调用。

总结来说,call()apply() 是立即调用函数并传递参数,而 bind()
是创建一个新函数并延迟调用。它们的选择取决于需要的具体场景。

6. 简单说下Event loop事件循环机制

Event
Loop(事件循环)是一种程序执行结构,用于在等待事件和发送事件之间进行切换。它在JavaScript中非常重要,因为JavaScript是单线程的,事件循环使它可以进行非阻塞I/O操作(如读写文件、执行耗时的计算任务等)。

事件循环的工作流程大致如下:

  1. 首先,JavaScript会先执行同步代码,这些代码被称为执行栈。

  2. 在执行完所有的同步代码后,JavaScript会不断地查看是否有异步代码需要执行,这个过程就是事件循环。

  3. 异步代码包括setTimeout和promise.then这类API的回调函数,它们会被放到一个任务队列中。

  4. 一旦执行栈为空,JavaScript引擎就会去任务队列中读取事件,然后转入执行栈开始执行。

  5. 上述过程会不断重复,也就是我们说的“循环”。这就是JavaScript的Event Loop(事件循环)。

事件循环机制使得JavaScript虽然是单线程,但能够异步处理任务,提高了效率和用户体验。

7. 简单说下settimeout、promise、async/await的区别?

  1. setTimeout:这是一个异步操作,允许我们在指定的时间后执行某个函数或一段代码。注意,它只是将任务排入事件队列,不保证在指定时间后立即执行。

  2. Promise:Promise对象表示一个异步操作的最终完成或失败。它是一个返回值(或抛出错误)的代理,这个值(或错误)可能是现在已经确定的,也可能是将来才会确定。它有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。通过then、catch和finally方法,我们可以对成功或失败的结果进行处理。

  3. async/await:async/await是ES2017引入的新特性,它是Promise的语法糖,让异步操作看起来更像同步操作。async用于声明一个函数是异步的,而await则用在async函数内部,等待Promise的解决,并返回其值。注意,await只能在async函数内部使用。

以上三者都可以用于处理异步操作,但各有侧重。setTimeout更侧重于定时操作,Promise更侧重于异步结果的处理,而async/await则提供了一种更简洁、易读的方式来处理异步操作。

8. 什么是防抖,什么是节流?他们的区别是什么,分别应用到哪种场景?

防抖(Debounce)和节流(Throttle)都是用于控制函数执行频率的技术。

防抖:在事件触发后,等待一定时间再执行回调函数。如果在等待时间内又触发了该事件,那么重新计时,等待时间重新开始计算。防抖可以防止函数被频繁调用,常用于输入框输入事件、窗口调整事件等。

节流:在事件触发后,固定时间间隔内只执行一次回调函数。如果在固定时间间隔内又触发了该事件,那么忽略该次事件,等待下一个固定时间间隔再执行回调函数。节流可以控制函数的执行频率,常用于滚动事件、按钮点击事件等。

区别:

  1. 防抖会在等待时间结束后执行一次回调函数,而节流会在固定时间间隔内定时执行回调函数。
  2. 防抖在等待时间内如果又触发了事件,会重新计时,而节流在固定时间间隔内无论触发多少次事件,只会执行一次回调函数。

应用场景:

  • 防抖适用于需要等待用户输入完毕后再执行某个操作的场景,如搜索框输入事件、窗口调整事件等。
  • 节流适用于需要控制函数执行频率的场景,如滚动事件、按钮点击事件等。

需要根据具体的需求来选择使用防抖或节流,以达到更好的用户体验和性能优化。

9. nomouseover out 和 onmouseenter leave的区别是什么?

nomouseover和mouseout是DOM事件,而onmouseenter和onmouseleave是特定于IE的DOM事件。它们之间的区别如下:

  1. nomouseover和mouseout:
  • nomouseover事件在鼠标指针从元素上方移出时触发,不论鼠标指针是否进入到元素的子元素。
  • mouseout事件在鼠标指针从元素上方移出时触发,但是如果鼠标指针进入到元素的子元素,则不会触发mouseout事件。
  1. onmouseenter和onmouseleave:
  • onmouseenter事件在鼠标指针从元素外部移入时触发,不论鼠标指针是否进入到元素的子元素。
  • onmouseleave事件在鼠标指针从元素内部移出时触发,但是如果鼠标指针进入到元素的子元素,则不会触发onmouseleave事件。

总结:
nomouseover和mouseout在鼠标进入元素的子元素时也会触发事件,而onmouseenter和onmouseleave不会。因此,onmouseenter和onmouseleave更适合于只关心鼠标进入或离开元素本身的情况,而nomouseover和mouseout则适用于需要考虑鼠标进入和离开元素及其子元素的情况。需要注意的是,onmouseenter和onmouseleave是特定于IE的事件,而其他浏览器一般使用mouseover和mouseout事件来处理类似的情况。

10. onload和DOMContentLoaded是什么?

onload和DOMContentLoaded是两个不同的事件,用于表示页面加载的不同阶段。

  1. onload事件:当整个页面及其所有资源(包括图片、样式表、脚本等)都加载完成后,触发onload事件。可以将onload事件绑定到window对象上或某个具体元素上。通常用于在页面完全加载后执行一些操作,例如初始化页面、加载动态内容等。

示例代码: window.onload = function() { // 页面加载完成后执行的代码 };

  1. DOMContentLoaded事件:当初始的HTML文档被完全加载和解析后,即DOM树构建完成时,触发DOMContentLoaded事件。可以将DOMContentLoaded事件绑定到document对象上或某个具体元素上。与onload事件不同,DOMContentLoaded事件在页面加载过程中即可触发,无需等待所有资源加载完成。

示例代码: document.addEventListener('DOMContentLoaded', function() { // DOM树构建完成后执行的代码 });

区别:

  • onload事件要等待所有资源加载完成,包括图片、样式表、脚本等,才会触发。而DOMContentLoaded事件只需要等待HTML文档解析完成,即DOM树构建完成,就会触发。
  • 因为DOMContentLoaded事件触发时,可能还有一些外部资源(如图片)未加载完成,所以它比onload事件更早触发,页面可能还没有完全呈现出来。

一般来说,如果只需要在DOM树构建完成后执行一些操作,可以使用DOMContentLoaded事件。如果需要等待页面中的所有资源都加载完成后再执行操作,可以使用onload事件。

11. 简单说下加载JS时间线

JavaScript 的加载和执行是按照一定的时间线进行的,可以简单描述为以下几个阶段:

  1. 加载:当浏览器解析到 <script> 标签或通过其他方式引入 JavaScript 文件时,会开始加载 JavaScript 代码。浏览器会发起网络请求,将 JavaScript 文件下载到本地。

  2. 解析:在加载过程中,浏览器会逐行解析 JavaScript 代码,并将其转化为可执行的语法树。这个过程中,如果遇到依赖其他文件的模块或库,浏览器会按需加载这些文件,并等待它们下载完成。

  3. 执行:当 JavaScript 文件被解析完毕后,解析器会执行其中的代码。代码的执行顺序是按照加载顺序来的,从上到下逐行执行。如果代码中包含函数调用、事件监听等异步操作,这些异步操作会被添加到事件队列中,等待执行。

  4. 事件循环:JavaScript 引擎会不断地从事件队列中取出待执行的任务,并加入到调用栈中执行。这个过程称为事件循环(Event Loop)。事件循环会不断重复,直到事件队列为空。

需要注意的是,由于浏览器是多线程的,JavaScript 执行的过程中还要考虑与其他线程(如渲染线程)的交互。例如,当浏览器执行
JavaScript 代码时,如果遇到 DOM 操作或页面重绘等任务,它们会被放入渲染队列中,等待渲染线程的执行。

总结来说,JavaScript
的加载和执行是一个异步的过程,通过事件循环机制来协调代码的执行顺序。通过合理地管理加载、解析和执行的过程,可以提高页面的性能和用户体验。

12. 什么是base64?

Base64 是一种用于将二进制数据转换为 ASCII
字符串的编码方式。它是一种常用的数据格式,用于在文本协议和传输中以文本形式表示二进制数据。

在 Base64 编码中,每三个字节(3 * 8 = 24 bits)被编码为四个字符(4 * 6 = 24
bits)。如果原始数据字节数不是 3 的倍数,则会进行填充。

Base64 使用了64个不同的字符来表示编码后的数据,这些字符包括大小写字母、数字和两个特殊字符(+ 和 /),具体字符集可能会略有差异。

编码过程如下:

  1. 将原始数据划分为每 3 个字节一组。
  2. 将每组 3 个字节转换为 4 个 6 位的数字。
  3. 根据对应的索引表,将每个 6 位的数字映射到相应的字符。
  4. 若原始数据字节数不是 3 的倍数,则会进行填充,一般使用字符 “=”。

Base64 编码主要有以下特点:

  1. 可以将任意二进制数据编码为纯文本格式,方便在文本协议中传输。
  2. 编码后的数据长度会比原始数据稍长,增加了约 33% 的大小。
  3. Base64 可能会在 URL 中出现一些特殊字符(+、/、=),需要进行转义或处理。

Base64
主要应用于网络传输中,例如在邮件中传输二进制附件、在网页中嵌入图片或字体文件等。另外,也常用于存储和表示一些需要以文本形式展示的二进制数据。

13. 为什么转换成base64后体积会膨胀?

转换成 Base64 后体积增大的原因主要有两个方面:

  1. 编码方式:Base64 将每 3 个字节的数据编码为 4 个字符,相当于将原始数据按照 8 位(1 字节)划分为 6 位(6 比特)的数据块。由于 2^6 = 64,所以 Base64 使用了 64 个不同的字符来表示这 6
    位数据。因此,编码后的数据长度会比原始数据稍长,增加了约 33% 的大小。

  2. 填充:如果原始数据的字节数不是 3 的倍数,Base64 编码会进行填充操作,通常使用字符 “=” 进行填充。填充操作使得最终的编码结果长度能够被 4 整除。填充字符会增加编码后数据的长度。

举个例子来说明,假设原始数据长度为 n 字节,则:

  • 如果 n 是 3 的倍数,Base64 编码后的数据长度为 (4 * n) / 3;
  • 如果 n 不是 3 的倍数,Base64 编码后的数据长度为 ((4 * (n / 3)) + 4)。

因此,对于较大的原始数据,转换成 Base64 后会导致数据体积的膨胀。但是在实际应用中,Base64
主要用于在文本协议中传输二进制数据,而不是为了减小体积。相比于二进制数据直接传输,Base64
编码后的数据可以保证数据可读性和传输的稳定性。

  • 18
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值