面经-02

84 阅读8分钟

一、JS 基础 & 数据类型

1. JS 基本数据类型

  • 原始类型(Primitive) :存储在 栈内存,按值访问。

    • NumberNaNInfinity
    • String
    • Boolean
    • Null
    • Undefined
    • Symbol
    • BigInt
  • 引用类型(Reference) :存储在 堆内存,变量存的是引用地址。

    • ObjectArrayFunctionDate

📌 追问:为什么 typeof null === 'object'
👉 因为 JS 最初的实现里用二进制前三位表示数据类型,000 代表对象,null 的值全是 0,被误判为对象,成了历史遗留。


2. Symbol

  • Symbol 用来生成独一无二的值:
const s1 = Symbol("id");
const s2 = Symbol("id");
console.log(s1 === s2); // false
  • 常用于 对象属性键,避免冲突:
const id = Symbol("id");
let obj = { [id]: 123 };
console.log(obj[id]); // 123
  • 内置的 Symbol.iterator 用于迭代协议。

3. 深浅拷贝

  • 浅拷贝:只复制第一层,内部对象仍然共享引用。

    const obj = { a: 1, b: { c: 2 } };
    const copy = { ...obj };
    copy.b.c = 3;
    console.log(obj.b.c); // 3
    
  • 深拷贝

    • JSON 方法(缺陷:丢失属性值为 “”函数、undefined、Symbol“)

      JSON.parse(JSON.stringify(obj));
      
      JSON.stringify({x: undefined, y: Symbol()}); // "{}"
      JSON.stringify([undefined, Symbol()]); // "[null,null]"
      
      
    • 现代方案:structuredClone(obj)

    • 库:lodash.cloneDeep

// 手写深拷贝
function deepClone (obj,map = new WeakMap()){
    if(obj === null || typeof obj!=='object') return obj;// 基本数据类型直接返回
    if(map.has(obj)) return map.get(obj) // 解决循环引用
    
    const result = Array.isArray(obj)?[]:{};
    
    map.set(obj,result);
    // **for...in**遍历的结果可能包含原型链上继承的属性,
    // 为了确保安全性,常常需要使用`hasOwnProperty`方法进行过滤,从而增加了些许复杂性
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            result[key] = deepClone(obj[key],map)
        }
    }
}

4. 判断两个对象是否相等

  • 简单:JSON.stringify(a) === JSON.stringify(b)(要求 key 顺序一致)
  • 复杂:递归比较 key/value
  • 专业:_.isEqual(obj1, obj2)

5. 数组扁平化

const arr = [1, [2, [3, 4]]];
arr.flat(Infinity); // [1,2,3,4]

手写递归:

function flatten(arr) {
  return arr.reduce((res, item) =>
    res.concat(Array.isArray(item) ? flatten(item) : item), []);
}

6. 判断是不是数组

  • Array.isArray(obj) ✅ 推荐
  • obj instanceof Array
  • Object.prototype.toString.call(obj) === "[object Array]"

🔹 二、异步 & Promise

7. Promise.all 错误处理

  • 默认行为:任意一个失败就 reject。
  • 如果要拿到全部结果:用 Promise.allSettled
Promise.allSettled([
  Promise.resolve(1),
  Promise.reject("error"),
  Promise.resolve(2)
]).then(console.log);

输出:

[
  { status: "fulfilled", value: 1 },
  { status: "rejected", reason: "error" },
  { status: "fulfilled", value: 2 }
]

8. 作用域链 & 闭包

  • 作用域链:访问变量时,先查当前函数 → 上层函数 → 全局
  • 闭包:函数(内部函数)记住了它定义时候的作用域,即使函数在外部执行,也能访问当时作用域里的变量
function outer() {
  let count = 0;
  return function inner() {
    return ++count;
  }
}
const add = outer();
console.log(add()); // 1
console.log(add()); // 2

👉 闭包常用于缓存、模块化、数据私有化。


🔹 三、浏览器机制

9. 输入 URL 发生了什么

  1. DNS 解析 → IP
  2. TCP 三次握手
  3. 发送 HTTP 请求
  4. 服务器返回响应
  5. 浏览器解析 HTML → 构建 DOM 树
  6. 下载 CSS → 构建 CSSOM
  7. 合并 DOM + CSSOM → Render Tree
  8. Layout(计算位置/大小)
  9. Paint(绘制像素)
  10. Composite(GPU 合成)

🔹 扩展下三次握手过程

