JavaScript面试题

发布于:2025-03-13 ⋅ 阅读:(26) ⋅ 点赞:(0)

文章目录

执行上下文

  1. 上下文
    1. 抽象概念,包含当前代码执行的环境信息。
    2. 类型:
      1. 概述:全局、函数、eval
      2. 全局:由 js 引擎默认创建,执行任何代码前创建的一个执行上下文。整个 js 程序只有一个,包含了全局信息
      3. 函数上下文:每当执行函数时都会创建此执行上下文。包含函数的参数、局部变量、this 等信息。
      4. eval:是在一个eval函数中创建的,可将字符串当做 js 代码来执行。由于存在安全风险和性能问题,所以没有深入研究【向面试官请教】
  2. 执行上下文创建(这是一个复杂的过程,没理解)
  3. 补充(如果想起来就提及):补充细节全局上下文里有哪些预定义的东西?函数执行上下文创建过程中变量提升是什么?
    1. 全局上下文中域定义的是window对象它包含 document、location 等。通过var声明的全局变量和函数声明,都会成为window对象的属性和方法。
    2. 函数上下文创建时的变量提升,就是函数内部用var声明的变量和函数声明被提升到函数顶部,但只是声明提升,赋值不会提升【链接到变量提升】

作用域

  1. js中的作用域作用域链
    1. 作用域概念:规定了哪些代码区域可以访问哪些变量、函数
    2. 全局作用域、局部作用域
    3. 全局作用域:
      1. 最外层函数和在最外层函数外面定义的变量拥有全局作用域
      2. 所有未定义直接赋值的变量自动声明为拥有全局作用域(在非严格模式下)
      3. 所有window对象的属性拥有全局作用域(在浏览器环境中)
    4. 局部作用域:函数内部
    5. 块级作用域:<font style="color:rgba(0, 0, 0, 0.85);">if</font><font style="color:rgba(0, 0, 0, 0.85);">while</font><font style="color:rgba(0, 0, 0, 0.85);">for</font> 等语句用 <font style="color:rgba(0, 0, 0, 0.85);">{}</font> 包裹部分构成块级作用域。在其中用 <font style="color:rgba(0, 0, 0, 0.85);">let</font><font style="color:rgba(0, 0, 0, 0.85);">const</font> 声明的变量,仅在该块级作用域内可访问;而 <font style="color:rgba(0, 0, 0, 0.85);">var</font> 声明的变量无此块级作用域特性。
    6. 作用域链:
      1. 访问一个变量,首先在当前作用域中查找,如果找不到,就会沿着作用域链向上层作用域查找,直到找到该变量或者到达全局作用域为止。这个查找变量的路径就是作用域链。

闭包

  1. 闭包
    1. 一个函数访问了此函数的父级及父级以上的作用域变量,就形成了闭包。
    2. 由于闭包的存在,即使创建闭包的函数已经执行完毕,其内部的变量也不会被垃圾回收机制回收,而是会一直存在于内存中,直到闭包被销毁。
    3. 作用:
      1. 私有变量和方法,实现数据的封装和隐藏
      2. 缓存数据,保持状态
    4. 问题:内存泄漏
    5. 应用:Vue的事件处理函数、防抖节流 【还有很多】

原型

  1. 原型
    1. 每个对象(除null外)在创建时,都会创建与之关联的另一个对象,这个关联的对象就是我们所说的原型。
    2. 目的:实现继承和共享属性的机制
    3. 显式原型:<font style="color:rgba(0, 0, 0, 0.85);">prototype</font>,隐式原型:<font style="color:rgba(0, 0, 0, 0.85);">__proto__</font>
    4. 当使用构造函数创建实例对象时,实例对象会通过隐式原型(proto)属性关联到该构造函数的显式原型(prototype)
    5. 原型链:每一个实例化对象都有一个__proto__属性,这个__proto__属性指向构造该对象的构造函数的原型对象。而原型对象本身也是一个对象,它也有一个__proto__属性,这样就形成了一个链式结构,一层一层往上找,直到找到Object.prototype

this

  1. this指向
    1. 概念:this是一个指针型变量,它动态指向当前函数的运行环境

    2. 特点:this 的值取决于函数的调用方式,而不是定义方式。

    3. 绑定方式:显示绑定、隐式绑定;尽可能进行显示绑定

    4. 简单来讲:**this** 的指向取决于“谁调用”

      1. 普通函数:看函数是怎么被调用的。
        1. 如果是 obj.func()this 就是 obj
        2. 如果是直接调用 func()this 就是全局对象或 undefined(严格模式)。
      2. 箭头函数:看函数定义时的外层作用域的 this
        1. 箭头函数的 this 是固定的,不会因为调用方式而改变。
    5. this指向的几种情况

      1. 全局环境下的this指向:this始终指向全局对象window
      2. 函数内的this指向:
        1. 直接调用:this 指向全局
        2. 通过对象调用,this 指向调用的对象
      3. 箭头函数中的this指向:箭头函数本身没有 this,在内部使用 this指向外层作用域的 this
      4. 对象中的this指向:
        1. 对象内部普通函数 this 指向对象;
        2. 如果函数被赋值给其他变量并调用。如const fn = obj.method; fn()this 会丢失,指向全局
        3. 对象内部箭头函数 this 指向全局或者构造函数实例
      5. 构造函数中的this:构造函数中的this是指向实例
      6. DOM 事件处理器中的 this:
        1. 普通函数 this 指向触发事件的元素
        2. 箭头函数 this 指向定义时的上下文
      7. 类中 this
        1. 实例方法:指向类的实例
        2. 静态方法:指向类本身(构造函数)
        3. 箭头函数:
          1. 定义在构造函数中,this 指向实例
          2. 定义在类属性中,this 指向外层(通常是全局)
      8. 高阶函数中的 this
        1. call、apply 或 bind 调用函数

