您的位置:首页 > 科技 > IT业 > 传媒公司宣传片_设计软件培训班学费一般多少_今天的新闻头条最新消息_怎样能在百度上搜索到自己的店铺

传媒公司宣传片_设计软件培训班学费一般多少_今天的新闻头条最新消息_怎样能在百度上搜索到自己的店铺

2025/4/2 16:41:21 来源:https://blog.csdn.net/qq_34645412/article/details/146116728  浏览:    关键词:传媒公司宣传片_设计软件培训班学费一般多少_今天的新闻头条最新消息_怎样能在百度上搜索到自己的店铺
传媒公司宣传片_设计软件培训班学费一般多少_今天的新闻头条最新消息_怎样能在百度上搜索到自己的店铺

1. JS 由哪三部分组成?

JavaScript 由以下三部分组成:

  1. ECMAScript(ES):JavaScript 的核心语法,如变量、作用域、数据类型、函数、对象等。
  2. DOM(文档对象模型):用于操作 HTML 和 XML 文档的 API,可以动态修改网页内容、结构和样式。
  3. BOM(浏览器对象模型):用于操作浏览器窗口和页面,例如 windownavigatorlocationhistoryscreen 等对象。

2. JS 有哪些内置对象?

JavaScript 具有以下内置对象:

  1. 基本对象ObjectFunctionBooleanSymbol
  2. 数值对象NumberBigIntMath
  3. 字符串对象String
  4. 数组对象Array
  5. 日期对象Date
  6. 正则对象RegExp
  7. 错误对象ErrorTypeErrorSyntaxErrorReferenceError
  8. 集合对象SetMapWeakSetWeakMap
  9. 异步对象PromiseAsyncFunction

3. 操作数组的方法有哪些?

数组方法可以分为几类:

① 增加元素
  • push(value):在数组末尾添加元素,返回新长度。
  • unshift(value):在数组头部添加元素,返回新长度。
  • splice(index, 0, value):在指定位置插入元素。
② 删除元素
  • pop():删除数组最后一个元素,并返回该元素。
  • shift():删除数组第一个元素,并返回该元素。
  • splice(index, count):删除指定位置的 count 个元素。
③ 查找元素
  • indexOf(value):返回元素第一次出现的位置,找不到返回 -1
  • find(callback):返回符合条件的第一个元素,没有符合条件的返回 undefined
  • findIndex(callback):返回符合条件的元素索引,找不到返回 -1
  • includes(value):判断数组是否包含某个元素,返回 true/false
④ 其他常用方法
  • map(callback):返回一个新数组,每个元素由回调函数处理。
  • filter(callback):筛选符合条件的元素,返回新数组。
  • reduce(callback, initialValue):累加数组值,常用于计算总和、扁平化数组。
  • sort(callback):对数组进行排序(默认按 Unicode 编码排序)。
  • reverse():反转数组元素顺序。
  • concat(arr):合并数组,返回新数组。
  • slice(start, end):返回数组的部分片段,不修改原数组。

4. JS 对数据类型的检测方式有哪些?

  • typeof:适用于基本数据类型,但 null 误判为 "object"
  • instanceof:判断对象是否属于某个构造函数的实例。
  • Object.prototype.toString.call(value):返回精准数据类型,如 "[object Array]"
  • Array.isArray(value):判断是否为数组。

5. 说一下闭包,闭包有什么特点?

闭包(Closure) 是指函数可以访问其外部作用域的变量,即使外部函数已经执行结束。
特点

  1. 可以访问外部函数的变量,即使外部函数执行完毕。
  2. 变量不会被垃圾回收(可能导致内存泄露)。
  3. 适用于模块化开发,模拟私有变量。

示例

function outer() {let count = 0;return function inner() {count++;console.log(count);};
}
const counter = outer();
counter(); // 1
counter(); // 2

闭包(Closure)是指一个函数能够访问其外部作用域中的变量,即使在外部函数执行结束后,仍然可以保留对外部变量的访问权限。闭包的主要使用场景如下:


1. 数据私有化(模拟私有变量)

闭包可以创建私有变量,防止外部直接访问或修改数据。

示例:模拟私有变量

function createCounter() {let count = 0; // 作为私有变量return {increment: function() {count++;console.log(count);},decrement: function() {count--;console.log(count);},getCount: function() {return count;}};
}const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined(无法直接访问私有变量)

🔹 应用场景

  • 需要封装变量,避免外部随意修改(如计数器、缓存管理、权限控制)。

2. 事件监听器 & 回调

闭包常用于事件监听器或回调函数,使得事件处理函数能够访问外部作用域中的变量。

示例:按钮点击计数

function setupButton() {let count = 0;document.getElementById("myButton").addEventListener("click", function() {count++;console.log(`Button clicked ${count} times`);});
}
setupButton();

🔹 应用场景

  • addEventListener 回调中保留数据(如点击次数、鼠标移动距离等)。

3. 函数柯里化(参数预处理)

柯里化(Currying)是指将一个接收多个参数的函数,转换为多个接收单一参数的函数。

示例:实现加法柯里化

function add(x) {return function(y) {return x + y;};
}const addFive = add(5);
console.log(addFive(3)); // 8
console.log(addFive(10)); // 15

🔹 应用场景

  • 预设部分参数,提高代码复用性(如 bind 预设 this)。
  • 在 Redux、Lodash 等库中广泛应用。

4. 延迟执行(定时器)

闭包可以用于在定时器或异步操作中保留执行上下文。

示例:定时器

function delayedMessage(msg, delay) {setTimeout(function() {console.log(msg);}, delay);
}delayedMessage("Hello, Closure!", 2000);

🔹 应用场景

  • setTimeout / setInterval 相关的任务管理(如轮询、延迟加载)。

5. 模拟块级作用域(ES5 以前)

在 ES6 之前,JavaScript 没有 letconst 关键字,使用闭包可以创建局部作用域,避免变量污染。

示例:IIFE(立即执行函数表达式)

(function() {var secret = "I am private";console.log(secret);
})();
console.log(typeof secret); // undefined(无法访问)

🔹 应用场景

  • 在 ES5 及以前,使用 IIFE 防止变量污染全局作用域。
  • 现代 JavaScript 用 letconst 取代该用法。

6. 记忆化(缓存计算结果,提高性能)

闭包可用于缓存计算结果,避免重复计算,提升性能。

示例:缓存计算结果

function memoize(fn) {let cache = {};return function(arg) {if (cache[arg]) {console.log("Fetching from cache:", arg);return cache[arg];}console.log("Calculating result for:", arg);cache[arg] = fn(arg);return cache[arg];};
}const square = memoize(x => x * x);
console.log(square(4)); // 计算并存入缓存
console.log(square(4)); // 直接从缓存获取
console.log(square(5)); // 计算并存入缓存

🔹 应用场景

  • 计算密集型任务的优化(如斐波那契数列、递归)。
  • 缓存 API 请求结果,减少重复网络请求。

7. 迭代器 & 生成唯一 ID

闭包可以用来创建迭代器或唯一 ID 生成器。

示例:唯一 ID 生成器

function createIdGenerator() {let id = 0;return function() {return id++;};
}const getId = createIdGenerator();
console.log(getId()); // 0
console.log(getId()); // 1
console.log(getId()); // 2

🔹 应用场景

  • 生成唯一标识符(如任务 ID、DOM 元素 ID)。

8. 绑定 this(模拟 bind 方法)

闭包可以用于创建绑定 this 的新函数。

示例:手写 bind

function myBind(fn, context) {return function(...args) {return fn.apply(context, args);};
}const obj = { name: "Alice" };
function sayName(greeting) {console.log(greeting + ", " + this.name);
}const boundSayName = myBind(sayName, obj);
boundSayName("Hello"); // Hello, Alice

🔹 应用场景

  • 事件处理时确保 this 绑定正确。

总结

使用场景说明示例
数据私有化模拟私有变量,防止外部访问计数器、权限管理
事件监听器保留外部变量的数据统计按钮点击次数
函数柯里化预处理参数,提升复用性add(5)(10)
定时器异步任务执行setTimeout 回调
模拟块级作用域防止变量污染IIFE
缓存优化记忆化函数,减少重复计算Fibonacci、API 请求缓存
生成唯一 ID生成不重复的 ID任务队列管理
绑定 this确保回调 this 指向正确myBind

面试高频问题:

  1. 闭包的本质是什么?

    • 一个函数可以访问其外部作用域的变量,即使外部函数执行结束后,变量依然可用。
  2. 闭包有哪些常见应用场景?

    • 数据私有化、事件监听、定时器、柯里化、缓存优化、唯一 ID 生成等。
  3. 闭包会导致内存泄漏吗?如何避免?

    • 是的,闭包可能导致变量无法被垃圾回收。
    • 解决方案:手动解除引用,如 element.onclick = null 释放 DOM 事件闭包,或者减少不必要的闭包使用。

闭包是 JavaScript 重要的特性之一,熟练掌握它的应用能提升代码质量和性能!🚀

6. 前端的内存泄露怎么理解?

内存泄露 是指程序不再使用某些对象,但垃圾回收机制无法释放它们,导致内存占用增加。
常见原因:

  1. 全局变量未释放window.variable 一直存在)
  2. 未清理的定时器setInterval 没有 clearInterval
  3. 闭包未正确释放(函数执行后仍然引用外部变量)
  4. 未移除的 DOM 事件监听element.addEventListener 没有 removeEventListener

解决方案

  • 避免全局变量,使用 let/const 限制作用域。
  • setInterval 用完后及时 clearInterval()
  • 及时 removeEventListener() 解除事件绑定。
  • 手动置 null 解除对象引用。

7. 事件委托是什么?

事件委托(Event Delegation) 是将事件监听器绑定在父级元素上,利用事件冒泡机制处理子元素事件,提高性能。
示例

document.getElementById('parent').addEventListener('click', function(event) {if (event.target.tagName === 'BUTTON') {console.log('Button clicked:', event.target.innerText);}
});

这样即使新 button 动态添加到 #parent,仍然可以触发事件。


8. 基本数据类型和引用数据类型的区别?

数据类型存储位置赋值方式比较方式
基本类型栈内存拷贝值值比较
引用类型堆内存赋引用地址比较

9. 说一下原型链。

原型链(Prototype Chain) 是 JavaScript 继承机制的核心。
每个对象都有 __proto__,指向其构造函数的 prototype,形成一个链式结构。
详细参照链接:js原型与原型链

10. new 操作符具体做了什么?

  1. 创建一个新对象 obj
  2. obj.__proto__ 关联到构造函数的 prototype
  3. 执行构造函数,并绑定 this 到新对象。
  4. 如果构造函数返回对象,则返回该对象,否则返回 obj

示例:

function Person(name) {this.name = name;
}
const p = new Person("Alice");
console.log(p.name); // Alice

11. callapplybind 三者有什么区别?

callapplybind 是 JavaScript 中用于更改函数 this 指向的方法,它们的主要区别如下:

