2026-01-19-杂记 2026-01-19

文章发布时间:

最后更新时间:

页面浏览: 加载中...

闭包

函数+创建时的词法环境

当一个函数能够“记住并访问”它被创建时所处的词法作用域中的变量,即使这个函数在它原本的作用域之外被执行,这个函数就形成了闭包。

“闭包就是函数和其词法环境的组合”
“闭包就是能够读取其他函数内部变量的函数”

闭包是语言运行时的一种行为现象

1
2
3
4
5
6
7
8
9
10
11
12
// 计数器
function createCounter() {
let count = 0; // 处于外部函数作用域的变量
return function() { // 内部函数形成了闭包
return count++;
};
}

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

作用:封装,保存状态,延迟执行
判断:能否访问已销毁外部作用域的变量(基于函数定义位置)

闭包应用场景

  • 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包:
1
2
3
4
5
6
7
8
// 定时器
setTimeout(function handler(){
console.log('1');
},1000);
// 事件监听
document.getElementById(app).addEventListener('click', () => {
console.log('Event Listener');
});
  • 作为函数参数传递的形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这是闭包
fn();
}
foo(); // 输出2,而不是1
  • IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量:
1
2
3
4
var a = 2;
(function IIFE(){
console.log(a); // 输出2
})();

IIFE 是一种自执行匿名函数,这个匿名函数拥有独立的作用域。这不仅可以避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。

  • 结果缓存(备忘模式)

备忘模式就是应用闭包的特点的一个典型应用。比如下面函数:

1
2
3
function add(a) {
return a + 1;
}

当多次执行 add() 时,每次得到的结果都是重新计算得到的,如果是开销很大的计算操作的话就比较消耗性能了,这里可以对已经计算过的输入做一个缓存。所以这里可以利用闭包的特点来实现一个简单的缓存,在函数内部用一个对象存储输入的参数,如果下次再输入相同的参数,那就比较一下对象的属性,如果有缓存,就直接把值从这个对象里面取出来。实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function memorize(fn) {
var cache = {}
return function() {
var args = Array.prototype.slice.call(arguments)
var key = JSON.stringify(args)
return cache[key] || (cache[key] = fn.apply(fn, args))
}
}

function add(a) {
return a + 1
}

var adder = memorize(add)

adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(2) // 输出: 3 当前: cache: { '[1]': 2, '[2]': 3 }

使用 ES6 的方式实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function memorize(fn) {
const cache = {}
return function(...args) {
const key = JSON.stringify(args)
return cache[key] || (cache[key] = fn.apply(fn, args))
}
}

function add(a) {
return a + 1
}

const adder = memorize(add)

adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(2) // 输出: 3 当前: cache: { '[1]': 2, '[2]': 3 }

备忘函数中用 JSON.stringify 把传给 adder 函数的参数序列化成字符串,把它当做 cache 的索引,将 add 函数运行的结果当做索引的值传递给 cache,这样 adder 运行的时候如果传递的参数之前传递过,那么就返回缓存好的计算结果,不用再计算了,如果传递的参数没计算过,则计算并缓存 fn.apply(fn, args),再返回计算的结果。

循环输出问题

最后来看一个常见的和闭包相关的循环输出问题,代码如下:

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

这段代码输出的结果是 5 个 6,那为什么都是 6 呢?如何才能输出 1、2、3、4、5 呢?

可以结合以下两点来思考第一个问题:

  • setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。
  • 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。

那如何按顺序依次输出 1、2、3、4、5 呢?

1)利用 IIFE

可以利用 IIFE(立即执行函数),当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行,改造之后的代码如下。

1
2
3
4
5
6
7
for(var i = 1;i <= 5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}

可以看到,通过这样改造使用 IIFE(立即执行函数),可以实现序号的依次输出。利用立即执行函数的入参来缓存每一个循环中的 i 值。

2)使用 ES6 中的 let

ES6 中新增的 let 定义变量的方式,使得 ES6 之后 JS 发生革命性的变化,让 JS 有了块级作用域,代码的作用域以块级为单位进行执行。

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

可以看到,通过 let 定义变量的方式,重新定义 i 变量,则可以用最少的改动成本,解决该问题。

3)定时器第三个参数

