# JS面试题

# 事件循环

  1. JS是单线程,防止代码阻塞,我们把代码(任务):同步和异步

  2. 同步代码给js引擎执行,异步代码交给宿主环境

  3. 同步代码放入执行栈中,异步代码等待时机成熟送入任务队列排队

  4. 执行栈执行完毕,会去任务队列(宏任务队列或微任务队列)看是否有异步任务,有就送到执行栈执行,反复循环查看执行,这个过程是事件循环(eventloop)

    *注意:微任务由JS引擎发起,宏任务由宿主环境发起(浏览器或者node环境)

# ==和===的区别

  • ***==***:等于操作符

    两个都为简单类型,字符串和布尔值都会转换成数值,再比较

    简单类型与引用类型比较,对象转换为原始类型的值,再比较

    两个都为引用类型,则比较它们是否指向同一个对象

    null和undefined相等

    存在NaN则返回false

    /**
     * ==:如果操作数相等,则返回true,并且在比较过程中,==会先进行类型转换再确定操作数是否相等
     * 
     */
    
    // 布尔值为转换为数值
    console.log('==============布尔值->会转换为数值==============')
    const result1 = (true == 1);
    console.log(result1); // 输出 true
    
    const result2 = (false == 0)
    console.log(result2); // 输出 true
    
    
    
    // 字符串会转换为数值
    console.log('==============字符串->转换为数值==============')
    const result3 = ('1' == 1)
    console.log(result3); // 输出 true
    
    const result4 = ('0' == 0)
    console.log(result4); // 输出 true
    
    
    
    // 如果一个操作数是对象,另一个操作数是字符串、数值或符号,则对象会转换为原始值,然后再进行比较
    console.log('==============对象->转换为原始值==============')
    const obj1 = {
        name: 'obj1-name',
        valueOf: function () {
            return '1';
        },
        toString: function () {
            return '1';
        },
    }
    console.log(obj1 == 1)
    
    // 货币对象,可以与数字直接比较
    class Money {
        constructor(amount, currency = 'USD') {
            this.amount = amount;
            this.currency = currency;
        }
    
        valueOf() {
            return this.amount; // 返回数值用于比较和计算
        }
    
        toString() {
            return `${this.amount} ${this.currency}`;
        }
    }
    
    const price = new Money(100, 'USD');
    console.log(price == 100); // true,可以直接与数字比较
    console.log(price > 50);   // true,可以参与数值比较
    
    
    // null和undefined只在与自身比较时相等
    console.log('==============null和undefined==============')
    console.log(null == null); // true
    console.log(undefined == undefined); // true
    console.log(null == undefined); // true
    console.log(null === undefined); // false
    console.log(null == 0); // false
    console.log(undefined == 0); // false
    console.log(null == null == undefined); // false (null == null) -> true; true == undefined -> false
    
    // 如果有任意操作数为NaN,则比较总是返回false
    console.log('==============NaN==============')
    console.log(NaN == NaN); // false
    console.log(NaN === NaN); // false
    
    
    // 两边都是对象,只有引用同一个对象时才相等
    console.log('==============对象引用==============')
    let obj2 = { name: "xxx" }
    let obj3 = { name: "xxx" }
    let obj4 = obj2;
    console.log(obj2 == obj3); // 输出 false, 因为它们引用不同的对象
    console.log(obj2 == obj4);
    
  • ===:全等操作符,只有两个操作数再不转换的前提下相等才返回true,即类型相同,值也需相同

    undefined和null与自身严格相等

    全等运算符不会做类型转换

    console.log(null === undefined); // false
    console.log(null === null)  // true
    console.log(undefined === undefined)    // true
    
    console.log(1 === 1)    // true
    console.log('1' === 1) // false
    

# 数据结构

数组,栈,队列,链表,字典

  • 数组

    数组是按顺序存储多个数据的集合,它允许通过索引(下标)快速访问、增删元素,是处理列表类数据的核心数据结构 。

    核心特性:

    1. 索引访问:通过数字索引(从 0 开始)直接定位元素,如 arr[0] 能快速获取第一个元素,这是其与对象(键值对访问)的核心区别 。
    2. 动态长度:数组长度可自动调整,执行 push()(末尾添加)、pop()(末尾删除)等操作时,length 属性会实时更新 。
    3. 多数据类型:可同时存储不同类型数据,如 [1, '前端', true, {}],但实际开发中建议存储同类型数据,便于遍历和处理 。
    4. 丰富 API:内置大量实用方法,如 map() 遍历转换数据、filter() 筛选元素、reduce() 汇总计算等,是前端数据处理的 “瑞士军刀” 。
  • 遵循 “先进后出(LIFO)” 规则的线性数据结构,仅允许在一端(栈顶)进行元素的插入(入栈)和删除(出栈)操作,如同叠放的盘子,只能从最顶端取放 。

    核心特性:

    1. 操作限制:仅栈顶可操作,底部元素需等上方元素全部移除才能访问。例如调用栈中,最内层函数执行完(出栈),外层函数才能继续执行 。
    2. 前端高频应用场景
      • 函数调用栈:JS 引擎执行代码时,每次调用函数会将其压入栈顶,函数执行完毕后从栈顶弹出。如递归函数调用,若递归无终止条件,会导致栈溢出(Stack Overflow) 。
      • 浏览器历史记录:浏览器的 “前进 / 后退” 功能基于栈实现,访问新页面时入栈,点击 “后退” 时将当前页面出栈,恢复上一页面(栈顶元素) 。
      • 编辑器撤销 / 重做:编辑文档时,每一步操作入栈,“撤销” 即弹出最近操作(恢复上一状态),部分场景下 “重做” 可借助辅助栈实现 。

    栈实例:

    class MinStack {
        _stack = [];
        _minStack = [];
    
        push(val){
            this._stack.push(val)
            if(this._minStack.length === 0 || val <= this._minStack[this._minStack.length - 1]){
                this._minStack.push(val)
            }
        }
    
        pop(){
            const val = this._stack.pop()
            if(val === this._minStack[this._minStack.length - 1]){
                this._minStack.pop()
            }
            return val;
        }
    
        top(){
            return this._stack[this._stack.length - 1]
        }
    
        // 获取栈中的最小值
        getMin(){
            return this._minStack[this._minStack.length - 1]
        }
    }
    
    const minStack = new MinStack();
    minStack.push(-2);
    minStack.push(0);
    minStack.push(5);
    console.log(minStack.top()); // 输出 5
    console.log(minStack._stack)
    console.log(minStack.getMin()); // 输出 -2
    

# script标签的位置以及加载顺序

默认行为

  1. 阻塞性加载:浏览器解析HTML时遇到<script>标签会暂停HTML解析,下载并执行脚本,完成后才继续解析HTML
  2. 顺序执行:多个<script>标签按它们在HTML中出现的顺序依次执行

放置位置

  1. <head>
<head>
  <script src="script1.js"></script>
  <script src="script2.js"></script>
</head>
  • 优点:尽早加载脚本
  • 缺点:会阻塞页面渲染,可能导致白屏时间延长
  1. <body>底部(推荐)
<body>
  <!-- 页面内容 -->
  <script src="script1.js"></script>
  <script src="script2.js"></script>
</body>
  • 优点:页面内容先渲染,用户体验更好
  • 缺点:脚本执行延迟,可能影响依赖脚本的功能

控制加载行为的属性

async属性

<script src="script.js" async></script>
  • 异步下载,不阻塞HTML解析
  • 下载完成后立即执行(可能在HTML解析完成前)
  • 执行顺序不确定(先下载完的先执行)

defer属性

<script src="script.js" defer></script>
  • 异步下载,不阻塞HTML解析
  • 等到HTML解析完成后,按顺序执行所有defer脚本
  • 执行顺序与在文档中的位置一致

type="module"

<script type="module" src="module.js"></script>
  • 默认具有defer行为
  • 可以添加async属性改变行为

动态加载

const script = document.createElement('script');
script.src = 'dynamic.js';
document.head.appendChild(script);
  • 完全异步加载和执行
  • 不阻塞页面渲染

最佳实践建议

  1. 关键脚本放在<head>中并使用defer
  2. 非关键脚本放在<body>底部
  3. 完全独立的脚本(如分析代码)可使用async
  4. 现代开发推荐使用模块化(type="module")和打包工具

