JavaScript 闭包作用域、原型链继承面试题解析教程

410 阅读13分钟

说到 JavaScript 面试,闭包、作用域、原型链、继承这些关键词几乎是“必考题”。很多同学一刷题就头大,明明看过好几遍原理,结果一到面试官面前,还是词不达意、思路混乱。这是为什么?

其实不是你不懂,而是没能用“讲故事”的方式把它讲清楚。面试不只是考概念,更是在考你能不能把复杂问题讲“简单”。比如,闭包到底是“函数里面套函数”这么简单吗?作用域链和执行上下文到底谁先谁后?原型链继承又是怎么一层一层传下去的?

在这篇文章里,我会带你一口气理清这些高频知识点,不讲花哨术语,只用最通俗的例子和最常见的面试题,帮你把零散的知识点串成“系统的知识树”。看完这篇,下一次再遇到相关题目,不仅能答对,还能讲得漂亮!

闭包与作用域

闭包的定义与原理

闭包(Closure)是 JavaScript 的核心特性,指一个函数能够“记住”并访问其定义时所在的作用域,即使该函数在其他作用域中执行。闭包由两部分组成:

  • 函数本身:定义的函数体。
  • 词法环境(Lexical Environment):函数定义时绑定的变量环境。

理论背景

  • JavaScript 使用词法作用域(Lexical Scoping),变量的作用域在代码编写时静态确定。
  • 每个函数创建时,会绑定其定义时的作用域链(Scope Chain),包含外部变量引用。
  • 闭包通过维持对外部变量的引用,延长变量的生命周期。

简单示例

function outer() {
    let count = 0;
    function inner() {
        count++;
        console.log(count);
    }
    return inner;
}

const counter = outer();
counter(); // 输出: 1
counter(); // 输出: 2

逐步分析

  1. outer 定义了变量 count 和函数 inner
  2. inner 引用了外部的 count,形成闭包。
  3. outer 返回 innercountinner 捕获,保存在内存中。
  4. 每次调用 counter()inner 更新并访问 count,实现计数器功能。

闭包的内存机制

  • count 存储在 inner 的词法环境中,不会因 outer 执行结束而销毁。
  • 垃圾回收器(GC)无法回收闭包引用的变量,可能导致内存泄漏,需谨慎管理。

作用域与作用域链

作用域(Scope)定义了变量的可见性和生命周期。JavaScript 有以下作用域类型:

  • 全局作用域:全局变量,生命周期贯穿整个程序。
  • 函数作用域:函数内定义的变量,仅在函数内可见。
  • 块级作用域:使用 letconst{} 内定义的变量(ES6 引入)。

作用域链

  • 当访问变量时,JavaScript 引擎从当前作用域开始,沿作用域链向上查找,直到全局作用域。
  • 作用域链由函数定义时的词法环境决定。

示例

let globalVar = "global";
function outer() {
    let outerVar = "outer";
    function inner() {
        let innerVar = "inner";
        console.log(innerVar, outerVar, globalVar);
    }
    inner();
}
outer();

输出

inner outer global

逐步分析

  1. inner 访问 innerVar(本地),outerVar(外层函数),globalVar(全局)。
  2. 作用域链:inner -> outer -> global
  3. 查找顺序:先本地作用域,再逐级向上。

闭包的常见面试题

面试题 1:闭包计数器

问题:修改以下代码,使每次调用返回不同的计数器实例。

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

答案

function createCounter() {
    let count = 0;
    return function() {
        return count++;
    };
}

const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // 0
console.log(counter1()); // 1
console.log(counter2()); // 0
console.log(counter2()); // 1

分析

  • 每次调用 createCounter 创建新的闭包,count 是独立的。
  • counter1counter2 引用不同的词法环境。

面试题 2:循环中的闭包

问题:以下代码输出什么?如何修复?

for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 1000);
}

输出

3
3
3

原因

  • var 具有函数作用域,i 是全局变量,setTimeout 回调执行时,i 已变为 3。
  • 闭包捕获的是变量引用,而非值。

修复方法 1:使用 let(块级作用域):

for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 1000);
}

输出

0
1
2

修复方法 2:使用 IIFE(立即执行函数表达式):

for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(() => console.log(j), 1000);
    })(i);
}

分析

  • let 为每次循环创建新的绑定。
  • IIFE 每次循环创建新的作用域,捕获当前 i 的值。

面试题 3:私有变量