前五节总结
  1. 上下文
    1. 抽象概念,包含当前代码执行的环境信息。
    2. 类型:
      1. 概述:全局、函数、eval
      2. 全局:由 js 引擎默认创建,执行任何代码前创建的一个执行上下文。整个 js 程序只有一个,包含了全局信息
      3. 函数上下文:每当执行函数时都会创建此执行上下文。包含函数的参数、局部变量、this 等信息。
      4. eval:是在一个eval函数中创建的,可将字符串当做 js 代码来执行。由于存在安全风险和性能问题,所以没有深入研究向面试官请教】
  1. 执行上下文创建(这是一个复杂的过程,没理解
  2. js中的作用域作用域链
    1. 作用域概念:规定了哪些代码区域可以访问哪些变量、函数
    2. 全局作用域、局部作用域
    3. 全局作用域:
      1. 最外层函数和在最外层函数外面定义的变量拥有全局作用域
      2. 所有未定义直接赋值的变量自动声明为拥有全局作用域(在非严格模式下)
      3. 所有window对象的属性拥有全局作用域(在浏览器环境中)
    1. 局部作用域:函数内部
    2. 作用域链:
      1. 访问一个变量,首先在当前作用域中查找,如果找不到,就会沿着作用域链向上层作用域查找,直到找到该变量或者到达全局作用域为止。这个查找变量的路径就是作用域链。
  1. 闭包【这个再去看看哔站视频讲的阿里题】
    1. 一个函数访问了此函数的父级及父级以上的作用域变量,就形成了闭包。
    2. 由于闭包的存在,即使创建闭包的函数已经执行完毕,其内部的变量也不会被垃圾回收机制回收,而是会一直存在于内存中,直到闭包被销毁。
    3. 作用:
      1. 私有变量和方法,实现数据的封装和隐藏
      2. 缓存数据,保持状态
    1. 问题:内存泄漏
    2. 应用:Vue的事件处理函数、【还有很多】
  1. 原型
    1. 每个对象(除null外)在创建时,都会创建与之关联的另一个对象,这个关联的对象就是我们所说的原型。
    2. 目的:实现继承和共享属性的机制
    3. 显式原型:prototype,隐式原型:__proto__
    4. 当使用构造函数创建实例对象时,实例对象会通过隐式原型(__proto__)属性关联到该构造函数的显式原型(prototype)
    5. 原型链:每一个实例化对象都有一个__proto__属性,这个__proto__属性指向构造该对象的构造函数的原型对象。而原型对象本身也是一个对象,它也有一个__proto__属性,这样就形成了一个链式结构,一层一层往上找,直到找到Object.prototype
  1. this指向
    1. 概念:this是一个指针型变量,它动态指向当前函数的运行环境
    2. this指向的几种情况
      1. 全局环境下的this指向:this始终指向全局对象window
      2. 函数内的this指向:this始终指向全局对象window
      3. 对象中的this指向:对象内部方法的this指向调用这些方法的对象
      4. 箭头函数中的this指向:this指向于函数作用域所用的对象
      5. 构造函数中的this:构造函数中的this是指向实例
  1. 高阶函数【比较复杂,可以深入研究】
    1. 定义:函数可接收另一个函数作为参数,或者函数作为返回值输出
  1. 设计模式【比较复杂,可以深入研究】
    1. 定义:经验总结的可复用的解决软件设计常见问题的方案
    2. 常见模式
      1. 单例模式
      2. 工厂模式
      3. 观察者模式
      4. 发布订阅者模式
## 高阶函数 ![](https://i-blog.csdnimg.cn/img_convert/ffb1fef62f485a516ab4b4e82cc26bdb.png)
  1. 概念:是函数式编程中的一个重要概念,函数可以作为参数或返回存在
  2. 特点:
    1. 函数可以作为参数被传递
    2. 函数可以作为返回值输出
  3. 应用:数组函数中 map、reduce、some、sort, 防抖节流

柯里化,纯函数

设计模式

单例–弹窗组件唯一,工厂–通过参数传入不同,一个来决定加工输出对象

JS 继承

题目:谈谈对 js 继承的看法**
出题频率:30%
难易程度:中等难度
**使用场景:在单页面应用中和封装插件的时候使用的频率比较高

由于现在已经出现了 ES6 类的继承,没怎么了解。

1. 常见继承方式

我会答原型链继承、构造函数继承,其他的组合式、寄生式不太了解,更常用 es6 classextends 继承。

原型和原型链继承
  • 简述:把父构造函数绑定到子构造函数的原型链(**prototype**)上
  • 原理:JavaScript中每个函数都有一个prototype属性,它是一个对象,当使用该函数作为构造函数创建实例时,实例的__proto__(非标准属性,标准方式是Object.getPrototypeOf)会指向构造函数的prototype。将父类的实例赋值给子类构造函数的prototype,就建立了原型链关系。例如:
function Parent() {  
    this.name = 'parent';
}
function Child() {}
Child.prototype = new Parent();
let child = new Child();
console.log(child.name); // 输出:parent
  • 优点:方法可复用,父类原型上的方法能被所有子类实例共享。
  • 缺点:父类实例的属性会被所有子类实例共享,一个子类实例修改属性可能影响其他实例;创建子类实例时不能向父类构造函数传参。
构造函数继承
  • 简述原理:在子类构造函数内部,使用callapply方法调用父类构造函数,将父类的属性和方法绑定到子类实例上。例如:
function Parent(name) {  
    this.name = name;  
}
function Child(name) {
    Parent.call(this, name);
}
let child = new Child('child');
console.log(child.name); // 输出:child
  • 优点:可以在创建子类实例时向父类构造函数传参,每个子类实例有自己独立的属性,互不影响。
  • 缺点:父类的方法不能复用,每个子类实例都有自己的一套方法副本,占用更多内存。
组合式继承
  • 原理:结合原型链继承和构造函数继承。先使用构造函数继承来初始化子类实例的属性,保证属性的独立性;再通过原型链继承来实现方法的复用。例如:
function Parent(name) {
    this.name = name;
}
Parent.prototype.sayHello = function() {
    console.log('Hello from parent');
};
function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
let child = new Child('child', 10);
console.log(child.name); // 输出:child
console.log(child.age); // 输出:10
child.sayHello(); // 输出:Hello from parent
  • 优点:融合了前两种继承方式的优点,既实现了方法复用,又保证了属性的独立性。
  • 缺点:父类构造函数会被调用两次,一次是创建子类原型时new Parent(),一次是创建子类实例时Parent.call(this),产生不必要的开销。
原型式继承
  • 原理:借助Object.create()方法,该方法创建一个新对象,新对象的原型指向传入的对象。例如:
function Parent() {
    this.name = 'parent';
}
Parent.prototype.sayHello = function() {
    console.log('Hello from parent');
};
let parent = new Parent();
let child = Object.create(parent);
console.log(child.name); // 输出:parent
child.sayHello(); // 输出:Hello from parent
  • 优点:简单方便,能快速基于一个对象创建新对象并继承其属性和方法。
  • 缺点:和原型链继承类似,共享属性可能带来问题;没有单独的构造函数,不利于创建多个相似但有区别的对象。
寄生式继承
  • 原理:在原型式继承基础上,创建一个工厂函数,在函数内部对继承来的对象进行扩展和增强,然后返回该对象。例如:
function createAnother(original) {
    let clone = Object.create(original);
    clone.sayGoodbye = function() {
        console.log('Goodbye');
    };
    return clone;
}
function Parent() {
    this.name = 'parent';
}
Parent.prototype.sayHello = function() {
    console.log('Hello from parent');
};
let parent = new Parent();
let child = createAnother(parent);
child.sayHello(); // 输出:Hello from parent
child.sayGoodbye(); // 输出:Goodbye
  • 优点:可以灵活地对继承来的对象进行增强和扩展。
  • 缺点:和原型式继承类似,共享属性存在风险,且没有复用性良好的构造函数。
基于ES6的extends的继承
  • 原理:ES6引入class关键字来定义类,使用extends关键字实现继承。class实际上是语法糖,底层仍然基于原型链。例如:
class Parent {
    constructor(name) {
        this.name = name;
    }
    sayHello() {
        console.log('Hello from parent');
    }
}
class Child extends Parent {
    constructor(name, age) {
        super(name);
        this.age = age;
    }
    sayAge() {
        console.log(`I'm ${this.age} years old`);
    }
}
let child = new Child('child', 10);
console.log(child.name); // 输出:child
console.log(child.age); // 输出:10
child.sayHello(); // 输出:Hello from parent
child.sayAge(); // 输出:I'm 10 years old
  • 优点:语法更加简洁直观,符合传统面向对象编程的习惯;通过super关键字可以方便地调用父类的构造函数和方法。
  • 缺点:需要运行环境支持ES6,如果在不支持的环境中使用,需要进行转码(如使用Babel)。

ES5/ES6 的继承除了写法以外还有什么区别?

  1. 实现机制不同:
    1. ES5先创建子类实例对象,再将父类方法通过 Parent.call(this) 添加到子类实例的 this 上
    2. 先创建父类实例对象,再通过子类构造函数修改 this(必须调用 super() 后才能使用 this)
function Child() {
  Parent.call(this); // 手动绑定父类实例属性
}
Child.prototype = Object.create(Parent.prototype); // 手动处理原型链
class Child extends Parent {
  constructor() {
    super(); // 必须先调用super()
  }
}
  1. 语法与行为特性
    1. 变量提升:
      1. ES5 函数声明会提升
      2. ES6 类生名不会提升
    2. 严格模式
      1. ES5 需手动添加 ‘use strict’
      2. ES6 的类内部默认启用严格模式
    3. 方法特性
      1. ES5 的原型方法是可枚举的
      2. ES6 的类方法不可枚举
    4. 构造函数调用
      1. ES5 的原型方法可以作为构造函数调用
      2. ES6 的类方法没有原型(无法通过 <font style="color:rgba(0, 0, 0, 0.9);">new</font> 调用)
  2. ES6 特性
    1. super 关键字的作用:通过super()吊用父类构造函数获取 this
    2. 原生构造函数继承:ES6 支持继承原生构造函数(如 Array、Number)
  3. 其他限制
    1. 类名重写
      1. ES5允许在构造函数内部重写父类名(如 Parent = ‘NewName’)
      2. ES6 的类名是常量,重写会报错
    2. 使用 new 调用
      1. ES5 的构造函数可直接调用
      2. ES6 的类必须通过 new 实例化
特性 ES5 继承 ES6 继承
实现机制 手动绑定原型链和构造函数 自动处理原型链,通过 extends
super
变量提升 支持 不支持(类声明不提升)
严格模式 需手动启用 默认启用
方法可枚举性 可枚举 不可枚举
静态方法继承 需手动复制 自动继承
原生构造函数继承 不支持 支持
构造函数调用限制 可省略 new 必须使用 new

:::color4
理解:

  1. 对于 this 的理解:
    1. ES6 class 是通过 super 创建父构造函数的实例(this 指向这个实例),之后子类构造函数修改 this。
    2. ES5 先创建自己的实例( this 指向子函数实例),然后再手动绑定父类的实例属性到子类。
  2. ES5 无法正确继承内置对象(Array):
    1. 内部插槽不可访问:Array 实例内部插槽 [[ArrayData]] 存储元素;Parent.call(this)无法出发 Parent内部插槽的初始化逻辑。
    2. 原型链割裂:即使手动设置 MyArray.prototype = Object.create(Array.prototype),子类实例的原型链仍无法完整继承内置对象的特性。
  3. 对 Class 的理解
    1. Class 是语法糖,构造函数逻辑在 constructor 中,类的方法添加到构造函数的 prototype 上。
    2. 必须通过new调用
      1. 必须 new 实例化,底层通过检查new.target确保调用方式正确
    3. 不可枚举的方法
      1. 通过Object.defineProperty设置enumerable: false
    4. extends
      1. ES6通过extends实现继承,子类的原型对象(prototype)指向父类实例,形成原型链。例如,class Student extends Person会导致Student.prototype.proto === Person.prototype
    5. super
      1. 先通过super()创建父类实例this,再通过子类构造函数修改this
  4. 原型链关系
    1. Student实例 → Student.prototype → Person.prototype → Object.prototype

(实例属性) (子类方法) (父类方法) (基础方法)

  1. 继承顺序
    1. 通过 extends 调整原型链(Student.prototype.proto = Person.prototype)
    2. super() 创建父类实例的 this,并绑定到子类
    3. 子类构造函数修改 this,添加新属性

:::

  1. class 为什么要实例化?【先谈 new 本身的操作,其次谈问题】
  2. 帮我理清构造函数,原型对象,构造函数的原型对象。Student.prototype.proto === Person.prototype 这里我理解的是子类的原型对象指向父类的原型对象,但是是说的是指向父类实例,疑惑。【原型链继承】
  3. 我理解的是 extends 实现原型链部分的继承,super 创建父类实例,子类的 constructor 修改 this。我觉得它顺序是先创建父类实例,然后原型链继承父类实例的原型对象,再修改 this。对吗?【继承顺序】

call、apply

  1. 概念: **<font style="color:rgb(31, 35, 41);">call,apply</font>**都是 **<font style="color:rgb(31, 35, 41);">Function</font>**原型中的方法,而所有的函数都是<font style="color:rgb(31, 35, 41);">Function</font>的实例
  2. 作用:例如 <font style="color:rgb(31, 35, 41);">func.call(obj,参数)</font>,就是将 func 的上下文(this)指向 obj 的上下文(this)
  3. 常用举例:
    1. 构造函数继承时使用,Child 构造函数调用<font style="color:rgb(31, 35, 41);">Parent.call(this,参数)</font>就可以在 child 实例上继承属性方法。
    2. 判断对象类型使用,<font style="color:rgb(31, 35, 41);">Object.prototype.toString.call(obj)</font>,执行时的 <font style="color:rgb(31, 35, 41);">this</font> 上下文,调用该方法来处理 <font style="color:rgb(31, 35, 41);">obj</font>
  4. 关于手写 call:
    1. 举例讲基本思想:func.Call(obj,1,2) 把调用的函数(func)地址赋值为传入对象(obj)的一个属性,这时候调用函数 func,它的上下文(this)就是 obj 的上下文(this)。
    2. 基本实现步骤:1️⃣在 Function 原型上添加 call 方法,2️⃣获取上下文(从传入的第一个参数 obj 就是),3️⃣转换 this 指向(将当前函数(this)赋值给 context 上的一个属性)。
    3. 这里得补充(想到就说):Function 原型中的上下文 this 指向是实例函数 func,可以使用 Symbol 创建唯一属性名,传入 null、undefineded 就赋值为全局
Function.prototype.customCall = function(context, ...args) {  
    // 如果 context 是 null 或 undefined,则默认指向全局对象(浏览器中为 window,Node.js 中为 global)
    context = context || globalThis;  
    console.log("0初始context:",context);
    // 将当前函数(this)赋值给 context 上的一个属性
    context.that = this;
    // 使用 context 调用该函数,并传入参数
    const result = context.that(...args);
    // 删除临时属性
    delete context.that;
    // 返回函数调用结果
    return result;  
};
Object.prototype.toString.call(obj) 判断对象类型笔记

Object.prototype.toString.call(obj) 笔记

一、基础原理

  • Object.prototype.toString 用于获取对象内部类属性的字符串表示。
  • call(obj)obj 设置为 Object.prototype.toString 执行时的 this 上下文,调用该方法来处理 obj

二、内部实现

每个对象都有内部属性 [[Class]],存储类型信息,无法直接访问。Object.prototype.toString.call(obj) 会检查 obj[[Class]] 属性,根据其值生成对应字符串。

三、不同对象类型输出

  1. 普通对象[[Class]]"Object",输出 "[object Object]"
  2. 数组对象[[Class]]"Array",输出 "[object Array]"
  3. 函数对象[[Class]]"Function",输出 "[object Function]"
  4. 日期对象[[Class]]"Date",输出 "[object Date]"
  5. 正则表达式对象[[Class]]"RegExp",输出 "[object RegExp]"
  6. 自定义对象:通常 [[Class]] 也是 "Object",除非重写 toString 或修改内部属性 。

四、与instanceof区别

  • instanceof 检查对象是否为某个构造函数的实例,依赖原型链。
  • Object.prototype.toString.call 直接查看对象内部类型,跨 iframe 或跨窗口场景更可靠。

五、优势

  1. 精确性:能准确识别对象类型。
  2. 跨环境:在不同JavaScript环境表现一致。

六、常见用途

  1. 类型检查:用于判断对象是否属于特定类型,弥补 typeofinstanceof 局限。
  2. 处理各种类型数据:获取对象精确类型,方便统一处理不同类型数据。
## 深浅拷贝 ![](https://i-blog.csdnimg.cn/img_convert/7565444f95c20445841b98557eddd510.png)
  1. 浅拷贝:简单数据类型复制值,复杂数据类型复制引用地址。修改拷贝后对象,也会修改源对象。
  2. 深拷贝:都是复制值。修改拷贝后对象,不会修改源对象。

拷贝实现:

  1. 浅拷贝的实现:
    • 展开运算符(最简单)
    • Object.assign(常用)
  2. 深拷贝的两种实现:
    • JSON方法JSON.parse(JSON.stringify(obj))(简单但有局限:无法处理函数、Symbol 类型、undefined 以及循环引用
    • 手写完整实现(处理了各种特殊情况)
    • lodash 的cloneDeep(value)
  3. 深拷贝处理的特殊情况:
    • 基本类型数据、null、 日期对象、正则对象、循环引用、Map和Set、Symbol类型、原型链

防抖节流

  1. 防抖:
    1. 概念:在时间间隔内,连续触发,会重新计时,在最后一次触发后执行。【一直抖动,抖停了才执行】
    2. 场景:搜索框 onchange 事件、窗口变化
    3. 实现思想:设置一个定时器,每次调用函数时清除之前的定时器并重新计时,定时器到期了才执行函数
    4. 注意:使用 func.apply 来确保 this 指向正确
  2. 节流:
    1. 概念:在时间间隔内,连续触发,只有第一次生效。
    2. 场景:加载更多、防止高频按钮点击、函数执行频率
    3. 实现思想:利用定时器,仅当定时器不存在时,设置新定时器,并在定时器中调用目标函数。
  3. 补充:闭包、高阶函数在防抖节流中的作用
    1. 闭包可以保存关键状态信息(如定时器和上一次触发时间),并在多次函数调用中保持这些信息的持久性和可访问性。
    2. 高阶函数 可以接收目标函数作为参数,并返回一个经过封装、具备防抖或节流功能的新函数
/**
 * 基础防抖函数
 * @param {Function} fn 需要防抖的函数
 * @param {number} delay 延迟时间,单位ms
 * @returns {Function} 返回防抖处理后的函数
 */
function debounce(fn, delay) {  
    let timer = null;    
    return function(...args) {  
        // 如果已经设定过定时器,则清空上一次的定时器
        if (timer) clearTimeout(timer);  
        // 设定新的定时器
        timer = setTimeout(() => {   
            fn.apply(this, args);  
        }, delay);
    }
}
/**
 * 定时器版节流函数
 * @param {Function} fn 需要节流的函数
 * @param {number} delay 延迟时间    
 * @returns {Function} 返回节流处理后的函数
 */
function throttleByTimer(fn, delay) {  
    let timer = null;
    return function(...args) {  
        // 如果定时器不存在,则设置新的定时器,并在定时器中调用函数
        if (!timer) {
            fn.apply(this, args);   
            timer = setTimeout(() => {    
                timer = null;
            }, delay);
        }
    }
}

js 内存泄漏

  1. 什么是内存泄漏?
    1. 内存泄漏指无效引用,即无法被程序使用,但又没有被系统回收的内存,直至浏览器进程结束。
  2. 常见内存泄漏的情况及解决方案
    1. 全局变量:未声明的变量缓存大量数据,仅在页面刷新或关闭时才释放内存,导致内存意外泄漏。
      解决方案:声明变量,避免全局直接引用。
    2. setInterval:其中引用的变量和对象易造成内存泄漏。
      解决方案:及时清除定时器。
    3. DOM 事件:DOM 节点被移除但事件仍留在内存中。
      解决方案:使用 removeEventListener 移除。
    4. 数据交叉赋值循环引用:会导致内存泄漏。
      解决方案:引用完毕释放空间,将相关引用设为 null。
    5. DOM 移除:进行删除、更新操作后,可能忘记释放已缓存的 DOM。
      解决方案:操作后将其设为 null。
    6. 闭包:易引发内存泄漏。
      解决方案:使用完毕,手动释放闭包内数据,将其中变量设为 null 。
    7. 或者使用 WeakMap、WeakSet 弱引用,当键对象除了在 WeakMap 中被引用外没有其他强引用时,就会被回收。

介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

Set

  1. 概念:是集合,成员值唯一不重复
  2. 方法:new Set()创建、.add()加入
  3. 遍历操作:.keys==.value返回值(因为键值是一样的);.entries返回键值数组;
  4. 注意:Set.prototype[Symbol.iterator] === Set.prototype.values可以直接使用for...of来代替 .values

WeakSet:

  1. 概念:只存对象,不重复的集合,弱引用(不经过垃圾回收机制,不引用它时自动回收)
  2. 方法:.add .delete .has
  3. 注意:没有 .size 和 forEach,因为它不能遍历(随时可能消失,很可能刚刚遍历结束,成员就取不到了)
  4. 应用:储存 DOM 节点

Map:

  1. 概念:键值对,键可以是对象(本来 Object 也是键值对,但是键只能是字符串)
  2. 方法:.set .get(key) .has(key) .delete(key) .clear .size
  3. 注意:
    1. 读取未知键返回undefined
    2. 只有引用同一个对象才会视作同一个键
    3. 简单类型值严格相等就会视作一个键
  4. 转化
    1. Map 转为对象:遍历obj[k] = v;进行添加;非字符串的键会被转成字符串
    2. 对象转为 Map:通过Object.entries(obj)
    3. Map 与 JSON 之间转换

WeakMap

  1. 键名只为对象,键值对(键所指对象不计入垃圾回收机制)

函数柯里化

一、柯里化的定义

柯里化是一种将多参数函数转换为一系列单参数函数的技术。通过这种转换,每次调用函数时仅传递一个参数,而函数会返回一个新函数,等待接收下一个参数,直到接收到足够的参数来执行原函数的操作。

二、优点

  1. 参数复用(固定部分参数)
  2. 延迟执行(按需触发)
  3. 函数组合(增强灵活性)

三、缺点

  1. 性能开销:创建大量嵌套函数可能增加性能开销。
  2. 调试困难:嵌套函数使调试变得复杂。
  3. 理解难度:对不熟悉函数式编程的开发者来说不直观。
  4. 不适用于所有场景:在某些情况下可能使代码过于复杂。

:::color4

  1. 关于柯里化实现:
    1. 实现逻辑:
      创建一个函数,它接收一个多参数函数作为参数,返回一个新函数。新函数检查传入参数数量是否足够执行原函数,若不够就返回新函数继续接收参数,若足够就执行原函数。
    2. 闭包维护参数数组
    3. (思路,自己理解用) 调用 const curriedAdd = curry(add); 时,进入 curry 函数返回 curried 函数,使 curriedAdd 指向 curried。调用 curriedAdd(1)(2)(3) 会依次调用 curried(1)、curried(2) 和 curried(3),闭包维护的 args 数组会逐步存储 [1,2,3],最终满足原函数参数数量时执行原函数。

:::

function curry(fn) {
    let count=0;
    return function curried(...args) {
        // 判断当前已接收的参数数量是否足够
        // fn.length 表示原函数的形参个数
        // args.length 表示当前已接收的实参个数
        if (args.length >= fn.length) {
            // 参数足够时,执行原函数
            // 使用 apply 绑定 this 上下文并传入所有参数
            // console.log(args); // 传入的实参[1, 2, 3]
            return fn.apply(this, args);
            // return fn(...args);  //这样是隐式绑定,this根据调用时的上下文决定
        }
        // 参数不足时,返回新的函数继续接收参数
        console.log(count++,"原函数的形参个数:",fn.length,"已接收的实参个数:",args.length)
        // 闭包保存已接收的参数,等待后续调用传入新参数
        return function(...args2) {
            // 递归调用 curried,合并之前的参数和新参数
            return curried.apply(this, args.concat(args2));
        }
    };
}

同步异步

js如何实现异步

js 事件循环

  1. (因为 JS 是单线程的,只有一个执行栈。)
  2. 流程
    1. 执行同步代码,直至调用栈清空。
    2. 检查微任务队列,依次执行所有微任务(直到队列清空)。
    3. 取一个宏任务执行,完成后再次检查微任务队列。
    4. 循环上述步骤,直至所有任务完成
  3. 核心规则:同步代码 → 微任务 → 宏任务 → 循环
  4. 宏任务:
    1. setTimeout 和 setInterval;
    2. I/O 操作,例如读取文件、网络请求;【<font style="color:rgba(0, 0, 0, 0.9);"><input type="file"></font><font style="color:rgba(0, 0, 0, 0.9);">FileReader</font> API 实现文件读取;Fetch API 的请求发起是宏任务,但 <font style="color:rgba(0, 0, 0, 0.9);">.then()</font> 是微任务】
    3. DOM 事件,例如点击事件、输入事件;
    4. requestAnimationFrame;
    5. script 标签;
  5. 微任务:
    1. Promise 的 resolve 和 reject 回调
    2. async/await 中的异步函数;
    3. MutationObserver;

Promise

:::color5
对于手写 Promise 的解疑答惑:

  1. 如何执行的代码:
    1. 在创建实例的时候传入执行函数 executor不会调用,其中resolve, reject这两个参数是形参;到了构建函数内部,才会执行executor,并传入实参
  2. 如何实现保证涉及到同步的处理

:::

:::color5

  1. 异步编程基础
    1. 单线程与异步:JavaScript 是单线程语言,按顺序执行任务,有同步和异步操作。异步任务给浏览器处理,有结果后将回调函数插入待处理队列尾部。
    2. 回调函数:作为实参传入另一个函数并在其内部调用,用于完成特定任务。有普通函数和箭头函数两种写法,如 setTimeout 可开启子线程执行回调,不影响主线程运行。
  2. 回调地狱
    1. 概念:多个回调函数嵌套导致代码可维护性差,例如多个依赖结果的 AJAX 请求嵌套,会使代码难以阅读和维护。
  3. Promise
    1. 定义与特点:ES6 提供的类,是异步编程解决方案。有两个特点:状态不受外界影响,有 pending、fulfilled、rejected 三种状态;
    2. 语法格式:使用 new Promise 创建实例,构造函数接收一个带 resolvereject 参数的回调函数。resolve 用于将状态变为成功并传递结果,reject 用于将状态变为失败并传递错误。实例可通过 thencatch 方法分别处理成功和失败回调。
    3. 链式调用then 方法返回新的 Promise 实例,可链式调用。如获取帖子信息后根据帖子 ID 获取评论,需在前一个 then 方法中返回新的 Promise 实例。
    4. Promise.all():将多个 Promise 实例包装成一个新实例,接收数组作为参数。所有实例都成功时,新实例才成功,结果为各实例结果组成的数组;有一个失败则新实例失败。可用于等待多个请求返回数据后再统一处理,如页面加载时显示加载中,数据返回后隐藏加载中并渲染页面。
  4. 记忆:
    1. 单线程 + 事件循环:异步任务通过回调队列执行。
    2. 回调地狱:多层嵌套,难以维护。
    3. Promise:
      1. 状态:pending → fulfilled/rejected。
      2. 链式调用:.then 返回新 Promise。
      3. 并行:Promise.all 等待所有任务完成。

:::

new Promise(function (resolve, reject) {  
  // resolve 表示成功的回调
  // reject 表示失败的回调
}).then(function (res) {
  // 成功的函数
}).catch(function (err) {  
  // 失败的函数
})
  • 执行过程Promise 是构造函数,接收一个函数作为参数,该函数有 resolvereject 两个参数,它们也是函数。resolve 用于将 Promise 对象状态变为成功,reject 用于变为失败。实例生成后,用 then 方法指定成功回调,catch 方法指定失败回调。

:::color4
后面还有 async 和 await,generator(这个不清楚)

:::

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

js 能否实现多线程

new

  1. 创建空对象:使用<font style="color:rgba(0, 0, 0, 0.9);">Object.create</font>方法创建一个新对象,并将该对象的原型设置为构造函数的<font style="color:rgba(0, 0, 0, 0.9);">prototype</font>属性
  2. 绑定**<font style="background-color:rgb(252, 252, 252);">this</font>**并执行构造函数:使用<font style="color:rgba(0, 0, 0, 0.9);">apply</font>方法将构造函数的<font style="color:rgba(0, 0, 0, 0.9);">this</font>绑定到新创建的对象上,并执行构造函数
  3. 处理构造函数的返回值:如果返回值是一个对象,则返回该对象;否则返回新创建的对象
function myNew(Constructor, ...args) {
  // 创建一个空对象,并将其原型指向构造函数的prototype
  const obj = Object.create(Constructor.prototype);
  // 绑定this并执行构造函数
  const result = Constructor.apply(obj, args);
  // 根据返回值判断最终返回结果
  return result instanceof Object ? result : obj;
}

Generator


Generator (生成器)函数


1. 基本概念
  • 定义:通过 function* 声明的特殊函数,内部使用 yield 暂停执行。
  • 特征
    • 惰性执行:调用后返回迭代器对象,需手动触发执行。
    • 状态暂停:通过 yield 暂停,通过 next() 恢复。
  • 示例
function* gen() {
  yield 1;
  yield 2;
  return 3;
}
const it = gen(); // 返回迭代器,未执行函数体

2. 核心方法
方法 作用 示例
next() 恢复执行,返回 {value, done} it.next(); // {value:1, done:false}
return() 终止生成器,直接返回 {value, done:true} it.return(10); // {value:10, done:true}
throw() 向生成器内部抛出错误 it.throw(new Error('Fail'));

3. 执行流程控制
  • yield** 表达式**:
function* gen() {
  const a = yield 'First'; // a = 'From Next'
  yield a;
}
const it = gen();
it.next();      // {value: 'First', done: false}
it.next('From Next'); // {value: 'From Next', done: false}  
- 暂停执行,将右侧的值作为 `next()` 返回值的 `value`。
- 下一次 `next(arg)` 的参数 `arg` 会作为当前 `yield` 的返回值。
  • return 的区别
    • return 结束生成器,后续 yield 不再执行。
    • return 的值作为最后一次 next()value

4. 应用场景
场景 说明 示例
异步流程控制 yield 暂停异步操作,结合自动执行器实现类似 async/await 的效果 见下方代码示例
生成迭代器 简化自定义迭代器的实现 for (const num of gen()) { ... }
状态机 管理多状态切换逻辑 游戏角色状态切换、流程步骤控制
惰性求值 按需生成数据,节省内存 无限数列生成(如斐波那契数列)

5. 异步控制示例
function* fetchUser() {
  const user = yield fetch('/api/user'); // yield 返回 Promise
  const posts = yield fetch(`/api/posts/${user.id}`);
  return posts;
}

// 自动执行器
function run(generator) {
  const it = generator();
  function handle(result) {
    if (result.done) return result.value;
    return result.value.then(data => handle(it.next(data)));
  }
  return handle(it.next());
}

run(fetchUser).then(posts => console.log(posts));

6. 与 async/await 对比
特性 Generator async/await
语法 需手动编写执行器 原生语法支持,无需额外逻辑
错误处理 需手动捕获 try/catch 自动 Promise 链式错误捕获
可中止性 可通过 return() 提前终止 无法直接终止
兼容性 ES6+ 支持 ES2017+ 支持
适用场景 复杂流程控制、自定义迭代逻辑 常规异步流程

7. 常见面试问题
  1. 为什么 Generator 能暂停执行?
    • JavaScript 引擎通过 协程(Coroutine) 机制实现,保存函数执行上下文(栈帧),切换时恢复现场。
  2. 如何实现 Generator 的自动执行?
    • 递归调用 next(),并通过 Promise 链传递结果(如 co 库的实现原理)。
  3. yield*** 的作用是什么?**
    • 委托给另一个 Generator 或可迭代对象,等价于 for...of 遍历:
function* genA() { yield 1; }
function* genB() { yield* genA(); } // 等价于 yield 1

一句话总结

Generator 通过 yield 实现函数暂停与恢复,是 JavaScript 协程的基础,适合复杂异步流程和自定义迭代逻辑。虽然 async/await 更简洁,但 Generator 在状态管理和惰性计算中仍有独特价值。

js 脚本延迟加载

延迟加载的作用

  • 提升页面加载速度、避免脚本阻塞和用户体验:延迟加载可以让主要内容先显示,再逐步加载和执行 JavaScript 脚本,避免因脚本阻塞而导致的空白页面或加载缓慢,从而提升用户体验。
  • 减少带宽资源:通过延迟加载,只在需要时加载脚本,避免一开始就占用过多带宽,节省了带宽资源。
  • 降低服务器压力:延迟加载将脚本请求分散到不同时间,减轻服务器在短时间内承受的请求压力。

延迟加载的方法

  1. 使用defer属性:在<script>标签中添加defer属性,如<script defer src="a.js"></script>,用于加载外部JavaScript文件。浏览器会在解析HTML文档的同时异步下载该脚本文件,等HTML解析完成后,按照脚本在HTML中出现的顺序依次执行。这样不会阻塞HTML的解析过程。
  2. 使用async属性<script async src="a.js"></script>同样用于加载外部脚本。它也是异步下载脚本,但与defer不同的是,脚本一旦下载完成就会立即执行,多个带async属性的脚本执行顺序是不确定的,可能会打乱在HTML中出现的顺序,并且一般会在load事件之前执行。
  3. 动态创建DOM:通过JavaScript动态创建<script>元素来实现延迟加载。示例代码,先创建一个<script>元素对象,然后设置其src属性指定要加载的脚本文件。之后可以将这个元素添加到DOM中(如document.head.appendChild(obj) ),从而开始加载脚本。这种方式非常灵活,可以在特定条件下才创建和加载脚本。
var obj = createElement('script'); 
obj.src = "a.js";  
  1. setTimeout()setTimeout()是JavaScript的一个定时器函数。可以使用它来延迟执行脚本代码。比如setTimeout(function() { // 这里写要延迟执行的脚本代码 }, 1000);,表示1000毫秒(1秒)后执行相应的脚本代码。不过需要注意,它只是延迟了代码的执行时机,并没有异步加载脚本文件,如果要加载外部脚本,还需要结合动态创建DOM等方式。
  2. 将js代码写在文档的最后:把JavaScript代码放在HTML文档的最后(</body>标签之前)。这样在浏览器解析HTML时,会先把页面的结构和内容呈现出来,然后再去加载和执行脚本,一定程度上避免了脚本阻塞页面渲染的问题。

Commonjs、AMD、CMD

关系

  • 目标相同:ES6模块系统与AMD、CMD、CommonJS都是为了解决JavaScript项目中模块的定义、依赖管理和加载问题,以提高代码的可维护性、可复用性和可扩展性。
  • 历史承接:AMD、CMD、CommonJS在ES6模块系统出现之前,已经在JavaScript社区中得到了广泛应用,为JavaScript模块化发展奠定了基础。ES6模块系统在一定程度上借鉴了它们的一些思想和实践经验,是对JavaScript模块化的进一步标准化和完善。

区别

对比项 AMD CMD CommonJS ES6模块系统
语法风格 使用define函数定义模块,以数组形式声明依赖 使用define函数定义模块,在函数内部通过require引入依赖,一个文件就是一个模块 使用exportsmodule.exports导出,require引入 使用exportimport关键字,更简洁直观
加载方式 依赖前置,异步加载 依赖就近,按需异步加载 同步加载 静态加载,编译时确定依赖关系,可异步加载
执行时机 在所有依赖模块加载完成后执行回调函数 require时才加载并执行模块 require时阻塞执行,加载并执行完模块后继续 import时确定模块的引用关系,执行时机由引擎决定
浏览器支持 需要引入如RequireJS等库来实现 需要引入如SeaJS等库来实现 在浏览器端需借助工具转换,原生不支持 现代浏览器大多已原生支持
适用场景 适合_浏览器_端复杂的前端项目,对模块加载顺序有要求 适合对性能要求较高,按需加载模块的浏览器端项目 主要用于Node.js服务端项目 适用于各种场景,尤其在现代前端和后端项目中都有广泛应用
优点 异步加载提升性能,依赖关系清晰 按需加载优化性能,语法灵活 简单直观,Node.js生态丰富 语法简洁,静态分析支持,支持动态导入
缺点 加载顺序不一定 加载时机复杂,调试困难 浏览器端同步加载阻塞, 对旧环境兼容性差

实现 sleep 效果

使用场景:在数据渲染和 DOM 操作时使用

// 方法1:使用 Promise 实现 sleep
function sleep(time) {
    return new Promise(resolve => setTimeout(resolve, time));
}

// 方法2:使用 async/await 实现 sleep
async function sleepAsync(time) {
    await new Promise(resolve => setTimeout(resolve, time));
}

// 方法3:使用回调方式实现 sleep
function sleepCallback(time, callback) {
    setTimeout(callback, time);
}

// 使用示例:

// Promise方式
sleep(2000).then(() => {
    console.log('休眠2秒后执行');
});

// async/await方式
async function demo() {
    console.log('开始执行');
    await sleepAsync(2000);
    console.log('休眠2秒后继续执行');
}
demo();

// 回调方式
console.log('开始执行');
sleepCallback(2000, () => {
    console.log('休眠2秒后执行回调');
});

some 和 every 的区别

使用场景:辅助类型的面试题,主要了解 some 和 every 的不同点和使用

用 2 种方式实现数组去重

数组去重一般在获取接口数据或者处理时,重构数据中使用

  1. 使用**Set**去重:利用 ES6 的Set数据结构自动去除重复值的特性,通过将数组转换为Set实例再转回数组实现数组去重 。
  2. 使用**filter**去重:借助filter遍历数组,结合indexOf判断元素首次出现位置与当前索引是否一致,保留首次出现的元素达成数组去重
// 定义一个包含重复元素的数组
const arr = [1, 2, 2, 3, 4, 4, 5];

// 使用 Set 对象进行去重
// new Set(arr) 会创建一个 Set 实例,它会自动去除重复元素
// 再使用扩展运算符 ... 将 Set 实例转换回数组
const uniqueArr1 = [...new Set(arr)];  

// 打印去重后的数组
console.log(uniqueArr1); 
// 定义一个包含重复元素的数组
const arr = [1, 2, 2, 3, 4, 4, 5];

// 使用 filter 方法进行去重
// filter 方法会遍历数组中的每个元素
// indexOf 方法会返回元素在数组中第一次出现的索引
// 如果当前元素的索引等于 indexOf 返回的索引,说明该元素是第一次出现,保留该元素
const uniqueArr2 = arr.filter((item, index) => {  
    return arr.indexOf(item) === index;
});

// 打印去重后的数组
console.log(uniqueArr2); 

找出多维数组的最大值所组成的数组

使用场景:算法题目,考核逻辑思维

  1. 方法一:通过两层for循环,外层循环遍历多维数组的子数组,内层循环找出每个子数组中的最大值,将其存入结果数组。
  2. 方法二:运用map方法遍历多维数组,对每个子数组使用Math.max结合扩展运算符获取最大值,组成新数组。
function getMaxValues(arr) {
    const result = [];
    // 遍历多维数组
    for (let i = 0; i < arr.length; i++) {
        let max = arr[i][0];
        // 遍历当前子数组
        for (let j = 1; j < arr[i].length; j++) {
            if (arr[i][j] > max) {
                max = arr[i][j];
            }
        }
        // 将当前子数组的最大值添加到结果数组中
        result.push(max);
    }
    return result;
}

// 测试示例
const multiDimensionalArray = [
    [1, 3, 2],
    [4, 6, 5],
    [7, 9, 8]
];
const maxValues = getMaxValues(multiDimensionalArray);
console.log(maxValues); 
function getMaxValues(arr) {
    return arr.map(subArr => Math.max(...subArr));
}

// 测试示例
const multiDimensionalArray = [
    [1, 3, 2],
    [4, 6, 5],
    [7, 9, 8]
];
const maxValues = getMaxValues(multiDimensionalArray);  
console.log(maxValues); 

找出字符串中出现次数最多的字符

使用场景:算法题目,考核逻辑思维

function findMostFrequentChar(str) {
    // 创建一个对象来存储每个字符的出现次数
    const charFrequency = {};  
    // 遍历字符串,统计每个字符出现的次数
    for (const char of str) {
        charFrequency[char] = (charFrequency[char] || 0) + 1;  
    }

    let maxFrequency = 0;
    let mostFrequentChar = '';
    // 遍历存储字符出现次数的对象
    for (const char in charFrequency) {
        if (charFrequency[char] > maxFrequency) {  
            maxFrequency = charFrequency[char];  
            mostFrequentChar = char;
        }
    }
    return mostFrequentChar;
}

// 测试示例
const testString = "aabbbccd";
const result = findMostFrequentChar(testString);
console.log(result);  

先创建对象 <font style="color:rgb(31, 35, 41);">charFrequency</font>统计字符串中各字符出现次数,再遍历此对象找出出现次数最多的字符并返回

实现一个数组和对象的扁平化

题目:实现一个数组和对象的扁平化
出题频率:30%
难易程度:中等难度
使用场景:属于算法题目,在接口拿到数据的时候对数据重构时使用

  1. 数组扁平化:
    1. 接收一个数组和扁平化深度(默认 Infinity),利用 <font style="color:rgba(0, 0, 0, 0.85);">reduce</font> 方法遍历数组,若元素为数组则递归调用自身并按指定深度继续扁平化,最后将结果拼接返回,当深度为 0 时直接返回原数组。
    2. 使用<font style="color:rgb(0, 0, 0);">var newArray = arr.flat(depth)</font>
  2. 对象扁平化:接收一个对象和属性前缀,通过遍历对象属性,若属性值为对象且非 null、非数组则递归处理并将结果合并,若为基本类型或数组则直接赋值,最终构建并返回扁平化后的对象。
/**
 * 数组扁平化函数
 * @param {Array} arr - 需要扁平化的数组
 * @param {number} depth - 扁平化的深度,默认为Infinity
 * @returns {Array} - 扁平化后的数组
 */
function flattenArray(arr, depth = Infinity) {
    if (depth === 0) return arr;
    return arr.reduce((result, item) => {
        console.log("result",result);  
        // 如果当前项是数组,则递归处理
        if (Array.isArray(item)) {
            return result.concat(flattenArray(item, depth - 1));  
        }
        // 如果是普通项,直接添加到结果中
        return result.concat(item);  
    }, []);
}
/**
 * 对象扁平化函数
 * @param {Object} obj - 需要扁平化的对象
 * @param {string} prefix - 当前属性的前缀,用于构建完整的属性路径
 * @returns {Object} - 扁平化后的对象
 */
function flattenObject(obj, prefix = '') {
    const result = {};

    for (const key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            const value = obj[key];
            // 构建当前属性的完整路径
            const newKey = prefix ? `${prefix}.${key}` : key;  

            if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
                // 如果值是对象且不是数组,则递归处理
                Object.assign(result, flattenObject(value, newKey));
            } else {
                // 如果是基本类型或数组,直接赋值
                result[newKey] = value;
            }
        }
    }

    return result;
}

js实现回文

题目:js实现回文
出题频率:30%
难易程度:中等难度
使用场景:属于算法题目

  1. 方法1:转换为字符串并反转比较:将输入的字符串或数字转换为小写字符串后,通过 <font style="color:rgba(0, 0, 0, 0.85);">split</font><font style="color:rgba(0, 0, 0, 0.85);">reverse</font><font style="color:rgba(0, 0, 0, 0.85);">join</font> 方法将其反转,再与原字符串比较,若相同则判定为回文并返回 <font style="color:rgba(0, 0, 0, 0.85);">true</font>,否则返回 <font style="color:rgba(0, 0, 0, 0.85);">false</font>
  2. 方法2:双指针法:先把输入的字符串或数字转换为小写字符串,接着使用双指针,左指针从字符串开头、右指针从结尾开始,向中间移动并比较对应字符,若有不相等的字符则返回 <font style="color:rgba(0, 0, 0, 0.85);">false</font>,全部相等则返回 <font style="color:rgba(0, 0, 0, 0.85);">true</font>
  3. 备注:可以使用正则表达式<font style="color:rgba(0, 0, 0, 0.85);">.replace(/[^a-z0-9]/g, '')</font>去除非字母数字字符
/**
 * 方法1:转换为字符串并反转比较
 * @param {string|number} str - 需要判断的字符串或数字
 * @returns {boolean} - 是否为回文
 */
function isPalindrome1(str) {
    // 转换为字符串并转换为小写
    str = String(str).toLowerCase();
    // 反转字符串并比较
    return str === str.split('').reverse().join('');  
}

/**
 * 方法2:双指针法
 * @param {string|number} str - 需要判断的字符串或数字
 * @returns {boolean} - 是否为回文
 */
function isPalindrome2(str) {
    str = String(str).toLowerCase();
    let left = 0;
    let right = str.length - 1;
    
    // 左右指针向中间移动,比较字符是否相等
    while (left < right) {
        if (str[left] !== str[right]) {
            return false;
        }
        left++;
        right--;
    }
    return true;
}

/**
 * 方法3:正则表达式 + 双指针(处理特殊字符)
 * @param {string} str - 需要判断的字符串
 * @returns {boolean} - 是否为回文
 */
function isPalindrome3(str) {
    // 将字符串转换为小写,并移除非字母数字字符
    str = str.toLowerCase().replace(/[^a-z0-9]/g, '');  
    let left = 0;
    let right = str.length - 1;
    
    while (left < right) {
        if (str[left] !== str[right]) {  
            return false;
        }
        left++;
        right--;
    }
    return true;
}

// 测试用例
console.log('测试方法1:');
console.log(isPalindrome1('level')); // true
console.log(isPalindrome1('12321')); // true
console.log(isPalindrome1('hello')); // false

console.log('\n测试方法2:');
console.log(isPalindrome2('A man a plan a canal Panama')); // false
console.log(isPalindrome2('race a car')); // false
console.log(isPalindrome2('racecar')); // true  

console.log('\n测试方法3(处理特殊字符):');
console.log(isPalindrome3('A man, a plan, a canal: Panama')); // true
console.log(isPalindrome3('race a car')); // false
console.log(isPalindrome3('Was it a car or a cat I saw?')); // true

冒泡和快速排序

题目:冒泡和快速排序
出题频率:30%
难易程度:中等难度
使用场景:属于算法题目

  1. 冒泡排序方法

冒泡排序方法通过两层循环,外层控制排序轮数,内层对相邻元素进行比较,若前一个元素大于后一个元素则交换位置,同时设置优化标志,若一轮中无交换则说明数组已有序可提前退出,最终实现数组排序。

  1. 快速排序方法

快速排序方法先判断数组长度,若小于等于 1 则直接返回;选取基准值,将数组元素与基准值比较,小于的放入左数组,大于的放入右数组,再递归对左右数组进行快速排序,最后合并左右数组与基准值得到排序后的数组。

  1. 原地快速排序方法

原地快速排序方法先确定分区点,选择最右边元素为基准值,将小于基准值的元素移到左边,然后把基准值放到正确位置,再递归对基准值左右两部分进行排序,最终实现数组的原地排序。

瀑布流

题目:瀑布流
出题频率:30%
难易程度:中等难度
使用场景:在现实图片和信息的页面上常见

初始化
init  
|--loadimages


绑定事件
bindEvents    
|--scroll  
    |--loadimages  
|--resize
    |--calculatePosition      

加载图片(核心功能实现)
loadimages
|--生成图片url
|--异步加载所有图片
    |--addItem  添加单个图片项到页面
      |--创建完毕
      |--calculatePosition 定位      
      
<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <title>瀑布流布局</title>
    <style>
        /* 重置默认样式,确保所有元素的盒模型为 border-box */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        /* 设置容器的基本样式 */
        #container {
            position: relative;
            margin: 0 auto;
            width: 90%;
        }

        /* 设置每个项目的样式 */
        .item {
            position: absolute;
            width: 220px;
            padding: 8px;
            background: #fff;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, .1);
            transition: transform .2s;
        }

        /* 鼠标悬停时,项目向上移动以突出显示 */
        .item:hover {
            transform: translateY(-5px);
        }

        /* 确保图片宽度占满项目,并且显示为块级元素以去除底部空白 */
        .item img {
            width: 100%;
            display: block;
        }
    </style>
</head>

<body>
    <div id="container"></div>
    <script>
        /**
         * Waterfall 类实现了一个瀑布流布局
         * @param {string} container - 容器的选择器
         * @param {Object} options - 可选配置项
         */
        class Waterfall {
            constructor(container, options = {}) {
                // 合并默认配置和用户配置
                this.options = {
                    columns: 4, // 列数
                    gap: 15, // 项目之间的间距
                    itemWidth: 220, // 每个项目的宽度
                    threshold: 100, // 触发加载的阈值
                    batchSize: 10, // 每批加载的数量
                    ...options
                };

                // 初始化核心属性
                this.container = document.querySelector(container);
                this.items = []; // 存储所有项目元素的数组
                this.heights = new Array(this.options.columns).fill(0); // 记录每列高度的数组
                this.loading = false; // 是否正在加载的标志
                this.page = 1; // 当前页码

                this.init();
                this.bindEvents();
            }

            /**
             * 初始化容器和加载第一批图片
             */
            init() {
                // 设置容器为相对定位,作为定位上下文
                this.container.style.position = 'relative';
                this.loadImages();
            }

            /**
             * 异步加载图片
             */
            async loadImages() {
                // 防止重复加载
                if (this.loading) return;
                this.loading = true;

                try {
                    // 获取新的图片URL数组
                    const newData = this.getImgUrl();
                    // 并行加载所有图片,等待所有图片加载完成
                    await Promise.all(newData.map(url => this.addItem({ url })));
                    // 更新页码
                    this.page++;
                } finally {
                    // 无论成功失败,都需要重置加载状态
                    this.loading = false;
                }
            }

            /**
             * 添加一个新的项目到容器中
             * @param {Object} itemData - 包含图片URL的对象
             * @returns {Promise} - 返回一个Promise对象,表示图片加载和添加的过程
             */
            addItem(itemData) {
                return new Promise(resolve => {
                    // 创建item容器元素
                    const item = document.createElement('div');
                    item.className = 'item';

                    // 创建并配置图片元素
                    const img = new Image();
                    img.src = itemData.url;
                    // 图片加载完成后执行
                    img.onload = () => {
                        // 将图片添加到item容器
                        item.appendChild(img);
                        // 将item添加到主容器
                        this.container.appendChild(item);
                        // 计算并设置item的位置
                        this.calculatePosition(item);
                        // 将item存入数组中
                        this.items.push(item);
                        // 解决Promise
                        resolve();
                    };
                });
            }

            /**
             * 计算并设置项目的位置
             * @param {HTMLElement} item - 项目元素
             */
            calculatePosition(item) {
                // 获取当前最小列高度及其索引
                const minHeight = Math.min(...this.heights);
                const minIndex = this.heights.indexOf(minHeight);

                // 计算水平位置(左偏移量)
                const left = minIndex * (this.options.itemWidth + this.options.gap);

                // 使用transform设置元素位置,比left/top性能更好
                item.style.transform = `translate(${left}px, ${minHeight}px)`;

                // 更新该列的高度(当前高度 + 元素高度 + 间距)
                this.heights[minIndex] = minHeight + item.offsetHeight + this.options.gap;

                // 更新容器高度为最高列的高度
                this.container.style.height = `${Math.max(...this.heights)}px`;
            }

            /**
             * 获取图片的URL
             */
            getImgUrl() {
                // 生成指定数量的随机图片URL数组
                return Array.from(
                    { length: this.options.batchSize },
                    (_, i) => `https://picsum.photos/220/${Math.floor(Math.random() * 100 + 200)}?random=${Date.now()}${i}`
                );
            }

            /**
             * 绑定滚动和调整窗口大小的事件监听器
             */
            bindEvents() {
                // 防抖函数:限制函数在一定时间内只能执行一次
                const debounce = (fn, delay = 100) => {
                    let timer;
                    return (...args) => {
                        clearTimeout(timer);
                        timer = setTimeout(() => fn.apply(this, args), delay);
                    };
                };

                // 监听滚动事件,检测是否需要加载更多图片
                window.addEventListener('scroll', debounce(() => {
                    // 当滚动到距离底部threshold距离时触发加载
                    if ((window.innerHeight + window.scrollY) >= document.documentElement.scrollHeight - this.options.threshold) {
                        this.loadImages();
                    }
                }));

                // 监听窗口大小变化,重新计算布局
                window.addEventListener('resize', debounce(() => {
                    // 重置所有列高度
                    this.heights.fill(0);
                    // 重新计算所有项目的位置
                    this.items.forEach(item => this.calculatePosition(item));
                }));
            }
        }

        // 当DOM内容加载完成后,创建Waterfall实例
        window.addEventListener('DOMContentLoaded', () => new Waterfall('#container', { columns: 5 }));
    </script>
</body>

</html>

时间空间复杂度

js事件绑定_事件冒泡_事件代理

阻止事件冒泡:

// 阻止事件冒泡
        child.addEventListener('click', function(e) {    
            alert('子元素被点击');
            e.stopPropagati on();  
        });

事件代理:

const list = document.getElementById('list');  
// 使用事件代理处理列表点击
        list.addEventListener('click', function(e) {
            // 确保点击的是列表项
            if (e.target.className === 'list-item') {
                alert(`你点击了:${e.target.textContent}`);    
            }
        });

两个对象如何比较

对象比较(具体)

使用JSON.stringify()方法

  • 原理:将两个对象分别通过JSON.stringify()转化为JSON字符串,再比较字符串是否相等。如果两个对象的结构和属性值完全相同,转化后的JSON字符串也会相同。
  • 示例代码
let obj1 = {a: 1, b: 2};
let obj2 = {a: 1, b: 2};
let str1 = JSON.stringify(obj1);
let str2 = JSON.stringify(obj2);
console.log(str1 === str2); 
  • 优点:实现简单直观,对于纯数据类型(如对象中只包含数字、字符串、布尔值等基本类型及普通嵌套对象)的比较效率较高。
  • 缺点:无法处理对象中包含函数、正则表达式、Date对象等特殊类型的情况,会忽略这些特殊类型属性;并且不能处理对象的循环引用,可能导致报错。

使用Object.is()方法

  • 原理Object.is()是ES6新增的方法,用于判断两个值是否为同一个值。它在处理NaN-0等特殊值时与===有所不同,NaN与自身用Object.is()比较返回true+0-0Object.is()比较返回false
  • 示例代码
console.log(Object.is(NaN, NaN)); 
console.log(Object.is(+0, -0)); 
  • 优点:在特殊值的比较上更精准。
  • 缺点:它主要用于基本数据类型或对象引用的比较,若要比较两个对象的内容是否相等,仅靠它是不够的,还需结合其他逻辑,比如递归遍历对象属性进行比较。

使用递归的方式

  • 原理:通过递归遍历对象的属性,对每个属性值进行比较。如果属性值还是对象,则继续递归比较,直到比较完所有层级的属性。
  • 示例代码
function deepEqual(obj1, obj2) {
    if (obj1 === obj2) return true;
    if (typeof obj1!== 'object' || obj1 === null || typeof obj2!== 'object' || obj2 === null) return false;
    const keys1 = Object.keys(obj1);
    const keys2 = Object.keys(obj2);
    if (keys1.length!== keys2.length) return false;
    for (let key of keys1) {
        if (!obj2.hasOwnProperty(key) ||!deepEqual(obj1[key], obj2[key])) {
            return false;
        }
    }
    return true;
}
let objA = {a: 1, b: {c: 3}};
let objB = {a: 1, b: {c: 3}};
console.log(deepEqual(objA, objB)); 
  • 优点:可以深度比较复杂的嵌套对象,处理对象的各种层级结构,能准确判断对象内容是否相等。
  • 缺点:实现相对复杂,容易出错;在处理非常庞大和复杂的对象结构时,由于递归调用可能会导致性能问题,甚至造成栈溢出。
## 变量提升,以及 var、let、const 1. 概念: 变量提升是 JS 预处理机制 2. 特点: var 声明提升但未赋值,函数整体提升,let/const 提升但存在暂时性死区 TDZ(声明前无法访问)。 3. ES6 改进:块级作用域和严格模式可规避传统提升的副作用。
特性 **var** function **let** **const**
变量提升 声明提升,值不提升 整体提升(声明+值) 声明提升,但有 TDZ 声明提升,但有 TDZ
作用域 函数作用域 块级作用域 块级作用域
重复声明 ✅ 允许 ✅ 允许(覆盖) ❌ 禁止 ❌ 禁止

js传值传址

基本类型是复制值,引用类型是引用

JavaScript 中的变量分为两种类型:基本类型和引用类型。它们在赋值和传递时的行为有所不同。
基本类型(值类型)

  • 包括:Number、String、Boolean、Undefined、Null、Symbol(ES6+)
  • 赋值和传递: 当给一个变量赋值时,会创建一个基本类型值的副本,并将副本赋值给该变量。传递基本类型值时,同样是将值的副本传递给函数或另一个变量。
  • 例子
let a = 10;
let b = a; // b 获得的是 a 的副本
a = 20; // b 的值不会改变,仍然是 10
function changeValue(num) {
    num = 30; // 修改的是 num 的副本
}
changeValue(a);
console.log(a); // 输出 20,并没有因为在changeValue函数中被改变

引用类型(对象类型)

  • 包括:Object、Array、Function 等
  • 赋值和传递: 当给一个变量赋值时,实际上是将对象的引用(地址)赋值给该变量。传递引用类型值时,也是将引用传递给函数或另一个变量。
  • 例子
let obj1 = {name: 'Alice'};
let obj2 = obj1; // obj2 指向 obj1 所引用的对象
obj1.name = 'Bob'; // obj2 的 name 也会变为 'Bob'
function changeObject(obj) {
    obj.name = 'Charlie'; // 修改的是 obj 所引用的对象
}
changeObject(obj1);
console.log(obj1.name); // 输出 'Charlie'

总结

  • 基本类型变量存储的是值本身,赋值和传递时都是复制。
  • 引用类型变量存储的是值的引用(地址),赋值和传递时都是引用复制。
  • 修改基本类型变量的值不会影响其他引用该值的变量。
  • 修改引用类型变量的值会影响所有引用该值的变量。
    注意: 函数参数传递也遵循上述规则。基本类型参数是值传递,引用类型参数是引用传递。

以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣

  1. <font style="color:rgb(26, 32, 41);">Object.prototype.toString.call()</font>
    1. 每一个继承 Object 的对象都有 toString 方法(如果 tostring 方法没被重写的话,回返回 [Object type]格式的字符串(Type 为对象类型);
    2. 对于Object 以外的对象时,需要结合 call 来改变 this 指向,确保正确输出
  2. <font style="color:rgb(26, 32, 41);">instanceof</font>
    1. 内部机制:检查对象原型链中是否存在构造函数的 prototype 属性来判断类型
    2. 使用:<font style="color:rgb(26, 32, 41);">[] instanceof Array; // true</font>
    3. 注意,<font style="color:rgb(26, 32, 41);">instanceof</font>只能判断对象类型,且<font style="color:rgb(26, 32, 41);">instanceof Object</font>均为 true
  3. <font style="color:rgb(26, 32, 41);">Array.isArray()</font>
    1. 直接返回传入值是否为数组,不受原型链篡改或跨域(如 iframe)影响
  4. <font style="color:rgb(26, 32, 41);">constructor</font>
    1. 通过 constructor 属性可追溯对象的构造函数
    2. 使用<font style="color:rgb(26, 32, 41);">arr.constructor === Array</font>
    3. 但是构造函数可能被修改

扩展运算符

扩展运算符

将迭代对象展开为单独元素。扩展运算符 ... 用于展开可迭代对象或对象,实现浅拷贝、数据合并和函数传参,注意其浅拷贝特性和对象合并时的覆盖规则

应用

  1. 合并:数组与对象均可合并,对象合并时相同属性取后者,不同属性都保留。
  2. 拷贝:一维数组深拷贝;二维数组第一维深拷贝;对象一层深拷贝,二层浅拷贝。
  3. 传参:用于函数传递参数。
  4. 数组去重:辅助数组去重操作。
  5. 类型转换:字符串、nodeList转数组。
  6. 解构:支持对象与数组解构,可单独赋值或解构出对应数据结构。
  7. 伪数组转换为数组
// 1. 合并操作
// 合并数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];  
const mergedArr = [...arr1, ...arr2];
console.log('合并数组:', mergedArr); // [1, 2, 3, 4, 5, 6]

// 合并对象
const obj1 = { name: '张三', age: 20 };  
const obj2 = { age: 30, city: '北京' };
const mergedObj = { ...obj1, ...obj2 };
console.log('合并对象:', mergedObj); // { name: '张三', age: 30, city: '北京' }

// 2. 拷贝数组和对象
// 一维数组深拷贝
const originalArr = [1, 2, 3];
const copyArr = [...originalArr];
copyArr[0] = 100;
console.log('一维数组拷贝,是深拷贝,修改拷贝数组不会修改原数组:',originalArr, copyArr); // [1, 2, 3]

// 二维数组第一维深拷贝
const matrix = [[1, 2], [3, 4]];
const copyMatrix = [...matrix];
copyMatrix[0][0] = 100;
console.log('二维数组拷贝,是浅拷贝,修改拷贝数组会修改原数组:',matrix, copyMatrix); //  (浅拷贝)

// 对象拷贝
// 一维对象深拷贝示例
const originalObj = { name: 'John', age: 30 };
const copyObj = { ...originalObj };
copyObj.name = 'Mike';
console.log('一维对象拷贝,是深拷贝,修改拷贝对象不会修改原对象:', 
            originalObj,  // { name: 'John', age: 30 }
            copyObj       // { name: 'Mike', age: 30 }
           );

// 嵌套对象浅拷贝示例
const nestedObj = { 
  info: { name: 'John', age: 30 },
  scores: [90, 80]
};
const copyNestedObj = { ...nestedObj };
copyNestedObj.info.name = 'Mike';
copyNestedObj.scores[0] = 100;
console.log('嵌套对象拷贝,是浅拷贝,修改拷贝对象会修改原对象:', 
            nestedObj,     // { info: { name: 'Mike', age: 30 }, scores: [100, 80] }
            copyNestedObj  // { info: { name: 'Mike', age: 30 }, scores: [100, 80] }
           );

// 3. 传递参数
function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}
console.log('传参示例:', sum(1, 2, 3, 4)); // 10

// 4. 数组去重
const duplicateArr = [1, 2, 2, 3, 3, 4];
const uniqueArr = [...new Set(duplicateArr)];
console.log('数组去重:', uniqueArr); // [1, 2, 3, 4]

// 5. 字符串转数组
const str = 'Hello';
const strArr = [...str];
console.log('字符串转数组:', strArr); // ['H', 'e', 'l', 'l', 'o']

// 6. 解构赋值
// 解构数组
const [first, ...rest] = [1, 2, 3, 4];
console.log('解构数组:', first, rest); // 1, [2, 3, 4]

// 解构对象
const { name, ...otherProps } = { name: '张三', age: 20, city: '北京' };
console.log('解构对象:', name, otherProps); // '张三', { age: 20, city: '北京' }

// // 7. NodeList转数组
// // 假设有以下DOM结构:<div>1</div><div>2</div> (需要在浏览器中运行)

// const divs = document.querySelectorAll('div');
// const divsArr = [...divs];
// // 现在 divsArr 是一个真正的数组,可以使用数组方法

js 类型(数组/字符串/对象/数字) 常用方法

数组

JavaScript 数组提供了丰富的常用方法,用于操作和管理数组数据。以下是一些常用的数组方法:
创建数组

  • <font style="color:rgb(26, 32, 41);">new Array()</font>: 创建一个空数组或包含初始值的数组。
  • <font style="color:rgb(26, 32, 41);">Array.of()</font>: 创建一个包含初始值的数组,即使只有一个参数也能创建数组。
  • <font style="color:rgb(26, 32, 41);">Array.from()</font>: 将类数组对象或可迭代对象转换为数组。

访问数组元素

  • <font style="color:rgb(26, 32, 41);">arr[index]</font>: 通过索引访问数组元素。
  • <font style="color:rgb(26, 32, 41);">arr.length</font>: 获取数组长度。
  • <font style="color:rgb(26, 32, 41);">arr.includes(value)</font>: 判断数组是否包含特定值。
    修改数组
  • <font style="color:rgb(26, 32, 41);">arr.push(value)</font>: 向数组末尾添加一个或多个元素,并返回新数组的长度。
  • <font style="color:rgb(26, 32, 41);">arr.pop()</font>: 删除数组末尾的元素,并返回该元素。
  • <font style="color:rgb(26, 32, 41);">arr.shift()</font>: 删除数组开头的元素,并返回该元素。
  • <font style="color:rgb(26, 32, 41);">arr.unshift(value)</font>: 向数组开头添加一个或多个元素,并返回新数组的长度。
  • <font style="color:rgb(26, 32, 41);">arr.splice(index, deleteCount, ...items)</font>: 删除或替换数组中指定位置的元素,并返回被删除的元素,修改原数组。
  • <font style="color:rgb(26, 32, 41);">arr.fill(value, start, end)</font>: 用指定值填充数组中指定范围的元素。
    迭代数组
  • <font style="color:rgb(26, 32, 41);">arr.forEach(callback)</font>: 对数组的每个元素执行一次回调函数。
  • <font style="color:rgb(26, 32, 41);">arr.map(callback)</font>: 创建一个新数组,其中包含原数组每个元素执行回调函数的结果。
  • <font style="color:rgb(26, 32, 41);">arr.filter(callback)</font>: 创建一个新数组,包含原数组中所有通过测试(回调函数返回 true)的元素。
  • <font style="color:rgb(26, 32, 41);">arr.reduce(callback, initialValue)</font>: 将数组中的所有元素通过一个回调函数累加到一个初始值上,最终返回求和值。
  • <font style="color:rgb(26, 32, 41);">arr.reduceRight(callback, initialValue)</font>: 从数组末尾开始迭代,与 <font style="color:rgb(26, 32, 41);">reduce</font> 类似。
  • <font style="color:rgb(26, 32, 41);">arr.some(callback)</font>: 测试数组中是否至少有一个元素满足测试函数的条件。
  • <font style="color:rgb(26, 32, 41);">arr.every(callback)</font>: 测试数组中的所有元素是否都满足测试函数的条件。
  • <font style="color:rgb(26, 32, 41);">arr.find(callback)</font>: 返回数组中第一个满足测试函数条件的元素。
  • <font style="color:rgb(26, 32, 41);">arr.findIndex(callback)</font>: 返回数组中第一个满足测试函数条件的元素的索引。
  • <font style="color:rgb(26, 32, 41);">arr.entries()</font>: 返回一个包含数组键值对的迭代器对象。
  • <font style="color:rgb(26, 32, 41);">arr.keys()</font>: 返回一个包含数组索引的迭代器对象。
  • <font style="color:rgb(26, 32, 41);">arr.values()</font>: 返回一个包含数组元素的迭代器对象。
    排序和搜索
  • <font style="color:rgb(26, 32, 41);">arr.sort(compareFunction)</font>: 对数组元素进行排序,默认按照字典顺序排序。
  • <font style="color:rgb(26, 32, 41);">arr.reverse()</font>: 颠倒数组中元素的顺序。
  • <font style="color:rgb(26, 32, 41);">arr.indexOf(searchElement)</font>: 返回数组中第一个与指定值相等的元素的索引,如果不存在则返回 -1。
  • <font style="color:rgb(26, 32, 41);">arr.lastIndexOf(searchElement)</font>: 返回数组中最后一个与指定值相等的元素的索引,如果不存在则返回 -1。
  • <font style="color:rgb(26, 32, 41);">arr.find(callback)</font>: 返回数组中第一个满足测试函数条件的元素。
  • <font style="color:rgb(26, 32, 41);">arr.findIndex(callback)</font>: 返回数组中第一个满足测试函数条件的元素的索引。
    其他方法
  • <font style="color:rgb(26, 32, 41);">arr.copyWithin(target, start, end)</font>: 将数组的一部分复制到另一个位置。
  • <font style="color:rgb(26, 32, 41);">arr.concat(...items)</font>: 创建一个新数组,(不修改原数组)包含原数组和其他数组的所有元素。
  • <font style="color:rgb(26, 32, 41);">arr.slice(start, end)</font>: 返回数组中指定范围的元素,不改变原数组。
  • <font style="color:rgb(26, 32, 41);">arr.join(separator)</font>: 将数组元素连接成一个字符串,并使用指定的分隔符分隔。
  • <font style="color:rgb(26, 32, 41);">arr.fill(value, start, end)</font>: 用指定值填充数组中指定范围的元素。
  • <font style="color:rgb(26, 32, 41);">arr.flat(depth)</font>: 展平嵌套数组,depth 指定展开的深度。
  • <font style="color:rgb(26, 32, 41);">arr.flatMap(callback)</font>: 对数组每个元素执行回调函数,并使用 map 和 flat 的组合结果创建一个新数组。
    示例
// 创建数组
let arr = new Array(1, 2, 3);
let arr2 = Array.of(4, 5, 6);
let arr3 = Array.from([7, 8, 9]);
// 访问数组元素
console.log(arr[1]); // 输出 2
console.log(arr.length); // 输出 3
console.log(arr.includes(2)); // 输出 true
// 修改数组
arr.push(4);
arr.pop();
arr.shift();
arr.unshift(0);
arr.splice(1, 1, 5);
arr.fill(9, 1, 3);
// 迭代数组
arr.forEach(value => console.log(value));
let mappedArr = arr.map(value => value * 2);
let filteredArr = arr.filter(value => value > 3);
let reducedArr = arr.reduce((acc, value) => acc + value, 0);//
let someArr = arr.some(value => value > 5);
let everyArr = arr.every(value => value > 0);
// 排序和搜索
arr.sort();
arr.reverse();
let index = arr.indexOf(2);
let lastIndex = arr.lastIndexOf(3);
let found = arr.find(value => value === 3);
let foundIndex = arr.findIndex(value => value === 3);
// 其他方法
let copiedArr = arr.copyWithin(1, 2, 3);
let concatenatedArr = arr.concat(arr2);
let slicedArr = arr.slice(1, 3);
let joinedArr = arr.join(", ");
let flattenedArr = arr.flat();
let flatMappedArr = arr.flatMap(value => [value, value * 2]);

注意

  • 数组方法分为修改原数组和创建新数组两种。
  • 选择合适的方法需要根据实际情况和需求。
  • 建议熟悉数组方法的参数和返回值。

字符串

创建字符串:
  • <font style="color:rgb(26, 32, 41);">new String(value)</font>: 创建一个字符串对象。
  • <font style="color:rgb(26, 32, 41);">String.fromCharCode(...)</font>: 从给定的 Unicode 值创建字符串。
访问字符串元素:
  • <font style="color:rgb(26, 32, 41);">str[index]</font>: 通过索引访问字符串中的字符。
  • <font style="color:rgb(26, 32, 41);">str.length</font>: 获取字符串的长度。
  • <font style="color:rgb(26, 32, 41);">str.charAt(index)</font>: 返回指定位置的字符。
  • <font style="color:rgb(26, 32, 41);">str.charCodeAt(index)</font>: 返回指定位置字符的 Unicode 编码。
修改字符串:
  • <font style="color:rgb(26, 32, 41);">str.concat(...strings)</font>: 连接两个或多个字符串。
  • <font style="color:rgb(26, 32, 41);">str.replace(searchValue, newValue)</font>: 替换字符串中的第一个匹配项。
  • <font style="color:rgb(26, 32, 41);">str.slice(beginIndex, endIndex)</font>: 提取字符串的一部分。
  • <font style="color:rgb(26, 32, 41);">str.substring(indexStart, indexEnd)</font>: 提取字符串中两个指定索引之间的字符。
  • <font style="color:rgb(26, 32, 41);">str.toUpperCase()</font>: 将字符串转换为大写。
  • <font style="color:rgb(26, 32, 41);">str.toLowerCase()</font>: 将字符串转换为小写。
  • <font style="color:rgb(26, 32, 41);">str.trim()</font>: 去除字符串两端的空白字符。
  • <font style="color:rgb(26, 32, 41);">str.padStart(targetLength, padString)</font>: 在字符串开头填充指定的字符串,直到达到指定的长度。
  • <font style="color:rgb(26, 32, 41);">str.padEnd(targetLength, padString)</font>: 在字符串结尾填充指定的字符串,直到达到指定的长度。
迭代字符串:
  • <font style="color:rgb(26, 32, 41);">str.split(separator, limit)</font>: 将字符串分割成数组。
  • <font style="color:rgb(26, 32, 41);">str.includes(searchString, position)</font>: 判断字符串是否包含指定的子字符串。
  • <font style="color:rgb(26, 32, 41);">str.indexOf(searchValue, fromIndex)</font>: 返回子字符串在字符串中首次出现的位置,如果未找到则返回 -1。
  • <font style="color:rgb(26, 32, 41);">str.lastIndexOf(searchValue, fromIndex)</font>: 返回子字符串在字符串中最后一次出现的位置。
排序和搜索:
  • <font style="color:rgb(26, 32, 41);">str.split(separator)</font>: 将字符串分割成数组并返回。
  • <font style="color:rgb(26, 32, 41);">str.search(regexp)</font>: 使用正则表达式测试字符串,返回匹配的索引。
  • <font style="color:rgb(26, 32, 41);">str.match(regexp)</font>: 用正则表达式匹配字符串,并返回匹配的结果。
其他方法:
  • <font style="color:rgb(26, 32, 41);">str.repeat(count)</font>: 返回一个新字符串,该字符串包含指定次数的字符串重复。
  • <font style="color:rgb(26, 32, 41);">str.normalize(form)</font>: 将字符串正规化为 Unicode 形式。
  • <font style="color:rgb(26, 32, 41);">str.valueOf()</font>: 返回字符串对象的原始值。
示例:
// 创建字符串
let str1 = new String("Hello");
let str2 = String.fromCharCode(72, 101, 108, 108, 111);

// 访问字符串元素
console.log(str1[1]); // 输出 "e"
console.log(str1.length); // 输出 5
console.log(str1.charAt(1)); // 输出 "e"
console.log(str1.charCodeAt(1)); // 输出 101

// 修改字符串
let combined = str1.concat(" World");
let replaced = str1.replace("Hello", "Hi");
let sliced = str1.slice(1, 4); // "ell"
let upper = str1.toUpperCase(); // "HELLO"
let trimmed = "   Hello   ".trim(); // "Hello"

// 迭代字符串
let splitArr = str1.split("l"); // ["He", "", "o"]

// 排序和搜索
console.log(str1.includes("lo")); // true
console.log(str1.indexOf("l")); // 2
console.log(str1.lastIndexOf("l")); // 3

// 其他方法
let repeated = str1.repeat(3); // "HelloHelloHello"
let normalized = "Café".normalize("NFC"); // "Café"

对象

创建对象:
  • <font style="color:rgb(26, 32, 41);">new Object()</font>: 创建一个空对象。
  • <font style="color:rgb(26, 32, 41);">Object.create(proto)</font>: 使用指定的原型对象创建一个新对象。
  • <font style="color:rgb(26, 32, 41);">Object.assign(target, ...sources)</font>: 将一个或多个源对象的可枚举属性复制到目标对象,并返回目标对象。
  • 键值对对象:
    • <font style="color:rgb(26, 32, 41);">Object.entries(obj)</font>: 返回一个给定对象自身可枚举属性的键值对数组。
    • <font style="color:rgb(26, 32, 41);">Object.keys(obj)</font>: 返回一个给定对象自身可枚举属性的键名数组。
    • <font style="color:rgb(26, 32, 41);">Object.values(obj)</font>: 返回一个给定对象自身可枚举属性的键值数组。
访问对象属性:(<font style="color:rgb(26, 32, 41);">property</font>为具体属性 )
  • <font style="color:rgb(26, 32, 41);">obj.property</font>: 通过点操作符访问对象的属性。
  • <font style="color:rgb(26, 32, 41);">obj['property']</font>: 通过方括号操作符访问对象的属性。
  • <font style="color:rgb(26, 32, 41);">Object.getOwnPropertyNames(obj)</font>: 返回一个数组,包含对象自身的所有属性(包括非可枚举属性)。
修改对象:
  • <font style="color:rgb(26, 32, 41);">obj.property = value</font>: 直接修改对象的属性值。
  • <font style="color:rgb(26, 32, 41);">Object.defineProperty(obj, prop, descriptor)</font>: 在对象上定义一个新属性或修改一个现有属性,并返回该对象。
    • <font style="background-color:rgb(248, 248, 248);">value</font>:属性的值,默认为 <font style="background-color:rgb(248, 248, 248);">undefined</font>
    • <font style="background-color:rgb(248, 248, 248);">writable</font>:如果为 <font style="background-color:rgb(248, 248, 248);">true</font>,属性的值可以被修改,默认为 <font style="background-color:rgb(248, 248, 248);">false</font>
    • <font style="background-color:rgb(248, 248, 248);">enumerable</font>:如果为 <font style="background-color:rgb(248, 248, 248);">true</font>,属性将在对象的枚举属性中被包含,默认为 <font style="background-color:rgb(248, 248, 248);">false</font>
    • <font style="background-color:rgb(248, 248, 248);">configurable</font>:如果为 <font style="background-color:rgb(248, 248, 248);">true</font>,属性描述符可以被修改,属性也可以被删除,默认为 <font style="background-color:rgb(248, 248, 248);">false</font>
    • <font style="background-color:rgb(248, 248, 248);">get</font>:一个给属性提供 getter 的函数,默认为 <font style="background-color:rgb(248, 248, 248);">undefined</font>
    • <font style="background-color:rgb(248, 248, 248);">set</font>:一个给属性提供 setter 的函数,默认为 <font style="background-color:rgb(248, 248, 248);">undefined</font>
  • <font style="color:rgb(26, 32, 41);">delete obj.property</font>: 删除对象的属性。
迭代对象:
  • <font style="color:rgb(26, 32, 41);">for...in</font>: 遍历对象的可枚举属性。
  • <font style="color:rgb(26, 32, 41);">Object.keys(obj).forEach(key => {...})</font>: 通过键遍历对象的属性。
  • <font style="color:rgb(26, 32, 41);">Object.entries(obj).forEach(([key, value]) => {...})</font>: 遍历对象的键值对。
合并和比较对象:
  • <font style="color:rgb(26, 32, 41);">Object.assign(target, ...sources)</font>: 将源对象的属性合并到目标对象。
  • <font style="color:rgb(26, 32, 41);">Object.is(value1, value2)</font>: 判断两个值是否是相同的(具有相同的类型和相同的值)。
其他方法:
  • <font style="color:rgb(26, 32, 41);">Object.freeze(obj)</font>: 冻结对象,防止对对象的任何修改。
  • <font style="color:rgb(26, 32, 41);">Object.seal(obj)</font>: 密封对象,防止添加新属性,但可以修改现有属性。
  • <font style="color:rgb(26, 32, 41);">Object.preventExtensions(obj)</font>: 防止向对象添加新属性。
  • <font style="color:rgb(26, 32, 41);">Object.getPrototypeOf(obj)</font>: 返回对象的原型。
  • <font style="color:rgb(26, 32, 41);">Object.setPrototypeOf(obj, prototype)</font>: 设置对象的原型。
示例:
// 创建对象
let obj1 = new Object(); // 空对象  
let obj2 = { name: "Alice", age: 25 };
let obj3 = Object.create(obj2); // 以 obj2 为原型的新对象

// 访问对象属性
console.log(obj2.name); // 输出 "Alice"
console.log(obj2['age']); // 输出 25
console.log(Object.keys(obj2)); // 输出 ["name", "age"]

// 修改对象
obj2.age = 26; // 修改属性值
Object.defineProperty(obj2, 'gender', { value: 'female', writable: false }); // 定义只读属性
delete obj2.age; // 删除属性

// 迭代对象
for (let key in obj2) {
    console.log(key, obj2[key]); // 遍历属性
}
Object.entries(obj2).forEach(([key, value]) => {
    console.log(key, value); // 遍历键值对
});

// 合并和比较对象
let target = {};
Object.assign(target, obj2); // 合并对象
console.log(Object.is(target.name, "Alice")); // true

// 其他方法
let frozenObj = Object.freeze(obj2); // 冻结对象  
let sealedObj = Object.seal(obj2); // 密封对象
console.log(Object.getPrototypeOf(obj3)); // 返回 obj2

数字 number

创建数字:
  • <font style="color:rgb(26, 32, 41);">new Number(value)</font>: 创建一个数字对象,但不推荐使用,通常使用基本数据类型的数字。
数字属性:
  • <font style="color:rgb(26, 32, 41);">Number.EPSILON</font>: 表示两个可表示的数字之间最小的可区分差值。
  • <font style="color:rgb(26, 32, 41);">Number.MAX_VALUE</font>: JavaScript 中可以表示的最大数字。
  • <font style="color:rgb(26, 32, 41);">Number.MIN_VALUE</font>: JavaScript 中可以表示的最小正数(非零)。
  • <font style="color:rgb(26, 32, 41);">Number.NaN</font>: 表示不是一个数字的特殊值。
  • <font style="color:rgb(26, 32, 41);">Number.NEGATIVE_INFINITY</font>: 表示负无限大。
  • <font style="color:rgb(26, 32, 41);">Number.POSITIVE_INFINITY</font>: 表示正无限大。
数字转换:
  • <font style="color:rgb(26, 32, 41);">Number(value)</font>: 将给定值转换为数字类型。
  • <font style="color:rgb(26, 32, 41);">parseInt(string, radix)</font>: 解析一个字符串并返回一个整数。
  • <font style="color:rgb(26, 32, 41);">parseFloat(string)</font>: 解析一个字符串并返回一个浮点数。
数字格式化:
  • <font style="color:rgb(26, 32, 41);">num.toFixed(digits)</font>: 将数字转换为字符串,保留指定的小数位数。
  • <font style="color:rgb(26, 32, 41);">num.toExponential(digits)</font>: 以指数形式返回数字。
  • <font style="color:rgb(26, 32, 41);">num.toPrecision(precision)</font>: 将数字转换为字符串,保留指定的有效数字位数。
  • <font style="color:rgb(26, 32, 41);">num.toString(radix)</font>: 将数字转换为字符串,并可指定基数(如二进制、十六进制等)。
常用方法:
  • <font style="color:rgb(26, 32, 41);">Number.isFinite(value)</font>: 判断传入的值是否是有限的数字。
  • <font style="color:rgb(26, 32, 41);">Number.isInteger(value)</font>: 判断传入的值是否是整数。
  • <font style="color:rgb(26, 32, 41);">Number.isNaN(value)</font>: 判断传入的值是否是 NaN。
  • <font style="color:rgb(26, 32, 41);">Number.isSafeInteger(value)</font>: 判断传入的值是否是安全整数(在 <font style="color:rgb(26, 32, 41);">-(2^53 - 1)</font><font style="color:rgb(26, 32, 41);">2^53 - 1</font> 之间)。
示例:
// 创建数字
let num1 = new Number(10); // 不推荐使用
let num2 = 20; // 推荐使用基本数据类型

// 数字属性
console.log(Number.EPSILON); // 输出 2.220446049250313e-16
console.log(Number.MAX_VALUE); // 输出 1.7976931348623157e+308
console.log(Number.MIN_VALUE); // 输出 5e-324
console.log(Number.NaN); // 输出 NaN
console.log(Number.NEGATIVE_INFINITY); // 输出 -Infinity
console.log(Number.POSITIVE_INFINITY); // 输出 Infinity  

// 数字转换
console.log(Number("123")); // 输出 123
console.log(parseInt("123abc")); // 输出 123  
console.log(parseFloat("123.45abc")); // 输出 123.45

// 数字格式化
let num = 2.34567;
console.log(num.toFixed(2)); // 输出 "2.35"
console.log(num.toExponential(2)); // 输出 "2.35e+0"
console.log(num.toPrecision(3)); // 输出 "2.35"
console.log(num.toString(16)); // 输出 "2.57"

// 常用方法
console.log(Number.isFinite(123)); // 输出 true
console.log(Number.isInteger(123)); // 输出 true
console.log(Number.isNaN(NaN)); // 输出 true
console.log(Number.isSafeInteger(9007199254740991)); // 输出 false

<font style="color:rgb(26, 32, 41);">BigInt</font>

比较:
  • <**, >, <=, >=, ===, **!==: 支持比较运算符,注意 BigIntNumber 之间的比较会有类型转换。
转换:
  • BigInt.asIntN(width, bigint): 将 bigint 转换为指定宽度的有符号整数。
  • BigInt.asUintN(width, bigint): 将 bigint 转换为指定宽度的无符号整数。
示例:
// 创建 BigInt
let bigInt1 = BigInt(123456789012345678901234567890);
let bigInt2 = 123456789012345678901234567890n; // 使用 n 后缀

// 运算
let sum = bigInt1 + bigInt2; // 加法
let difference = bigInt1 - bigInt2; // 减法
let product = bigInt1 * bigInt2; // 乘法
let quotient = bigInt1 / bigInt2; // 除法
let remainder = bigInt1 % bigInt2; // 取余
let exponentiation = bigInt1 ** BigInt(2); // 指数运算

// 比较
console.log(bigInt1 > bigInt2); // 输出 true 或 false
console.log(bigInt1 === bigInt2); // 输出 true 或 false

// 转换
let intN = BigInt.asIntN(64, bigInt1); // 转换为有符号整数
let uintN = BigInt.asUintN(64, bigInt1); // 转换为无符号整数

console.log(intN); // 输出转换后的结果
console.log(uintN); // 输出转换后的结果

注意事项:

  • BigInt 不能与 Number 直接混合运算,例如不能将 BigIntNumber 相加,必须先转换类型。
  • BigInt 适用于需要表示非常大的整数的场景,但不支持小数。

深拷贝(Deep Copy)

深拷贝会复制对象的所有层级属性。如果属性值是基本类型,则拷贝的是基本类型的值;如果属性值是引用类型,则会创建一个新的对象或数组,并递归复制其所有属性。
实现深拷贝的几种方法:

  1. 使用JSON.parseJSON.stringify
const original = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(original));