# script标签中的module有什么意义

  1. 模块化代码组织

    模块内部的变量、函数、类默认具有 块级作用域,不会污染全局作用域。只有通过 export 显式导出的内容,才能被其他模块通过 import 引入使用,实现了代码的封装和隔离。

    <!-- 模块文件 module.js -->
    <script type="module">
      const message = "Hello Module";
      export function logMessage() {
        console.log(message);
      }
    </script>
    
    <!-- 引入模块 -->
    <script type="module">
      import { logMessage } from './module.js';
      logMessage(); // 输出 "Hello Module"
      console.log(message); // 报错:message 未定义(模块内私有)
    </script>
    
  2. 支持 import/export 语法

    允许使用 import 加载其他模块的导出内容,或通过 export 暴露当前模块的接口,实现模块间的依赖管理和代码复用,这是模块化开发的核心能力。

  3. 自动采用严格模式

    模块内默认启用严格模式(use strict),对变量声明、函数作用域等有更严格的语法限制,减少代码错误(例如禁止未声明的变量赋值)。

  4. 延迟执行(类似 defer

    模块脚本会 异步加载,不会阻塞 HTML 解析,且会按照在文档中出现的顺序执行(与 defer 特性类似),但执行时机晚于普通脚本。

  5. 跨域限制

    模块脚本的加载受同源策略限制,若从其他域名加载模块,服务器需配置 CORS 响应头,否则会加载失败(普通脚本无此限制)。

  6. 顶层 thisundefined

    模块顶层的 this 指向 undefined,而普通脚本中 this 指向全局对象(如浏览器中的 window),进一步强化了模块的独立性。

在module中访问this

console.log(globalThis)

# foreEach和map的区别

forEachmap都是 JavaScript 中用于遍历数组的方法,但它们有几个关键区别:

主要区别

特性 forEach map
返回值 undefined 新数组(由回调函数的返回值组成)
链式调用 不支持(返回undefined 支持(返回新数组)
使用场景 仅需遍历数组执行操作 需要基于原数组创建新数组

详细说明

  1. 返回值
  • forEach:总是返回 undefined

    const result = [1, 2, 3].forEach(item => item * 2);
    console.log(result); // undefined
    
  • map:返回一个新数组,包含回调函数每次执行的返回值

    const result = [1, 2, 3].map(item => item * 2);
    console.log(result); // [2, 4, 6]
    
  1. 是否改变原数组

两者都不会直接改变原数组,但:

  • forEach 可以在回调中修改原数组:

    const arr = [1, 2, 3];
    arr.forEach((item, index, array) => {
      array[index] = item * 2; // 直接修改原数组
    });
    console.log(arr); // [2, 4, 6]
    
  • map 的设计理念是不应该修改原数组,而是返回新数组

  1. 链式调用
  • forEach 不能链式调用,因为它返回 undefined

    // 错误示例
    [1, 2, 3].forEach(...).filter(...); // 报错
    
  • map 可以链式调用:

    const result = [1, 2, 3]
      .map(x => x * 2)
      .filter(x => x > 3);
    console.log(result); // [4, 6]
    
  1. 性能考虑

在大多数情况下,forEachmap稍微快一些,因为 map需要创建并返回新数组。但差异通常很小,不应作为选择的主要依据。

使用场景

使用 forEach的情况:

  • 只需要遍历数组执行某些操作(如打印、修改外部变量等)
  • 不需要返回新数组
  • 不需要链式调用
// 示例:打印数组元素
[1, 2, 3].forEach(item => console.log(item));

// 示例:累加外部变量
let sum = 0;
[1, 2, 3].forEach(item => {
  sum += item;
});

使用 map的情况:

  • 需要基于原数组创建新数组
  • 需要进行链式操作
  • 需要保持原数组不变
// 示例:创建新数组
const doubled = [1, 2, 3].map(item => item * 2);

// 示例:链式调用
const result = users
  .map(user => user.name)
  .filter(name => name.startsWith('A'));

总结

选择 forEach还是 map取决于你的需求:

  • 如果只需要遍历数组并执行某些操作,使用 forEach
  • 如果需要转换数组并得到新数组,使用 map
  • 在函数式编程中,通常优先使用 map,因为它更符合不可变性的原则

# indexof和includes的区别

indexOfincludes都是 JavaScript 中用于检查数组中是否包含某个元素的方法,但它们有几个重要区别:

主要区别

特性 indexOf includes
返回值 元素的索引(找不到返回-1) 布尔值(true/false)
NaN处理 无法检测NaN 可以正确检测NaN
空值处理 可以检测undefined/null 可以检测undefined/null
可读性 较低(需要与-1比较) 较高(直接返回布尔值)
ES版本 ES5 ES6 (ES2015)

详细说明

  1. 返回值不同
  • indexOf 返回元素在数组中的位置索引(从0开始),如果找不到则返回-1

    const arr = ['a', 'b', 'c'];
    console.log(arr.indexOf('b')); // 1
    console.log(arr.indexOf('d')); // -1
    
  • includes 返回布尔值,表示数组是否包含该元素

    const arr = ['a', 'b', 'c'];
    console.log(arr.includes('b')); // true
    console.log(arr.includes('d')); // false
    
  1. 对NaN的处理不同
  • indexOf 无法检测NaN

    const arr = [1, NaN, 3];
    console.log(arr.indexOf(NaN)); // -1 (找不到)
    
  • includes 可以正确检测NaN

    const arr = [1, NaN, 3];
    console.log(arr.includes(NaN)); // true
    
  1. 语法差异
  • indexOf 语法:

    arr.indexOf(searchElement[, fromIndex])
    
  • includes 语法:

    arr.includes(searchElement[, fromIndex])
    

两者都接受可选的第二个参数,表示从哪个索引开始搜索。

  1. 使用场景对比

使用 indexOf的情况:

  • 需要知道元素的具体位置
  • 需要兼容老版本JavaScript环境(ES5及以下)
  • 不需要检测NaN
// 查找元素位置
const index = fruits.indexOf('apple');
if (index !== -1) {
  console.log(`找到苹果,位置在${index}`);
}

// 删除特定元素
const index = items.indexOf(itemToRemove);
if (index > -1) {
  items.splice(index, 1);
}

使用 includes的情况:

  • 只需要知道元素是否存在
  • 需要检测NaN
  • 代码可读性更重要
  • 运行环境支持ES6+
// 检查权限
if (userPermissions.includes('admin')) {
  // 执行管理员操作
}

// 验证输入
const validOptions = ['yes', 'no'];
if (!validOptions.includes(userInput)) {
  console.log('无效输入');
}

性能比较

在大多数现代JavaScript引擎中,两者的性能差异可以忽略不计。但在某些情况下:

  • 当元素存在时,indexOf可能在找到元素后立即返回,稍微快一点
  • 当元素不存在时,两者都需要遍历整个数组

总结

  1. 如果你需要元素的位置,使用 indexOf
  2. 如果你只需要知道元素是否存在,使用 includes(更直观)
  3. 如果你需要检测NaN,必须使用 includes
  4. 在ES6+环境中,includes通常是更好的选择,除非你需要兼容旧环境或需要元素位置信息

补充:字符串中的使用

这两个方法也可以用于字符串:

// 字符串中的indexOf
const str = 'hello';
console.log(str.indexOf('e')); // 1
console.log(str.indexOf('x')); // -1

// 字符串中的includes
console.log(str.includes('ell')); // true
console.log(str.includes('x')); // false

在字符串中使用时的区别与数组中的区别类似。

# uniapp的问题 打包,如何缩小打包体积

UniApp 打包体积过大会影响应用加载速度和用户体验。以下是一些有效的优化策略:

  1. 代码层面优化

移除无用代码和依赖

// 定期检查package.json,移除未使用的依赖
// 使用工具分析依赖关系:
npx depcheck

按需引入组件和API

// 避免整体引入
import * as utils from '@/common/utils.js'; // ❌ 不推荐

// 按需引入
import { formatDate, debounce } from '@/common/utils.js'; // ✅ 推荐

优化静态资源

<!-- 压缩图片 -->
<image src="/static/optimized-image.jpg" mode="aspectFit"></image>

<!-- 使用WebP格式(兼容性处理) -->
<picture>
  <source srcset="image.webp" type="image/webp">
  <image src="image.jpg" alt="fallback"></image>
</picture>
  1. 构建配置优化

修改 manifest.json

{
  "app-plus": {
    "optimization": {
      "treeShaking": {
        "enable": true
      }
    },
    "compilerVersion": 3, // 使用V3编译器,体积更小
    "webpack-bundle-analyzer": true // 启用分析工具
  }
}

配置 vue.config.js

// vue.config.js
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  configureWebpack: {
    optimization: {
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          vendor: {
            name: 'chunk-vendors',
            test: /[\\/]node_modules[\\/]/,
            priority: 10,
            chunks: 'initial'
          },
          common: {
            name: 'chunk-common',
            minChunks: 2,
            priority: 5,
            chunks: 'initial'
          }
        }
      }
    },
    plugins: [
      // 启用Gzip压缩
      new CompressionWebpackPlugin({
        algorithm: 'gzip',
        test: /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i,
        threshold: 10240,
        minRatio: 0.8
      }),
      // 包分析工具(开发时使用)
      process.env.NODE_ENV === 'analyze' && new BundleAnalyzerPlugin()
    ].filter(Boolean)
  },
  chainWebpack: (config) => {
    // 移除prefetch插件减少冗余代码
    config.plugins.delete('prefetch');
    
    // 生产环境配置
    if (process.env.NODE_ENV === 'production') {
      config.optimization.minimize(true);
    }
  }
};
  1. 分包优化策略

