闭包面试宝典:高频考点与实战解析

JavaScript 闭包面试宝典:高频考点与实战解析

🎯 目录

  1. 引言
  2. 闭包基础概念
  3. 闭包的应用场景
  4. 私有变量与封装
  5. 防抖与节流
  6. 事件监听与上下文绑定
  7. 立即执行函数IIFE
  8. 记忆函数与缓存
  9. 柯里化与偏函数
  10. 闭包的性能考虑
  11. 最佳实践与注意事项
  12. 总结

🚀 引言

在JavaScript的世界里,闭包(Closure)是一个既优雅又强大的特性。它不仅是JavaScript语言的核心概念,更是许多高级编程技巧的基础。从模块化开发到函数式编程,从性能优化到代码组织,闭包无处不在。

本文将通过丰富的实例,深入探讨闭包在实际开发中的各种应用场景,帮助你真正理解和掌握这一重要概念。

为什么闭包如此重要?

闭包的重要性体现在以下几个方面:

  1. 数据封装:创建私有变量和方法
  2. 状态保持:在函数调用之间保持状态
  3. 模块化:实现模块模式和命名空间
  4. 函数式编程:支持高阶函数和函数组合
  5. 异步编程:处理回调函数中的变量访问

🔍 闭包基础概念

什么是闭包?

闭包是指一个函数能够访问其外部(封闭)作用域中变量的特性。换句话说,闭包让你可以在内部函数中访问外部函数的作用域。

javascript 复制代码
function outerFunction(x) {
    // 外部函数的变量
    let outerVariable = x;
    
    // 内部函数
    function innerFunction(y) {
        // 可以访问外部函数的变量
        console.log(outerVariable + y);
    }
    
    return innerFunction;
}

const myClosure = outerFunction(10);
myClosure(5); // 输出: 15

闭包的工作原理

当一个函数在另一个函数内部定义时,内部函数会形成一个闭包。这个闭包包含了:

  1. 内部函数本身
  2. 外部函数的变量(自由变量)
  3. 执行上下文
javascript 复制代码
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

🎪 闭包的应用场景

闭包在JavaScript中有着广泛的应用场景,让我们逐一深入探讨:

1. 记忆函数(Memoization)

2. 柯里化(Currying)

3. 防抖与节流(Debounce & Throttle)

4. 私有变量与封装

5. 事件监听器

6. 立即执行函数(IIFE)

7. 模块模式

8. 偏函数应用

🏠 私有变量与封装

传统面向对象的封装需求

在传统的面向对象编程中,类的封装是一个重要概念:

  • 对内(private):隐藏内部实现细节,保护数据完整性
  • 对外(public):提供公共接口,供外部调用

JavaScript在ES6之前没有真正的私有变量概念,但可以通过闭包来模拟实现。

使用闭包创建私有变量

让我们看一个经典的计数器例子:

javascript 复制代码
function CreateCounter(num) {
    // 对外的接口
    // 对内的私有
    this.num = num;
    
    // 私有变量
    // 私有的数据属性 private
    let count = 0;
    
    return {
        num,
        increment: function() {
            count++;
        },
        decrement: function() {
            count--;
        },
        getCount: function() {
            console.log("count value is ", count);
            return count;
        },
    };
}

const counter = CreateCounter(1);
// console.log(counter.count); // undefined, 因为 count 是私有变量,不能直接访问
// 闭包延长了变量的生命周期
// 不直接操作它。
counter.increment(); // 让方法来操作它
console.log(counter.num); // 1
counter.getCount(); // count value is 1

更复杂的封装实例:图书管理系统

javascript 复制代码
function Book(title, author, year) {
    // 对内的私有属性
    // 对外的公有属性
    let _title = title;    // 约定_varname 为私有属性,内部有利于可读性的编程风格
    let _author = author;
    let _year = year;
    
    // 公共方法
    this.getTitle = function() {
        return _title;
    };
    
    // 私有方法
    function getFullTitle() {
        return `${_title} by ${_author}`;
    }
    
    // 公共方法调用私有方法
    this.getFullInfo = function() {
        return getFullTitle() + `, published in ${_year}`;
    };
    
    this.getAuthor = function() {
        return _author;
    };
    
    this.getYear = function() {
        return _year;
    };
    
    // 类的开发者和使用者,可能是两拨人
    // 大型项目防止后续的使用者错误使用
    this.updateYear = function(newYear) {
        if (typeof newYear === 'number' && !isNaN(newYear) && newYear > 0) {
            _year = newYear;
        } else {
            console.error('|||Invalid year|||');
        }
    };
}