这种方法简单但有一些限制,例如它不能复制函数、undefined、循环引用等。
2. 手动实现递归深拷贝:

function deepClone(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj; // 如果不是复杂数据类型,直接返回
  }
  let clone;
  if (obj instanceof Array) {
    clone = [];
    for (let i = 0, len = obj.length; i < len; i++) {  
      clone[i] = deepClone(obj[i]);
    }
  } else {
    clone = {};
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        clone[key] = deepClone(obj[key]);
      }
    }
  }
  return clone;
}
const original = { a: 1, b: { c: 2 } };
const deepCopy = deepClone(original);
  1. 使用第三方库,如lodash的_.cloneDeep方法:
const original = { a: 1, b: { c: 2 } };
const deepCopy = _.cloneDeep(original);

选择深拷贝还是浅拷贝取决于你的具体需求。如果你需要完全独立的副本,应该使用深拷贝。如果你知道不会修改原始对象中的引用类型属性,或者你需要的是属性引用的共享,那么浅拷贝可能就足够了。

构造函数和new操作符

在JavaScript中,构造函数(Constructor Functions)和new操作符是创建对象的重要方式。以下是关于它们的基本概念和用法。

构造函数

构造函数是用于创建和初始化对象的特殊函数。在JavaScript中,构造函数通常以大写字母开头,这是约定俗成的命名方式,以区分于普通函数。
构造函数的特点:

  • 使用this关键字来设置对象的属性和方法。
  • 通常不返回任何值,但在不使用new操作符调用时,可以显式地返回一个对象。
    构造函数的例子:
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayHello = function() {
        console.log('Hello, my name is ' + this.name);
    };
}
// 创建一个Person对象
var person1 = new Person('Alice', 30);
person1.sayHello(); // 输出: Hello, my name is Alice

