JS常见问题

发布于:2025-08-02 ⋅ 阅读:(19) ⋅ 点赞:(0)

​​1.解释JavaScript中的数据类型​​

  • 基本类型:String、Number、Boolean、Null、Undefined、Symbol、BigInt
  • 引用类型:Object(包括Array、Function、Date等)

2.let、const和var的区别​

特性 var let const
作用域 函数作用域 块级作用域 块级作用域
变量提升 提升且初始化为 undefined 提升但未初始化(TDZ) 提升但未初始化(TDZ)
重复声明 允许 不允许 不允许
重新赋值 允许 允许 不允许(常量必须初始化)
暂时性死区
(1)函数作用域(var)
变量仅在声明它的函数内部有效:
function test() {
  var x = 10;
  if (true) {
    var x = 20; // 同一个变量!
  }
  console.log(x); // 20(if块内修改影响了外部)
}
(2)块级作用域(let/const)
变量仅在声明它的 {} 块内有效:
function test() {
  let x = 10;
  if (true) {
    let x = 20; // 不同的变量
  }
  console.log(x); // 10(if块内的x不影响外部)
}
(3)变量提升(Hoisting)
var 的变量提升
声明会被提升到作用域顶部,并初始化为 undefined:
console.log(a); // undefined(不会报错)
var a = 10;
let/const 的变量提升
声明会提升,但不会被初始化(处于 TDZ):
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
(4)暂时性死区(TDZ, Temporal Dead Zone)
在变量声明之前访问 let/const 会触发 TDZ 错误:
javascript
// TDZ 开始
console.log(c); // ReferenceError
// TDZ 结束
const c = 30;
(5)重新赋值与声明
const 必须初始化且不能重新赋值:
const PI = 3.14;
PI = 3.1415; // TypeError: Assignment to constant variable
但对象属性可修改(因为对象引用不变):
const obj = { name: "Alice" };
obj.name = "Bob"; // 允许

let 允许重新赋值:

let count = 1;
count = 2; // 允许

var 允许重复声明(易导致 bug):

var name = "Alice";
var name = "Bob"; // 不报错

3、什么是闭包?举例说明​

闭包 是指 函数能够访问并记住其词法作用域(lexical scope)外的变量。简单来说,闭包让函数可以“记住”它被创建时的环境
(1)闭包的核心特点
函数嵌套函数:外部函数包裹内部函数。
内部函数引用外部变量:内部函数使用了外部函数的变量。
外部函数执行后,变量仍被保留:即使外部函数已经执行完毕,内部函数仍能访问其变量。
(2)闭包的经典示例
示例 1:计数器(利用闭包保存状态)
function createCounter() {
  let count = 0; // 外部函数的变量

  return function() {
    count++; // 内部函数引用外部变量
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

// createCounter() 执行后,返回一个内部函数。
// 内部函数 counter 仍然可以访问 count,即使 createCounter() 已经执行完毕。
// count 不会被垃圾回收,因为闭包保留了它的引用。
示例 2:循环中的闭包(经典面试题)
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 输出 3, 3, 3
  }, 1000);
}
// var 是函数作用域,i 在整个循环中共享同一个变量。
// setTimeout 回调执行时,循环已经结束,i 的值是 3。
// 解决方案(使用let):
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 输出 0, 1, 2
  }, 1000);
}
// let 具有 块级作用域,每次循环都会创建一个新的变量绑定,而 var 是函数作用域,导致循环共享同一个变量。
// 解决方案(使用闭包):
for (var i = 0; i < 3; i++) {
  (function(j) { // 立即执行函数(IIFE)创建闭包
    setTimeout(function() {
      console.log(j); // 输出 0, 1, 2
    }, 1000);
  })(i);
}
// 每次循环时,i 的值被传给 j,j 被闭包保留,因此 setTimeout 回调访问的是不同的 j。
(3)注意事项
避免滥用闭包,防止内存泄漏。
在需要数据封装或状态管理时优先考虑闭包。

4、解释事件循环(Event Loop)​

