IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    web每日一题(不断更新中)

    夕水发表于 2024-08-06 14:51:34
    love 0

    前言

    这是一个每天更新一题的文章,欢迎评论区网友提供题目和答案,不断更新中,记录下来,方便学习,参考答案如有问题,敬请批评指正,废话不多说,开始进入正文。

    第一题

    题目

    1. 判断一个对象是否为空,包含了其原型链上是否有自定义数据或者方法。 该如何判定?

    参考解答

    参考解答:

    要判断一个对象是否为空,并且考虑到对象自身及其原型链上是否有自定义数据或方法,您可以使用以下的 JavaScript 函数来实现:

    function isObjectEmpty(obj) {
      // 首先获取对象自身的属性
      const ownProperties = Object.getOwnPropertyNames(obj);
      // 遍历自身属性
      for (const property of ownProperties) {
        const descriptor = Object.getOwnPropertyDescriptor(obj, property);
        // 如果属性是数据属性并且有值,或者是方法(可调用函数),则对象不为空
        if (
          (descriptor.value && descriptor.value !== null && descriptor.value !== undefined) ||
          typeof descriptor.value === "function"
        ) {
          return false;
        }
      }
      // 获取对象的原型
      const prototype = Object.getPrototypeOf(obj);
      // 如果有原型并且原型不是 `Object.prototype`(避免误判普通对象的默认方法)
      while (prototype && prototype !== Object.prototype) {
        const prototypeProperties = Object.getOwnPropertyNames(prototype);
        // 遍历原型的属性
        for (const property of prototypeProperties) {
          const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
          // 如果原型上的属性是数据属性并且有值,或者是方法(可调用函数),则对象不为空
          if (
            (descriptor.value && descriptor.value !== null && descriptor.value !== undefined) ||
            typeof descriptor.value === "function"
          ) {
            return false;
          }
        }
        // 继续沿着原型链向上查找
        prototype = Object.getPrototypeOf(prototype);
      }
      // 如果以上检查都没有找到非空属性或方法,则对象为空
      return true;
    }

    可以使用这个函数来判断对象是否为空,例如:

    function MyClass() {}
    MyClass.prototype.myMethod = function () {};
    const instance = new MyClass();
    console.log(isObjectEmpty(instance));

    第二题

    题目

    1. 为何通常在微前端应用隔离,不选择 iframe 方案?

    参考解答

    在微前端架构中,虽然iframe能提供很好的应用隔离(包括 JavaScript 和 CSS 隔离),确保微前端应用之间不会相互干扰,但一般不把它作为首选方案,原因包括:

    1. 性能开销

    iframe会创建一个全新的浏览器上下文环境,每个iframe都有自己的文档对象模型(DOM)树、全局执行环境等。如果一个页面中嵌入了多个iframe,就会导致额外的内存和 CPU 资源消耗,特别是在性能有限的设备上更为显著。

    1. 应用集成和交互问题

    iframe自然隔离了父子页面的环境,这虽然提供了隔离,但同时也使得主应用与子应用之间的通信难度增加。虽然可以通过postMessage等 API 实现跨iframe通信,但这种方式相比于直接 JavaScript 调用来说,更为复杂,交互效率也较低。

    1. UI 体验一致性

    在iframe中运行的应用在视觉上可能与主应用难以实现无缝集成。iframe内外的样式、字体等一致性需要额外的处理。此外,iframe可能带来额外的滚动条,影响用户体验。

    1. SEO 问题

    如果微前端的某些内容是通过iframe呈现的,那么这部分内容对于搜索引擎是不可见的,这可能会对应用的
    SEO 产生负面影响。

    1. 安全问题

    虽然iframe可以提供一定程度的隔离,但它也可能引入点击劫持等安全风险。此外,过多地使用iframe也可能增加网站被恶意脚本攻击的风险。

    因此,虽然iframe是一种可行的应用隔离方法,它的这些局限性使得开发者在选择微前端技术方案时,往往会考虑其他提供更轻量级隔离、更好集成与交互体验的方案,如使用 JavaScript 沙箱、CSS 隔离技术、Web Components 等。这些方法虽然隔离性可能不如iframe彻底,但在整体的应用性能、用户体验和开发效率上通常会有更好的表现。

    第三题

    题目

    1. Webpack 为何不支持 CMD 模块化?

    参考解答

    Webpack 本身并不直接“不支持”CMD(CommonJS Modules/1.x 规范)模块化,因为 CMD 模块化规范在前端开发中并不常见,且 Webpack 实际上可以处理多种模块化规范,包括 CommonJS。

    然而,当我们讨论 Webpack 是否“支持”CMD 时,可能是在对比 Webpack 对不同模块化规范的支持程度和默认行为。Webpack 主要关注于将模块打包成浏览器可以理解的格式,而 CommonJS(以及 AMD、UMD 等)模块化规范在 Node.js 环境中更为常见。

    Webpack 默认支持 ES Module(ECMAScript Modules,ES6 引入的模块系统)和 CommonJS 模块化规范。对于 CommonJS 模块,Webpack 能够识别 require() 和 module.exports 语法,并将这些模块按照依赖关系打包成一个或多个文件。

    至于 CMD 模块化规范,它主要是 Sea.js 这类模块加载器所使用的规范,并不是 JavaScript 官方标准或广泛支持的模块化规范。因此,Webpack 没有内置对 CMD 的直接支持,也不需要将 CMD 模块化规范作为其主要关注点。

    在实际开发中,如果你在使用 Webpack 并且遇到了需要处理 CMD 模块化规范的情况,这通常意味着你可能需要:

    转换模块规范:使用 Babel 或其他转换工具将 CMD 模块化规范转换为 Webpack 支持的规范,如 CommonJS 或 ES Module。
    使用插件或加载器:查看是否有现成的 Webpack 插件或加载器可以处理 CMD 模块化规范,尽管这样的插件可能不太常见。
    重新考虑模块化策略:评估是否有可能将项目中的模块迁移到更广泛支持的模块化规范上,如 CommonJS 或 ES Module。
    总的来说,Webpack 本身不直接“不支持”CMD 模块化规范,但它更关注于支持 ECMAScript 标准和广泛使用的模块化规范,如 CommonJS 和 ES Module。如果你需要使用 CMD 模块化规范,可能需要通过一些额外的步骤来适配 Webpack 的工作流程。

    第四题

    题目

    1. Webpack 如何将一些通用的依赖打包成一个独立的 bundle?

    参考解答

    在 Webpack 中,将一些通用的依赖,如 React、React DOM、React Router 等库和框架,打包成一个独立的 bundle,通常是为了长期缓存和减少每次部署更新的下载量。这可以通过 "代码分割" (code splitting) 和 "优化" (optimization) 配置来实现。
    以下是 Webpack 中分离通用依赖的几个步骤:

    1、使用 entry 来定义不同的入口点: 可以通过配置一个额外的入口来创建一个只包含通用库的 bundle,也就是所谓的 "vendor" bundle。

    module.exports = {
      entry: {
        main: "./src/index.js", // 你的应用代码
        vendor: ["react", "react-dom", "react-router"], // 指定共享库
      },
      // ...
    };

    2、使用 SplitChunksPlugin: 这个插件可以将共享代码分割成不同的 chunks,并可以通过配置将其从业务代码中分离出来。在 Webpack 4 及之后的版本中,默认内置了 optimization.splitChunks,就是这个插件的配置方法。

    module.exports = {
      // ...
      optimization: {
        splitChunks: {
          cacheGroups: {
            vendor: {
              test: /[\\/]node_modules[\\/]/, // 指定是 node_modules 下的第三方包
              name: "vendors", // 打包后的文件名,任意命名
              chunks: "all", // 对所有的 chunk 生效
            },
          },
        },
      },
    };

    3、配置 output: 虽然不是必须的,你还可以在 output 中定义 filename 和 chunkFilename,来控制主入口和非主入口 chunks 的文件名。

    output: {
      filename: '[name].[contenthash].js',
      chunkFilename: '[name].[contenthash].js'
    }

    通过这样的配置,Webpack 在打包时会自动将 node_modules 中的依赖和业务代码分离开来,业务代码会被打包到 main chunk 中,而第三方库则会打包到 vendors chunk。

    第五题

    题目

    1. 如何统计长任务时间、长任务执行次数?

    参考解答

    在 JavaScript 中,可以使用 Performance API 中的 PerformanceObserver 来监视和统计长任务(Long Task)。长任务是指那些执行时间超过 50 毫秒的任务,这些任务可能会阻塞主线程,影响页面的交互性和流畅性。

    // 创建一个性能观察者实例来订阅长任务
    let observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        console.log("Long Task detected:");
        console.log(`Task Start Time: ${entry.startTime}, Duration: ${entry.duration}`);
      }
    });
    
    // 开始观察长任务
    observer.observe({ entryTypes: ["longtask"] });
    
    // 启动长任务统计数据的变量
    let longTaskCount = 0;
    let totalLongTaskTime = 0;
    
    // 更新之前的性能观察者实例,以增加统计逻辑
    observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        longTaskCount++; // 统计长任务次数
        totalLongTaskTime += entry.duration; // 累加长任务总耗时
        // 可以在这里添加其他逻辑,比如记录长任务发生的具体时间等
      });
    });
    
    // 再次开始观察长任务
    observer.observe({ entryTypes: ["longtask"] });
    在上面的代码中,我们创建了一个PerformanceObserver对象来订阅长任务。每当检测到长任务时,它会向回调函数传递一个包含长任务性能条目的列表。在这个回调中,我们可以统计长任务的次数和总耗时。
    注意:PerformanceObserver需要在支持该 API 的浏览器中运行。截至到我所知道的信息(2023 年 4 月的知识截点),所有现代浏览器都支持这一 API,但在使用前你应该检查用户的浏览器是否支持这个特性。
    以下是如何在实际使用中停止观察和获取当前的统计数据:
    // 停止观察能力
    observer.disconnect();
    
    // 统计数据输出
    console.log(`Total number of long tasks: ${longTaskCount}`);
    console.log(`Total duration of all long tasks: ${totalLongTaskTime}ms`);

    使用这种方法,你可以监控应用程序中的性能问题,并根据长任务的发生频率和持续时间进行优化。

    第六题

    题目

    1. forwardsRef 作用是啥, 有哪些使用场景?

    参考解答

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

    forwardRef 的作用:

    访问子组件的 DOM 节点: 当需要直接访问子组件中的 DOM 元素(例如,需要管理焦点或测量尺寸)时,可以使用 forwardRef。

    在高阶组件(HOC)中转发 refs: 封装组件时,通过 forwardRef 可以将 ref 属性透传给被封装的组件,这样父组件就能够通过 ref 访问到实际的子组件实例或 DOM 节点。

    在函数组件中使用 refs(React 16.8+): 在引入 Hook 之前,函数组件不能直接与 refs 交互。但是,引入了 forwardRef 和 useRef 之后,函数组件可以接受 ref 并将它透传给子节点。

    使用场景举例:

    1. 访问子组件的 DOM 节点

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

    const FancyButton = React.forwardRef((props, ref) => (
      <button ref={ref} className="FancyButton">
        {props.children}
      </button>
    ));
    
    // 现在你可以从父组件中直接获取DOM引用
    const ref = React.createRef();
    <FancyButton ref={ref}>Click me!</FancyButton>;
    1. 在高阶组件中转发 refs

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

    function logProps(Component) {
      class LogProps extends React.Component {
        componentDidUpdate(prevProps) {
          console.log("old props:", prevProps);
          console.log("new props:", this.props);
        }
    
        render() {
          const { forwardedRef, ...rest } = this.props;
    
          // 将自定义的 prop 属性 "forwardedRef" 定义为 ref
          return <Component ref={forwardedRef} {...rest} />;
        }
      }
    
      // 注意:React.forwardRef 回调的第二个参数 "ref" 传递给了LogProps组件的props.forwardedRef
      return React.forwardRef((props, ref) => {
        return <LogProps {...props} forwardedRef={ref} />;
      });
    }
    1. 在函数组件中使用 ref

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

    const MyFunctionalComponent = React.forwardRef((props, ref) => {
      return <input type="text" ref={ref} />;
    });
    
    const ref = React.createRef();
    <MyFunctionalComponent ref={ref} />;

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

    第七题

    题目

    1. 如何一次性渲染十万条数据还能保证页面不卡顿?

    参考解答

    原理其实就是通过 requestAnimationFrame 实现分块儿加载。

    requestAnimationFrame + fragment(时间分片)

    既然定时器的执行时间和浏览器的刷新率不一致,那么我就可以用requestAnimationFrame来解决
    requestAnimationFrame也是个定时器,不同于setTimeout,它的时间不需要我们人为指定,这个时间取决于当前电脑的刷新率,如果是 60Hz ,那么就是 16.7ms 执行一次,如果是 120Hz 那就是 8.3ms 执行一次。

    这么一来,每次电脑屏幕 16.7ms 后刷新一下,定时器就会产生 20 个li,dom结构的出现和屏幕的刷新保持了一致。

    const total = 100000;
    let ul = document.getElementById("container");
    let once = 20;
    let page = total / once;
    
    function loop(curTotal) {
      if (curTotal <= 0) return;
    
      let pageCount = Math.min(curTotal, once);
    
      window.requestAnimationFrame(() => {
        for (let i = 0; i < pageCount; i++) {
          let li = document.createElement("li");
          li.innerHTML = ~~(Math.random() * total);
          ul.appendChild(li);
        }
        loop(curTotal - pageCount);
      });
    }
    
    loop(total);

    其实目前这个代码还可以优化一下,每一次appendChild都是新增一个新的li,也就意味着需要回流一次,总共十万条数据就需要回流十万次。

    此前讲回流的时候提出过虚拟片段fragment来解决这个问题。

    fragment是虚拟文档碎片,我们一次for循环产生 20 个li的过程中可以全部把真实dom挂载到fragment上,然后再把fragment挂载到真实dom上,这样原来需要回流十万次,现在只需要回流100000 / 20次。

    const total = 100000;
    let ul = document.getElementById("container");
    let once = 20;
    let page = total / once;
    
    function loop(curTotal) {
      if (curTotal <= 0) return;
    
      let pageCount = Math.min(curTotal, once);
    
      window.requestAnimationFrame(() => {
        let fragment = document.createDocumentFragment(); // 创建一个虚拟文档碎片
        for (let i = 0; i < pageCount; i++) {
          let li = document.createElement("li");
          li.innerHTML = ~~(Math.random() * total);
          fragment.appendChild(li); // 挂到fragment上
        }
        ul.appendChild(fragment); // 现在才回流
        loop(curTotal - pageCount);
      });
    }
    
    loop(total);

    第八题

    题目

    1. jsBridge 是什么?原理是啥?

    参考解答

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

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

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

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

    jsBridge 在移动应用开发中尤为重要,因为它提供了一种方式来整合 Web 技术和原生应用功能,让开发者能够利用 Web 技术来编写跨平台的应用,同时还能够访问设备的原生功能,如相机、GPS 等。
    这种机制特别适合于混合应用的开发,在这些应用中,部分界面和逻辑使用 Web 技术实现,而另一部分则利用原生代码以获取更好的性能和更丰富的设备功能支持。通过 jsBridge,两种不同的代码和技术可以互相协作,提供统一的用户体验。

    第九题

    题目

    1. ts 项目中,如何使用 node_modules 里面定义的全局类型包到自己项目 src 下面使用?

    参考解答

    在 TypeScript 项目中导入 node_modules 中定义的全局包,并在你的 src 目录下使用它,通常遵循以下步骤:
    安装包: 使用包管理器如 npm 或 yarn 来安装你需要的全局包。

    npm install <package-name>
    # 或者
    yarn add <package-name>

    类型声明: 确保该全局包具有类型声明。如果该全局包包含自己的类型声明,则 TypeScript 应该能够自动找到它们。如果不包含,则可能需要安装对应的 DefinitelyTyped 声明文件。

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

    tsconfig.json 配置:

    确保你的 tsconfig.json 文件配置得当,以便 TypeScript 能够找到 node_modules 中的声明文件。

    如果包是模块形式的,确保 "moduleResolution" 设置为 "node"。
    确保 compilerOptions 中的 "types" 和 "typeRoots" 属性没有配置错误。

    使用全局包:
    现在你可以在你的 src 目录中的任何文件里使用这个全局包。

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

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



沪ICP备19023445号-2号
友情链接