new操作符

new操作符用于创建一个给定构造函数的新实例。以下是new操作符所做事情:

  1. 创建一个空对象,绑定到构造函数的 this。
  2. 将这个空对象的原型(__proto__[[Prototype]])指向构造函数的prototype属性。
  3. 调用构造函数。
  4. 如果构造函数 return 了一个对象,那么这个对象会被返回;如果没有 return 对象,则返回新创建的对象。
    使用new操作符的步骤:
// 假设有一个构造函数叫做MyConstructor
var myInstance = new MyConstructor(arg1, arg2, ...);

以下是new操作符执行时发生的事情的伪代码表示:

function newOperator(constructorFunction, ...args) {
    // 创建一个新对象
    var obj = {};
    // 设置新对象的原型
    obj.__proto__ = constructorFunction.prototype;
    // 绑定this并调用构造函数
    var result = constructorFunction.apply(obj, args);
    // 如果构造函数返回了一个对象,则返回这个对象,否则返回新创建的对象
    return (typeof result === 'object' && result !== null) ? result : obj;
}

在使用new操作符时,需要注意以下几点:

  • 如果忘记使用new操作符,构造函数中的this将不会指向新创建的对象,而可能指向全局对象(在非严格模式下)或undefined(在严格模式下),这可能导致意外的行为和错误。
  • 构造函数内部的属性和方法通常使用this来定义,这样它们就会成为实例的属性和方法。