问题:实现一个带有私有变量的模块。

function createPerson(name) {
    let _age = 0; // 私有变量
    return {
        getName: () => name,
        getAge: () => _age,
        setAge: (age) => { _age = age; }
    };
}

const person = createPerson("Alice");
console.log(person.getName()); // Alice
console.log(person.getAge()); // 0
person.setAge(25);
console.log(person.getAge()); // 25
console.log(person._age); // undefined

分析

  • _age 是闭包中的私有变量,无法直接访问。
  • 通过返回对象的方法控制访问,模拟封装。

闭包的应用场景

  1. 数据封装:如上例的私有变量。
  2. 状态维护:如计数器、事件处理。
  3. 函数柯里化
function curryAdd(a) {
    return function(b) {
        return a + b;
    };
}

const add5 = curryAdd(5);
console.log(add5(3)); // 8
  1. 事件处理
function setupButton(id) {
    let count = 0;
    document.getElementById(id).addEventListener('click', () => {
        console.log(`Clicked ${++count} times`);
    });
}
setupButton('myButton');

分析

  • 闭包维护 count,确保按钮点击次数持久化。
  • 避免全局变量污染。

原型链与继承

原型链的定义与原理

JavaScript 使用原型链(Prototype Chain)实现继承。每个对象有一个内部 [[Prototype]] 属性(通过 __proto__Object.getPrototypeOf 访问),指向其原型对象。原型链是对象查找属性的路径。

核心概念

  • 原型对象:每个函数有一个 prototype 属性,指向原型对象。
  • 构造函数:通过 new 创建对象时,对象的 [[Prototype]] 指向构造函数的 prototype
  • 属性查找:访问对象属性时,若对象本身没有,则沿原型链向上查找。

示例

function Person(name) {
    this.name = name;
}

Person.prototype.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
};

const alice = new Person("Alice");
alice.sayHello(); // Hello, I'm Alice
console.log(alice.__proto__ === Person.prototype); // true

逐步分析

  1. Person 是一个构造函数,其 prototype 属性指向原型对象。
  2. new Person("Alice") 创建对象 alice,其 [[Prototype]] 指向 Person.prototype
  3. alice.sayHello() 查找 sayHello,在 alice 自身找不到,沿原型链找到 Person.prototype.sayHello

原型链的继承

JavaScript 通过原型链实现继承,子类原型指向父类实例。

示例

function Animal(type) {
    this.type = type;
}

Animal.prototype.eat = function() {
    console.log(`${this.type} is eating`);
};

function Dog(name, type) {
    Animal.call(this, type); // 继承属性
    this.name = name;
}

Dog.prototype = Object.create(Animal.prototype); // 继承方法
Dog.prototype.constructor = Dog; // 修复构造函数
Dog.prototype.bark = function() {
    console.log(`${this.name} barks`);
};

const dog = new Dog("Max", "Dog");
dog.eat(); // Dog is eating
dog.bark(); // Max barks

逐步分析

  1. Animal.call(this, type) 调用父类构造函数,继承 type 属性。
  2. Object.create(Animal.prototype) 创建新对象,继承 Animal.prototype 的方法。
  3. 修复 constructor 确保 dog instanceof Dog 正确。
  4. 原型链:dog -> Dog.prototype -> Animal.prototype -> Object.prototype

原型链的常见面试题

面试题 1:原型链查找

问题:以下代码输出什么?

function Foo() {}
Foo.prototype.x = 1;

const foo = new Foo();
console.log(foo.x); // 1
foo.x = 2;
console.log(foo.x); // 2
console.log(Foo.prototype.x); // 1

分析

  • foo.x 初始查找 Foo.prototype.x,输出 1。
  • foo.x = 2foo 自身创建属性 x,不影响原型。
  • Foo.prototype.x 仍为 1。

面试题 2:继承实现

问题:实现一个继承方法,支持多级继承。

function inherit(Child, Parent) {
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
}

function Animal(type) {
    this.type = type;
}
Animal.prototype.eat = function() {
    console.log(`${this.type} eats`);
};

function Dog(name, type) {
    Animal.call(this, type);
    this.name = name;
}
inherit(Dog, Animal);
Dog.prototype.bark = function() {
    console.log(`${this.name} barks`);
};

const dog = new Dog("Max", "Dog");
dog.eat(); // Dog eats
dog.bark(); // Max barks