setTimeout 作为经常使用的定时器,它是存在第三个参数的。我们经常使用前两个,一个是回调函数,另外一个是定时时间,setTimeout 从第三个入参位置开始往后,是可以传入无数个参数的。这些参数会作为回调函数的附加参数存在。那么结合第三个参数,调整完之后的代码如下:

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

可以看到,第三个参数的传递,可以改变 setTimeout 的执行逻辑,从而实现想要的结果


高阶函数(Higher-Order Function)

JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

一个最简单的高阶函数:

1
2
3
function add(x, y, f) {
return f(x) + f(y);
}

柯里化(Currying)

柯里化是将一个接受 多个参数 的函数,转换为 一系列只接受单个参数 的函数的变换过程。

f(a, b, c) === f(a)(b)(c)

把“一次传入多个参数” → 拆成“多次每次只传一个参数”的调用链。

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 add(a, b, c) {
return a + b + c;
}

add(1, 2, 3); // 6


// 柯里化版本
function addCurried(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}

addCurried(1)(2)(3); // 6


// 更常见的简写形式(箭头函数)
const addC = a => b => c => a + b + c;

addC(1)(2)(3); // 6

防抖(Debounce)

定义:当事件被触发后,延迟 n 秒再执行回调函数。如果在这 n 秒内事件再次被触发,则重新开始计时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 方式1:简单版(非立即执行)
function debounce(fn, wait) {
let timer = null;

return function (...args) {
// 每次触发都清除之前的定时器
if (timer) clearTimeout(timer);

// 重新设置定时器
timer = setTimeout(() => {
fn.apply(this, args);
}, wait);
};
}

// 方式2:立即执行版 + 尾部执行(更常用)
function debounce(fn, wait, immediate = false) {
let timer = null;

return function (...args) {
const context = this;

if (timer) clearTimeout(timer);

if (immediate) {
const callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, wait);

if (callNow) fn.apply(context, args);
} else {
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
}
};
}

**应用:

  • 搜索框输入自动搜索(用户输入完再发请求)
  • 表单验证(输入停止后才校验)
  • 按钮短时间内多次点击(防止重复提交)
  • window.resize 事件

节流(Throttle)

定义:规定时间内只执行一次函数,无论这段时间内触发多少次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 方式1:时间戳版(第一次触发会立即执行)
function throttle(fn, wait) {
let prev = 0;

return function (...args) {
const now = Date.now();

if (now - prev >= wait) {
fn.apply(this, args);
prev = now;
}
};
}

// 方式2:定时器版(最后一次一定会执行)
function throttle(fn, wait) {
let timer = null;
let lastArgs = null;

return function (...args) {
lastArgs = args;

if (!timer) {
fn.apply(this, lastArgs);
lastArgs = null;

timer = setTimeout(() => {
if (lastArgs) {
fn.apply(this, lastArgs);
}
timer = null;
lastArgs = null;
}, wait);
}
};
}

// 方式3:最推荐的综合版(头尾都能执行)
function throttle(fn, wait) {
let timer = null;
let previous = 0;

return function (...args) {
const now = Date.now();
const remaining = wait - (now - previous);

if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
previous = now;
} else if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
previous = Date.now();
timer = null;
}, remaining);
}
};
}

应用场景

  • 页面滚动到底部加载更多(无限滚动)
  • 拖拽元素时实时计算位置
  • 鼠标移动时画板实时绘制
  • 游戏中技能释放冷却(最典型的思想来源)

call、apply 和 bind

call、apply 和 bind 是 JavaScript 中 Function 对象提供的三个非常重要且经常被面试考察的方法,它们的主要作用都是改变函数执行时的 this 指向,但在用法、参数传递方式和执行时机上存在显著差异。

特性 call apply bind
主要功能 立即执行函数,并改变 this 指向 立即执行函数,并改变 this 指向 不立即执行,返回一个新函数,永久绑定 this
参数传递方式 逐个参数列出 参数以数组形式传递 逐个参数列出(与 call 相同)
返回值 函数执行后的返回值 函数执行后的返回值 新的绑定了 this 的函数
是否立即执行
参数是否可以后续补充 否(一次性传完) 否(一次性传完) 是(可以分两次传参)
典型使用场景 需要明确控制 this 并立即执行时 处理类数组对象/动态参数时 事件处理、固定 this 的回调函数
语法示例 fn.call(obj, 1, 2, 3) fn.apply(obj, [1, 2, 3]) const newFn = fn.bind(obj, 1, 2)
1
2
3
4
5
6
7
8
9
10
11
12
// 准备一个测试函数和对象
function greet(greeting, punctuation) {
console.log(`${greeting}, 我是${this.name}${punctuation}`);
}