// 错误的使用方式,没有使用new操作符
var person2 = Person('Bob', 25); // 在非严格模式下,this指向全局对象
console.log(person2); // undefined,因为Person没有返回值
console.log(window.name); // 'Bob',因为name被添加到了全局对象上
// 正确的使用方式,使用new操作符
var person3 = new Person('Charlie', 35);
console.log(person3); // Person { name: 'Charlie', age: 35 }

在 window 上绑定内容的问题

在前端开发中,window对象是一个全局对象,它代表了浏览器窗口,并作为全局变量的宿主。在window对象上频繁绑定内容可能会带来以下风险:

风险分析

  1. 命名冲突
    当不同的脚本或模块尝试在window对象上绑定相同的属性名时,会发生命名冲突。例如,两个不同的库都试图定义window.hello,这将导致一个库覆盖另一个库的hello属性,从而引发不可预见的错误。
  2. 全局污染
    随意在window对象上添加属性会导致全局命名空间变得混乱,使得代码难以维护和理解。这种污染可能导致意外的变量覆盖,并增加代码的复杂度。
  3. 安全风险
    如果攻击者能够控制window对象上的属性,他们可能会注入恶意代码或窃取敏感信息。例如,如果window.hello被设置为恶意函数,它可能会在用户不知情的情况下执行。
  4. 性能问题
    window对象上绑定过多的属性会增加内存的使用,因为全局变量在页面生命周期内不会被垃圾回收。这可能导致页面性能下降,尤其是在低内存设备上。