配置分包加载

// manifest.json 或 pages.json
{
  "optimization": {
    "subPackages": true
  },
  "subPackages": [
    {
      "root": "pages/sub1",
      "pages": [
        "page1",
        "page2"
      ]
    },
    {
      "root": "pages/sub2", 
      "pages": [
        "page3",
        "page4"
      ]
    }
  ]
}

按需加载组件

// 使用异步组件
export default {
  components: {
    HeavyComponent: () => import('@/components/HeavyComponent.vue')
  }
}
  1. 静态资源优化

使用CDN加速

<!-- 将大体积库改为CDN引入 -->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

<!-- 在vue.config.js中配置externals -->
module.exports = {
  configureWebpack: {
    externals: {
      'lodash': '_',
      'moment': 'moment'
    }
  }
}

压缩和优化图片

# 使用图片压缩工具
npm install -g imagemin-cli
imagemin static/images/* --out-dir=static/optimized-images
  1. 运行时优化

懒加载和预加载策略

// pages.json 中配置
{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页",
        "lazyCodeLoading": "requiredComponents" // 按需加载代码
      }
    }
  ]
}

清理console日志

// vue.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  configureWebpack: {
    optimization: {
      minimizer: [
        new TerserPlugin({
          terserOptions: {
            compress: {
              drop_console: process.env.NODE_ENV === 'production'
            }
          }
        })
      ]
    }
  }
};
  1. 分析和监控工具

使用webpack-bundle-analyzer

// package.json
{
  "scripts": {
    "analyze": "cross-env NODE_ENV=analyze uni-build"
  }
}
# 运行分析
npm run analyze

监控打包体积

// 在vue.config.js中添加体积限制
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  configureWebpack: {
    performance: {
      hints: 'warning',
      maxAssetSize: 500000, // 500KB
      maxEntrypointSize: 500000
    }
  }
};
  1. 平台特定优化

H5平台优化

// 使用路由懒加载
const routes = [
  {
    path: '/home',
    component: () => import('@/pages/home.vue')
  }
];

小程序平台优化

{
  "app-plus": {
    "optimization": {
      "codeSplitting": true,
      "minifyJS": true,
      "minifyWXML": true,
      "minifyWXSS": true
    }
  }
}

最佳实践总结

  1. 定期清理:每季度检查并移除未使用的依赖
  2. 按需引入:避免整体引入大型库
  3. 分包策略:将不常用的功能拆分为子包
  4. 资源优化:压缩图片和使用CDN
  5. 构建分析:使用分析工具识别体积问题
  6. 持续监控:设置体积阈值防止反弹

通过综合运用这些策略,通常可以将UniApp打包体积减少30%-50%,显著提升应用性能。

#

# 有三个请求,需要前两个接口请求之后再去请求第三个接口

  1. 使用 Promise 链式调用
function request1() {
  return fetch('https://api.example.com/request1');
}

function request2() {
  return fetch('https://api.example.com/request2');
}

function request3() {
  return fetch('https://api.example.com/request3');
}

// 顺序执行
request1()
  .then(response1 => {
    console.log('请求1完成', response1);
    return request2();
  })
  .then(response2 => {
    console.log('请求2完成', response2);
    return request3();
  })
  .then(response3 => {
    console.log('请求3完成', response3);
  })
  .catch(error => {
    console.error('请求出错', error);
  });
  1. 使用 async/await 语法
async function executeRequests() {
  try {
    const response1 = await request1();
    console.log('请求1完成', response1);
    
    const response2 = await request2();
    console.log('请求2完成', response2);
    
    const response3 = await request3();
    console.log('请求3完成', response3);
  } catch (error) {
    console.error('请求出错', error);
  }
}

executeRequests();
  1. 使用 Promise.all 并行前两个请求

如果前两个请求可以并行执行,然后第三个请求再执行:

Promise.all([request1(), request2()])
  .then(([response1, response2]) => {
    console.log('请求1和请求2都已完成', response1, response2);
    return request3();
  })
  .then(response3 => {
    console.log('请求3完成', response3);
  })
  .catch(error => {
    console.error('请求出错', error);
  });
  1. 使用 async/await 结合 Promise.all
async function executeRequests() {
  try {
    const [response1, response2] = await Promise.all([request1(), request2()]);
    console.log('请求1和请求2都已完成', response1, response2);
    
    const response3 = await request3();
    console.log('请求3完成', response3);
  } catch (error) {
    console.error('请求出错', error);
  }
}

executeRequests();
  1. 使用 Axios 的示例

如果你使用 Axios 库:

import axios from 'axios';

async function fetchData() {
  try {
    const result1 = await axios.get('/api/request1');
    const result2 = await axios.get('/api/request2');
    
    // 前两个请求都完成后
    const result3 = await axios.get('/api/request3', {
      params: {
        dataFromFirst: result1.data,
        dataFromSecond: result2.data
      }
    });
    
    console.log('最终结果:', result3.data);
  } catch (error) {
    console.error('请求失败:', error);
  }
}

fetchData();
  1. 处理请求间的数据依赖

如果第三个请求需要前两个请求的结果作为参数:

async function executeDependentRequests() {
  try {
    const response1 = await request1();
    const response2 = await request2();
    
    // 使用前两个请求的结果作为第三个请求的参数
    const response3 = await request3({
      data1: response1.data,
      data2: response2.data
    });
    
    console.log('所有请求完成', response3);
  } catch (error) {
    console.error('请求出错', error);
  }
}

注意事项

  1. 错误处理:确保使用 try/catch 或 .catch() 来捕获可能的错误
  2. 性能考虑:如果请求之间没有依赖关系,考虑并行执行以提高性能
  3. 取消请求:在 React 组件中使用时,注意组件卸载时取消未完成的请求
  4. 加载状态:在 UI 中显示适当的加载状态

选择哪种方法取决于你的具体需求:

  • 如果请求需要严格顺序执行,使用顺序的 await 或 then 链
  • 如果前两个请求可以并行,使用 Promise.all
  • 如果有复杂的数据依赖关系,确保正确处理数据传递

# fetch和axios如何中断

Fetch 中断请求

Fetch API 本身不直接提供中断请求的方法,但可以使用 AbortController来实现。

  1. 使用 AbortController
// 创建 AbortController 实例
const controller = new AbortController();
const signal = controller.signal;

// 发起 fetch 请求
fetch('https://api.example.com/data', {
  signal: signal // 将 signal 传递给 fetch
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('请求已被取消');
    } else {
      console.error('请求错误:', error);
    }
  });

// 在需要时中断请求
controller.abort(); // 这会触发 AbortError
  1. React 组件中使用示例
import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

    fetch('https://api.example.com/data', { signal })
      .then(response => response.json())
      .then(data => console.log(data))
      .catch(error => {
        if (error.name === 'AbortError') {
          console.log('组件卸载,请求取消');
        }
      });

    // 组件卸载时取消请求
    return () => controller.abort();
  }, []);

  return <div>组件内容</div>;
}

Axios 中断请求

Axios 提供了两种方式来中断请求:使用 CancelToken(旧版)或 AbortController(新版)。

  1. 使用 CancelToken(Axios < 0.22.0)
import axios from 'axios';

// 创建 CancelToken
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

// 发起请求
axios.get('https://api.example.com/data', {
  cancelToken: source.token
})
  .then(response => console.log(response.data))
  .catch(error => {
    if (axios.isCancel(error)) {
      console.log('请求已被取消:', error.message);
    } else {
      console.error('请求错误:', error);
    }
  });

// 在需要时取消请求
source.cancel('操作被用户取消');
  1. 使用 AbortController(Axios ≥ 0.22.0)
import axios from 'axios';

const controller = new AbortController();

axios.get('https://api.example.com/data', {
  signal: controller.signal
})
  .then(response => console.log(response.data))
  .catch(error => {
    if (axios.isCancel(error)) {
      console.log('请求已被取消');
    } else {
      console.error('请求错误:', error);
    }
  });

// 在需要时取消请求
controller.abort();
  1. React 组件中使用示例
import { useEffect } from 'react';
import axios from 'axios';

function MyComponent() {
  useEffect(() => {
    const controller = new AbortController();

    axios.get('https://api.example.com/data', {
      signal: controller.signal
    })
      .then(response => console.log(response.data))
      .catch(error => {
        if (axios.isCancel(error)) {
          console.log('组件卸载,请求取消');
        }
      });

    // 组件卸载时取消请求
    return () => controller.abort();
  }, []);

  return <div>组件内容</div>;
}

对比总结

特性 Fetch Axios
中断机制 AbortController CancelToken 或 AbortController
错误类型 AbortError Cancel 对象
React 集成 需要手动处理 提供 isCancel 方法检查
兼容性 现代浏览器 需要 Axios 版本适配
使用便捷性 较基础 更高级的 API

最佳实践

  1. 组件卸载时取消请求:在 React 的 useEffect cleanup 函数中取消请求
  2. 用户操作取消:为用户提供取消按钮,点击时中断请求
  3. 错误处理:区分取消错误和其他类型的错误
  4. 版本适配:注意 Axios 版本差异,新版推荐使用 AbortController
  5. 多个请求:可以共享同一个 AbortController 来取消多个请求
// 共享 AbortController 取消多个请求
const controller = new AbortController();

// 请求1
fetch('/api/data1', { signal: controller.signal });

// 请求2
fetch('/api/data2', { signal: controller.signal });

// 同时取消两个请求
controller.abort();

通过合理使用请求中断机制,可以提升应用性能和用户体验,避免不必要的网络请求和潜在的内存泄漏问题。

# 浏览器的本地缓存(*)

  1. Cookie

基本特性

  • 存储大小:约 4KB
  • 生命周期:可设置过期时间,或会话结束时过期
  • 自动发送:每次请求都会自动携带到服务器
  • 作用域:可设置域名和路径范围

使用方法

// 设置 Cookie
document.cookie = "username=John; expires=Thu, 18 Dec 2025 12:00:00 UTC; path=/";

// 读取 Cookie
function getCookie(name) {
  const cookies = document.cookie.split(';');
  for (let cookie of cookies) {
    const [key, value] = cookie.trim().split('=');
    if (key === name) return decodeURIComponent(value);
  }
  return null;
}

// 删除 Cookie
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";

使用场景

  • 用户身份认证
  • 会话管理
  • 简单的用户偏好设置
  1. Web Storage

localStorage

  • 存储大小:约 5-10MB(不同浏览器有差异)
  • 生命周期:永久存储,除非手动删除
  • 作用域:同一域名下的所有页面共享
// 存储数据
localStorage.setItem('user', JSON.stringify({ name: 'John', age: 30 }));

// 读取数据
const user = JSON.parse(localStorage.getItem('user'));

// 删除数据
localStorage.removeItem('user');

// 清空所有数据
localStorage.clear();

// 监听存储变化(在同源的其他页面)
window.addEventListener('storage', (event) => {
  console.log('存储发生变化:', event.key, event.newValue);
});

sessionStorage

  • 存储大小:约 5-10MB
  • 生命周期:会话结束时清除(关闭标签页或浏览器)
  • 作用域:仅当前标签页可用
// 使用方法与 localStorage 相同
sessionStorage.setItem('token', 'abc123');
const token = sessionStorage.getItem('token');

使用场景

  • localStorage:长期存储的用户设置、缓存数据
  • sessionStorage:临时会话数据、表单数据暂存
  1. IndexedDB

基本特性

  • 存储大小:通常至少 50MB,可请求更多空间
  • 数据类型:支持存储复杂对象、文件等
  • 事务支持:ACID 事务保证
  • 异步操作:所有操作都是异步的

使用方法

// 打开或创建数据库
const request = indexedDB.open('MyDatabase', 1);

request.onerror = (event) => {
  console.error('数据库打开失败:', event.target.error);
};

request.onsuccess = (event) => {
  const db = event.target.result;
  console.log('数据库打开成功');
};

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  
  // 创建对象存储空间(类似表)
  if (!db.objectStoreNames.contains('users')) {
    const store = db.createObjectStore('users', { keyPath: 'id' });
    store.createIndex('name', 'name', { unique: false });
  }
};

// 添加数据
function addUser(db, user) {
  const transaction = db.transaction(['users'], 'readwrite');
  const store = transaction.objectStore('users');
  store.add(user);
}

// 读取数据
function getUser(db, id) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const store = transaction.objectStore('users');
    const request = store.get(id);
    
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

使用场景

  • 大量结构化数据存储
  • 离线应用数据存储
  • 需要复杂查询的场景
  1. Cache API (Service Worker)

基本特性

  • 主要用于:缓存网络请求和响应
  • 存储大小:与存储空间相关
  • 生命周期:由 Service Worker 控制

使用方法

// 在 Service Worker 中
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('my-cache-v1').then((cache) => {
      return cache.addAll([
        '/',
        '/styles.css',
        '/script.js',
        '/image.jpg'
      ]);
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});



使用场景

  • PWA 应用的离线支持
  • 静态资源缓存
  • 网络请求的缓存策略
  1. 对比总结
技术 存储大小 生命周期 数据类型 同步/异步 主要用途
Cookie 4KB 可设置 字符串 同步 身份认证、会话管理
localStorage 5-10MB(Chrome/Chromium/Edge:通常为 5MB。Firefox:约为 10MB。Safari:一般在 4MB 到 5MB 之间。) 永久 字符串 同步 长期数据存储
sessionStorage 5-10MB(Chrome/Chromium/Edge:通常为 5MB。Firefox:约为 10MB。Safari:一般在 4MB 到 5MB 之间。) 会话期间 字符串 同步 临时会话数据
IndexedDB 大量 永久 复杂对象 异步 大量结构化数据
Cache API 大量 可控 请求响应 异步 网络资源缓存
  1. 最佳实践

选择合适的存储方案

// 小量简单数据 → localStorage
localStorage.setItem('theme', 'dark');

// 会话临时数据 → sessionStorage  
sessionStorage.setItem('formData', JSON.stringify(formValues));

// 大量复杂数据 → IndexedDB
const db = await openDB('app-database', 1);
await db.add('users', userData);

// 网络资源缓存 → Cache API
const cache = await caches.open('assets-v1');
await cache.add('/static/logo.png');

安全考虑

// 敏感数据不要存储在本地
// 使用 HTTPS 保护传输数据
// 考虑数据加密存储

// 简单的加密示例(实际应用应使用更安全的加密方式)
const encryptData = (data, key) => {
  // 实际应用中应使用 Web Crypto API
  return btoa(JSON.stringify(data) + key);
};

容量管理

// 检查存储空间
function checkLocalStorageSpace() {
  let total = '';
  for (let i = 0; i < 1024 * 1024; i++) {
    total += 'a';
  }
  try {
    localStorage.setItem('test', total);
    localStorage.removeItem('test');
    return true;
  } catch (e) {
    return false;
  }
}

// 清理过期数据
function cleanupOldData() {
  const now = Date.now();
  const data = JSON.parse(localStorage.getItem('cachedData') || '{}');
  
  Object.keys(data).forEach(key => {
    if (data[key].expiry && data[key].expiry < now) {
      delete data[key];
    }
  });
  
  localStorage.setItem('cachedData', JSON.stringify(data));
}
  1. 现代存储方案推荐

使用库简化操作

// 使用 localForage 简化 IndexedDB 操作
import localForage from 'localforage';

// 配置
localForage.config({
  name: 'myApp',
  storeName: 'keyvaluepairs'
});

// 使用类似 localStorage 的 API
await localForage.setItem('key', 'value');
const value = await localForage.getItem('key');

选择合适的浏览器存储技术取决于你的具体需求:数据大小、生命周期、复杂度等因素。

# promise(*)

Promise 是 JavaScript 中用于处理异步操作的对象,它解决了传统回调函数嵌套("回调地狱")的问题,让异步代码的逻辑更清晰、更易维护。

核心概念

Promise 代表一个尚未完成但最终会完成的操作,有三种状态:

  • pending(等待中):初始状态,操作尚未完成
  • fulfilled(已成功):操作完成并返回结果
  • rejected(已失败):操作出错并返回原因

状态一旦改变(从 pending → fulfilled 或 pending → rejected),就不会再变

promise的方法

实例方法(Promise 对象自身的方法)

  1. then()

    • 作用:处理 Promise 成功(fulfilled)或失败(rejected)的结果,返回一个新的 Promise,支持链式调用。

    • 语法:promise.then(onFulfilled, onRejected)

    • 参数:

      • onFulfilled:成功时的回调(接收 resolve 的结果)
      • onRejected:失败时的回调(可选,接收 reject 的原因)
  2. catch()

    • 作用:专门处理 Promise 失败(rejected)的情况,相当于 then(null, onRejected)
    • 语法:promise.catch(onRejected)
    • 用途:集中捕获链式调用中的所有错误,更清晰。
  3. finally()

    • 作用:无论 Promise 成功或失败,都会执行的回调(无参数)。
    • 语法:promise.finally(onFinally)
    • 用途:清理操作(如关闭加载动画、释放资源)。

静态方法(Promise 类自身的方法)

  1. Promise.all(iterable)

    • 作用:并行处理多个 Promise,等待所有都成功才返回结果数组;只要有一个失败,立即返回该失败原因。

    • 适用场景:需要所有异步操作完成后再执行下一步(如同时加载多个资源)。

  2. Promise.race(iterable)

    • 作用:并行处理多个 Promise,第一个状态改变(无论成功或失败) 的结果就是最终结果。

    • 适用场景:设置超时控制(如请求超时后使用默认值)。

  3. Promise.resolve(value)

    • 作用:快速创建一个已成功的 Promise,值为 value(若 value 本身是 Promise,则直接返回它)。

    • 用途:将非 Promise 对象转为 Promise,统一异步代码风格。

  4. Promise.reject(reason)

    • 作用:快速创建一个已失败的 Promise,原因是 reason
  5. Promise.allSettled(iterable)

    • 作用:等待所有 Promise 完成(无论成功或失败),返回一个包含所有结果的数组(每个结果包含状态和值)。

    • 适用场景:需要知道所有异步操作的最终状态(如批量任务统计成功 / 失败数量)。

这些方法覆盖了大多数异步处理场景,其中 then()catch()Promise.all()Promise.race() 是日常开发中最常用的。

优势

  • 解决回调地狱:通过 .then() 链式调用,替代多层嵌套
  • 错误处理统一:使用 .catch() 集中处理所有异步操作的错误
  • 状态明确:清晰区分异步操作的不同阶段

# Promise 和async await的区别

  • 语法差异:

    • promise:是链式调用 ;
    • async/await:是基于promise的语法糖,简洁
  • 错误处理方式:

    • promise:是then中的第二个参数或者catch方法来处理异步操作;
    • async/await是使用try/catch来捕获异常处理
  • 代码结构:

    • promise是链式调用可能会出现回调地狱;
    • async/await更加清晰明了
  • 性能上:性能差别不大,某些情况下 promise是更快一些

# JavaScript精度问题

主要由于 JavaScript 使用 IEEE 754 标准的浮点数表示数字,这种表示方式会产生一些精度损失。

这是因为二进制浮点数无法精确表示某些十进制小数,会产生微小的误差。

  1. 使用第三方库 decimal.js:使用的二进制来计算

    const a = 9.99;
    const b = 8.03;
    // 加法
    let c = new Decimal(a).add(new Decimal(b)) 
    // 减法
    let d = new Decimal(a).sub(new Decimal(b))
    // 乘法
    let e = new Decimal(a).mul(new Decimal(b))
    // 除法
    let f = new Decimal(a).div(new Decimal(b))
    
    let c = a.plus(b); // c 的值是 Decimal(0.3)
    // 比较浮点数
    console.log(c.equals(new Decimal(0.3))); // true
    
  2. 在货币计算中使用固定小数位

    let price = 9.99;
    let quantity = 3;
    let total = (price * quantity).toFixed(2); // "29.97"
    console.log(typeof total); // "string"
    
  3. 整数运算

    let a = 0.1;
    let b = 0.2;
    let result = (a * 10 + b * 10) / 10; // 0.3
    

# 强缓存和协商缓存(*)

  1. 强缓存(强制缓存):

    • 强缓存是指浏览器在请求资源时,先检查本地缓存(本地磁盘缓存)是否存在该资源的有效副本,并根据缓存规则决定是否直接使用缓存副本,而不发送请求到服务器。

    • 浏览器在收到服务器返回的响应头中的缓存控制字段(如Expires、Cache-Control)后,会根据这些字段判断是否使用强缓存。

    • 如果资源未过期,浏览器直接从本地缓存中加载资源,不会向服务器发送请求,从而加快页面加载速度。

      前端代码:

      const getApi = async () => {
      	let res = await axios.get('http://localhost:3000/api/dev')
      	console.log(res)
      }
      onBeforeMount(async() => {
      	getApi()
      })
      

      后端代码:

      router.get('/api/dev', (req, res, next) => {
          res.setHeader('Cache-Control', max-age-3600')	// 缓存一小时
          res.json({a:1})
      })
      
  2. 协商缓存:

    • 当浏览器没有命中强缓存时就会发送请求,验证协商缓存是否命中,如果缓存命中则返回304状态码,否则返回新的资源数据

      前端代码:

      const getApi = async () => {
      	let res = await axios.get('http://localhost:3000/api/dev', {
      		headers: {
      			'If-None-Match': localStorage.getItem('ETag') || ''
      		}
      	})
      	if(res.status == 200){
      		console.log(res, res.headers.etag)
      		localStorage.setItem('ETag', res.headers.etag)
      	}
      }
      onBeforeMount(async() => {
      	getApi()
      })
      

      后端代码:

      let data = { name: '张三'}
      function generateETag(data){
          const hash = crypto.createHash('md5').update(JSON.stringify(data))
          return `"${hase}"`
      }
      let currentETag = generateETag(data)
      router.get('/api/data', (req, res, next) => {
          if(req.headers['If-None-Match'] === currentETag){
              res.status(304).end()
          } else {
              res.send('ETag', currentETag)
              res.json(data)
          }
      })
      

      具体请求流程:

      当浏览器发起一个资源请求时,浏览器会先判断本地是否有缓存记录,如果没有会向浏览器请求新的资源。

      如果有缓存记录,先判断强缓存是否存在,如果强缓存的时间没有过期则返回本地缓存资源(状态码为200).

      如果强缓存失败了,客户端会发起请求进行协商缓存策略,首先服务端判断头信息标识符,如果客户端传来标识符和当前服务器上的标识符是一致的,则返回状态码304(也就是不返回资源内容)。

      如果etag和服务器上的标识符不一致,重新获取新的资源,并进行协商缓存返回数据。

# ES6新特性

  1. let, const
  2. for-of
  3. arguments
  4. promise
  5. Set(), Map()
  6. Proxy
  7. Generator
  8. 模板字符串
  9. 箭头函数
  10. 扩展运算符
  11. 解构赋值

# 构造函数新增的方法

Array.from()
Array.of()

# 实例对象新增的方法

copyWithin()
find()、findIndex()
fill()
entries(),keys(),values()
includes()
flat(),flatMap()

# JS 数据类型 ?存储上的差别?(*)

数据类型主要包括两部分:

基本数据类型: Undefined、Null、bigInt、Boolean、Number 和 String,Symbol(创建后独一无二且不可变的数据类型 )

引用数据类型: Object (包括 Object 、Array 、Function,Date,RegExp,Set,Map)

存储区别:

  • 内存分配:

​ 基本数据类型(如numberstring)存储在栈中:因其占用空间固定且较小,适合在栈中快速分配内存,便于高效访问。 ​ 引用类型(如对象、数组)存储在堆中:变量仅在栈中保存指向堆内存的地址(指针),由于引用值大小不固定,堆存储可动态分配内存,避免影响栈的访问效率。

  • 访问机制:不同的内存分配机制也带来了不同的访问机制

    - 基本类型:直接访问栈中的值(按值访问)。

    • 引用类型:无法直接访问堆中对象,需先通过栈中的地址找到堆内存位置,再获取对象值(按引用访问)。
  • 复制变量时的不同   - 基本类型:复制时会生成原始值的独立副本,两个变量此后互不影响(值相同但存储独立)。   - 引用类型:复制的是栈中的地址指针,两个变量指向堆中同一个对象,任何一方修改都会影响另一方(共享同一份数据)。

# null 和 undefined 的区别

相同点:

​ 都是原始类型的值,且保存在栈中 ​ 进行条件判断时,两者都是false:

不同点:

  • null是js的关键字,表示空值;undefined不是js的关键字,它是一个全局变量
  • null是Object的一个特殊值,如果一个Object为null,表示这个对象不是有效对象,null是一个不存在的对象的占位符;undefined是Globel的一个属性
  • 类型不一样:
 typeof(null) // object
 typeof(undefined) //undefined

 console.log(typeof(null) === 'object')//true
 console.log(typeof(undefied) === 'undefined')//true
  • 转换的值不一样:
console.log(Number(undefined));//NaN
console.log(Number(11+ undefined));//NaN

console.log(Number(null));//0
console.log(Number(11+ null));//11

# 跨域

什么是“域”:域,也叫做“源”。一个完整的域由三部分组成:协议,IP,端口。

什么是跨域:指的就是在一个域中向另一个不同的域发送请求。两个域之间,协议、IP、端口三者中有任意一个不一样,则判定为是两个不同的域。

为什么存在跨域:基于JavaScript的同源策略,该策略要求,在浏览器中,只能进行同源访问,不能进行跨域请求。

跨域的方式:

  • jsonp(只支持get请求)

    • jsonp的原理就是利用<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。

       <script>
          var script = document.createElement('script');
          script.type = 'text/javascript';
       
          // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
          script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
          document.head.appendChild(script);
       
          // 回调执行函数
          function handleCallback(res) {
              alert(JSON.stringify(res));
          }
       </script>
      
    • $ajax中的使用方式

      $.ajax({
          url: 'http://localhost:3000/teachers/getTeacherInfo',
          type: 'GET',
          dataType: 'jsonp',		// 重点
          success(res) {
              console.log('getTeacherInfo', res);
          }
      })
      
      // express后端返回时,将res.send()换成res.jsonp(返回数据)
      
  • proxy

  • websocket

    • WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,允许客户端和服务器之间进行实时数据传输。与传统的 HTTP 请求-响应模式不同,WebSocket 可以实现服务器主动向客户端推送数据,实现实时更新和即时通讯等功能。

    • 前端方法:

      • onopen():当 WebSocket 连接成功建立时触发的事件。
      • onmessage 事件:当接收到服务端发送的消息时触发的事件。
      • send() 方法:用于向服务端发送消息。
      • close() 方法:用于关闭 WebSocket 连接。
      • onerror 事件:当 WebSocket 连接发生错误时触发的事件。
    • 后端方法:

      • connection 事件:当客户端与服务器建立连接时触发的事件。
      • message 事件:当接收到客户端发送的消息时触发的事件。
      • send() 方法:用于向客户端发送消息。
      • close() 方法:用于关闭与客户端的连接。
      • error 事件:当 WebSocket 服务器发生错误时触发的事件。
    • 缺陷以及优化

      • WebSocket 作为一种实时通信协议,虽然具有很多优势,但也存在一些缺陷和需要优化的地方,主要包括:

        1. 兼容性问题:

          • WebSocket 协议需要浏览器和服务器端都支持,对于老版本的浏览器和服务器可能存在兼容性问题。
          • 为了解决兼容性问题,通常需要采用 WebSocket 回退机制,如 Socket.IO 等库。
        2. 资源消耗问题:

          • WebSocket 连接会一直保持打开状态,这可能会增加服务器和客户端的资源消耗。
          • 需要采取措施,如实现心跳机制、连接池管理等来优化资源使用。
        3. 安全问题:

          • WebSocket 连接可能存在安全隐患,如跨站脚本攻击(XSS)、跨站请求伪造(CSRF)等。
          • 需要采取安全措施,如验证消息来源、过滤输入输出、使用 SSL/TLS 加密等。
        4. 消息丢失问题:

          • 由于 WebSocket 连接可能会断开,导致消息丢失的问题。
          • 需要采取措施,如实现消息重发、消息缓存等机制来保证消息的可靠性。
  • nginx:反向代理

  • cors:跨域资源分享

    // CORS 配置跨域
    var allowCrossDomain = function (req, res, next) {
        // 设置允许跨域访问的请求源(* 表示接受任意域名的请求)   
        res.header("Access-Control-Allow-Origin", "*");
        // 设置允许跨域访问的请求头
        res.header("Access-Control-Allow-Headers", "X-Requested-With,Origin,Content-Type,Accept,Authorization");
        // 设置允许跨域访问的请求类型
        res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
        // 同意 cookie 发送到服务器(如果要发送cookie,Access-Control-Allow-Origin 不能设置为星号)
        res.header('Access-Control-Allow-Credentials', 'true');
        next();
    };
    app.use(allowCrossDomain);
    

# 箭头函数的特点

  • 不需要function关键字来创建函数,省略return关键字
  • 箭头函数没有this,箭头函数中使用this是指向的外层普通函数的this,如果外层没有普通函数,则指向的是全局的this
  • 箭头函数是匿名函数,不能作为构造函数,不可以使用new命令,否则后抛出错误。
  • 箭头函数没有原型对象prototype这个属性
  • 箭头函数不绑定arguments,取而代之用rest参数解决,同时没有super和new.target。
  • 使用call,apply,bind并不会改变箭头函数中的this指向。
  • 不能使用yield关键字,不能用作Generator函数

# var、let、const 区别

变量提升: var声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined let和const不存在变量提升,即它们所声明的变量一定要在声明后使用,(暂时性死区)

块级作用域: var不存在块级作用域 let和const存在块级作用域

重复声明: var允许重复声明变量 let和const在同一作用域不允许重复声明变量

修改声明的变量: var和let可以 const声明一个只读的常量。一旦声明,常量的值就不能改变,但对于对象和数据这种引用类型,内存地址不能修改,可以修改里面的值。

# new操作符具体干了什么?

  • 新建一个实例对象obj

  • 把obj实例对象和构造函数通过原型链连接起来

  • 将构造函数的this指向obj

  • 根据构造函数返回的类型做判断,如果是原始值则被忽略,如果是返回对象,需要正常使用

    function Test(name){
    	this.name = name
    	return 1;
    }
    const test = new Test('张三')
    console.log(test.name)		// 张三
    
    function Test(name){
    	this.name = name
    	return { name: '李四'};
    }
    const test = new Test('张三')
    console.log(test.name)		// 李四
    

# 手写new

/**
 * new操作符都干了什么?
 * 1. 创建一个空对象
 * 2. 将实例对象的__proto__指向构造函数的prototype
 * 3. 将构造函数的this指向实例对象
 * 4. 返回实例对象
 * 注意:
 * 1. 构造函数的返回值如果是基本类型,则返回实例对象;如果是引用类型,则返回该引用类型
 */