事件循环是 JavaScript 处理异步操作的底层机制,它决定了代码的执行顺序,使得单线程的 JavaScript 能够非阻塞地运行。		
(1)为什么需要事件循环?
JavaScript 是单线程语言,但需要处理异步任务(如网络请求、定时器、用户交互)。事件循环通过 任务队列 和 回调机制 实现非阻塞执行。
(2)事件循环的核心组成
组成部分 作用
调用栈(Call Stack) 同步代码的执行栈,遵循“后进先出”(LIFO)原则。
任务队列(Task Queue) 存放异步任务的回调函数(如 setTimeout、DOM 事件)。
微任务队列(Microtask Queue) 存放优先级更高的异步回调(如 Promise.then、MutationObserver)。
事件循环线程 持续检查调用栈是否为空,然后按规则调度任务。
(3)事件循环的工作流程

时间循环的工作流程
具体步骤:

同步代码:直接进入调用栈执行(如 console.log)。

微任务(高优先级):

    每当调用栈为空时,先清空所有微任务(包括执行微任务时新产生的微任务)。

    常见微任务:Promise.then、queueMicrotask、MutationObserver。

宏任务(低优先级):

    只有当微任务队列完全清空后,才会执行一个宏任务。

    常见宏任务:setTimeout、setInterval、DOM 事件、I/O 操作。

循环:重复上述过程。
(4)经典示例分析
示例 1:同步 + Promise + setTimeout
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
输出顺序:1432
解释:
    同步代码:14 直接输出。
    微任务(Promise.then):3 优先于宏任务执行。
    宏任务(setTimeout):2 最后执行。
示例 2:嵌套微任务
setTimeout(() => console.log("A"), 0); // 宏任务
Promise.resolve()
  .then(() => {
    console.log("B"); // 微任务1
    Promise.resolve().then(() => console.log("C")); // 微任务2(嵌套)
  })
  .then(() => console.log("D")); // 微任务3
console.log("E"); // 同步
输出顺序:EBCDA
执行过程:
    同步代码:E。
    清空微任务队列:
        执行第一个 then 输出 B,生成新的微任务 C。
        执行 C(微任务队列未空)。
        执行第二个 then 输出 D。
    执行宏任务:A
(5)总结
同步代码 > 微任务 > 宏任务。

每次调用栈清空后,必须彻底清空微任务队列。

一个事件循环周期只执行一个宏任务(然后回到微任务检查)。

5、解释this关键字的指向​

this 是 JavaScript 中一个动态绑定的关键字,它的指向 取决于函数的调用方式,而非定义位置。
(1)默认绑定(独立函数调用)
规则:在普通函数中(非严格模式),this 指向全局对象(浏览器中为 window,Node.js 中为 global);严格模式('use strict')下为 undefined。
function showThis() {
  console.log(this);
}
showThis(); // 浏览器中输出: window(非严格模式)
            // 严格模式下输出: undefined
(2) 隐式绑定(方法调用)
规则:当函数作为对象的方法调用时,this 指向 调用该方法的对象。
const user = {
  name: "Alice",
  greet() {
    console.log(`Hello, ${this.name}!`);
  }
};
user.greet(); // 输出: "Hello, Alice!"(this → user)

易错点:方法赋值后丢失 this

const greet = user.greet;
greet(); // 输出: "Hello, undefined!"(this → window,因为独立调用)
(3) 显式绑定(call/apply/bind)
规则:通过 call、apply 或 bind 强制指定 this 的指向。

方法 作用 示例
call 立即调用函数,指定 this 和参数列表 func.call(obj, arg1, arg2)
apply 类似 call,参数以数组传递 func.apply(obj, [arg1, arg2])
bind 返回一个绑定 this 的新函数 const boundFunc = func.bind(obj)
javascript

function introduce(lang) {
  console.log(`${this.name} codes in ${lang}`);
}
const dev = { name: "Bob" };

introduce.call(dev, "JavaScript");  // 输出: "Bob codes in JavaScript"
introduce.apply(dev, ["Python"]);   // 输出: "Bob codes in Python"

const boundFunc = introduce.bind(dev);
boundFunc("Java");                  // 输出: "Bob codes in Java"
(4)new 绑定(构造函数调用)
规则:使用 new 调用构造函数时,this 指向 新创建的对象实例。
function Person(name) {
  this.name = name;
}
const alice = new Person("Alice");
console.log(alice.name); // 输出: "Alice"(this → alice 实例)
(5) 箭头函数的 this
规则:箭头函数没有自己的 this,它继承 外层作用域的 this(定义时决定,不可更改)。
const obj = {
  name: "Alice",
  regularFunc: function() {
    console.log(this.name); // this → obj
  },
  arrowFunc: () => {
    console.log(this.name); // this → 外层作用域(如 window)
  }
};