const person = {
name: "张三"
};

const anotherPerson = {
name: "李四"
};

1. call()

立即执行,参数逐个传入

1
2
3
4
5
greet.call(person, "您好", "!");     
// 输出:您好,我是张三!

greet.call(anotherPerson, "早上好", "~");
// 输出:早上好,我是李四~

2. apply()

立即执行,参数以数组形式传入(非常适合处理 arguments 或类数组对象)

1
2
3
4
5
6
7
greet.apply(person, ["下午好", "。"]);   
// 输出:下午好,我是张三。

// 经典用法:求数组最大/最小值(以前常考,现在 Math.max(...arr) 更常用)
const numbers = [5, 2, 9, 1, 7];
console.log(Math.max.apply(null, numbers)); // 9
// 现代写法:Math.max(...numbers)

3. bind()

最重要的特点:不立即执行,而是返回一个永久绑定了 this 的新函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 绑定 this,并预设部分参数(函数柯里化常用)
const greetZhangSan = greet.bind(person, "你好");

greetZhangSan("!");
// 输出:你好,我是张三!

greetZhangSan("呀~");
// 输出:你好,我是张三呀~

// 事件处理中最常见的用法
class Button {
constructor(text) {
this.text = text;
this.element = document.createElement("button");
this.element.textContent = text;

// 错误写法:this.handleClick 中的 this 会指向 DOM 元素
// this.element.addEventListener("click", this.handleClick);

// 正确写法:
this.element.addEventListener("click", this.handleClick.bind(this));
}

handleClick() {
console.log(`按钮 ${this.text} 被点击了`);
}
}

场景:

  • 需要立刻执行 → 选 callapply
  • 处理数组/类数组参数 → 优先考虑 apply
  • 需要保存一个固定 this 的函数(最常见:事件回调、setTimeout、React class 组件等)→ 选 bind
  • 同时固定 this 又预设部分参数(函数柯里化)→ 也用 bind

IIFE

IIFE是 Immediately Invoked Function Expression 的缩写

JavaScript 中一种非常常见且重要的设计模式,主要用于创建独立的作用域并立即执行代码,同时避免污染全局命名空间

ES6前常用

1
2
3
4
5
(function() {
// 这里的所有变量都是局部变量
var privateVar = '这是一个私有变量';
console.log('立即执行!');
})(); // 最后的 () 负责立即调用

事件循环

事件循环是 JavaScript 单线程运行时用来处理异步任务的调度器,它通过“同步代码 → 微任务 → 一个宏任务 → 微任务 → 下一个宏任务……”的循环,不断地把异步回调安排到合适的时机执行,从而实现了看似并发的效果

组成部分 英文名称 主要职责 执行时机 示例任务来源
调用栈 Call Stack 存放当前正在执行的同步代码,按 LIFO(后进先出)执行 立即执行 函数调用、代码块
微任务队列 Microtask Queue 存放优先级最高的异步回调 当前宏任务结束后立即清空 Promise.then/catch/finally、queueMicrotask、MutationObserver
宏任务队列 Task Queue / Macrotask Queue 存放普通的异步回调 微任务队列清空后取一个执行 setTimeout、setInterval、setImmediate、I/O、UI rendering、requestAnimationFrame
Web APIs (浏览器提供) 处理真正的异步操作(定时器、网络请求、DOM事件等),完成后将回调放入队列 由浏览器/运行时独立线程管理 XMLHttpRequest、fetch、DOM事件监听
事件循环(Event Loop) Event Loop 不停询问:调用栈是否为空?→ 是的话先清空微任务 → 再取一个宏任务执行 持续运行

事件循环一次完整循环的执行顺序

  1. 执行当前调用栈中的所有同步代码(直到调用栈清空)
  2. 清空微任务队列(执行所有微任务,直到微任务队列为空)
    只要有微任务,就会一直执行完所有微任务
  3. 宏任务队列中取一个任务执行(注意:只取一个!)
  4. 执行完这个宏任务后 → 回到第 2 步,再次清空微任务队列
  5. 重复 3~4 步,直到所有任务处理完毕

代码示例与执行顺序演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log('1');

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