分析

  • inherit 函数封装原型链继承,复用性高。
  • Object.create 避免直接修改父类原型。

面试题 3:instanceof 原理

问题:以下代码输出什么?

console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true

分析

  • instanceof 检查对象原型链是否包含构造函数的 prototype
  • dog 的原型链:Dog.prototype -> Animal.prototype -> Object.prototype

ES6 Class 继承

ES6 引入 class 语法,简化继承:

class Animal {
    constructor(type) {
        this.type = type;
    }
    eat() {
        console.log(`${this.type} eats`);
    }
}

class Dog extends Animal {
    constructor(name, type) {
        super(type);
        this.name = name;
    }
    bark() {
        console.log(`${this.name} barks`);
    }
}

const dog = new Dog("Max", "Dog");
dog.eat(); // Dog eats
dog.bark(); // Max barks

分析

  • class 是原型继承的语法糖,super 调用父类构造函数。
  • 更直观,但底层仍是原型链。

数据结构与算法在前端面试中的重要性

为什么重要

数据结构与算法(DSA)在前端面试中至关重要,原因如下:

  • 性能优化:高效算法减少 DOM 操作、渲染时间,提升用户体验。
  • 逻辑能力:算法题考察逻辑思维和问题解决能力。
  • 跨领域应用:前端与后端(如 Node.js)、机器学习(如 CNN 可视化)交互需要 DSA 知识。
  • 竞争力:顶级公司(如 Google、Meta)要求扎实的算法基础。

前端场景

  • 数组操作:过滤、排序、去重(如用户列表处理)。
  • 树结构:DOM 树遍历、组件树优化。
  • 图算法:依赖解析(如 Webpack 模块依赖)。
  • 时间复杂度:优化大数据量渲染(如虚拟列表)。

常见数据结构与算法

数组与字符串

面试题:反转字符串

问题:编写函数反转字符串,不使用内置方法。

function reverseString(s) {
    let arr = s.split('');
    let left = 0, right = arr.length - 1;
    while (left < right) {
        [arr[left], arr[right]] = [arr[right], arr[left]];
        left++;
        right--;
    }
    return arr.join('');
}

console.log(reverseString("hello")); // "olleh"

分析

  • 时间复杂度:O(n),空间复杂度:O(n)。
  • 使用双指针交换字符,避免额外空间。

链表

面试题:反转链表

class ListNode {
    constructor(val, next = null) {
        this.val = val;
        this.next = next;
    }
}