obj.regularFunc(); // 输出: "Alice"
obj.arrowFunc();   // 输出: undefined(假设外层为全局)

典型应用:解决回调函数 this 丢失

const timer = {
  count: 0,
  start() {
    setInterval(() => {
      this.count++; // this 继承自 start 方法(指向 timer)
      console.log(this.count);
    }, 1000);
  }
};

timer.start(); // 每秒输出递增的 count
(6) 特殊场景

(1)DOM 事件处理函数

规则:this 指向 触发事件的元素。
button.addEventListener("click", function() {
  console.log(this); // 输出: <button> 元素
});

(2)类(Class)中的 this

规则:类方法中的 this 指向实例,但单独提取方法时可能丢失绑定。

class User {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    console.log(`Hi, ${this.name}!`);
  }
}
const user = new User("Alice");
user.sayHi(); // 输出: "Hi, Alice!"
const sayHi = user.sayHi;
sayHi();      // 报错: Cannot read property 'name' of undefined
普通函数:谁调用,this 指向谁(无调用者则指向全局)。

箭头函数:this 是“透明的”,继承定义时的上下文。

强制绑定:用 call/apply/bind 手动控制。

6、如何避免全局变量污染

(1)使用模块化(ES Modules)
通过 import/export 隔离作用域,每个文件拥有独立作用域。
// utils.js
export function calculate() { /* ... */ }
// main.js
import { calculate } from './utils.js';
calculate(); // 无需全局变量
(2)立即执行函数(IIFE)
适用场景:传统非模块化项目快速隔离作用域。
(function() {
  const privateVar = "局部变量"; // 不会污染全局
  window.publicApi = { // 按需暴露少数全局接口
    init() { console.log(privateVar); }
  };
})();
publicApi.init(); // 可控的全局暴露
(3)命名空间模式
解决冲突:将多个变量封装到一个全局对象下。
// 统一全局入口
const MY_APP = {
  utils: {
    version: '1.0',
    getData() { /* ... */ }
  },
  config: { /* ... */ }
};
MY_APP.utils.getData(); // 替代全局函数 getData()
(4)块级作用域(let/const)
替代 var:用 let/const 限制变量作用域。
{
  const tempData = []; // 只在当前块有效
  // ...
}
console.log(tempData); // ReferenceError
(5)类封装(Class)
面向对象:通过类组织代码,避免暴露内部状态。
class DataService {
  #privateField = "私有"; // 私有字段(ES2022)
  fetch() {
    console.log(this.#privateField);
  }
}
const service = new DataService();
service.fetch(); // 不泄露私有变量
(6)闭包保护变量
隐藏私有数据:函数作用域保护变量不被外部访问。
function createCounter() {
  let count = 0; // 受闭包保护
  return {
    increment() { count++; },
    get() { return count; }
  };
}
const counter = createCounter();
counter.increment();
console.log(counter.get()); // 1
console.log(count); // ReferenceError
(7)严格模式(‘use strict’)
预防意外全局:禁止未声明的变量赋值。
'use strict';
accidentalGlobal = 10; // 抛出 ReferenceError

7、async/await的工作原理

async函数返回Promise
await暂停async函数执行,等待Promise解决

8、cookie、localStorage、sessionStorage区别

特性 Cookie LocalStorage SessionStorage
设计用途 客户端与服务端通信(如身份验证) 持久化存储客户端数据 临时存储会话级数据
存储容量 ≤ 4KB(单个域名下所有 Cookie 总和) 5MB~10MB per domain(浏览器差异) 同 LocalStorage
生命周期 可设置过期时间(默认会话级关闭失效) 永久存储,需手动清除 页面会话结束时自动清除(标签页关闭)
数据共享范围 同域名下所有窗口和标签页 同域名下所有窗口和标签页 仅当前标签页(同源页面可共享)
是否自动发送到服务端 每次请求自动携带在 HTTP Headers 仅客户端存储,不自动发送 同 LocalStorage
适用场景 用户登录状态、CSRF 令牌 用户偏好设置、缓存数据 表单草稿、页面间临时传递数据

9、跨域解决方案

当浏览器发起跨域请求时,同源策略(Same-Origin Policy)会阻止不同源(协议、域名、端口任一不同)的资源交互。
(1)服务端解决方案(推荐)
方案 原理 适用场景
CORS 服务端设置 Access-Control-Allow-Origin 等响应头 主流跨域方案
反向代理 通过同域代理服务器转发请求(如 Nginx) 前端无法修改服务端时
JSONP 利用 <script> 标签不受同源限制的特性 仅限GET请求(历史遗留方案)
① CORS(跨域资源共享)

服务端设置响应头:

// Node.js 示例(Express)

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*'); // 或指定域名如 'https://example.com'
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Credentials', 'true'); // 允许携带Cookie
  next();
});