解决方案

  1. 模块化
    使用模块化编程(如ES6模块或CommonJS)可以减少全局变量的使用,通过导出和导入的方式来使用变量和函数,从而避免全局污染。
  2. 命名空间
    创建一个唯一的命名空间对象,并将相关的属性和方法绑定到这个对象上,而不是直接绑定到window上。例如,可以创建一个var app = {}对象,并将属性绑定到app上。
  3. IIFE(立即调用函数表达式)
    使用IIFE可以创建一个闭包,这样定义的变量和函数在外部是不可见的,从而不会污染全局命名空间。
(function() {
    var hello = 'heiyi';
    // 内部变量和函数不会影响全局作用域
})();
  1. 开启严格模式
    使用'use strict';可以避免一些JavaScript的静默错误,并强制更严格的错误处理,这有助于减少潜在的安全风险。

了解qiankun的快照沙箱实现原理(不清楚)

qiankun是一个基于微前端架构的框架,它允许在同一个页面中同时运行多个微前端应用。为了解决微前端应用之间的全局变量污染问题,qiankun实现了一种叫做“快照沙箱”的机制。
快照沙箱的工作原理如下:

  • 激活时:在微前端应用激活时,沙箱会记录当前全局环境的状态(即快照),然后恢复应用上一次运行时的全局环境。
  • 运行时:微前端应用在独立的沙箱环境中运行,其修改的全局变量不会影响到其他应用或全局环境。
  • 卸载时:当微前端应用卸载时,沙箱会恢复到激活时的快照状态,撤销应用对全局环境的所有修改,从而避免全局污染。
    快照沙箱通过这种方式保护了全局环境的纯净性,并确保了微前端应用之间的隔离性。