// 引入 MyNew 函数,用于创建一个新的函数
function MyNew(Func, ...args) {
    // 创建一个新的对象
    const obj = {};
    // 将新对象的 prototype 设置为传入的 Func 函数的 prototype
    obj.__proto__ = Func.prototype;
    // 调用 Func 函数,并将 args 作为参数传入,获取返回值
    const result = Func.apply(obj, args);
    // 如果返回值是对象,则直接返回;否则返回新对象
    return typeof result === 'object' ? result : obj;
}
// 定义 Person 函数,用于设置人的名字和年龄
function Person(name, age) {
    // 将名字和年龄绑定到 this 上
    this.name = name;
    this.age = age;
}
// 定义 Person 的原型方法,用于输出人的信息
Person.prototype.sayHello = function() {
    console.log('Hello, my name is ' + this.name + ', I am ' + this.age + ' years old.');
}
// 使用 MyNew 函数创建一个 Person 对象
const person = MyNew(Person, 'John', 25);
person.sayHello()

# 浅拷贝

指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝 , 两个对象指向同一个地址

Object.assign,扩展运算符,Array.concat,Array.slice

var obj = {
	name: 'John',
	age: 30,
	sports: ['football', 'basketball'],
	hobbies: {
		sports: ['football', 'basketball'],
		music: ['rock', 'jazz'],
		books: {
			fiction: ['novel', 'mystery'],
			poetry: ['shi', 'song']
		}
	}
}
const aa = Object.assign({}, obj);