假设:客户端(Client)要连接服务端(Server)。

  1. 第一次握手 —— 客户端发起连接请求

    • Client → Server:SYN=1, seq=x
    • 作用:告诉服务端 “我要建立连接,我这边的初始序号是 x”。
  2. 第二次握手 —— 服务端确认并响应

    • Server → Client:SYN=1, ACK=1, ack=x+1, seq=y
    • 作用:服务端确认收到了客户端的请求(ack=x+1),同时告诉客户端 “我也可以建立连接,我这边的初始序号是 y”。
  3. 第三次握手 —— 客户端确认

    • Client → Server:ACK=1, ack=y+1, seq=x+1
    • 作用:客户端确认收到了服务端的响应。

此时连接建立完成,双向通信通道就建立了。


10. 事件循环

  • 宏任务:setTimeout, setInterval, I/O, script

  • 微任务:Promise.then, MutationObserver, queueMicrotask

  • 执行顺序:

    • 执行一个宏任务(脚本/定时器回调)
    • 执行完后清空所有微任务
    • 再取下一个宏任务

例子:

console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
console.log(4);
// 输出:1, 4, 3, 2

步骤执行顺序:

  1. 整个脚本(script)是一个宏任务。

    • 执行 console.log(1) → 输出 1
    • 执行 setTimeout → 把回调放入宏任务队列(等当前宏任务执行完后再调度)。
    • 执行 Promise.then → 把回调放入微任务队列。
    • 执行 console.log(4) → 输出 4
  2. 当前宏任务执行完,清空微任务队列

    • 执行 console.log(3) → 输出 3
  3. 取下一个宏任务:

    • 执行定时器回调 → console.log(2) → 输出 2

🔹 四、CSS & 页面优化

11. 语义化

  • 用合适的标签表达含义 <header> <article> <footer>

  • 好处:

    • SEO 友好
    • 可读性强
    • 对屏幕阅读器/无障碍更友好

常见语义化标签分类

(1) 页面结构标签

  • <header>:页面或区块的头部。
  • <nav>:导航栏。
  • <main>:文档主体部分,一个页面只能有一个 <main>
  • <section>:页面中的一个区块,通常有标题。
  • <article>:文章、独立内容块(博客文章、论坛帖子)。
  • <aside>:侧边栏,相关信息或广告。
  • <footer>:底部信息。

(2) 文本内容标签

  • <h1> ~ <h6>:标题。
  • <p>:段落。
  • <blockquote>:长引用。
  • <cite>:引用来源。
  • <strong>:强调,语义上是重要。
  • <em>:强调,语气上突出。
  • <mark>:标记高亮。
  • <abbr>:缩写解释。
  • <time>:时间。

(3) 列表相关

  • <ul>:无序列表。
  • <ol>:有序列表。
  • <li>:列表项。
  • <dl>:定义列表。
  • <dt>:定义标题。
  • <dd>:定义描述。

(4) 表格相关

  • <table>:表格。
  • <caption>:表格标题。
  • <thead> <tbody> <tfoot>:表头/主体/表尾。
  • <th>:表头单元格。
  • <td>:数据单元格。

(5) 表单相关

  • <form>:表单。
  • <label>:表单字段说明。
  • <input>:输入框。
  • <button>:按钮。
  • <select> <option>:下拉框。
  • <textarea>:多行文本。

12. 重绘 & 回流

  • 回流 (Reflow) :影响布局,比如 width/height/top/left

  • 重绘 (Repaint) :影响外观,比如 color/background

  • 优化手段

    • 批量 DOM 操作(DocumentFragment)
    • 读写分离(先读 DOM,再写 DOM)
    • 动画用 transform/opacity
    • 慎用 will-change

13. 居中弹框 + 溢出按钮

.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
.close-btn {
  position: absolute;
  top: -10px;
  right: -10px;
}

14. 雪碧图

.icon {
  background: url(sprite.png) no-repeat;
  background-position: -20px -40px;
  width: 16px; height: 16px;
}

🔹 五、Vue

15. 虚拟 DOM & diff

  • 虚拟 DOM:用 JS 对象描述 DOM 节点(VNode)

  • diff 原理:

    • 只比较同层节点
    • 有 key 时复用节点,最小化更新
    • 新旧节点相同 → 复用;不同 → 删除/新建

16. 插入节点

  • 原生:
const el = document.createElement("div");
parent.appendChild(el);
  • Vue:

    • 生成 VNode
    • 调用 patch 比较
    • 最终操作真实 DOM