递归实现目录树

我可以再去看看我自己写的目录

构建Vue项目的侧边栏组件:Aside

GIT版本控制Git+GitHub Desktop

面试回答

  1. 概念: git 是分布式版本控制系统:每个人的本地仓库都保存完整的项目历史记录
  2. 基础操作:
    1. git add .添加文件到暂存区;
    2. git commit -m "提交信息" 保存快照(commit 类型有 feat,fix,docs,style,__refactor
    3. git pull origin main``git push origin main 从远程仓库拉取推送代码;
    4. 回退:git revert选择版本进行回退
  3. 协作操作:
    1. 可以设置 Branch 分支,在远程仓库 Merge 合并;
    2. 解决冲突:手动编辑后重新提交
  4. 工具:GitHub Desktop 的图形化操作
以下是关于 Git 版本控制 GitHub Desktop 的详细讲解

一、Git 基础概念

1. 什么是 Git?
  • 分布式版本控制系统:每个人的本地仓库都保存完整的项目历史记录。
  • 核心功能
    • 跟踪代码变更(提交历史)。
    • 支持多人协作(分支与合并)。
    • 快速切换不同版本(回滚操作)。
2. 关键术语
  • Repository(仓库):存储代码和历史记录的地方。
  • Commit(提交):记录一次代码变更的快照。
  • Branch(分支):并行开发的独立线路。
  • Merge(合并):将分支代码整合到主分支。
  • Fetch/Pull(拉取):从远程仓库获取最新代码。
  • Push(推送):将本地代码上传到远程仓库。

二、Git 安装与配置

1. 安装 Git
  • Git 官网 下载对应系统的安装包。
  • 验证安装:打开终端输入 git --version
2. 基本配置
git config --global user.name "你的用户名"
git config --global user.email "你的邮箱"
  • 这会关联你的提交记录到 GitHub 账号。

三、GitHub Desktop 操作指南

1. 下载与安装
2. 核心功能界面
  • 左侧栏:显示本地仓库列表。
  • 右侧面板
    • 源代码:编辑文件。
    • 提交历史:查看所有提交记录。
    • 分支:管理本地和远程分支。
    • 状态栏:显示当前未提交的更改。
3. 常用操作步骤
  1. 克隆仓库(Clone)
    • 点击菜单栏 File > Clone,输入仓库 URL。
    • 选择本地保存路径,完成克隆。
  1. 修改代码
    • 直接在 GitHub Desktop 中编辑文件。
    • 右侧面板会自动标记未提交的更改。
  1. 提交更改
    • 输入提交信息(Commit Message)。
    • 点击 Commit to main(默认分支为 main)。
  1. 推送代码(Push)
    • 提交后点击右上角 Push origin
    • 代码会同步到 GitHub 仓库。
  1. 拉取更新(Pull)
    • 点击 Pull origin 获取远程仓库最新代码。
  1. 创建分支
    • 在分支面板输入新分支名称,点击 Create branch
    • 切换分支后进行开发,完成后合并到主分支。
4. 解决冲突
  • 当多人修改同一文件时可能发生冲突。
  • GitHub Desktop 会提示冲突文件,需手动编辑后重新提交。

四、Git 命令行补充(进阶)

1. 基础命令
# 克隆仓库
git clone https://github.com/username/repository.git

查看状态

git status

添加文件到暂存区

git add .

提交更改

git commit -m "提交信息"

推送代码

git push origin main

拉取代码

git pull origin main

查看提交历史

git log

2. 分支操作
# 创建并切换分支
git checkout -b feature/new-feature

合并分支到主分支

git checkout main
git merge feature/new-feature

删除本地分支

git branch -d feature/new-feature


五、协作流程(团队开发)

  1. Fork 仓库:在 GitHub 上复制他人项目到自己账户。
  2. 克隆到本地:开始开发自己的功能。
  3. 提交 Pull Request:将修改推送至原仓库,发起合并请求。
  4. Code Review:等待原作者审核代码。
  5. 合并到主分支:通过后代码被正式收录。

六、实用技巧

  1. 使用 .gitignore:在仓库根目录添加 .gitignore 文件,忽略不需要提交的文件(如 node_modules)。
  2. 标签(Tag):为重要版本打标签:
git tag v1.0.0
git push origin --tags
  1. 查看文件差异
git diff

在 Git 版本控制系统中,提交(commit)信息是对代码变更的简要描述,它有助于团队成员理解每次提交做了什么以及为什么这么做。一份好的 commit 信息通常包含以下几个部分:

1. 标题(Header)

  • 格式类型(范围): 简短描述
  • 类型(Type)
    • feat:表示新增功能。例如 feat(user): 添加用户注册功能,表明这次提交为项目的用户模块增加了注册功能。
    • fix:用于修复 bug。如 fix(login): 修复登录时密码验证错误问题,说明对登录模块的密码验证错误进行了修复。
    • docs:涉及文档的修改,像更新 README 文件、注释等。比如 docs(api): 更新 API 文档说明
    • style:代码样式的调整,不影响代码逻辑,例如修改代码的缩进、空格、换行等。示例:style(css): 统一 CSS 文件的缩进格式
    • refactor:代码重构,既不是新增功能也不是修复 bug,只是对现有代码进行优化,提高代码的可读性、可维护性等。例如 refactor(order): 重构订单处理逻辑
    • test:添加或修改测试代码。比如 test(cart): 增加购物车功能的单元测试
    • chore:日常维护类的工作,如更新依赖、修改配置文件等。如 chore(deps): 更新项目依赖的 jQuery 版本
    • perf:对代码进行性能优化。例如 perf(search): 优化搜索功能的查询性能
    • build:影响构建系统或外部依赖的更改,如修改打包配置、添加或删除依赖等。比如 build: 升级 Webpack 到最新版本
    • ci:对持续集成配置文件或脚本的修改。例如 ci: 修改 Travis CI 的构建脚本
  • 范围(Scope):可选部分,用于指定本次提交影响的模块、功能或文件范围。例如 feat(cart): 添加购物车商品数量限制功能cart 就是范围,表明该功能与购物车模块相关。
  • 简短描述:用简洁的语言概括本次提交的主要内容,一般不超过 50 个字符,且以祈使句、现在时表述,首字母小写,结尾不要加句号。

2. 正文(Body)

  • 可选部分,用于详细描述本次提交的动机、背景、实现细节等,解释为什么要做这些更改以及是如何实现的。可以多行书写,每行不超过 72 个字符。例如:
fix(login): 修复登录时密码验证错误问题

在之前的版本中,当用户输入错误密码时,系统没有正确提示错误信息,而是直接跳转到了一个错误页面。
本次修改通过检查密码输入的返回值,当密码验证失败时,弹出正确的错误提示框,引导用户重新输入密码。

3. 脚注(Footer)

  • 可选部分,通常用于关联问题跟踪系统中的问题编号、引用相关的 pull request 等,也可用于表示重大变更或不兼容变更。例如:
feat(user): 添加用户注册功能

本次提交为系统新增了用户注册功能,用户可以通过填写用户名、密码等信息完成注册。

Closes: #123

其中 Closes: #123 表示本次提交解决了问题跟踪系统中编号为 123 的问题。如果是不兼容变更,可以使用 BREAKING CHANGE 来标记,例如:

BREAKING CHANGE: API 接口的参数格式发生了变化,调用方需要相应调整。

文件处理相关 API

FormData

  1. 概念:是一个类数组存储键值对,支持文件数据
  2. 底层原理
  3. 操作: new FormData()创建 FormData, 通过 .append 存入数据(键值对,值可以是文件);后端接受到后使用 multer 中间件 将 FormData 拆分为 req.body (表单)和 req.file(文件)
  4. 高级用法:
    1. 单/多 文件上传(多文件需要遍历文件项再 append)
    2. 进度监控
进度监控详细
XHR 实现进度跟踪
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
  if (e.lengthComputable) {
    const percentComplete = Math.round((e.loaded / e.total) * 100);
    console.log(`上传进度: ${percentComplete}%`);
  }
});