浅拷贝在修改对象属性时,只能修改对象中第一层的基本数据类型,修改第一层的引用数据类型以及嵌套属性,均会受到影响

# 深拷贝

深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性

  • 封装一个递归方法
  • JSON.stringify + JSON.parse
  • lodash(_.cloneDeep())
  • MessageChannel()

# 作用域

作用域指的是变量、函数和对象的可访问范围,它决定了代码中标识符(如变量名)的可见性和生命周期。

分类:

  • 全局作用域;

    // 全局变量
    var greeting = 'Hello World!';
    function greet() {
      console.log(greeting);
    }
    // 打印 'Hello World!'
    greet();
    
  • 函数作用域;

    function greet() {
      var greeting = 'Hello World!';
      console.log(greeting);
    }
    // 打印 'Hello World!'
    greet();
    // 报错: Uncaught ReferenceError: greeting is not defined
    console.log(greeting);
    
  • 块级作用域;

    {
      // 块级作用域中的变量
      let greeting = 'Hello World!';
      var lang = 'English';
      console.log(greeting); // Prints 'Hello World!'
    }
    // 变量 'English'
    console.log(lang);
    // 报错:Uncaught ReferenceError: greeting is not defined
    console.log(greeting);
    
  • 词法作用域

    let number = 42;
    function printNumber() {
        console.log(number);
    }
    function log() {
        let number = 54;
        printNumber();
    }
    log(); // 42
    

