文章目录
-
- 执行上下文
- 作用域
- 闭包
- 原型
- this
- 设计模式
- JS 继承
- ES5/ES6 的继承除了写法以外还有什么区别?
- call、apply
- 防抖节流
- js 内存泄漏
- 介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
- 函数柯里化
- 同步异步
- js如何实现异步
- js 事件循环
- Promise
- js 能否实现多线程
- new
- Generator
- js 脚本延迟加载
- Commonjs、AMD、CMD
- 实现 sleep 效果
- some 和 every 的区别
- 用 2 种方式实现数组去重
- 找出多维数组的最大值所组成的数组
- 找出字符串中出现次数最多的字符
- 实现一个数组和对象的扁平化
- js实现回文
- 冒泡和快速排序
- 瀑布流
- 时间空间复杂度
- js事件绑定_事件冒泡_事件代理
- 两个对象如何比较
- js传值传址
- 以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣
- 扩展运算符
- js 类型(数组/字符串/对象/数字) 常用方法
- 构造函数和new操作符
- 在 window 上绑定内容的问题
- 递归实现目录树
- GIT版本控制Git+GitHub Desktop
- 查看状态
- 添加文件到暂存区
- 提交更改
- 推送代码
- 拉取代码
- 查看提交历史
- 合并分支到主分支
- 删除本地分支
执行上下文
- 上下文
- 抽象概念,包含当前代码执行的环境信息。
- 类型:
- 概述:全局、函数、eval
- 全局:由 js 引擎默认创建,执行任何代码前创建的一个执行上下文。整个 js 程序只有一个,包含了全局信息
- 函数上下文:每当执行函数时都会创建此执行上下文。包含函数的参数、局部变量、this 等信息。
- eval:是在一个
eval
函数中创建的,可将字符串当做 js 代码来执行。由于存在安全风险和性能问题,所以没有深入研究【向面试官请教】
- 执行上下文创建(这是一个复杂的过程,没理解)
- 补充(如果想起来就提及):补充细节全局上下文里有哪些预定义的东西?函数执行上下文创建过程中变量提升是什么?
- 全局上下文中域定义的是window对象它包含 document、location 等。通过var声明的全局变量和函数声明,都会成为window对象的属性和方法。
- 函数上下文创建时的变量提升,就是函数内部用var声明的变量和函数声明会被提升到函数顶部,但只是声明提升,赋值不会提升【链接到变量提升】
作用域
- js中的作用域和作用域链
- 作用域概念:规定了哪些代码区域可以访问哪些变量、函数
- 全局作用域、局部作用域
- 全局作用域:
- 最外层函数和在最外层函数外面定义的变量拥有全局作用域
- 所有未定义直接赋值的变量自动声明为拥有全局作用域(在非严格模式下)
- 所有window对象的属性拥有全局作用域(在浏览器环境中)
- 局部作用域:函数内部
- 块级作用域:
<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>
声明的变量无此块级作用域特性。 - 作用域链:
- 访问一个变量,首先在当前作用域中查找,如果找不到,就会沿着作用域链向上层作用域查找,直到找到该变量或者到达全局作用域为止。这个查找变量的路径就是作用域链。
闭包
- 闭包
- 一个函数访问了此函数的父级及父级以上的作用域变量,就形成了闭包。
- 由于闭包的存在,即使创建闭包的函数已经执行完毕,其内部的变量也不会被垃圾回收机制回收,而是会一直存在于内存中,直到闭包被销毁。
- 作用:
- 私有变量和方法,实现数据的封装和隐藏
- 缓存数据,保持状态
- 问题:内存泄漏
- 应用:Vue的事件处理函数、防抖节流 【还有很多】
原型
- 原型
- 每个对象(除null外)在创建时,都会创建与之关联的另一个对象,这个关联的对象就是我们所说的原型。
- 目的:实现继承和共享属性的机制
- 显式原型:
<font style="color:rgba(0, 0, 0, 0.85);">prototype</font>
,隐式原型:<font style="color:rgba(0, 0, 0, 0.85);">__proto__</font>
- 当使用构造函数创建实例对象时,实例对象会通过隐式原型(proto)属性关联到该构造函数的显式原型(prototype)
- 原型链:每一个实例化对象都有一个__proto__属性,这个__proto__属性指向构造该对象的构造函数的原型对象。而原型对象本身也是一个对象,它也有一个__proto__属性,这样就形成了一个链式结构,一层一层往上找,直到找到Object.prototype
this
- this指向
概念:this是一个指针型变量,它动态指向当前函数的运行环境
特点:this 的值取决于函数的调用方式,而不是定义方式。
绑定方式:显示绑定、隐式绑定;尽可能进行显示绑定
简单来讲:
**this**
的指向取决于“谁调用”- 普通函数:看函数是怎么被调用的。
- 如果是
obj.func()
,this
就是obj
。 - 如果是直接调用
func()
,this
就是全局对象或undefined
(严格模式)。
- 如果是
- 箭头函数:看函数定义时的外层作用域的
this
。- 箭头函数的
this
是固定的,不会因为调用方式而改变。
- 箭头函数的
- 普通函数:看函数是怎么被调用的。
this指向的几种情况
- 全局环境下的this指向:this始终指向全局对象window
- 函数内的this指向:
- 直接调用:this 指向全局
- 通过对象调用,this 指向调用的对象
- 箭头函数中的this指向:箭头函数本身没有 this,在内部使用 this指向外层作用域的 this
- 对象中的this指向:
- 对象内部普通函数 this 指向对象;
- 如果函数被赋值给其他变量并调用。如
const fn = obj.method; fn()
,this
会丢失,指向全局 - 对象内部箭头函数 this 指向全局或者构造函数实例
- 构造函数中的this:构造函数中的this是指向实例
- DOM 事件处理器中的 this:
- 普通函数 this 指向触发事件的元素
- 箭头函数 this 指向定义时的上下文
- 类中 this
- 实例方法:指向类的实例
- 静态方法:指向类本身(构造函数)
- 箭头函数:
- 定义在构造函数中,this 指向实例
- 定义在类属性中,this 指向外层(通常是全局)
- 高阶函数中的 this
- call、apply 或 bind 调用函数
前五节总结
- 上下文
-
- 抽象概念,包含当前代码执行的环境信息。
- 类型:
-
-
- 概述:全局、函数、eval
- 全局:由 js 引擎默认创建,执行任何代码前创建的一个执行上下文。整个 js 程序只有一个,包含了全局信息
- 函数上下文:每当执行函数时都会创建此执行上下文。包含函数的参数、局部变量、this 等信息。
- eval:是在一个
eval
函数中创建的,可将字符串当做 js 代码来执行。由于存在安全风险和性能问题,所以没有深入研究【向面试官请教】
-
- 执行上下文创建(这是一个复杂的过程,没理解)
- js中的作用域和作用域链
-
- 作用域概念:规定了哪些代码区域可以访问哪些变量、函数
- 全局作用域、局部作用域
- 全局作用域:
-
-
- 最外层函数和在最外层函数外面定义的变量拥有全局作用域
- 所有未定义直接赋值的变量自动声明为拥有全局作用域(在非严格模式下)
- 所有window对象的属性拥有全局作用域(在浏览器环境中)
-
-
- 局部作用域:函数内部
- 作用域链:
-
-
- 访问一个变量,首先在当前作用域中查找,如果找不到,就会沿着作用域链向上层作用域查找,直到找到该变量或者到达全局作用域为止。这个查找变量的路径就是作用域链。
-
- 闭包【这个再去看看哔站视频讲的阿里题】
-
- 一个函数访问了此函数的父级及父级以上的作用域变量,就形成了闭包。
- 由于闭包的存在,即使创建闭包的函数已经执行完毕,其内部的变量也不会被垃圾回收机制回收,而是会一直存在于内存中,直到闭包被销毁。
- 作用:
-
-
- 私有变量和方法,实现数据的封装和隐藏
- 缓存数据,保持状态
-
-
- 问题:内存泄漏
- 应用:Vue的事件处理函数、【还有很多】
- 原型
-
- 每个对象(除null外)在创建时,都会创建与之关联的另一个对象,这个关联的对象就是我们所说的原型。
- 目的:实现继承和共享属性的机制
- 显式原型:
prototype
,隐式原型:__proto__
- 当使用构造函数创建实例对象时,实例对象会通过隐式原型(__proto__)属性关联到该构造函数的显式原型(prototype)
- 原型链:每一个实例化对象都有一个__proto__属性,这个__proto__属性指向构造该对象的构造函数的原型对象。而原型对象本身也是一个对象,它也有一个__proto__属性,这样就形成了一个链式结构,一层一层往上找,直到找到Object.prototype
- this指向
-
- 概念:this是一个指针型变量,它动态指向当前函数的运行环境
- this指向的几种情况
-
-
- 全局环境下的this指向:this始终指向全局对象window
- 函数内的this指向:this始终指向全局对象window
- 对象中的this指向:对象内部方法的this指向调用这些方法的对象
- 箭头函数中的this指向:this指向于函数作用域所用的对象
- 构造函数中的this:构造函数中的this是指向实例
-
- 高阶函数【比较复杂,可以深入研究】
-
- 定义:函数可接收另一个函数作为参数,或者函数作为返回值输出
- 设计模式【比较复杂,可以深入研究】
-
- 定义:经验总结的可复用的解决软件设计常见问题的方案
- 常见模式
-
-
- 单例模式
- 工厂模式
- 观察者模式
- 发布订阅者模式
-
- 概念:是函数式编程中的一个重要概念,函数可以作为参数或返回存在
- 特点:
- 函数可以作为参数被传递
- 函数可以作为返回值输出
- 应用:数组函数中 map、reduce、some、sort, 防抖节流
柯里化,纯函数
设计模式
单例–弹窗组件唯一,工厂–通过参数传入不同,一个来决定加工输出对象
JS 继承
题目:谈谈对 js 继承的看法**
出题频率:30%
难易程度:中等难度
**使用场景:在单页面应用中和封装插件的时候使用的频率比较高
由于现在已经出现了 ES6 类的继承,没怎么了解。
1. 常见继承方式
我会答原型链继承、构造函数继承,其他的组合式、寄生式不太了解,更常用
es6
class
的extends
继承。
原型和原型链继承
- 简述:把父构造函数绑定到子构造函数的原型链(
**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
- 优点:方法可复用,父类原型上的方法能被所有子类实例共享。
- 缺点:父类实例的属性会被所有子类实例共享,一个子类实例修改属性可能影响其他实例;创建子类实例时不能向父类构造函数传参。
构造函数继承
- 简述原理:在子类构造函数内部,使用
call
或apply
方法调用父类构造函数,将父类的属性和方法绑定到子类实例上。例如:
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 的继承除了写法以外还有什么区别?
- 实现机制不同:
- ES5先创建子类实例对象,再将父类方法通过 Parent.call(this) 添加到子类实例的 this 上
- 先创建父类实例对象,再通过子类构造函数修改 this(必须调用 super() 后才能使用 this)
function Child() {
Parent.call(this); // 手动绑定父类实例属性
}
Child.prototype = Object.create(Parent.prototype); // 手动处理原型链
class Child extends Parent {
constructor() {
super(); // 必须先调用super()
}
}
- 语法与行为特性
- 变量提升:
- ES5 函数声明会提升
- ES6 类生名不会提升
- 严格模式
- ES5 需手动添加 ‘use strict’
- ES6 的类内部默认启用严格模式
- 方法特性
- ES5 的原型方法是可枚举的
- ES6 的类方法不可枚举
- 构造函数调用
- ES5 的原型方法可以作为构造函数调用
- ES6 的类方法没有原型(无法通过
<font style="color:rgba(0, 0, 0, 0.9);">new</font>
调用)
- 变量提升:
- ES6 特性
- super 关键字的作用:通过super()吊用父类构造函数获取 this
- 原生构造函数继承:ES6 支持继承原生构造函数(如 Array、Number)
- 其他限制
- 类名重写
- ES5允许在构造函数内部重写父类名(如 Parent = ‘NewName’)
- ES6 的类名是常量,重写会报错
- 使用 new 调用
- ES5 的构造函数可直接调用
- ES6 的类必须通过 new 实例化
- 类名重写
特性 | ES5 继承 | ES6 继承 |
---|---|---|
实现机制 | 手动绑定原型链和构造函数 | 自动处理原型链,通过 extends 和 super |
变量提升 | 支持 | 不支持(类声明不提升) |
严格模式 | 需手动启用 | 默认启用 |
方法可枚举性 | 可枚举 | 不可枚举 |
静态方法继承 | 需手动复制 | 自动继承 |
原生构造函数继承 | 不支持 | 支持 |
构造函数调用限制 | 可省略 new |
必须使用 new |
:::color4
理解:
- 对于 this 的理解:
- ES6 class 是通过 super 创建父构造函数的实例(this 指向这个实例),之后子类构造函数修改 this。
- ES5 先创建自己的实例( this 指向子函数实例),然后再手动绑定父类的实例属性到子类。
- ES5 无法正确继承内置对象(Array):
- 内部插槽不可访问:Array 实例内部插槽 [[ArrayData]] 存储元素;Parent.call(this)无法出发 Parent内部插槽的初始化逻辑。
- 原型链割裂:即使手动设置 MyArray.prototype = Object.create(Array.prototype),子类实例的原型链仍无法完整继承内置对象的特性。
- 对 Class 的理解
- Class 是语法糖,构造函数逻辑在 constructor 中,类的方法添加到构造函数的 prototype 上。
- 必须通过new调用
- 必须 new 实例化,底层通过检查new.target确保调用方式正确
- 不可枚举的方法
- 通过Object.defineProperty设置enumerable: false
- extends
- ES6通过extends实现继承,子类的原型对象(prototype)指向父类实例,形成原型链。例如,class Student extends Person会导致Student.prototype.proto === Person.prototype
- super
- 先通过super()创建父类实例this,再通过子类构造函数修改this
- 原型链关系
- Student实例 → Student.prototype → Person.prototype → Object.prototype
(实例属性) (子类方法) (父类方法) (基础方法)
- 继承顺序
- 通过 extends 调整原型链(Student.prototype.proto = Person.prototype)
- super() 创建父类实例的 this,并绑定到子类
- 子类构造函数修改 this,添加新属性
:::
- class 为什么要实例化?【先谈 new 本身的操作,其次谈问题】
- 帮我理清构造函数,原型对象,构造函数的原型对象。Student.prototype.proto === Person.prototype 这里我理解的是子类的原型对象指向父类的原型对象,但是是说的是指向父类实例,疑惑。【原型链继承】
- 我理解的是 extends 实现原型链部分的继承,super 创建父类实例,子类的 constructor 修改 this。我觉得它顺序是先创建父类实例,然后原型链继承父类实例的原型对象,再修改 this。对吗?【继承顺序】
call、apply
- 概念:
**<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>
的实例 - 作用:例如
<font style="color:rgb(31, 35, 41);">func.call(obj,参数)</font>
,就是将 func 的上下文(this)指向 obj 的上下文(this) - 常用举例:
- 构造函数继承时使用,Child 构造函数调用
<font style="color:rgb(31, 35, 41);">Parent.call(this,参数)</font>
就可以在 child 实例上继承属性方法。 - 判断对象类型使用,
<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>
- 构造函数继承时使用,Child 构造函数调用
- 关于手写 call:
- 举例讲基本思想:func.Call(obj,1,2) 把调用的函数(func)地址赋值为传入对象(obj)的一个属性,这时候调用函数 func,它的上下文(this)就是 obj 的上下文(this)。
- 基本实现步骤:1️⃣在 Function 原型上添加 call 方法,2️⃣获取上下文(从传入的第一个参数 obj 就是),3️⃣转换 this 指向(将当前函数(this)赋值给 context 上的一个属性)。
- 这里得补充(想到就说):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]]
属性,根据其值生成对应字符串。
三、不同对象类型输出
- 普通对象:
[[Class]]
为"Object"
,输出"[object Object]"
。 - 数组对象:
[[Class]]
为"Array"
,输出"[object Array]"
。 - 函数对象:
[[Class]]
为"Function"
,输出"[object Function]"
。 - 日期对象:
[[Class]]
为"Date"
,输出"[object Date]"
。 - 正则表达式对象:
[[Class]]
为"RegExp"
,输出"[object RegExp]"
。 - 自定义对象:通常
[[Class]]
也是"Object"
,除非重写toString
或修改内部属性 。
四、与instanceof区别
instanceof
检查对象是否为某个构造函数的实例,依赖原型链。Object.prototype.toString.call
直接查看对象内部类型,跨 iframe 或跨窗口场景更可靠。
五、优势
- 精确性:能准确识别对象类型。
- 跨环境:在不同JavaScript环境表现一致。
六、常见用途
- 类型检查:用于判断对象是否属于特定类型,弥补
typeof
和instanceof
局限。 - 处理各种类型数据:获取对象精确类型,方便统一处理不同类型数据。
- 浅拷贝:简单数据类型复制值,复杂数据类型复制引用地址。修改拷贝后对象,也会修改源对象。
- 深拷贝:都是复制值。修改拷贝后对象,不会修改源对象。
拷贝实现:
- 浅拷贝的实现:
- 展开运算符(最简单)
- Object.assign(常用)
- 深拷贝的两种实现:
- JSON方法
JSON.parse(JSON.stringify(obj))
(简单但有局限:无法处理函数、Symbol 类型、undefined 以及循环引用 - 手写完整实现(处理了各种特殊情况)
- lodash 的
cloneDeep(value)
- JSON方法
- 深拷贝处理的特殊情况:
- 基本类型数据、null、 日期对象、正则对象、循环引用、Map和Set、Symbol类型、原型链
防抖节流
- 防抖:
- 概念:在时间间隔内,连续触发,会重新计时,在最后一次触发后执行。【一直抖动,抖停了才执行】
- 场景:搜索框 onchange 事件、窗口变化
- 实现思想:设置一个定时器,每次调用函数时清除之前的定时器并重新计时,定时器到期了才执行函数
- 注意:使用 func.apply 来确保 this 指向正确
- 节流:
- 概念:在时间间隔内,连续触发,只有第一次生效。
- 场景:加载更多、防止高频按钮点击、函数执行频率
- 实现思想:利用定时器,仅当定时器不存在时,设置新定时器,并在定时器中调用目标函数。
- 补充:闭包、高阶函数在防抖节流中的作用
- 闭包可以保存关键状态信息(如定时器和上一次触发时间),并在多次函数调用中保持这些信息的持久性和可访问性。
- 高阶函数 可以接收目标函数作为参数,并返回一个经过封装、具备防抖或节流功能的新函数
/**
* 基础防抖函数
* @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 内存泄漏
- 什么是内存泄漏?
- 内存泄漏指无效引用,即无法被程序使用,但又没有被系统回收的内存,直至浏览器进程结束。
- 常见内存泄漏的情况及解决方案
- 全局变量:未声明的变量缓存大量数据,仅在页面刷新或关闭时才释放内存,导致内存意外泄漏。
解决方案:声明变量,避免全局直接引用。 - setInterval:其中引用的变量和对象易造成内存泄漏。
解决方案:及时清除定时器。 - DOM 事件:DOM 节点被移除但事件仍留在内存中。
解决方案:使用 removeEventListener 移除。 - 数据交叉赋值循环引用:会导致内存泄漏。
解决方案:引用完毕释放空间,将相关引用设为 null。 - DOM 移除:进行删除、更新操作后,可能忘记释放已缓存的 DOM。
解决方案:操作后将其设为 null。 - 闭包:易引发内存泄漏。
解决方案:使用完毕,手动释放闭包内数据,将其中变量设为 null 。 - 或者使用 WeakMap、WeakSet 弱引用,当键对象除了在 WeakMap 中被引用外没有其他强引用时,就会被回收。
- 全局变量:未声明的变量缓存大量数据,仅在页面刷新或关闭时才释放内存,导致内存意外泄漏。
介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
Set
- 概念:是集合,成员值唯一不重复
- 方法:
new Set()
创建、.add()
加入 - 遍历操作:
.keys==.value
返回值(因为键值是一样的);.entries
返回键值数组; - 注意:
Set.prototype[Symbol.iterator] === Set.prototype.values
可以直接使用for...of
来代替.values
WeakSet:
- 概念:只存对象,不重复的集合,弱引用(不经过垃圾回收机制,不引用它时自动回收)
- 方法:
.add .delete .has
- 注意:没有
.size
和 forEach,因为它不能遍历(随时可能消失,很可能刚刚遍历结束,成员就取不到了) - 应用:储存 DOM 节点
Map:
- 概念:键值对,键可以是对象(本来 Object 也是键值对,但是键只能是字符串)
- 方法:
.set .get(key) .has(key) .delete(key) .clear .size
- 注意:
- 读取未知键返回
undefined
- 只有引用同一个对象才会视作同一个键
- 简单类型值严格相等就会视作一个键
- 读取未知键返回
- 转化
- Map 转为对象:遍历
obj[k] = v;
进行添加;非字符串的键会被转成字符串 - 对象转为 Map:通过
Object.entries(obj)
- Map 与 JSON 之间转换
- Map 转为对象:遍历
WeakMap
- 键名只为对象,键值对(键所指对象不计入垃圾回收机制)
函数柯里化
一、柯里化的定义
柯里化是一种将多参数函数转换为一系列单参数函数的技术。通过这种转换,每次调用函数时仅传递一个参数,而函数会返回一个新函数,等待接收下一个参数,直到接收到足够的参数来执行原函数的操作。
二、优点
- 参数复用(固定部分参数)
- 延迟执行(按需触发)
- 函数组合(增强灵活性)
三、缺点
- 性能开销:创建大量嵌套函数可能增加性能开销。
- 调试困难:嵌套函数使调试变得复杂。
- 理解难度:对不熟悉函数式编程的开发者来说不直观。
- 不适用于所有场景:在某些情况下可能使代码过于复杂。
:::color4
- 关于柯里化实现:
- 实现逻辑:
创建一个函数,它接收一个多参数函数作为参数,返回一个新函数。新函数检查传入参数数量是否足够执行原函数,若不够就返回新函数继续接收参数,若足够就执行原函数。 - 闭包维护参数数组
- (思路,自己理解用) 调用 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 事件循环
- (因为 JS 是单线程的,只有一个执行栈。)
- 流程
- 执行同步代码,直至调用栈清空。
- 检查微任务队列,依次执行所有微任务(直到队列清空)。
- 取一个宏任务执行,完成后再次检查微任务队列。
- 循环上述步骤,直至所有任务完成
- 核心规则:同步代码 → 微任务 → 宏任务 → 循环
- 宏任务:
- setTimeout 和 setInterval;
- 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>
是微任务】 - DOM 事件,例如点击事件、输入事件;
- requestAnimationFrame;
- script 标签;
- 微任务:
- Promise 的 resolve 和 reject 回调
- async/await 中的异步函数;
- MutationObserver;
Promise
:::color5
对于手写 Promise 的解疑答惑:
- 如何执行的代码:
- 在创建实例的时候传入执行函数 executor不会调用,其中resolve, reject这两个参数是形参;到了构建函数内部,才会执行executor,并传入实参
- 如何实现保证涉及到同步的处理
:::
:::color5
- 异步编程基础
- 单线程与异步:JavaScript 是单线程语言,按顺序执行任务,有同步和异步操作。异步任务给浏览器处理,有结果后将回调函数插入待处理队列尾部。
- 回调函数:作为实参传入另一个函数并在其内部调用,用于完成特定任务。有普通函数和箭头函数两种写法,如
setTimeout
可开启子线程执行回调,不影响主线程运行。
- 回调地狱
- 概念:多个回调函数嵌套导致代码可维护性差,例如多个依赖结果的 AJAX 请求嵌套,会使代码难以阅读和维护。
- Promise
- 定义与特点:ES6 提供的类,是异步编程解决方案。有两个特点:状态不受外界影响,有 pending、fulfilled、rejected 三种状态;
- 语法格式:使用
new Promise
创建实例,构造函数接收一个带resolve
和reject
参数的回调函数。resolve
用于将状态变为成功并传递结果,reject
用于将状态变为失败并传递错误。实例可通过then
和catch
方法分别处理成功和失败回调。 - 链式调用:
then
方法返回新的 Promise 实例,可链式调用。如获取帖子信息后根据帖子 ID 获取评论,需在前一个then
方法中返回新的 Promise 实例。 - Promise.all():将多个 Promise 实例包装成一个新实例,接收数组作为参数。所有实例都成功时,新实例才成功,结果为各实例结果组成的数组;有一个失败则新实例失败。可用于等待多个请求返回数据后再统一处理,如页面加载时显示加载中,数据返回后隐藏加载中并渲染页面。
- 记忆:
- 单线程 + 事件循环:异步任务通过回调队列执行。
- 回调地狱:多层嵌套,难以维护。
- Promise:
- 状态:pending → fulfilled/rejected。
- 链式调用:.then 返回新 Promise。
- 并行:Promise.all 等待所有任务完成。
:::
new Promise(function (resolve, reject) {
// resolve 表示成功的回调
// reject 表示失败的回调
}).then(function (res) {
// 成功的函数
}).catch(function (err) {
// 失败的函数
})
- 执行过程:
Promise
是构造函数,接收一个函数作为参数,该函数有resolve
和reject
两个参数,它们也是函数。resolve
用于将Promise
对象状态变为成功,reject
用于变为失败。实例生成后,用then
方法指定成功回调,catch
方法指定失败回调。
:::color4
后面还有 async 和 await,generator(这个不清楚)
:::
js 能否实现多线程
new
- 创建空对象:使用
<font style="color:rgba(0, 0, 0, 0.9);">Object.create</font>
方法创建一个新对象,并将该对象的原型设置为构造函数的<font style="color:rgba(0, 0, 0, 0.9);">prototype</font>
属性 - 绑定
**<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>
绑定到新创建的对象上,并执行构造函数 - 处理构造函数的返回值:如果返回值是一个对象,则返回该对象;否则返回新创建的对象
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. 常见面试问题
- 为什么 Generator 能暂停执行?
- JavaScript 引擎通过 协程(Coroutine) 机制实现,保存函数执行上下文(栈帧),切换时恢复现场。
- 如何实现 Generator 的自动执行?
- 递归调用
next()
,并通过 Promise 链传递结果(如co
库的实现原理)。
- 递归调用
yield*
** 的作用是什么?**- 委托给另一个 Generator 或可迭代对象,等价于
for...of
遍历:
- 委托给另一个 Generator 或可迭代对象,等价于
function* genA() { yield 1; }
function* genB() { yield* genA(); } // 等价于 yield 1
一句话总结
Generator 通过
yield
实现函数暂停与恢复,是 JavaScript 协程的基础,适合复杂异步流程和自定义迭代逻辑。虽然async/await
更简洁,但 Generator 在状态管理和惰性计算中仍有独特价值。
js 脚本延迟加载
延迟加载的作用
- 提升页面加载速度、避免脚本阻塞和用户体验:延迟加载可以让主要内容先显示,再逐步加载和执行 JavaScript 脚本,避免因脚本阻塞而导致的空白页面或加载缓慢,从而提升用户体验。
- 减少带宽资源:通过延迟加载,只在需要时加载脚本,避免一开始就占用过多带宽,节省了带宽资源。
- 降低服务器压力:延迟加载将脚本请求分散到不同时间,减轻服务器在短时间内承受的请求压力。
延迟加载的方法
- 使用defer属性:在
<script>
标签中添加defer
属性,如<script defer src="a.js"></script>
,用于加载外部JavaScript文件。浏览器会在解析HTML文档的同时异步下载该脚本文件,等HTML解析完成后,按照脚本在HTML中出现的顺序依次执行。这样不会阻塞HTML的解析过程。 - 使用async属性:
<script async src="a.js"></script>
同样用于加载外部脚本。它也是异步下载脚本,但与defer
不同的是,脚本一旦下载完成就会立即执行,多个带async
属性的脚本执行顺序是不确定的,可能会打乱在HTML中出现的顺序,并且一般会在load
事件之前执行。 - 动态创建DOM:通过JavaScript动态创建
<script>
元素来实现延迟加载。示例代码,先创建一个<script>
元素对象,然后设置其src
属性指定要加载的脚本文件。之后可以将这个元素添加到DOM中(如document.head.appendChild(obj)
),从而开始加载脚本。这种方式非常灵活,可以在特定条件下才创建和加载脚本。
var obj = createElement('script');
obj.src = "a.js";
- setTimeout():
setTimeout()
是JavaScript的一个定时器函数。可以使用它来延迟执行脚本代码。比如setTimeout(function() { // 这里写要延迟执行的脚本代码 }, 1000);
,表示1000毫秒(1秒)后执行相应的脚本代码。不过需要注意,它只是延迟了代码的执行时机,并没有异步加载脚本文件,如果要加载外部脚本,还需要结合动态创建DOM等方式。 - 将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 引入依赖,一个文件就是一个模块 |
使用exports 或module.exports 导出,require 引入 |
使用export 和import 关键字,更简洁直观 |
加载方式 | 依赖前置,异步加载 | 依赖就近,按需异步加载 | 同步加载 | 静态加载,编译时确定依赖关系,可异步加载 |
执行时机 | 在所有依赖模块加载完成后执行回调函数 | 在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 种方式实现数组去重
数组去重一般在获取接口数据或者处理时,重构数据中使用
- 使用
**Set**
去重:利用 ES6 的Set
数据结构自动去除重复值的特性,通过将数组转换为Set
实例再转回数组实现数组去重 。 - 使用
**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);
找出多维数组的最大值所组成的数组
使用场景:算法题目,考核逻辑思维
- 方法一:通过两层
for
循环,外层循环遍历多维数组的子数组,内层循环找出每个子数组中的最大值,将其存入结果数组。 - 方法二:运用
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%
难易程度:中等难度
使用场景:属于算法题目,在接口拿到数据的时候对数据重构时使用
- 数组扁平化:
- 接收一个数组和扁平化深度(默认 Infinity),利用
<font style="color:rgba(0, 0, 0, 0.85);">reduce</font>
方法遍历数组,若元素为数组则递归调用自身并按指定深度继续扁平化,最后将结果拼接返回,当深度为 0 时直接返回原数组。 - 使用
<font style="color:rgb(0, 0, 0);">var newArray = arr.flat(depth)</font>
- 接收一个数组和扁平化深度(默认 Infinity),利用
- 对象扁平化:接收一个对象和属性前缀,通过遍历对象属性,若属性值为对象且非 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:转换为字符串并反转比较:将输入的字符串或数字转换为小写字符串后,通过
<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:双指针法:先把输入的字符串或数字转换为小写字符串,接着使用双指针,左指针从字符串开头、右指针从结尾开始,向中间移动并比较对应字符,若有不相等的字符则返回
<font style="color:rgba(0, 0, 0, 0.85);">false</font>
,全部相等则返回<font style="color:rgba(0, 0, 0, 0.85);">true</font>
- 备注:可以使用正则表达式
<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 则直接返回;选取基准值,将数组元素与基准值比较,小于的放入左数组,大于的放入右数组,再递归对左右数组进行快速排序,最后合并左右数组与基准值得到排序后的数组。
- 原地快速排序方法
原地快速排序方法先确定分区点,选择最右边元素为基准值,将小于基准值的元素移到左边,然后把基准值放到正确位置,再递归对基准值左右两部分进行排序,最终实现数组的原地排序。
瀑布流
题目:瀑布流
出题频率: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
和-0
用Object.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** |
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 个判断数组的方法,请分别介绍它们之间的区别和优劣
<font style="color:rgb(26, 32, 41);">Object.prototype.toString.call()</font>
- 每一个继承 Object 的对象都有 toString 方法(如果 tostring 方法没被重写的话,回返回 [Object type]格式的字符串(Type 为对象类型);
- 对于Object 以外的对象时,需要结合 call 来改变 this 指向,确保正确输出
<font style="color:rgb(26, 32, 41);">instanceof</font>
- 内部机制:检查对象原型链中是否存在构造函数的 prototype 属性来判断类型
- 使用:
<font style="color:rgb(26, 32, 41);">[] instanceof Array; // true</font>
- 注意,
<font style="color:rgb(26, 32, 41);">instanceof</font>
只能判断对象类型,且<font style="color:rgb(26, 32, 41);">instanceof Object</font>
均为 true
<font style="color:rgb(26, 32, 41);">Array.isArray()</font>
- 直接返回传入值是否为数组,不受原型链篡改或跨域(如 iframe)影响
<font style="color:rgb(26, 32, 41);">constructor</font>
- 通过 constructor 属性可追溯对象的构造函数
- 使用
<font style="color:rgb(26, 32, 41);">arr.constructor === Array</font>
- 但是构造函数可能被修改
扩展运算符
扩展运算符
将迭代对象展开为单独元素。扩展运算符 ...
用于展开可迭代对象或对象,实现浅拷贝、数据合并和函数传参,注意其浅拷贝特性和对象合并时的覆盖规则
应用
- 合并:数组与对象均可合并,对象合并时相同属性取后者,不同属性都保留。
- 拷贝:一维数组深拷贝;二维数组第一维深拷贝;对象一层深拷贝,二层浅拷贝。
- 传参:用于函数传递参数。
- 数组去重:辅助数组去重操作。
- 类型转换:字符串、nodeList转数组。
- 解构:支持对象与数组解构,可单独赋值或解构出对应数据结构。
- 伪数组转换为数组
// 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>
比较:
<
**,>
,<=
,>=
,===
, **!==
: 支持比较运算符,注意BigInt
和Number
之间的比较会有类型转换。
转换:
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
直接混合运算,例如不能将BigInt
与Number
相加,必须先转换类型。BigInt
适用于需要表示非常大的整数的场景,但不支持小数。
深拷贝(Deep Copy)
深拷贝会复制对象的所有层级属性。如果属性值是基本类型,则拷贝的是基本类型的值;如果属性值是引用类型,则会创建一个新的对象或数组,并递归复制其所有属性。
实现深拷贝的几种方法:
- 使用
JSON.parse
和JSON.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);
- 使用第三方库,如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
操作符所做事情:
- 创建一个空对象,绑定到构造函数的 this。
- 将这个空对象的原型(
__proto__
或[[Prototype]]
)指向构造函数的prototype
属性。 - 调用构造函数。
- 如果构造函数 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
对象上频繁绑定内容可能会带来以下风险:
风险分析
- 命名冲突:
当不同的脚本或模块尝试在window
对象上绑定相同的属性名时,会发生命名冲突。例如,两个不同的库都试图定义window.hello
,这将导致一个库覆盖另一个库的hello
属性,从而引发不可预见的错误。 - 全局污染:
随意在window
对象上添加属性会导致全局命名空间变得混乱,使得代码难以维护和理解。这种污染可能导致意外的变量覆盖,并增加代码的复杂度。 - 安全风险:
如果攻击者能够控制window
对象上的属性,他们可能会注入恶意代码或窃取敏感信息。例如,如果window.hello
被设置为恶意函数,它可能会在用户不知情的情况下执行。 - 性能问题:
在window
对象上绑定过多的属性会增加内存的使用,因为全局变量在页面生命周期内不会被垃圾回收。这可能导致页面性能下降,尤其是在低内存设备上。
解决方案
- 模块化:
使用模块化编程(如ES6模块或CommonJS)可以减少全局变量的使用,通过导出和导入的方式来使用变量和函数,从而避免全局污染。 - 命名空间:
创建一个唯一的命名空间对象,并将相关的属性和方法绑定到这个对象上,而不是直接绑定到window
上。例如,可以创建一个var app = {}
对象,并将属性绑定到app
上。 - IIFE(立即调用函数表达式):
使用IIFE可以创建一个闭包,这样定义的变量和函数在外部是不可见的,从而不会污染全局命名空间。
(function() {
var hello = 'heiyi';
// 内部变量和函数不会影响全局作用域
})();
- 开启严格模式:
使用'use strict';
可以避免一些JavaScript的静默错误,并强制更严格的错误处理,这有助于减少潜在的安全风险。
了解qiankun的快照沙箱实现原理(不清楚)
qiankun是一个基于微前端架构的框架,它允许在同一个页面中同时运行多个微前端应用。为了解决微前端应用之间的全局变量污染问题,qiankun实现了一种叫做“快照沙箱”的机制。
快照沙箱的工作原理如下:
- 激活时:在微前端应用激活时,沙箱会记录当前全局环境的状态(即快照),然后恢复应用上一次运行时的全局环境。
- 运行时:微前端应用在独立的沙箱环境中运行,其修改的全局变量不会影响到其他应用或全局环境。
- 卸载时:当微前端应用卸载时,沙箱会恢复到激活时的快照状态,撤销应用对全局环境的所有修改,从而避免全局污染。
快照沙箱通过这种方式保护了全局环境的纯净性,并确保了微前端应用之间的隔离性。
递归实现目录树
我可以再去看看我自己写的目录
GIT版本控制Git+GitHub Desktop
面试回答
- 概念: git 是分布式版本控制系统:每个人的本地仓库都保存完整的项目历史记录
- 基础操作:
git add .
添加文件到暂存区;git commit -m "提交信息"
保存快照(commit 类型有 feat,fix,docs,style,__refactor)git pull origin main``git push origin main
从远程仓库拉取推送代码;- 回退:
git revert
选择版本进行回退
- 协作操作:
- 可以设置 Branch 分支,在远程仓库
Merge
合并; - 解决冲突:手动编辑后重新提交
- 可以设置 Branch 分支,在远程仓库
- 工具: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. 下载与安装
- GitHub Desktop 官网 下载安装。
- 登录 GitHub 账号即可同步仓库。
2. 核心功能界面
- 左侧栏:显示本地仓库列表。
- 右侧面板:
-
- 源代码:编辑文件。
- 提交历史:查看所有提交记录。
- 分支:管理本地和远程分支。
- 状态栏:显示当前未提交的更改。
3. 常用操作步骤
- 克隆仓库(Clone)
-
- 点击菜单栏
File > Clone
,输入仓库 URL。 - 选择本地保存路径,完成克隆。
- 点击菜单栏
- 修改代码
-
- 直接在 GitHub Desktop 中编辑文件。
- 右侧面板会自动标记未提交的更改。
- 提交更改
-
- 输入提交信息(Commit Message)。
- 点击
Commit to main
(默认分支为main
)。
- 推送代码(Push)
-
- 提交后点击右上角
Push origin
。 - 代码会同步到 GitHub 仓库。
- 提交后点击右上角
- 拉取更新(Pull)
-
- 点击
Pull origin
获取远程仓库最新代码。
- 点击
- 创建分支
-
- 在分支面板输入新分支名称,点击
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
五、协作流程(团队开发)
- Fork 仓库:在 GitHub 上复制他人项目到自己账户。
- 克隆到本地:开始开发自己的功能。
- 提交 Pull Request:将修改推送至原仓库,发起合并请求。
- Code Review:等待原作者审核代码。
- 合并到主分支:通过后代码被正式收录。
六、实用技巧
- 使用
.gitignore
:在仓库根目录添加.gitignore
文件,忽略不需要提交的文件(如node_modules
)。 - 标签(Tag):为重要版本打标签:
git tag v1.0.0
git push origin --tags
- 查看文件差异:
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 的构建脚本
。
- feat:表示新增功能。例如
- 范围(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
- 概念:是一个类数组存储键值对,支持文件数据
- 底层原理
- 操作: new FormData()创建 FormData, 通过 .append 存入数据(键值对,值可以是文件);后端接受到后使用 multer 中间件 将 FormData 拆分为 req.body (表单)和 req.file(文件)
- 高级用法:
- 单/多 文件上传(多文件需要遍历文件项再 append)
- 进度监控
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)
- 概念:顺序访问集合元素的标准化接口;任何实现了
<font style="color:rgba(0, 0, 0, 0.9);">Symbol.iterator</font>
方法的对象都称为 可迭代对象(如数组、字符串、Map、Set 等) - 核心是
<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方法
<font style="color:rgba(0, 0, 0, 0.9);">for...of</font>
- 作用:用于遍历 可迭代对象(如数组、字符串、Map 等)。
- 原理:其底层通过调用对象的
<font style="color:rgba(0, 0, 0, 0.9);">Symbol.iterator</font>
方法获取迭代器,并自动处理<font style="color:rgba(0, 0, 0, 0.9);">next()</font>
调用 - 可中断:可通过
<font style="color:rgba(0, 0, 0, 0.9);">break</font>
或<font style="color:rgba(0, 0, 0, 0.9);">return</font>
提前终止循环
<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">forEach</font>
- 是数组的迭代方法,用于遍历数组元素。
- 仅限数组:不能直接遍历普通对象或非可迭代对象
- 不可中断:无法通过
<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
- 全称: Asynchronous JavaScript and XML(异步 JavaScript 和 XML)
- 核心思想:通过 JavaScript 在后台与服务器进行数据交换,无需刷新整个页面即可更新网页内容。
- 特点
- 异步通信(关键):浏览器无需等待服务器响应即可执行其他操作。
- 数据格式灵活:早期主要使用 XML,现代多用 JSON 或者 HTML。
- 局部刷新页面:仅更新部分网页内容,提升用户体验。
JSON
- (JavaScript Object Notation)轻量级的数据交换格式
- 数据结构:
- 对象:用
<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>
- 数组:用
<font style="background-color:rgb(252, 252, 252);">[]</font>
表示有序值集合,支持嵌套对象或数组<font style="background-color:rgb(252, 252, 252);">["数学", "英语", {"course": "科学"}]</font>
- 对象:用
- 优势:
- 轻量、跨语言兼容、易读易维护
- 应用:
- RESTful API常用JSON进行前后端数据交互
<font style="color:rgba(0, 0, 0, 0.9);">.json</font>
格式的配置文件- 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】
- 特点:
- 只能存储字符串(可用 JSON.parse)
<font style="color:rgba(0, 0, 0, 0.9);">sessionStorage</font>
新建窗口不可用
- 使用场景
- localStorage:(长期数据存储)用户偏好设置(如主题、字体大小)、登录状态(需结合服务端验证)、离线缓存数据。
- sessionStorage:(临时数据存储)表单草稿、页面滚动位置、用户未提交的临时操作
手写 Promise
- 注意# 是定义实例静态变量,static 是类静态变量。这里用#,因为每个 Promise 状态要独立。
- resolve 写在构造器内部,这样每个实例都会有一个 resolve 方法,代码会占用空间。为什么不写成类的方法?
if (this.#status !== PENDING) return
这样状态只能改变一次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('成功')
})