Promise.resolve().then(() => {
console.log('5');
Promise.resolve().then(() => console.log('6'));
});

console.log('7');

执行结果顺序

1
2
3
4
5
6
7
1
7
5
6
2
3
4
  1. 同步代码 → 打印 1、7
  2. 微任务队列有 then(5) → 执行 → 打印 5,并产生新的微任务 then(6)
  3. 继续清空微任务 → 执行 then(6) → 打印 6
  4. 微任务队列清空,执行第一个宏任务(setTimeout 0ms)→ 打印 2
  5. 宏任务执行中产生微任务 then(3) → 宏任务结束后立即清微任务 → 打印 3
  6. 微任务清空,继续下一个宏任务(setTimeout 里面的)→ 打印 4

序号 问题 回答
1 项目里用了泛型,怎么根据传入类型适配不同处理函数 通常用泛型约束 + 映射类型 + 条件类型 + infer 实现函数重载效果 最常见两种做法: 1. 传入对象类型 → 用 keyof T 约束字段名 2. 传入联合类型 → 用条件类型分发实现不同分支
2 泛型是什么 泛型(Generics) 是 TypeScript(以及许多现代编程语言)中一种强大的类型系统特性,其核心目的是允许开发者编写可复用、类型安全的代码,同时让代码在不同具体类型下都能保持正确的类型推断和约束。

简单来说,泛型就是“类型参数化”:把类型当作参数一样传入,让同一个函数、类、接口能够处理多种类型的数据,而无需为每种类型都重复编写代码。
3 interface 和 type 的区别 现在差别已经很小,主要记住这几点: • interface 可多次声明自动合并(对库/模块声明补丁最有用) • type 可做联合、交叉、映射、条件、模板字符串等复杂类型操作 • interface 性能略好(声明合并时) • 目前绝大多数团队:能用 type 就用 type,除非明确需要声明合并
4 keyof、in、typeof、infer 实际工作中怎么用 - keyof:取对象所有键名(表单字段校验、权限映射最常用) - in:映射类型必备(生成只读、部分、可选等类型) - typeof:获取的类型(尤其是从 const 断言对象取类型) - infer:在条件类型中推断出某一部分类型(最强大,常用于提取 Promise 返回值、数组元素类型、函数返回值等)
5 封装表单组件,要求字段名必须是后端返回字段的一部分,怎么处理 最推荐做法(2024~2026 主流): ts
type ApiResponse = { id: number; name: string; age: number; email?: string }

type FormField = keyof T \
${keyof T}.${string}