作用域链

概念:多层函数嵌套下,变量的访问范围,访问变量时,先在当前作用域下查找变量,如果当前作用域下没有则逐级向上查找,知道全局作用域

# 原型,原型链

  • prototype 每个函数都有一个prototype属性,被称为显示原型
  • __proto__ 每个实例对象都会有__proto__属性,其被称为隐式原型 每一个实例对象的隐式原型__proto__属性指向自身构造函数的显式原型prototype
  • constructor 每个prototype原型都有一个constructor属性,指向它关联的构造函数。
function Parent(month){
    this.month = month;
}
// Parent.prototype.father = '爸爸'
Object.prototype.father = '爸爸'
var child = new Parent('Ann');
// console.log(child.month); // Ann

// console.log(child.father); // undefined

原型链(Prototype Chain)

原型链是 JavaScript 中对象通过__proto__属性串联起来的继承关系链,终点为null,决定了对象属性和方法的查找规则。

如:当访问一个对象的属性时:

  1. 首先在对象自身查找该属性
  2. 如果找不到,则去对象的proto(即构造函数的prototype)中查找
  3. 如果还找不到,则继续去原型对象的proto中查找
  4. 直到找到Object.prototype(所有对象的最终原型),如果仍然找不到则返回undefined

u=1564045444,121577099&fm=253&fmt=auto&app=138&f=PNG