// 使用示例
const book = new Book('JavaScript高级程序设计', 'Nicholas C. Zakas', 2020);
const info = book.getFullInfo();
console.log(info); // JavaScript高级程序设计 by Nicholas C. Zakas, published in 2020

book.updateYear(2021);
book.updateYear('invalid year'); // |||Invalid year|||
console.log(book.getFullInfo()); // JavaScript高级程序设计 by Nicholas C. Zakas, published in 2021

封装的优势

  1. 数据安全:外部无法直接访问私有变量
  2. 接口稳定:通过公共方法控制数据访问
  3. 代码维护:内部实现可以随时修改,不影响外部调用
  4. 错误防护:通过验证逻辑防止无效数据

⚡ 防抖与节流

在现代Web开发中,用户交互事件(如输入、滚动、点击等)往往会高频触发。如果不加以控制,可能会导致性能问题和资源浪费。防抖(Debounce)和节流(Throttle)是解决这类问题的两种重要策略。

防抖(Debounce)

防抖的核心思想是:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

基础防抖实现
javascript 复制代码
function debounce(fn, delay) {
    return function(args) {
        // fn 是自由变量
        // 函数也是一等对象
        // 给对象一个id 属性
        let that = this; // 保存 this 上下文
        clearTimeout(fn.id);
        fn.id = setTimeout(() => {
            // this 丢失问题
            fn.call(that, args);
        }, delay);
    };
}

let obj = {
    count: 0,
    inc: debounce(function(val) {
        console.log(this, "||||");
        this.count += val;
        console.log(this.count);
    }, 500),
};

obj.inc(2);
实际应用场景:搜索建议
html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>防抖节流</title>
</head>
<body>
    <input type="text" id="inputA" placeholder="正常输入">
    <input type="text" id="inputB" placeholder="防抖输入">
    
    <script>
        const inputA = document.getElementById('inputA');
        const inputB = document.getElementById('inputB');
        
        // 模拟耗时的AJAX请求
        function ajax(content) {
            console.log('ajax request:', content);
            // 实际应用中这里会是真实的API调用
        }
        
        // 通用防抖函数
        function debounce(fn, delay) {
            return function(args) {
                clearTimeout(fn.id);
                fn.id = setTimeout(() => {
                    fn(args);
                }, delay);
            };
        }
        
        // 创建防抖版本的ajax函数
        let debounceAjax = debounce(ajax, 300);
        
        // 对比效果
        inputA.addEventListener('keyup', (e) => {
            console.log('正常keyup');
            ajax(e.target.value);
        });
        
        inputB.addEventListener('keyup', function(e) {
            console.log('防抖keyup');
            debounceAjax(e.target.value);
        });
    </script>
</body>
</html>

节流(Throttle)

节流的核心思想是:单位时间内只执行一次函数,无论触发多少次。