17. Vuex:action vs mutation

  • mutation:唯一能修改 state 的方法,必须同步
  • action:支持异步逻辑,最后调用 mutation 修改 state

🔹 六、工程化 & 网络

18. Webpack 打包原理

  1. 从入口文件出发,构建依赖图
  2. Loader 转换源码(如 TS → JS)
  3. Plugin 扩展功能(压缩、热更新)
  4. 输出 bundle

19. HTTP 状态码

  • 200 OK
  • 301 永久重定向
  • 302 临时重定向
  • 304 Not Modified(协商缓存命中)
  • 404 Not Found
  • 500 Server Error

20. 缓存头

  • 强缓存:

    • Cache-Control: max-age=3600
    • Expires
  • 协商缓存:

    • ETag / If-None-Match
    • Last-Modified / If-Modified-Since

🔹 七、ES6 语法

箭头函数

  • 没有 this,继承外层
  • 不能 new
  • 没有 arguments

数组展开符

const arr1 = [1,2];
const arr2 = [...arr1, 3,4];

let / const vs var

  • var:函数作用域,存在变量提升
  • let/const:块级作用域,不可重复声明,TDZ(暂时性死区)

split / slice / splice / reduce

  • split: 字符串切分 → 数组
  • slice(start,end): 截取数组,不修改原数组
  • splice(start,deleteCount,items): 删除/插入,修改原数组
  • reduce: 累计
[1,2,3].reduce((acc,v) => acc+v, 0); // 6

ES6 class转 ES5

class Person {
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    sayName(){
        console.log(this.name)
    }
    static createAnonymous(){
        return new Person('anonymous',18)
    }
}

const p = new Person('Tom',20)
p.sayName()
  • 等价ES5 语法
// 构造函数
function Person(name,age){
    this.name = name;
    this.age = age;
}

// 原型方法
Person.prototype.sayName = functions90{
    console.log(this.name)
}
// 静态方法
Person.createAnonymous = function(){
    return new Person('anonymous',18)
}

var p = new Person("Tom", 20);
p.sayName(); // Tom
对比总结
  • constructor 👉 构造函数
  • class 里的普通方 👉 加在prototype 上
  • static方法 👉 直接挂在构造函数对象上
  • extends继承 👉 用 Object.create() + call super 实现
带继承的例子
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(this.name + " makes a noise");
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  speak() {
    console.log(this.name + " barks");
  }
}


// 转成ES5
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function () {
  console.log(this.name + " makes a noise");
};

function Dog(name, breed) {
  // super(name)
 // 是在 **子类构造函数 `Dog` 内“借用”父类构造函数 `Animal`** 来初始化当前正在创建的实例(也就是 `this`)。它对应 ES6 里的 `super(name)`
  Animal.call(this, name);
  this.breed = breed;
  // Function.prototype.call是干嘛的?call的作用是 **指定函数执行时的this**,并立即调用该函数
  // 所以 `Animal.call(this, name)` 就等价于:用当前的this(正被 new Dog(...))创建出来的那个对象,作为Animal的this去执行Animal(name)
}

// 继承原型
Dog.prototype = Object.create(Animal.prototype);// 让 Dog.prototype.__proto__ 指向 Animal.prototype 
// 从而让 Dog实例可以通过原型链找到Animal的方法
Dog.prototype.constructor = Dog;
// 但是有个副作用:Object.create(Animal.prototype)返回的是一个新的对象,这个对象的constructor是指向Animal,所以需要Dog.prototype.constructor = Dog 来修正


// 重写方法
Dog.prototype.speak = function () {
  console.log(this.name + " barks");
};


总结
  • class是语法糖,就是构造函数+原型
  • Babel 编译 ES6 → ES5 时,也是按照这种方式做的。
扩展复习下原型链
  • 每个函数 都有一个属性 prototype ,这个属性是一个对象,里面可以放方法/属性
  • 当你使用new来调用这个函数时,创建的对象会有一个隐藏的属性**__proto__** ,指向构造函数的prototype

原型链是怎么工作的?

  • 当访问一个对象的属性,js引擎会这样查找
    • 先找对象自身的属性
    • 如果没有,就顺着 __proto__ → 构造函数的 prototype
    • 如果 prototype 上也没有,就继续往上找 prototype 的 prototype
    • 一直追溯到 Object.prototype
    • 如果还没找到,返回 undefined

constructor 和 prototype的关系

  • 构造函数有prototype,用来生成实例的原型
  • prototype对象上有个constructor,指向构造函数本身
function Person{}
Person.prototype.constructor === Person // true