# 事件代理

事件代理(Event Delegation),又称之为事件委托。是 JavaScript 中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。使用事件代理的好处是可以提高性能,可以大量节省内存占用,减少事件注册,比如在table上代理所有td的click事件就非常棒 可以实现当新增子对象时无需再次对其绑定

# this

this 是 JavaScript 中的关键字,它指向当前代码执行时的上下文对象,具体指向谁完全由调用方式决定,而非定义位置。

  • 如果单独使用,this 表示全局对象。

  • 在方法中,this 表示该方法所属的对象。

  • 在函数中,this 表示全局对象。

  • 在事件中,this 表示接收事件的元素。

  • 严格模式下:

    • 单独使用,this指向全局对象

    • 在函数中,this 是未定义的(undefined)。

# 数组sort的底层原理,怎么实现

1.JavaScript中的sort()方法是用来对数组元素进行排序的。它使用了一种名为快速排序(QuickSort)的算法来进行排序。这个算法的基本思路是:选择一个基准元素,将数组分成两个子数组,比基准元素小的放在左边,比基准元素大的放在右边,然后再对左右两个子数组分别进行排序。这个过程一直递归下去,直到整个数组有序。 2.具体实现中,sort()方法会先将数组分成两个子数组,然后对每个子数组分别进行排序。在排序的过程中,sort()方法会比较数组元素的大小,然后通过交换元素的位置来实现排序。在排序完成后,sort()方法会返回排序后的数组。

# JavaScript中,实现继承的方式

  1. 原型链继承:

    function Parent() {
      this.name = 'Parent';
    }
    
    Parent.prototype.sayHello = function() {
      console.log('Hello from ' + this.name);
    }
    
    function Child() {
      this.name = 'Child';
    }
    
    Child.prototype = new Parent();
    
    var child = new Child();
    child.sayHello(); // Output: Hello from Child
    
  2. 构造函数继承(借用构造函数):

    function Parent() {
      this.name = 'Parent';
      this.sayHello = function() {
        console.log('Hello from ' + this.name);
      }
    }
    Parent.prototype.sayHello = function() {
        
    }
    function Child() {
      Parent.call(this);
      this.name = 'Child';
    }
    
    var child = new Child();
    child.sayHello(); // Output: Hello from Child
    

    在构造函数内部声明的方法和prototype上的方法之间的区别:

    对比点 构造函数内声明 prototype 上声明
    存储位置 每个实例自身 所有实例共享
    内存占用 高(每个实例一份) 低(只存一份)
    访问优先级 ✅ 更高 ❌ 更低
    是否可访问 prototype 版本 ❌ 被覆盖 ✅ 仅当实例没有该方法
    适合用途 需要私有/实例级状态 公共方法
  3. 组合继承(原型链继承 + 构造函数继承):

    function Parent() {
      this.name = 'Parent';
    }
    
    Parent.prototype.sayHello = function() {
      console.log('Hello from ' + this.name);
    }
    
    function Child() {
      Parent.call(this);
      this.name = 'Child';
    }
    
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
    
    var child = new Child();
    child.sayHello(); // Output: Hello from Child
    
  4. 原型式继承:

    function createObject(obj) {
      function F() {}
      F.prototype = obj;
      return new F();
    }
    
    var parent = {
      name: 'Parent',
      sayHello: function() {
        console.log('Hello from ' + this.name);
      }
    };
    
    var child = createObject(parent);
    child.name = 'Child';
    
    child.sayHello(); // Output: Hello from Child
    
  5. ES6中的类继承(extends关键字):

    class Parent {
      constructor() {
        this.name = 'Parent';
      }
    
      sayHello() {
        console.log('Hello from ' + this.name);
      }
    }
    
    class Child extends Parent {
      constructor() {
        super();
        this.name = 'Child';
      }
    }
    
    var child = new Child();
    child.sayHello(); // Output: Hello from Child
    

# apply,bind,call的区别

apply bind call
参数 (this, []) (this, 参数列表) (this, 参数列表)
执行时机 立即执行 不会立即执行 立即执行
执行时限 只临时改变this一次 返回一个永久改变this指向的函数 临时改变this指向一次
  • 三者都可以改变函数的this对象指向
  • 三者第一个参数都是this要指向的对象,如果没有这个参数或参数为undefinednull,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call是参数列表,且applycall是一次性传入参数,而bind可以分为多次传入
  • bind是返回绑定this之后的函数,applycall 则是立即执行

# 防抖和节流

  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效

    • function throttle(fn, interval = 300) {
        let lastTime = 0;
      
        return function (...args) {
          const now = Date.now();
          const context = this;
      
          if (now - lastTime >= interval) {
            fn.apply(context, args);
            lastTime = now;
          }
        };
      }
      window.addEventListener('scroll', throttle(() => {
        console.log('scrolling...');
      }, 500));
      
  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

    • function debounce(fn, delay = 300) {
        let timer = null;
      
        return function (...args) {
          const context = this;
      
          clearTimeout(timer);
          timer = setTimeout(() => {
            fn.apply(context, args);
          }, delay);
        };
      }
      const input = document.querySelector('#input');
      
      input.addEventListener('input', debounce((e) => {
        console.log(e.target.value);
      }, 500));
      

一个经典的比喻:

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应

假设电梯有两种运行策略 debouncethrottle,超时设定为15秒,不考虑容量限制

电梯第一个人进来后,15秒后准时运送一次,这是节流

电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖

# 单点登录

SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统

SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过passport,子系统本身将不参与登录操作

# web常见攻击

  • xss(跨站脚本攻击)
    • 存储型XSS
      • 攻击者将恶意代码提交到目标网站的数据库中
      • 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器
      • 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行
      • 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作
    • 反射型XSS
    • DOM型XSS
    • 预防:
  • CSRF(跨站请求伪造)
  • SQL注入攻击

