JS今日份练习题

今日份练习题

image-20250308010244500

1、异步执行顺序

1
2
3
4
console.log(1);
setTimeout(() => { console.log(2); }, 0);
Promise.resolve().then(() => { console.log(3); });
console.log(4);

? 什么是setTimeout

Web API,window的方法,设置一个定时器,定时器到期,就会执行一个函数或者指定的代码片段。

setTimeout(function, delay, arg1, arg2, …);

如果省略delay或者delay为0,意味着立即执行(被放入事件队列,在下一个事件循环执行)

(实际延长可能比预期长,因为同步代码优先,事件循环处理事件队列)

? 宏任务与微任务

代码执行顺序:同步代码 - 微任务队列 - 宏任务队列

微任务:Promise的回调、queueMicrotask()专门把回调放入微任务队列、MutationObserver监听DOM变化

宏任务:setTimeout、setInterval、I/O 操作、script、requestAnimationFrame、用户交互事件

解答

答案是1 4 3 2。

同步执行阶段

  • 执行 console.log(1),输出 1
  • 执行 setTimeout,将回调函数放入宏任务队列(虽然延时为 0,但仍属于宏任务)。
  • 执行 Promise.resolve().then(...),将回调函数放入微任务队列。
  • 执行 console.log(4),输出 4

微任务执行阶段

  • 同步代码执行完毕后,立即执行微任务队列中的任务,输出 3

宏任务执行阶段

  • 最后执行宏任务队列中的 setTimeout 回调,输出 2

2、变量提升

1
2
3
4
5
6
7
var a = 10;
function foo() {
console.log(a);
var a = 20;
console.log(a);
}
foo();

? 变量提升 hoisting

var 变量声明会被提升到函数作用域 / 全局作用域的顶部。

变量声明会提升,赋值不会被提升,默认为undefined。

? 原理

当 JavaScript 引擎解析代码时,它会先扫描代码中的变量声明和函数声明,并将它们提升到其作用域的顶部。

1、变量声明的提升:

  • var : 变量声明会被提升到作用域的顶部(函数作用域/全局作用域),被初始化为undefined。赋值操作保留在原位置。
  • let/const:变量声明会被提升到作用域的顶部(块级作用域),不会被初始化,处于暂时性死区,访问会报错。

2、函数声明的提升:

  • 函数声明,完全提升,函数体也会被提升到作用域的顶部。可以在声明之前调用它。

    1
    2
    3
    4
    console.log(myFunc()); // 输出 "Hello, World!",因为函数声明被完全提升
    function myFunc() {
    return "Hello, World!";
    }
  • 函数表达式,只有变量声明会被提升,函数表达式的赋值操作不会提升。

    1
    2
    3
    4
    5
    console.log(myFunc()); // 报错:TypeError: myFunc is not a function
    console.log(myFunc); // undefined
    var myFunc = function() {
    return "Hello, World!";
    };

? JS引擎是什么

1、解析代码:把JS代码解析为抽象语法树(AST)

2、编译代码:AST转换为可执行的字节码或者机器码

3、执行代码:运行编译后的代码

4、管理内存、垃圾回收

5、优化代码(JIT技术)

6、提供运行时环境(支持JS内置对象、API和事件循环机制)

常见的有V8(Chrome 浏览器、Node.js)

现代 JavaScript 引擎通过多种技术优化性能:

  1. 即时编译(JIT):动态编译代码,优化热点代码。
  2. 内联缓存:缓存函数调用的结果,减少重复计算。
  3. 增量垃圾回收:避免长时间的垃圾回收暂停,提高响应速度。
  4. 代码缓存:缓存编译后的代码,减少重复编译。

解答

答案是undefined 20

3、闭包与作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createIncrement() {
let count = 0;
function increment() {
count++;
}
let message = `Count is ${count}`;
function log() {
console.log(message);
}
return [increment, log];
}
const [increment, log] = createIncrement();
increment();
increment();
log();

? 执行要点

1、createIncrement函数执行:message是字符串模版,定义时”Count is 0”

2、外部调用:count经过两次调用,值变为2

3、log函数执行:输出定义好的message(message在createIncrement函数执行时就已经确定)

? 闭包的作用

increment、log函数都访问了createIncrement函数作用域的变量

这些变量被闭包捕获,即使createIncrement函数已经执行完毕,但是仍然可以访问这些变量

总结:闭包允许函数访问其创建时所在的作用域链中的变量。

解答

答案是0

4、for循环与var的作用域

1
2
3
for (var i = 0; i < 3; i++) {
setTimeout(function() { console.log(i); }, 0);
}

?怎么会

闭包捕获的是变量的引用,因此回调函数访问的是变量的当前值。