xhr.open(‘POST’, ‘/upload’);
xhr.send(formData);

Fetch API 实现进度跟踪
// 需要自定义实现进度监听(因 Fetch 不直接支持)
const controller = new AbortController();
const signal = controller.signal;

fetch(‘/upload’, {
method: ‘POST’,
body: formData,
signal
})
.then(response => response.json())
.catch(e => {
if (e.name === ‘AbortError’) console.log(‘上传取消’);
});

// 定期计算上传进度(需通过 XHR 获取上传状态)
const xhr = new XMLHttpRequest();
xhr.open(‘POST’, ‘/upload’);
xhr.upload.onprogress = (e) => {
// 更新进度条…
};
xhr.send(formData);

相互搭配

FormData 与其他技术的组合

4.1 与 FileReader 配合预览文件

// 预览图片
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = (e) => {
  const previewImg = document.getElementById('preview');
  previewImg.src = e.target.result;
};
reader.readAsDataURL(file);

// 预览文本
if (file.type.match(‘text’)) {
reader.readAsText(file, ‘utf-8’).then(text => console.log(text));
}

4.2 与 Object.keys() 遍历字段

const formData = new FormData();
formData.append(‘a’, 1);
formData.append(‘b’, 2);

// 遍历所有键(不包括文件输入的原始 File 对象)
const keys = Object.keys(formData);
console.log(keys); // [‘a’, ‘b’]

// 获取文件字段的值(File 对象)
const fileField = formData.get(‘b’); // 如果是非文件字段则返回字符串

迭代器(Iterator)

  1. 概念:顺序访问集合元素的标准化接口;任何实现了 <font style="color:rgba(0, 0, 0, 0.9);">Symbol.iterator</font> 方法的对象都称为 可迭代对象(如数组、字符串、Map、Set 等)
  2. 核心是<font style="color:rgba(0, 0, 0, 0.9);">next()</font> 方法,每次调用返回一个包含 <font style="color:rgba(0, 0, 0, 0.9);">value</font><font style="color:rgba(0, 0, 0, 0.9);">done</font>

for…of 循环、forEach方法

  1. <font style="color:rgba(0, 0, 0, 0.9);">for...of</font>
    1. 作用:用于遍历 可迭代对象(如数组、字符串、Map 等)。
    2. 原理:其底层通过调用对象的 <font style="color:rgba(0, 0, 0, 0.9);">Symbol.iterator</font> 方法获取迭代器,并自动处理 <font style="color:rgba(0, 0, 0, 0.9);">next()</font> 调用
    3. 可中断:可通过 <font style="color:rgba(0, 0, 0, 0.9);">break</font><font style="color:rgba(0, 0, 0, 0.9);">return</font> 提前终止循环
  2. <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">forEach</font>
    1. 是数组的迭代方法,用于遍历数组元素。
    2. 仅限数组:不能直接遍历普通对象或非可迭代对象
    3. 不可中断:无法通过 <font style="color:rgba(0, 0, 0, 0.9);">break</font><font style="color:rgba(0, 0, 0, 0.9);">return</font> 终止循环

AJAX、JSON

AJAX

  1. 全称: Asynchronous JavaScript and XML(异步 JavaScript 和 XML)
  2. 核心思想:通过 JavaScript 在后台与服务器进行数据交换,无需刷新整个页面即可更新网页内容
  3. 特点
    1. 异步通信(关键):浏览器无需等待服务器响应即可执行其他操作。
    2. 数据格式灵活:早期主要使用 XML,现代多用 JSON 或者 HTML。
    3. 局部刷新页面:仅更新部分网页内容,提升用户体验。

JSON

  1. (JavaScript Object Notation)轻量级的数据交换格式
  2. 数据结构:
    1. 对象:用<font style="color:rgba(0, 0, 0, 0.9);">{}</font>表示无序键值对集合,键必须用双引号包裹,值可为字符串、数值、布尔值等<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(243, 243, 243);">{"name": "张三", "age": 30}</font>
    2. 数组:用<font style="background-color:rgb(252, 252, 252);">[]</font>表示有序值集合,支持嵌套对象或数组<font style="background-color:rgb(252, 252, 252);">["数学", "英语", {"course": "科学"}]</font>
  3. 优势:
    1. 轻量、跨语言兼容、易读易维护
  4. 应用:
    1. RESTful API常用JSON进行前后端数据交互
    2. <font style="color:rgba(0, 0, 0, 0.9);">.json</font>格式的配置文件
    3. MongoDB 以JSON存储数据

<font style="color:rgba(0, 0, 0, 0.9);">localStorage</font><font style="color:rgba(0, 0, 0, 0.9);">sessionStorage</font> 存储【还有 cookie】

  1. 特点:
    1. 只能存储字符串(可用 JSON.parse)
    2. <font style="color:rgba(0, 0, 0, 0.9);">sessionStorage</font> 新建窗口不可用
  2. 使用场景
  • localStorage:(长期数据存储)用户偏好设置(如主题、字体大小)、登录状态(需结合服务端验证)、离线缓存数据。
  • sessionStorage:(临时数据存储)表单草稿、页面滚动位置、用户未提交的临时操作

手写 Promise

  1. 注意# 是定义实例静态变量,static 是类静态变量。这里用#,因为每个 Promise 状态要独立。
  2. resolve 写在构造器内部,这样每个实例都会有一个 resolve 方法,代码会占用空间。为什么不写成类的方法?
  3. if (this.#status !== PENDING) return这样状态只能改变一次
  4. try{executor}catch{reject}这样写可以处理传入 throw 报错的内容。
// 使用 ES6 类语法和私有字段实现封装
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  //1.定义静态属性
    #status = PENDING
    #result = undefined
    constructor(executor) {
      // 2. 构造器中定义resolve和reject,更改状态结果
        const resolve = (data) => {
            this.#changeState(FULFILLED, data)
        }
        const reject = (reason) => {
            this.#changeState(REJECTED, reason)
        }
        try {
          //3. 执行函数(执行顺序:调用执行函数→调用resolve更改状态结果)
            executor(resolve, reject)
        } catch (error) {
            reject(error)
        }
    }
  // 提取出通用代码
    #changeState(state, result) {
        if (this.#status !== PENDING) return
        this.#status = state
        this.#result = result
        console.log('状态变更为:', this.#status, '结果为:', this.#result);
    }
}

const p=new MyPromise((resolve,reject)=>{
    reject('失败')
    resolve('成功')
})