# typeof 与 instanceof 区别

  1. 功能定位
  • typeof:返回一个字符串,表示操作数的基本类型(如 "number"、"string"、"object" 等),主要用于检测基础数据类型。
  • instanceof:返回布尔值,判断某个对象是否是指定构造函数的实例(基于原型链判断),主要用于检测引用类型的具体类型。
  1. 适用场景与局限性
  • typeof
    • 能准确识别基础类型(number/string/boolean/undefined/symbol),但对 null 会误判为 "object"。
    • 对引用类型,除 function 会返回 "function" 外,其他(如数组、对象、日期)均返回 "object",无法区分具体类型。
  • instanceof
    • 能精准判断引用类型(如 [1,2] instanceof Array → truenew Date() instanceof Date → true)。
    • 无法判断基础数据类型(如 123 instanceof Number → false,因基础类型不是对象实例)。
    • 结果受原型链影响(如 [] instanceof Object → true,因数组是对象的子类型)。
  1. 本质差异
  • typeof 基于值的类型标签判断(底层类型编码),操作数可以是任意类型。
  • instanceof 基于原型链查找,检测构造函数的 prototype 是否存在于对象的原型链上,仅适用于对象。

# 判断是否为数组的5种方法?

  • instanceof: data instanceof Array
  • constructor: data.constructor == Array
  • Array.isArray(): Array.isArray(data) 最推荐
  • Object.prototype.toSrtring.call()

# 哪些操作会导致内存泄漏

内存泄漏: JavaScript 内存泄露指对象在不需要使用它时仍然存在,导致占用的内存不能使用或回收

  • 未使用 var 声明的全局变量

  • 闭包函数(Closures)

  • 循环引用(两个对象相互引用)

  • 控制台日志(console.log)

  • 移除存在绑定事件的DOM元素(IE)

  • 定时器未清除

    // ❌ 定时器未清理
    setInterval(() => {
        // 每次都创建新对象
        data = new LargeData();
    }, 100);
    
    // ✅ 及时清除
    const timer = setInterval(() => {...}, 100);
    clearInterval(timer);
    
  • 事件监听未移除

    // ❌ 重复添加未移除
    element.addEventListener('click', handler);
    
    // ❌ 移除时没用引用
    element.removeEventListener('click', handler); // 必须是同一个函数引用
    
  • 全局变量

    // ❌ 全局变量无法被回收
    var globalData = new LargeData();
    
    // ✅ 减少全局变量,使用 let/const
    let localData = new LargeData();
    
  • Map/Set缓存未清理

    const cache = new Map();
    function process(data) {
        if (!cache.has(data)) {
            cache.set(data, heavyComputation(data));
        }
        return cache.get(data);
    }
    // cache 无限增长
    
    

# Javascript中如何实现函数缓存?函数缓存有哪些应用场景?

函数缓存,就是将函数运算过的结果进行缓存

实现方式: 实现函数缓存主要依靠闭包、柯里化、高阶函数

应用场景:

  • 对于昂贵的函数调用,执行复杂计算的函数
  • 对于具有有限且高度重复输入范围的函数
  • 对于具有重复输入值的递归函数
  • 对于纯函数,即每次使用特定输入调用时返回相同输出的函数

# 闭包和闭包常用场景

闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包常见方式,就是在一个函数的内部创建另一个函数 闭包有三个特性:

  • 函数内再嵌套函数
  • 内部函数可以引用外层的参数和变量
  • 参数和变量不会被垃圾回收机制回收

闭包的好处:

  • 能够实现封装和缓存等;

闭包的缺点:

  • 就是常驻内存,会增大内存使用量,使用不当会造成内存泄漏

应用场景:

  • 常见的防抖节流
  • 使用闭包可以在 JavaScript 中模拟块级作用域
  • 闭包可以用于在对象中创建私有变量

# 用过哪些设计模式?

工厂模式: 工厂模式解决了重复实例化的问题,但还有一个问题,那就是识别问题 主要好处就是可以消除对象间的耦合,通过使用工程方法而不是new关键字 构造函数模式: 使用构造函数的方法,即解决了重复实例化的问题,又解决了对象识别的问题,该模式与工厂模式的不同之处在于,直接将属性和方法赋值给 this对象;

# web开发中会话跟踪的方法有哪些?

  • cookie
  • session
  • url重写
  • 隐藏input
  • ip地址

# for…in 和 for … of区别

for…in 循环是用来遍历对象属性的,它可以枚举目标对象的所有可枚举属性,包括继承链上的属性,但遍历的顺序是不确定的

for…of 循环是用来遍历可迭代对象 (Iterable) 的,它可以遍历数组、字符串、Map、Set 等内置的可迭代对象,但不能遍历普通的对象,也不能遍历对象的属性

区别:

  • for…in遍历数组返回下标,遍历对象返回键
  • for…of遍历数组返回数据,不可以遍历普通对象

# 如何判断一个对象为空对象

通过 Object.keys(obj) 方法获取对象的所有属性名,并判断属性数量是否为 0 来实现 、

let obj = {'name':'zs'}
Object.keys(obj).length     //1
let objs = {}
Object.keys(objs).length	//0

# 如何让多个异步函数顺序执行

  • 使用 Promisethen 方法链接异步任务。
  • 使用 async/await 关键字。

Cookie:Cookie是在用户端存储数据的一种机制,它可以存储一些简单的用户信息和标识。服务器通过设置Cookie并发送到客户端,在下次请求时客户端会自动携带该Cookie,从而实现对用户身份的验证或其他操作。Cookie的缺点是可能面临安全问题,因为Cookie存储在客户端,容易遭受窃取或伪造攻击。 Session:Session是在服务器端存储数据的一种机制,它可以保存一些复杂的用户信息和状态,用于实现对用户身份的验证和跟踪。服务器使用一个唯一的Session ID来和客户端进行交互,从而避免了安全性问题。但是Session也存在一些缺点,例如对服务器负载压力较大等问题。 Token:Token是一种包含用户身份和权限信息的加密字符串,通常由服务器生成并发送给客户端。客户端使用Token代替Cookie或Session来进行身份验证和数据传输。Token的优点是可以减轻服务器压力,减少网络流量和延迟,并且减少了安全性问题。Token的缺点是需要保证其加密和传输的安全性。 Cookie适用于简单的身份验证和数据存储,Session适用于需要复杂状态管理和用户跟踪的场景,而Token则更适合于分布式系统和APP等跨平台的数据传输。

# 导致页面加载白屏时间长的原因有哪些,怎么进行优化

原因:

大量 HTTP 请求:

​ 在页面加载过程中,浏览器需要请求服务器获取页面的 HTML、CSS、JavaScript、图片等资源,如果请求过多,会导致页面加载时间变长。可以通过减少 HTTP 请求的数量来优化加载速度,例如合并 CSS 和 JavaScript 文件,压缩图片等。

大量 JavaScript 代码:

​ 当浏览器下载并解析 JavaScript 代码时,页面的渲染会被阻塞,这也会导致页面加载时间变长。可以通过将 JavaScript 代码异步加载、延迟加载或分割成多个小文件来优化加载速度。

大量 CSS 代码:

​ 与 JavaScript 类似,CSS 代码也会阻塞页面渲染,可以通过压缩 CSS 代码、减少 CSS 文件的大小和数量、使用外部链接等方法来优化加载速度。

服务器响应时间过长:

​ 如果服务器响应时间过长,也会导致页面加载时间变长。可以通过升级服务器硬件、优化代码等方式来减少服务器响应时间。 不合理的 DOM 结构:如果页面的 DOM 结构不合理,也会导致页面加载时间变长。可以通过减少 DOM 节点数量、避免使用 table 布局、使用 CSS Sprite(雪碧图) 等方式来优化加载速度。

优化:

  • 压缩 HTML、CSS、JavaScript、图片等资源,减少文件大小。
  • 合并 CSS 和 JavaScript 文件,减少 HTTP 请求的数量。
  • 将 JavaScript 代码异步加载、延迟加载或分割成多个小文件。
  • 使用浏览器缓存,避免重复下载资源。
  • 使用外部链接或 CDN 加速器等方式来加速资源加载。
  • 减少 DOM 节点数量,避免使用 table 布局等方式来优化页面渲染速度。

# javascript 代码中的"use strict";是什么意思?说说严格模式的限制

use strict是一种ECMAscript 5 添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行,使JS编码更加规范化的模式,消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为 限制:

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性
  • 不能使用with语句
  • 禁止this指向window
本章目录