JavaScript is required

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

笔记#JavaScript#闭包#作用域#前端

闭包

函数+创建时的词法环境

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

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

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

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

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

  • 结果缓存(备忘模式)

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

ts
function add(a) {
    return a + 1;
}

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

ts
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 的方式实现:

ts
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),再返回计算的结果。

循环输出问题

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

ts
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 传递到定时器中,然后执行,改造之后的代码如下。

ts
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 有了块级作用域,代码的作用域以块级为单位进行执行。

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

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

3)定时器第三个参数

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

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

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



高阶函数(Higher-Order Function)

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

一个最简单的高阶函数:

javascript
function add(x, y, f) {
    return f(x) + f(y);
}

柯里化(Currying)

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

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

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

ts
// 普通函数(非柯里化)
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 秒内事件再次被触发,则重新开始计时。

**应用:

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

节流(Throttle)

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

应用场景:

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

call、apply 和 bind

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

特性callapplybind
主要功能立即执行函数,并改变 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)
JavaScript
// 准备一个测试函数和对象
function greet(greeting, punctuation) {
  console.log(`${greeting}, 我是${this.name}${punctuation}`);
}

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

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

1. call()

立即执行,参数逐个传入

JavaScript
greet.call(person, "您好", "!");     
// 输出:您好,我是张三!

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

2. apply()

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

JavaScript
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 的新函数

JavaScript
// 绑定 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} 被点击了`);
  }
}

场景:

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


IIFE

IIFE是 Immediately Invoked Function Expression 的缩写

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

ES6前常用

JavaScript
(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 步,直到所有任务处理完毕

代码示例与执行顺序演示

JavaScript
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');

执行结果顺序:

text
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(以及许多现代编程语言)中一种强大的类型系统特性,其核心目的是允许开发者编写可复用、类型安全的代码,同时让代码在不同具体类型下都能保持正确的类型推断和约束。

简单来说,泛型就是**“类型参数化”**:把类型当作参数一样传入,让同一个函数、类、接口能够处理多种类型的数据,而无需为每种类型都重复编写代码。
3interface 和 type 的区别现在差别已经很小,主要记住这几点: • interface 可多次声明自动合并(对库/模块声明补丁最有用) • type 可做联合、交叉、映射、条件、模板字符串等复杂类型操作 • interface 性能略好(声明合并时) • 目前绝大多数团队:能用 type 就用 type,除非明确需要声明合并
4keyof、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<T extends Record<string, any>>(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 常见)
10async 函数在执行栈中的位置是怎么样的async 函数本身是同步执行到第一个 await 第一个 await 之后 → 后续代码作为微任务放入微任务队列 所以:async 函数 = 同步前半段 + 多个微任务
11Promise.all 和 Promise.allSettled 的区别Promise.all:有一个 reject 就整体 reject,结果顺序严格对应 Promise.allSettled:永远 resolve,返回每个 promise 的 {status, value/reason},永远不会 reject
12JS 是单线程,为什么可以实现并发加载多个接口?JS 单线程指的是执行 JavaScript 代码的线程只有一个 真正的网络请求、定时器、DOM事件等都由浏览器底层多线程完成 JS 只负责注册回调,回调通过事件循环被调度执行 → 所以宏观上是并发
13setTimeout 和 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. 自定义组件纯数据组件化
16H5 页面嵌入小游戏时,遇到过页面内存持续上涨吗?怎么解决的?非常常见,尤其 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 = 1let 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参数列表否新函数
javascript
func.call(thisArg, arg1, arg2);
func.apply(thisArg, [arg1, arg2]);
const newFunc = func.bind(thisArg, arg1, arg2);

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

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

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

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

typescript
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. 服务器间无同源限制,可自由通信

流程:

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

Cookie和Session的定义和差异?

特性CookieSession
存储位置客户端服务器端
安全性较低(可篡改)较高
存储大小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的区别?

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

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

强缓存:

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

协商缓存:

  • 响应头:ETag/If-None-Match、Last-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

设置方式:

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