function Form>(props: { fields: FormField[] }) { … }
进阶:用 const assertion + typeof 做更严格的路径类型
6 回流(reflow)和重绘(repaint)的区别 回流(Layout/Reflow):几何属性/布局变化 → 影响元素位置/大小 → 必须重新计算布局 重绘(Repaint):外观变化(颜色、visibility、阴影等)→ 不影响布局 回流代价远大于重绘(通常是重绘的几倍到几十倍)
7 浏览器加载一个页面时,整个渲染流程(关键节点) 1. DNS → TCP → TLS 2. 请求 HTML 3. 构建 DOM + CSSOM 4. 合成 Render Tree 5. Layout(回流) 6. Paint(重绘) 7. Composite(图层合成) 重要:Parse → Style → Layout → Paint → Composite
8 做骨架屏时,怎么判断骨架内容和真实内容之间的切换时机 推荐组合方式(从激进到保守): 1. 最激进:第一个/几个关键接口请求完成就切 2. 常用:所有必须数据接口都 resolve 后切(Promise.all) 3. 保守:主图/首屏关键资源加载完成(img onload + 定时兜底) 4. 极致体验:分层渐进式替换(文字先换、图片后换)
9 移动端怎么做适配(2025~2026 主流方案) 当前最主流组合: 1. postcss-px-to-viewport-8-plugin / postcss-px-to-rem + tailwindcss 2. 设计稿 375px → vw 方案(750 设计稿用 100vw = 750px) 3. 大屏/横屏特殊处理:100vh polyfill + env(safe-area-inset-*) 4. 字体:clamp() + rem(根字体 100px 常见)
10 async 函数在执行栈中的位置是怎么样的 async 函数本身是同步执行到第一个 await 第一个 await 之后 → 后续代码作为微任务放入微任务队列 所以:async 函数 = 同步前半段 + 多个微任务
11 Promise.all 和 Promise.allSettled 的区别 Promise.all:有一个 reject 就整体 reject,结果顺序严格对应 Promise.allSettled:永远 resolve,返回每个 promise 的 {status, value/reason},永远不会 reject
12 JS 是单线程,为什么可以实现并发加载多个接口? JS 单线程指的是执行 JavaScript 代码的线程只有一个 真正的网络请求、定时器、DOM事件等都由浏览器底层多线程完成 JS 只负责注册回调,回调通过事件循环被调度执行 → 所以宏观上是并发
13 setTimeout 和 requestAnimationFrame 哪个更精确 requestAnimationFrame 更精确(与浏览器刷新率同步,通常 16.7ms/60fps) setTimeout 实际延迟往往偏长(最低 4ms + 事件循环排队延迟) 动画/滚动相关优先使用 rAF
14 帧率监控你们是怎么做的 主流几种方式(按精度/成本排序): 1. requestAnimationFrame 循环 + performance.now() 差值(最常用) 2. PerformanceObserver + ‘longtask’(监控长任务) 3. Web Vitals(CLS、FID、LCP) 4. 第三方:sentry、ddtrace、阿里云 ARMS 等
15 小程序渲染卡顿时怎么优化(微信/支付宝小程序) 优先级排序(效果最明显→一般): 1. setData 合并 + 减少次数(一次 setData 尽量多字段) 2. 使用 block wx:if 代替大量 wx:if 3. 列表用 virtual-list / recycle-view 4. 图片用 webp + lazy-load 5. 避免在循环里写 setData 6. 自定义组件纯数据组件
16 H5 页面嵌入小游戏时,遇到过页面内存持续上涨吗?怎么解决的? 非常常见,尤其 canvas + webgl 小游戏 常见原因: • 事件监听未移除 • canvas 未清理(ctx.clearRect 不够) • 纹理/缓冲区未释放 • 闭包引用大对象 • requestAnimationFrame 未取消 解决办法: 1. 组件卸载时务必 cancelAnimationFrame 2. 清理纹理:gl.deleteTexture/gl.deleteBuffer 3. 用 WeakMap/WeakSet 存引用 4. 定时检查 performance.memory(chrome devtools) 5. 必要时主动销毁整个 canvas 元素再重建

JS中异步的概念是什么?

JavaScript异步是指代码的执行不必等待某个操作完成,可以继续执行后续代码。当异步操作完成后,通过回调函数、Promise或async/await来处理结果。这是为了解决JS单线程可能导致的阻塞问题。

核心机制:事件循环 + 回调队列

  • 同步任务在主线程执行
  • 异步任务交由Web APIs处理
  • 完成后的回调进入任务队列
  • 事件循环从队列中取出回调执行

为什么JS是单线程还需要事件循环?

看似矛盾实则巧妙的设计

  1. 单线程的必然性:避免多线程的复杂性(锁、竞态条件)
  2. 事件循环的价值
    • 处理I/O密集型操作(网络、文件)
    • 维持UI响应性
    • 管理定时器、用户交互
    • 实现”非阻塞”的并发

简单比喻:就像餐厅里只有一个服务员(单线程),但通过叫号系统(事件循环)可以高效服务多桌客人。

栈内存和堆内存的差异?

特性 栈内存 堆内存
存储内容 基本类型、引用地址 引用类型(对象、数组)
分配方式 自动分配/释放 动态分配,GC回收
大小 固定,较小 动态,较大
访问速度
管理 系统自动管理 开发者/GC管理
示例 let a = 1 let obj = {x: 1}

为什么数组要存在堆内存而不是栈内存?

根本原因

  1. 大小不确定:数组长度动态变化,栈内存固定大小无法满足
  2. 性能考虑:复制大数组到栈中成本高
  3. 内存管理:堆内存可动态扩展,GC负责回收
  4. 引用语义:多个变量可共享同一数组

JS中有哪些基本数据类型?

7种基本类型

  1. String
  2. Number
  3. Boolean
  4. Undefined
  5. Null
  6. Symbol(ES6)
  7. BigInt(ES2020)

1种引用类型:Object(包括Array、Function、Date等)

函数内部的this含义是什么?