function reverseList(head) {
    let prev = null, curr = head;
    while (curr) {
        let next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}

const list = new ListNode(1, new ListNode(2, new ListNode(3)));
const reversed = reverseList(list);
console.log(reversed.val); // 3

分析

  • 时间复杂度:O(n),空间复杂度:O(1)。
  • 链表在前端用于事件队列、历史记录。

面试题:二叉树前序遍历

class TreeNode {
    constructor(val, left = null, right = null) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

function preorderTraversal(root) {
    const result = [];
    function traverse(node) {
        if (!node) return;
        result.push(node.val);
        traverse(node.left);
        traverse(node.right);
    }
    traverse(root);
    return result;
}

const tree = new TreeNode(1, new TreeNode(2), new TreeNode(3));
console.log(preorderTraversal(tree)); // [1, 2, 3]

分析

  • 时间复杂度:O(n),空间复杂度:O(h)(h 为树高)。
  • 前端应用:DOM 树遍历、组件树解析。

面试题:深度优先搜索(DFS)

function dfs(graph, start) {
    const visited = new Set();
    function traverse(node) {
        visited.add(node);
        console.log(node);
        for (let neighbor of graph[node]) {
            if (!visited.has(neighbor)) {
                traverse(neighbor);
            }
        }
    }
    traverse(start);
}

const graph = {
    A: ['B', 'C'],
    B: ['A', 'D', 'E'],
    C: ['A', 'F'],
    D: ['B'],
    E: ['B', 'F'],
    F: ['C', 'E']
};
dfs(graph, 'A'); // A, B, D, E, F, C

分析

  • 时间复杂度:O(V + E),空间复杂度:O(V)。
  • 应用:依赖解析、组件关系图。

算法在前端的实际应用

虚拟列表优化

处理大数据量列表(如 10,000 条记录):

function createVirtualList(container, items, itemHeight, visibleHeight) {
    let startIndex = 0;
    let endIndex = Math.ceil(visibleHeight / itemHeight);
    
    function render() {
        container.innerHTML = '';
        for (let i = startIndex; i < endIndex; i++) {
            const div = document.createElement('div');
            div.style.height = `${itemHeight}px`;
            div.textContent = items[i];
            container.appendChild(div);
        }
    }
    
    container.addEventListener('scroll', () => {
        startIndex = Math.floor(container.scrollTop / itemHeight);
        endIndex = startIndex + Math.ceil(visibleHeight / itemHeight);
        render();
    });
    
    render();
}

const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
createVirtualList(document.getElementById('list'), items, 50, 500);

分析

  • 仅渲染可视区域,降低 DOM 操作开销。
  • 时间复杂度:O(k),k 为可视项数。

CNN 结果可视化

结合 Python CNN 项目,前端可视化训练结果:

fetch('/api/cnn_results')
    .then(response => response.json())
    .then(data => {
        const ctx = document.getElementById('chart').getContext('2d');
        new Chart(ctx, {
            type: 'line',
            data: {
                labels: data.epochs,
                datasets: [{
                    label: '验证准确率',
                    data: data.val_accuracy,
                    borderColor: '#007bff',
                    fill: false
                }]
            }
        });
    });

分析

  • 使用 Chart.js 绘制 CNN 训练曲线。

  • 后端(Node.js 或 Python Flask)提供数据:

    from flask import Flask, jsonify
    app = Flask(__name__)
    
    @app.route('/api/cnn_results')
    def cnn_results():
        return jsonify({
            'epochs': list(range(1, 51)),
            'val_accuracy': [0.65, 0.70, 0.75, ...]
        })
    

企业级实践

Node.js 与 Python 交互

前端通过 Node.js 调用 Python CNN 模型:

const { spawn } = require('child_process');

function runPythonScript(scriptPath, args) {
    return new Promise((resolve, reject) => {
        const python = spawn('python', [scriptPath, ...args]);
        let output = '';
        python.stdout.on('data', (data) => {
            output += data.toString();
        });
        python.stderr.on('data', (data) => {
            reject(data.toString());
        });
        python.on('close', () => {
            resolve(output);
        });
    });
}

runPythonScript('cifar10_project/scripts/predict.py', ['image.jpg'])
    .then(result => console.log(result))
    .catch(err => console.error(err));

Python 脚本 (predict.py):

import sys
import tensorflow as tf
import numpy as np

model = tf.keras.models.load_model('cifar10_project/models/cifar10_model.h5')
image = tf.keras.preprocessing.image.load_img(sys.argv[1], target_size=(32, 32))
image = tf.keras.preprocessing.image.img_to_array(image) / 255.0
image = np.expand_dims(image, axis=0)
prediction = model.predict(image)
print(np.argmax(prediction[0]))

分析

  • Node.js 使用 child_process 调用 Python 脚本。
  • 适合前端展示 CNN 预测结果。

Docker 部署

部署前端与 CNN 后端:

echo 'FROM node:16
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD ["node", "server.js"]' > Dockerfile
docker build -t frontend_app .

server.js:

const express = require('express');
const { runPythonScript } = require('./utils');

const app = express();
app.use(express.static('public'));

app.get('/api/predict', async (req, res) => {
    const result = await runPythonScript('predict.py', ['image.jpg']);
    res.json({ prediction: result });
});

app.listen(3000, () => console.log('Server running on port 3000'));

分析

  • 前端通过 Express 提供静态文件和 API。
  • Docker 容器化部署,确保环境一致。

深入闭包与作用域

闭包的底层实现

闭包的实现依赖于 JavaScript 引擎(如 V8)的词法环境(Lexical Environment)和执行上下文(Execution Context)。以下是其底层机制:

  • 词法环境:每个函数创建时,V8 为其生成一个词法环境对象,包含:
    • 变量对象:存储本地变量(如 letconst)。
    • 外部引用:指向外层函数的词法环境。
  • 执行上下文:包含变量环境、词法环境和 this 绑定,栈式管理(调用栈)。
  • 闭包捕获:当函数返回时,其词法环境被保留,外部变量引用不会被垃圾回收。

示例(深入分析):

function createCounter() {
    let count = 0;
    return {
        increment: () => ++count,
        getCount: () => count
    };
}

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

逐步分析

  1. createCounter 创建词法环境,包含 count = 0
  2. 返回对象 { increment, getCount },两个函数共享同一词法环境。
  3. V8 引擎为 count 分配堆内存,闭包函数通过引用访问。
  4. 垃圾回收器无法回收 count,因为 incrementgetCount 仍在使用。

内存管理

  • 内存泄漏风险:闭包可能导致未释放的变量累积。例如,事件监听器未移除:
function setupLeak() {
    let data = new Array(1000000).fill(0); // 大数组
    document.getElementById('button').addEventListener('click', () => {
        console.log(data.length); // 闭包引用 data
    });
}

解决

  • 手动移除监听器:

    const button = document.getElementById('button');
    const handler = () => console.log('Clicked');
    button.addEventListener('click', handler);
    // 移除
    button.removeEventListener('click', handler);
    

作用域的进阶应用

块级作用域与 Temporal Dead Zone(TDZ)

ES6 的 letconst 引入块级作用域,并伴随 TDZ(暂时性死区),防止变量在声明前使用。

面试题:以下代码输出什么?

function testTDZ() {
    console.log(x); // ReferenceError
    let x = 10;
}
testTDZ();

分析

  • let x 在声明前不可访问,触发 TDZ 错误。
  • var 无 TDZ,可能导致 undefined

模块作用域

ES6 模块(ESM)引入模块作用域,变量默认私有。

// counter.js
let count = 0;
export function increment() {
    return ++count;
}
export function getCount() {
    return count;
}

// main.js
import { increment, getCount } from './counter.js';
console.log(increment()); // 1
console.log(getCount()); // 1
console.log(increment()); // 2

分析

  • 模块作用域类似闭包,count 仅在模块内可访问。
  • ESM 支持静态分析,优化 Tree Shaking。

面试题 4:闭包与模块

问题:使用闭包重写模块模式。

const counterModule = (function() {
    let count = 0;
    return {
        increment: () => ++count,
        getCount: () => count
    };
})();

console.log(counterModule.increment()); // 1
console.log(counterModule.getCount()); // 1

分析

  • IIFE(立即执行函数表达式)创建私有作用域,模拟模块。
  • 与 ESM 相比,IIFE 动态但不支持 Tree Shaking。

原型链与继承进阶

原型链的底层机制

原型链基于 JavaScript 的对象模型,V8 引擎通过 [[Prototype]] 实现属性查找。以下是关键点:

  • 原型对象Function.prototypeObject.prototype 是原型链的根。
  • 属性遮蔽:对象自身属性优先于原型属性。
  • 性能:深层原型链查找可能影响性能。

示例(属性遮蔽):

function Person(name) {
    this.name = name;
}
Person.prototype.name = "Default";

const person = new Person("Alice");
console.log(person.name); // Alice
delete person.name;
console.log(person.name); // Default

分析

  • delete person.name 移除自身属性,暴露原型属性。
  • 原型链:person -> Person.prototype -> Object.prototype

高级继承模式

寄生组合继承

寄生组合继承是高效的继承方式,避免重复调用父类构造函数。

function inherit(Child, Parent) {
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
}

function Animal(type) {
    this.type = type;
}
Animal.prototype.eat = function() {
    console.log(`${this.type} eats`);
};

function Dog(name, type) {
    Animal.call(this, type);
    this.name = name;
}
inherit(Dog, Animal);
Dog.prototype.bark = function() {
    console.log(`${this.name} barks`);
};

const dog = new Dog("Max", "Dog");
dog.eat(); // Dog eats
dog.bark(); // Max barks

分析

  • Object.create 创建中间对象,避免 Dog.prototype = new Animal() 的副作用。
  • Animal.call 继承属性,inherit 继承方法。

Mixin 模式

Mixin 允许多重继承,复用代码。

const canRun = {
    run() {
        console.log(`${this.name} runs`);
    }
};

function Dog(name) {
    this.name = name;
}
Object.assign(Dog.prototype, canRun);

const dog = new Dog("Max");
dog.run(); // Max runs

分析

  • Object.assign 将 Mixin 方法复制到原型。
  • 适合复用独立功能,如日志、事件处理。

面试题 5:原型链修改

问题:以下代码输出什么?如何避免问题?

function Person() {}
Person.prototype.name = "Shared";

const p1 = new Person();
const p2 = new Person();
p1.name = "Alice";
console.log(p1.name); // Alice
console.log(p2.name); // Shared
Person.prototype.name = "Modified";
console.log(p1.name); // Alice
console.log(p2.name); // Modified

分析

  • p1.name = "Alice"p1 自身创建属性,不影响原型。
  • 修改 Person.prototype.name 影响未遮蔽的实例(如 p2)。
  • 避免问题:避免直接修改原型,使用 Mixin 或实例属性。

面试题 6:instanceof 实现

问题:手动实现 instanceof

function myInstanceof(obj, constructor) {
    let proto = Object.getPrototypeOf(obj);
    while (proto) {
        if (proto === constructor.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}

console.log(myInstanceof(dog, Dog)); // true
console.log(myInstanceof(dog, Animal)); // true

分析

  • 遍历 obj 的原型链,检查是否包含 constructor.prototype
  • 时间复杂度:O(n),n 为原型链长度。

数据结构与算法进阶

动态规划

面试题:最长公共子序列(LCS)

问题:求两个字符串的最长公共子序列长度。

function longestCommonSubsequence(text1, text2) {
    const m = text1.length, n = text2.length;
    const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
    
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (text1[i - 1] === text2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[m][n];
}

console.log(longestCommonSubsequence("ABCD", "ACDF")); // 3 (ACD)

分析

  • 时间复杂度:O(m_n),空间复杂度:O(m_n)。
  • 前端应用:文本差异比较(如代码编辑器高亮)。

图算法

面试题:广度优先搜索(BFS)

function bfs(graph, start) {
    const visited = new Set();
    const queue = [start];
    visited.add(start);
    
    while (queue.length) {
        const node = queue.shift();
        console.log(node);
        for (let neighbor of graph[node]) {
            if (!visited.has(neighbor)) {
                visited.add(neighbor);
                queue.push(neighbor);
            }
        }
    }
}

const graph = {
    A: ['B', 'C'],
    B: ['A', 'D', 'E'],
    C: ['A', 'F'],
    D: ['B'],
    E: ['B', 'F'],
    F: ['C', 'E']
};
bfs(graph, 'A'); // A, B, C, D, E, F

分析

  • 时间复杂度:O(V + E),空间复杂度:O(V)。
  • 前端应用:组件依赖解析、路由导航。

LeetCode 高频题

面试题:两数之和

问题:给定数组和目标值,找出两个数的索引,使其和等于目标值。

function twoSum(nums, target) {
    const map = new Map();
    for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];
        if (map.has(complement)) {
            return [map.get(complement), i];
        }
        map.set(nums[i], i);
    }
    return [];
}

console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]

分析

  • 使用哈希表,时间复杂度:O(n),空间复杂度:O(n)。
  • 前端应用:快速查找 DOM 元素对。

前端性能优化

节流与防抖

节流(Throttle):限制函数在固定时间间隔内执行一次。

function throttle(fn, delay) {
    let last = 0;
    return function(...args) {
        const now = Date.now();
        if (now - last >= delay) {
            fn.apply(this, args);
            last = now;
        }
    };
}

const scrollHandler = throttle(() => console.log('Scrolled'), 1000);
window.addEventListener('scroll', scrollHandler);

防抖(Debounce):延迟执行,直到事件停止触发。

function debounce(fn, delay) {
    let timer;
    return function(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
    };
}

const resizeHandler = debounce(() => console.log('Resized'), 500);
window.addEventListener('resize', resizeHandler);

分析

  • 节流适合高频事件(如滚动),防抖适合输入验证。
  • 优化前端交互性能,减少不必要的计算。

虚拟 DOM 优化

React 的虚拟 DOM 优化 DOM 操作:

class List extends React.Component {
    shouldComponentUpdate(nextProps) {
        return this.props.items !== nextProps.items;
    }
    
    render() {
        return (
            <div>
                {this.props.items.map(item => <div key={item.id}>{item.text}</div>)}
            </div>
        );
    }
}

分析

  • shouldComponentUpdate 避免不必要的重新渲染。
  • 时间复杂度:O(n) 比较虚拟 DOM 树。

与 CNN 项目的整合

前端可视化 CNN 结果

使用 Chart.js 可视化 Python CNN 训练结果:

fetch('/api/cnn_results')
    .then(response => response.json())
    .then(data => {
        const ctx = document.getElementById('accuracyChart').getContext('2d');
        new Chart(ctx, {
            type: 'line',
            data: {
                labels: data.epochs,
                datasets: [
                    {
                        label: '训练准确率',
                        data: data.accuracy,
                        borderColor: '#007bff',
                        fill: false
                    },
                    {
                        label: '验证准确率',
                        data: data.val_accuracy,
                        borderColor: '#28a745',
                        fill: false
                    }
                ]
            },
            options: {
                scales: {
                    y: {
                        beginAtZero: true,
                        max: 1
                    }
                }
            }
        });
    });

Python 后端(Flask):

from flask import Flask, jsonify
app = Flask(__name__)

@app.route('/api/cnn_results')
def cnn_results():
    return jsonify({
        'epochs': list(range(1, 51)),
        'accuracy': [0.65, 0.70, 0.75, ...], # 训练数据
        'val_accuracy': [0.60, 0.65, 0.70, ...]
    })

if __name__ == '__main__':
    app.run(port=5000)

分析

  • 前端通过 Fetch API 获取数据,Chart.js 绘制曲线。
  • 后端使用 Flask 提供 REST API,结合 Anaconda 环境运行。

WebAssembly 调用 CNN

使用 TensorFlow.js 或 ONNX.js 运行 CNN 模型:

import * as tf from '@tensorflow/tfjs';

async function predict(imageElement) {
    const model = await tf.loadLayersModel('/models/cifar10_model.json');
    const img = tf.browser.fromPixels(imageElement).resizeNearestNeighbor([32, 32]).toFloat().div(255).expandDims();
    const prediction = model.predict(img);
    const result = await prediction.data();
    console.log(result);
}

const img = document.getElementById('inputImage');
predict(img);

分析

  • TensorFlow.js 在浏览器运行 CNN 模型,无需后端。

  • 需将 Python 模型转换为 TF.js 格式:

    tensorflowjs_converter --input_format keras cifar10_project/models/cifar10_model.h5 cifar10_project/models/web_model
    

Node.js 与 Python 交互

Node.js 调用 Python CNN 预测:

const { spawn } = require('child_process');

function runPrediction(imagePath) {
    return new Promise((resolve, reject) => {
        const python = spawn('python', ['predict.py', imagePath]);
        let output = '';
        python.stdout.on('data', (data) => output += data);
        python.stderr.on('data', (data) => reject(data.toString()));
        python.on('close', () => resolve(output));
    });
}

runPrediction('image.jpg').then(result => console.log(`Prediction: ${result}`));

predict.py

import sys
import tensorflow as tf
import numpy as np

model = tf.keras.models.load_model('cifar10_project/models/cifar10_model.h5')
image = tf.keras.preprocessing.image.load_img(sys.argv[1], target_size=(32, 32))
image = tf.keras.preprocessing.image.img_to_array(image) / 255.0
image = np.expand_dims(image, axis=0)
prediction = model.predict(image)
print(np.argmax(prediction[0]))

分析

  • 使用 child_process 调用 Python 脚本。

  • Linux 命令管理进程:

    ps aux | grep python
    kill -9 <pid>
    

企业级实践

微前端架构

使用 Module Federation 实现微前端:

// webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
    plugins: [
        new ModuleFederationPlugin({
            name: 'host',
            remotes: {
                app1: 'app1@http://localhost:3001/remoteEntry.js'
            }
        })
    ]
};

// app1.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
    plugins: [
        new ModuleFederationPlugin({
            name: 'app1',
            filename: 'remoteEntry.js',
            exposes: {
                './Chart': './src/Chart.js'
            }
        })
    ]
};

分析

  • 微前端分解大型应用,独立部署。
  • 适合 CNN 可视化模块的动态加载。

CI/CD 集成

使用 GitHub Actions 自动化部署:

name: Deploy Frontend
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '16'
      - run: npm install
      - run: npm run build
      - name: Deploy to S3
        run: aws s3 sync ./dist s3://my-bucket

分析

  • 自动化构建和部署前端代码。

  • 结合 Docker 部署 CNN 后端:

    docker push myrepo/cnn_app:latest
    

Kubernetes 部署

部署前端与 CNN 服务:

kubectl create -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cnn-frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: cnn-frontend
  template:
    metadata:
      labels:
        app: cnn-frontend
    spec:
      containers:
      - name: frontend
        image: frontend_app:latest
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: cnn-frontend-service
spec:
  selector:
    app: cnn-frontend
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: LoadBalancer
EOF

分析

  • 部署前端服务,负载均衡提高可用性。
  • 可扩展到 CNN 后端,分配 GPU 资源。