方法作用参数是否立即执行返回值
call绑定 this 并调用函数thisArg, arg1, arg2, ...调用结果
apply绑定 this 并调用函数thisArg, [arg1, arg2, ...]调用结果
bind绑定 this返回新函数thisArg, arg1, arg2, ...新函数

1. call 方法

call(thisArg, arg1, arg2, ...) 方法可以手动指定 this立即调用 该函数,参数按顺序传递。

示例 1:基本用法

function greet(greeting, punctuation) {console.log(greeting + ", " + this.name + punctuation);
}
const person = { name: "Alice" };
greet.call(person, "Hello", "!"); // Hello, Alice!
  • this 被绑定到 person,输出 "Hello, Alice!"
  • 参数 "Hello""!" 依次传入。

示例 2:继承构造函数

function Parent(name) {this.name = name;
}
function Child(name, age) {Parent.call(this, name); // 调用 Parent 构造函数this.age = age;
}
const child = new Child("Bob", 10);
console.log(child.name, child.age); // Bob 10
  • Child 通过 call 调用了 Parent,继承了 name 属性。

2. apply 方法

apply(thisArg, [arg1, arg2, ...]) 也是手动指定 this立即调用 该函数,但参数必须是 数组

示例 1:基本用法

function sum(a, b, c) {return a + b + c;
}
console.log(sum.apply(null, [1, 2, 3])); // 6
  • null 代表 this,因为 sum 本身不依赖 this
  • apply 传入参数数组 [1, 2, 3]

示例 2:获取数组中的最大/最小值

const numbers = [3, 8, 2, 7, 4];
console.log(Math.max.apply(null, numbers)); // 8
console.log(Math.min.apply(null, numbers)); // 2
  • Math.maxMath.min 只能接收多个单独的参数,而 apply 允许传递数组。

3. bind 方法

bind(thisArg, arg1, arg2, ...)callapply 的最大区别是:

  • 不会立即调用函数,而是返回一个新的函数
  • 新函数永久绑定 this,无论以后如何调用,它的 this 都不会改变。

示例 1:基本用法

function greet(greeting, punctuation) {console.log(greeting + ", " + this.name + punctuation);
}
const person = { name: "Alice" };
const boundGreet = greet.bind(person, "Hello");
boundGreet("!"); // Hello, Alice!
  • bind 生成的新函数 boundGreet 绑定了 thisperson
  • "Hello" 作为 greeting 预设进去,调用时只需提供 punctuation

示例 2:延迟执行

const obj = {name: "Charlie",sayName: function() {console.log(this.name);}
};
const say = obj.sayName.bind(obj);
setTimeout(say, 1000); // 1 秒后打印 "Charlie"
  • bind 绑定 this,即使 setTimeout 在全局环境中执行,this 仍然指向 obj

4. callapplybind 的主要区别