节流实现
javascript 复制代码
const throttle = function(fn, delay) {
    let last = null;     // 上一次的执行时间
    let deferTimer = null; // 定时器的id
    
    return function() {
        let args = arguments;
        let that = this;    // 闭包的运用场景
        let now = +new Date(); // 类型转换
        
        if (last && now < last + delay) {
            clearTimeout(deferTimer);
            deferTimer = setTimeout(() => {
                last = now;
                fn.apply(that, args);
            }, delay);
        } else {
            last = now;
            fn.apply(that, args);
        }
    };
};
实际应用:滚动事件优化
html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>节流示例</title>
</head>
<body>
    <input type="text" id="inputC" placeholder="节流输入">
    
    <script>
        let inputC = document.getElementById('inputC');
        
        const ajax = (text) => {
            console.log('ajax请求:', text);
            // 模拟ajax请求
            // 实际上可以使用fetch或XMLHttpRequest来发送请求
        };
        
        const throttle = function(fn, delay) {
            let last = null;
            let deferTimer = null;
            
            return function() {
                let args = arguments;
                let that = this;
                let now = +new Date();
                
                if (last && now < last + delay) {
                    clearTimeout(deferTimer);
                    deferTimer = setTimeout(() => {
                        last = now;
                        fn.apply(that, args);
                    }, delay);
                } else {
                    last = now;
                    fn.apply(that, args);
                }
            };
        };
        
        let throttleAjax = throttle(ajax, 1000);
        
        inputC.addEventListener('input', function(e) {
            console.log('输入值:', e.target.value);
            throttleAjax(e.target.value);
            // 防止任务超载
        });
    </script>
</body>
</html>

防抖与节流的对比

特性 防抖 (Debounce) 节流 (Throttle)
执行时机 停止触发后延迟执行 固定时间间隔执行
触发频率 可能只执行一次 保证定期执行
典型场景 搜索框输入、按钮防重复点击 滚动事件、鼠标移动
核心思想 "等等再执行" "限制执行频率"

应用场景总结

防抖适用于:

  • 搜索框输入联想
  • 按钮防重复点击
  • 文本输入验证
  • 窗口resize事件

节流适用于:

  • 页面滚动事件
  • 鼠标移动事件
  • 播放进度条拖拽
  • 高频点击事件

🎧 事件监听与上下文绑定

在JavaScript中,事件监听器经常遇到this指向问题。闭包提供了一种优雅的解决方案。

this指向问题

javascript 复制代码
const obj = {
    message: 'Hello, World!',
    init: function() {
        const button = document.getElementById('myButton');
        
        // 问题:this指向可能丢失
        button.addEventListener('click', function() {
            console.log(this.message); // undefined,this指向button
        });
    }
};

闭包解决方案

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>闭包的事件监听</title>
</head>
<body>
    <button id="myButton">点击我</button>
    <script>
        const obj = {
            message: 'Hello, World!',
            init: function() {
                const button = document.getElementById('myButton');
                const that = this; // 闭包保存this引用
                
                button.addEventListener('click', function(e) {
                    console.log("this.message:", this.message);    // undefined
                    console.log("that.message:", that.message);   // Hello, World!
                });
            }
        };
        
        obj.init();
    </script>
</body>
</html>

多种this绑定方式对比

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>this绑定方式对比</title>
</head>
<body>
    <script>
        const person = {
            name: "Allen",
            sayHello: function() {
                let that = this;
                
                // 方式1:箭头函数(推荐)
                setTimeout(() => {
                    console.log(`Hello, my name is ${this.name}`);
                }, 1000);
                
                // 方式2:普通函数(this丢失)
                setTimeout(function() {
                    console.log(`Hello, my name is ${this.name}`); // undefined
                }, 1000);
                
                // 方式3:闭包保存this
                setTimeout(function() {
                    console.log(`Hello, my name is ${that.name}`);
                }, 1000);
                
                // 方式4:bind绑定
                setTimeout(function() {
                    console.log(`Hello, my name is ${this.name}`);
                }.bind(this), 1000);
            }
        };
        
        person.sayHello();
    </script>
</body>
</html>

三种绑定方式总结

  1. 箭头函数:自动绑定外层this,最简洁
  2. 闭包(that = this):手动保存this引用,兼容性好
  3. bind方法:显式绑定this,灵活但略繁琐

🔄 立即执行函数(IIFE)

立即执行函数表达式(Immediately Invoked Function Expression)是闭包的一个重要应用,常用于创建独立的作用域。

IIFE基本语法

javascript 复制代码
(function() {
    // 代码块
})();

// 或者
(function() {
    // 代码块
}());