this的指向规则

  1. 默认绑定:独立函数调用 → window/undefined(严格模式)
  2. 隐式绑定:作为对象方法调用 → 该对象
  3. 显式绑定:call/apply/bind → 指定的对象
  4. new绑定:构造函数调用 → 新创建的对象
  5. 箭头函数:无自己的this,继承外层

call、apply、bind的区别?

方法 参数传递 立即执行 返回
call 参数列表 函数结果
apply 数组 函数结果
bind 参数列表 新函数
1
2
3
func.call(thisArg, arg1, arg2);
func.apply(thisArg, [arg1, arg2]);
const newFunc = func.bind(thisArg, arg1, arg2);

TypeScript联合类型和交叉类型的概念?

联合类型 (Union Types)|表示”或”

1
2
type Status = "success" | "error" | "loading";
let value: string | number;

交叉类型 (Intersection Types)&表示”且”

1
2
3
4
5
6
7
interface A {
x: number;
}
interface B {
y: string;
}
type C = A & B; // { x: number, y: string }

区别

  • 联合:可接受任一类型
  • 交叉:必须同时满足所有类型

二、浏览器/网络类

什么是同源策略?

同源:协议、域名、端口完全相同

限制内容

  • DOM访问
  • Cookie/LocalStorage访问
  • AJAX请求
  • 脚本执行

目的:防止恶意网站窃取用户数据

如何实现跨域请求?

常用方案

  1. CORS:服务器设置响应头
  2. JSONP:利用<script>标签
  3. 代理服务器:同源服务器转发
  4. WebSocket:不受同源限制
  5. postMessage:窗口间通信
  6. nginx反向代理:服务器端代理

为什么代理可以绕过同源限制?

核心原理

  1. 同源限制是浏览器的安全策略,不是服务器的
  2. 代理服务器与页面同源,不受限制
  3. 服务器间无同源限制,可自由通信

流程

1
浏览器(同源) → 代理服务器(同源) → 目标服务器(不同源)

Cookie和Session的定义和差异?

特性 Cookie Session
存储位置 客户端 服务器端
安全性 较低(可篡改) 较高
存储大小 4KB左右 更大
生命周期 可设置过期时间 依赖服务器配置
跨域 可设置domain 依赖Cookie或token

Cookie是怎么种下的?

两种方式

  1. HTTP响应头Set-Cookie: name=value; options
  2. JavaScript设置document.cookie = "name=value; options"

关键属性

  • HttpOnly:禁止JS访问
  • Secure:仅HTTPS传输
  • SameSite:限制跨站发送
  • Domain/Path:作用域

LocalStorage和SessionStorage的区别?

特性 LocalStorage SessionStorage
生命周期 永久,需手动清除 标签页关闭即清除
作用域 同源窗口共享 仅当前标签页
存储大小 5-10MB 5-10MB
数据同步 同源窗口实时同步 仅当前标签页

HTTP强缓存和协商缓存的区别?

强缓存

  • 响应头:Cache-ControlExpires
  • 状态码:200 (from cache)
  • 过程:不请求服务器,直接使用缓存

协商缓存

  • 响应头:ETag/If-None-MatchLast-Modified/If-Modified-Since
  • 状态码:304 (Not Modified)
  • 过程:请求服务器,验证缓存有效性

301和302状态码的区别?

301 Moved Permanently

  • 永久重定向
  • 搜索引擎更新索引
  • 浏览器缓存新地址
  • 权重传递

302 Found (临时重定向)

  • 临时重定向
  • 搜索引擎不更新
  • 浏览器不缓存
  • 权重不传递

实际使用

  • 网站改版用301
  • 登录跳转用302
  • 移动端适配用302

三、CSS类

1. 设备像素和逻辑像素的区别?

设备像素 (Physical Pixel)

  • 物理像素,屏幕实际发光点
  • 固定不变
  • 高DPI设备像素更密集

逻辑像素 (CSS Pixel)

  • 编程使用的像素单位
  • 与实际像素有比例关系
  • devicePixelRatio决定

关系物理像素 = 逻辑像素 × devicePixelRatio

2. CSS两种盒模型的差异

标准盒模型 (content-box)

  • width/height= 内容宽度
  • 总宽度 = width + padding + border + margin

IE盒模型 (border-box)

  • width/height= 内容 + padding + border
  • 总宽度 = width + margin

设置方式

1
2
box-sizing: content-box; /* 默认 */
box-sizing: border-box; /* 常用 */