1.对于闭包的理解,以及优点缺点和应用场景
理解:
闭包是由函数以及相关引用环境组合而成的实体。它包含了函数本身以及内部函数可以访问到其外部函数的作用域,这个作用域包含了函数声明时的所有变量和变量的值
优点:
数据封装:闭包可以将函数内部的数据封装起来,通过保护变量不被外界随意访问,实现私有变量的效果
持久化状态:闭包可以将函数内部的变量和状态保存下来,并在函数执行结束后仍然可以访问,从而可以在函数外部继续使用这些信息
实现回调和延迟执行:闭包可以用于实现回调函数和延迟执行。将函数作为参数传递给其他函数,并在其他函数中调用,可以实现回调功能。通过延迟执行函数,可以在需要的时候执行函数,而不是立即执行。
缺点:
内存占用:闭包会使得函数内部的变量一直保存在内存中,不会被垃圾回收机制回收,可能会导致内存占用过多。
性能损耗:在使用闭包时,由于需要不断访问外部函数的作用域,可能会对程序的性能产生一定的影响
调试困难:由于闭包具有延迟执行和隐式传递状态的特性,对闭包的变量进行调式和跟踪可能会变得更加困难
应用场景:
私有变量和函数:通过闭包可以创建私有变量和函数,将其封装在函数内部,只需暴露必要的接口给外部,从而实现模块化的开发
实现函数工厂:通过返回一个内部函数,外部函数形成了闭包,可以根据外部函数的参数,生成不同的函数或对象
异步操作和回调:闭包可以用于实现回调函数,将函数作为参数传递,并在需要的时候执行。也可以用于保存异步操作的状态,例如保存定时器的标识符,实现延迟执行等功能。
防抖节流、延长变量生命周期等
2.事件循环的理解,以及应用理解
理解:
事件循环是一个在程序运行时不断轮询、监听和分发事件的机制。它维护了一个事件队列,并根据事件的类型和优先级,依次处理这些事件。事件可以是用户输入、定时器到期、网络请求响应等。
**宏任务:**是指需要在事件循环中按顺序排队执行的任务单元。每个任务之间会有一个清晰的边界,当一个宏任务执行完毕后,才会执行下一个宏任务。常见的任务包括:
定时器任务:通过定时器函数注册的任务,在指定的时间到达后被添加到宏任务队列中等待执行
I/O操作任务:包括网络请求、玩家读写等异步的输入输出操作
UI渲染任务:更新页面的渲染操作,如重绘、布局计算等
**微任务:**是相对于宏任务来说执行时机更早、优先级更高的任务单元。当一个宏任务执行完毕后,在下一个宏任务开始之前,会先执行当前微任务队列中的所有任务。常见的微任务包括:
Promise回调:当一个Promise对象状态变化时,会将相应的回调函数添加到微任务队列中等待执行。
MutationObserver:用于监听DOM的变化,在变化发生后触发回调函数添加到微任务队列中
process.nxtTick:在每一个事件循环执行完毕后,将回调函数添加到微任务队列中等待执行
宏任务是需要按顺序执行的任务,而微任务是在当前宏任务执行完毕后立即执行的优先级较高的任务。通过合理地使用宏任务和微任务,可以实现异步任务的管理和调度,提高代码的执行效率和用户体验。
工作原理:
(1)程序首先进入事件循环,并监听事件队列。
(2)如果事件队列中又待处理的事件,事件循环会将该事件取出并处理。
(3)事件循环根据事件的类型,将其分发给合适的处理函数或回调函数进行处理。
(4)处理函数执行完成后,可以产生新的事件并加入到事件队列中。
(5)事件循环不断重复以上步骤,直到事件队列为空或程序退出。
应用理解:
-
异步编程: 事件循环是实现异步编程的基础。通过将耗时的 I/O 操作和事件处理放入事件循环中,程序可以继续执行其他任务,而不需要等待这些操作完成。这使得在大量并发请求或高频率的事件处理场景下,程序能够更高效地利用计算资源。
-
非阻塞 I/O: 事件循环使得非阻塞的 I/O 操作成为可能。当一个 I/O 操作被发起时,事件循环会注册一个回调函数,并继续执行其他任务,而不是等待 I/O 完成。当 I/O 操作完成后,事件循环会通知相关的回调函数进行处理,从而实现对 I/O 的并发处理。
-
事件驱动编程: 事件循环支持事件驱动编程范式。程序将关注的事件注册到事件循环中,并指定对应的回调函数。当事件发生时,事件循环会调用相应的回调函数进行处理。这种方式适合处理各种类型的事件,例如用户输入、网络请求、定时器等。
-
单线程执行: 事件循环通常在单线程上执行,即事件循环线程(主线程)。这意味着事件循环中的任务会按照顺序一个接一个地执行,而不涉及多线程的同步和竞争条件。这简化了并发编程的复杂性,并避免了多线程带来的线程安全问题。
-
高效利用资源: 通过非阻塞 I/O 和事件驱动的方式,事件循环可以更好地利用计算资源。在等待 I/O 操作的同时,事件循环可以继续执行其他任务,从而最大限度地减少资源的闲置时间。
-
跨平台: 事件循环机制是许多现代编程语言和框架的核心部分,它们在不同操作系统上都提供了统一的异步编程接口。这使得开发者能够编写跨平台的代码,而无需关心底层实现的差异。
事件循环能够有效地处理异步操作,提高程序的性能和响应速度。它充分利用了计算资源,避免了阻塞等待,使得程序能够同时处理多个任务。然而,正确理解和使用事件循环是需要经验和技巧的,因为错误的使用可能导致竞态条件、死锁等问题
3.js类型检验的方式
typeof操作符:typeof操作符可以用于检测一个值的数据类型。它返回一个表示该值的类型的字符串。例如:
typeof 42; // "number"
typeof "hello"; // "string"
typeof true; // "boolean"
typeof {}; // "object"
typeof []; // "object" (数组也被判断为对象)
typeof null; // "object" (null 被判断为对象)
typeof undefined; // "undefined"
注意!typeof null返回的是"object",这是一个历史遗留问题
instanceof运算符:instanceof运算符用于检测一个对象是否属于某个类或构造函数的实例。它返回一个布尔值。例如:
const arr = [];
arr instanceof Array; // true
const date = new Date();
date instanceof Date; // true
instanceof运算符只能用于检查对象是否属于特定的类或构造函数
Object.prototype.toString方法: Object.prototype.toString方法返回一个表示对象类型的字符串。可以使用这个方法来进行更准确的类型检查:例如:
Object.prototype.toString.call(42); // "[object Number]"
Object.prototype.toString.call("hello"); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
通过使用call方法,将要检测的值作为this指向的对象进行调用,可以获取更准确的类型信息
typeof和instanceof结合: 由于typeof和instanceof有一些限制和不完善的地方,可以结合使用这两种方式来进行类型检测。例如:
function isString(value) {
return typeof value === "string" || value instanceof String;
}
isString("hello"); // true
isString(new String("hello")); // true
在这个例子中,结合了typeof和instanceof来判断一个值是否是字符串类型
第三方库: 还有一些第三方库和Typescript、Flow等提供了更强大、更丰富的类型检查功能,可以在大型项目中使用这些工具来进行类型检查和静态类型分析。
需要注意的是,JavaScript 是一门动态类型的语言,类型检验只是一种辅助手段,并不能完全解决类型相关的问题。因此,在编写 JavaScript 代码时,合理的编码规范、注释和代码组织也非常重要。
4.面向对象编程的方式的理解
(1)封装:封装是指将数据属性和相关的方法封装在一个对象中,通过访问控制来隐藏对象的内部细节,只暴露必要的接口供外部使用。这样可以保护树的完整性和安全性,同时提高代码的可维护性和重用性。
(2)继承:继承是指一个类(子类)可以继承另一个类(父类)的属性和方法,并可以在次基础上进行扩展或修改。通过继承,可以实现代码的复用、层次化的组织结构和多态性。
(3)多态:多态是指同一种操作或函数可以在不同的对象上产生不同的行为。通过多态,可以以统一的方式处理不同类型的对象,提高代码的灵活性和可扩展性。多态常通过继承和接口实现。
(4)抽象:抽象是指将现实世界中的事物抽象成类或接口,忽略不必要的细节而关注于对象的本质和行为。通过抽象,可以将复杂的问题简化为易于理解和实现的模型,使代码更加清晰和可维护。
(5)类和对象:类是对一组具有相同属性和行为的对象进行抽象的模板,它定义了对象的结构和行为。对象是根据类定义创建的具体实例。通过类和对象,可以方便地创建和管理对象,实现数据的封装和操作的封装。
(6)消息传递:面向对象编程的基本思想是对象之间通过发送消息来进行通信和交互。每个对象都有自己的状态和行为,通过消息传递,一个对象可以请求另一个对象执行某个特定的操作
面向对象编程的方式使得程序更加模块化、可扩展和易于理解。它提供了一种组织代码的方式,使得代码结构清晰、功能清晰分割,并且提供了灵活性、可维护性和代码复用性。通过合理地应用封装、继承、多态和抽象等特性,可以更好地设计和构建复杂的软件系统。
5.封装一个使用递归方式的深拷贝方法deepClone
import copy
def deepClone(obj):
if isinstance(obj, (list, tuple)):
# 如果是列表或元组,创建一个空列表用于存储拷贝后的元素
cloned_obj = []
# 遍历原列表或元组的每个元素,递归调用 deepClone 进行深拷贝
for item in obj:
cloned_obj.append(deepClone(item))
# 如果原对象是元组,则返回深拷贝后的元组
if isinstance(obj, tuple):
cloned_obj = tuple(cloned_obj)
elif isinstance(obj, dict):
# 如果是字典,创建一个空字典用于存储拷贝后的键值对
cloned_obj = {}
# 遍历原字典的每个键值对,递归调用 deepClone 进行深拷贝
for key, value in obj.items():
cloned_obj[key] = deepClone(value)
elif isinstance(obj, set):
# 如果是集合,创建一个空集合用于存储拷贝后的元素
cloned_obj = set()
# 遍历原集合的每个元素,递归调用 deepClone 进行深拷贝
for item in obj:
cloned_obj.add(deepClone(item))
else:
# 对于其他类型的对象,直接进行浅拷贝(使用 copy 模块的 copy 方法)
cloned_obj = copy.copy(obj)
return cloned_obj
使用事例:
# 测试深拷贝函数
data = [1, 2, [3, 4], {'a': 5}]
cloned_data = deepClone(data)
print(cloned_data) # [1, 2, [3, 4], {'a': 5}]
print(cloned_data is data) # False
print(cloned_data[2] is data[2]) # False
print(cloned_data[3] is data[3]) # False
以上的 deepClone
方法可以处理列表、元组、字典和集合等常见的 Python 数据类型,对于其他类型的对象,会使用浅拷贝来实现。请注意,在使用递归进行深拷贝时,需要注意避免无限递归的情况,例如对象之间形成了循环引用。这可以通过在递归调用之前检查对象是否已在拷贝过程中出现过来解决。
实现深拷贝的方法:
手动递归复制:逐个遍历对象或数组的属性,对于每个属性进行判断:
如果是基本类型,则直接赋值给新对象或新数组
如果是对象或数组,则进行递归调用,继续复制内部属性
示例:
function deepCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let copy = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key]);
}
}
return copy;
}
const original = { x: 1, y: { z: 2 } };
const copied = deepCopy(original);
使用JSON序列化和反序列化:
将对象先通过JSON.stringfy()方法转为字符串,再通过JSON.parse()方法转回对象
注意:该方法无法复制函数和特殊对象(如正则表达式、日期对象)。
示例:
const original = { x: 1, y: { z: 2 } };
const copied = JSON.parse(JSON.stringify(original));
使用第三方库:
可以使用一些第三方库,如lodash的cloneDeep()方法或jQuery的extend()方法来实现拷贝
示例:
const _ = require('lodash');
const original = { x: 1, y: { z: 2 } };
const copied = _.cloneDeep(original);
6.你对自定义hook的理解,模拟简易版的useState
React的自定义Hook是一种能够在函数组件中复用状态逻辑的机制。它可以让我们将一些通用的逻辑抽取出来,封装成可重用的函数,并在不同的组件中共享使用
使用自定义Hook有以下几个好处:
(1)代码复用和逻辑抽象:自定义Hook允许我们将一些常见的逻辑封装起来,使得这些逻辑在不同的组件中可以重复使用。通过讲逻辑抽象为自定义Hook,我们可以提高代码的复用性,避免重复编写相似的逻辑。这样也使得代码更加模块化和清晰
(2)解耦和单一职责:自定义Hook可以帮助我们解耦业务逻辑和组件渲染逻辑,使得组件更专注于处理用于界面的渲染。通过将状态管理、数据请求等逻辑抽取到自定义Hook中,可以使组件具有更单一的职责,提高代码的可维护性和可测试性
(3)共享状态和副作用逻辑:自定义Hook可以帮助我们在不同的组件之间共享状态和副作用逻辑。例如,我们可以创建一个Hook来处理数据请求,多个组件可以共享该Hook来获取和管理数据。这样可以避免状态逻辑的重复,提高代码的效率和一致性
自定义Hook的命名以“use”开头,并且可以使用React的内置Hook。自定义Hook就是一个普通的JavaScript函数,但它可以调用其他的Hook,包括useState、useEffect、useContext等等
下面是一个简化版的自定义Hook,模拟实现useState
import React from 'react';
function useState(initialValue) {
const [state, setState] = React.useState(initialValue);
function updateState(newValue) {
setState(newValue);
}
return [state, updateState];
}
// 使用自定义 Hook
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
7.Redux实现原理
Redux是一个用于管理JavaScript应用程序状态的库。它通过建立一个单一的、可预测的数据流来简化状态的管理。下面是Redux实现原理的详细说明。
Redux的实现原理主要包括三个核心概念:Store、Action、Reducer
1.Store(存储器):
Store是应用程序的状态管理中心,一个Redux应用只有一个Store
Store储存整个应用程序的状态,并提供一些方法来获取、更新和订阅状态
通过createStore函数创建Store,并将应用的根Reducer传递给createStore
2.Action(动作):
Action是一个简单的JavaScript对象,用于描述发生了什么变化
Action必须包含一个type属性,用于指示Action的类型
还可以包含其他任意属性,用于传递数据或参数
Action是通过dispatch函数发送给Store的
3.Reducer(规约函数):
Reducer是一个纯函数,接收旧的状态和一个Action,返回新的状态
Reducer定义了如何根据Action更新状态
Reducer可以有多个,但每个Reducer只负责管理状态数的一部分
Reducer需要定义初始状态,并在初始化时返回该状态
Redux的数据流如下:
1.使用createStore创建一个Store,并传递一个根Reducer
2.当应用程序的状态发生变化时,通过dispatch发送一个Action给Store
3.Store接收到Action后,将Action和当前的状态传递给根Reducer
4.根Reducer遵循Reducer的原则,根据Action的类型和数据更新当前的状态
5.Store更新完状态后,通知所有订阅了Store的回调函数
6.订阅的回调函数根据新的状态进行相应的更新操作
8.扩展运算符都有哪些作用,详细介绍一下
扩展运算符是JavaScript中的一种语法,可以将一个数组或对象拆分为独立的元素,并在需要的地方使用。它具有一下几个主要的作用:
(1)数组的展开:
可以用于在数组字面量中展开另一个数组的元素
例如:const arr1 = [1, 2, 3]; const arr2 = [...arr1, 4, 5];
,会生成一个包含 [1, 2, 3, 4, 5]
的新数组。
(2)对象的展开:
可以用于在对象字面量中展开另一个对象的属性
例如:const obj1 = { x: 1, y: 2 }; const obj2 = { ...obj1, z: 3 };
,会生成一个包含 { x: 1, y: 2, z: 3 }
的新对象。
(3)函数调用:
可以将一个数组展开为函数的参数列表
例如:function sum(a, b, c) { return a + b + c; } const numbers = [1, 2, 3]; const result = sum(...numbers);
,会得到结果 6。
(4)数组的复制:
可以通过展开一个数组来创建一个全新的数组副本
例如:const original = [1, 2, 3]; const copy = [...original];
,生成一个与 original
相同的新数组 copy
。
(5)对象的浅拷贝:
可以通过展开一个对象来创建该对象的浅拷贝
例如:const original = { x: 1, y: 2 }; const copy = { ...original };
,生成一个与 original
相同的新对象 copy
。
(6)合并数组和对象:
可以使用展开运算符合并多个数组和对象
例如:const arr1 = [1, 2]; const arr2 = [3, 4]; const merged = [...arr1, ...arr2];
,会生成一个包含 [1, 2, 3, 4]
的新数组。
(7)字符串转换为数组:
可以将字符串转换为字符数组
例如:const str = 'hello'; const chars = [...str];
,会生成一个包含 ['h', 'e', 'l', 'l', 'o']
的新数组。
扩展运算符可以提高代码的简洁性和可读性,使操作数组和对象更加方便。它在函数调用、数据复制、合并和拆分等场景中都有广泛的应用,是JavaScript中非常使用的语法特性之一。
9.React性能优化的手段有哪些?
1.使用组件的shouldComponentUpdate方法或React.memo来避免不必要的重新渲染:
shouldComponentUpdate方法和React.memo都可以用来在组件更新前进行浅层比较,以确定是否需要重新渲染组件。通过实现shouldComponentUpdate方法或React.memo包裹纯函数组件,可以在确保组件正确显示的前提下,避免不必要的重新渲染,提高性能。
2.使用React.PureComponent或React.memo来优化函数组件的性能:
React.PureComponent是一个自动实现了shouldComponentUpdate方法的组件基类,它会对组件接收的props进行浅层比较,从而避免了无效的重新渲染。对于函数组件,可以使用React.memo进行类似的优化,它会对组件的输入进行浅层比较,以决定是否进行重新渲染。
3.使用React.lazy和React.Supense进行代码分割和延迟加载:
通过将应用程序代码拆分成多个按需加载的模块,可以减小初始加载量并提高应用程序的响应速度。React.lazy和React.Supense是将React提供的懒加载的机制和组件,可以方便地实现代码分割和按需加载。
4.使用useMemo和useCallback进行记忆优化:
useMemo和useCallback都可以用于在组件重新渲染时缓存计算结果。useMemo接受一大依赖项数组和一个回调函数,并返回缓存的值。useCallback用于缓存回调函数。通过使用这两个hook,可以避免每次组件重新渲染时重复计算相同的值或创建相同的回调函数。
5.使用虚拟列表或无限滚动来优化大型列表的性能:
当面临需要渲染大量数据的列表时,使用虚拟列表或无限滚动技术可以显著提高性能。虚拟列表只会渲染可见区域的元素,而不是将所有元素都渲染到DOM中。
6.避免在渲染期间进行昂贵的操作:
在组件的渲染过程中,尽量避免执行耗时的操作,如网络请求、计算密集型的算法等。可以将这些操作移动到适当的生命周期方法、副作用钩子(如useEffect中使用异步操作),或使用WebWorker进行处理,
7.使用React DevTools进行性能检测和优化:
React DevTools是一个浏览器组件,可以帮助开发者检测和分析React组件的性能问题。它提供了一些功能来识别不必要的重新渲染、组件层级、性能剖析等,并可以根据这些信息来进行优化。
10.找出数组[1,2,3,4,5,3,2,2,4,2,2,3,1,3,5] 中出现次数最多的数,并统计出现多少次,编写个函数?
function findMostFrequentNumber(arr) {
// 创建一个对象用于记录数字出现次数
let counts = {};
// 遍历数组并统计数字出现次数
for (let i = 0; i < arr.length; i++) {
const num = arr[i];
counts[num] = (counts[num] || 0) + 1;
}
// 找到出现次数最多的数字及其出现次数
let mostFrequentNum;
let maxCount = 0;
for (const num in counts) {
if (counts[num] > maxCount) {
mostFrequentNum = parseInt(num);
maxCount = counts[num];
}
}
return { number: mostFrequentNum, count: maxCount };
}
// 调用函数查找出现次数最多的数字
const arr = [1, 2, 3, 4, 5, 3, 2, 2, 4, 2, 2, 3, 1, 3, 5];
const result = findMostFrequentNumber(arr);
console.log(`出现次数最多的数字是 ${result.number},出现了 ${result.count} 次。`);
在上述代码中,findMostFrequentNumber函数接收一个数组作为参数,并使用counts对象记录每个数字出现的次数。然后通过遍历counts对象,找到出现次数最多的数字和其对应的出现次数。最后,输出结果即可。
对于给定的数组 [1,2,3,4,5,3,2,2,4,2,2,3,1,3,5]
,以上函数将输出:
出现次数最多的数字是 2,出现了 5 次。
11.如何通过原生js 实现一个节流函数和防抖函数,写出核心代码,不是简单的思路?
节流函数:是指在指定时间内,只会执行一次函数。如果能在指定时间内反复调用函数,只有第一次会立即执行,之后的调用会被忽略。
function throttle(func, delay) {
let timerId;
return function() {
if (!timerId) {
timerId = setTimeout(() => {
func.apply(this, arguments);
timerId = null;
}, delay);
}
};
}
该函数接受两个参数:
func
:要执行的函数。delay
:每次执行的最小时间间隔。
函数返回一个闭包函数,在闭包函数中通过设置定时器来控制函数的执行频率。
具体解释如下:
-
在
throttle
函数内部,定义了一个变量timerId
来存储定时器的标识。 -
返回一个匿名函数作为节流后的函数。
-
在匿名函数中,首先检查
timerId
是否为空。若为空,则表示可以执行函数。 -
若
timerId
为空,则通过setTimeout
设置一个定时器,延迟delay
毫秒后执行函数。 -
在定时器的回调函数中,使用
func.apply(this, arguments)
执行传入的原始函数,并为其设置正确的上下文和参数。 -
执行完函数后,将
timerId
重置为null
,表示可以再次触发函数。
通过这种方式,当短时间内多次调用节流函数时,只会在指定的时间间隔后执行一次原始函数,减少了函数执行的频率。
使用节流函数可以避免频繁的函数调用,特别是在一些高频事件(如滚动事件、窗口调整大小等)中,可以有效地控制函数的执行次数,提升性能。
防抖函数:是指在指定的时间内,如果函数被连续调用,会清除之前的计时器重新开始计时。只有在最后一次函数调用之后的指定时间内没有再次调用时,才会执行函数
function debounce(func, delay) {
let timerId;
return function() {
clearTimeout(timerId);
timerId = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}
该函数接受两个参数:
func
:要执行的函数。delay
:在函数最后一次调用后延迟执行的时间。
函数返回一个闭包函数,在闭包函数中通过设置定时器来控制函数的执行频率和延迟。
具体解释如下:
-
在
debounce
函数内部,定义了一个变量timerId
来存储定时器的标识。 -
返回一个匿名函数作为防抖后的函数。
-
在匿名函数中,首先通过
clearTimeout
清除之前设置的定时器,以确保只有最后一次调用才会执行函数。 -
然后通过
setTimeout
设置一个新的定时器,在delay
毫秒之后执行函数。 -
在定时器的回调函数中,使用
func.apply(this, arguments)
执行传入的原始函数,并为其设置正确的上下文和参数。
通过这种方式,当短时间内多次调用防抖函数时,只会在最后一次调用后的指定延迟时间内执行一次原始函数。如果在延迟时间内再次调用函数,则会重新计时延迟时间,确保只有最后一次调用才生效。
使用防抖函数可以避免在某个时间间隔内频繁触发函数,特别是在一些连续的输入或者触发事件中,可以有效地控制函数的执行次数,提升性能和用户体验。
12.说说javascript内存泄漏的几种情况?
JavaScript内存泄漏是指在代码中不再需要的对象仍让占用内存,而这些对象无法被垃圾回收机制释放。
1.无意的全局变量:如果将一个对象赋值给全局变量,当不再需要该对象时忘记将其更改为null或从全局作用域中删除,就会导致内存泄漏
function foo() {
// 错误示例:未使用var/let/const声明,成为全局变量
myObject = new Object();
// 其他代码...
}
2.定时器未清除:通过setTimeout或setInterval设置的定时器,如果在不需要时未清除,就会一直在于内存中,导致内存泄漏
// 错误示例:定时器未清除
function startTimer() {
setInterval(function() {
// 定时器代码...
}, 1000);
}
// 正确示例:清除定时器
function startTimer() {
var timerId = setInterval(function() {
// 定时器代码...
}, 1000);
// 在适当的时机清除定时器
clearInterval(timerId);
}
3.闭包引用:闭包是指函数中的内部函数可以访问外部函数的变量。如果内部函数持有对外部函数作用域不再需要的对象的引用,就会导致内存的泄漏
// 错误示例:闭包引用外部函数的变量
function createClosure() {
var obj = new Object();
return function() {
// 内部函数持有对obj的引用
console.log(obj);
};
}
4.DOM引用:如果JavaScript代码中的DOM元素被保存在变量中,即使页面上不再需要该元素,它仍然占用内存并导致内存泄漏
// 错误示例:DOM 元素引用
var element = document.getElementById('myElement');
// 正确示例:DOM 元素不再使用时将其移除
var element = document.getElementById('myElement');
element.parentNode.removeChild(element);
element = null;
避免内存泄漏的常见方法包括:正确使用变量作用域、适时清除定时器、避免循环引用等。在编写JavaScript代码时,应注意及时释放不再需要的对象,并确保没有任何对象持有对这些对象的引用,这样就可以减少内存泄漏的发生。
13.如何使用css实现一个三角形,写出两种以上方案
使用伪元素::before或::after,并设置边框属性
.triangle {
width: 0;
height: 0;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
border-bottom: 100px solid red;
}
使用旋转变形
.triangle {
width: 100px;
height: 100px;
background-color: blue;
transform: rotate(45deg);
}
14.说说React生命周期有哪些不同的阶段?每个阶段对应的方法是什么?
挂载阶段:
constructor():在组件被创建时调用,用于初始化状态和绑定方法
static getDerivedStateFromProps(props,state):在组件初始化时和接收到新的属性时调用,用于根据属性生成状态的派生状态
render():根据组件的状态和属性,返回一个描述组件用户界面的React元素。
componentDidMount():在组件被插入DOM树后调用,可以进行异步数据获取、订阅事件等副作用操作
更新阶段:
static getDerivedStateFromProps(props,state):在接收到新的属性或状态发生改变时调用,用于派生新的状态
shouldComponentUpdate(nextProps,nextState):在组建即将重新渲染时调用,用于决定是否触发重新渲染,默认返回true
render():更新组件的用户界面
getSnapshotBeforeUpdate(preProps,preState):在render方法之后、更新DOM之前调用,用于获取当前DOM状态的快照信息
componentDidUpdate(prevProps,prevState,snapshot):在组件完成更新后调用,可以进行DOM操作或其他副作用操作
销毁阶段:
componentWillUnmount():在组件被移除前调用,进行清理操作,如取消定时器、取消事件订阅等
错误处理阶段:
static getDerivedStateFromError(error)
:在子组件抛出错误时调用,返回一个新的状态对象以更新组件
componentDidCatch(error, info)
:在子组件抛出错误后调用,用于记录错误信息。
从 React 17 开始,下列方法被标记为过时,将在未来版本中被删除:
componentWillMount()
componentWillReceiveProps(nextProps)
componentWillUpdate(nextProps, nextState)
需要注意的是,自 React 16.3 版本开始,componentWillMount
和 componentWillReceiveProps
被废弃,分别被 constructor
和 static getDerivedStateFromProps
替代。此外,React Hooks 的引入进一步改变了组件的生命周期处理方式。
15.说说对于Typescript函数重载的理解?
定义:
函数重载是指在一个类或者一个命名空间中,可以定义多个同名函数,但它们的参数类型、参数个数或者参数顺序不同。在调用这个同名函数时,编译器会根据给定的参数匹配最合适的函数进行调用。
目的:
函数重载的目的是提供一个便捷的方式来处理具有相似功能但参数类型或参数个数不同的函数,并且能够根据不同的参数自动选择调用哪个函数
函数重载的实现方式有两种:
**函数声明方式:**在同一个作用域中同时声明多个同名函数,但是它们的参数类型、个数和顺序不同。编译器会根据传入的参数类型、个数或顺序来决定调用哪个函数。
void foo(int x);
void foo(int x, int y);
void foo(double x);
**函数模板方式:**使用函数模板(泛型)来定义一个通用的函数,可以接受不同类型的参数。
template <typename T>
void foo(T x) {
// 函数实现
}
函数重载的优点包括:
提高代码的可读性和可维护性,通过函数名的统一性可以更方便地理解和记忆函数的功能
简化函数接口设计,避免为相似但参数不同的功能写不同的函数名。
提高代码的复用性,避免因为参数不同而需要多次实现类似的功能。
需要注意的是,函数重载的条件时参数的类型、分数或顺序不同。在进行函数调用时,编译器会根据传入的参数进行匹配,选择最合适的函数进行调用。如果存在多个函数都能匹配传入的参数,那么编译器会选择最精确的匹配。
总而言之,函数重载是一种在同一个作用域中定义多个同名函数的技术,使得程序员可以方便地处理相似但参数类型或参数个数不同的函数,并且让编译器根据传入的参数自动选择调用哪个参数。
16.TypeScript中interface和type的区别
语法差异:
interface使用interface关键字进行定义,例如:interface Person {name:string,age:number}
type使用type关键字进行定义,例如:type Person = {name:string,age:number}
可扩展性:
interface可以通过继承其他接口来进行扩展,使用关键字extends,例如:interface Student extends Person {id:number}
type不支持继承,无法扩展其他类型
使用场景:
interface适合用于描述对象的形状和结构,更贴近面向对象的概念
type可以表示更复杂的类型,包括联合类型、交叉类型等,适用于需要使用联合类型或者从现有类型派生新类型的情况
可读性:
interface的语法对于描述对象的结构和属性更加直观易懂
type的语法更加灵活,适用于更复杂的类型定义
兼容性:
interface支持声明合并,可以多次声明同一个接口,自动合并属性。例如,多个同名的interface可以合并为一个接口
type不支持声明合并
总的来说,interface更适合用于描述对象的形状和结构,type更适合用于表示复杂类型或者从现有派生新类型。在实际使用中,可以根据具体的需求和喜好来选择使用interface还是type。通常情况下,两者可互换使用,但在某些情况下,interface的一些特性(如声明合并)可能更适合。
17.typescript 中都有哪些修饰符,说明他们的作用?
public(公共修饰符):表示成员是公共的,可以在任何地方访问,并且默认为公有,无需显式指定。
private(私有修饰符):表示成员只能在类内部进行访问,不能在类外部或子类中访问。通过私有成员可以实现封装和隐藏实现细节
protected(受保护修饰符):表示受保护成员只能在类内部和该类的子类中进行访问,不能在类外部访问
readonly(只读修饰符):用于修饰属性,表示该属性只能在声明时或构造函数中被赋值,之后不允许修改
static(静态修饰符):用于修饰属性或方法,表示该属性或方法属于类本身而不是实例,可以直接通过类名进行访问
abstract(抽象修饰符):用于声明抽象类、抽象属性和抽象方法。抽象类不能被实例化,只能被继承,并且子类必须实现抽象方法或属性
18.实现一个 myMap方法,可以跟数组 map 方法一样,可以进行数组循环并且,可以返回新的数组?
Array.proptotype.mymap = function(callback){
const newArray = []
for(let i = 0;i<this.length;i++){
newArray.push(callback(this[i],i,this))
}
return newArray
}
const numbers =[1, 2,3,4,5];
const doubled = numbers.mymap((num) => num * 2);
console.log(doubled); // 输出 [2,4,6,8,107
这段代码首先在Array.prototype对象上定义了一个名为mymap的新方法,我们可以在任何数组上直接调用mymap方法,就像调用数组原生的map方法一样
在方法内部,我们创建了一个空数组newArray,用于存储映射后的结果
然后,使用for循环遍历原始数组(this指向调用mymap方法的数组)。对于每个元素,我们都调用传入的回调函数callback,并将当前元素、索引和原始数组作为参数传递给它。将回调函数的返回值添加到新数组newArray中。
最后,返回新数组newArray,它包含了原始数组经过回调函数处理后的映射结果
在使用示例中,我们将数组[1,2,3,4,5]调用自定义的mymap方法,并传入一个回调函数。该回调函数调用后会得到一个新的数组,并将其赋值给变量doubled。最后我们通过console.log打印出该新数组
19.说说对typescript抽象类的理解?
在typescript中,抽象类是一种特殊类型的类,用于定义其他类的通用行为和结构。抽象类本身不能被实例化,而是作为其他类的基类使用
抽象类通过abstract关键字进行声明,并可以包含抽象方法、普通方法和属性。抽象方法是一种没有具体实现的方法,在抽象类中只定义了方法的签名,具体的实现由派生类来完成。派生类必须实现抽象类中的所有抽象方法,否则编译器会报错
抽象类的主要目的是提供一个通用的基类,通过继承它来创建具体的子类,并在子类中实现抽象方法。这样可以确保子类拥有相同的接口和行为,同时也可以在抽象类中定义共享的属性和方法。
下面是一个使用抽象类的简单示例:
abstract class Animal {
abstract makeSound(): void; // 抽象方法
move(): void {
console.log('Animal is moving.');
}
}
class Dog extends Animal {
makeSound(): void {
console.log('Dog barks.');
}
}
class Cat extends Animal {
makeSound(): void {
console.log('Cat meows.');
}
}
const dog = new Dog();
dog.makeSound(); // 输出: "Dog barks."
dog.move(); // 输出: "Animal is moving."
const cat = new Cat();
cat.makeSound(); // 输出: "Cat meows."
cat.move(); // 输出: "Animal is moving."
在上面的示例中,Animal
是一个抽象类,它定义了 makeSound
抽象方法和普通方法 move
。Dog
和 Cat
是派生类,它们继承了 Animal
并实现了 makeSound
方法。通过创建 Dog
和 Cat
的实例,可以调用它们各自的 makeSound
方法和继承自 Animal
的 move
方法。
抽象类提供了一种有效的方式来定义通用的类结构,并确保子类具有一致的行为。它们在面向对象的程序设计中起到很重要的作用,使得代码更具可维护性和扩展性。
20.说说对typescript命名空间的理解?
在typescript中,命名空间是一种将代码组织成逻辑模块的方式。它提供了一种避免全局命名冲突的方式,将相关的函数、类、接口等封装在一个命名空间中,从而使得代码更加模块化和可维护
使用命名空间可以通过namespace关键字来定义,然后在其中定义相应的函数、类、接口等。命名空间的变量和类型定义不会污染全局命名空间,只有在使用时才需要显示地引用
下面是一个简单的例子来说明命名空间的使用:
namespace MyNamespace {
export interface Person {
name: string;
age: number;
}
export function greet(person: Person): void {
console.log(`Hello, ${person.name}! You are ${person.age} years old.`);
}
}
const person: MyNamespace.Person = { name: 'Alice', age: 25 };
MyNamespace.greet(person); // 输出: "Hello, Alice! You are 25 years old."
在上面的示例中,我们创建了一个名为 MyNamespace
的命名空间。在该命名空间中,定义了一个接口 Person
和一个函数 greet
。通过在命名空间外部使用 export
关键字,我们使得这些定义可以在命名空间外部访问到。
在使用命名空间中的成员时,我们需要使用命名空间的完全限定名称。例如,使用 MyNamespace.Person
来引用接口 Person
,使用 MyNamespace.greet
来调用函数 greet
。
命名空间可以帮助我们组织代码,封装相关的功能,并提供更好的模块化和可重用性。然而,从 TypeScript 2.0 开始,推荐使用 module
关键字和模块化系统来替代命名空间,因为模块化系统更加灵活和强大,能够更好地满足复杂的项目需求。
21.Umi中 dva 的工作流程是什么?
-
创建 Model:首先,在
src/models
目录下创建一个新的 model 文件。一个 model 对应着应用中的一个特定领域或功能模块。例如,你可以创建一个user.js
的 model 文件来处理用户相关的状态和逻辑。 -
定义 State:在 model 文件中,你需要定义该 model 的初始状态(initial state)。初始状态是一个普通的 JavaScript 对象,其中包含了该 model 需要管理的数据。
-
定义 Reducer:Reducer 是一个纯函数,负责处理 state 的变化。你可以在 model 文件中定义多个 reducer 函数,每个 reducer 函数接收当前的 state 和一个 action 对象,并返回新的 state。通过这种方式,你可以根据不同的 action 来更新 state。
-
定义 Effect:Effect 是用于处理异步逻辑的函数。你可以在 model 文件中定义多个 effect 函数,用来处理网络请求、与后端交互等异步操作。Effect 函数内部使用 Generator 函数来实现异步流程控制。
-
订阅数据:在 model 文件中,你可以定义一个 subscriptions 对象,用于订阅数据的变化。subscriptions 对象中可以定义多个函数,在每个函数中可以监听具体的 action,并在相应的 action 被触发时执行特定的逻辑,如刷新页面、重新获取数据等。
-
在组件中调用 Model:在需要使用该 model 数据的组件中,你可以使用
connect
方法将该 model 的 state 以及 dispatch 函数连接到组件中。然后通过 props 来访问和更新该 model 的状态,并触发相应的 action。 -
触发 Action:在组件中触发 action,可以通过调用该 model 中定义的 action 创建函数来实现。action 创建函数是一个纯函数,返回一个描述动作的 action 对象,通常包含一个
type
字段来指明具体的动作类型,以及一些 payload 数据。 -
Reducer 处理 Action:当一个 action 被触发后,它会被发送到该 model 中对应的 reducer 函数处理。reducer 函数根据 action 的类型,做相应的处理并返回新的 state。这样,状态的变化就通过 reducer 函数来管理了。
-
Effect 处理 Action:如果一个 action 需要进行异步操作,它会被发送到对应的 effect 函数处理。effect 函数利用 Generator 函数(通常使用
yield call
或yield put
)来执行异步操作,并通过触发其他 action 来改变状态。
22.Context 状态树的执行流程?
-
创建 Context:首先,通过调用
React.createContext()
方法来创建一个 Context 对象。这个 Context 对象包含了两个组件:Provider 和 Consumer。 -
创建 Provider:Provider 组件是 Context 的数据提供者,它将状态数据和相关的方法封装在 value 属性中,并将其传递给后代组件。可以在组件树的某个地方将 Provider 放置起来,以供其子组件访问共享的状态数据。
-
设置状态值:在 Provider 组件中,你可以设置初始状态值,并提供一些操作状态的方法(例如更新状态的方法)。这些状态值和操作方法将作为 value 传递给子组件。
-
使用 Consumer:Consumer 组件用于在 React 组件树中访问 Context 的值。在需要获取状态值的组件中,将 Consumer 组件包裹在函数组件或 class 组件的 JSX 中,并在 Consumer 内部使用函数作为子元素(render props)接收 Context 的 value 值。
-
获取状态值:在 Consumer 组件的子元素函数中,可以通过形参(通常命名为 value)获取传递的 Context 值,即共享的状态数据和操作方法。子组件可以使用这些状态值进行渲染或执行其他操作。
-
更新状态:如果要更新状态,可以在 Provider 组件中定义方法,并将这些方法传递给子组件。子组件可以通过调用这些方法来更新 Context 中的状态值,以便在整个组件树中同步更新相关组件。
-
组件订阅:如果状态发生变化,Provider 组件会重新渲染,并通过 Context 重新传递新的状态值给 Consumer 组件。这样,Consumer 组件及其子组件可以通过监听 Context 的变化来获取最新的状态值,并触发相应的更新。
总结来说,Context 状态树的执行流程包括创建 Context、设置初始状态值、使用 Provider 包裹需要共享数据的组件、使用 Consumer 获取状态值并渲染组件、更新状态时重新渲染 Provider 组件,并通过 Context 传递最新的状态值给 Consumer 组件。
需要注意的是,Context 应该尽可能地被细化,保证只有那些需要共享数据的组件才订阅相应的 Context,这样可以避免不必要的组件更新,提高性能。