使用IIFE创建模块

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>IIFE模块模式</title>
</head>
<body>
    <script>
        const Counter = (function() {
            let count = 0;    // 自由变量,私有
            
            function increment() {
                count++;
            }
            
            function reset() {
                count = 0;
            }
            
            // 返回工厂函数
            return function() {
                return {
                    getCount: function() {
                        return count;
                    },
                    increment,
                    reset,
                };
            };
        })();
        
        const firstInstance = Counter();
        const secondInstance = Counter();
        
        console.log(firstInstance.getCount()); // 0
        firstInstance.increment();
        firstInstance.increment();
        firstInstance.increment();
        console.log(firstInstance.getCount()); // 3
        
        console.log(secondInstance.getCount()); // 0 (独立的实例)
    </script>
</body>
</html>

IIFE的优势

  1. 避免全局污染:创建独立作用域
  2. 立即执行:代码立即运行,不需要额外调用
  3. 变量隔离:防止变量冲突
  4. 模块化:实现简单的模块系统

实际应用场景

javascript 复制代码
// 1. 初始化代码
(function() {
    // 应用初始化逻辑
    console.log('Application initialized');
})();

// 2. 配置对象
const AppConfig = (function() {
    const config = {
        apiUrl: 'https://api.example.com',
        timeout: 5000,
        debug: false
    };
    
    return {
        get: function(key) {
            return config[key];
        },
        set: function(key, value) {
            if (config.hasOwnProperty(key)) {
                config[key] = value;
            }
        }
    };
})();

// 3. 计数器工厂
const createCounter = (function() {
    let instanceCount = 0;
    
    return function(initialValue = 0) {
        instanceCount++;
        let count = initialValue;
        
        return {
            id: instanceCount,
            increment: () => ++count,
            decrement: () => --count,
            getValue: () => count,
            reset: () => count = initialValue
        };
    };
})();

🧠 记忆函数与缓存

记忆函数(Memoization)是一种优化技术,通过缓存函数的计算结果来避免重复计算。

基础记忆函数实现

javascript 复制代码
function memoize(fn) {
    const cache = new Map();
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (cache.has(key)) {
            console.log('从缓存中获取结果');
            return cache.get(key);
        }
        
        console.log('计算新结果');
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

// 使用示例
const fibonacci = memoize(function(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(10)); // 计算新结果
console.log(fibonacci(10)); // 从缓存中获取结果

高级记忆函数实现

javascript 复制代码
function createMemoizedFunction(fn, options = {}) {
    const cache = new Map();
    const { maxSize = 100, ttl = 0 } = options;
    
    return function(...args) {
        const key = JSON.stringify(args);
        const now = Date.now();
        
        // 检查缓存是否存在且未过期
        if (cache.has(key)) {
            const cached = cache.get(key);
            if (ttl === 0 || now - cached.timestamp < ttl) {
                return cached.value;
            } else {
                cache.delete(key);
            }
        }
        
        // 计算新结果
        const result = fn.apply(this, args);
        
        // 缓存大小控制
        if (cache.size >= maxSize) {
            const firstKey = cache.keys().next().value;
            cache.delete(firstKey);
        }
        
        // 存储结果
        cache.set(key, {
            value: result,
            timestamp: now
        });
        
        return result;
    };
}

// 使用示例
const expensiveFunction = createMemoizedFunction(
    function(x, y) {
        // 模拟耗时计算
        console.log(`计算 ${x} + ${y}`);
        return x + y;
    },
    { maxSize: 50, ttl: 5000 } // 最大缓存50个结果,TTL 5秒
);

console.log(expensiveFunction(1, 2)); // 计算 1 + 2
console.log(expensiveFunction(1, 2)); // 从缓存获取

实际应用场景

javascript 复制代码
// API调用缓存
const cachedFetch = memoize(async function(url) {
    const response = await fetch(url);
    return response.json();
});

// 复杂计算缓存
const cachedFactorial = memoize(function(n) {
    if (n <= 1) return 1;
    return n * cachedFactorial(n - 1);
});

// DOM查询缓存
const cachedQuery = memoize(function(selector) {
    return document.querySelectorAll(selector);
});

🍛 柯里化与偏函数

柯里化(Currying)是函数式编程的重要概念,通过闭包可以优雅地实现。

柯里化的基本概念

柯里化是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数。

javascript 复制代码
// 普通函数
function add(a, b, c) {
    return a + b + c;
}

// 柯里化后的函数
function curryAdd(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        };
    };
}

