每日一题 - 202407
7-19
VUEX 和单纯的全局对象有什么区别?
Vuex 状态存储是响应式的,当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会更新。
程序不能直接改变 store 中的状态,而是进行显式的提交(commit),这样就可以跟踪每一个状态的变化。
7-18
Vue3.0 为什么要用 proxy?
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象,通过 getter 和 setter 实现。 所以检测不到对象属性的添加和删除,而且它需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题
Proxy
直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的,它的很多拦截方法是 Object.defineProperty
不具备的。
在 Vue3.0 中,使用了 Proxy 取代了 defineProperty,主要是因为 Proxy 的强大以及 defineProperty 自身的缺陷。
7-17
多线程的优势
降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 提高响应速度: 当任务到达时,可以不需要等到线程创建就能立即执行。 提高线程的可管理性: 统一管理线程,避免系统创建大量同类线程而导致消耗完内存。
7-16
如何优化接口性能?
- 优化数据库索引
- 优化 SQL 语句
- 避免在事务中进行大量操作
- 异步处理,剥离主逻辑和副逻辑
- 多线程场景下降低锁粒度
- 加缓存,Redis 等
- 分库分表
- 避免循环查询数据库
7-15
JAVA 实例化的顺序
- 静态属性,静态代码块
- 普通属性,普通代码块
- 构造方法
7-14
String s = new String("mufeng"); 创建了几个对象?
在执行 String s = new String("mufeng");
时,创建了两个对象。
- 字面量对象: Java 字符串字面量 "mufeng" 被存储在字符串池中,如果该字面量之前没有在字符串池中出现过,会在字符串池中创建一个新的字符串对象。
- 堆中的字符串对象:
new String("mufeng")
会在堆中创建一个新的字符串对象,其内容为 "mufeng",即使字符串池中已经有 "mufeng" 对象,也会在堆中再创建一个新的对象。
7-13
什么是字符串常量池?
字符串常量池(String Constant Pool)是 Java 中一种特殊的内存区域,用于存储字符串字面量。这种机制的主要目的是为了节省内存空间并提高性能。它位于 Java 堆内存(Heap Memory)中的一个特定区域。
当使用双引号创建字符串(如 "mufeng")时,编译器会检查字符串常量池,如果该字符串已经存在,则直接引用池中的对象;如果不存在,则在池中创建一个新的字符串对象。
字符串常量池可以提高内存效率和性能:
- 通过避免重复创建相同内容的字符串,可以显著减少内存开销。
- 字符串比较操作(使用 ==)在常量池中的字符串上会更快,因为它们比较的是内存地址而不是内容。
public class StringPoolExample {
public static void main(String[] args) {
String str1 = "mufeng"; // 字符串字面量,存储在常量池中
String str2 = "mufeng"; // 引用常量池中的同一个对象
String str3 = new String("mufeng"); // 在堆中创建新的字符串对象
String str4 = str3.intern(); // 将 str3 引用的字符串添加到常量池中,或获取常量池中的相同内容的字符串引用
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str1 == str4); // true
}
}
在这个示例中,str1
和 str2
都引用常量池中的同一个字符串对象,而 str3
引用的是堆中的新对象。通过调用 str3.intern()
,str4
引用了常量池中的字符串对象。
7-12
什么是 StringJoiner ?
StringJoiner
是 JAVA8 新增的 API,基于 StringBuilder 实现,用于实现字符串之间通过分隔符拼接的场景。
常用的 Collectors.joining(",")
,底层就是通过 StringJoiner
实现的。
String[] strings = {"白鹿原", "平凡的世界", "穆斯林的葬礼", "呼兰河传"};
StringJoiner stringJoiner = new StringJoiner("》,《", "《", "》");
for (String string : strings) {
stringJoiner.add(string);
}
stringJoiner:
《白鹿原》,《平凡的世界》,《穆斯林的葬礼》,《呼兰河传》
7-11
为何 JDK9 要将 String 的底层实现由 char[] 改成 byte[]?
主要是为了节约内存
在大部分 Java 程序的堆内存中,String 占用的空间最大,并且绝大多数 String 只有 Latin-1 字符,这些 Latin-1 字符只需要1个字节就够了。
而在 JDK9 之前,JVM 因为 String 使用 char 数组存储,每个 char 占2个字节,所以即使字符串只需要1字节,它也要按照2字节进行分配,浪费了一半的内存空间。
到了 JDK9 之后,对于每个字符串,会先判断它是不是只有 Latin-1 字符,如果是,就按照1字节的规格进行分配内存,如果不是,就按照2字节的规格进行分配, 这样便提高了内存使用率,同时GC次数也会减少,提升效率。
不过 Latin-1 编码集支持的字符有限,比如不支持中文字符,因此对于中文字符串,用的是 UTF16 编码(两个字节),所以用 byte[]
和 char[]
实现没什么区别。
7-10
什么是自动拆装箱?
由于 JAVA 面向对象的特性,很多地方都需要引用数据类型而非基础数据类型,比如在集合类中,要求放入元素为 对象,所以无法放入基础数据类型 int 等。
为了让基础数据类型也拥有引用数据类型的特性,就出现了包装类型,将引用数据类型用对象包装,让其拥有对象的特性,为其添加属性和方法,丰富操作。
原始类型和包装类型对应关系:
原始类型 | 包装类型 |
---|---|
boolean | Boolean |
byte | Byte |
char | Character |
float | Float |
int | Integer |
long | Long |
short | Short |
double | Double |
当基础类型与它们的包装类有如下几种情况时,编译器会自动帮我们进行装箱或拆箱:
- 赋值操作(装箱或拆箱)
- 进行加减乘除混合运算 (拆箱)
- 进行>,<,==比较运算(拆箱)
- 调用equals进行比较(装箱)
- ArrayList、HashMap 等集合类添加基础类型数据时(装箱)
7-9
怎么实现图片懒加载?
懒加载是一种网页性能优化的方式,它能极大的提升用户体验。就比如说图片,图片一直是影响网页性能的主要元凶,现在一张图片超过几兆已经是很经常的事了。如果每次进入页面就请求所有的图片资源,那么可能等图片加载出来用户也早就走了。所以,我们需要懒加载,进入页面的时候,只请求可视区域的图片资源。
1、html实现
最简单的实现方式是给 img
标签加上 loading="lazy"
,比如
<img src="./example.jpg" loading="lazy">
2、JS实现原理
我们通过js监听页面的滚动
也能实现。
使用js实现的原理主要是判断当前图片是否到了可视区域:
- 拿到所有的图片 dom 。
- 遍历每个图片判断当前图片是否到了可视区范围内。
- 如果到了就设置图片的 src 属性。
- 绑定 window 的 scroll 事件,对其进行事件监听。
在页面初始化的时候,图片的src实际上是放在data-src
属性上的,当元素处于可视范围内的时候,就把data-src赋值给src属性,完成图片加载。
// 在一开始加载的时候
<img data-src="http://xx.com/xx.png" src="" />
// 在进入可视范围内时
<img data-src="http://xx.com/xx.png" src="http://xx.com/xx.png" />
// 在一开始加载的时候
<div
data-src="http://xx.com/xx.png"
style="background-image: none;background-size: cover;"
></div>
// 在进入可视范围内时
<div
data-src="http://xx.com/xx.png"
style="background-image: url(http://xx.com/xx.png);background-size: cover;"
></div>
示例
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Lazyload</title>
<style>
img {
display: block;
margin-bottom: 50px;
height: 200px;
width: 400px;
}
</style>
</head>
<body>
<img src="./img/default.png" data-src="./img/1.jpg" />
<img src="./img/default.png" data-src="./img/2.jpg" />
<img src="./img/default.png" data-src="./img/3.jpg" />
<img src="./img/default.png" data-src="./img/4.jpg" />
<img src="./img/default.png" data-src="./img/5.jpg" />
<img src="./img/default.png" data-src="./img/6.jpg" />
<img src="./img/default.png" data-src="./img/7.jpg" />
<img src="./img/default.png" data-src="./img/8.jpg" />
<img src="./img/default.png" data-src="./img/9.jpg" />
<img src="./img/default.png" data-src="./img/10.jpg" />
</body>
</html>
先获取所有图片的 dom,通过 window.innerHeight || document.documentElement.clientHeight|| document.body.clientHeight
获取可视区高度,再使用 element.getBoundingClientRect()
API 直接得到元素相对浏览的 top 值, 遍历每个图片判断当前图片是否到了可视区范围内。最后给 window 绑定 onscroll 事件。
function lazyload() {
let viewHeight = window.innerHeight || document.documentElement.clientHeight|| document.body.clientHeight //获取可视区高度,兼容不同浏览器
let imgs = document.querySelectorAll('img[data-src]')
imgs.forEach((item, index) => {
if (item.dataset.src === '') return
// 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
let rect = item.getBoundingClientRect()
if (rect.bottom >= 0 && rect.top < viewHeight) {
item.src = item.dataset.src
item.removeAttribute('data-src')
}
})
}
window.addEventListener('scroll', lazyload)
这样存在较大的性能问题,因为 scroll 事件会在很短的时间内触发很多次,严重影响页面性能,为了提高网页性能,我们需要一个节流函数来控制函数的多次触发,在一段时间内(如 200ms)只执行一次回调。
优化(节流)
function throttle(fn, delay) {
let timer
let prevTime
return function (...args) {
const currTime = Date.now()
const context = this
if (!prevTime) prevTime = currTime
clearTimeout(timer)
if (currTime - prevTime > delay) {
prevTime = currTime
fn.apply(context, args)
clearTimeout(timer)
return
}
timer = setTimeout(function () {
prevTime = Date.now()
timer = null
fn.apply(context, args)
}, delay)
}
}
window.addEventListener('scroll', throttle(lazyload, 200))
7-8
什么是事件代理,以及它的应用场景有哪些?
一、事件代理是什么
事件代理,俗地来讲,就是把一个元素响应事件(click
、keydown
......)的函数委托到另一个元素
前面讲到,事件流的都会经过三个阶段: 捕获阶段 -> 目标阶段 -> 冒泡阶段,而事件委托就是在冒泡阶段完成
事件委托,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,而不是目标元素
当事件响应到目标元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数
二、应用场景
如果我们有一个列表,列表之中有大量的列表项,我们需要在点击列表项的时候响应一个事件
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的
// 获取目标元素
const lis = document.getElementsByTagName("li")
// 循环遍历绑定事件
for (let i = 0; i < lis.length; i++) {
lis[i].onclick = function(e){
console.log(e.target.innerHTML)
}
}
这时候就可以事件委托,把点击事件绑定在父级元素ul
上面,然后执行事件的时候再去匹配目标元素
// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
// 兼容性处理
var event = e || window.event;
var target = event.target || event.srcElement;
// 判断是否匹配目标元素
if (target.nodeName.toLocaleLowerCase === 'li') {
console.log('the content is: ', target.innerHTML);
}
});
还有一种场景是上述列表项并不多,我们给每个列表项都绑定了事件
但是如果用户能够随时动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件
如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的
三、总结
适合事件委托的事件有:click
,mousedown
,mouseup
,keydown
,keyup
,keypress
从上面应用场景中,我们就可以看到使用事件委托存在两大优点:
- 减少整个页面所需的内存,提升整体性能
- 动态绑定,减少重复工作
但是使用事件委托也是存在局限性:
focus
、blur
这些事件没有事件冒泡机制,所以无法进行委托绑定事件mousemove
、mouseout
这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的
如果把所有事件都用事件代理,可能会出现事件误判,即本不该被触发的事件被绑定上了事件
7-7
JavaScript 中内存泄漏有哪几种情况?
一、内存泄漏是什么
内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存
并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费
程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存
对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃
二、垃圾回收机制
Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存
原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存
通常情况下有两种实现方式:
- 标记清除
- 引用计数
标记清除
JavaScript
最常用的垃圾收回机制
当变量进入执行环境是,就标记这个变量为“进入环境“。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为“离开环境“
垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉
在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了
随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存
var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
a++
var c = a + b
return c
}
引用计数
语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0
,就表示这个值不再用到了,因此可以将这块内存释放
如果一个值不再需要了,引用数却不为0
,垃圾回收机制无法释放这块内存,从而导致内存泄漏
三、常见内存泄漏情况
意外的全局变量
function foo(arg) {
bar = "this is a hidden global variable";
}
另一种意外的全局变量可能由 this
创建:
function foo() {
this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();
上述使用严格模式,可以避免意外的全局变量
定时器也常会造成内存泄露
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource);
}
}, 1000);
如果id
为Node的元素从DOM
中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource
的引用,定时器外面的someResource
也不会被释放
包括我们之前所说的闭包,维持函数内局部变量,使其得不到释放
function bindEvent() {
var obj = document.createElement('XXX');
var unused = function () {
console.log(obj, '闭包内引用obj obj不会被释放');
};
obj = null; // 解决方法
}
没有清理对DOM
元素的引用同样造成内存泄露
const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom删除了
console.log(refA, 'refA'); // 但是还存在引用能console出整个div 没有被回收
refA = null;
console.log(refA, 'refA'); // 解除引用
包括使用事件监听addEventListener
监听的时候,在不监听的情况下使用removeEventListener
取消对事件监听
7-6
RESTful 接口规范是什么?
RESTful 接口规范
是一种设计 Web 服务接口的风格和规范,遵循 REST(Representational State Transfer)架构。它的设计原则包括以下几点:
- 资源(Resources):将系统中的所有事物视为资源,每个资源都有一个唯一的标识符(通常是 URL),用于对其进行操作。
- 统一接口(Uniform Interface):接口设计应该简单一致,包括以下几个方面:
- 使用标准的 HTTP 方法(GET、POST、PUT、DELETE 等)来对资源进行操作。
- 使用标准的 HTTP 状态码(如 200、404、500)来表示请求结果。
- 使用资源的 URL 来唯一标识资源。
- 使用适当的 MIME 类型(如 JSON、XML)来传输数据。
- 状态无关(Stateless):每个请求都应该包含足够的信息,服务器不需要保存客户端的状态。这样可以使系统更加简单、可伸缩性更好。
- 客户端 - 服务器分离(Client-Server Separation):客户端和服务器之间的交互应该通过标准化的接口进行,使得客户端和服务器可以独立地进行演化。
- 可缓存性(Cacheability):对于经常不变的数据,应该使用合适的缓存机制,提高系统的性能和可伸缩性。
- 按需代码(Code on Demand)(可选):服务器可以向客户端传输可执行代码,以提供更丰富的功能。
遵循 RESTful 接口规范能够使得系统具有良好的可维护性、可伸缩性和性能,并且更容易与其他系统进行集成。
7-5
如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功?
如果直接运行:
javascriptconst obj={a:'1',b:'2'} const [a,b]=obj
运行结果: 报错!
Uncaught TypeError: obj is not iterable at .....
obj这个对象是不可迭代的
1、可迭代协议
可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 for..of
结构中,哪些值可以被遍历到。一些内置类型同时是内置的可迭代对象,并且有默认的迭代行为,比如 Array
或者 Map
,而其他内置类型则不是(比如 Object
)。
要成为可迭代对象,该对象必须实现 @@iterator
方法,这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator
的属性,可通过常量 Symbol.iterator
访问该属性:
[Symbol.iterator]
一个无参数的函数,其返回值为一个符合迭代器协议的对象。
当一个对象需要被迭代的时候(比如被置入一个 for...of
循环时),首先,会不带参数调用它的 @@iterator
方法,然后使用此方法返回的迭代器获得要迭代的值。
简而言之就是让obj成为一个可迭代的对象需要它实现
@@iterator
方法,具体表现为对象身上要有一个名为[Symbol.iterator]
的方法。而数组和Map则是一开始就有这个方法,所以它们是可迭代的。
数组解构的本质
const array = [1,2,3]
var [a,b,c] = array
// 本质上是
const iterator = array[Symbol.iterator]()
var a = iterator.next().value
var b = iterator.next().value
var c = iterator.next().value
2、解决方案
const obj = {
a: '1',
b: '2',
[Symbol.iterator]() {
let index = 0
const keys = Object.keys(this)
return {
next() {
if (index < keys.length) {
return {
done: false,
value: obj[keys[index++]]
}
}
return {done:true,value:undefined}
}
}
}
}
const [a, b] = obj
7-4
Proxy 能够监听到对象中的对象的引用吗?
Proxy
可以监听到对象中的对象的引用。
当使用Proxy
包装一个对象时,可以为该对象的任何属性创建一个拦截器,包括属性值为对象的情况。
const obj = {
nestedObj: { foo: 'bar' }
}
const handler = {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver)
if (typeof value === 'object' && value !== null) {
return new Proxy(value, handler)
}
console.log('get', prop, target[prop])
return value
},
set(target, property, value) {
target[property] = value
console.log(`Setting property '${property}' to '${value}'`)
return true
}
}
const proxyObj = new Proxy(obj, handler)
proxyObj.nestedObj.foo = 'baz' // 输出: Setting property 'foo' to 'baz'
7-3
MessageChannel 是什么,有什么使用场景?
MessageChannel
是一个 JavaScript API,用于在两个独立的执行环境(如 Web Workers 或者不同的 browsing contexts)之间建立双向通信的通道。
MessageChannel
提供了两个通信端点(port1
和 port2
),可以在两个不同的执行环境之间传递消息,并通过事件监听的方式来处理这些消息。
使用场景包括但不限于:
- Web Workers 通信:在 Web 开发中,
MessageChannel
通常用于在主线程和 Web Worker 之间建立通信通道,以便主线程与 Worker 之间传递消息和数据。 - 不同浏览上下文(browsing context)之间的通信:在现代浏览器中,多个标签页、iframe 或者其他类型的 browsing context 可以通过
MessageChannel
实现通信。 - SharedWorker 通信:
MessageChannel
可以用于在主线程和 Shared Worker 之间建立通信通道。 - 服务端和客户端之间的通信:
MessageChannel
可以用于客户端(如浏览器)与服务端(如 WebSocket 服务器)之间的通信,特别是在与 WebSocket 或其他类似技术结合使用时。 - 异步任务处理:在某些场景中,使用
MessageChannel
可以更方便地处理异步任务,因为它提供了独立于主线程的通信通道。
// 创建 MessageChannel
const channel = new MessageChannel();
const port1 = channel.port1;
const port2 = channel.port2;
// 在主线程中
const worker = new Worker('worker.js');
worker.postMessage({ port: port2 }, [port2]);
port1.onmessage = function(event) {
console.log('Received message from worker:', event.data);
};
// 发送消息给 worker
port1.postMessage('Hello, Worker!');
7-2
mouseEnter 和 mouseOver 有什么区别?
mouseenter
和 mouseover
是两个用于处理鼠标进入元素时的事件,但它们在一些关键点上有所不同:
- 事件冒泡:
mouseenter
:这个事件在鼠标指针首次进入特定元素(或其子元素)时触发。当鼠标进入元素时,会触发该元素的mouseenter
事件,但不会在元素的子元素上冒泡。因此,该事件通常用于检测鼠标首次进入元素时的动作。mouseover
:这个事件在鼠标指针移动到某个元素上时触发,不论它是直接在这个元素上触发还是在其子元素上触发。当鼠标进入一个元素时,它会在该元素上触发mouseover
事件,然后冒泡到父元素。
- 事件触发范围:
mouseenter
:当鼠标进入元素自身时触发,只在目标元素上触发,不会因为鼠标移动到其子元素上而再次触发。mouseover
:不仅在目标元素上触发,也在其子元素上触发。所以,如果鼠标从一个子元素移动到另一个子元素,这些元素的父元素会触发多个mouseover
事件。
- 事件对象的属性:
mouseenter
:事件对象通常会有relatedTarget
属性,它指向鼠标移动前的那个元素。如果relatedTarget
指向目标元素或为null
,那么事件就不会触发。mouseover
:事件对象也会有relatedTarget
属性,通常指向从中离开的那个元素。
使用场景
mouseenter
更适合用来检测鼠标首次进入某个元素时的行为。mouseover
更适合用来检测鼠标在元素或其子元素之间移动时的行为,因为它冒泡。
在实际使用时,如果你只想在鼠标首次进入元素时触发某些行为(比如显示一个提示),可以使用
mouseenter
;如果你希望在鼠标移动到某个元素或其子元素上时都触发某些行为(比如动态改变样式),可以使用mouseover
。
7-1
Js的基础类型,typeof和instanceof的区别?
基础类型有:boolean、string、number、bigint、undefined、symbol、null
。
typeof
能识别所有的值类型,识别函数,能区分是否是引用类型。
instanceof
用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
const a = "str";
console.log("typeof a :>> ", typeof a); // typeof a :>> string
const b = 999;
console.log("typeof b :>> ", typeof b); // typeof b :>> number
const c = BigInt(9007199254740991);
console.log("typeof c :>> ", typeof c); // typeof c :>> bigint
const d = false;
console.log("typeof d :>> ", typeof d); // typeof d :>> boolean
const e = undefined;
console.log("typeof e :>> ", typeof e); // typeof e :>> undefined
const f = Symbol("f");
console.log("typeof f :>> ", typeof f); // typeof f :>> symbol
const g = null;
console.log("typeof g :>> ", typeof g); // typeof g :>> object
const h = () => {};
console.log("typeof h :>> ", typeof h); // typeof h :>> function
const i = [];
console.log("typeof i :>> ", typeof i); // typeof i :>> object