方法是否立即执行参数传递适用场景
call依次传递参数立即执行,适用于手动指定 this 的方法调用
apply以数组形式传递立即执行,适用于参数数量不固定的情况(如 Math.max
bind依次传递参数返回新函数,适用于事件绑定、延迟调用等

5. 适用场景总结

场景推荐方法
立即调用函数,并更改 thiscall
立即调用函数,并且参数是数组apply
需要返回一个新函数,稍后执行bind
继承构造函数call
事件绑定,避免 this 丢失bind
setTimeout 绑定 thisbind

6. 面试高频考点

  1. 为什么 bind 返回的是新函数?

    • 因为 bind 不会立即执行,而是返回一个永久绑定 this 的新函数,适用于回调和事件处理。
  2. callapply 什么时候使用?

    • 当参数已知,使用 call(传递参数更直观)。
    • 当参数是动态数组,使用 apply(例如 Math.max.apply(null, array))。
  3. 为什么 bindsetTimeout 中很重要?

    • 因为 setTimeout 内部的 this 默认指向 window,使用 bind 可以确保 this 指向原对象。
const obj = { name: "Bob" };
setTimeout(function() {console.log(this.name); // undefined
}, 1000);setTimeout(function() {console.log(this.name); // Bob
}.bind(obj), 1000);

这三个方法是 JavaScript 面试的高频考点,掌握它们的区别和应用场景能帮助你更高效地编写代码!🚀

12… JS 是如何实现继承的?**

JavaScript 主要通过 原型链ES6 class 语法 实现继承:

1. 原型链继承(Prototype Inheritance)

每个 JavaScript 对象都有一个 __proto__ 指向它的原型对象,子类可以通过 prototype 继承父类的方法和属性。

function Parent(name) {this.name = name;
}
Parent.prototype.sayHello = function() {console.log("Hello, " + this.name);
};function Child(name, age) {Parent.call(this, name); // 继承父类的属性this.age = age;
}// 继承父类方法
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;const child = new Child("Tom", 10);
child.sayHello(); // Hello, Tom
2. ES6 class 继承

ES6 引入 class 语法,使用 extends 关键字更直观地实现继承。

class Parent {constructor(name) {this.name = name;}sayHello() {console.log("Hello, " + this.name);}
}class Child extends Parent {constructor(name, age) {super(name); // 继承父类构造函数this.age = age;}
}const child = new Child("Tom", 10);
child.sayHello(); // Hello, Tom

🔹 区别

  • prototype 继承是基于原型链,而 class 继承是 ES6 语法糖,底层仍然依赖原型。
  • super 关键字可以更方便地调用父类方法。

13. JS 的设计原理是什么?

JavaScript 的设计基于以下原则:

  1. 单线程:JavaScript 主要用于 Web 页面,设计为单线程避免 UI 渲染冲突。
  2. 事件驱动:依赖 事件循环(Event Loop) 执行异步任务,如 setTimeoutPromise
  3. 动态类型:变量类型动态变化,无需声明类型。
  4. 原型继承:JS 采用 原型链 作为继承机制,而非类继承。
  5. 函数式编程:JS 允许高阶函数、闭包等,支持函数式编程风格。

14. JS 中关于 this 指向的问题

this 的指向取决于 调用方式

1. 全局作用域
console.log(this); // 在浏览器中:window,在 Node.js 中:global
2. 对象方法
const obj = {name: "Tom",sayHello() {console.log(this.name); // this 指向 obj}
};
obj.sayHello(); // Tom
3. 构造函数
function Person(name) {this.name = name;
}
const p = new Person("Tom");
console.log(p.name); // Tom(this 指向实例对象)
4. call / apply / bind
function sayHi() {console.log(this.name);
}
const user = { name: "Alice" };sayHi.call(user); // Alice
sayHi.apply(user); // Alice
const boundSayHi = sayHi.bind(user);
boundSayHi(); // Alice
5. 箭头函数
const obj = {name: "Tom",sayHello: function() {setTimeout(() => {console.log(this.name); // this 继承自 obj}, 1000);}
};
obj.sayHello(); // Tom

🔹 总结

  • 箭头函数的 this 由外层作用域决定。
  • bind 可手动绑定 thiscall/apply 可立即执行。

15. script 标签里的 async 和 defer 有什么区别?

<script> 标签中的 asyncdefer 的区别

在 HTML 文档中,<script> 标签用于加载 JavaScript 脚本,默认情况下,脚本会阻塞 HTML 解析,直到脚本加载并执行完毕。而 asyncdefer 这两个属性用于优化脚本加载方式,以提高页面性能。


1. asyncdefer 的基本概念

属性解析 HTML下载脚本执行脚本执行顺序
默认(无 async/defer暂停下载脚本执行脚本HTML 书写顺序执行,阻塞渲染
async不暂停并行下载下载完成立即执行执行顺序不一定,谁先下载完就先执行
defer不暂停并行下载HTML 解析完后按顺序执行按照 HTML 中的顺序执行

2. asyncdefer 详细区别

1️⃣ async(异步加载并执行)

  • async 允许脚本 异步下载,即不会阻塞 HTML 解析。
  • 一旦下载完成,就立即执行,不会等待 HTML 解析结束。
  • 多个 async 脚本的执行顺序不确定,取决于哪个脚本先下载完。

📌 示例

<script async src="script1.js"></script>
<script async src="script2.js"></script>

执行顺序:

  • script1.jsscript2.js 并行下载
  • 哪个先下载完,哪个就先执行,与 HTML 书写顺序无关。

⚠️ 适用场景

  • async 适用于 不依赖 DOM 结构不依赖其他脚本 的 JavaScript 代码,如:
    • 广告脚本
    • 统计分析脚本
    • 第三方 SDK(如 Google Analytics)

2️⃣ defer(异步加载,但按顺序执行)

  • defer 也允许脚本 异步下载,不会阻塞 HTML 解析。
  • 所有 defer 脚本会等到 HTML 解析完成后,按照 HTML 中的顺序执行。
  • 适合多个脚本有执行顺序要求的情况

📌 示例

<script defer src="script1.js"></script>
<script defer src="script2.js"></script>

执行顺序:

  • script1.jsscript2.js 同时下载
  • 等 HTML 完全解析完毕后,按照 script1.jsscript2.js 的顺序执行

⚠️ 适用场景

  • defer 适用于 依赖 DOM 结构多个脚本之间有执行顺序要求 的情况,如:
    • DOM 操作脚本
    • 框架初始化脚本
    • 多个依赖关系的 JavaScript 文件

3. async vs defer vs 默认 <script>

加载方式HTML 解析JS 下载JS 执行执行顺序
默认 <script>暂停下载执行HTML 书写顺序
async不暂停并行下载下载完成立即执行下载顺序不确定
defer不暂停并行下载HTML 解析完成后执行按 HTML 书写顺序

4. asyncdefer 适用场景

需求适合 async适合 defer
独立的第三方脚本(如广告、分析工具)
多个脚本之间无依赖关系
需要操作 DOM,必须等待 HTML 解析完成
多个脚本之间有顺序依赖

5. asyncdefer 结合使用?

HTML 规范规定,不能同时使用 asyncdefer。如果一个 <script> 标签同时有 asyncdefer,则 async 优先,defer 被忽略。


6. 最佳实践

如果脚本不依赖 DOM,可用 async

<script async src="analytics.js"></script>
<script async src="ads.js"></script>

如果脚本依赖 DOM 或多个脚本有执行顺序,可用 defer

<script defer src="jquery.js"></script>
<script defer src="main.js"></script>

如果脚本必须立即执行(阻塞执行),则不加 asyncdefer

<script src="important.js"></script>

7. 结论

asyncdefer
并行下载,下载完立刻执行并行下载,HTML 解析完后按顺序执行
执行顺序不确定(谁先下载完谁先执行)按 HTML 书写顺序执行
适用于独立、不依赖 DOM 的脚本适用于需要等待 DOM 解析完成的脚本

结论

  • async 适合独立的、无依赖的脚本(如统计、广告)。
  • defer 适合依赖 DOM 或者需要按顺序执行的脚本(如框架、主逻辑)。
  • defer加载多个脚本的最佳选择,不会阻塞页面解析,又能保证执行顺序。 🚀

16. setTimeout 最小执行时间是多少?

在大多数浏览器中,setTimeout 的最小延迟时间是 4ms(如果时间小于 4ms,实际延迟仍为 4ms)。

setTimeout(() => console.log("Hello"), 0); // 最早 4ms 后执行
  • 浏览器为了节能,嵌套 setTimeout 超过 5 次,最小延迟变成 4ms
  • 事件循环(Event Loop) 影响,setTimeout(fn, 0) 也不会立即执行。

17. ES6 和 ES5 有什么区别?

特性ES5ES6
变量声明varlet / const
作用域函数作用域块级作用域
字符串字符串拼接 (+)模板字符串(`${}`)
箭头函数() => {}
基于原型class 语法
this 绑定call/apply/bind箭头函数继承 this
模块化script 标签import/export

18. ES6 的新特性有哪些?

ES6 带来了许多新特性,包括:

  1. letconst 变量声明
    let a = 10; // 块级作用域
    const b = 20; // 常量
    
  2. 模板字符串
    let name = "Tom";
    console.log(`Hello, ${name}!`);
    
  3. 箭头函数
    const add = (x, y) => x + y;
    
  4. 解构赋值
    let { name, age } = { name: "Alice", age: 25 };
    
  5. 扩展运算符
    let arr = [1, 2, 3];
    let newArr = [...arr, 4, 5];
    
  6. class 语法
    class Person {constructor(name) {this.name = name;}
    }
    
  7. 模块化
    // 导出
    export function sayHi() { console.log("Hi!"); }
    // 导入
    import { sayHi } from "./module.js";
    

18.高阶函数和闭包的区别

在 JavaScript 中,高阶函数和闭包都是重要的概念,虽然有一定的关联,但它们的核心作用和使用方式不同。下面详细分析它们的概念、特点、区别、应用场景


1. 高阶函数(Higher-Order Function)

定义
高阶函数接收一个函数作为参数或者返回一个函数的函数。

特点

  • 参数可以是函数(回调函数)
  • 返回值可以是函数(返回新的函数)
  • 使代码更具可复用性和可组合性

示例

1. 作为参数传递的高阶函数
function operate(a, b, fn) {return fn(a, b);
}function add(x, y) {return x + y;
}console.log(operate(3, 5, add)); // 8

operate 是一个高阶函数,因为它接受 fn 作为参数。

2. 返回一个函数的高阶函数
function multiplier(factor) {return function (number) {return number * factor;};
}const double = multiplier(2);
console.log(double(5)); // 10

multiplier 是一个高阶函数,因为它返回了一个新函数。


2. 闭包(Closure)

定义
闭包是一个可以访问其外部作用域变量的函数,即使在外部作用域执行结束后,函数仍然可以访问这些变量。

特点

  • 函数内部引用了外部作用域的变量
  • 外部作用域被销毁后,闭包仍然可以访问变量
  • 变量不会被垃圾回收,可能会造成内存泄漏(如果不正确释放)

示例

1. 典型闭包示例
function outer() {let count = 0;return function inner() {count++;console.log(count);};
}const counter = outer();
counter(); // 1
counter(); // 2
counter(); // 3

inner 仍然可以访问 outer 作用域中的 count 变量,即使 outer 执行完毕。

2. 闭包用于模拟私有变量
function createCounter() {let count = 0;return {increment: function () {count++;console.log(count);},decrement: function () {count--;console.log(count);}};
}const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1

变量 count 只能被 incrementdecrement 访问,形成私有作用域。


3. 高阶函数 vs. 闭包

特性高阶函数闭包
定义函数作为参数返回函数访问外部作用域变量的函数
主要作用代码复用抽象回调机制数据封装模拟私有变量
是否涉及作用域主要关注函数传递,不强调作用域强调函数保留外部作用域
返回值可能返回一个函数返回的函数可访问外部变量
是否影响垃圾回收不一定可能会导致变量不会被回收
常见应用mapfilterreduce、回调函数计数器、私有变量、缓存函数

4. 结合高阶函数和闭包

实际上,闭包和高阶函数可以结合使用。例如,返回函数的高阶函数通常会创建闭包:

function createMultiplier(factor) {return function (number) {return number * factor;};
}const double = createMultiplier(2);
console.log(double(5)); // 10
console.log(double(10)); // 20

createMultiplier 是高阶函数,因为它返回了一个新函数,而返回的函数是闭包,因为它引用了 factor 变量。


5. 适用场景

高阶函数适用场景

  • 数组操作mapfilterreduce
  • 回调函数setTimeoutaddEventListener
  • 函数柯里化
  • 动态函数生成

闭包适用场景

  • 创建私有变量
  • 缓存计算结果
  • 延迟执行
  • 封装业务逻辑

6. 结论

  • 高阶函数 主要用于 函数的传递和返回,提高代码复用性。
  • 闭包 主要用于 保持状态和封装作用域,防止变量污染。

🔹 简单来说

  • 高阶函数 = 操作函数的函数
  • 闭包 = 访问外部变量的函数
  • 高阶函数和闭包可以结合使用,返回的函数往往会形成闭包 🚀

19. 谈谈js的事件循环(Event Loop)

JavaScript 事件循环(Event Loop)详解

1. 什么是事件循环(Event Loop)?

JavaScript 是 单线程 语言,主要用于浏览器和 Node.js 等环境中。由于 JavaScript 需要同时处理 UI 渲染、用户交互、网络请求等操作,因此采用了 事件循环(Event Loop) 机制,使其能够高效地执行任务,而不会阻塞主线程。


2. 事件循环的核心机制

JavaScript 的运行机制可以概括为:

  1. 所有同步任务(同步代码、函数调用)都在主线程上执行,形成一个 执行栈(Call Stack)
  2. 异步任务(如 setTimeoutPromiseDOM 事件 等)被挂起,等待执行时机,并存入相应的 任务队列(Task Queue)
  3. 事件循环不断检查 执行栈是否为空,如果为空,就会从任务队列中取出任务放入执行栈执行。
  4. 这个过程循环往复,形成 事件循环(Event Loop)

3. 任务队列(宏任务与微任务)

JavaScript 的任务分为 宏任务(Macro Task)微任务(Micro Task)

🔹 宏任务(Macro Task)

宏任务通常包含:

  • setTimeout
  • setInterval
  • setImmediate(Node.js)
  • I/O 任务
  • UI 渲染(Rendering)
  • MessageChannel
  • requestAnimationFrame

每次 事件循环 执行时,只会从 宏任务队列 取出一个任务执行,执行完后会检查 微任务队列

🔹 微任务(Micro Task)

微任务通常包含:

  • Promise.thencatchfinally
  • MutationObserver
  • process.nextTick(Node.js 专属)

微任务的特点

  • 在当前事件循环的 最后,也就是 当前任务执行完后立即执行
  • 微任务优先级高于宏任务,会在每次事件循环结束后立即执行所有微任务。

4. 事件循环的执行流程

  1. 执行同步代码(全局代码),将函数调用入栈。
  2. 遇到异步任务(如 setTimeoutPromise
    • setTimeout宏任务 进入 宏任务队列
    • Promise.then微任务 进入 微任务队列
  3. 同步代码执行完毕后,执行所有微任务
  4. 执行一个宏任务(如 setTimeout 回调)。
  5. 执行所有微任务
  6. 重复步骤 4 和 5,直到所有任务执行完毕

5. 代码示例

🔹 示例 1:同步、宏任务、微任务的执行顺序
console.log("1");  // 同步任务setTimeout(() => {console.log("2");  // 宏任务
}, 0);Promise.resolve().then(() => {console.log("3");  // 微任务
});console.log("4");  // 同步任务

执行结果

1
4
3
2

📌 解析

  1. 先执行同步代码,输出 1
  2. setTimeout 进入宏任务队列。
  3. Promise.then 进入微任务队列。
  4. 执行同步代码 console.log(4),输出 4
  5. 执行微任务 console.log(3),输出 3
  6. 执行宏任务 console.log(2),输出 2

🔹 示例 2:多个微任务与宏任务
console.log("A");setTimeout(() => {console.log("B");
}, 0);Promise.resolve().then(() => {console.log("C");
}).then(() => {console.log("D");
});console.log("E");

执行结果

A
E
C
D
B

📌 解析

  1. 先执行同步代码,输出 AE
  2. setTimeout 进入 宏任务队列
  3. Promise.then 进入 微任务队列,执行 console.log("C"),输出 C
  4. Promise.then 产生的第二个微任务 console.log("D") 执行,输出 D
  5. 事件循环进入宏任务阶段,执行 setTimeout,输出 B

6. 常见问题

🔹 setTimeout(fn, 0) 真的会立即执行吗?

不会。即使 setTimeout 设为 0,它依然是 宏任务,必须等到当前执行栈和所有 微任务执行完毕后,才会执行。

🔹 async/await 和事件循环

async/await 其实是 Promise 的语法糖,其 await 关键字会 暂停代码执行,并将后续代码作为微任务放入微任务队列

示例:

async function test() {console.log("A");await Promise.resolve();console.log("B");
}console.log("C");
test();
console.log("D");

执行结果

C
A
D
B

📌 解析

  1. console.log("C") 先执行,输出 C
  2. test() 执行,输出 A,遇到 await,暂停。
  3. console.log("D") 执行,输出 D
  4. await 后的代码作为微任务 console.log("B") 进入微任务队列,随后执行,输出 B

7. 浏览器和 Node.js 的 Event Loop 区别

环境微任务顺序setImmediate
浏览器Promise.then 优先于 setTimeoutsetImmediatesetTimeout(0) 之后
Node.jsprocess.nextTick 优先于 Promise.thensetImmediatesetTimeout(0) 之前

示例:

setImmediate(() => console.log("setImmediate"));
setTimeout(() => console.log("setTimeout"), 0);
Promise.resolve().then(() => console.log("Promise"));
process.nextTick(() => console.log("nextTick"));

Node.js 结果

nextTick
Promise
setImmediate
setTimeout

浏览器结果

Promise
setTimeout
setImmediate

📌 解析

  • Node.jsprocess.nextTick 优先级最高,Promise.then 其次,setImmediatesetTimeout(0) 先执行。
  • 浏览器Promise.then 先执行,然后 setTimeout(0),最后 setImmediate(浏览器中 setImmediate 表现与 setTimeout(0) 类似)。

8. 总结

  1. JavaScript 是单线程的,依赖事件循环(Event Loop)处理异步任务
  2. 任务分为
    • 同步任务(同步执行)
    • 微任务(Promise.then, MutationObserver, process.nextTick)
    • 宏任务(setTimeout, setInterval, setImmediate, UI渲染等)
  3. 事件循环流程
    • 执行同步任务
    • 执行所有微任务
    • 执行下一个宏任务(然后再执行微任务)
  4. async/await 也是基于 Promise,await 后的代码是微任务

💡 掌握事件循环的机制,有助于理解 JavaScript 的异步行为,编写高效的前端代码!🚀

20. 用递归的时候有没有遇到什么问题?**

在使用 递归(Recursion) 时,可能会遇到以下问题:

1. 递归深度过大,导致栈溢出(Stack Overflow)
  • 递归会不断调用自身,每次调用都会占用 调用栈(Call Stack) 的空间。如果递归深度过大,可能会导致 堆栈溢出(Maximum call stack size exceeded) 错误。
  • 示例(错误示范):
    function infiniteRecursion() {infiniteRecursion(); // 无限递归,导致栈溢出
    }
    infiniteRecursion();
    
    解决方案
    • 使用尾递归优化(Tail Recursion)(ES6 支持)
    • 改用循环(如 for/while) 代替递归
    • 设定递归终止条件
2. 递归效率低,可能导致性能问题
  • 递归可能导致 重复计算,特别是在计算斐波那契数列等问题时。

  • 示例(低效的递归):

    function fibonacci(n) {if (n <= 1) return n;return fibonacci(n - 1) + fibonacci(n - 2);
    }
    console.log(fibonacci(40)); // 计算量非常大
    

    解决方案

    • 使用缓存(记忆化存储,Memoization)
    • 使用动态规划(Dynamic Programming) 代替递归
    function fibonacciMemo(n, memo = {}) {if (n in memo) return memo[n];if (n <= 1) return n;return memo[n] = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo);
    }
    console.log(fibonacciMemo(40)); // 计算快很多
    
3. 递归终止条件错误,导致无限循环
  • 示例
    function countdown(n) {if (n === 0) return; // 终止条件console.log(n);countdown(n - 1);
    }
    countdown(5); // 正确
    
    • 终止条件错误可能导致 无限递归,从而引发栈溢出。
4. 递归函数的参数处理错误
  • 在递归过程中,参数的状态可能变化,影响递归逻辑。
  • 示例(错误的参数传递):
    function sum(arr) {if (arr.length === 0) return 0;return arr[0] + sum(arr); // 这里没有去除 arr[0],会无限递归
    }
    console.log(sum([1, 2, 3])); // 错误
    
    正确方式
    function sum(arr) {if (arr.length === 0) return 0;return arr[0] + sum(arr.slice(1)); // 传入去掉首元素的数组
    }
    console.log(sum([1, 2, 3])); // 6
    

21. 如何实现一个深拷贝(Deep Clone)?

深拷贝是指创建一个 新对象,并 完全复制原对象的所有属性值,包括嵌套对象,而不是仅仅拷贝引用(浅拷贝)。

方法 1:JSON 方式(简单但有局限)
const obj = { a: 1, b: { c: 2 } };
const clone = JSON.parse(JSON.stringify(obj));
console.log(clone); // { a: 1, b: { c: 2 } }

优点

  • 适用于 简单对象(无函数、循环引用等)。

缺点

  • 无法处理 函数、undefinedDateRegExpMapSet 等类型。
  • 会移除 原型链(Prototype)

方法 2:递归深拷贝(适用于大部分场景)
function deepClone(obj, hash = new WeakMap()) {if (obj === null || typeof obj !== "object") return obj;if (hash.has(obj)) return hash.get(obj); // 处理循环引用let cloneObj = Array.isArray(obj) ? [] : {};hash.set(obj, cloneObj);for (let key in obj) {if (obj.hasOwnProperty(key)) {cloneObj[key] = deepClone(obj[key], hash);}}return cloneObj;
}
const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
const clone = deepClone(obj);
console.log(clone); // 深拷贝成功

优点

  • 适用于 普通对象、数组
  • 解决循环引用 问题。

缺点

  • 无法拷贝函数、特殊对象(如 Map, Set, RegExp

方法 3:基于 lodash

lodash 提供 _.cloneDeep() 方法进行深拷贝:

const _ = require("lodash");
const obj = { a: 1, b: { c: 2 } };
const clone = _.cloneDeep(obj);
console.log(clone);

优点

  • 处理 大多数数据类型(对象、数组、Date、RegExp、Map、Set 等)

缺点

  • 需要额外引入 lodash

方法 4:使用 structuredClone()(推荐,现代浏览器支持)
const obj = { a: 1, b: { c: 2 }, d: new Date() };
const clone = structuredClone(obj);
console.log(clone);

优点

  • 支持大多数数据类型(对象、数组、Date、Blob、Map、Set等)
  • 性能比递归方式更优

缺点

  • 不支持函数、undefinedDOM 元素

总结

方法优点缺点
JSON.parse(JSON.stringify())适用于 简单对象,性能较优无法处理 functionundefinedRegExpDate、循环引用
递归深拷贝适用于大部分对象,支持循环引用无法拷贝 Map, Set, RegExp, Date
lodash _.cloneDeep()功能强大,支持 Date, Map, Set需要引入第三方库
structuredClone()(推荐)原生方法,支持多数数据类型,性能优秀不支持函数、undefined

🚀 推荐方案

  • 普通对象:用 JSON.parse(JSON.stringify())
  • 复杂对象(包含 MapSet、循环引用):用 structuredClone()_.cloneDeep()
  • 需要最大兼容性:用 递归方法

22.如何实现一个浅拷贝(Shallow Copy)?

浅拷贝(Shallow Copy)指的是 仅拷贝对象的第一层属性,如果属性是 引用类型(如对象、数组),那么拷贝的只是引用(即指针),而不是值本身。


1. 使用 Object.assign()

Object.assign() 方法可以用于浅拷贝对象:

const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);console.log(shallowCopy); // { a: 1, b: { c: 2 } }
console.log(shallowCopy.b === obj.b); // true(引用同一个对象)

优点

  • 简单直观,适用于浅拷贝。

缺点

  • 无法拷贝原型链(Prototype)。
  • 对于嵌套对象,只复制引用(不是深拷贝)。

2. 使用展开运算符 { ...obj }(ES6 推荐)

const obj = { a: 1, b: { c: 2 } };
const shallowCopy = { ...obj };console.log(shallowCopy); // { a: 1, b: { c: 2 } }
console.log(shallowCopy.b === obj.b); // true(仍然指向相同的对象)

优点

  • 语法更简洁
  • 适用于 对象 的浅拷贝。

缺点

  • 仅拷贝 第一层,内部对象仍然是引用。

3. 使用 Array.prototype.slice() 进行数组浅拷贝

如果是数组,可以使用 slice() 方法:

const arr = [1, 2, { a: 3 }];
const shallowCopy = arr.slice();console.log(shallowCopy); // [1, 2, { a: 3 }]
console.log(shallowCopy[2] === arr[2]); // true(仍然指向同一个对象)

优点

  • 适用于数组的浅拷贝

缺点

  • 数组中的 对象引用不会被复制,仍然指向原对象。

4. 使用 Array.prototype.concat() 进行数组浅拷贝

const arr = [1, 2, { a: 3 }];
const shallowCopy = [].concat(arr);console.log(shallowCopy); // [1, 2, { a: 3 }]
console.log(shallowCopy[2] === arr[2]); // true

优点

  • 适用于数组浅拷贝。

5. 使用 Object.create()

如果希望复制 原型链,可以使用 Object.create()

const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));console.log(shallowCopy); // { a: 1, b: { c: 2 } }
console.log(shallowCopy.b === obj.b); // true(仍然指向同一个对象)

优点

  • 复制 原型链,比 Object.assign() 更完整。

缺点

  • 仍然是浅拷贝

📌 总结

方法适用类型是否拷贝原型是否深拷贝优缺点
Object.assign()对象❌ 否❌ 否简单易用,但不能拷贝嵌套对象
{ ...obj }(展开运算符)对象❌ 否❌ 否语法更简洁,适用于浅拷贝
slice()数组❌ 否❌ 否适用于数组,但嵌套对象仍然是引用
concat()数组❌ 否❌ 否适用于数组浅拷贝
Object.create()对象✅ 是❌ 否复制原型链,但仍然是浅拷贝

🚀 推荐方案

  • 拷贝对象{ ...obj }(ES6)
  • 拷贝数组slice()concat()
  • 需要保留原型Object.create()

⚠️ 注意:如果对象中有 嵌套对象(深层引用),则需要使用 深拷贝,如 structuredClone() 或递归拷贝。

23.深浅拷贝的区别和使用场景

深浅拷贝是指在复制对象时,是否复制对象的引用还是复制对象的内容。它们的区别在于对原始对象的修改是否影响到拷贝后的对象。

1. 浅拷贝

浅拷贝是指创建一个新的对象,但新对象的属性仍然引用原始对象中的引用类型数据(比如数组或对象)。因此,如果原始对象中的引用类型属性被修改,拷贝后的对象也会受到影响。

示例:
const obj1 = {name: "Alice",details: { age: 25 }
};const shallowCopy = { ...obj1 }; // 浅拷贝obj1.details.age = 26; // 修改原对象的属性console.log(shallowCopy.details.age); // 输出 26,浅拷贝的对象会受到影响
使用场景:
  • 当你希望快速复制一个对象,但不关心嵌套对象的引用时。
  • 适用于处理浅层次的简单数据结构,如直接的基本数据类型或只包含基本数据类型的对象。

2. 深拷贝

深拷贝是指创建一个新的对象,并递归地复制原始对象的所有属性,包括对象中的引用类型数据。这样,修改原始对象中的引用类型属性不会影响拷贝后的对象。

示例:
const obj1 = {name: "Alice",details: { age: 25 }
};const deepCopy = JSON.parse(JSON.stringify(obj1)); // 深拷贝obj1.details.age = 26; // 修改原对象的属性console.log(deepCopy.details.age); // 输出 25,深拷贝的对象不会受到影响
使用场景:
  • 当你需要完全独立的对象,确保修改原始对象的引用类型数据时不会影响到拷贝后的对象。
  • 适用于复杂数据结构,包含嵌套对象和数组的情况,尤其是当你需要保证每个对象是独立的副本时。

总结:

  • 浅拷贝:仅复制对象的一级属性,对引用类型的属性(如数组、对象)进行的是引用拷贝。
  • 深拷贝:递归地复制对象及其所有嵌套的引用类型属性,生成一个完全独立的副本。

选择何种拷贝方式取决于你对数据独立性的需求,是否需要对嵌套对象进行修改或保留原对象的状态。

24. AJAX 是什么?怎么实现的?

AJAX(Asynchronous JavaScript and XML)是一种异步通信技术,允许网页在不刷新的情况下与服务器交换数据。
实现方式

  1. 创建 XMLHttpRequest 对象。
  2. 定义回调函数处理响应(onreadystatechangeonload)。
  3. 使用 open() 设置请求方法(GET/POST)和 URL。
  4. 发送请求(send()),如 POST 需设置请求头 Content-Type
    现代方法可使用 fetch API:
fetch(url).then(response => response.json()).then(data => console.log(data));

25. GET 和 POST 有什么区别?

GETPOST
参数在 URL 中,长度受限(约 2048 字符)参数在请求体中,无长度限制
用于获取数据,幂等(多次请求结果相同)用于提交数据,非幂等
可缓存不可缓存
安全性较低(URL 可见)相对安全

26. Promise 的内部原理是什么?优缺点?

原理

  • 状态机模型(pendingfulfilled/rejected)。
  • 通过 then 方法注册回调,返回新 Promise 实现链式调用。
  • 微任务机制(回调放入微任务队列,优先级高于宏任务)。

优点

  • 解决回调地狱,代码更线性。
  • 统一错误处理(catch)。

缺点

  • 无法取消正在执行的 Promise。
  • 未正确处理错误可能导致静默失败。

27. Promise 和 async/await 的区别?

  • async/await 是 Promise 的语法糖,异步代码同步化,提高可读性。

  • async 函数隐式返回 Promise,await 后接 Promise 或值。

  • 错误处理:

    // Promise
    fetch().catch(err => {});// async/await
    try { await fetch(); } catch (err) {}
    

28. 浏览器的存储方式有哪些?

类型特点
Cookie最大 4KB,每次请求携带,可设过期时间,同源限制。
localStorage持久存储(除非手动删除),同源,大小约 5-10MB。
sessionStorage会话级存储(标签页关闭清除),同源。
IndexedDB非关系型数据库,支持事务,存储大量数据。
Web Storage(localStorage 和 sessionStorage 统称)

29. Token 存在 sessionStorage 还是 localStorage?

  • sessionStorage:会话结束(标签页关闭)自动清除,降低 XSS 攻击后 Token 泄漏风险。
  • localStorage:持久存储,适合需要长期登录的场景,但需防范 XSS。
    建议:敏感数据优先存 sessionStorage,结合服务端设置较短 Token 有效期。

30. Token 的登录流程

  1. 用户输入账号密码提交登录。
  2. 服务端验证通过,生成 Token(如 JWT)返回。
  3. 前端存储 Token(如 sessionStorage)。
  4. 后续请求在 Authorization 头添加 Token(如 Bearer <token>)。
  5. 服务端校验 Token 有效性,返回数据。

安全优化

  • Token 设置较短有效期,搭配 Refresh Token 用于续期。
  • 启用 HTTPS,防范中间人攻击。

31. 页面渲染的过程

  1. 构建 DOM 树:解析 HTML 生成节点树。
  2. 构建 CSSOM:解析 CSS 生成样式树。
  3. 合成渲染树:合并 DOM 和 CSSOM,排除不可见元素(如 display: none)。
  4. 布局(Layout) :计算节点几何信息(尺寸、位置)。
  5. 绘制(Paint) :将布局信息转换为像素。
  6. 合成(Composite) :分层绘制,GPU 加速渲染。

32. DOM 树和渲染树的区别

DOM 树渲染树
包含所有 HTML 节点(包括隐藏元素)仅包含需渲染的节点
结构完整,描述文档内容结合 DOM 和 CSSOM,描述可视内容

33. 精灵图和 Base64 的区别

CSS Sprites(精灵图)Base64
多图合并为一张,减少请求图片转字符串嵌入代码
需通过 background-position 定位增大文件体积(约 30%)
适合多图标场景适合小图标,避免额外请求

34. SVG 格式的特点

  • 矢量图形:无损缩放,适用于高分辨率屏幕。
  • XML 格式:可通过 CSS/JS 动态修改,支持动画和交互。
  • SEO 友好:文本内容可被搜索引擎读取。
  • 缺点:复杂图形可能性能不如 Canvas。

35. JWT 的组成与特点

组成

  • Header:算法和类型(如 HS256)。
  • Payload:自定义数据(如用户 ID)。
  • Signature:签名(防篡改)。

特点:无需服务端存储 Session,支持跨域。
注意:需防范 Token 盗用,避免存储敏感信息,设置短过期时间。


36. npm 的底层环境

  • 依赖管理:基于 package.json 描述依赖及版本(遵循语义化版本 semver)。

  • 模块加载:Node.js 的 CommonJS 规范。

  • 包存储:默认从 npm 仓库下载(registry.npmjs.org)。

  • 安装机制

    • npm install 解析依赖树,扁平化安装(避免嵌套过深)。
    • package-lock.json 锁定精确版本,确保环境一致性。

37.HTTP协议规定的协议头和请求头的区别**

“协议头”可能指的是HTTP协议的头部,即整体结构中的头部,而请求头则是请求部分的头部。
每个HTTP请求由三部分组成:请求行(请求方法、URI、HTTP版本)、请求头部(headers)、请求正文。
请求行有时也称为起始行,而请求头部则是首部字段。响应类似,有状态行、响应头部、正文。
HTTP协议中,“协议头”与“请求头”的区别可以通过以下结构化说明进行澄清:
HTTP协议中,“协议头”与“请求头”的区别可以通过以下结构化说明进行澄清:


一、明确术语定义

1. 协议头(HTTP Message Headers)
  • 定义:在HTTP标准中,“协议头”没有严格对应的定义。此词可能被误解为以下两种场景:

    • 广义:指整个HTTP消息的头部(包含起始行和首部字段)。
    • 狭义:可能指请求行或响应状态行(即描述协议版本的部分)。
  • 实际标准:RFC文档中未使用“协议头”一词,需结合上下文理解。

2. 请求头(Request Headers)
  • 明确定义:严格属于HTTP请求中的首部字段部分。
  • 作用:包含客户端发送的附加信息(如身份验证、内容类型、缓存策略等)。
  • 标准依据:由RFC 7230等规范中的header-field定义。

二、HTTP请求的结构分解

一个HTTP请求由以下三部分组成(按顺序排列):

  1. 请求行(Request Line)

    • 格式:[Method] [URI] [HTTP Version]
      (如 GET /index.html HTTP/1.1

    • 关键字段

      • HTTP方法:GET、POST等操作类型。
      • URI:请求的资源路径。
      • 协议版本(如HTTP/1.1):规定通信规则。
  2. 请求头(Request Headers)

    • 格式:键值对的集合,每行一个字段。
      (如 Host: example.comUser-Agent: Chrome/123

    • 常见类型

      • 通用头(如Cache-Control
      • 请求头(如AcceptAuthorization
      • 实体头(如Content-Type)。
  3. 请求正文(Body)

    • 可选部分,主要用于POST/PUT等包含数据的请求。

三、关键对比

对比维度协议头(可能场景)请求头(Request Headers)
**组成内容若指请求行:包含方法、URI、HTTP版本。
若指整个头部:包含请求行+首部字段。
仅包含键值对的头部字段(如Host, Accept)。
功能定位若为请求行:定义操作类型、资源路径和协议规则。提供请求的附加信息和控制参数。
标准化术语非RFC标准术语,需结合上下文。RFC 7230明确定义的header-field

四、示例说明

HTTP请求示例
GET /search?q=test HTTP/1.1     ← 请求行(请求方法、URI、HTTP版本)
Host: www.example.com          ← 请求头(首部字段)
User-Agent: Mozilla/5.0
Accept: text/html
  • 协议头(若指请求行)GET /search?q=test HTTP/1.1
  • 请求头:从HostAccept的所有键值对字段。

五、总结

  • 请求头是HTTP请求中明确的首部字段部分,用于传递附加信息。

  • 协议头需要根据上下文判断:

    • 若指请求行,表示操作和协议版本;
    • 若指整个头部范畴,则包含请求行+请求头,但此用法不符合标准术语。
  • 实际开发中应严格遵循RFC标准术语,避免混淆。

**38. 同源策略是什么你怎么理解

同源策略(Same-Origin Policy)是浏览器为防止恶意攻击而实施的核心安全机制。以下为系统化的解析:


一、定义

同源策略规定:浏览器仅允许网页脚本(如JavaScript)访问与其同源的资源,跨源访问会被默认禁止。

  • 同源的三要素:要求 协议域名端口三者完全相同。
    示例

    • https://www.example.com/page https://www.example.com/api → 同源(路径不同不影响)
    • http://www.example.com https://www.example.com → 不同源(协议不同)
    • www.example.com api.example.com → 不同源(子域名不同)

二、作用目标

同源策略针对以下操作进行限制:

  1. 数据访问

    • Cookie/LocalStorage:A网站的脚本无法读取B网站的存储数据。
    • DOM操作:A网站的页面无法通过iframe嵌入并操作B网站的DOM(除非明确同源)。
  2. 网络请求

    • XMLHttpRequest、Fetch API默认禁止跨域请求(需CORS或代理支持)。
  3. 其他资源限制

    • scriptimg等标签可跨域加载资源,但脚本无法直接读取内容(如跨域图片的像素数据需许可)。

三、核心逻辑

浏览器在以下场景中执行同源检查:

场景是否允许示例说明
AJAX请求默认禁止跨域,除非服务器返回CORS头fetch('https://api.site.com/data')被拦截
操作跨域iframe内容禁止读写DOM/调用函数iframe.contentWindow.document.body会报错
Web存储访问本地存储数据仅允许同源脚本访问localStorage不同源页面无法共享数据
Web Workers脚本需同源或明确启用跨域加载跨域Worker脚本需服务器支持CORS

四、例外机制

以下场景可绕过同源策略(但需显式配置):

  1. CORS(跨域资源共享)

    • 服务器通过 Access-Control-Allow-Origin 响应头授权特定域访问资源。
    • 预检请求:复杂请求(如带自定义头的POST)需先发送 OPTIONS 请求确认权限。
  2. JSONP(过时技术)

    • 利用 <script> 标签不受同源策略限制的特性,通过回调函数获取跨域数据。
  3. document.domain(仅限同主域)

    • 设置 document.domain = 'example.com',使同主域不同子域的页面可以互操作。
  4. postMessage API

    • 允许不同源的窗口间通过消息传递安全通信(如iframe父子页面)。

五、开发实践建议

  1. 前端解决方案

    • 开发环境通过代理(如Webpack DevServer)转发跨域请求。
  2. 后端设置

    • 配置CORS头(精确控制允许的源、方法、头字段),避免使用 Access-Control-Allow-Origin: *
  3. 安全权衡

    • 确保动态开放的跨域资源(如公共API)不会泄露敏感信息或暴露未授权操作。

六、类比理解

将同源策略想象成酒店房卡系统

  • 每个房间(源)的房卡(脚本权限)仅能打开自己的房门(同源资源)。
  • 若需访问其他房间(跨域),需前台(服务器)授权临时通行证(CORS头)。

总结

同源策略是浏览器安全的基石,平衡了功能性与风险。掌握其规则与跨域解决方案(如CORS),是开发现代Web应用的关键技能。

39. 防抖与节流

详细查看链接

1. 概念对比

1.1 防抖(Debounce)

  • 定义: 在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时
  • 场景:
    • 搜索框输入联想
    • 窗口大小调整
    • 表单验证
    • 按钮提交事件

1.2 节流(Throttle)

  • 定义: 规定在一个单位时间内,只能触发一次函数,如果这个单位时间内触发多次函数,只有一次生效
  • 场景:
    • 滚动事件处理
    • 页面resize事件
    • 射击游戏中的子弹发射
    • 表单快速提交

1.3 区别示意

// 防抖:等待一段时间后执行,期间重新触发会重新计时
Input Events:   │─ABC─ │────D──│──E──│
Debounced:      │────────│────D──│──E──│// 节流:按照一定时间间隔执行
Input Events:   │─ABCDEFG─│
Throttled:      │─A─────D─────G─│
  • 防抖(Debounce)
function debounce(fn, delay) {let timer;return (...args) => {clearTimeout(timer);timer = setTimeout(() => fn(...args), delay);};
}
// 应用场景:输入框搜索联想
  • 节流(Throttle)
function throttle(fn, delay) {let last = 0;return (...args) => {const now = Date.now();if (now - last >= delay) {fn(...args);last = now;}};
}
// 应用场景:滚动事件监听

40. JSON简介

  • 定义:轻量级数据交换格式(基于键值对)

  • 结构:支持字符串、数字、布尔、数组、对象、null

  • 转换方法

    JSON.parse('{"name":"John"}');  // 字符串 → 对象
    JSON.stringify({name: 'John'}); // 对象 → 字符串
    

41. 数据未请求时的处理方案

  1. 加载状态:显示 Loading 动画或骨架屏

  2. 错误反馈

    • 网络错误提示
    • 超时自动重试(最多 3 次)
  3. 数据降级

    • 使用本地缓存或默认值(如空列表占位)
  4. 重试逻辑

    function fetchWithRetry(url, retries = 3) {return fetch(url).catch(err => retries > 0 ? fetchWithRetry(url, retries - 1) : Promise.reject(err));
    }
    

42. 无感登录实现

  1. Access Token 过期检测:拦截 401 状态码

  2. Refresh Token 刷新

    // 在响应拦截器中处理
    axios.interceptors.response.use(response => response,async error => {if (error.response.status === 401) {const newToken = await refreshToken();error.config.headers.Authorization = `Bearer ${newToken}`;return axios.request(error.config); // 重发原请求}return Promise.reject(error);}
    );
    
  3. 安全性

    • Refresh Token 存为 httpOnly Cookie
    • 设置较短有效期并限制使用次数

43. 大文件上传方案

  1. 文件分片

    const chunkSize = 5 * 1024 * 1024; // 5MB/片
    const chunks = [];
    let start = 0;
    while (start < file.size) {chunks.push(file.slice(start, start + chunkSize));start += chunkSize;
    }
    
  2. 唯一标识:计算文件哈希(如 MD5)

  3. 并发上传

    chunks.forEach((chunk, index) => {const formData = new FormData();formData.append('chunk', chunk);formData.append('hash', `${fileHash}-${index}`);axios.post('/upload', formData);
    });
    
  4. 断点续传:根据已上传分片列表跳过已传部分

  5. 合并分片:服务端接收到所有分片后合并成完整文件


44. 说一下浏览器的缓存策略

浏览器的缓存策略主要分为 强缓存(Strong Cache)协商缓存(Negotiated Cache),它们用于减少重复请求、提升页面加载速度。


1. 强缓存(Strong Cache)

强缓存是指浏览器在缓存有效期内 不向服务器发送请求,直接从本地缓存中获取资源,提高访问速度。

常见的强缓存策略

  1. Expires(HTTP 1.0)

    • Expires 响应头指定资源的过期时间,如:
      Expires: Wed, 22 Mar 2025 08:00:00 GMT
      
    • 本地时间影响,如果客户端时间不准确,可能导致缓存失效问题。
  2. Cache-Control: max-age(HTTP 1.1,优先级高于 Expires)

    • Cache-Control: max-age=3600 表示资源在 3600 秒内有效,不需要重新请求。
    • 示例
      Cache-Control: max-age=86400
      
    • 作用:
      • no-cache:不使用强缓存,但会触发协商缓存。
      • no-store:不缓存资源,每次都重新请求。
      • public:所有用户都可以缓存该资源(包括代理服务器)。
      • private:只能被当前用户缓存,代理服务器不能缓存。

强缓存流程

  • 浏览器先检查 Cache-ControlExpires 是否有效。
  • 如果缓存有效,则直接使用缓存,不发送请求。

2. 协商缓存(Negotiated Cache)

当强缓存失效时,浏览器会向服务器发送请求,并通过 协商缓存机制 确定资源是否需要重新下载。

常见的协商缓存策略

  1. Last-Modified & If-Modified-Since

    • 服务器返回资源最后修改时间
      Last-Modified: Wed, 22 Mar 2025 08:00:00 GMT
      
    • 浏览器请求时带上 If-Modified-Since
      If-Modified-Since: Wed, 22 Mar 2025 08:00:00 GMT
      
    • 服务器对比
      • 若资源未修改,返回 304 Not Modified,使用缓存。
      • 若资源已修改,返回新的资源 200 OK
  2. ETag & If-None-Match(优先级高于 Last-Modified)

    • 服务器返回资源的唯一标识
      ETag: "abc123"
      
    • 浏览器请求时带上 If-None-Match
      If-None-Match: "abc123"
      
    • 服务器对比
      • 若 ETag 未变,返回 304 Not Modified,使用缓存。
      • 若 ETag 变化,返回新的资源 200 OK

协商缓存流程

  • 浏览器向服务器发送请求,带上 If-Modified-SinceIf-None-Match
  • 服务器检查资源是否更新:
    • 未更新:返回 304,使用缓存。
    • 已更新:返回 200 并提供新资源。

3. 缓存策略对比

缓存策略适用情况是否向服务器请求响应状态码主要控制字段
强缓存资源未过期200(from cache)Cache-ControlExpires
协商缓存资源已过期304(Not Modified)ETagLast-Modified

4. 现实应用中的缓存优化

1. 静态资源使用强缓存

  • 方式:设置 Cache-Control: max-age=31536000,并使用 文件名哈希 处理更新,如 app.123abc.js
  • 示例
    Cache-Control: max-age=31536000, immutable
    

2. 重要数据使用协商缓存

  • 方式:使用 ETagLast-Modified,确保数据变更时能及时更新。
  • 示例
    ETag: "abc123"
    

3. HTML 文件使用 no-cache

  • 避免用户访问时看到旧的 HTML 页面,但仍然允许缓存 CSS、JS:
    Cache-Control: no-cache
    

4. AJAX 接口请求

  • 如果数据变化频繁,可以使用 Cache-Control: no-store 强制不缓存:
    Cache-Control: no-store
    

5. 总结

  1. 强缓存ExpiresCache-Control: max-age)优先,避免请求。
  2. 强缓存失效后,协商缓存Last-ModifiedETag)减少数据传输。
  3. 静态资源(JS/CSS/图片)用强缓存 + 文件名哈希 方式优化。
  4. HTML、API 响应一般使用 no-cacheno-store 确保数据最新。
  5. ETag 优先级高于 Last-Modified,适用于精确缓存控制。

45 ** 延迟加载 JS 有哪些方式?**

延迟加载(Lazy Loading)JS 主要有以下方式:

  1. defer 属性(适用于外部 JS 文件)

    • 脚本会在 HTML 解析完成后 按顺序执行,不会阻塞 HTML 解析。
    • 示例
      <script src="script.js" defer></script>
      
  2. async 属性(适用于外部 JS 文件)

    • 脚本会在 下载完成后立即执行,不会阻塞 HTML 解析,但执行顺序不确定。
    • 示例
      <script src="script.js" async></script>
      
  3. 动态创建 <script> 标签

    • 可以在需要时动态加载 JS,适用于按需加载。
    • 示例
      const script = document.createElement("script");
      script.src = "script.js";
      document.body.appendChild(script);
      
  4. 按需加载(懒加载)

    • 通过 import() 方式进行模块化异步加载(适用于 ES6+)。
    • 示例
      import("./module.js").then((module) => {module.default();
      });
      
  5. 使用 Webpack 的 code-splitting

    • Webpack 提供 import() 进行代码拆分,只有在需要时才加载模块。
    • 示例
      function loadModule() {import("./module.js").then((module) => {module.default();});
      }
      

46. JS 数据类型有哪些?

JS 具有 7 种原始类型(Number、String、Boolean、Undefined、Null、Symbol、BigInt)和 引用类型(Object)。
JavaScript 数据类型分为 原始类型(Primitive Types)引用类型(Reference Types)

(1)原始类型(基本数据类型)
  1. Number(数字类型)

    • 包括整数、浮点数、NaN(不是一个数字)、Infinity
    • 示例
      let a = 42;
      let b = 3.14;
      let c = NaN;
      let d = Infinity;
      
  2. String(字符串类型)

    • 由字符组成的文本数据,可以使用单引号、双引号或模板字符串。
    • 示例
      let str1 = "Hello";
      let str2 = 'World';
      let str3 = `Template ${str1}`;
      
  3. Boolean(布尔类型)

    • 只有 truefalse 两个值。
    • 示例
      let isTrue = true;
      let isFalse = false;
      
  4. Undefined(未定义)

    • 变量声明但未赋值时的默认值。
    • 示例
      let x;
      console.log(x); // undefined
      
  5. Null(空值)

    • 一个表示 “无值” 的特殊值,通常用于手动赋值为空。
    • 示例
      let y = null;
      
  6. Symbol(唯一值,ES6)

    • 创建独一无二的标识符,常用于对象属性。
    • 示例
      let sym = Symbol("unique");
      
  7. BigInt(大整数,ES11)

    • 适用于比 Number 能表示的范围更大的整数。
    • 示例
      let bigInt = 123456789012345678901234567890n;
      
(2)引用类型(复杂数据类型)
  1. Object(对象)

    • key-value 组成的集合。
    • 示例
      let obj = { name: "Alice", age: 25 };
      
  2. Array(数组)

    • 数组是一种特殊的对象,可以存储有序数据。
    • 示例
      let arr = [1, 2, 3];
      
  3. Function(函数)

    • JavaScript 中函数本质上也是对象,可以赋值给变量。
    • 示例
      function greet() {return "Hello";
      }
      
  4. DateRegExpMapSet 也是常见的引用类型。


3. nullundefined 的区别

null 表示"空值",undefined 表示"未定义"

关键点nullundefined
含义表示 “无值”,需手动赋值变量未赋值时的默认值
类型object(JS 设计缺陷)undefined
使用场景明确赋值为空变量未声明或未赋值
示例let a = null;let b; console.log(b); // undefined

判断方法

console.log(typeof null);       // "object"
console.log(typeof undefined);  // "undefined"console.log(null == undefined);  // true(值相等)
console.log(null === undefined); // false(类型不同)

47 . JS 数据类型考题

(1)typeof 结果
console.log(typeof null);       // "object"(历史遗留问题)
console.log(typeof undefined);  // "undefined"
console.log(typeof 42);         // "number"
console.log(typeof "Hello");    // "string"
console.log(typeof true);       // "boolean"
console.log(typeof {});         // "object"
console.log(typeof []);         // "object"
console.log(typeof function(){}); // "function"
(2)instanceof 判断
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function); // true

5. ===== 有什么不同?

运算符是否比较类型是否进行类型转换例子
=="1" == 1 // true
==="1" === 1 // false
(1)==(值相等,类型可转换)
console.log(0 == false);    // true
console.log(1 == "1");      // true
console.log(null == undefined); // true
  • == 允许不同数据类型进行转换后再比较。
(2)===(值和类型都必须相等)
console.log(0 === false);   // false
console.log(1 === "1");     // false
console.log(null === undefined); // false
  • === 需要严格相等,避免了类型转换导致的潜在错误。
  • == 会进行类型转换,=== 需要类型和值都相等
  • JS 代码优化建议使用 ===,避免隐式转换导致的错误

48.slice和splice的区别

slicesplice 的区别

slicesplice 都是 JavaScript 数组方法,但它们的作用、影响和返回值不同:

方法作用是否修改原数组返回值
slice截取数组的一部分不会修改原数组返回截取的新数组
splice删除/替换/插入数组元素修改原数组返回被删除的元素组成的数组

1. slice(start, end)

  • 作用:从数组中 截取 指定范围的元素,不修改原数组
  • 参数
    • start(必填):起始索引(包含)。
    • end(可选):结束索引(不包含)。
  • 返回值:截取的新数组。

示例

let arr = [1, 2, 3, 4, 5];console.log(arr.slice(1, 4));  // [2, 3, 4]  (索引 1 ~ 3)
console.log(arr.slice(2));     // [3, 4, 5]  (索引 2 到末尾)
console.log(arr.slice(-3));    // [3, 4, 5]  (倒数第 3 个元素到末尾)console.log(arr); // [1, 2, 3, 4, 5] (原数组不变)

2. splice(start, deleteCount, ...items)

  • 作用:删除、替换或插入数组中的元素,修改原数组
  • 参数
    • start(必填):起始索引(从该索引开始操作)。
    • deleteCount(可选):删除的元素个数,若为 0 则不删除。
    • items(可选):要插入的元素(可变参数)。
  • 返回值被删除的元素组成的新数组

示例

(1)删除元素
let arr1 = [1, 2, 3, 4, 5];// 从索引 1 开始删除 2 个元素
console.log(arr1.splice(1, 2)); // [2, 3] (返回删除的部分)
console.log(arr1); // [1, 4, 5] (原数组被修改)
(2)插入元素
let arr2 = [1, 2, 3, 4, 5];// 从索引 2 处插入 "a" 和 "b"
arr2.splice(2, 0, "a", "b");
console.log(arr2); // [1, 2, "a", "b", 3, 4, 5]
(3)替换元素
let arr3 = [1, 2, 3, 4, 5];// 替换索引 1 处的 2 个元素(2、3 替换为 "x", "y")
arr3.splice(1, 2, "x", "y");
console.log(arr3); // [1, "x", "y", 4, 5]

3. slice vs splice 总结

方法修改原数组返回值用途
slice❌ 不修改新数组截取 一部分数组
splice✅ 修改被删除的元素数组删除、替换、插入

什么时候用哪个?

  • 想获取数组的部分数据,但不改变原数组 → slice
  • 想删除/插入/替换元素并修改原数组 → splice

4. 扩展:如何用 splice 模拟 slice

虽然 splice 通常会修改原数组,但如果我们想要 splice返回值slice 一样,可以先复制一份数组:

let arr = [1, 2, 3, 4, 5];
let slicedArr = arr.slice(1, 3);
let splicedArr = arr.concat().splice(1, 2);console.log(slicedArr); // [2, 3] (slice 结果)
console.log(splicedArr); // [2, 3] (splice 结果)
console.log(arr); // [1, 2, 3, 4, 5] (原数组未变)

49.js数组去重的方法

JS 数组去重的常见方法

在 JavaScript 中,数组去重有多种方式,以下是几种常见的方法:


1. 使用 Set(最简洁高效)

Set 是 ES6 提供的一个数据结构,天然去重。

const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1, 2, 3, 4, 5]
  • 优点:代码简洁,性能优秀,适用于基本数据类型(numberstring 等)。
  • 缺点:无法去重对象(引用类型)。

2. 使用 filter + indexOf

const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index);
console.log(uniqueArr); // [1, 2, 3, 4, 5]
  • 原理indexOf(item) 返回当前元素首次出现的索引,只有首次出现的位置和当前索引相等时才保留。
  • 缺点:对于大数组,性能不如 Set,因为 indexOfO(n),导致整体 O(n²) 复杂度。

3. 使用 reduce + includes

const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.reduce((acc, cur) => {if (!acc.includes(cur)) acc.push(cur);return acc;
}, []);
console.log(uniqueArr); // [1, 2, 3, 4, 5]
  • 原理:遍历数组,若 acc(累积数组)中没有当前值,则添加进去。
  • 缺点:内部使用 includes 进行查找,性能比 Set 略低。

4. 使用 Map(适用于对象去重)

如果数组包含对象,Set 不能去重,可以用 Map

const arr = [{ id: 1, name: "Alice" },{ id: 2, name: "Bob" },{ id: 1, name: "Alice" }
];const uniqueArr = [...new Map(arr.map(item => [item.id, item])).values()];
console.log(uniqueArr);
// [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
  • 原理Mapid 作为键,后面的相同 id 覆盖前面的,最终去重。

5. 使用 sort + for(适用于已排序数组)

如果数组是 有序的,可以使用 sort() + 遍历:

const arr = [1, 1, 2, 3, 3, 4, 5, 5];
const uniqueArr = [];
for (let i = 0; i < arr.length; i++) {if (arr[i] !== arr[i + 1]) {uniqueArr.push(arr[i]);}
}
console.log(uniqueArr); // [1, 2, 3, 4, 5]
  • 缺点:需要先排序,不适用于未排序数组。

6. 使用 Lodash 库的 _.uniq()

如果项目中使用 Lodash,可以用 _.uniq()

const _ = require("lodash");
const arr = [1, 2, 2, 3, 4, 4, 5];
console.log(_.uniq(arr)); // [1, 2, 3, 4, 5]
  • 优点:库方法优化过,性能好。
  • 缺点:需要引入外部库。

7. 适用于复杂去重(对象+多字段)

如果对象去重的标准不只是 id,可以使用 JSON.stringify

const arr = [{ id: 1, name: "Alice" },{ id: 2, name: "Bob" },{ id: 1, name: "Alice" }
];const uniqueArr = arr.filter((item, index, self) =>index === self.findIndex(t => JSON.stringify(t) === JSON.stringify(item))
);
console.log(uniqueArr);
  • 优点:适用于多字段的对象去重。
  • 缺点:性能不高,JSON.stringify() 可能影响排序。

方法对比

方法可去重基本数据类型可去重对象是否修改原数组适用场景性能
Set数字、字符串⭐⭐⭐⭐⭐
filter + indexOf适用于小数组⭐⭐⭐
reduce + includes适用于小数组⭐⭐⭐
Map对象去重⭐⭐⭐⭐
sort + for已排序数组⭐⭐⭐
Lodash _.uniq()Lodash 用户⭐⭐⭐⭐⭐
JSON.stringify复杂对象去重⭐⭐

总结

  • 最推荐Set(简单高效,适用于基本数据类型)。
  • 对象去重Map(适用于 id 唯一的对象数组)。
  • 其他方法:按需选择,如 filterreduce 等。

50.js判断变量是否是数组的方法有哪些

在 JavaScript 中,可以用多种方法来判断变量是否为数组,下面是常见的几种方法:


1. Array.isArray(value)(推荐,最可靠)

Array.isArray() 是 ES5 引入的方法,专门用于判断变量是否是数组,推荐使用。

console.log(Array.isArray([]));        // true
console.log(Array.isArray({}));        // false
console.log(Array.isArray("hello"));   // false
console.log(Array.isArray(new Array())); // true

优点

  • 语义清晰,专门用于判断数组。
  • 兼容 iframewindow 等不同的执行环境。

2. instanceof Array

instanceof 运算符用于判断对象是否是某个构造函数的实例:

console.log([] instanceof Array);  // true
console.log({} instanceof Array);  // false
console.log(new Array() instanceof Array);  // true

缺点

  • 不同 windowiframe 可能判断失效,因为不同 iframe 可能有不同的 Array 构造函数:
    let iframe = document.createElement('iframe');
    document.body.appendChild(iframe);
    let iframeArray = window.frames[0].Array;
    let arr = new iframeArray();console.log(arr instanceof Array);  // false (不同 window 造成的问题)
    

3. Object.prototype.toString.call(value)

利用 Object.prototype.toString.call(value) 可以返回准确的数据类型:

console.log(Object.prototype.toString.call([]));      // "[object Array]"
console.log(Object.prototype.toString.call({}));      // "[object Object]"
console.log(Object.prototype.toString.call("hello")); // "[object String]"

可以封装成一个函数:

function isArray(value) {return Object.prototype.toString.call(value) === "[object Array]";
}

优点

  • 适用于任何数据类型,不会被 iframe 问题影响。

4. constructor 判断

可以通过 constructor 检查 Array

console.log([].constructor === Array);  // true
console.log({}.constructor === Array);  // false
console.log(new Array().constructor === Array);  // true

缺点

  • constructor 可能被修改:
    let arr = [];
    arr.constructor = Object;
    console.log(arr.constructor === Array); // false
    

5. typeof(❌ 不适用于数组)

console.log(typeof []);  // "object"
console.log(typeof {});  // "object"

问题

  • typeof 无法区分数组和普通对象,不推荐用于判断数组。

对比总结

方法兼容性是否受 iframe 影响是否可靠推荐指数
Array.isArray(value)ES5+❌ 不受影响✅ 最推荐⭐⭐⭐⭐⭐
instanceof ArrayES3+✅ 受 iframe 影响❌ 可能失效⭐⭐⭐
Object.prototype.toString.call(value)ES3+❌ 不受影响✅ 可靠⭐⭐⭐⭐⭐
constructorES3+✅ 受影响❌ 可被修改⭐⭐
typeofES3+❌ 不受影响❌ 无法区分数组和对象

最佳实践(推荐)

在项目中,最推荐 这两种方法:

  1. 首选:Array.isArray(value)
  2. 备选:Object.prototype.toString.call(value) === "[object Array]"

51.js找出多维数组中的最大值

在 JavaScript 中,要找出 多维数组(嵌套数组)中的最大值,可以使用递归、flat(Infinity) 或者 reduce 来实现。下面介绍几种方法:


方法 1:递归查找(适用于任意深度)

如果数组是 不规则的多维数组,递归是最通用的方法:

function findMax(arr) {let max = -Infinity;for (let item of arr) {if (Array.isArray(item)) {max = Math.max(max, findMax(item)); // 递归处理子数组} else {max = Math.max(max, item); // 处理当前数值}}return max;
}const arr = [1, [2, [3, 8, [10]], 5], 4, [7, 6]];
console.log(findMax(arr)); // 10

优点

  • 适用于不规则的嵌套数组
  • 递归实现,逻辑清晰。

方法 2:使用 flat(Infinity) + Math.max(最简洁,适用于规则数组)

如果数组是 规则的多维数组(例如 [[1, 2], [3, 4]]),可以用 flat(Infinity) 展开:

const arr = [1, [2, [3, 8, [10]], 5], 4, [7, 6]];
const max = Math.max(...arr.flat(Infinity));
console.log(max); // 10

优点

  • 代码简洁,一行搞定
  • 适用于规则的嵌套数组

缺点

  • flat(Infinity) 可能影响性能,如果数组特别大,不建议使用。
  • 如果数组中包含非数值元素(如 nullundefined),需要预处理。

方法 3:reduce + 递归(适用于不规则数组)

使用 reduce 进行递归处理:

function findMax(arr) {return arr.reduce((max, item) => Array.isArray(item) ? Math.max(max, findMax(item)) : Math.max(max, item),-Infinity);
}const arr = [1, [2, [3, 8, [10]], 5], 4, [7, 6]];
console.log(findMax(arr)); // 10

优点

  • 递归 + reduce 实现,代码更函数式

方法 4:使用 JSON.stringify()(不推荐,仅供参考)

可以将数组转换为字符串,然后用正则匹配所有数值:

const arr = [1, [2, [3, 8, [10]], 5], 4, [7, 6]];
const max = Math.max(...JSON.stringify(arr).match(/-?\d+/g).map(Number));
console.log(max); // 10

缺点

  • 性能差,需要解析 JSON 并匹配正则,不适用于大数组。
  • 无法处理非数值元素(如 nullundefined)。

方法对比

方法适用情况代码简洁性性能
递归适用于不规则多维数组一般⭐⭐⭐⭐
flat(Infinity) + Math.max适用于规则多维数组最简洁⭐⭐⭐
reduce + 递归适用于所有多维数组结构清晰⭐⭐⭐⭐
JSON.stringify() + 正则仅适用于简单数组不推荐

结论(最佳实践)

  • 如果数组是规则的推荐 Math.max(...arr.flat(Infinity))(简单高效)。
  • 如果数组是嵌套的(不规则)推荐 递归 方法 findMax(arr)(通用性最强)。
  • 如果你喜欢函数式编程,可以使用 reduce + 递归。

52.js new操作符做了什事情

new 操作符在 JavaScript 中用于创建一个实例对象,它会执行以下四个步骤:

1. 创建一个新的空对象

首先,new 操作符会创建一个新的空对象,这个对象会继承构造函数的 prototype

let obj = {}; // 创建一个空对象

2. 让新对象的 __proto__ 指向构造函数的 prototype

新对象会继承构造函数的 prototype 属性:

obj.__proto__ = Constructor.prototype;

这意味着新对象可以访问构造函数的原型方法


3. 执行构造函数,并绑定 this 到新对象

使用 call 方式调用构造函数,并将 this 绑定到新对象:

let result = Constructor.call(obj, ...args); // 传递参数,执行构造函数

如果构造函数返回的是一个对象(非 null),那么 new 操作符最终返回该对象。否则,返回新创建的对象。


4. 返回新对象

如果构造函数显式返回一个对象new 操作符会返回该对象,否则返回新创建的实例:

return typeof result === "object" && result !== null ? result : obj;

完整示例

function Person(name, age) {this.name = name;this.age = age;
}const p = new Person("Alice", 25);
console.log(p.name); // "Alice"
console.log(p.age);  // 25

这里 p 继承了 Person.prototype,是 Person 的实例。


手写实现 new

function myNew(constructor, ...args) {// 1. 创建一个新的空对象let obj = Object.create(constructor.prototype);// 2. 绑定 this 并执行构造函数let result = constructor.apply(obj, args);// 3. 如果构造函数返回一个对象,则返回该对象,否则返回新创建的对象return result instanceof Object ? result : obj;
}// 测试
function Person(name) {this.name = name;
}const p = myNew(Person, "Bob");
console.log(p.name); // "Bob"

总结

new 操作符的核心作用:

  1. 创建一个新对象
  2. 链接原型,让新对象的 __proto__ 指向构造函数的 prototype
  3. 执行构造函数,并将 this 绑定到新对象。
  4. 返回新对象(如果构造函数返回的是对象,则返回该对象)。

53. js找出字符串中出现最多的字符及其出现次数

要找出字符串中出现最多的字符及其出现次数,可以使用 MapObject 进行统计。


方法 1:使用 Map 统计字符频率

function findMostFrequentChar(str) {let charMap = new Map();let maxChar = '';let maxCount = 0;// 统计字符出现次数for (let char of str) {charMap.set(char, (charMap.get(char) || 0) + 1);// 更新最大值if (charMap.get(char) > maxCount) {maxCount = charMap.get(char);maxChar = char;}}return { maxChar, maxCount };
}// 测试
const str = "abcaabbcccccddd";
console.log(findMostFrequentChar(str)); // { maxChar: 'c', maxCount: 5 }

优点

  • Map 具有 O(1) 读写效率,性能好。
  • 代码清晰,易于理解。

方法 2:使用 Object 统计

function findMostFrequentChar(str) {let charCount = {};let maxChar = '';let maxCount = 0;for (let char of str) {charCount[char] = (charCount[char] || 0) + 1;if (charCount[char] > maxCount) {maxCount = charCount[char];maxChar = char;}}return { maxChar, maxCount };
}console.log(findMostFrequentChar("abcaabbcccccddd")); // { maxChar: 'c', maxCount: 5 }

优点

  • 适用于简单场景,兼容性好。

方法 3:使用 reduce(函数式写法)

function findMostFrequentChar(str) {let charCount = [...str].reduce((acc, char) => {acc[char] = (acc[char] || 0) + 1;return acc;}, {});let maxChar = Object.keys(charCount).reduce((a, b) =>charCount[a] >= charCount[b] ? a : b);return { maxChar, maxCount: charCount[maxChar] };
}console.log(findMostFrequentChar("abcaabbcccccddd")); // { maxChar: 'c', maxCount: 5 }

优点

  • 代码更简洁,符合函数式编程思想。

方法对比

方法代码简洁性性能适用场景
Map 统计⭐⭐⭐O(n) 高效适用于所有情况,推荐
Object 统计⭐⭐O(n)适用于普通字符串
reduce⭐⭐⭐⭐O(n)代码简洁,适合喜欢函数式编程的开发者

结论(最佳实践)

  • 如果想要高效且直观的方案,推荐 Map 方案。
  • 如果想要代码最短,可用 reduce
  • Object 适用于简单情况,但 Map 在大数据量时更优。

54. 给字符串新增方法实现功能

要在 JavaScript 的 String 原型(prototype) 上定义 addPrefix 方法,使其能够为字符串添加前缀,可以这样实现:


解法:扩展 String.prototype

String.prototype.addPrefix = function(str) {return str + this;
};// 测试
console.log("world".addPrefix("hello")); // 输出:"helloworld"

解释

  1. 扩展 String.prototype

    • 我们在 String.prototype 上定义了 addPrefix 方法,使所有字符串都可以调用它。
  2. 使用 this 关键字

    • String.prototype 方法内部,this 指的是当前字符串(即 "world")。
    • str + this 连接 str(前缀)和 this(原字符串)。

注意事项

  1. 不推荐修改 String.prototype
    • 这样做可能会影响全局 String 对象,引发兼容性问题
    • 生产环境通常使用独立函数替代:
    function addPrefix(str, prefix) {return prefix + str;
    }console.log(addPrefix("world", "hello")); // "helloworld"
    
  2. 可使用 ES6 Class 实现(如果想避免修改原型):
    class CustomString {constructor(str) {this.str = str;}addPrefix(prefix) {return prefix + this.str;}
    }let myStr = new CustomString("world");
    console.log(myStr.addPrefix("hello")); // "helloworld"
    

总结

面试推荐解法

  • 定义 String.prototype.addPrefix(简单易懂,适合面试展示)
  • 避免污染原型,可使用独立函数或类

55.sort背后的原理是什么

Array.prototype.sort() 的背后原理主要涉及 排序算法稳定性,不同的 JavaScript 引擎可能采用不同的排序实现。下面是详细解析:


1. V8 引擎中的 sort 实现

V8(Chrome 和 Node.js 使用的 JavaScript 引擎)对 sort() 进行了优化,主要采用 双轴快速排序(Dual-Pivot Quicksort)插入排序(Insertion Sort),不同情况下采用不同算法:

  • 数组长度 ≤ 10 时,使用 插入排序
  • 数组长度 > 10 时,使用 双轴快速排序
  • 特殊情况(如大量相等元素) 时,可能改用 归并排序(TimSort)

2. 排序算法的核心思想

(1)双轴快速排序(Dual-Pivot Quicksort)

V8 在 sort() 里使用 双轴快排,它是 改进版的快速排序,相比传统单轴快排效率更高:

  • 选取 两个基准值(pivot1, pivot2)
  • 将数组划分成 三个区域
    • 小于 pivot1
    • 介于 pivot1pivot2 之间
    • 大于 pivot2
  • 递归对子区间进行排序

特点:

  • 平均时间复杂度:O(n log n)
  • 最坏时间复杂度:O(n²)(若选取的 pivot 较差)
  • 空间复杂度:O(log n)
  • 不稳定排序(即相同值的元素排序后相对顺序可能变)
(2)插入排序(Insertion Sort)

适用于小数组:

  • 依次遍历数组元素,将当前元素插入到前面已经排好序的部分
  • 时间复杂度 O(n²)
  • 稳定排序

3. sort() 的稳定性

V8 的 sort() 默认是不稳定的,因为 双轴快排是不稳定排序,但在某些情况下(如大量相等元素时)可能会使用 TimSort,它是 稳定排序


4. sort 使用时的注意事项

(1)默认按 Unicode 码点排序

如果 sort() 不提供 比较函数,它会把元素 转换为字符串,然后按 Unicode 码点 排序:

const arr = [10, 2, 5, 30];
console.log(arr.sort()); // [10, 2, 30, 5] (按字符串 "10"、"2"、"30"、"5" 进行排序)

所以对于数字排序,必须提供比较函数

console.log(arr.sort((a, b) => a - b)); // [2, 5, 10, 30]
(2)如果比较函数返回值不规范

sort((a, b) => a - b) 里的回调函数必须返回:

  • 负数(a 排在 b 之前)
  • 正数(b 排在 a 之前)
  • 0(ab 位置不变)

如果返回非数字值,可能会导致 排序行为不确定


5. 如何手写一个 sort

这里用 快排 手写 sort()

function quickSort(arr) {if (arr.length <= 1) return arr;const pivot = arr[Math.floor(arr.length / 2)];const left = arr.filter(x => x < pivot);const middle = arr.filter(x => x === pivot);const right = arr.filter(x => x > pivot);return [...quickSort(left), ...middle, ...quickSort(right)];
}console.log(quickSort([10, 2, 5, 30])); // [2, 5, 10, 30]

6. 总结

  1. sort() 在 V8 引擎中采用 双轴快速排序(大数组)+ 插入排序(小数组)。
  2. sort() 默认按 字符串 Unicode 码点 排序,排序数字时需提供 比较函数
  3. V8 的 sort() 不稳定(快排不稳定)。
  4. 不同 JS 引擎可能采用不同排序算法(如 TimSort)。
  5. 手写 sort() 一般用 快排归并排序堆排序

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com