// 使用
const add5 = curryAdd(5);
const add5And3 = add5(3);
const result = add5And3(2); // 10

通用柯里化函数

javascript 复制代码
function curry(fn, ...args) {
    return function(...newArgs) {
        const allArgs = [...args, ...newArgs];
        
        if (allArgs.length >= fn.length) {
            return fn.apply(this, allArgs);
        } else {
            return curry(fn, ...allArgs);
        }
    };
}

// 使用示例
const multiply = (a, b, c) => a * b * c;
const curriedMultiply = curry(multiply);

console.log(curriedMultiply(2)(3)(4)); // 24
console.log(curriedMultiply(2, 3)(4)); // 24
console.log(curriedMultiply(2)(3, 4)); // 24

偏函数应用

偏函数是固定函数的某些参数,返回一个新的函数。

javascript 复制代码
function partial(fn, ...presetArgs) {
    return function(...laterArgs) {
        return fn.apply(this, [...presetArgs, ...laterArgs]);
    };
}

// 使用示例
const log = (level, message) => {
    console.log(`[${level}] ${message}`);
};

const logError = partial(log, 'ERROR');
const logWarning = partial(log, 'WARNING');

logError('Something went wrong!');    // [ERROR] Something went wrong!
logWarning('This is a warning!');     // [WARNING] This is a warning!

实际应用场景

javascript 复制代码
// 1. 事件处理器
const handleClick = curry(function(eventType, element, handler) {
    element.addEventListener(eventType, handler);
});

const addClickListener = handleClick('click');
const button = document.getElementById('myButton');
addClickListener(button, () => console.log('Button clicked!'));

// 2. 数据验证
const validate = curry(function(rule, value) {
    return rule(value);
});

const isRequired = value => value != null && value !== '';
const isEmail = value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);

const validateRequired = validate(isRequired);
const validateEmail = validate(isEmail);

console.log(validateRequired('test'));        // true
console.log(validateEmail('test@email.com')); // true

// 3. 数据处理管道
const pipe = (...fns) => value => fns.reduce((acc, fn) => fn(acc), value);

const addTax = curry((rate, price) => price * (1 + rate));
const applyDiscount = curry((discount, price) => price * (1 - discount));
const formatPrice = price => `$${price.toFixed(2)}`;

const calculateFinalPrice = pipe(
    addTax(0.1),
    applyDiscount(0.15),
    formatPrice
);

console.log(calculateFinalPrice(100)); // $93.50

⚡ 闭包的性能考虑

虽然闭包很强大,但也需要注意性能问题。

内存泄漏风险

javascript 复制代码
// 潜在的内存泄漏
function createHandler() {
    const largeData = new Array(1000000).fill('data');
    
    return function(event) {
        // 即使不使用largeData,它也会被保留在内存中
        console.log('Event handled');
    };
}

// 改进版本
function createHandler() {
    const largeData = new Array(1000000).fill('data');
    
    // 处理数据
    const processedData = largeData.map(item => item.toUpperCase());
    
    return function(event) {
        // 只保留需要的数据
        console.log('Event handled');
    };
}

性能优化建议

javascript 复制代码
// 1. 避免在循环中创建闭包
// 不好的做法
for (let i = 0; i < 1000; i++) {
    setTimeout(function() {
        console.log(i);
    }, 100);
}

// 好的做法
function createTimeoutHandler(index) {
    return function() {
        console.log(index);
    };
}

for (let i = 0; i < 1000; i++) {
    setTimeout(createTimeoutHandler(i), 100);
}

// 2. 及时清理不需要的引用
function createCachedFunction() {
    let cache = new Map();
    
    function cachedFn(key) {
        if (cache.has(key)) {
            return cache.get(key);
        }
        
        const result = expensiveOperation(key);
        cache.set(key, result);
        return result;
    }
    
    // 提供清理方法
    cachedFn.clear = function() {
        cache.clear();
        cache = null;
    };
    
    return cachedFn;
}

🎯 最佳实践与注意事项

