JS
JS数据类型
- 简单数据类型:number string boolean undefined null 栈内存
- 复杂数据类型:object function array 堆内存
数组转字符串-字符串转数组
var msg = '12345'
const revMsg = msg.split('').reverse().join('')//字符串-数组-反转-字符串
console.log(revMsg)// 54321
事件捕获事件冒泡---事件流
<div class="box1">
<div class="box2">
<div class="box3">
</div>
</div>
</div>
<script>
const box1 = document.querySelector('.box1');
const box2 = document.querySelector('.box2');
const box3 = document.querySelector('.box3');
box1.addEventListener('click', (e) => {
console.log('我是box1');
}, true);//此处配置了事件捕获
box2.addEventListener('click', (e) => {
console.log('我是box2');
});
box3.addEventListener('click', (e) => {
console.log('我是box3');
});
// 默认是冒泡顺序 点击3则是3 2 1
// 如果配置了true则是捕获阶段就触发 现最大的配置了true,继续点击3 则是1 3 2
</script>
- 事件冒泡是默认顺序,点击内层元素则会默认由内到外执行函数
- 事件捕获是需要配置第三个参数为true,则在捕获阶段就触发
事件委托/事件代理
- 是一种设计模式,用于简化处理事件的方式。它利用事件冒泡的特性,将事件处理程序绑定到一个父元素而不是每个子元素上。通过这种方式,可以减少事件处理程序的数量,并提高性能。
- 在事件处理函数中,可以通过事件对象的属性(如
event.target
)来确定是哪个子元素触发了事件 - 可以优化性能,只用写一个监听器
- 动态生成的元素也可以通过事件委托来监听
- 简化代码
常见数组方法
迭代就是通过循环重复执行某个操作,直到满足特定条件为止,它是处理数据集合或执行重复操作的一种常见方式。
影响原数组的方法
push
后增、unshift
前增、shift
删除第一个、pop
删除末尾、reverse
反转、sort
排序、splice
截取、替换
let arr = [1, 2, 3, 4, 5];
// 数组后增
arr.push(6);
console.log("使用 push 后:", arr); // 输出: [1, 2, 3, 4, 5, 6]
// 数组前增
arr.unshift(0);
console.log("使用 unshift 后:", arr); // 输出: [0, 1, 2, 3, 4, 5, 6]
// 删除数组第一个元素
arr.shift();
console.log("使用 shift 后:", arr); // 输出: [1, 2, 3, 4, 5]
// 删除数组最后一个元素
arr.pop();
console.log("使用 pop 后:", arr); // 输出: [1, 2, 3, 4]
// 反转数组
arr.reverse();
console.log("使用 reverse 后:", arr); // 输出: [4, 3, 2, 1]
// 数组排序
arr.sort((a, b) => a - b);
console.log("使用 sort 后:", arr); // 输出: [1, 2, 3, 4]
// 删除/替换数组中的元素
arr.splice(1, 2, 6, 7);// 从下标为 1 的元素开始删除 2 个元素, 然后插入 6, 7
console.log("使用 splice 后:", arr); // 输出: [1, 6, 7, 4]
返回新数组的方法
concat
合并、slice
截取、filter
过滤、map
每个元素执行某个操作、reduce
归并方法、flat
扁平化
- map会返回新数组 forEach没有返回值,当数组里是引用数据类型(数组、对象)时,会影响到原数组
- forEach适合于你并不打算改变数据的时候-(打印每个元素,更新 DOM 元素的状态等); map()适用于你要改变数据值的时候,返回一个新的数组.
- forEach不支持链式调用,map支持链式调用,例如map(...).reverse()
// 示例数组
const originalArray = [1, 2, 3, 4, 5];
// 使用 concat 方法合并数组
const concatArray = originalArray.concat([6, 7]);
console.log("使用 concat 合并数组:", concatArray); // 输出: [1, 2, 3, 4, 5, 6, 7]
// 使用 slice 截取数组
const slicedArray = originalArray.slice(1, 4);
console.log("使用 slice 截取数组:", slicedArray); // 输出: [2, 3, 4]
// 使用 filter 过滤数组
const filteredArray = originalArray.filter(num => num % 2 === 0);
console.log("使用 filter 过滤数组:", filteredArray); // 输出: [2, 4]
// 使用 map 对每个元素执行操作
const mappedArray = originalArray.map(num => num * 2);
console.log("使用 map 对每个元素执行操作:", mappedArray); // 输出: [2, 4, 6, 8, 10]
// 使用 reduce 进行数组归并
const reducedValue = originalArray.reduce((acc, num) => acc + num, 0);
console.log("使用 reduce 进行数组归并:", reducedValue); // 输出: 15
// 使用 flat 进行数组扁平化
const nestedArray = [1, [2, [3, [4, 5]]]];
const flattenedArray = nestedArray.flat(Infinity);
console.log("使用 flat 进行数组扁平化:", flattenedArray); // 输出: [1, 2, 3, 4, 5]
// 使用 forEach 打印每个元素,不改变原数组
originalArray.forEach(num => console.log("forEach 打印:", num));
// 使用 map 改变每个元素并返回新数组
const mappedArray2 = originalArray.map(num => num * 3);
console.log("使用 map 改变每个元素:", mappedArray2); // 输出: [3, 6, 9, 12, 15]
reduce案例
const arr = [1, 2, 3, 4]
const a = arr.reduce((sum, item, index) => {
//sum:初始值(未传初始值默认数组第一项),item:当前项,index:当前索引
console.log('和为:', sum, '当前项为:', item, '当前索引为:', index)
// 执行了三次。因为数组长度为4,初始值就是第一项,所以执行了三次
// 和为: 1 当前项为: 2 当前索引为: 1
// 和为: 3 当前项为: 3 当前索引为: 2
// 和为: 6 当前项为: 4 当前索引为: 3
return sum + item
})
console.log(a) // 10
// 传入初始值
const arr2 = [1, 2, 3, 4]
const b = arr.reduce((sum, item, index) => {
//sum:初始值10,item:当前项,index:当前索引
console.log('和为:', sum, '当前项为:', item, '当前索引为:', index)
// 执行了四次。因为数组长度为4,初始值为10,所以执行了四次
// 和为: 10 当前项为: 1 当前索引为: 0
// 和为: 11 当前项为: 2 当前索引为: 1
// 和为: 13 当前项为: 3 当前索引为: 2
// 和为: 16 当前项为: 4 当前索引为: 3
return sum + item
},10)//传入初始值10
console.log(b) // 20
indexOf和findIndex:返回索引
indexof传入具体值,findIndex传入回调函数,函数内写查找条件
const arr = [1, 2, 3, 4]
//indexOf和findIndex找不到的话都会返回 -1
const indexOf = arr.indexOf(3)//indexOf只关注具体值
console.log(indexOf) // 2
const findIndex = arr.findIndex(item => item === 3)//findIndex可以自定义查找条件,返回第一个符合条件的索引,可用大于小于等于
console.log(findIndex) // 2
数组去重
#方法1:使用Set
const arr = [1, 2, 3, 1, 2, 3];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1, 2, 3]
#方法2:使用filter搭配indexOf
const uniqueArr2 = arr.filter((item, index) => {
//检查item在数组中第一次出现的位置是否等于当前遍历元素的索引位置,如果等于的话那么就是第一次出现,就会保留在uniqueArr数组中
return arr.indexOf(item) === index;
});
console.log(uniqueArr2); // [1, 2, 3]
判断是否是数组
const arr = [1, 2, 3];
// 使用 Array.isArray 判断对象是否为数组
console.log(Array.isArray(arr)); // 输出: true
// 使用 Object.prototype.toString.call 获取对象类型
const typeString = Object.prototype.toString.call(arr);
console.log(typeString); // 输出: [object Array]
// 判断对象是否为数组
const isArray = typeString === '[object Array]';
console.log(isArray); // 输出: true
new执行期间做了四件事件
创建一个空对象
将this指向这个空对象
执行构造函数中的代码,为对象赋值
返回新对象
this的指向
- 普通函数 =》window;js严格模式(函数内写'use strict';开启严格模式) 指向undefined
- 函数调用 =》调用者
- 构造函数 =》new出来的实例
- 箭头函数 =》没有this,寻找上一层作用域的this
更改this指向
- call(指向的函数,值1,值2...)
- apply(指向的函数,['值1','值2'...])
- bind方法:let newFun = bind(指向的函数,值1,值2...) newFun() 必须要调用
//对象字面量方式
let dog = {
name: 'dog',
say(food1, food2) {
console.log('我是' + this.name + ',我喜欢吃' + food1 + '和' + food2)
}
}
let cat = {
name: 'cat'
}
// 更改dog的this指向为cat
dog.say.call(cat, '鱼', '肉') //我是cat,我喜欢吃鱼和肉
dog.say.apply(cat, ['鱼', '肉']) //我是cat,我喜欢吃鱼和肉
let newDog = dog.say.bind(cat, '鱼', '肉') //bind返回一个新函数,这个函数的this指向cat
newDog() //我是cat,我喜欢吃鱼和肉 必须调用才能执行
闭包
闭包是两个嵌套的函数,内层函数可以访问外层函数的变量,变量不会被垃圾回收机制回收,长期驻扎在内存中,避免全局变量污染,缺点是增大内存的使用量,使用不当会造成内存的泄露,解决方法是执行完的变量赋值为null,·让垃圾回收机制进行回收释放
//闭包方式1
function fnA(a) {
return function (b) {
return a + b
}
}
let funB = fnA(1) //funB = function (b) { return 1 + b; }
let result = funB(2) //result 是 3 1+2
let result2 = funB(4) //result2 是 5 1+4
//闭包方式2
function fnC() {
let count = 0
return function () {
count++
console.log(count)
}
}
let funD = fnC() //funD = function () { return count++ }
funD()//1
funD()//2
funD()//3
JS 里垃圾回收机制是什么,常用的是哪种,怎么处理的?
- JS 的垃圾回收机制是为了以防内存泄漏,内存泄漏的含义就是当已经不需要某块内存时 这块内存还存在着,垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,并释放掉它们所指向的内存。引用计数法在处理循环引用时可能会有问题。
- 最常见的垃圾回收方式是标记清除法,浏览器会定期检查在JavaScript程序中哪些对象不再被引用,然后执行标记和清除操作来释放不再需要的内存。
- 工作流程:
- 标记阶段:从根对象开始,标记所有仍然被引用的对象。
- 清除阶段:清除所有未标记的对象,释放它们占用的内存。
js中的变量提升
- 是一种语言特性,它使得在用var声明变量或用function声明函数之前可以引用它们,但在var声明之前使用值的话会导致undefined
- 而let和const没有变量提升的特性,还有作用域的限制,若在定义前使用则会报错ReferenceError,不能在初始化前访问
- const person = Object.freeze({ name: 'John' }); 通过Object.freeze来禁止修改const声明的引用类型的数据,修改时不会报错,但是不起作用
const、let、var
- var声明的变量是挂载在window上的,可以通过window.a进行访问,这也是为什么var可以重复声明同一个变量,本质是修改对象的值
- const声明的简单数据类型不能修改,复杂数据类型可以修改里面的值,但是不能重新赋值,重新赋值就会改变内存地址
Promise
Promise 是一个对象,它提供了一种处理异步代码的优雅方式,避免了回调地狱的问题。Promise 有三种状态:pending(等待态)、fulfilled(成功态)、rejected(失败态)。当一个 Promise 被创建时,它的初始状态为 pending。在异步任务成功后,Promise 变为 fulfilled 状态;在异步任务失败后,变为 rejected 状态。
处理 Promise 的状态变化通常使用
.then()
和.catch()
方法。.then()
方法接受两个参数,第一个用于处理成功的回调函数,第二个用于处理失败的回调函数,功能与.catch相同。.catch()
方法用于捕获 Promise 内部的错误或拒绝情况。静态方法
.all()
接受一个 promise 对象组成的数组,只有当所有的 promise 对象都成功时才返回一个成功状态的 Promise,返回值是一个包含所有 promise 结果的数组。静态方法.race()
接受一个 promise 对象组成的数组,只要有一个 promise 对象变为 fulfilled 或 rejected 状态,就会返回对应的结果或错误。promise的等价写法:
- Promise.reject(new Error('123')) ==》 new Promise((resolve,reject)=>{reject(new Error('123'))})
- Promise.resolve('1') ==》 new Promise((resolve,reject)=>{resolve('1')})
ES6新特性
- 箭头函数、模板字符串、解构赋值、let和const声明、promise、模块化、展开运算符、数组API,比如findIndex、includes等等
浅拷贝和深拷贝
浅拷贝只复制第一层,修改第二层数据会相互影响, 深拷贝是完全独立开来两个不同存储,修改不会相互影响
浅拷贝:扩展运算符、Object.assign({},对象)、Array.from(数组)
const a = JSON.parse(JSON.stringify(数据)) 注意:不能拷贝函数、undefined lodash库cloneDeep方法
- js
//通过MessageChannel方法 function deepClone(obj) { return new Promise((resolve) => { //通过MessageChannel 克隆数据 messageChannel是用来在不同页面或者不同代码块之间传递数据的 const { port1, port2 } = new MessageChannel(); port1.postMessage(obj);//传送需要克隆的数据 port2.onmessage = ev => resolve(ev.data);//接收克隆后的数据,通过resolve返回出去 }) } const arr = [[1, 2, 3], [111, 22]] deepClone(arr).then(res => { console.log(res)// [[1, 2, 3], [111, 22]] console.log(arr === res)// false })
防抖和节流
- 防抖的核心思想是事件触发后设置一个定时器,没有继续触发就让定时器等待一段时间后再执行函数。如果在等待时间内继续触发事件,则清除之前的定时器,开始一个新的定时器进行倒计时,场景:搜索框输入框
- 当事件触发时,立即执行相应的操作,并在指定的时间间隔内忽略后续触发的事件,直到时间间隔过去后才允许下一次事件触发。场景:高频率触发事件(例如滚动、鼠标移动)
document.querySelector('.ipt').addEventListener('input', debounce(function () {
console.log(this.value);//拿到输入框的值
}, 500));
/**
* 防抖函数
* 先定义一个timer为null 然后return一个函数,如果timer存在,就清除timer,
* 然后再设置一个新的timer赋值个定时器,然后必须使用箭头函数,
* 函数内部调用fn.call(this),这样就可以保证this指向调用者
*/
function debounce(fn, delay) {
let timer = null; // 定义一个计时器变量,初始值为 null
return function () { // 返回一个函数
if (timer) clearTimeout(timer); // 如果计时器存在,清除之前的计时器
timer = setTimeout(() => fn.call(this), delay);
// 设置一个新的计时器,将计时器赋值为新的 setTimeout,在延时结束后调用 fn,并使用 call(this) 以确保函数内部的 this 指向调用者
};
}
document.querySelector('.box1').addEventListener('scroll', throttle(function () {
console.log(this);
}, 500));
/**
* 节流函数
* 先定义一个 timer 为 null,然后返回一个函数。如果 timer 不存在,就设置一个定时器,
* 然后使用箭头函数,在函数内部调用 fn.call(this),之后将 timer 置为 null。
* 这样可以保证 this 指向调用者,且在延时结束后仍然能够继续调用。
*/
function throttle(fn, delay) {
let timer = null; // 先定义一个 timer 为 null
return function () { // 返回一个函数
if (!timer) { // 如果 timer 不存在
timer = setTimeout(() => { // 设置一个定时器
fn.call(this);
timer = null; // 定时器执行完毕后,将 timer 置为 null
}, delay);
}
};
}
事件循环(eventloop)、js异步
- JavaScript是一门单线程语言,运行在浏览器的渲染主线程中。 主线程负责执行很多任务,比如渲染页面、解析HTML/CSS、执行JavaScript等。同步执行所有操作会导致主线程阻塞,页面就无法及时响应用户操作,甚至可能导致页面假死。为了解决这个问题,浏览器就采用了异步机制。当涉及到请求、计时器等异步任务时,主线程将这些任务委托给其他线程去处理,以便能够继续执行后续代码。
- 异步机制的实现依赖于事件循环(Event Loop)。 事件循环是浏览器渲染主线程的工作方式。在每一次循环时,主线程从消息队列中取出第一个任务执行。其他线程负责在适当的时候将任务追加到消息队列末尾,等待主线程调度执行。异步任务分为宏任务和微任务,微任务具有较高的优先级,排在宏任务之前执行。
- 微任务(Promise、MutationObserver) 宏任务(
setTimeout
、setInterval
、postMessage
、点击、键盘、滚动事件等)
js计时器能做到精确计时吗?为什么?不能
- 操作系统的计时函数本身有少许偏差,不同操作系统之间也会有偏差
- 受事件循环影响,计时器回调函数只能在主线程空闲时运行,调度又会带来偏差
- w3c标准如果计时器嵌套超过5层,则会有最少4毫秒的偏差
高阶函数
满足函数作为参数或返回一个函数任意其一都属于高阶函数
常见:数组方法,定时器延时器,bind,防抖节流函数、或者自己封装
JS设计模式
单例设计模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象
发布订阅模式
基于一个事件中心,接收通知的对象是订阅者,需要先订阅某个事件,触发事件的对象是发布者,发布者通过触发事件,通知各个订阅者。vue中的事件总线就是使用的发布订阅模式。
观察者模式
目标者对象和观察者对象有相互依赖的关系,观察者对某个对象的状态进行观察,如果对象的状态发生改变,就会通知所有依赖这个对象的观察者。观察者模式相比发布订阅模式少了个事件中心,订阅者和发布者不是直接关联的。Vue中响应式数据变化是观察者模式。
输出代码执行所需时间
console.time('服务器启动时间');
server.listen(3000, () => {
console.log('express服务器启动成功');
})
console.timeEnd('服务器启动时间');
冒泡排序
//冒泡排序
let arr = [4, 2, 1,3,5,0]
function bubbleSort(arr) {
//第一层循环控制比较轮数
for (var i = 0; i < arr.length - 1; i++) {
//第二层循环控制每轮比较次数
for (var j = 0; j < arr.length - 1 - i; j++) {
//此处减i是因为每轮比较后最大的数都会放在最后,所以每轮比较后都可以少比较一次,优化性能
console.log('我是第' + i + '轮第' + j + '次')
if (arr[j] > arr[j + 1]) {//相邻元素两两对比 如果前一个比后一个大
let temp = arr[j]//定义个变量来存大的那个值
arr[j] = arr[j + 1]//把小的值赋给前一个
arr[j + 1] = temp//把大的值赋给后一个
}
}
}
return arr
}
console.log(bubbleSort(arr))