所有通过定时器注册的回调函数在执行时,访问的是同一个作用域中的i,而i在循环结束后已经是3

? 怎么解决

1、使用let定义变量,每次循环创建新的i属于单独的块级作用域

2、使用立即执行函数IIFE,创建独立的作用域,把i传给定时器

解答

答案是3 3 3

5、反转字符串

1
2
请编写一个函数 reverseString(str),要求返回字符串 str 的反转结果。
示例: 输入 "hello",输出 "olleh"

解答

1、字符串没有reverse方法,数组有reverse方法。

2、因此,考虑使用split方法将字符串转化为数组。

3、再将数组进行反转,使用join方法连接为字符串。

1
2
3
4
function reverseString(str){
const strToArr = str.split('').reverse().join('')
return strToArr
}

6、数组去重

1
2
请编写一个函数 uniqueArray(arr),移除数组 arr 中的重复元素,并返回去重后的新数组。
示例: 输入 [1, 2, 2, 3, 1],输出 [1, 2, 3]

解答

1、去重,首先考虑使用Set数据结构,因为Set存储唯一的值。

Set的特性:唯一性、但具有无序性,可动态添加元素,有以下方法:add/delete/clear/has/size;存储任意类型

2、把Set类型转换为数组:

  • 使用扩展运算符(可迭代对象)
  • 使用Array.from方法(类数组对象、可迭代对象)
  • 使用for of循环手动遍历Set并把值添加到数组
1
2
3
4
function uniqueArray(arr){
const myset = new Set(arr)
return [...myset]
}

7、深拷贝

1
2
3
请编写一个函数 deepClone(obj),对传入的对象 obj 进行深拷贝,要求能够正确拷贝嵌套的对象、数组以及其他类型(如 DateRegExp 等)。

提示: 注意循环引用和特殊对象的处理。

? 深拷贝和浅拷贝是什么意思

浅拷贝:只复制对象的第一层属性,不递归复制嵌套的对象。如果属性值是引用类型,新对象和原对象会指向同一个内存地址。

1、扩展运算符 …

2、Object.assign()

3、数组的 slice()concat()

深拷贝:递归复制所有层级的属性,确保新对象和原对象完全独立,修改新对象不会影响原对象。

1、JSON.parse(JSON.stringify()) ,但是不支持函数、特殊对象、undefined

2、手写递归

3、库loadsh

? 什么是递归

允许函数调用自身,将复杂问题分解为更小的子问题来解决

解答

终止条件:如果是null或者不是对象类型都直接返回

考虑情况:

特殊对象问题:Date或者RegExp对象,需要实例化后返回

循环引用问题:使用WeakMap记录

接收初始化问题:要考虑是数组还是普通对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function deepClone(obj, hash = new WeakMap()) {
// 基本类型或函数直接返回
if (obj === null || typeof obj !== 'object') return obj;
// 处理 Date 对象
if (obj instanceof Date) return new Date(obj);
// 处理 RegExp 对象
if (obj instanceof RegExp) return new RegExp(obj);
// 如果对象已经被拷贝过,直接返回,处理循环引用
if (hash.has(obj)) return hash.get(obj);

// 根据 obj 的类型初始化拷贝结果:数组或普通对象
const cloneObj = Array.isArray(obj) ? [] : {};

// 记录到 hash 中,防止循环引用
hash.set(obj, cloneObj);

// 遍历 obj 的所有可枚举属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}

8、防抖函数

1
2
3
请编写一个防抖函数 debounce(func, delay),要求返回一个新的函数。
这个新函数在连续触发时,只有在最后一次触发后经过 delay 毫秒没有继续触发时才会执行 func。
请写出代码实现,并简要说明其原理和使用场景。

? 思路是

1、防抖函数的目标是:在短时间内频繁触发目标函数,但只在最后一次触发事件等待延迟时间delay后执行。

2、防抖函数的作用是:把目标函数做防抖处理,返回一个新函数,内部封装了防抖逻辑,用户只需要调用新函数并且正常传参。

3、防抖函数内部需要先定义一个变量timer,存储定时器的引用。返回新函数与timer构成了闭包,使得timer被共享和更新。

4、新函数需要接收参数,并且在每次触发事件前,清除定时器,使得频繁的触发情况下不会生效。设置指定延迟的定时器,显示指定this调用目标函数,保持上下文的正确绑定。

解答

1
2
3
4
5
6
7
8
9
function debounce(func, delay){
let timer; //默认为undefined,因为被清除的时候也会变成undefined
return function(...args){
if (timer) clearTimeout(timer);
timer = setTimeout(()=>{
func.apply(this, args);
}, delay);
}
}