1. 合理使用闭包

javascript 复制代码
// 好的使用方式
function createValidator(rules) {
    return function(data) {
        return rules.every(rule => rule(data));
    };
}

// 避免过度使用
function simpleAdd(a, b) {
    // 不需要闭包的简单函数
    return a + b;
}

2. 内存管理

javascript 复制代码
// 避免循环引用
function createCircularReference() {
    const obj = {};
    
    obj.method = function() {
        // 这里引用了obj,形成循环引用
        console.log(obj);
    };
    
    return obj;
}

// 改进版本
function createObject() {
    const obj = {};
    
    obj.method = function() {
        // 使用this代替直接引用
        console.log(this);
    };
    
    return obj;
}

3. 调试技巧

javascript 复制代码
// 为闭包函数命名,便于调试
const createCounter = function createCounter(initialValue) {
    let count = initialValue;
    
    return function increment() {
        return ++count;
    };
};

// 使用console.dir查看闭包
const counter = createCounter(0);
console.dir(counter);

4. 测试策略

javascript 复制代码
// 创建可测试的闭包
function createTestableModule() {
    let privateVar = 0;
    
    function privateFunction() {
        return privateVar * 2;
    }
    
    return {
        // 公共接口
        publicMethod: function() {
            return privateFunction();
        },
        
        // 测试接口(仅在测试环境中暴露)
        __test__: {
            getPrivateVar: () => privateVar,
            setPrivateVar: (val) => privateVar = val,
            callPrivateFunction: privateFunction
        }
    };
}

📚 总结

闭包是JavaScript中最强大和最优雅的特性之一。通过本文的详细探讨,我们了解了闭包在实际开发中的各种应用:

🎯 核心应用场景

  1. 数据封装:创建私有变量和方法,实现真正的封装
  2. 状态管理:在函数调用之间保持状态
  3. 事件处理:解决this指向问题,保持上下文
  4. 性能优化:通过防抖、节流、记忆化等技术提升性能
  5. 函数式编程:实现柯里化、偏函数等高级技巧
  6. 模块化:通过IIFE创建独立的模块作用域

🔧 技术要点

  • 理解闭包的本质:函数+外部变量的引用
  • 掌握应用场景:知道何时使用闭包解决问题
  • 注意性能影响:避免内存泄漏和性能问题
  • 遵循最佳实践:写出可维护、可测试的代码

🚀 进阶方向

  1. 深入学习函数式编程:掌握更多高阶函数技巧
  2. 探索设计模式:学习如何用闭包实现各种设计模式
  3. 性能优化:了解V8引擎的闭包优化机制
  4. 框架应用:研究主流框架中闭包的使用

闭包不仅是JavaScript的语法特性,更是编程思维的体现。掌握闭包,就是掌握了JavaScript编程的核心技能之一。希望本文能帮助你在实际项目中更好地运用闭包,写出更优雅、更高效的代码。


本文基于实际项目经验编写,所有代码示例都经过测试验证。如有问题或建议,欢迎交流讨论。

相关推荐
中微子1 小时前
🔥 React Context 面试必考!从源码到实战的完整攻略 | 99%的人都不知道的性能陷阱
前端·react.js
秋田君1 小时前
深入理解JavaScript设计模式之命令模式
javascript·设计模式·命令模式
中微子2 小时前
React 状态管理 源码深度解析
前端·react.js
风吹落叶花飘荡3 小时前
2025 Next.js项目提前编译并在服务器
服务器·开发语言·javascript
加减法原则3 小时前
Vue3 组合式函数:让你的代码复用如丝般顺滑
前端·vue.js
yanlele3 小时前
我用爬虫抓取了 25 年 6 月掘金热门面试文章
前端·javascript·面试
lichenyang4534 小时前
React移动端开发项目优化
前端·react.js·前端框架
你的人类朋友4 小时前
🍃Kubernetes(k8s)核心概念一览
前端·后端·自动化运维
web_Hsir4 小时前
vue3.2 前端动态分页算法
前端·算法
烛阴4 小时前
WebSocket实时通信入门到实践
前端·javascript