前端请求:

fetch('https://api.example.com/data', {
  credentials: 'include' // 如需携带Cookie
});
② 反向代理(Nginx)

nginx

# nginx.conf
server {
  listen 80;
  server_name localhost;

  location /api {
    proxy_pass https://api.example.com; # 转发到目标服务器
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }
}

前端直接请求同域接口:

fetch('/api/data') // 实际代理到 https://api.example.com/data
③ JSONP(历史方案)

前端:

function handleResponse(data) {
  console.log(data);
}
const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleResponse';
document.body.appendChild(script);

服务端需返回JS代码:

// 返回格式:handleResponse({ "data": "xxx" })
(2)纯前端解决方案
方案 原理 缺点
WebSocket 全双工通信协议,不受同源限制 需服务端支持
postMessage window.postMessage 实现跨窗口通信 仅限特定窗口间通信
document.domain 强制修改为相同基础域名(如 a.example.com 和 b.example.com) 仅限同主域二级域名
① WebSocket(全双工通信)
原理:WebSocket协议不受同源策略限制,适合实时通信。
要求:目标服务器需支持WebSocket。

javascript

// 前端代码

const socket = new WebSocket('wss://api.example.com/socket');
socket.onopen = function() {
  socket.send(JSON.stringify({ action: 'subscribe' }));
};
socket.onmessage = function(event) {
  console.log('收到消息:', JSON.parse(event.data));
};
socket.onerror = function(error) {
  console.error('WebSocket错误:', error);
};
② postMessage(跨窗口通信)
原理:通过 window.postMessage 实现跨域窗口/iframe间通信。
场景:主站与嵌入式iframe或弹出窗口交互。

主页面代码

// 发送消息到子iframe(或弹出窗口)
const iframe = document.getElementById('my-iframe');
iframe.contentWindow.postMessage(
  { key: 'value' }, 
  'https://子页面域名.com'
);

// 接收子页面消息
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://子页面域名.com') return; // 安全检查
  console.log('收到子页面消息:', event.data);
});

子页面代码

// 接收主页面消息
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://主页面域名.com') return;
  console.log('收到主页面消息:', event.data);

  // 回复消息
  event.source.postMessage(
    { response: '数据已收到' },
    event.origin
  );
});

10、前端性能优化

(1)网络层优化
① 减少HTTP请求
合并CSS/JS文件(Webpack打包)
使用CSS Sprites雪碧图
内联关键CSS(Critical CSS)
② CDN加速
<!-- 使用CDN加载第三方库 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.37/dist/vue.global.min.js"></script>
③ 预加载关键资源
<link rel="preload" href="font.woff2" as="font" crossorigin>
(2)渲染优化
① 减少重排重绘
// 批量DOM操作
const fragment = document.createDocumentFragment();
items.forEach(item => fragment.appendChild(createElement(item)));
container.appendChild(fragment);
② 使用transform代替top/left
.box {
  transform: translateX(100px); /* 触发GPU加速 */
}
③ 防抖/节流
// Lodash节流
window.addEventListener('scroll', _.throttle(updatePosition, 100));
④ 虚拟滚动
<VirtualScroller :items="bigList" item-height="50px"/>
(3)JS执行优化
① 代码分割
// 动态导入
const module = await import('./heavy-module.js');
② Web Worker
// 主线程
const worker = new Worker('task.js');
worker.postMessage(data);
③ 避免内存泄漏
// 移除无用事件监听
window.removeEventListener('resize', handleResize);
(4)缓存策略
① 强缓存
// http
Cache-Control: max-age=31536000
② 协商缓存
// http
ETag: "33a64df5"
If-None-Match: "33a64df5"
③ Service Worker
// 缓存API响应
self.addEventListener('fetch', event => {
  event.respondWith(caches.match(event.request));
});

网站公告

今日签到

点亮在社区的每一天
去签到