阅读视图

发现新文章,点击刷新页面。

可怕!我的Nodejs系统因为日志打印了Error 对象就崩溃了😱 Node.js System Crashed Because of Logging

本文为中英文双语,需要英文博客可以滑动到下面查看哦 | This is a bilingual article. Scroll down for the English version.

小伙伴们!今天我在本地调试项目的过程中,想记录一下错误信息,结果程序就"啪"地一下报出 "Maximum call stack size exceeded" 错误,然后项目直接就crash了。但是我看我用的这个开源项目,官方的代码里好多地方就是这么用的呀?我很纳闷,这是为什么呢?

Snipaste_2025-10-10_00-28-45.png

报错信息


[LOGGER PARSING ERROR] Maximum call stack size exceeded
2025-10-13T17:06:59.643Z debug: Error code: 400 - {'error': {'message': 'Budget has been exceeded! Current cost: 28.097367900000002, Max budget: 0.0', 'type': 'budget_exceeded', 'par... [truncated]
{
  unknown: [object Object],
}
2025-10-13T17:06:59.643Z debug: [api/server/middleware/abortMiddleware.js] respondWithError called
2025-10-13T17:06:59.644Z error: There was an uncaught error: Cannot read properties of undefined (reading 'emit')
2025-10-13T17:06:59.645Z debug: [indexSync] Clearing sync timeouts before exiting...
[nodemon] app crashed - waiting for file changes before starting...

报错截图

image

错误分析

晚上下班以后,晚上躺在床上,我翻来覆去睡不着,干脆打开电脑一番探究,想要知道 ,这个错误到底为何触发,实质原因是什么,以及如何解决它。让我们一起把这个小调皮鬼揪出来看看它到底在搞什么鬼吧!👻

场景复现

想象一下这个场景,你正在开心地写着代码:

app.get('/api/data', async (req, res) => {
  try {
    // 一些可能会出小差错的业务逻辑
    const data = await fetchDataFromAPI();
    res.json(data);
  } catch (error) {
    // 记录错误信息
    logger.debug('获取数据时出错啦~', error); // 哎呀!这一行可能会让我们的程序崩溃哦!
    res.status(500).json({ error: '内部服务器出错啦~' });
  }
});

看起来是不是很正常呢?但是当你运行这段代码的时候,突然就出现了这样的错误:

[LOGGER PARSING ERROR] Maximum call stack size exceeded

更神奇的是,如果你把代码改成这样:

console.log(error); // 这一行却不会让程序崩溃哦,但是上prod的系统,不要这么用哦

它就能正常工作啦!这是为什么呢?🤔

小秘密大揭秘!🔍

console.log虽好,但请勿用它来记录PROD错误!

console.log 是 Node.js 原生提供的函数,它就像一个经验超级丰富的大叔,知道怎么处理各种"调皮"的对象。当 console.log 遇到包含循环引用的对象时,它会聪明地检测这些循环引用,并用 [Circular] 标记来代替实际的循环部分,这样就不会无限递归啦!

简单来说,Node.js 的 console.log 就像一个超级厉害的武林高手,知道如何闪转腾挪,避开各种陷阱!🥋

日志库的"小烦恼"

但是我们自己封装的日志系统(比如项目中使用的 Winston)就不一样啦!为了实现各种炫酷的功能(比如格式化、过滤敏感信息等),日志库通常会使用一些第三方库来处理传入的对象。

在我们的案例中,日志系统使用了 [traverse] 库来遍历对象。这个库在大多数情况下工作得都很好,但当它遇到某些复杂的 Error 对象时,就可能会迷路啦!

Error 对象可不是普通对象那么简单哦!它们可能包含各种隐藏的属性、getter 方法,甚至在某些情况下会动态生成属性。当 [traverse] 库尝试遍历这些复杂结构时,就可能陷入无限递归的迷宫,最终导致调用栈溢出。

什么是循环引用?🌀

在深入了解这个问题之前,我们先来了解一下什么是循环引用。循环引用指的是对象之间相互引用,形成一个闭环。比如说:

const objA = { name: '小A' };
const objB = { name: '小B' };

objA.ref = objB;
objB.ref = objA; // 哎呀!形成循环引用啦!

当尝试序列化这样的对象时(比如用 JSON.stringify),就会出现问题,因为序列化过程会无限递归下去,就像两只小仓鼠在滚轮里永远跑不完一样!🐹

Error 对象虽然看起来简单,但内部结构可能非常复杂,特别是在一些框架或库中创建的 Error 对象,它们可能包含对 request、response 等对象的引用,而这些对象又可能包含对 Error 对象的引用,从而形成复杂的循环引用网络,就像一张大蜘蛛网一样!🕷️

怎样才能让我们的日志系统乖乖听话呢?✨

1. 只记录我们需要的信息

最简单直接的方法就是不要把整个 Error 对象传递给日志函数,而是只传递我们需要的具体属性:

// ❌ 不推荐的做法 - 会让日志系统"生气"
logger.debug('获取数据时出错啦~', error);

// ✅ 推荐的做法 - 让日志系统开心地工作
logger.debug('获取数据时出错啦~', {
  message: error.message,
  stack: error.stack,
  code: error.code
});

2. 使用专门的错误序列化函数

你可以创建一个专门用于序列化 Error 对象的函数,就像给 Error 对象穿上一件"安全外套":

function serializeError(error) {
  return {
    name: error.name,
    message: error.message,
    stack: error.stack,
    code: error.code,
    // 添加其他你需要的属性
  };
}

// 使用方式
logger.debug('获取数据时出错啦~', serializeError(error));

3. 使用成熟的错误处理库

有些库专门为处理这类问题而设计,比如 serialize-error,它们就像专业的保姆一样,会把 Error 对象照顾得好好的:

const { serializeError } = require('serialize-error');

logger.debug('获取数据时出错啦~', serializeError(error));

4. 配置日志库的防护机制

如果你使用的是 Winston,可以配置一些防护机制,给它穿上"防弹衣":

const winston = require('winston');

const logger = winston.createLogger({
  format: winston.format.combine(
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  // ... 其他配置
});

最佳实践小贴士 🌟

  1. 永远不要直接记录原始的 Error 对象:它们可能包含复杂的循环引用结构,就像一个调皮的小恶魔。

  2. 提取关键信息:只记录我们需要的错误信息,比如 message、stack 等,就像挑选糖果一样只拿最喜欢的。

  3. 使用安全的序列化方法:确保我们的日志系统能够处理各种边界情况,做一个贴心的小棉袄。

  4. 添加防护措施:在日志处理逻辑中添加 try-catch 块,防止日志系统本身成为故障点,就像给程序戴上安全帽。

  5. 测试边界情况:在测试中模拟各种错误场景,确保日志系统在极端情况下也能正常工作,做一个负责任的好孩子。

image

Terrifying! My Node.js System Crashed Because of Logging an Error Object 😱

Fellow developers! Today, while debugging a project locally, I wanted to log some error information, but suddenly the program threw a "Maximum call stack size exceeded" error and crashed the entire project. But when I look at the open-source project I'm using, I see that the official code does this in many places. I was puzzled, why is this happening?

Error Message


[LOGGER PARSING ERROR] Maximum call stack size exceeded
2025-10-13T17:06:59.643Z debug: Error code: 400 - {'error': {'message': 'Budget has been exceeded! Current cost: 28.097367900000002, Max budget: 0.0', 'type': 'budget_exceeded', 'par... [truncated]
{
  unknown: [object Object],
}
2025-10-13T17:06:59.643Z debug: [api/server/middleware/abortMiddleware.js] respondWithError called
2025-10-13T17:06:59.644Z error: There was an uncaught error: Cannot read properties of undefined (reading 'emit')
2025-10-13T17:06:59.645Z debug: [indexSync] Clearing sync timeouts before exiting...
[nodemon] app crashed - waiting for file changes before starting...

Error Screenshot

image

Error Analysis

After work, I couldn't resist investigating why this error was triggered, what the root cause was, and how to solve it. Let's together catch this little troublemaker and see what it's up to! 👻

Reproducing the Scenario

Imagine this scenario, you're happily coding:

app.get('/api/data', async (req, res) => {
  try {
    // Some business logic that might go wrong
    const data = await fetchDataFromAPI();
    res.json(data);
  } catch (error) {
    // Log the error
    logger.debug('Error fetching data~', error); // Oops! This line might crash our program!
    res.status(500).json({ error: 'Internal server error~' });
  }
});

Doesn't this look normal? But when you run this code, suddenly this error appears:

[LOGGER PARSING ERROR] Maximum call stack size exceeded

What's even more神奇 is, if you change the code to this:

console.log(error); // This line won't crash the program, but don't use this in production systems

It works fine! Why is that? 🤔

The Big Reveal of Little Secrets! 🔍

console.log is Good, But Don't Use It to Log PROD Errors!

console.log is a native Node.js function. It's like an extremely experienced uncle who knows how to handle all kinds of "naughty" objects. When console.log encounters objects with circular references, it cleverly detects these circular references and replaces the actual circular parts with [Circular] markers, so it won't recurse infinitely!

Simply put, Node.js's console.log is like a super skilled martial arts master who knows how to dodge and avoid all kinds of traps! 🥋

The "Little Troubles" of Logging Libraries

But our custom logging systems (like Winston used in the project) are different! To implement various cool features (like formatting, filtering sensitive information, etc.), logging libraries often use third-party libraries to process incoming objects.

In our case, the logging system uses the [traverse] library to traverse objects. This library works well in most cases, but when it encounters certain complex Error objects, it might get lost!

Error objects are not as simple as ordinary objects! They may contain various hidden properties, getter methods, and in some cases, dynamically generated properties. When the [traverse] library tries to traverse these complex structures, it may fall into an infinite recursion maze, ultimately causing a stack overflow.

What Are Circular References? 🌀

Before diving deeper into this issue, let's first understand what circular references are. Circular references refer to objects that reference each other, forming a closed loop. For example:

const objA = { name: 'A' };
const objB = { name: 'B' };

objA.ref = objB;
objB.ref = objA; // Oops! Circular reference formed!

When trying to serialize such objects (like with JSON.stringify), problems arise because the serialization process will recurse infinitely, like two hamsters running forever in a wheel! 🐹

Although Error objects look simple, their internal structure can be very complex, especially Error objects created in some frameworks or libraries. They may contain references to request, response, and other objects, and these objects may in turn contain references to the Error object, forming a complex circular reference network, like a giant spider web! 🕷️

How to Make Our Logging System Behave? ✨

1. Only Log the Information We Need

The simplest and most direct method is not to pass the entire Error object to the logging function, but to pass only the specific properties we need:

// ❌ Not recommended - will make the logging system "angry"
logger.debug('Error fetching data~', error);

// ✅ Recommended - makes the logging system work happily
logger.debug('Error fetching data~', {
  message: error.message,
  stack: error.stack,
  code: error.code
});

2. Use a Dedicated Error Serialization Function

You can create a dedicated function for serializing Error objects, like putting a "safety coat" on the Error object:

function serializeError(error) {
  return {
    name: error.name,
    message: error.message,
    stack: error.stack,
    code: error.code,
    // Add other properties you need
  };
}

// Usage
logger.debug('Error fetching data~', serializeError(error));

3. Use Mature Error Handling Libraries

Some libraries are specifically designed to handle these kinds of issues, such as serialize-error. They're like professional nannies who will take good care of Error objects:

const { serializeError } = require('serialize-error');

logger.debug('Error fetching data~', serializeError(error));

4. Configure Protective Mechanisms for Logging Libraries

If you're using Winston, you can configure some protective mechanisms to give it "bulletproof armor":

const winston = require('winston');

const logger = winston.createLogger({
  format: winston.format.combine(
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  // ... other configurations
});

Best Practice Tips 🌟

  1. Never log raw Error objects directly: They may contain complex circular reference structures, like a mischievous little devil.

  2. Extract key information: Only log the error information we need, such as message, stack, etc., like picking candy - only take your favorites.

  3. Use safe serialization methods: Ensure our logging system can handle various edge cases, be a thoughtful companion.

  4. Add protective measures: Add try-catch blocks in the logging logic to prevent the logging system itself from becoming a failure point, like giving the program a safety helmet.

  5. Test edge cases: Simulate various error scenarios in testing to ensure the logging system works properly under extreme conditions, be a responsible good child.

Conclusion | 结语

  • That's all for today~ - | 今天就写到这里啦~

  • Guys, ( ̄ω ̄( ̄ω ̄〃 ( ̄ω ̄〃)ゝ See you tomorrow~ | 小伙伴们,( ̄ω ̄( ̄ω ̄〃 ( ̄ω ̄〃)ゝ我们明天再见啦~~

  • Everyone, be happy every day! 大家要天天开心哦

  • Welcome everyone to point out any mistakes in the article~ | 欢迎大家指出文章需要改正之处~

  • Learning has no end; win-win cooperation | 学无止境,合作共赢

  • Welcome all the passers-by, boys and girls, to offer better suggestions! ~ | 欢迎路过的小哥哥小姐姐们提出更好的意见哇~~

理解 JavaScript 中的 this 上下文保存

保存 this 上下文是 JavaScript 中一个非常重要的概念,尤其是在处理闭包、定时器等场景时。让我们深入理解这个概念。

this 是什么?

this 是 JavaScript 中的一个特殊关键字,它指向的是当前代码执行的上下文对象。简单来说,this 的值取决于函数被调用的方式,而不是函数被定义的位置。

为什么需要保存 this 上下文?

在防抖函数中,我们遇到了一个典型问题:在 setTimeout 回调函数中,this 的指向会发生变化

让我们看一个例子来说明这个问题:

function debounce(func, wait) {
    let timeout;
    
    return function executedFunction(...args) {
        // 这里的 this 指向的是调用 debounced 函数的对象
        console.log('外层 this:', this); // 假设是按钮元素
        
        timeout = setTimeout(function() {
            // 这里的 this 默认指向 window 或 undefined(严格模式)
            console.log('setTimeout 中的 this:', this);
            func.apply(this, args); // 这会导致错误,因为 this 已经变了
        }, wait);
    };
}

问题所在:当我们在 setTimeout 的回调函数中使用 this 时,它不再指向原始调用上下文(比如按钮元素),而是指向全局对象 window(非严格模式)或 undefined(严格模式)。

如何正确保存 this 上下文

为了解决这个问题,我们需要在进入 setTimeout 之前保存原始的 this 引用:

function debounce(func, wait) {
    let timeout;
    
    return function executedFunction(...args) {
        // 保存原始的 this 上下文
        const context = this; // 关键步骤!
        
        timeout = setTimeout(function() {
            // 现在我们使用保存的 context 而不是这里的 this
            func.apply(context, args);
        }, wait);
    };
}

通过 const context = this; 这行代码,我们将原始的 this 引用保存到了 context 变量中,这样即使在 setTimeout 回调函数中 this 发生了变化,我们仍然可以通过 context 访问到原始的上下文。

实际应用场景示例

让我们看一个更贴近实际开发的例子:

// 假设我们有一个计数器对象
const counter = {
    count: 0,
    increment: function() {
        this.count++;
        console.log(`当前计数: ${this.count}`);
    }
};

// 创建防抖版本的 increment 方法
const debouncedIncrement = debounce(counter.increment, 1000);

// 添加事件监听
button.addEventListener('click', debouncedIncrement);

如果防抖函数中没有正确保存 this 上下文,点击按钮时会出现错误,因为 this.count 会变成 undefined.count

但如果我们使用正确实现的防抖函数(保存了 this 上下文),就不会有问题:

button.addEventListener('click', function() {
    // 手动绑定 this 到 counter
    debouncedIncrement.call(counter);
});

总结

保存 this 上下文是 JavaScript 中处理函数调用的重要技巧,特别是在使用闭包和定时器时:

  1. this 的值取决于函数被调用的方式
  2. setTimeout 等异步回调中,this 的指向会改变
  3. 通过在异步操作前保存 this 引用,我们可以确保函数在正确的上下文中执行
  4. applycall 方法允许我们显式地设置函数执行的上下文

理解并掌握 this 的工作原理,对于前端开发者至关重要,前端学习ing,欢迎各位佬指正

JavaScript字符串填充:padStart()方法

原文:xuanhu.info/projects/it…

JavaScript字符串填充:padStart()方法

在编程实践中,字符串填充是高频操作需求。无论是格式化输出、数据对齐还是生成固定格式标识符,都需要高效可靠的填充方案。本文将深入探讨JavaScript中最优雅的字符串填充方案——padStart()方法,通过理论解析+实战案例带你掌握这一核心技能。

🧩 字符串填充的本质需求

字符串填充指在原始字符串的指定侧添加特定字符直至达到目标长度。常见应用场景包括:

  • 数字补零(如日期格式化 "2023-1-1" → "2023-01-01")
  • 表格数据对齐
  • 生成固定长度交易号
  • 控制台输出美化

🚫 传统填充方案的痛点

在ES2017规范前,开发者通常采用以下方式实现填充:

// 手动实现左填充函数
function leftPad(str, length, padChar = ' ') {
  const padCount = length - str.length;
  return padCount > 0 
    ? padChar.repeat(padCount) + str 
    : str;
}

console.log(leftPad('42', 5, '0')); // "00042"

这种方案存在三大缺陷:

  1. 代码冗余:每个项目需重复实现工具函数
  2. 边界处理复杂:需手动处理超长字符串、空字符等边界情况
  3. 性能瓶颈:大数量级操作时循环效率低下

✨ padStart()方法

ES2017引入的padStart()是String原型链上的原生方法,完美解决上述痛点。

📚 方法参数

/**
 * 字符串起始位置填充
 * @param {number} targetLength - 填充后目标长度
 * @param {string} [padString=' '] - 填充字符(默认空格)
 * @returns {string} 填充后的新字符串
 */
String.prototype.padStart(targetLength, padString);

🔬 核心特性详解

  1. 智能截断:当填充字符串超出需要长度时自动截断

    '7'.padStart(3, 'abcdef'); // "ab7" 
    
  2. 类型安全:自动转换非字符串参数

    const price = 9.9;
    price.toString().padStart(5, '0'); // "09.9"
    
  3. 空值处理:对null/undefined返回原始值

    String(null).padStart(2, '0'); // "null"
    

🚀 应用场景

场景1:数据格式化

// 金额分转元并补零
function formatCurrency(cents) {
  const yuan = (cents / 100).toFixed(2);
  return yuan.padStart(8, ' '); // 对齐到8位
}

console.log(formatCurrency(12345)); // "  123.45"

场景2:二进制数据转换

// 10进制转8位二进制
function toBinary(num) {
  return num.toString(2).padStart(8, '0');
}

console.log(toBinary(42)); // "00101010"

场景3:日志系统对齐

const logLevels = ['DEBUG', 'INFO', 'WARN'];
const messages = ['Starting app', 'User logged in', 'Memory low'];

// 生成对齐的日志输出
logLevels.forEach((level, i) => {
  console.log(
    `[${level.padStart(5)}] ${messages[i].padEnd(20)}`
  );
});
/*
[DEBUG] Starting app        
[ INFO] User logged in      
[ WARN] Memory low          
*/

⚖️ 性能对比测试

通过Benchmark.js对10万次操作进行性能测试:

方法 操作耗时(ms) 内存占用(MB)
手动循环填充 142.5 82.3
Array.join填充 98.7 76.1
padStart 32.8 54.2
pie
    title 各方法CPU耗时占比
    "手动循环填充" : 42
    "Array.join填充" : 29
    "padStart" : 29

🛠️ 进阶技巧与陷阱规避

技巧1:链式填充组合

// 生成银行账号格式:****-****-1234
const lastFour = '1234';
const masked = lastFour
  .padStart(12, '*')      // "********1234"
  .replace(/(.{4})/g, '$1-') // 每4位加分隔符
  .slice(0, -1);          // 移除末尾多余分隔符

console.log(masked); // "****-****-1234"

技巧2:多字符模式填充

// 创建文本装饰线
const title = " CHAPTER 1 ";
console.log(
  title.padStart(30, '═').padEnd(40, '═')
);
// "══════════ CHAPTER 1 ══════════"

⚠️ 常见陷阱及解决方案

  1. 负数长度处理:目标长度小于原字符串时返回原字符串

    'overflow'.padStart(3); // "overflow" 
    
  2. 非字符串填充符:自动调用toString()转换

    '1'.padStart(3, true); // "tr1" 
    
  3. 多字符截断规则:从左向右截取填充字符

    'A'.padStart(5, 'XYZ'); // "XYXYA" 
    

🌐 浏览器兼容性与Polyfill

虽然现代浏览器普遍支持padStart(),但需考虑兼容旧版环境:

// 安全垫片实现
if (!String.prototype.padStart) {
  String.prototype.padStart = function(targetLen, padStr) {
    targetLen = Math.floor(targetLen) || 0;
    if (targetLen <= this.length) return String(this);
    
    padStr = padStr ? String(padStr) : ' ';
    let repeatCnt = Math.ceil((targetLen - this.length) / padStr.length);
    
    return padStr.repeat(repeatCnt).slice(0, targetLen - this.length) 
           + String(this);
  };
}

💡 总结

  1. 优先选择padStart:性能优于手动实现方案
  2. 明确长度预期:提前计算目标长度避免意外截断
  3. 处理特殊字符:对换行符等特殊字符需额外处理
  4. 组合使用padEnd:实现双向填充需求

原文:xuanhu.info/projects/it…

这份超全JavaScript函数指南让你从小白变大神

你是不是曾经看着JavaScript里各种函数写法一头雾水?是不是经常被作用域搞得晕头转向?别担心,今天这篇文章就是要帮你彻底搞懂JavaScript函数!

读完本文,你将收获:

  • 函数的各种写法和使用场景
  • 参数传递的底层逻辑
  • 作用域和闭包的彻底理解
  • 箭头函数的正确使用姿势

准备好了吗?让我们开始这场函数探险之旅!

函数基础:从“Hello World”开始

先来看最基础的函数声明方式:

// 最传统的函数声明
function sayHello(name) {
  return "Hello, " + name + "!";
}

// 调用函数
console.log(sayHello("小明")); // 输出:Hello, 小明!

这里有几个关键点要记住:function是关键字,sayHello是函数名,name是参数,花括号里面是函数体。

但JavaScript的函数写法可不止这一种,还有函数表达式:

// 函数表达式
const sayHello = function(name) {
  return "Hello, " + name + "!";
};

console.log(sayHello("小红")); // 输出:Hello, 小红!

这两种写法看起来差不多,但在底层处理上有些细微差别。函数声明会被提升到作用域顶部,而函数表达式不会。

函数参数:比你想的更灵活

JavaScript的函数参数处理真的很贴心,不像其他语言那么死板:

function introduce(name, age, city) {
  console.log("我叫" + name + ",今年" + age + "岁,来自" + city);
}

// 正常调用
introduce("张三", 25, "北京"); // 输出:我叫张三,今年25岁,来自北京

// 参数不够 - 缺失的参数会是undefined
introduce("李四", 30); // 输出:我叫李四,今年30岁,来自undefined

// 参数太多 - 多余的参数会被忽略
introduce("王五", 28, "上海", "多余参数1", "多余参数2"); // 输出:我叫王五,今年28岁,来自上海

看到没?JavaScript不会因为参数个数不匹配就报错,这既是优点也是坑点。

为了解决参数不确定的情况,我们可以用arguments对象或者更现代的rest参数:

// 使用arguments对象(较老的方式)
function sum() {
  let total = 0;
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i];
  }
  return total;
}

console.log(sum(1, 2, 3, 4)); // 输出:10

// 使用rest参数(ES6新特性,推荐!)
function sum2(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum2(1, 2, 3, 4)); // 输出:10

rest参数的写法更清晰,而且它是个真正的数组,能用所有数组方法。

作用域深度探秘:变量在哪生效?

作用域可能是JavaScript里最让人困惑的概念之一,但理解它至关重要。

先看个简单例子:

let globalVar = "我是全局变量"; // 全局变量,在任何地方都能访问

function testScope() {
  let localVar = "我是局部变量"; // 局部变量,只在函数内部能访问
  console.log(globalVar); // 可以访问全局变量
  console.log(localVar); // 可以访问局部变量
}

testScope();
console.log(globalVar); // 可以访问
// console.log(localVar); // 报错!localVar在函数外部不存在

但事情没那么简单,看看这个经典的var和let区别:

// var的怪癖
function varTest() {
  if (true) {
    var x = 10; // var没有块级作用域
    let y = 20; // let有块级作用域
  }
  console.log(x); // 输出:10 - var声明的变量在整个函数都可用
  // console.log(y); // 报错!y只在if块内可用
}

varTest();

这就是为什么现在大家都推荐用let和const,避免var的奇怪行为。

闭包: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

看到神奇之处了吗?count变量本来应该在createCounter执行完就消失的,但因为返回的函数还在引用它,所以它一直存在。

闭包在实际开发中超级有用,比如创建私有变量:

function createBankAccount(initialBalance) {
  let balance = initialBalance; // 私有变量,外部无法直接访问
  
  return {
    deposit: function(amount) {
      balance += amount;
      return balance;
    },
    withdraw: function(amount) {
      if (amount <= balance) {
        balance -= amount;
        return balance;
      } else {
        return "余额不足";
      }
    },
    getBalance: function() {
      return balance;
    }
  };
}

const myAccount = createBankAccount(1000);
console.log(myAccount.getBalance()); // 输出:1000
console.log(myAccount.deposit(500)); // 输出:1500
console.log(myAccount.withdraw(200)); // 输出:1300
// console.log(balance); // 报错!balance是私有变量,无法直接访问

这样我们就实现了数据的封装和保护。

箭头函数:现代JavaScript的利器

ES6引入的箭头函数让代码更简洁:

// 传统函数
const add = function(a, b) {
  return a + b;
};

// 箭头函数
const addArrow = (a, b) => {
  return a + b;
};

// 更简洁的箭头函数(只有一条return语句时)
const addShort = (a, b) => a + b;

console.log(add(1, 2)); // 输出:3
console.log(addArrow(1, 2)); // 输出:3
console.log(addShort(1, 2)); // 输出:3

但箭头函数不只是语法糖,它没有自己的this绑定:

const obj = {
  name: "JavaScript",
  regularFunction: function() {
    console.log("普通函数this:", this.name);
  },
  arrowFunction: () => {
    console.log("箭头函数this:", this.name); // 这里的this不是obj
  }
};

obj.regularFunction(); // 输出:普通函数this: JavaScript
obj.arrowFunction(); // 输出:箭头函数this: undefined(在严格模式下)

这就是为什么在对象方法里通常不用箭头函数。

立即执行函数:一次性的工具

有时候我们需要一个函数只执行一次:

// 立即执行函数表达式 (IIFE)
(function() {
  const secret = "这个变量不会污染全局作用域";
  console.log("这个函数立即执行了!");
})();

// 带参数的IIFE
(function(name) {
  console.log("Hello, " + name);
})("世界");

// 用箭头函数写的IIFE
(() => {
  console.log("箭头函数版本的IIFE");
})();

在模块化规范出现之前,IIFE是防止变量污染全局的主要手段。

高阶函数:把函数当参数传递

在JavaScript中,函数是一等公民,可以像变量一样传递:

// 高阶函数 - 接收函数作为参数
function processArray(arr, processor) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(processor(arr[i]));
  }
  return result;
}

const numbers = [1, 2, 3, 4, 5];

// 传递不同的处理函数
const doubled = processArray(numbers, function(num) {
  return num * 2;
});

const squared = processArray(numbers, function(num) {
  return num * num;
});

console.log(doubled); // 输出:[2, 4, 6, 8, 10]
console.log(squared); // 输出:[1, 4, 9, 16, 25]

这就是函数式编程的基础,也是数组方法map、filter、reduce的工作原理。

实战演练:构建一个简单的事件系统

让我们用今天学的知识构建一个实用的小工具:

function createEventEmitter() {
  const events = {}; // 存储所有事件和对应的监听器
  
  return {
    // 监听事件
    on: function(eventName, listener) {
      if (!events[eventName]) {
        events[eventName] = [];
      }
      events[eventName].push(listener);
    },
    
    // 触发事件
    emit: function(eventName, data) {
      if (events[eventName]) {
        events[eventName].forEach(listener => {
          listener(data);
        });
      }
    },
    
    // 移除监听器
    off: function(eventName, listenerToRemove) {
      if (events[eventName]) {
        events[eventName] = events[eventName].filter(
          listener => listener !== listenerToRemove
        );
      }
    }
  };
}

// 使用示例
const emitter = createEventEmitter();

// 定义监听器函数
function logData(data) {
  console.log("收到数据:", data);
}

// 监听事件
emitter.on("message", logData);

// 触发事件
emitter.emit("message", "你好世界!"); // 输出:收到数据: 你好世界!
emitter.emit("message", "这是第二条消息"); // 输出:收到数据: 这是第二条消息

// 移除监听器
emitter.off("message", logData);
emitter.emit("message", "这条消息不会被接收"); // 不会有输出

这个例子用到了我们今天学的几乎所有概念:函数返回函数、闭包、高阶函数等。

常见坑点与最佳实践

学到这里,你已经是函数小能手了!但还要注意这些常见坑点:

// 坑点1:循环中的闭包
console.log("=== 循环闭包问题 ===");
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 输出:3, 3, 3 而不是 0, 1, 2
  }, 100);
}

// 解决方案1:使用let
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 输出:0, 1, 2
  }, 100);
}

// 解决方案2:使用IIFE
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j); // 输出:0, 1, 2
    }, 100);
  })(i);
}

最佳实践总结:

  1. 优先使用const,其次是let,避免var
  2. 简单的函数用箭头函数,方法定义用普通函数
  3. 注意this的指向问题
  4. 合理使用闭包,但要注意内存泄漏

总结

恭喜你!现在已经对JavaScript函数有了全面的理解。从基础声明到高级概念,从作用域到闭包,这些都是JavaScript编程的核心基础。

记住,理解函数的关键在于多写代码、多思考。每个概念都要亲手试一试,看看不同的写法会产生什么效果。

弃用 uni-app!Vue3 的原生 App 开发框架来了!

长久以来,"用 Vue 3 写真正的原生 App" 一直是块短板。

uni-app 虽然"一套代码多端运行",但性能瓶颈厂商锁仓原生能力羸弱的问题常被开发者诟病。

整个 Vue 生态始终缺少一个能与 React Native 并肩的"真·原生"跨平台方案

直到 NativeScript-Vue 3 的横空出世,并被 尤雨溪 亲自点赞。

为什么是时候说 goodbye 了?

uni-app 现状 开发者痛点
渲染层基于 WebView 或弱原生混合 启动慢、掉帧、长列表卡顿
自定义原生 SDK 需写大量 renderjs / plus 桥接 维护成本高,升级易断裂
锁定 DCloud 生态 工程化、VitePinia 等新工具跟进慢
Vue 3 支持姗姗来迟,Composition API 兼容碎裂 类型推断、生态插件处处踩坑

"我们只是想要一个 Vue 语法 + 真原生渲染 + 社区插件开箱即用 的解决方案。"
—— 这,正是 NativeScript-Vue 给出的答案。

尤雨溪推特背书

2025-10-08Evan You 转发 NativeScript 官方推文:

"Try Vite + NativeScript-Vue today — HMR, native APIs, live reload."

配图是一段 <script setup> + TypeScript 的实战 Demo,意味着:

  • 真正的 Vue 3 语法Composition API
  • Vite 秒级热重载
  • 直接调用 iOS / Android 原生 API

获创始人的公开推荐,无疑给社区打了一剂强心针。

NativeScript-Vue 是什么?

一句话:Vue 的自定义渲染器 + NativeScript 原生引擎

  • 运行时 没有 WebView,JS 在 V8 / JavaScriptCore 中执行
  • <template> 标签 → 原生 UILabel / android.widget.TextView
  • 支持 NPM、CocoaPods、Maven/Gradle 全部原生依赖
  • React Native 同级别的性能,却拥有 Vue 完整开发体验

5 分钟极速上手

1. 环境配置(一次过)

# Node ≥ 18
npm i -g nativescript
ns doctor                # 按提示安装 JDK / Android Studio / Xcode
# 全部绿灯即可

2. 创建项目

ns create myApp \
  --template @nativescript-vue/template-blank-vue3@latest
cd myApp

模板已集成 Vite + Vue3 + TS + ESLint

3. 运行 & 调试

# 真机 / 模拟器随你选
ns run ios
ns run android

保存文件 → 毫秒级 HMRconsole.log 直接输出到终端。

4. 目录速览

myApp/
├─ app/
│  ├─ components/          // 单文件 .vue
│  ├─ app.ts               // createApp()
│  └─ stores/              // Pinia 状态库
├─ App_Resources/
└─ vite.config.ts          // 已配置 nativescript-vue-vite-plugin

5. 打包上线

ns build android --release   # 生成 .aab / .apk
ns build ios --release       # 生成 .ipa

签名渠道自动版本号——标准原生流程,CI 友好。

Vue 3 生态插件兼容性一览

插件 是否可用 说明
Pinia 零改动,app.use(createPinia())
VueUse ⚠️ 无 DOM 的 Utilities 可用
vue-i18n 9.x 实测正常
Vue Router 官方推荐用 NativeScript 帧导航$navigateTo(Page)
Vuetify / Element Plus 依赖 CSS & DOM,无法渲染

检测小技巧:

npm i xxx
grep -r "document\|window\|HTMLElement" node_modules/xxx || echo "大概率安全"

调试神器:Vue DevTools 支持

NativeScript-Vue 3 已提供 官方 DevTools 插件

  • 组件树PropsEventsPinia 状态 实时查看
  • 沿用桌面端调试习惯,无需额外学习成本

👉 配置指南:https://nativescript-vue.org/docs/essentials/vue-devtools

插件生态 & 原生能力

  • 700+ NativeScript 官方插件
    ns plugin add @nativescript/camera | bluetooth | sqlite...

  • iOS/Android SDK 直接引入
    CocoaPods / Maven 一行配置即可:

 // 调用原生 CoreBluetooth
 import { CBCentralManager } from '@nativescript/core'
  • 自定义 View & 动画
    注册即可在 <template> 使用,与 React Native 造组件体验一致

结语:这一次,Vue 开发者不再低人一等

React NativeFacebook 撑腰,FlutterGoogle 背书,

现在 Vue 3 也有了自己的 真·原生跨平台答案 —— NativeScript-Vue

它让 Vue 语法第一次 完整、无损、高性能 地跑在 iOS & Android 上,
并获得 尤雨溪 公开点赞与 Vite 官方生态加持。

弃用 uni-app,拥抱 NativeScript-Vue
性能、原生能力、工程化 三者兼得,
用你最爱的 .vue 文件,写最硬核的移动应用!

🔖 一键直达资源

  • 官网 & 文档https://nativescript-vue.org
  • 插件兼容列表https://nativescript-vue.org/docs/essentials/vue-plugins
  • DevTools 配置https://nativescript-vue.org/docs/essentials/vue-devtools
  • 环境安装指南https://docs.nativescript.org/setup/

Hello 算法:让前端人真正理解算法

每个系列一本前端好书,帮你轻松学重点。

本系列来自上海交通大学硕士,华为高级算法工程师 靳宇栋《Hello,算法》

程序员圈儿有两种怪象:

1、人人称工程师,但少有人能真正担起一项“工程”。

2、掌握算法本是理所应当,实际寥寥无几。

一直以来,算法好像跟前端开发没多少关联,顶多用来应付面试。

本系列要做的,就是同大家一起啃下这块硬骨头,真正理解算法。

算法是什么

算法是什么,没有标准答案。

先看几个实际案例:

查字典

在字典里,每个汉字都对应一个拼音,而字典是按照字母顺序排列的。

查找”ren“的大概过程如下:

  • 翻开字典约一半的页数,查看该页的首字母,假设为 j。
  • 字母表中r位于j之后,所以排除前半部分,查找范围缩小一半。
  • 不断重复前两个步骤 ,直至找到首字母为r的页码。

打扑克

打牌时,每局都需要整理扑克牌,使其从小到大排列。过程大概如下:

  • 将扑克牌划分为“有序”和“无序”两部分,并假设初始状态下最左 1 张扑克牌已经有序。
  • 从无序部分抽出一张扑克牌,插入有序部分的正确位置。
  • 不断重复步骤 2 ,直至所有扑克牌都有序。

找零钱

在超市购买了 69 元的商品,给收银员 100 元,收银员需要找 31 元。大概过程如下:

  • 可选项是比 31 元面值更小的货币,包括 1 元、5 元、10 元、20 元。
  • 从中拿出最大的 20 元,剩余 31 − 20 = 11 元。
  • 从剩余选项中拿出最大的 10 元,还剩 11 − 10 = 1 元。

这三个例子涉及的场景大家都非常熟悉,只是可能没意识到,它们就是算法。

算法就是问题的解决方案,并不一定跟编程相关。

从平常的生活细节,到巧夺天工的技艺,都隐藏着精妙的算法思想。

有个著名公式:程序 = 数据结构 + 算法

所以不要说你没用过算法,只要在写程序,就在运用算法。

学习的难处

如此说来,理解算法并不难,为什么大家觉得难?原因大致两点:

1、大家所熟知的,只是简单问题的一般解,不一定是最优解,算法学习的目标是寻找最优解

2、从解决思路到代码表示,需要一个转换过程

编程中有两道坎:一是从完全不懂到具备编程思维,二是从语法正确到算法精良。

跨过第一道坎,写10行代码和1000行代码没有本质区别;跨过第二道坎,就能摆脱”工具人“属性,真正创造价值。

很多人都有这样的感受,学习算法像是重新学一遍编程,若干新概念扑面而来:冒泡、贪心、递归、动态规划、红黑树、B+树...

你得先理解这些概念,再将概念转换成代码。

理解不难,难的是转换,如果脑子转不过来弯儿,就难以完成转换。

怎么办?有人说刷题,刷题的确是一种受欢迎且看似有效的方法,但只是对自学能力强的人而言,基础薄弱的,可能每次都是从一道题开始,又从一道题结束,就像有些人学英语永远都是“abandon”。

何以解忧

学习这件事,没有毫无争议的“标准答案”,只有匹配读者当下认知水平的答案。

我们通常看到的资料和教程,讲师的背景和履历都很强,但稍作了解就知道,段位远高于基础。

它试图降低身段教会我们,但你刚理解了一个简单的问题,觉得自己好像可以,接着就迎来一段100行的代码,大脑马上投降——“谁爱看谁看去吧”。

我们需要的不是“看起来很厉害”,是能看懂,大脑可以跟随思考的东西,所以有了本书的选择,以及本系列文章。

本书重点着眼于“如何入门“,讲解的内容不一定是最优解,也不能保证学完拿到offer,但可以帮你慢慢找到”吸收“算法的感觉,这样以来,学习的进度条才能往前走,哪怕走得慢一些,只要有动力一直走,早晚能到不是吗?

先来两道开胃菜。

数据结构和算法

数据结构总是和算法同时出现,它们是什么关系?

其实生活中随处可见数据结构。

地铁线路是“”,公司组织架构是“”,叠在一起的盘子是”“,车站排队进站是”队列“。

你或许想问,这些在JavaScript中都没有,怎么办?

JavaScript中,我们熟悉的数据结构只有“数组”和“对象”,但其实数组是一种通用型数据结构,以它为基础,可构造出栈、队列、树等,对象则是散列。

那么多数据结构,怎么区分和记忆?

它们可分为“逻辑结构”和“存储结构”这两类。

逻辑结构:比如树,逻辑上具有“父—>子”关系。

存储结构:物理上是集中存储,还是分散存储。如:数组、链表。

数据结构是算法的基石,提供了结构化存储的数据和操作数据的方法。

算法是数据结构发挥作用的舞台,数据结构结合算法才能解决特定问题。

但要清楚,二者不是一对一的绑定关系,就像生活中的问题总有不同解。算法通常可以基于不同的数据结构实现,但执行效率可能相差很大,所以,往往对应着更优的选择。

质量评估

研究算法,是为了写出更好的程序,那什么是“好”?

如果把代码比作一匹马,好的算法就是“吃得少,还跑得快”。

当我们运行一段代码,直观感受到的,是快慢,不易察觉的,是内存占用。

那么优劣的评定就可归为两个维度:速度和内存,即“时间”和“空间”。

有了评定标准,怎么验证呢?不可能带着代码去每一台机器上跑一遍。

这时候,就需要一套与机器和网络都无关的评定方法——”复杂度分析“。

复杂度分析体现算法运行所需的资源与输入数据大小之间的关系。它描述了随着数据大小的增加,算法执行所需时间和空间的增长趋势

趋势是个很有意思的东西,有三个特点:

1、不精确,但能区分相对优劣。

2、忽略细枝末节,只关注大头。

3、量变引起质变,小于某个值的时候,A比B优,超过之后,可能是B比A优。

同时,时间和空间的利用往往难以共存,需要牺牲一方,成全另一方。

后续的学习过程中,还有很多机会感受它们的魅力。

我们的目标

本系列文章,尝试帮助不那么擅长编程的伙伴消除对算法的排斥和恐惧,培养算法思维,了解常见算法的用途和优势。

这里不得不提本书作者靳宇栋(@krahets) ,富有且慷慨,自身技术过硬的同时,能关注到基础相对薄弱的群体的需求,有耐心和创造力创作出这么好的作品,是为行业一大贡献。

著名物理学家费曼教授曾说:“Knowledge isn't free. You have to pay attention.”

书籍和文章可能是免费的,但要掌握它们,你需要付出时间和耐心。

希望大家通过这次旅程,体会到思考的乐趣,在后续的生活和工作中,更从容地解决自己遇到的问题。

本书 Github 已有超 110k star, 官网地址: www.hello-algo.com

不论你使用哪种语言,喜欢什么形式,它都能满足,如果等不及,可以先睹为快~

更多好文第一时间接收,可关注公众号:“前端说书匠”

前端梳理体系从常问问题去完善-框架篇(Vue2&Vue3)

前言

对于Vue2,Vue3,我项目上用得不是很多,用得最多得还是React,也不知为啥,我是自学的Vue2出来得,然后出来找到得工作是React得,直到现在都是React,虽然中间也维护Vue得项目,写写Vue得项目还是可以得,不过嘛,框架原理大致都是相通得,所以整理得不像React那么细致。

对于知识体系,需要学一遍,之所以,以问题得方式去梳理就跟我们刷题一样,多刷才能记住嘛,通过提问得方式,去记住他,查缺补漏,这就是我为什么分享了四篇体系概念篇得原因,对于计算机网咯,还有移动端,小程序,这种虽然整理有,但不是很细致。有机会在分享吧。

Vue2&Vue3

Vue3 的defineProps,defineEmits,defineExpose

在 Vue3 的 <script setup> 语法糖中,definePropsdefineEmitsdefineExpose 是三个核心的编译时宏(compiler macros),用于处理组件的 props 接收、事件触发和内部成员暴露,无需手动导入即可使用。它们是 Vue3 为简化组件逻辑、提升开发效率设计的语法糖,仅在 <script setup> 中生效。

1. defineProps:声明组件接收的 Props

用于在子组件中声明可以从父组件接收的属性(props),类似 Vue2 中的 props 选项或 Vue3 非 setup 语法中的 props 配置。它的作用是定义 props 的类型、默认值和校验规则,同时让 TypeScript 能够正确推断类型。

基本用法:

<!-- 子组件 Child.vue -->
<template>
  <div>父组件传递的消息:{{ msg }}</div>
  <div>用户年龄:{{ age }}</div>
</template>

<script setup>
// 方式1:仅声明类型(TypeScript)
const props = defineProps<{
  msg: string;
  age?: number; // 可选属性
}>();

// 方式2:声明类型 + 默认值(需用 withDefaults 辅助函数)
const props = withDefaults(defineProps<{
  msg: string;
  age?: number;
}>(), {
  age: 18, // age 的默认值
});

// 方式3:JavaScript 中使用(对象形式声明)
const props = defineProps({
  msg: {
    type: String,
    required: true, // 必传
  },
  age: {
    type: Number,
    default: 18, // 默认值
  },
});

// 使用 props
console.log(props.msg); // 访问父组件传递的 msg
</script>

核心特点:

  • 无需导入defineProps 是编译时宏,Vue 会自动处理,无需 import
  • 只读性:返回的 props 对象是只读的(响应式代理),不能直接修改(如需修改,应通过 emit 通知父组件)。
  • 类型支持:在 TypeScript 中可通过泛型直接声明类型,配合 withDefaults 可设置默认值;JavaScript 中通过对象形式声明(类似 Vue2 的 props 选项)。

2. defineEmits:声明组件触发的事件

用于在子组件中声明可以触发的事件(类似 Vue2 中的 $emitemits 选项),作用是定义事件的类型、参数,让父组件可以通过 @事件名 监听,同时提供类型校验和 TypeScript 类型推断。

基本用法:

<!-- 子组件 Child.vue -->
<template>
  <button @click="handleClick">点击触发事件</button>
  <input @input="handleInput" placeholder="输入内容" />
</template>

<script setup>
// 方式1:TypeScript 中声明事件类型(泛型)
const emit = defineEmits<{
  // 事件名: (参数1类型, 参数2类型) => 返回值(通常为 void)
  'change': (value: string) => void;
  'update-age': (newAge: number) => void;
}>();

// 方式2:JavaScript 中声明事件(数组或对象形式)
const emit = defineEmits(['change', 'update-age']);
// 或带校验的对象形式
const emit = defineEmits({
  'change': (value) => typeof value === 'string', // 校验参数是否为字符串
  'update-age': (newAge) => newAge > 0, // 校验年龄是否为正数
});

// 触发事件(传递参数)
const handleClick = () => {
  emit('update-age', 20); // 触发 update-age 事件,传递参数 20
};

const handleInput = (e) => {
  emit('change', e.target.value); // 触发 change 事件,传递输入值
};
</script>

父组件监听事件:

<!-- 父组件 Parent.vue -->
<template>
  <Child 
    @change="onChange" 
    @update-age="onUpdateAge" 
  />
</template>

<script setup>
const onChange = (value) => {
  console.log('子组件输入:', value);
};

const onUpdateAge = (newAge) => {
  console.log('新年龄:', newAge);
};
</script>

核心特点:

  • 事件声明:明确组件可触发的事件,增强代码可读性和可维护性。
  • 参数校验:JavaScript 中可通过函数对事件参数进行校验(返回 true 表示校验通过)。
  • 类型安全:TypeScript 中可通过泛型定义事件参数类型,触发时会自动校验参数类型。

3. defineExpose:暴露组件内部成员

<script setup> 中,组件的内部方法、属性默认是私有的(父组件无法通过 ref 访问)。defineExpose 用于将组件内部的方法或属性暴露出去,让父组件可以通过 ref 获取子组件实例并访问这些成员。

基本用法:

<!-- 子组件 Child.vue -->
<template>
  <div>内部计数:{{ count }}</div>
</template>

<script setup>
import { ref } from 'vue';

// 内部状态和方法
const count = ref(0);
const increment = () => {
  count.value++;
};
const reset = () => {
  count.value = 0;
};

// 暴露给父组件的成员(仅暴露的内容可被父组件访问)
defineExpose({
  count,
  increment,
  reset,
});
</script>

父组件通过 ref 访问子组件暴露的成员:

<!-- 父组件 Parent.vue -->
<template>
  <Child ref="childRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>

<script setup>
import { ref } from 'vue';
import Child from './Child.vue';

// 获取子组件实例的 ref
const childRef = ref(null);

const callChildMethod = () => {
  // 访问子组件暴露的 count
  console.log('子组件当前计数:', childRef.value.count.value);
  
  // 调用子组件暴露的 increment 方法
  childRef.value.increment();
  
  // 调用子组件暴露的 reset 方法
  // childRef.value.reset();
};
</script>

核心特点:

  • 选择性暴露:仅通过 defineExpose 声明的成员会被暴露,未声明的仍为私有。
  • 避免过度耦合:谨慎使用,过度暴露会导致组件间耦合度升高,优先通过 propsemit 通信。
  • TypeScript 支持:可通过 defineComponent 或接口定义子组件暴露的类型,实现类型推断。

总结

  • defineProps:处理父组件到子组件的数据输入,定义接收的 props 类型和默认值。
  • defineEmits:处理子组件到父组件的事件输出,声明可触发的事件及参数。
  • defineExpose:将子组件的内部成员暴露给父组件,用于特殊场景下的组件交互(谨慎使用)。

这三个 API 是 Vue3 <script setup> 语法中组件通信的核心工具,配合使用可实现清晰、类型安全的组件交互逻辑。

Vue3 watch和 watchEffect的区别

在 Vue3 中,watchwatchEffect 都是用于监听响应式数据变化并执行副作用的 API,但它们的设计理念和使用场景有显著区别。核心差异体现在监听方式、执行时机、依赖追踪等方面,具体如下:

1. 监听目标:明确指定 vs 自动追踪

  • watch:需要明确指定监听的数据源 watch 必须手动指定要监听的响应式数据(如 refreactive 的属性、计算属性等),只有当这些指定的数据源变化时,才会触发回调。

    <script setup>
    import { ref, watch } from 'vue';
    const count = ref(0);
    const name = ref('vue');
    
    // 明确监听 count 的变化
    watch(count, (newVal, oldVal) => {
      console.log(`count 从 ${oldVal} 变到 ${newVal}`);
    });
    
    // 监听多个数据源(数组形式)
    watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
      console.log('count 或 name 变化了');
    });
    
    // 监听 reactive 对象的某个属性(需用 getter 函数)
    const user = reactive({ age: 18 });
    watch(() => user.age, (newAge) => {
      console.log(`年龄变为 ${newAge}`);
    });
    </script>
    
  • watchEffect:自动追踪函数内部的响应式数据 watchEffect 不需要指定监听目标,它会自动追踪回调函数内部用到的所有响应式数据,当这些数据变化时,自动触发回调。

    <script setup>
    import { ref, watchEffect } from 'vue';
    const count = ref(0);
    const name = ref('vue');
    
    // 自动追踪 count 和 name(函数内用到的响应式数据)
    watchEffect(() => {
      console.log(`count: ${count.value}, name: ${name.value}`);
    });
    // 初始化时执行一次(输出 "count: 0, name: vue")
    // 当 count 或 name 变化时,会重新执行
    </script>
    

2. 执行时机:懒执行 vs 立即执行

  • watch:默认懒执行 watch 的回调只会在监听的数据源变化时执行,初始化时(页面加载时)不会执行。 (可通过 immediate: true 配置改为立即执行)

    watch(count, () => {
      console.log('count 变化了'); // 初始时不执行,只有 count 变了才执行
    }, { immediate: true }); // 加上 immediate 后,初始化时会执行一次
    
  • watchEffect:默认立即执行 watchEffect 的回调在初始化时会立即执行一次(用于收集初始依赖),之后当依赖变化时再次执行。 (这是因为它需要通过首次执行来 “发现” 内部用到的响应式数据)

3. 回调参数:关注新旧值 vs 仅关注副作用

  • watch 的回调:接收新旧值 watch 的回调函数有三个参数:newVal(新值)、oldVal(旧值)、onCleanup(清理函数),适合需要对比数据变化前后状态的场景。

    watch(count, (newVal, oldVal, onCleanup) => {
      // 对比新旧值
      if (newVal > oldVal) {
        console.log('count 增加了');
      }
      // 清理副作用(如防抖、取消请求)
      onCleanup(() => {
        // 下次回调执行前或组件卸载时触发
      });
    });
    
  • watchEffect 的回调:仅接收清理函数 watchEffect 的回调只接收一个 onCleanup 参数,不提供新旧值(因为它自动追踪依赖,无法精确知道哪个数据变化),适合只需要执行副作用(如发请求、更新 DOM)的场景。

    watchEffect((onCleanup) => {
      // 发送请求(依赖 count)
      const timer = setTimeout(() => {
        console.log(`count 为 ${count.value}`);
      }, 1000);
      // 清理副作用(避免多次请求)
      onCleanup(() => clearTimeout(timer));
    });
    

4. 依赖追踪精度:精确控制 vs 自动全量

  • watch:可精确监听部分依赖 对于 reactive 对象,watch 可以通过 getter 函数只监听某个具体属性,避免不必要的触发。

    const user = reactive({ name: 'vue', age: 18 });
    // 只监听 age 的变化(name 变化不会触发)
    watch(() => user.age, () => {
      console.log('只有年龄变化时触发');
    });
    
  • watchEffect:自动追踪所有依赖 只要回调函数中用到的响应式数据发生变化,无论是否是 “关键数据”,都会触发回调。如果函数内用到多个数据,任何一个变化都会执行。

    const user = reactive({ name: 'vue', age: 18 });
    watchEffect(() => {
      // 用到了 name 和 age,任何一个变化都会触发
      console.log(`${user.name} 的年龄是 ${user.age}`);
    });
    

5. 使用场景

场景 推荐 API 原因
需要知道数据变化的 “新旧值”(如表单验证、比较变化) watch 回调提供 newValoldVal,方便对比
只需要在依赖变化时执行副作用(如发请求、更新 DOM) watchEffect 自动追踪依赖,代码更简洁
需精确控制监听的数据源(避免无关变化触发) watch 可手动指定监听目标,减少不必要的执行
初始化时需要立即执行一次副作用(如初始加载数据) watchEffect 默认立即执行,无需额外配置

总结

  • watch 是 “精确监听”:需手动指定目标,懒执行,提供新旧值,适合需要精确控制和对比变化的场景。
  • watchEffect 是 “自动追踪”:无需指定目标,立即执行,不提供新旧值,适合简单的副作用场景,代码更简洁。

两者都可以通过返回的函数停止监听(const stop = watch(...)const stop = watchEffect(...),调用 stop() 即可)。

从new Vue到页面实例使用经历那些步骤

  1. 创建项目

使用 Vue CLI 创建项目:

# 安装 Vue CLI(如果未安装)
npm install -g @vue/cli

# 创建 Vue 2 项目
vue create my-vue2-app --default
cd my-vue2-app
npm install
  1. 项目结构概览

Vue 2 项目的核心结构:

src/
  ├── App.vue           # 根组件
  ├── main.js           # 入口文件
  ├── components/       # 组件目录
  ├── router/           # 路由配置(可选)
  └── store/            # Vuex 状态管理(可选)
  1. 定义组件(.vue 文件)

使用单文件组件(SFC)结构,包含 <template><script><style>

<!-- src/components/HelloWorld.vue -->
<template>
  <div class="hello">
    <h1>{{ message }}</h1>
    <button @click="increment">计数: {{ count }}</button>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data() {
    return {
      message: 'Hello Vue 2!',
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++;
    }
  }
}
</script>

<style scoped>
.hello {
  padding: 20px;
}
</style>
  1. 在 App.vue 中使用组件
<!-- src/App.vue -->
<template>
  <div id="app">
    <HelloWorld />
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue';

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  margin-top: 60px;
}
</style>
  1. 入口文件配置(main.js)

创建 Vue 实例并挂载到 DOM:

// src/main.js
import Vue from 'vue';
import App from './App.vue';
import router from './router'; // 路由(如果使用)

// 关闭生产提示
Vue.config.productionTip = false;

// 创建 Vue 实例
new Vue({
  router,           // 注入路由(如果使用)
  render: h => h(App),
}).$mount('#app');
  1. HTML 模板(public/index.html)
<!DOCTYPE ht>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue 2 App</title>
</head>
<body>
  <div id="app"></div>
  <!-- Vue CLI 会自动注入 JS/CSS -->
</body>
</html>
  1. 响应式原理

Vue 2 使用 Object.defineProperty()

export default {
  data() {
    return {
      count: 0,
      user: {
        name: 'John',
        age: 30
      }
    }
  },
  created() {
    // 修改数据会触发视图更新
    this.count++;
    this.user.age = 31;
  }
}
  1. 生命周期钩子

Vue 2 组件的生命周期方法:

export default {
  beforeCreate() {
    console.log('组件实例初始化后,数据观测和 event/watcher 事件配置前');
  },
  created() {
    console.log('实例已经创建完成之后被调用');
  },
  beforeMount() {
    console.log('挂载开始之前被调用');
  },
  mounted() {
    console.log('挂载完成后被调用(DOM 已渲染)');
  },
  beforeUpdate() {
    console.log('数据更新前被调用');
  },
  updated() {
    console.log('数据更新后被调用');
  },
  beforeDestroy() {
    console.log('实例销毁之前被调用');
  },
  destroyed() {
    console.log('实例销毁之后被调用');
  }
}
  1. 运行项目
npm run serve   # 开发环境
npm run build   # 生产环境构建

关键概念(Vue 2 特有)

  1. Options API:通过 datamethodscomputed 等选项组织代码。

  2. 响应式限制

    • 无法检测对象属性的添加或删除
    • 数组通过特定方法触发更新(如 push()splice()
  3. 混合(Mixins):代码复用机制(Vue 3 推荐 Composition API)

  4. 过滤器(Filters):格式化文本(Vue 3 中移除,推荐使用计算属性)

完整示例(待办应用)

<!-- src/components/TodoList.vue -->
<template>
  <div class="todo-list">
    <h1>{{ title }}</h1>
    
    <input v-model="newTodo" @keyup.enter="addTodo" placeholder="添加待办">
    <button @click="addTodo">添加</button>
    
    <ul>
      <li v-for="(todo, index) in todos" :key="index">
        <input type="checkbox" v-model="todo.done">
        <span :class="{ done: todo.done }">{{ todo.text }}</span>
        <button @click="removeTodo(index)">删除</button>
      </li>
    </ul>
    
    <p>已完成: {{ completedCount }} / {{ todos.length }}</p>
  </div>
</template>

<script>
export default {
  name: 'TodoList',
  data() {
    return {
      title: 'Vue 2 待办列表',
      newTodo: '',
      todos: [
        { text: '学习 Vue 2', done: false },
        { text: '掌握 Options API', done: false }
      ]
    }
  },
  computed: {
    completedCount() {
      return this.todos.filter(todo => todo.done).length;
    }
  },
  methods: {
    addTodo() {
      if (this.newTodo.trim()) {
        this.todos.push({ text: this.newTodo, done: false });
        this.newTodo = '';
      }
    },
    removeTodo(index) {
      this.todos.splice(index, 1);
    }
  }
}
</script>

<style scoped>
.done {
  text-decoration: line-through;
  color: #888;
}
</style>

总结

Vue 2 框架下的开发流程:

  1. 项目初始化:使用 Vue CLI 创建项目。
  2. 组件化开发:使用单文件组件(SFC)结构。
  3. Options API:通过 datamethodscomputed 等选项组织代码。
  4. 实例创建与挂载:通过 new Vue() 创建实例并挂载。
  5. 响应式更新:基于 Object.defineProperty() 实现数据响应式。

Vue 2 是一个成熟的框架,适合现有项目维护或对学习曲线有要求的团队。对于新项目,推荐考虑 Vue 3 及其 Composition API。

Vue核心实现原理

Vue.js 的核心实现原理围绕响应式系统虚拟 DOM组件化生命周期展开。以下是其核心机制的详细解析:

一、响应式系统(Reactivity System)

Vue 通过 Object.defineProperty()(Vue 2.x)或 Proxy(Vue 3.x)实现数据劫持,当数据变化时自动更新 DOM。

核心流程:

  1. 数据劫持
    • Vue 初始化时遍历 data 对象,将所有属性转换为 getter/setter
    • 每个属性关联一个 Dep(依赖收集器)。
  2. 依赖收集
    • 组件渲染时,访问数据触发 getter,将当前渲染函数(Watcher)添加到 Dep 的依赖列表。
  3. 通知更新
    • 数据变更时触发 setter,Dep 通知所有依赖的 Watcher 更新。

简化代码示例(Vue 2.x 原理):

class Vue {
  constructor(options) {
    this.data = options.data;
    this.observe(this.data);
    // 创建渲染 Watcher
    new Watcher(this, this.render);
  }

  observe(obj) {
    if (!obj || typeof obj !== 'object') return;
    Object.keys(obj).forEach(key => {
      this.defineReactive(obj, key, obj[key]);
    });
  }

  defineReactive(obj, key, value) {
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      get() {
        if (Dep.target) dep.depend(); // 收集依赖
        return value;
      },
      set(newValue) {
        if (value === newValue) return;
        value = newValue;
        dep.notify(); // 通知更新
      }
    });
    this.observe(value); // 递归处理嵌套对象
  }
}

class Dep {
  constructor() {
    this.subscribers = new Set();
  }

  depend() {
    if (Dep.target) this.subscribers.add(Dep.target);
  }

  notify() {
    this.subscribers.forEach(watcher => watcher.update());
  }
}

Dep.target = null;

class Watcher {
  constructor(vm, updateFn) {
    this.vm = vm;
    this.updateFn = updateFn;
    Dep.target = this;
    this.updateFn(); // 触发依赖收集
    Dep.target = null;
  }

  update() {
    this.updateFn(); // 更新视图
  }
}

二、虚拟 DOM(Virtual DOM)

Vue 使用 JavaScript 对象(VNode)表示真实 DOM 结构,通过差异化算法高效更新 DOM。

核心流程:

  1. VNode 生成
    • 模板编译或手写 render 函数生成 VNode 树。
  2. Diff 算法
    • 新旧 VNode 对比,找出最小变更集。
    • 采用 双指针 + key 追踪 优化性能。
  3. Patch 操作
    • 根据差异更新真实 DOM。

简化 Diff 算法示例:

function patch(oldVnode, newVnode) {
  // 1. 节点类型不同,直接替换
  if (oldVnode.tag !== newVnode.tag) {
    oldVnode.el.parentNode.replaceChild(createEl(newVnode), oldVnode.el);
    return;
  }

  // 2. 节点相同,更新属性
  const el = newVnode.el = oldVnode.el;
  updateProperties(el, oldVnode.props, newVnode.props);

  // 3. 处理子节点
  if (typeof newVnode.text === 'string') {
    // 文本节点
    el.textContent = newVnode.text;
  } else {
    // 递归处理子节点
    updateChildren(el, oldVnode.children, newVnode.children);
  }
}

function updateChildren(parentEl, oldChildren, newChildren) {
  // 双指针 + key 优化的 Diff 算法
  let oldStartIdx = 0;
  let newStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newEndIdx = newChildren.length - 1;
  let oldStartVnode = oldChildren[0];
  let oldEndVnode = oldChildren[oldEndIdx];
  let newStartVnode = newChildren[0];
  let newEndVnode = newChildren[newEndIdx];

  // 循环比较新旧子节点
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (!oldStartVnode) {
      oldStartVnode = oldChildren[++oldStartIdx];
    } else if (!oldEndVnode) {
      oldEndVnode = oldChildren[--oldEndIdx];
    } else if (isSameVnode(oldStartVnode, newStartVnode)) {
      // 头头比较
      patch(oldStartVnode, newStartVnode);
      oldStartVnode = oldChildren[++oldStartIdx];
      newStartVnode = newChildren[++newStartIdx];
    } else if (isSameVnode(oldEndVnode, newEndVnode)) {
      // 尾尾比较
      patch(oldEndVnode, newEndVnode);
      oldEndVnode = oldChildren[--oldEndIdx];
      newEndVnode = newChildren[--newEndIdx];
    } else if (isSameVnode(oldStartVnode, newEndVnode)) {
      // 头尾比较(移动节点)
      patch(oldStartVnode, newEndVnode);
      parentEl.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
      oldStartVnode = oldChildren[++oldStartIdx];
      newEndVnode = newChildren[--newEndIdx];
    } else if (isSameVnode(oldEndVnode, newStartVnode)) {
      // 尾头比较(移动节点)
      patch(oldEndVnode, newStartVnode);
      parentEl.insertBefore(oldEndVnode.el, oldStartVnode.el);
      oldEndVnode = oldChildren[--oldEndIdx];
      newStartVnode = newChildren[++newStartIdx];
    } else {
      // 使用 key 进行映射查找(优化)
      const idxInOld = findIndexInOld(newStartVnode, oldChildren, oldStartIdx, oldEndIdx);
      if (idxInOld > -1) {
        const vnodeToMove = oldChildren[idxInOld];
        patch(vnodeToMove, newStartVnode);
        parentEl.insertBefore(vnodeToMove.el, oldStartVnode.el);
        oldChildren[idxInOld] = null;
      } else {
        // 新增节点
        parentEl.insertBefore(createEl(newStartVnode), oldStartVnode.el);
      }
      newStartVnode = newChildren[++newStartIdx];
    }
  }

  // 处理剩余节点
  if (newStartIdx <= newEndIdx) {
    const refEl = newEndIdx + 1 < newChildren.length ? newChildren[newEndIdx + 1].el : null;
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      parentEl.insertBefore(createEl(newChildren[i]), refEl);
    }
  }

  if (oldStartIdx <= oldEndIdx) {
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      if (oldChildren[i]) {
        parentEl.removeChild(oldChildren[i].el);
      }
    }
  }
}

三、组件化与生命周期

Vue 组件是独立的实例,拥有自己的生命周期和作用域。

核心机制:

  1. 组件实例化
    • 每个组件都是 Vue 构造函数的实例。
    • 组件间通过 propsevents 通信。
  2. 生命周期钩子
    • 关键阶段:beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed
  3. 异步渲染队列
    • 多次数据变更会合并为一次 DOM 更新,通过 nextTick 访问更新后的 DOM。

生命周期简化流程图:

创建实例 → 初始化数据 → 编译模板 → 挂载 DOM → 数据变更 → 虚拟 DOM  diff → 更新 DOM → 销毁实例

四、模板编译

Vue 将模板字符串编译为 render 函数,生成 VNode。

编译流程:

  1. 解析(Parse)
    • 将模板字符串转换为 AST(抽象语法树)。
  2. 优化(Optimize)
    • 标记静态节点,避免重复 diff。
  3. 生成(Generate)
    • 将 AST 转换为 render 函数代码。

示例:

<!-- 模板 -->
<div id="app">
  <p>{{ message }}</p>
</div>

编译后的 render 函数:

function render() {
  return createVNode('div', { id: 'app' }, [
    createVNode('p', null, [this.message])
  ]);
}

五、Vue 3.x 核心改进

  1. Proxy 响应式系统
    • 解决 Vue 2.x 对象新增属性、数组索引修改等限制。
  2. 组合式 API(Composition API)
    • 通过 setup() 函数实现逻辑复用和代码组织。
  3. Tree-Shaking 支持
    • 按需打包,减小生产包体积。
  4. 性能优化
    • 更高效的 Diff 算法(PatchFlag)、静态提升(Static Hoisting)等。

六、常见面试问题解答

  1. Vue 响应式原理是什么?
    • Vue 通过 Object.defineProperty()Proxy 劫持数据的 getter/setter,结合依赖收集和发布订阅模式实现。
  2. Vue 如何检测数组变化?
    • Vue 2.x 通过重写数组原型方法(如 push, pop)实现监听,Vue 3.x 直接使用 Proxy
  3. 虚拟 DOM 的优缺点?
    • 优点:减少 DOM 操作次数、跨平台支持(SSR、移动端);
    • 缺点:首次渲染效率较低、复杂场景 diff 算法可能耗时。
  4. Vue 生命周期钩子的作用?
    • 例如 created 用于数据初始化,mounted 用于 DOM 操作,beforeDestroy 用于清理资源。

总结

Vue 的核心优势在于响应式系统的优雅设计、虚拟 DOM 的高效更新和组件化的开发模式。理解这些原理有助于写出更高效、可维护的 Vue 应用,同时也能更好地应对性能优化和疑难问题排查。

Vue2 和 Vue3的区别

Vue 2 和 Vue 3 是 Vue.js 框架的两个主要版本,Vue 3 在保持与 Vue 2 兼容性的同时引入了多项重大改进。以下是它们的核心区别及升级建议:

一、架构与性能

  1. 响应式系统
  • Vue 2

    • 基于

      Object.defineProperty()
      

      实现,存在以下限制:

      • 无法检测对象属性的添加或删除。
      • 数组通过劫持原型方法实现响应式,部分操作(如直接通过索引修改)需手动处理。
  • Vue 3

    • 使用 ES6 Proxy 重构响应式系统,解决了上述问题:
      • 可以检测对象属性的添加 / 删除。
      • 对数组操作的响应式支持更全面。
      • 性能提升约 2 倍(更少的依赖追踪开销)。
  1. 编译优化
  • Vue 3

    • 引入

      Block Tree

      Patch Flag

      等编译时优化:

      • 静态内容不再参与虚拟 DOM 比较,提升渲染效率。
      • 动态绑定标记更精确,仅更新变化的部分。
  1. 体积优化
  • Vue 3
    • 通过 Tree-Shaking 移除未使用的代码,核心体积减少约 41%。

二、API 设计

  1. 组合式 API(Composition API)
  • Vue 3

    • 新增

      Composition API

      (基于

      setup()
      

      函数或

      <script setup>
      

      ):

      vue

      <script setup>
      import { ref, onMounted } from 'vue';
      
      const count = ref(0);
      
      const increment = () => {
        count.value++;
      };
      
      onMounted(() => {
        console.log('Component mounted');
      });
      </script>
      
    • 解决了 Vue 2 选项式 API(Options API)的以下问题:

      • 代码复用困难:逻辑分散在 datamethodscomputed 等选项中。
      • 大型组件难以维护:相关逻辑被拆分在不同选项,导致 “碎片化”。
  1. 选项式 API 的变化
  • Vue 3
    • 保留大部分选项式 API,但有以下调整:
      • data 必须是函数。
      • 生命周期钩子改名(如 beforeDestroybeforeUnmount)。
      • 新增 setup() 选项作为组件的入口点。

三、组件与模块

  1. 多根组件(Fragment)
  • Vue 3

    • 组件可以有多个根节点

      <template>
        <header>...</header>
        <main>...</main>
        <footer>...</footer>
      </template>
      
  1. Teleport(传送门)
  • Vue 3

    • 允许将组件渲染到 DOM 中的其他位置:

      <teleport to="body">
        <div class="modal">...</div>
      </teleport>
      
  1. Suspense(异步组件)
  • Vue 3

    • 内置对异步组件的支持:

      <Suspense>
        <template #default>
          <AsyncComponent />
        </template>
        <template #fallback>
          <LoadingSpinner />
        </template>
      </Suspense>
      

四、TypeScript 支持

  • Vue 2

    • 需要通过 vue-class-componentvue-property-decorator 等插件支持 TypeScript,集成不够自然。
  • Vue 3

    • 从底层设计支持 TypeScript,组合式 API 提供了更友好的类型推导:

      import { ref, computed } from 'vue';
      
      const count = ref(0); // 类型自动推断为 Ref<number>
      const double = computed(() => count.value * 2); // Ref<number>
      

五、生态与兼容性

  1. Vue Router
  • Vue 3

    • 需要使用 Vue Router 4.x,支持组合式 API:

      import { useRoute, useRouter } from 'vue-router';
      
      const route = useRoute();
      const router = useRouter();
      
  1. Vuex
  • Vue 3
    • 推荐使用 Pinia 替代 Vuex,提供更简洁的 API 和更好的 TypeScript 支持。
  1. 插件与库
  • 部分 Vue 2 插件需要更新后才能兼容 Vue 3(如 vuex-persistedstate)。

六、升级建议

  1. 新项目
  • 优先选择 Vue 3 + TypeScript + 组合式 API,充分利用新特性和性能优化。
  1. 现有项目升级
  • 渐进式迁移:使用 Vue 3 的 兼容构建版本(compatible build),允许在 Vue 3 中使用部分 Vue 2 特性。
  • Vue CLI → Vite:考虑迁移到 Vite 构建工具,提升开发体验。

总结

特性 Vue 2 Vue 3
响应式原理 Object.defineProperty() Proxy
API 风格 选项式 API 组合式 API + 选项式 API
TypeScript 支持 有限支持,需额外插件 原生支持
多根组件 不支持 支持
异步组件 需要第三方库 内置 Suspense 组件
性能 良好 显著提升
体积 较大 更小(Tree-Shaking)

Vue 3 在保持与 Vue 2 兼容性的同时,解决了长期存在的痛点(如 TypeScript 支持、大型组件维护),并提供了更现代的 API 设计和性能优化。对于新项目和有能力升级的现有项目,Vue 3 是更好的选择。

Vue怎么实现双向数据绑定,一些原理性的问题

Vue 的双向数据绑定(Two-Way Data Binding)是其核心特性之一,本质是数据变化时自动更新视图,视图变化时自动同步数据,形成 “数据 ↔ 视图” 的闭环。其实现依赖于 响应式系统模板编译事件监听 三大核心机制,Vue 2 和 Vue 3 在底层实现上有差异,但整体思路一致。

一、核心原理:双向绑定的 “双向” 拆解

双向绑定的本质是 “两个单向绑定的结合”:

  1. 数据 → 视图:数据变化时,自动更新视图(依赖响应式系统和视图更新机制)。
  2. 视图 → 数据:视图变化时(如用户输入),自动同步数据(依赖事件监听)。

二、数据 → 视图:数据驱动视图的原理

当数据发生变化时,Vue 能自动更新视图,核心依赖 响应式系统依赖收集机制

1. 响应式系统:数据劫持(监听数据变化)

Vue 通过 “劫持” 数据的读取和修改操作,实现对数据变化的感知。

  • Vue 2 实现:Object.defineProperty() Vue 2 对数据(data 中的对象)的每个属性通过 Object.defineProperty() 重写 gettersetter

    • getter:当属性被读取时触发,用于收集依赖(记录 “谁在使用这个数据”)。
    • setter:当属性被修改时触发,用于通知依赖更新(告诉 “使用这个数据的地方” 重新渲染)。

    示例简化代码

    function defineReactive(obj, key, value) {
      // 递归处理嵌套对象
      observe(value); 
      Object.defineProperty(obj, key, {
        get() {
          // 收集依赖(如 Watcher)
          Dep.target && dep.addSub(Dep.target); 
          return value;
        },
        set(newValue) {
          if (newValue !== value) {
            value = newValue;
            observe(newValue); // 新值若为对象,继续劫持
            // 通知所有依赖更新
            dep.notify(); 
          }
        }
      });
    }
    
  • Vue 3 实现:Proxy Vue 3 改用 ES6 的 Proxy 代理整个对象(而非单个属性),解决了 Vue 2 的局限性:

    • 支持检测对象属性的新增 / 删除Object.defineProperty 无法做到)。
    • 支持数组的索引修改(如 arr[0] = 1)和长度变化(如 arr.length = 0)。

    示例简化代码:

    function reactive(obj) {
      return new Proxy(obj, {
        get(target, key) {
          const value = Reflect.get(target, key);
          // 收集依赖(如 Effect)
          track(target, key); 
          return isObject(value) ? reactive(value) : value; // 递归代理
        },
        set(target, key, newValue) {
          Reflect.set(target, key, newValue);
          // 通知依赖更新
          trigger(target, key); 
        }
      });
    }
    

2. 依赖收集:记录 “谁在使用数据”

数据变化时,需要知道哪些地方(如视图、计算属性)依赖了该数据,才能精准更新。这一过程称为 “依赖收集”。

  • Vue 2 中的角色
    • Dep:每个响应式属性对应一个 Dep 实例,用于管理依赖(存储使用该属性的 Watcher)。
    • Watcher:“依赖” 的具体载体(如组件渲染、计算属性、$watch 回调)。当数据变化时,Dep 会通知所有关联的 Watcher 执行更新。
  • Vue 3 中的角色
    • Effect 替代 Watchertrack 函数收集依赖(记录当前活跃的 Effect),trigger 函数触发所有关联的 Effect 执行。

3. 视图更新:从数据变化到 DOM 渲染

当数据变化触发 setter(Vue 2)或 Proxy.set(Vue 3)后,会通过以下流程更新视图:

  1. 通知所有依赖(Watcher/Effect)“数据变了”。
  2. 依赖触发更新函数(如组件的渲染函数),生成新的虚拟 DOM(VNode)。
  3. 通过虚拟 DOM 的 diff 算法 对比新旧 VNode,计算出最小更新范围。
  4. 将差异应用到真实 DOM,完成视图更新。

三、视图 → 数据:视图驱动数据的原理

当用户操作视图(如输入框输入、按钮点击)时,Vue 通过事件监听同步更新数据,实现视图到数据的反向绑定。

v-model(双向绑定的语法糖)为例:

  • v-model
    

    在输入框(如

    <input>
    

    )上会被编译为:

    预览

    <!-- 模板 -->
    <input v-model="message">
    
    <!-- 编译后等价于:单向数据绑定 + 事件监听 -->
    <input :value="message" @input="message = $event.target.value">
    
    • :value="message":数据 → 视图的单向绑定(数据变化时更新输入框值)。
    • @input="message = $event.target.value":视图 → 数据的同步(用户输入时,通过 input 事件更新 message 数据)。

四、Vue 2 与 Vue 3 双向绑定的核心差异

机制 Vue 2 Vue 3
数据劫持方式 Object.defineProperty() Proxy
依赖收集载体 Watcher 实例 Effect 函数
缺陷 无法监听对象新增 / 删除属性、数组索引修改 无上述缺陷,原生支持所有数据操作
性能 依赖追踪开销较大 更高效的依赖追踪,性能提升约 2 倍

总结

Vue 双向数据绑定的核心逻辑是:

  1. 通过 数据劫持Object.definePropertyProxy)感知数据变化。
  2. 通过 依赖收集WatcherEffect)记录数据与视图的关联。
  3. 数据变化时,通过 虚拟 DOM diff 更新视图(数据 → 视图)。
  4. 视图变化时,通过 事件监听 同步数据(视图 → 数据)。

这一机制让开发者无需手动操作 DOM,只需关注数据逻辑,大幅提升开发效率。

虚拟DOM和真实DOM的区别

虚拟 DOM(Virtual DOM)和真实 DOM(Real DOM)是前端开发中的两个重要概念,它们的主要区别如下:

  1. 定义与结构
  • 真实 DOM 是由浏览器提供的 API,是文档的树形结构表示,每个节点都是一个对象,直接与浏览器渲染引擎交互。操作真实 DOM 的代价很高,因为每次修改都会触发浏览器的重排(reflow)和重绘(repaint)。
  • 虚拟 DOM 是真实 DOM 的抽象表示,通常用 JavaScript 对象或轻量级数据结构(如 React 中的element)来模拟 DOM 树。它是真实 DOM 的 "虚拟映射",不直接参与渲染。
  1. 性能差异
  • 真实 DOM 直接操作会导致频繁的重排和重绘,性能开销大,尤其在复杂应用中容易出现卡顿。
  • 虚拟 DOM 通过批量计算差异(Diff 算法),只将必要的变更一次性应用到真实 DOM 上,减少渲染次数,提高性能。
  1. 操作方式
  • 真实 DOM 操作直接影响页面,例如:

    document.getElementById('app').innerHTML = '<div>Hello World</div>';
    
  • 虚拟 DOM 通过状态变化触发重新渲染,框架内部计算差异后更新真实 DOM,例如 React 中的 JSX:

    function App() {
      return <div>Hello World</div>;
    }
    
  1. 应用场景
  • 真实 DOM 适合简单交互或直接操作特定元素的场景(如动画、临时 UI 更新)。
  • 虚拟 DOM 适合复杂 UI 和频繁更新的应用(如单页应用),通过减少 DOM 操作提升效率。
  1. 典型框架
  • 真实 DOM 原生 JavaScript、jQuery 等直接操作 DOM 的库。
  • 虚拟 DOM React、Vue.js(2.x 及以后)、Angular 等现代框架。

总结

虚拟 DOM 通过抽象和解耦 DOM 操作,减少了直接操作真实 DOM 的性能损耗,尤其在大型应用中优势明显。但它并非银弹,在简单场景下可能带来额外的复杂度。

前端Key有什么作用

在前端开发中,key 是一个特殊的属性(常见于 React、Vue 等框架),主要用于优化列表渲染性能和确保 DOM 元素的正确识别。其核心作用如下:

1. 标识列表项的唯一性

当渲染列表(如 v-formap 生成的元素)时,key 用于标识每个列表项的唯一身份。 框架通过 key 判断元素是否为新创建、已存在或需要删除,从而避免对整个列表重新渲染。

示例(Vue)

<ul>
  <li v-for="item in list" :key="item.id">
    {{ item.name }}
  </li>
</ul>

示例(React)

<ul>
  {list.map(item => (
    <li key={item.id}>{item.name}</li>
  ))}
</ul>
  • 这里 item.id 作为 key,确保每个列表项有唯一标识。

2. 优化 DOM 渲染性能

框架(如 React、Vue)采用虚拟 DOM 机制,通过对比新旧虚拟 DOM 树来更新真实 DOM。 key 的存在让框架能快速定位:

  • 相同 key:元素可能只是内容变化,无需重新创建 DOM 节点(仅更新内容)。
  • 不同 key:元素为新节点,需要创建;或旧节点已被移除,需要删除。

反例:不使用 key 或使用索引作为 key 的问题 若用数组索引(index)作为 key,当列表发生增删、排序时,key 可能会与实际元素错位,导致:

  • 不必要的 DOM 节点销毁与重建(性能浪费)。
  • 状态错乱(如表单输入值、组件状态与 DOM 不匹配)。

示例: 原列表 [A, B, C] 用索引 0,1,2 作为 key,若在头部插入 D,新列表变为 [D, A, B, C],此时 Akey0 变为 1,框架会误判 A 是新元素并重建,导致性能损耗。

3. 确保组件状态的正确复用

对于列表中的组件,key 决定组件实例是否复用:

  • key 不变,组件实例会被复用(保留内部状态,如 datastate)。
  • key 变化,组件会被销毁并重新创建(重置内部状态)。
<!-- Vue 中通过改变 key 重置组件 -->
<my-component :key="activeTab" />

4. 避免渲染错误

在某些场景下,缺少 key 可能导致渲染异常:

  • 列项包含表单元素(如 input)时,可能出现输入值与显示内容不匹配。
  • 动画或过渡效果错乱(框架无法正确识别元素的插入 / 删除状态)。

使用 key 的注意事项

  1. 唯一性key 在当前列表中必须唯一,不能重复(否则会报错或导致渲染异常)。
  2. 稳定性key 应尽量稳定(如用后端返回的 id),避免使用易变的值(如索引 index 或随机数)。
  3. 作用域key 的唯一性仅针对当前列表,不同列表的 key 可以重复。

总结

key 的核心作用是帮助框架高效识别列表项的身份,从而优化 DOM 更新性能、确保组件状态正确复用。 开发中应优先使用数据自身的唯一标识(如 id)作为 key,避免滥用索引或随机值。

怎么判断一个空对象,空数组

  • 判断空对象:优先使用 Object.keys(obj).length === 0,若需包含不可枚举属性,使用 Reflect.ownKeys()
  • 判断空数组:直接使用 arr.length === 0,若需严格校验元素,结合 Array.every()
  • 通用场景:使用 lodash.isEmpty() 或自定义函数结合多种方法。
  1. 后端接口返回的数据为空时,前端怎么判断

前端判断后端返回的 “空数据”,核心是:

  1. 明确 “空” 的具体形式(结合接口文档);
  2. 针对性判断(区分 null、空对象、空数组等);
  3. 避免误判(如不把 0false 当作空值)。

合理的空值判断能提升代码健壮性,避免因数据异常导致的页面崩溃或逻辑错误。

  1. Vue2 set 是什么,用来做什么

    在 Vue2 中,Vue.set(或其别名 this.$set)是一个全局 API,用于解决 Vue2 响应式系统的深层响应式限制。它的主要作用是向响应式对象中添加一个新属性,并确保这个新属性同样具备响应式能力。

    为什么需要 Vue.set?

    Vue2 的响应式系统基于 Object.defineProperty() 实现,它在初始化时会递归遍历 data 对象的所有属性,将其转换为 getter/setter。但这种方式存在两个限制:

    1. 无法检测对象属性的添加或删除 当你直接给响应式对象添加一个新属性时(如 this.obj.newProp = 'value'),Vue2 无法自动将这个新属性转换为响应式的,因此不会触发视图更新。
    2. 无法检测数组索引的直接修改 当你通过索引直接修改数组元素时(如 this.arr[0] = 'new value'),Vue2 也无法捕获到这个变化。

Vue.set 的用法

Vue.set 接受三个参数:

Vue.set(target, propertyName/index, value)
  • target:要添加属性的响应式对象或数组。
  • propertyName/index:属性名或数组索引。
  • value:新属性的值。

示例 1:给对象添加响应式属性

export default {
data() {
 return {
   user: {
     name: 'John',
   }
 }
},
methods: {
 addAge() {
   // 错误:非响应式,视图不会更新
   // this.user.age = 30;
   
   // 正确:使用 Vue.set 确保响应式
   Vue.set(this.user, 'age', 30);
   
   // 或使用 this.$set 别名(组件内)
   this.$set(this.user, 'age', 30);
 }
}
}

示例 2:更新数组元素

export default {
data() {
 return {
   items: ['a', 'b', 'c']
 }
},
methods: {
 updateFirstItem() {
   // 错误:非响应式,视图不会更新
   // this.items[0] = 'A';
   
   // 正确:使用 Vue.set 更新数组元素
   Vue.set(this.items, 0, 'A');
   
   // 或使用数组变异方法(Vue2 已对这些方法做了响应式处理)
   this.items.splice(0, 1, 'A');
 }
}
}

替代方案

除了 Vue.set,还有一些场景可以使用其他方式实现响应式更新:

  1. 替换整个对象 如果需要添加多个属性,可以用新对象替换原对象:

    // 替换整个 user 对象,确保所有新属性都是响应式的
    this.user = {
      ...this.user,  // 保留原有属性
      age: 30,
      gender: 'male'
    }
    
  2. 使用数组变异方法 Vue2 对数组的某些方法(如 pushsplice)做了响应式处理:

    // 添加元素(响应式)
    this.items.push('new item');
    
    // 删除元素(响应式)
    this.items.splice(1, 1);
    

注意事项

  1. **Vue3 中的变化**
     Vue3 使用 Proxy 实现响应式系统,基本解决了上述限制,因此不再需要 `Vue.set`。但为了兼容 Vue2 代码,Vue3 仍保留了这个 API,但多数场景已不再需要。
  2. **性能考虑**
     频繁使用 `Vue.set` 可能影响性能,尤其是在大型对象或数组中。尽量在初始化时就声明所有需要的属性。
  3. **深层对象**
     如果需要给深层嵌套的对象添加属性,可能需要先确保父级对象已经是响应式的。

总结

Vue.set 是 Vue2 中用于解决响应式系统限制的重要工具,主要用于:

  • 向响应式对象添加新属性。
  • 更新数组元素(通过索引)。

在 Vue2 项目中,当你发现直接赋值无法触发视图更新时,优先考虑使用 Vue.set 或其替代方案。

Vuex讲一下

Vuex 是专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。以下是关于 Vuex 的核心概念、工作流程及应用场景的详细介绍:

一、核心概念

  1. State
  • 单一数据源:应用的所有状态集中存储在一个 store 中。
  • 响应式:Vuex 的 state 变化会自动触发视图更新。
  1. Getter
  • 计算属性:类似于 Vue 组件中的 computed,用于获取 state 的派生数据。
  • 缓存机制:依赖的 state 不变时,多次调用不会重复计算。
  1. Mutation
  • 唯一修改途径:修改 state 必须通过提交 mutation。
  • 同步操作:确保状态变化可追踪和调试。
  1. Action
  • 异步操作:处理异步逻辑(如 API 请求),完成后提交 mutation。
  • 分发(dispatch):通过 store.dispatch() 触发。
  1. Module
  • 模块化:将 store 分割成多个模块,每个模块有自己的 state、mutation、action 等。

二、工作流程

组件触发 Action(异步操作) → Action 提交 Mutation → Mutation 修改 State → State 变化触发视图更新

关键流程说明

  1. 组件中触发 Action

    this.$store.dispatch('fetchUserInfo');
    
  2. Action 处理异步逻辑

    actions: {
      fetchUserInfo({ commit }) {
        api.getUser().then(data => {
          commit('SET_USER', data); // 提交 mutation
        });
      }
    }
    
  3. Mutation 修改 State

    mutations: {
      SET_USER(state, user) {
        state.user = user; // 直接修改 state
      }
    }
    
  4. 组件获取 State

    computed: {
      user() {
        return this.$store.state.user;
      }
    }
    

三、应用场景

  1. 多组件共享状态
    • 如用户信息、主题设置、购物车数据等。
  2. 复杂数据流管理
    • 多级组件嵌套通信(替代事件总线或 prop 层层传递)。
  3. 状态持久化
    • 结合插件(如 vuex-persistedstate)将 state 存储到本地存储或会话存储。
  4. 服务端渲染(SSR)
    • 在 Vue SSR 中预取数据并同步到客户端。

四、代码示例

1. 基础 Store 结构

// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    count: 0,
    user: null
  },
  getters: {
    doubleCount(state) {
      return state.count * 2;
    }
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    setUser(state, user) {
      state.user = user;
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    },
    fetchUser({ commit }) {
      return api.getUser().then(user => {
        commit('setUser', user);
        return user;
      });
    }
  }
});

2. 在组件中使用

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <button @click="increment">+</button>
    <button @click="incrementAsync">Async +</button>
  </div>
</template>

<script>
export default {
  computed: {
    // 获取 state
    count() {
      return this.$store.state.count;
    },
    // 获取 getter
    doubleCount() {
      return this.$store.getters.doubleCount;
    }
  },
  methods: {
    // 提交 mutation
    increment() {
      this.$store.commit('increment');
    },
    // 分发 action
    incrementAsync() {
      this.$store.dispatch('incrementAsync');
    }
  }
};
</script>

五、高级特性

  1. 模块化

    // store/modules/cart.js
    export default {
      namespaced: true, // 启用命名空间
      state: { items: [] },
      mutations: { /* ... */ },
      actions: { /* ... */ },
      getters: { /* ... */ }
    };
    
    // 在主 store 中引入模块
    new Vuex.Store({
      modules: {
        cart: cartModule
      }
    });
    
  2. 插件

    • 日志插件:记录 mutation 变化。
    • 持久化插件:自动同步 state 到本地存储。
  3. 严格模式

    • 开发环境中强制所有 state 变更通过 mutation,防止意外修改。

六、Vuex vs Pinia

Vuex 是 Vue 2 的主流状态管理库,而 Pinia 是 Vue 3 推荐的新方案,相比 Vuex 有以下优势:

  • 更简洁的 API(无需编写 mutations)。
  • 更好的 TypeScript 支持。
  • 更小的体积和更灵活的模块化。

Vuex 仍适用于现有 Vue 2 项目,而新项目建议优先考虑 Pinia。

总结

Vuex 通过集中式状态管理解决了复杂应用中的数据共享和数据流问题,遵循单向数据流原则,使状态变化可预测且易于调试。在中大型 Vue 应用中,合理使用 Vuex 能显著提升代码可维护性和开发效率。

Vue 如何做部分响应式得监听比如obj={a:1,c:,d:{}}只监听a,c,d不监听

在 Vue 中实现部分属性的响应式监听,可以根据 Vue 版本的不同采用不同的方案。核心思路是只让需要监听的属性(a、c)成为响应式,而不需要监听的属性(d)保持非响应式。

Vue 2 实现方式

Vue 2 基于 Object.defineProperty 实现响应式,只有初始化时在 data 中声明的属性才会被转为响应式。可以利用这一特性,让不需要监听的属性(d)在初始化后再添加,从而避免被响应式处理。

export default {
  data() {
    return {
      obj: {
        a: 1,
        c: 2 // 只在初始化时声明需要监听的属性
      }
    };
  },
  created() {
    // 初始化后手动添加不需要监听的属性d
    // 注意:不要使用 this.$set,否则会被转为响应式
    this.obj.d = {};
  },
  watch: {
    // 监听a的变化
    'obj.a'(newVal) {
      console.log('a变化了:', newVal);
    },
    // 监听c的变化
    'obj.c'(newVal) {
      console.log('c变化了:', newVal);
    }
  },
  methods: {
    updateProps() {
      this.obj.a = 10; // 会触发watch监听
      this.obj.c = 20; // 会触发watch监听
      this.obj.d = { x: 1 }; // 不会触发任何响应式更新
    }
  }
};

原理: Vue 2 只会对 data 初始化时存在的属性(a、c)进行响应式处理,后续直接添加的属性(d)不会被拦截,因此修改 d 不会触发组件更新或 watch 监听。

Vue 3 实现方式

Vue 3 基于 Proxy 实现响应式,默认会对对象的所有属性(包括新增属性)进行监听。需要通过 shallowReactive 或手动分离属性来实现部分响应式。

方案 1:使用 shallowReactive(浅响应式)

shallowReactive 只会使对象的第一层属性成为响应式,嵌套属性(如 d 内部的属性)不会被响应式处理。但如果只是不想监听 d 本身,可以结合手动赋值:

import { reactive, shallowReactive } from 'vue';

export default {
  setup() {
    // 用shallowReactive创建浅响应式对象(只监听第一层属性)
    const obj = shallowReactive({
      a: 1,
      c: 2
    });

    // 手动添加不需要监听的属性d(不会被响应式处理)
    obj.d = {};

    // 监听a和c的变化
    watch(
      () => obj.a,
      (newVal) => console.log('a变化了:', newVal)
    );
    watch(
      () => obj.c,
      (newVal) => console.log('c变化了:', newVal)
    );

    const updateProps = () => {
      obj.a = 10; // 会触发监听
      obj.c = 20; // 会触发监听
      obj.d = { x: 1 }; // 不会触发监听
    };

    return { obj, updateProps };
  }
};

方案 2:分离响应式与非响应式属性

将需要监听的属性(a、c)放在响应式对象中,不需要监听的属性(d)放在普通对象中:

import { reactive, watch } from 'vue';

export default {
  setup() {
    // 响应式对象:只包含需要监听的a和c
    const reactiveObj = reactive({
      a: 1,
      c: 2
    });

    // 普通对象:不需要监听的d
    const normalObj = {
      d: {}
    };

    // 合并为一个对象使用(可选)
    const obj = { ...reactiveObj, ...normalObj };

    // 监听a和c
    watch(
      () => reactiveObj.a,
      (newVal) => console.log('a变化了:', newVal)
    );

    const updateProps = () => {
      reactiveObj.a = 10; // 触发监听
      normalObj.d = { x: 1 }; // 不触发监听
    };

    return { obj, updateProps };
  }
};

原理: Vue 3 的 reactive 会对所有属性(包括新增)进行响应式处理,而 shallowReactive 只处理第一层属性。通过分离属性或使用浅响应式,可以避免不需要的属性(d)被监听。

总结

  • Vue 2:利用初始化时未声明的属性不会被响应式处理的特性,后续手动添加非监听属性。
  • Vue 3:使用 shallowReactive 限制响应式层级,或通过分离对象将非监听属性放在普通对象中。

两种方式都能实现只监听 ac,而忽略 d 的变化。

Vue2 provide/inject为什么有意设计不是响应式,而Vue3又是

Vue2 中 provide/inject 设计为非响应式,而 Vue3 使其支持响应式,本质上是由框架设计理念、响应式系统实现差异以及使用场景定位共同决定的,具体原因如下:

一、Vue2 中 provide/inject 非响应式的设计考量

  1. 核心定位:依赖注入而非状态管理 Vue2 中 provide/inject 的设计初衷是解决深层嵌套组件间的配置传递问题(如主题、国际化配置等静态 / 低频变动的数据),而非用于动态状态共享。 例如:一个组件库的底层组件可能需要获取顶层组件的配置(如是否禁用动画),这类配置通常在初始化后不会频繁变化,因此无需响应式。
  2. 避免滥用导致的数据流混乱 Vue2 强调 “单向数据流”(父→子通过 props,子→父通过事件),而 provide/inject 本质上是 “跨层级透传”,如果支持响应式,可能会导致:
    • 开发者过度依赖它实现状态共享,替代 Vuex 或 props,使数据流向变得隐蔽(难以追踪谁修改了数据)。
    • 响应式依赖链变长,增加调试复杂度(比如深层组件修改注入的数据,难以定位来源)。
  3. 响应式系统的技术限制 Vue2 的响应式基于 Object.defineProperty,对对象 / 数组的拦截存在天然限制(如无法监听新增属性、数组索引修改等)。若要让 provide/inject 支持响应式,需要手动将数据包装为 Vue.observable(Vue2 中让对象响应式的方法),但这会增加使用成本,且不符合其 “轻量配置传递” 的定位。

二、Vue3 中 provide/inject 支持响应式的原因

  1. 响应式系统的底层升级 Vue3 改用 Proxy 实现响应式,能原生支持对对象、数组的完整拦截(包括新增属性、删除属性等),且响应式数据的包装(ref/reactive)更轻量、直观。 这使得 provide/inject 可以自然地传递响应式数据 —— 只需将 refreactive 对象通过 provide 传递,inject 后即可直接触发响应式更新,无需额外处理。
  2. 使用场景的扩展:配合 Composition API Vue3 引入的 Composition API 鼓励将逻辑按功能拆分(而非按选项),这使得跨组件共享 “带响应式的业务逻辑” 成为常见需求。 例如:在组件树中共享用户登录状态(user = ref(null)),需要在多个层级的组件中实时响应状态变化。此时 provide/inject 作为轻量级跨层级通信方案,必须支持响应式才能满足需求。
  3. 更灵活的 “响应式控制权” Vue3 并未强制 provide/inject 必须响应式,而是让开发者自主选择:
    • 传递普通值(非响应式):用于静态配置。
    • 传递 ref/reactive 对象(响应式):用于动态状态共享。 这种设计既保留了 Vue2 中 “配置传递” 的能力,又新增了 “响应式状态共享” 的选项,更符合多样化场景。
  4. 与状态管理库的互补 Vue3 中,provide/inject 的响应式能力与 Pinia(官方状态管理库)并不冲突:
    • Pinia 适合全局共享的核心状态(如用户信息、购物车),提供更严格的规范(actions 修改状态)。
    • provide/inject 适合局部跨层级共享(如某个页面内的组件树),更轻量、无需定义全局 store。 响应式支持让 provide/inject 在局部场景中更实用。

总结

  • Vue2 的 provide/inject 非响应式,是为了聚焦静态配置传递、避免数据流混乱,同时受限于当时的响应式系统实现。
  • Vue3 使其支持响应式,是响应式系统升级Composition API 场景扩展的自然结果,既保留了灵活性,又满足了动态状态共享的需求。

本质上,这是框架在 “设计约束” 和 “开发者需求” 之间的平衡调整 ——Vue3 通过更强大的底层能力,让 provide/inject 从 “单纯的依赖注入” 进化为 “灵活的跨层级通信工具”。

Vue 如何在监听上一行代码执行完后在执行后面得代码

在 Vue 中,若需要保证 “上一行代码执行完毕后再执行后续代码”,核心取决于上一行代码是同步操作还是异步操作。Vue 本身遵循 JavaScript 的执行机制,同步代码会按顺序执行,而异步代码(如 API 请求、定时器、DOM 更新等)需要通过特定方式等待其完成。以下是具体场景和解决方案:

一、同步代码:天然按顺序执行

如果上一行是同步代码(如变量赋值、简单计算、同步函数调用),JavaScript 会按代码顺序依次执行,无需额外处理。

// 示例:同步代码天然顺序执行
const a = 1;
const b = a + 2; // 上一行执行完才会执行此行,b 一定是 3
console.log(b); // 输出 3

在 Vue 组件中(包括 <script setup>),同步代码同样遵循此规则:

<script setup>
import { ref } from 'vue';

const count = ref(0);

// 同步操作:按顺序执行
count.value = 1; // 第一步:修改值
const double = count.value * 2; // 第二步:使用修改后的值(double 一定是 2)
console.log(double); 
</script>

二、异步代码:需显式等待执行完成

如果上一行是异步操作(如 API 请求、setTimeout、Promise 等),JavaScript 会跳过异步操作继续执行后续代码,此时需要通过 async/await.then() 确保异步操作完成后再执行后续逻辑。

场景 1:异步 API 请求(如 axios)

假设上一行是发送 API 请求,后续代码需要使用请求结果:

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const data = ref(null);

// 错误示例:异步操作未等待,后续代码可能拿到 undefined
const res = axios.get('/api/data'); // 异步请求,不会阻塞后续代码
console.log(res.data); // 错误:此时请求未完成,res 是 Promise 对象

// 正确示例:使用 async/await 等待异步完成
const fetchData = async () => {
  // 上一行:等待 API 请求完成
  const res = await axios.get('/api/data'); 
  // 下一行:请求完成后才执行,可安全使用 res.data
  data.value = res.data; 
  console.log('数据获取成功:', data.value); 
};

fetchData();
</script>

场景 2:定时器或 Promise 异步操作

对于 setTimeout 或自定义 Promise 异步操作,同样需要通过 async/await 等待:

<script setup>
// 自定义异步函数(返回 Promise)
const delay = (ms) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('延迟完成');
    }, ms);
  });
};

// 使用 async/await 等待异步完成
const run = async () => {
  // 上一行:等待延迟完成
  const result = await delay(1000); 
  // 下一行:延迟结束后才执行
  console.log(result); // 输出 "延迟完成"
};

run();
</script>

三、等待 Vue 响应式更新或 DOM 渲染完成

在 Vue 中,修改响应式数据(如 refreactive)后,DOM 不会立即更新(Vue 会批量处理 DOM 更新以优化性能)。如果后续代码需要基于更新后的 DOM 状态执行(如获取 DOM 尺寸、位置),需要使用 nextTick

场景:修改数据后等待 DOM 更新

<template>
  <div ref="content">{{ message }}</div>
</template>

<script setup>
import { ref, nextTick } from 'vue';

const message = ref('初始文本');
const content = ref(null);

const updateMessage = async () => {
  // 上一行:修改响应式数据(DOM 不会立即更新)
  message.value = '更新后的文本'; 
  
  // 错误示例:直接获取 DOM,内容可能还是旧的
  console.log(content.value.textContent); // 可能输出 "初始文本"(DOM 未更新)

  // 正确示例:使用 nextTick 等待 DOM 更新完成
  await nextTick(); 
  // 下一行:DOM 已更新,可获取最新内容
  console.log(content.value.textContent); // 输出 "更新后的文本"
};

updateMessage();
</script>

原理nextTick 会在 Vue 完成当前批次 DOM 更新后执行回调,确保能获取到最新的 DOM 状态。

四、在 watch 中等待异步操作完成

如果在 watch 监听中需要等待上一行异步操作完成后再执行后续逻辑,可将 watch 回调定义为 async 函数,结合 await 使用。

<script setup>
import { ref, watch } from 'vue';
import axios from 'axios';

const userId = ref(1);
const userInfo = ref(null);

// 监听 userId 变化,等待 API 请求完成后再处理后续逻辑
watch(userId, async (newId) => {
  // 上一行:等待 API 请求完成
  const res = await axios.get(`/api/user/${newId}`); 
  // 下一行:请求完成后更新数据
  userInfo.value = res.data; 
  // 后续逻辑:基于 userInfo 执行
  console.log('用户名称:', userInfo.value.name); 
});
</script>

总结

确保 “上一行代码执行完后再执行后续代码” 的核心方案:

  1. 同步代码:无需处理,天然按顺序执行。
  2. 异步操作(API、定时器等):使用 async/await.then() 等待异步完成。
  3. 等待 DOM 更新:使用 nextTick 确保 DOM 已更新。
  4. watch 中处理异步:将 watch 回调定义为 async 函数,通过 await 等待异步操作。

根据具体场景选择对应方案即可保证代码执行顺序。

vue nextTick的原理和应用场景

在 Vue 中,nextTick 是一个核心 API,用于在 DOM 更新完成后执行回调函数。理解其原理和应用场景对编写高效、稳定的 Vue 代码至关重要。

一、原理:Vue 的异步更新队列

  1. Vue 的 DOM 更新机制

Vue 的响应式系统在数据变化时,不会立即更新 DOM,而是将 DOM 更新任务异步批量处理。这是为了避免频繁操作 DOM 导致性能损耗(例如,多次修改同一个数据会合并为一次 DOM 更新)。

流程

  • 当数据变化时,Vue 会触发 setter 拦截器。
  • Vue 将对应的 DOM 更新任务(即 watcher)加入异步队列,而非立即执行。
  • 在下一次 “事件循环”(Event Loop)的微任务阶段,Vue 会清空队列并执行所有 DOM 更新。
  1. nextTick 的作用

nextTick 的回调函数会被添加到微任务队列末尾,确保在 DOM 更新完成后执行。其核心原理是:

  • 等待当前所有同步代码执行完毕。
  • 等待 Vue 的异步更新队列(微任务)执行完毕。
  • 执行 nextTick 的回调函数。

伪代码逻辑

function nextTick(callback) {
  // 将回调添加到微任务队列
  if (Promise) {
    Promise.resolve().then(callback);
  } else {
    // 降级方案(兼容不支持 Promise 的环境)
    setTimeout(callback, 0);
  }
}

二、应用场景

  1. 在 DOM 更新后访问元素

当你修改数据后立即访问 DOM,此时 DOM 可能尚未更新,使用 nextTick 确保 DOM 已渲染。

react 与vue3得hooks得区别

React 和 Vue3 的 Hooks(或 Composition API)在设计理念、使用方式和底层机制上存在显著差异,核心区别体现在响应式模型、依赖追踪、函数调用规则逻辑组织方式上。以下从关键维度对比分析:

一、设计理念与核心目标

React Hooks

  • 目标:解决 class 组件的复用难题(如 HOC 嵌套地狱)、状态逻辑分散等问题,让函数组件拥有状态和生命周期能力。
  • 理念:基于 “函数式编程” 思想,强调 “每次渲染都是独立快照”,通过纯函数抽象状态逻辑,避免 class 组件的 this 指向混乱。
  • 核心场景:状态管理(useState)、副作用处理(useEffect)、逻辑复用(自定义 Hooks)。

Vue3 Composition API(类似 Hooks 的概念)

  • 目标:解决 Vue2 选项式 API(Options API)中逻辑复用困难(如 mixins 命名冲突、来源模糊)、复杂组件代码分散的问题。
  • 理念:基于 “响应式编程” 思想,通过组合函数(Composition Functions)将相关逻辑聚合,强调 “响应式数据驱动视图”,与 Vue 底层响应式系统深度结合。
  • 核心场景:响应式状态定义(ref/reactive)、副作用与依赖追踪(watch/watchEffect)、逻辑复用(组合函数)。

二、响应式模型与状态管理

React Hooks

  • 状态本质:通过 useStateuseReducer 定义的状态是 “非响应式” 的,本质是函数组件的局部变量,状态更新会触发组件重新渲染(重新执行函数)。
  • 状态更新:状态更新是 “替换式” 的(如 setCount(count + 1) 是生成新值替换旧值),对于引用类型(对象 / 数组),需手动创建新引用(如 setUser({...user, name: 'new'})),否则不会触发重渲染。
  • 访问方式:直接访问变量(如 count),但每次渲染的状态是 “快照”,闭包中捕获的是当前渲染周期的状态值。

Vue3 Composition API

  • 状态本质:通过 ref(基本类型)或 reactive(对象类型)定义的状态是 “响应式” 的,底层基于 ES6 Proxy 实现,状态变化会自动触发依赖更新。
  • 状态更新:状态更新是 “修改式” 的(如 count.value++user.name = 'new'),直接修改响应式对象的属性即可触发更新,无需替换整个对象(Proxy 会拦截修改操作)。
  • 访问方式ref 需通过 .value 访问 / 修改(模板中自动解包),reactive 直接访问属性(如 user.name),且始终能获取最新值(无闭包快照问题)。

三、副作用处理与依赖追踪

React Hooks(useEffect

  • 依赖显式声明useEffect 的执行时机由依赖数组控制,必须手动指定依赖项(如 useEffect(() => {}, [count])),依赖变化时才会重新执行副作用。
  • 依赖追踪机制:无自动依赖追踪,完全依赖开发者手动维护依赖数组。若依赖遗漏,可能导致副作用捕获旧状态(闭包问题);若依赖冗余,可能导致不必要的重复执行。
  • 清理机制:副作用函数返回的清理函数会在组件卸载或依赖变化前执行(如取消订阅、清除定时器)。
  • 执行时机:默认在 “浏览器绘制后” 执行(异步),可通过 { flush: 'sync' } 改为同步执行(不推荐)。

Vue3(watch/watchEffect

  • 依赖自动追踪watchEffect自动追踪副作用中使用的响应式数据,无需手动声明依赖。当这些响应式数据变化时,副作用自动重新执行(基于 Proxy 拦截访问)。
  • 精确监听watch 可显式指定监听源(如 watch(count, (newVal) => {})),支持监听单个 ref、reactive 对象属性或 getter 函数,依赖更可控。
  • 清理机制watchwatchEffect 的副作用函数可返回清理函数,在副作用重新执行前或组件卸载时自动调用。
  • 执行时机:默认在 “组件更新后” 执行,可通过 flush: 'pre' 改为更新前执行(适合 DOM 操作)。

四、函数调用规则与限制

React Hooks

  • 严格调用顺序:Hooks 必须在函数组件顶层调用,不能在条件语句、循环、嵌套函数中调用(如 if (flag) { useState() } 是错误的)。原因是 React 依赖 Hooks 的调用顺序来关联状态与组件,顺序错乱会导致状态匹配错误。
  • 唯一限制:必须在 React 函数组件或自定义 Hooks 中调用,否则会报错(React 内部通过上下文标记调用环境)。

Vue3 Composition API

  • 灵活调用位置refwatchonMounted 等函数可在 setup 函数或 <script setup>任意位置调用,包括条件语句、循环、嵌套函数中(如 if (flag) { const count = ref(0) } 是合法的)。原因是 Vue 的响应式依赖追踪基于 Proxy,与函数调用顺序无关,只关注实际使用的响应式数据。
  • 无严格环境限制:只要在组件实例生命周期内(如 setup 执行期间),即可调用,无需强制在特定类型的函数中。

五、生命周期对应与逻辑组织

React Hooks

  • 生命周期模拟

    :通过

    useEffect
    

    模拟生命周期,例如:

    • 组件挂载:useEffect(() => {}, [])(空依赖);
    • 组件更新:useEffect(() => {}, [dep1, dep2])(依赖变化);
    • 组件卸载:useEffect(() => { return () => {} }, [])(清理函数)。
  • 逻辑组织:按 “Hooks 调用顺序” 组织代码,同一逻辑的状态和副作用需放在相邻位置,复杂组件可能需要拆分多个自定义 Hooks(如 useUser()useForm())。

Vue3 Composition API

  • 生命周期显式化

    :提供专门的生命周期钩子(如

    onMounted
    
    onUpdated
    
    onUnmounted
    

    ),直接在

    setup
    

    中调用,语义更清晰:

    onMounted(() => { /* 挂载后执行 */ })
    onUnmounted(() => { /* 卸载时执行 */ })
    
  • 逻辑组织

    :按 “功能逻辑” 聚合代码,例如将 “用户信息加载与处理” 相关的

    ref
    
    watch
    
    onMounted
    

    放在同一个组合函数中,实现 “关注点分离”:

    function useUser() {
      const user = ref(null)
      onMounted(() => { /* 加载用户 */ })
      watch(user, () => { /* 处理用户变化 */ })
      return { user }
    }
    

六、闭包问题与状态获取

React Hooks

  • 闭包陷阱

    :由于每次渲染是独立快照,

    useEffect
    

    中捕获的状态是 “当前渲染周期的值”,若依赖数组未正确声明,可能导致副作用使用旧状态:

    const [count, setCount] = useState(0)
    useEffect(() => {
      setInterval(() => {
        console.log(count) // 始终打印 0,因未依赖 count
      }, 1000)
    }, []) // 错误:遗漏 count 依赖
    

    解决方式:需显式添加依赖

    [count]
    

    ,或用

    useRef
    

    保存最新值。

Vue3 Composition API

  • 无闭包陷阱

    :响应式数据(

    ref
    
    reactive
    

    )是 Proxy 代理对象,访问时始终获取最新值,即使在闭包中也不会捕获旧值:

    const count = ref(0)
    watchEffect(() => {
      setInterval(() => {
        console.log(count.value) // 始终打印最新值
      }, 1000)
    })
    

    原因是

    count.value
    

    访问的是 Proxy 拦截后的最新值,与闭包无关。

总结:核心差异对比表

维度 React Hooks Vue3 Composition API
响应式模型 非响应式,依赖重渲染更新 响应式(Proxy),自动追踪变化
状态更新方式 替换式(需创建新引用) 修改式(直接更新属性)
依赖追踪 显式依赖数组(需手动维护) 自动追踪(基于响应式访问)
函数调用限制 必须顶层调用(依赖顺序) 可任意位置调用(无关顺序)
闭包问题 易出现(依赖快照) 几乎无(始终访问最新值)
生命周期 useEffect 模拟 显式钩子(onMounted 等)

一句话总结

React Hooks 是 “函数式驱动的状态与副作用管理”,依赖显式声明和严格调用规则;Vue3 Composition API 是 “响应式驱动的逻辑组合”,依赖自动追踪和灵活调用方式,更贴近 Vue 原生响应式体系。

Proxy如何做的优化

在 Vue3 等框架中,使用 Proxy 实现响应式时,针对性能、内存占用和使用体验做了多项关键优化,核心思路是减少不必要的代理操作、精准追踪依赖、降低初始化成本。以下是具体的优化手段:

一、懒代理(Lazy Proxy):按需代理嵌套对象

Proxy 可以直接代理整个对象,但对于嵌套层级较深的对象(如 { a: { b: { c: 1 } } }),Vue3 不会一次性递归代理所有子对象,而是在访问子对象时才动态代理(懒加载思想)。

  • 优化点: 初始化时只代理顶层对象,避免对深层未访问的子对象做无用代理,大幅降低复杂对象的初始化时间和内存消耗。
  • 实现逻辑: 在 get 拦截器中,当访问的属性值是对象时,才对该子对象进行代理并缓存,后续访问直接复用已代理的子对象。
function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      const value = Reflect.get(target, key);
      // 若属性值是对象,递归代理(懒代理)
      if (isObject(value)) {
        return reactive(value); 
      }
      // 依赖收集(简化版)
      track(target, key);
      return value;
    },
    // ...set等其他拦截器
  });
}

二、缓存机制:避免重复代理

对同一个对象多次调用 reactive 时,返回同一个代理对象(而非创建新 Proxy),避免重复代理导致的内存浪费和逻辑混乱。

  • 优化点: 用 WeakMap 缓存 “原始对象 → 代理对象” 的映射,既保证缓存复用,又不会阻止原始对象被垃圾回收(WeakMap 的键是弱引用)。
  • 实现逻辑
const reactiveMap = new WeakMap(); // 缓存:原始对象 → 代理对象

function reactive(target) {
  // 若已代理过,直接返回缓存的代理对象
  const existingProxy = reactiveMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  // 否则创建新代理并缓存
  const proxy = new Proxy(target, handler);
  reactiveMap.set(target, proxy);
  return proxy;
}

三、精准拦截:只处理必要的操作

Proxy 的拦截器(getsetdeleteProperty 等)默认会拦截所有属性操作,但框架会通过过滤逻辑,只对 “有意义的操作” 进行拦截处理,减少无效计算。

  • 优化点:
    • 跳过对 Symbol 内置属性(如 Symbol.iterator)的拦截,避免干扰原生对象行为(如数组迭代)。
    • 跳过对不可配置、不可写属性的无意义拦截(如 Object.freeze 冻结的对象)。
    • 对数组的索引操作(如 arr[0] = 1)和原型方法(如 pushsplice)做特殊处理,避免全量拦截导致的性能损耗。

四、区分响应式类型:减少不必要的代理范围

Vue3 提供了 reactive(深层响应)、shallowReactive(浅层响应)、readonly(只读响应)等 API,通过不同的拦截器逻辑,精准控制响应式的范围:

  • shallowReactive:只代理顶层属性,不递归代理子对象,适合已知不会修改深层属性的场景(如配置对象),减少代理成本。
  • readonly:拦截 set 操作时直接报错(禁止修改),且不触发依赖更新,适合纯展示数据,避免无用的依赖追踪。
  • ref 对基本类型的优化:对 numberstring 等基本类型,用 { value: ... } 包装后再代理,既兼容 Proxy(只能代理对象),又减少对原始值的不必要处理。

五、依赖追踪的精准化

响应式的核心是 “访问时收集依赖,修改时触发更新”。Proxy 配合 effect 系统实现了更精准的依赖追踪:

  • 只收集实际访问的属性: 例如访问 obj.a.b 时,只会收集 ab 的依赖,而不是整个 obj,修改 obj.c 时不会触发无关更新。
  • 避免重复收集依赖: 同一 effect 多次访问同一属性,只会记录一次依赖,减少依赖表的冗余。

六、跳过非响应式值的代理

对非对象类型(如基本类型、nullundefined)、Symbol、函数等,直接返回原始值,不创建 Proxy,避免无效操作:

function reactive(target) {
  // 非对象类型直接返回,不代理
  if (!isObject(target)) {
    return target;
  }
  // ...后续代理逻辑
}

七、数组优化:高效拦截数组方法

数组的 pushpopsplice 等方法会修改数组本身,Vue3 对这些方法做了特殊处理:

  • 拦截并改写数组方法:在 get 拦截器中,当访问数组的原型方法时,返回一个 “被包装的方法”,该方法执行时会先触发原始操作,再通知依赖更新。
  • 避免索引遍历的性能损耗:相比 Vue2 对数组索引的逐个拦截,Proxy 可以直接拦截数组方法,更高效地处理批量修改(如 arr.push(1,2,3))。

总结

Proxy 实现响应式的优化核心是 “按需处理” 和 “精准控制”

  • 通过懒代理、缓存减少初始化成本;
  • 通过区分响应式类型、过滤无效操作缩小代理范围;
  • 通过精准的依赖追踪减少更新时的无效触发。

这些优化让 Proxy 相比 Vue2 的 Object.defineProperty 在性能(尤其是复杂对象)和灵活性上有了质的提升,也让响应式系统更贴合实际开发中的场景需求。

相关内容

写下自己求职记录也给正在求职得一些建议-CSDN博客

从初中级如何迈入中高级-其实技术只是“入门卷”-CSDN博客

前端梳理体系从常问问题去完善-基础篇(html,css,js,ts)-CSDN博客

前端梳理体系从常问问题去完善-工程篇(webpack,vite)_前端系统梳理-CSDN博客

前端梳理体系从常问问题去完善-框架篇(react生态)-CSDN博客

面试官爱问的 Object.defineProperty,90%的人倒在这些细节上!

我们都知道Vue2中的响应式原理使用了Object.defineProperty, 那么你知道Object.defineProperty的一些使用细节吗!在面试过程中遇到这些细节考察,你是否可以轻松面对呢!今天我们就来探索一下Object.defineProperty使用的4个细节点。

一、 Object.defineProperty定义对象的默认行为

面试官:请说出下面代码的输出结果。

function test1() {
  var a = {};
  var obj = Object.defineProperty(a, "b", {
    value: 1,
  });
  return obj;
}

var obj1 = test1();
console.log("结果1:", obj1);
obj1.b = 2;
console.log("结果2:", obj1);
obj1.c = 3;
for (var key in obj1) {
  console.log("结果3:", obj1[key]);
}
delete obj1.b;
console.log("结果4:", obj1);

答案如下:

image.png

对于结果1相信大家都觉得正常,不用思考就能写出答案,但是结果2、3、4,就需要你对Object.defineProperty的默认行为有所掌握了。结果2、3、4证明了Object.defineProperty 的3个默认行为:

  • Object.defineProperty 定义的属性默认不可以修改
  • Object.defineProperty 定义的属性默认不可枚举
  • Object.defineProperty 定义的属性默认不能删除

那么如何才能将Object.defineProperty 定义的属性变成可以修改,可以枚举和可以删除的呢?这时需要在定义时添加 writable,enumerable, configurable 3个配置。具体使用如下:

function test1() {
  var a = {};
  var obj = Object.defineProperty(a, "b", {
    value: 1,
    writable: true, // 定义是否可写,也就是否可以修改
    enumerable: true, // 定义是否可以枚举
    configurable: true, // 定义是否可以删除
  });
  return obj;
}

var obj1 = test1();
console.log("结果1:", obj1);
obj1.b = 2;
console.log("结果2:", obj1);
obj1.c = 3;
for (var key in obj1) {
  console.log("结果3:", obj1[key]);
}
delete obj1.b;
console.log("结果4:", obj1);

输出结果如下:

image.png

二、Object.defineProperty 中getter setter方法的使用

面试官:请用Object.defineProperty 写一个简单的响应式

分析: 其实这主要就是考察你是否知道Object.defineProperty中的getter setter方法的用法, 实现方式如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>响应简单实现</title>
  </head>
  <body>
    <div>
      <button id="btn">加一</button>
      <p>result:<span id="result">0</span></p>
    </div>
    <script>
      function reactive() {
        var obj = {};
        var result = 0;
        var obj2 = Object.defineProperty(obj, "result", {
          get: function (value) {
            return result;
          },
          set: function (newVal) {
            console.log(newVal);
            result++;
            document.getElementById("result").innerHTML = newVal;
          },
        });

        return obj;
      }
      var obj = reactive();
      document.getElementById("btn").onclick = function () {
        obj.result++;
      };
    </script>
  </body>
</html>

当点击按钮加一的时候,你会看到页面的result 更新来了,而在点击事件里面我们并没有去操作dom,这其实就是Vue2实现响应式的底层核心原理,只是Vue2中封装得更完善,可配置性更高。

三、Object.defineProperty 中, value 和getter setter 方法的互斥性

面试官: 请问下面代码会输出什么

function test() {
  var obj = {};
  Object.defineProperty(obj, "b", {
    value: 1,
    get: function () {
      return 5;
    },
    set: function (newVal) {},
  });
  return obj;
}

var newObj = test();
console.log(newObj);

答案是什么也不会输出,会直接报错:

image.png

这里其实不只是get setter 和value 不能共存,getter setter 和writable, enumerable 也不能共存,只有getter setter 和getter setter 一起的时候不会报错

四、 Object.defineProperties

面试官: 请问Object.defineProperty 可以同时定义多个属性吗?可以的话怎么定义,不可以的话有其他解决方案吗?

答案是Object.defineProperty不可以同时定义多个属性,可以使用Object.defineProperties 来解决。具体使用方式如下:

function test() {
  var obj = {};
  Object.defineProperties(obj, {
    a: {
      value: 1,
      writable: true,
      enumerable: true,
      configurable: true,
    },
    b: {
      value: 2,
    },
  });
  return obj;
}

var newObj = test();
console.log(newObj);

总结

本篇通过4个面试题来加深对Object.defineProperty 的理解,希望看了本篇遇到类似的面试题能够顺利通过,,感谢收看

TypeScript 和 JavaScript 的 'use strict' 有啥不同

都叫严格模式,但它们解决的问题完全不在一个层次上

前言

写完 JavaScript 严格模式的文章,突然想到一个问题:"TypeScript 不也有个 strict: true 吗?这俩是一回事吗?开了 TS 的 strict 还要写 'use strict' 吗?"

说实话,我刚学 TypeScript 时也搞混过。看着 tsconfig.json 里的 strict: true,心想这应该和 JS 的 'use strict' 差不多吧,结果配完发现代码里还是满屏标红。

后来花了个周末把 TypeScript 编译选项挨个试了一遍,才明白:这俩虽然名字像,但压根不是一个维度的东西——一个管编译时的类型检查,一个管运行时的语言行为。

先抛几个问题,看看你是不是也有同样的困惑:

  • TypeScript 的 strict 和 JavaScript 的 'use strict' 到底啥区别?
  • 开了 TS 的 strict 模式,还需要写 'use strict' 吗?
  • 它们检查的东西一样吗?(答案是完全不一样)
  • 为啥名字这么像,却是两个东西?(这锅 TypeScript 团队真得背)

这篇文章就来聊聊,同样是"严格",它们到底严在哪里,又有什么本质区别。


目录


一个真实的困惑:我到底该开哪个?

先看一个常见场景。你在写 TypeScript 项目,tsconfig.json 里配了:

{
  "compilerOptions": {
    "strict": true
  }
}

然后在代码里写:

function greet(name) {  // TS 报错:Parameter 'name' implicitly has an 'any' type
  console.log('Hello ' + name);
}

TypeScript 立马给你标红了。你想:行,TypeScript 的严格模式生效了

但是,这时候你在文件顶部加不加 'use strict',会有区别吗?

或者反过来,如果你只写了 'use strict',没开 TypeScript 的 strict: true,又会怎样?

这就是今天要搞清楚的问题


JavaScript 严格模式回顾:运行时的守护者

先快速回顾一下 JavaScript 的严格模式(详细内容可以看上一篇文章)

它是什么?

一个运行时开关,在代码执行时改变 JavaScript 引擎的行为。

'use strict';  // 告诉 JS 引擎:"用严格模式跑这段代码"

x = 10;  // ReferenceError: x is not defined(运行时报错)

它解决什么?

JavaScript 早期设计的语言层面的问题

  • 运行时错误:把静默失败变成抛出异常
  • 危险语法:禁止容易出错的语法(比如 with、八进制字面量)
  • 意外行为:修正反直觉的行为(比如自动创建全局变量)

关键特征

mindmap
  root((JavaScript<br/>严格模式))
    运行时生效
      代码执行时检查
      依赖 JS 引擎
      无法在编译时发现问题
    语言层面
      修改语言行为
      禁止危险语法
      修正历史问题
    向后兼容
      老代码不受影响
      需要主动开启
      只影响声明的作用域

TypeScript 严格模式:编译时的守护者

TypeScript 的 strict: true 是另一个完全不同的东西。

它是什么?

一个编译选项集合,在代码编译(转换为 JS)之前进行类型检查

// tsconfig.json
{
  "compilerOptions": {
    "strict": true  // 这是个"总开关"
  }
}

当你开启 strict: true,实际上是同时开启了这 7 个编译选项:

{
  "compilerOptions": {
    "strict": true,  // 👆 等价于下面 👇

    "noImplicitAny": true,               // 禁止隐式 any 类型
    "noImplicitThis": true,              // 禁止 this 有隐式 any 类型
    "strictNullChecks": true,            // 严格的 null/undefined 检查
    "strictFunctionTypes": true,         // 严格的函数类型检查
    "strictBindCallApply": true,         // 严格检查 bind/call/apply
    "strictPropertyInitialization": true,// 严格的类属性初始化检查
    "alwaysStrict": true,                // 始终以严格模式解析(会加 'use strict')
    "useUnknownInCatchVariables": true   // catch 变量默认为 unknown 类型
  }
}

等等,看到 alwaysStrict 了吗?这就是联系的地方!

它解决什么?

TypeScript 的严格模式解决的是类型安全问题

  • 编译时错误:在代码运行前就发现类型错误
  • 类型推断:强制明确类型,避免隐式 any
  • 空值安全:防止 null/undefined 引起的运行时错误
  • 函数安全:确保函数调用的类型正确性

关键特征

mindmap
  root((TypeScript<br/>严格模式))
    编译时生效
      转译前检查
      IDE 实时提示
      运行前发现问题
    类型系统层面
      强制类型明确
      空值安全检查
      函数类型检查
    配置灵活
      总开关
      可单独开关每个选项
          逐步迁移友好

核心差异:编译时 vs 运行时

现在重点来了,这两者的本质区别

1. 生效时机不同

flowchart LR
    A[编写代码] --> B[TypeScript 编译]
    B --> C[生成 JavaScript]
    C --> D[浏览器/Node.js 执行]

    B -.->|TypeScript strict| E[编译时检查]
    D -.->|JavaScript 'use strict'| F[运行时检查]

    style E fill:#e1f5ff
    style F fill:#fff4e1

TypeScript strict: true

  • ✅ 在编译阶段检查(你还在写代码的时候)
  • ✅ IDE 实时提示,根本不让你编译通过
  • ✅ 问题在开发阶段就被发现

JavaScript 'use strict'

  • ✅ 在运行阶段检查(代码已经在跑了)
  • ✅ 只有执行到那行代码才会报错
  • ✅ 问题可能在生产环境才暴露

2. 检查内容不同

检查项 TypeScript strict JavaScript 'use strict'
未声明变量 ❌ 不检查(这是 JS 运行时的事) ✅ 运行时报错
隐式 any 类型 ✅ 编译错误 ❌ 不检查(JS 没有类型)
null/undefined 安全 ✅ 编译错误 ❌ 不检查(运行时才知道是否为 null)
函数参数类型 ✅ 编译错误 ❌ 不检查
只读属性赋值 ✅ 编译错误(如果用了 readonly ✅ 运行时报错
重复参数名 ✅ 编译错误 ✅ 运行时报错
八进制字面量 ✅ 编译错误 ✅ 运行时报错
with 语句 ✅ 编译错误 ✅ 运行时报错
this 为 undefined ✅ 类型检查会提示 ✅ 运行时行为改变

3. 适用范围不同

TypeScript strict

  • 只在 .ts.tsx 文件中生效
  • 需要 TypeScript 编译器
  • 编译后的 JS 文件里没有类型信息

JavaScript 'use strict'

  • 在所有 JS 文件中都能用(.js.ts 编译后的文件)
  • 不需要任何工具,浏览器原生支持
  • 直接影响 JS 引擎的行为

深入对比:它们分别解决什么问题?

案例 1:未声明的变量

JavaScript 'use strict' 能捕获

'use strict';

function test() {
  myVar = 10;  // ❌ ReferenceError: myVar is not defined(运行时)
}

test();

TypeScript strict 不检查这个

// tsconfig.json: { "strict": true }

function test() {
  myVar = 10;  // ⚠️ TypeScript: Cannot find name 'myVar'
               // 但这是因为 TypeScript 要求先声明变量
               // 不是因为 strict 模式
}

TypeScript 编译后:

"use strict";  // 👈 注意这里!因为 alwaysStrict: true

function test() {
  myVar = 10;  // 运行时还是会被 'use strict' 捕获
}

结论

  • TS 的 strict 本身不处理未声明变量
  • strict 包含 alwaysStrict,会自动加 'use strict'
  • 最终还是靠 JS 的严格模式在运行时捕获

案例 2:隐式 any 类型

TypeScript strict 能捕获

// strict: true

function greet(name) {
  // ❌ 编译错误:Parameter 'name' implicitly has an 'any' type
  console.log('Hello ' + name);
}

JavaScript 'use strict' 完全不管

'use strict';

function greet(name) {
  // ✅ 没问题,JS 本来就是动态类型
  console.log('Hello ' + name);
}

结论

  • TS 的 strict 强制你明确类型
  • JS 的 'use strict' 对类型无能为力(因为 JS 没有静态类型)

案例 3:空值安全

TypeScript strict 的强项

// strict: true(包含 strictNullChecks)

function getLength(str: string) {
  return str.length;
}

const maybeStr: string | null = getSomeString();

getLength(maybeStr);
// 编译错误:Argument of type 'string | null' is not assignable to parameter of type 'string'

JavaScript 'use strict' 无能为力

'use strict';

function getLength(str) {
  return str.length;
}

const maybeStr = getSomeString();

getLength(maybeStr);
// 编译通过
// 运行时如果 maybeStr 是 null,会报错:Cannot read property 'length' of null

结论

  • TS 的 strict 在编译时就发现了潜在的 null 引用问题
  • JS 的 'use strict' 只能等到运行时才崩溃

案例 4:函数 this 类型

两者都有帮助,但方式不同

// TypeScript strict
interface User {
  name: string;
  greet(this: User): void;  // 明确 this 类型
}

const user: User = {
  name: 'Alice',
  greet() {
    console.log(this.name);
  }
};

const greetFn = user.greet;
greetFn();
// ❌ TS 编译错误:The 'this' context of type 'void' is not assignable to method's 'this' of type 'User'
// JavaScript 'use strict'
'use strict';

const user = {
  name: 'Alice',
  greet() {
    console.log(this.name);  // this 是 undefined
  }
};

const greetFn = user.greet;
greetFn();
// ✅ 编译通过
// 运行时报错:Cannot read property 'name' of undefined

结论

  • TS 的 strict 通过类型系统在编译时就警告你
  • JS 的 'use strict'thisundefined,在运行时才报错

实战案例:看看它们如何配合工作

完整示例:两者互补

// tsconfig.json
{
  "compilerOptions": {
    "strict": true  // 包含 alwaysStrict: true
  }
}
// user.ts

// 1️⃣ TypeScript 的 strict 检查类型
function calculateTotal(price: number, quantity: number): number {
  // 2️⃣ TypeScript 确保参数类型正确
  if (price < 0) {
    // 3️⃣ strictNullChecks 确保不返回 undefined
    throw new Error('Price cannot be negative');
  }

  return price * quantity;
}

// 4️⃣ 编译时就发现类型错误
// calculateTotal('100', 5);  // ❌ 编译错误

// 5️⃣ 如果不小心写了未声明变量
function buggyCode() {
  totol = 100;  // ❌ TS: Cannot find name 'totol'
}

编译后的 JavaScript:

// user.js
"use strict";  // 👈 自动加上!来自 alwaysStrict: true

// TypeScript 的类型检查已经完成,这里只剩运行时代码
function calculateTotal(price, quantity) {
  if (price < 0) {
    throw new Error('Price cannot be negative');
  }
  return price * quantity;
}

// 如果 TypeScript 没拦住(比如用了 any),运行时会拦住
function buggyCode() {
  totol = 100;  // 💥 ReferenceError(被 'use strict' 捕获)
}

双重保险

  1. 第一层(编译时) :TypeScript 的 strict 检查类型、空值、函数签名
  2. 第二层(运行时) :JavaScript 的 'use strict' 检查语言层面的问题

深入理解:为什么需要两者?

JavaScript 严格模式的局限

'use strict' 再严格,也只是让错误暴露得早一点,但:

  • ❌ 不能阻止类型错误(比如把字符串传给期望数字的函数)
  • ❌ 不能保证空值安全(比如访问 null 的属性)
  • ❌ 不能检查函数签名(比如参数数量、类型)

TypeScript 严格模式的局限

strict: true 再强大,也只在编译时有效,但:

  • ❌ 不能处理动态引入的第三方库(没有类型定义的)
  • ❌ 不能检查运行时的值(比如从 API 返回的数据)
  • ❌ 如果用了 any 或类型断言,类型检查就被绕过了

两者互补

graph LR
    A[开发阶段] --> B[TypeScript strict<br/>类型检查]
    B --> C[编译]
    C --> D[运行阶段]
    D --> E[JavaScript 'use strict'<br/>语言规则检查]

    B -.-> F[捕获类型错误<br/>空值引用<br/>函数签名问题]
    E -.-> G[捕获未声明变量<br/>静默失败<br/>危险语法]

    style B fill:#e1f5ff
    style E fill:#fff4e1

最佳组合

  • TypeScript strict: true:在开发时就把大部分问题拦住
  • JavaScript 'use strict' (自动加上):作为最后一道防线,拦住 TypeScript 也管不了的运行时问题

✅ 最佳实践:该怎么配置?

1. 新 TypeScript 项目:两个都要

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,  // 👈 已经包含 alwaysStrict: true
    "target": "ES2020",
    "module": "ESNext"
  }
}

这样配置后:

  • ✅ TypeScript 会做编译时检查
  • ✅ 自动为每个文件加上 'use strict'
  • 你不需要手写 'use strict'

2. 老 TypeScript 项目:逐步迁移

如果直接开 strict: true 会导致满屏报错,可以单独开启

{
  "compilerOptions": {
    "strict": false,  // 先不开总开关

    // 逐步开启单个选项
    "noImplicitAny": true,            // 第一步:禁止隐式 any
    "alwaysStrict": true,             // 第二步:加 'use strict'
    "strictNullChecks": true,         // 第三步:空值检查
    // ... 逐步开启其他选项
  }
}

3. 纯 JavaScript 项目:只能用 'use strict'

如果你不用 TypeScript,那就只能用 JavaScript 的严格模式:

// 方式 1:全局开启(文件顶部)
'use strict';

// 你的代码...
// 方式 2:函数级别开启
function myFunction() {
  'use strict';
  // 只在这个函数内严格
}

推荐:配合 ESLint 强制添加:

// .eslintrc.js
module.exports = {
  rules: {
    'strict': ['error', 'global']
  }
};

4. 配合 ESLint/Prettier

TypeScript 的 strict 模式专注类型检查,但代码质量还需要 ESLint:

// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking"
  ],
  "parserOptions": {
    "project": "./tsconfig.json"
  }
}

这样你会得到:

  • TypeScript strict:类型安全
  • ESLint:代码质量、最佳实践
  • Prettier:代码格式

对比总结表

维度 TypeScript strict: true JavaScript 'use strict'
本质 编译选项集合 运行时指令
生效时机 编译时(写代码时) 运行时(代码执行时)
检查内容 类型、空值、函数签名 语言规则、危险语法
错误提示 IDE 实时提示、编译失败 运行时抛出异常
依赖 TypeScript 编译器 JavaScript 引擎
适用文件 .ts.tsx 所有 .js 文件
性能影响 无(编译时) 微小(运行时)
向后兼容 需要 TypeScript 所有现代浏览器
配置方式 tsconfig.json 代码中写 'use strict'
关联关系 alwaysStrict 会自动加 'use strict' 无关 TypeScript
最佳实践 新项目必开 TS 项目自动加上,JS 项目手动加

常见误区澄清

误区 1:"开了 TypeScript strict 就不需要 'use strict' 了"

错误

虽然 strict: true 包含 alwaysStrict: true(会自动加 'use strict'),但:

  • TypeScript 只检查编译时的类型问题
  • 'use strict' 检查运行时的语言问题

正确理解:开了 strict: true 后,编译出的 JS 会自动带 'use strict',所以你不用手写。


误区 2:"'use strict' 能替代 TypeScript"

错误

'use strict' 再严格,也不能做类型检查。比如:

'use strict';

function add(a, b) {
  return a + b;
}

add('1', 2);  // ✅ 运行通过,结果是 '12'(字符串拼接)

TypeScript 会在编译时就发现类型问题:

function add(a: number, b: number) {
  return a + b;
}

add('1', 2);  // ❌ 编译错误:Argument of type 'string' is not assignable to parameter of type 'number'

误区 3:"strict: true 太严格了,影响开发效率"

错误(短期看似如此,长期受益)

刚开始确实会遇到很多类型错误,但:

  • 这些错误本来就存在,只是以前被隐藏了
  • 在编译时发现远比在生产环境崩溃要好
  • 类型提示会让重构和协作更安全

建议:新项目直接开 strict: true,老项目逐步迁移。


写在最后

研究完这两个"严格模式",我的理解是:

它们的关系

  • TypeScript strict:编译时的守护者,拦截类型错误、空值引用、函数签名问题
  • JavaScript 'use strict' :运行时的守护者,拦截语言层面的危险语法和意外行为
  • 它们不是替代关系,而是互补关系

为什么要两者都用

  • TypeScript 再强大,也只在编译时有效
  • 编译后的 JS 代码,依然需要 'use strict' 在运行时提供保护
  • strict: true 里的 alwaysStrict 会自动加上 'use strict',所以你只需要配置 TypeScript,不用手写

使用建议

  1. TypeScript 项目:开启 strict: true(已包含 alwaysStrict
  2. 纯 JavaScript 项目:手动加 'use strict',配合 ESLint 强制
  3. 不要因为名字相似就混淆它们:一个管编译时类型,一个管运行时语言规则

下次有人问你"TypeScript 的 strict 和 JavaScript 的 'use strict' 有啥区别",你可以自信地说:

一个在编译时保护你的类型安全,一个在运行时保护你的代码行为。名字像,但完全不是一回事!

TypeScript 官方文档

  1. Compiler Options: strict - TypeScript 严格模式官方说明
  2. TSConfig Reference - 完整的编译选项参考

JavaScript 官方规范

  1. ECMAScript Strict Mode - 严格模式的官方定义
  2. MDN - Strict mode - 最全面的严格模式文档

相关文档

  1. TypeScript Deep Dive: Strict - 深入理解 TypeScript 严格性
  2. JavaScript: The Good Parts - Douglas Crockford 讲解严格模式的设计哲学

🧩 Next.js在国内环境的登录机制设计:科学、务实、又带点“国风味”的安全艺术

一、引子:登录不是门锁,是生态的一环

我们总以为登录就是“输个手机号 → 输入验证码 → 登入成功”。
但在中国互联网的真实环境下,事情往往更微妙:

  • 网络波动像灵气未稳;
  • 第三方登录接口可能“偶尔放假”;
  • 用户手机号段五花八门;
  • 审计、安全、隐私法规一刻也不敢怠慢。

于是,登录机制设计在国内环境下就变成了一门“底层工程学 + 心理学 + 社会学的交叉艺术”。

今天,让我们用Next.js——这个被誉为“React时代的后端浪子”——来设计出一套既合理安全接地气的登录机制。


二、Next.js的登录问题,本质是“边界的艺术”

Next.js是一个“前后端同体”的混血框架。
它具备:

  • 前端渲染(SSR / SSG);
  • API 路由(内置轻量后端);
  • 中间层(Middleware);
  • Server Actions(从React 19时代引入的灵魂特性)。

在登录体系设计中,它扮演的是“守门员 + 信使 + 数据搬运工”三合一角色。
然而,在国内环境下,我们还得考虑几件“独特的世俗问题”:

  1. 验证码机制(手机 or 图片)
  2. 第三方登录(微信、钉钉、支付宝)接口不稳定
  3. 分布式 Session 存储策略
  4. 防爬虫与数据合规审计
  5. 轻量服务应对高并发环境下的速率控制

接下来,我们从底层原理逐步构筑起这座“国风登录系统”。


三、第一步:验证体系 = “身份 + 证明 + 信任链”

1. 传统ID密码?那已经是上古神器了

在移动优先的中国互联网,手机号验证码登录几乎成为事实标准。
逻辑上,这相当于组合三层保障:

  • 身份标识:手机号是唯一ID。
  • 短期令牌:验证码代表一次性授权。
  • 信任链闭环:签发JWT(或Session)完成登录。

2. 从底层看验证码机制

验证码不仅仅是UI上的一个文本框跳舞,它体现的是一种人机对抗的哲学。
系统必须在容忍误差与防止滥用之间找到平衡。

在Next.js中,你可以这么写一个简化版手机验证码接口:

// /app/api/send-sms/route.js
import { NextResponse } from 'next/server'

const CODE_CACHE = new Map(); // 生产环境请用Redis

export async function POST(request) {
  const { phone } = await request.json();

  if (!/^1\d{10}$/.test(phone)) {
    return NextResponse.json({ success: false, message: "手机号不合法" });
  }

  const code = Math.floor(100000 + Math.random() * 900000).toString();
  CODE_CACHE.set(phone, { code, expires: Date.now() + 3 * 60 * 1000 });

  console.log(`🎯 [DEBUG] 验证码 ${code} 已发送至 ${phone}`); // 真实系统应调用短信网关

  return NextResponse.json({ success: true, message: "验证码已发送" });
}

这个“看似简单”的API,其实暴露了一个国内环境特有的挑战:
短信接口限流 + 延迟 + 成本控制。

所以成熟系统往往会:

  • 在Redis中缓存发送频率;
  • 对单手机号、IP、设备指纹打速率标签;
  • 与短信服务商采用多路策略(主备通道切换)。

四、第二步:会话管理——登录不是一刻钟的浪漫,而是一段持久的关系

登录成功后,我们需要为用户建立一段可验证又可撤销的“关系”。
这时你有两种主要路径:

1. JWT 无状态方案(适合无后端集群依赖)

在Next.js中,我们可以直接使用Edge安全上下文,生成轻量JWT:

import jwt from "jsonwebtoken";

const SECRET = process.env.JWT_SECRET || "local_dev_secret";

export function createToken(payload) {
  return jwt.sign({ ...payload, ts: Date.now() }, SECRET, { expiresIn: "2h" });
}

export function verifyToken(token) {
  try {
    return jwt.verify(token, SECRET);
  } catch {
    return null;
  }
}

JWT的好处是适合Serverless部署,不依赖集中状态,天然适合Next.js的边缘部署。
但问题在于:

一旦签发出去,想让它立即失效?你得靠“黑名单表”配合。

2. Session有状态方案(更“国内体质”)

很多政企和电商项目仍偏好传统Session方式,因为:

  • 可直接支持服务端注销;
  • 审计系统易接入;
  • 与老式Nginx转发、负载均衡兼容性高。

在Next.js中,可以借助中间件实现Session控制:

// /middleware.js
import { NextResponse } from 'next/server'

export function middleware(req) {
  const token = req.cookies.get('session_id')?.value
  
  if (!token && req.nextUrl.pathname.startsWith('/dashboard')) {
    const loginUrl = new URL('/login', req.url)
    return NextResponse.redirect(loginUrl)
  }

  return NextResponse.next()
}

这就是Next.js的“边缘守卫”。
它像古代城门守卫一样:检查每个请求,有证件才能过。


五、第三步:微信 & 第三方登录的“波动适配哲学”

国内环境里,第三方授权是个“玄学系统”:

  • 微信API早上活着,下午超时;
  • 企业微信返回码有时像谜语;
  • 支付宝登录跳转后Param缺失。

所以我们要做两件事:

  1. 所有第三方接口封装Promise容错机制
  2. 务必在服务端验证回调,以防Token伪造
// /app/api/oauth/wechat/route.js
export async function GET(req) {
  const code = req.nextUrl.searchParams.get("code");
  if (!code) return new Response("缺少code", { status: 400 });

  try {
    // 调用微信OAuth接口
    const tokenRes = await fetch(`https://api.weixin.qq.com/sns/oauth2/access_token?...&code=${code}`);
    const data = await tokenRes.json();

    if (data.openid) {
      // 在数据库中查找或创建用户
      return Response.redirect("/dashboard");
    } else {
      return new Response(`微信登录失败:${data.errmsg}`, { status: 400 });
    }
  } catch (e) {
    console.error("微信登录异常:", e);
    return new Response("接口波动,请重试", { status: 500 });
  }
}

技术之外的启示:
容错与弹性是对现实的尊重,不是对Bug的纵容。


六、第四步:中间件与Server Action的哲学融合

React 19之后的Next.js新特性——Server Actions,让登录验证像写后端函数那样自然。
不过在国内环境部署时,要注意:

  • 云函数平台(如阿里云、腾讯云)对冷启动敏感;
  • 动态环境变量管理要合规(敏感秘钥隔离)。

于是更推荐一种架构心法:

轻业务在Server Action实现,核心逻辑独立部署为Service层

让Next.js像“外交接口”,真正的逻辑在后方有成熟的守备。


七、小结:设计之道 = “稳定为体,体验为魂”

登录系统绝不是UI表单,它是整个安全体系的第一关。
在国内环境下,Next.js的登录机制设计要遵循以下三条底层哲学:

  1. 一切状态,皆可被追踪(Session存储与日志审计)
  2. 一切波动,皆有缓冲层(短信、OAuth接口容错)
  3. 一切验证,终归边缘(Middleware + Server Action 策略)

八、尾声:在风起的网络中安放我们的“登录”

Web的本质,是信任的延伸
登录这件事,看似只是用户按下“确认”,
但在幕后,是你与服务器、网络、数据、法律之间的一场无声博弈。

而Next.js在这场博弈中,像一位兼具浪漫与理性的桥梁工人。
它用其SSR的柔性、API的轻盈、Middleware的精准,
在混乱的现实网络中,劈出一条干净的逻辑之路。


☕ 一句程序员文学

登录其实像恋爱——如果一开始验证太繁琐,对方就跑了;
但要是太随意,迟早有人冒充“真爱”。

Electron基本概念

Electron 是内部集成了两个运行环境: Nodejs 环境,称为主进程(Main Process) Chromium 环境,称为渲染器进程(Renderer Process) 可以理解成在主进程

在 Nestjs 中使用 Drizzle ORM

依赖安装

pnpm add drizzle-orm pg dotenv
pnpm add -D drizzle-kit tsx @types/pg

准备 DrizzleModule

这里我们都在 /src/drizzle 目录中操作

创建 drizzle.provider.ts 文件

import { Provider } from '@nestjs/common';
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';

import { ConfigService } from '@/config/config.service';

import { Schema, schema } from './schema';

export const PG_CONNECTION = 'PG_CONNECTION';

export const DrizzleProvider: Provider = {
    provide: PG_CONNECTION,
    inject: [ConfigService],
    useFactory(configService: ConfigService) {
        const connectionString = configService.database.DATABASE_URL;
        const pool = new Pool({ connectionString });
        return drizzle(pool, { schema, logger: true }) as NodePgDatabase<Schema>;
    },
};

创建 drizzle.service.ts 文件

有了这个以后,我们可以在后续的模块中直接注入该模块 constructor(private readonly drizzle: DrizzleService) {},而不是每次都要写 constructor(@Inject(PG_CONNECTION) readonly db: NodePgDatabase<Schema>) {} 这么一长串内容

import { Inject, Injectable } from '@nestjs/common';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';

import type { Schema } from './schema';

import { PG_CONNECTION } from './drizzle.provider';

@Injectable()
export class DrizzleService {
    constructor(@Inject(PG_CONNECTION) readonly db: NodePgDatabase<Schema>) {}
}

创建 drizzle.module.ts 文件

import { Global, Module } from '@nestjs/common';

import { DrizzleProvider } from './drizzle.provider';
import { DrizzleService } from './drizzle.service';

@Global()
@Module({
    imports: [],
    providers: [DrizzleService, DrizzleProvider],
    exports: [DrizzleService],
})
export class DrizzleModule {}

创建数据库 Schema

创建一个 user.entity.ts

import { relations } from 'drizzle-orm';
import { pgTable, serial, timestamp, varchar } from 'drizzle-orm/pg-core';
import { createSelectSchema } from 'drizzle-zod';
import { z } from 'zod/v4';

export const timestamps = {
    createdAt: timestamp('created_at').notNull().defaultNow(),
    updatedAt: timestamp('updated_at').notNull().defaultNow(),
};

export const user = pgTable('user', {
    id: serial().primaryKey(),
    username: varchar().notNull().unique(),
    password: varchar().notNull(),
    ...timestamps,
});

export const selectUserSchema = createSelectSchema(user);
export type SelectUser = z.infer<typeof selectUserSchema>;

创建 schema.ts 文件

import { user } from './user.entity';

export type { SelectUser } from './user.entity';

export const schema = {
    user
}

export type Schema = typeof schema;
export type SchemaName = keyof Schema;

这样我们的 DrizzleModule 就准备好了

drizzle 相关的配置

我们要在项目根目录下创建 drizzle.config.ts 文件,这个文件是 DrizzleOrm 的配置文件

import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
    schema: './src/drizzle/schema/**.entity.ts', // 这里是数据库 schema 文件的位置
    out: './drizzle/migrations', // 数据库迁移文件生成的地址
    dialect: 'postgresql', // 数据库驱动
    dbCredentials: {
        url: process.env.DATABASE_URL!,
    },
});

package.json 中的脚本

{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:push": "drizzle-kit push",
    "db:studio": "drizzle-kit studio",
  },
}
  • db:generate 声明时或后续 Schema 更改时基于 Drizzle Schema 生成 SQL 迁移
  • db:migrate 运行迁移后,Drizzle Kit 会将成功应用的迁移记录保存到数据库中。
  • db:push 允许您直接将您的架构和后续架构更改推送到数据库
  • db:studio 本地数据库可视化面板

生成 schema 并将改动同步到数据库

pnpm run db:generate
pnpm run db:migrate

成功后我们可以使用 pnpm run db:studio 查看数据库中的内容。

在 app.module.ts 中引入 DrizzleModule

@Module({
    imports: [
    // ...
        DrizzleModule,
    ],
})
export class AppModule {}

在模块中使用,这里用 UserModule 作为例子

@Injectable()
export class UserService {
    constructor(private readonly drizzle: DrizzleService) {}

    async create(createUserDto: CreateUserDto) {
        const result = await this.drizzle.db.insert(schema.user).values(createUserDto).returning();

        return result;
    }

    async findOne(id: number) {
        const [user] = await this.drizzle.db.select().from(schema.user).where(eq(schema.user.id, id));
        return user;
    }

    async update(id: number, updateUserDto: UpdateUserDto) {
        const user = this.drizzle.db
            .update(schema.user)
            .set(updateUserDto)
            .where(eq(schema.user.id, id));
        return user;
    }

    async remove(id: number) {
        const [user] = await this.drizzle.db
            .delete(schema.user)
            .where(eq(schema.user.id, id))
            .returning();
        return user;
    }
}

附加内容

vscode 插件推荐

vscode-drizzle-orm 这个插件可以让我们看到 drizzle 生成好的数据库模型图

数据库填充

在开发中我们通常都要填充一些模拟数据用来测试,那么在 nestjs + drizzle 这个组合下我们如何操作呢?

我们还是在 src/drizzle 文件夹中操作

创建 db.ts

import 'dotenv/config';
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';

import { Schema, schema } from './schema';

const connectionString = process.env.DATABASE_URL;

if (!connectionString) {
    throw new Error('DATABASE_URL is not defined');
}

const pool = new Pool({ connectionString });
export const db: NodePgDatabase<Schema> = drizzle(pool, { schema, logger: true });

export type db = NodePgDatabase<Schema>;

创建 seed 相关文件

import 'dotenv/config';
import { Table } from 'drizzle-orm';

import { db } from './db';
import { schema } from './schema';
import { seedUser } from './seeds/user.seed';

// 清空数据库中的数据
async function clearTable() {
    // eslint-disable-next-line drizzle/enforce-delete-with-where
    return await Promise.all(Object.values(schema).map((table: Table) => db.delete(table).execute()));
}

async function seed() {
    // 每次填充前先将数据库中的数据清空
    await clearTable();

    // 填充一些测试数据用来使用
    await seedUser(db);
}

seed().catch((e) => {
    console.error(e);
    process.exit(0);
});
import { hashSync } from 'bcrypt';

import { db } from '@/drizzle/db';

import { schema } from '../schema';

export async function seedUser(db: db) {
    await db.insert(schema.user).values([
        { username: 'admin', password: hashSync('123456', 10) },
        { username: 'user', password: hashSync('123456', 10) },
    ]);
}

在 package.json 中添加命令

{
  "db:seed": "ts-node ./src/drizzle/seed.ts",
}

我们就可以运行 pnpm run db:seed 来进行数据库填充了

面试官:讲讲这段react代码的输出(踩坑)

从一段看似正常的代码,到深入理解 React Hooks 的闭包陷阱

前言

之前面试,面试官递过来一段代码:"看看这段代码有啥问题?" image.png

我扫了一眼——标准的 React 组件,用了 useStateuseEffect,设置了个定时器每秒打印计数。代码看起来挺规范的,没有明显的语法错误。点击按钮,UI 上的数字也正常更新:1、2、3...

但打开 Console 一看,我愣住了:

Count: 0
Count: 0
Count: 0
Count: 0
...

UI 明明在变,为什么打印的永远是 0?

带着这个困惑,我回来后花了个晚上把 React Hooks 的闭包机制翻了个底朝天。没想到这个看起来简单的 bug,背后藏着的是 JavaScript 闭包和 React 渲染机制的深层交互。

先抛几个问题,看看你能答对几个:

  • 为什么 UI 正常更新,但 console 输出错误?
  • 闭包是怎么"困住"旧值的?
  • useEffect 的空依赖数组 [] 有什么影响?
  • 这个 bug 有几种修复方法?哪种最优?

这篇文章会详细讲解:

  • Bug 的完整复现和分析
  • JavaScript 闭包机制
  • useEffect 的依赖机制
  • 5 种解决方案的完整对比
  • 如何避免类似问题

目录


Bug 演示

完整代码

import { useEffect, useState } from "react";

export function App() {
  const [count, setCount] = useState(0);

  function handleLog() {
    console.log("Count:", count);
  }

  useEffect(() => {
    const id = setInterval(handleLog, 1000);
    return () => clearInterval(id);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

运行效果

UI 显示

Count: 3  ← 点击了3次,显示正常
[Increment 按钮]

Console 输出

Count: 0  ← 一直是0!
Count: 0
Count: 0
Count: 0
...

问题分析:陈旧闭包

什么是陈旧闭包(Stale Closure)?

这个 bug 的根源是 陈旧闭包(Stale Closure) ——函数"记住"了它创建时的环境,但这个环境里的值已经过时了。

为什么会出现?

执行流程详解

  1. 初始渲染(count = 0)

    • 创建 handleLog 函数,捕获 count = 0
    • useEffect 执行,设置 setInterval(handleLog, 1000)
    • 注意:useEffect 的依赖是 [],所以只执行一次
  2. 用户点击按钮

    • setCount(1) → 触发重新渲染
    • 创建新的 handleLog 函数,捕获新的 count = 1
    • 但是! useEffect 不会再执行(依赖是 []
    • setInterval 调用的还是第一次渲染时的旧 handleLog
  3. 结果

    • UI 显示的是最新的 count(React 状态正常更新)
    • setInterval 调用的 handleLog 里的 count 永远是 0(闭包捕获的旧值)

原理深挖:闭包如何困住旧值

闭包基础

先用个简单例子理解闭包:

function createCounter() {
  let count = 0;  // 被"捕获"的变量

  return function() {
    console.log(count);  // 能访问外层的 count
  };
}

const counter1 = createCounter();
const counter2 = createCounter();

counter1();  // 输出: 0
counter2();  // 输出: 0

// 即使外层函数执行完了,内层函数还能访问 count

闭包说穿了就是:函数能"记住"它创建时的环境

React 中的闭包陷阱

// 第一次渲染(count = 0)
function App() {
  const count = 0;  // ← 这个值

  function handleLog() {
    console.log(count);  // ← 被这个函数捕获
  }

  useEffect(() => {
    setInterval(handleLog, 1000);  // ← interval 记住了这个 handleLog
  }, []);  // ← 空数组,只执行一次

  // ...
}

// 第二次渲染(count = 1)
function App() {
  const count = 1;  // ← 新的值

  function handleLog() {
    console.log(count);  // ← 新的函数,捕获新值
  }

  // useEffect 不执行(依赖是空数组)
  // interval 还在调用第一次渲染时的旧 handleLog

  // ...
}

关键点

  • 每次渲染都会创建新的 count 变量和新的 handleLog 函数
  • useEffect 只在首次渲染时执行(依赖是 []
  • setInterval 调用的是第一次渲染时的 handleLog
  • 那个 handleLog 里捕获的 count 永远是 0

解决方案对比

下面介绍 5 种修复方法,每种都有适用场景。

方案对比表

方案
1. 添加 count 依赖
2. 使用 useRef
3. 函数式更新
4. useLatest 自定义 Hook
5. useEffectEvent (React 18+)

方案 1:添加 count 依赖

思路:让 useEffectcount 变化时重新执行。

useEffect(() => {
  const id = setInterval(handleLog, 1000);
  return () => clearInterval(id);
}, [count]);  // ← 添加 count 依赖

优点

  • 简单直接,一行改动

缺点

  • 性能差:每次 count 变化都会:

    1. 清除旧的 interval
    2. 创建新的 interval
  • 对于快速更新的状态,会频繁重建 interval


方案 2:使用 useRef ⭐⭐⭐⭐

思路:用 useRef 保存最新值,interval 读取 ref。

import { useEffect, useState, useRef } from "react";

export function App() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  // 同步 count 到 ref
  useEffect(() => {
    countRef.current = count;
  }, [count]);

  function handleLog() {
    console.log("Count:", countRef.current);  // ← 读取 ref
  }

  useEffect(() => {
    const id = setInterval(handleLog, 1000);
    return () => clearInterval(id);
  }, []);  // ← 空数组,只设置一次

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

为什么有效?

  • ref.current 是可变的,修改它不会触发重新渲染
  • 每次 count 更新时,同步到 countRef.current
  • handleLog 读取 countRef.current,总是最新值

优点

  • 性能好(interval 只创建一次)
  • 总是读取最新值

缺点

  • 需要额外的 useEffect 同步值
  • 代码稍显冗余

方案 3:函数式更新

思路:利用 setState 的函数式更新,不依赖闭包捕获的值。

import { useEffect, useState } from "react";

export function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount((prevCount) => {
        console.log("Count:", prevCount);  // ← 读取最新值
        return prevCount;  // 不修改,只打印
      });
    }, 1000);

    return () => clearInterval(id);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

优点

  • 简单,不需要 ref
  • 不需要依赖数组

缺点

  • 只适合简单场景:如果需要访问多个状态,代码会很丑陋
  • 滥用 setState 作为"读取"手段,语义不清晰

方案 4:useLatest 自定义 Hook ⭐⭐⭐⭐⭐

思路:封装方案 2 的 ref 逻辑,提高复用性。

import { useEffect, useState, useRef } from "react";

// 自定义 Hook:保存最新值
function useLatest(value) {
  const ref = useRef(value);

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref;
}

export function App() {
  const [count, setCount] = useState(0);
  const countRef = useLatest(count);  // ← 封装成 Hook

  function handleLog() {
    console.log("Count:", countRef.current);
  }

  useEffect(() => {
    const id = setInterval(handleLog, 1000);
    return () => clearInterval(id);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

优点

  • 复用性好,可在多个地方使用
  • 语义清晰:useLatest 明确表示"总是最新值"
  • 性能好

缺点

  • 需要额外维护自定义 Hook

方案 5:useEffectEvent (React 18+) ⭐⭐⭐⭐

思路:使用 React 官方的实验性 API。

import { useEffect, useState, experimental_useEffectEvent as useEffectEvent } from "react";

export function App() {
  const [count, setCount] = useState(0);

  // useEffectEvent:创建一个"总是最新"的事件处理函数
  const handleLog = useEffectEvent(() => {
    console.log("Count:", count);
  });

  useEffect(() => {
    const id = setInterval(handleLog, 1000);
    return () => clearInterval(id);
  }, []);  // ← 不需要添加 handleLog 依赖

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

优点

  • 官方解决方案,专为此设计
  • 语义清晰
  • 不需要手动管理 ref

缺点

  • 实验性 API(React 18 中可用,但可能变动)
  • 需要 React 18+

类似陷阱举例

闭包陷阱不仅出现在 useEffect 中,下面是几个常见场景:

1. 事件监听器

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    function handleClick() {
      console.log("Count:", count);  // ← 闭包捕获旧值
    }

    document.addEventListener("click", handleClick);

    return () => {
      document.removeEventListener("click", handleClick);
    };
  }, []);  // ← 空数组,只执行一次

  return <button onClick={() => setCount((c) => c + 1)}>Increment</button>;
}

修复:添加 count 依赖,或使用 useRef


2. 异步回调

function App() {
  const [count, setCount] = useState(0);

  function handleAsync() {
    setTimeout(() => {
      console.log("Count:", count);  // ← 3秒后打印,可能已经变了
    }, 3000);
  }

  return (
    <div>
      <button onClick={handleAsync}>异步打印</button>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

问题

  • 点击"异步打印"时 count = 0
  • 3秒内点击 Increment 多次,count = 5
  • 3秒后 setTimeout 执行,打印 Count: 0(闭包捕获的旧值)

修复:使用 useRefuseLatest


3. 防抖/节流函数

function App() {
  const [searchTerm, setSearchTerm] = useState("");

  const handleSearch = useMemo(
    () =>
      debounce(() => {
        console.log("搜索:", searchTerm);  // ← 闭包捕获旧值
      }, 500),
    []  // ← 空数组,只创建一次
  );

  return <input onChange={(e) => setSearchTerm(e.target.value)} />;
}

修复:添加 searchTerm 依赖,或使用 useLatest


避坑指南

1. 开启 ESLint 规则

// .eslintrc.json
{
  "rules": {
    "react-hooks/exhaustive-deps": "warn"
  }
}

这个规则会检查:

  • useEffectuseCallbackuseMemo 的依赖数组
  • 如果函数内使用了外部变量,但没有加入依赖,会报警告

2. 检查清单

遇到闭包相关的 bug 时,问自己这几个问题:

  • 函数内是否使用了组件的 props 或 state?
  • useEffect 的依赖数组是否完整?
  • 是否有定时器、事件监听器、异步回调?
  • 是否需要总是读取最新值?

3. 快速识别方法

看到这些代码模式,立即警惕

useEffect(() => {
  // 使用了 state/props,但依赖是空数组
  console.log(someState);
}, []);  // ← 🚨 危险!

useEffect(() => {
  setInterval(() => {
    // 使用了 state/props
    console.log(someState);
  }, 1000);
}, []);  // ← 🚨 危险!

useEffect(() => {
  document.addEventListener("click", () => {
    // 使用了 state/props
    console.log(someState);
  });
}, []);  // ← 🚨 危险!

4. 最佳实践

推荐顺序(从简单到复杂):

  1. 首选:使用 ESLint,添加完整依赖
  2. 性能要求高:使用 useLatest 自定义 Hook
  3. React 18+ :使用 useEffectEvent(实验性)
  4. 简单场景:使用函数式更新

总结

这个看起来简单的 bug,背后是 JavaScript 闭包和 React 渲染机制的交互:

核心原理

  • 每次渲染都会创建新的函数和变量
  • 闭包会"记住"函数创建时的环境
  • useEffect 的依赖数组决定何时重新执行
  • 空依赖数组 [] 导致 effect 只执行一次,捕获的是初始值

陈旧闭包的特征

  • UI 正常,但异步操作(定时器、事件监听、回调)读取到旧值
  • useEffect 依赖不完整
  • 函数内使用了外部变量,但没有加入依赖

推荐解决方案

  1. 通用场景:使用 useLatest 自定义 Hook(复用性好)
  2. 简单场景:使用 useRef(手动同步值)
  3. React 18+ :使用 useEffectEvent(官方方案,实验性)

面试启示: 这类题考察的是:

  • 对 JavaScript 闭包的理解
  • 对 React Hooks 机制的掌握
  • 解决实际问题的能力(多种方案对比)

下次写 useEffect 时,多问一句:这个函数用到的变量,是不是总是最新的? 养成这个习惯,就能避开大部分闭包陷阱。


相关资源

深入理解 Slot(插槽)

一、什么是 slot slot 最早来自 Web Components(原生自定义元素)规范,是组件内部的占位符,用于在组件外部填充内容。原生 HTML 的一个例子: template 本身不会直接渲

从Hello World到变量数据类型:JavaScript新手避坑指南

你是不是刚学JavaScript,对着黑乎乎的代码编辑器一脸懵?是不是写了半天代码,结果浏览器一片空白?别担心,每个程序员都是这么过来的!

今天这篇文章,就是为你量身打造的JavaScript入门指南。从最简单的Hello World开始,到让你头疼的变量和数据类型,我都会用最通俗的语言讲清楚。看完这篇文章,你能彻底搞懂JavaScript的基础概念,再也不会被那些专业术语吓到了!

第一行代码:Hello World!

学任何编程语言,第一个要写的肯定是Hello World。这就像学开车要先点火一样,是个仪式感满满的事情。

在JavaScript里,有几种方式可以输出Hello World。最简单的是用控制台输出:

// 在浏览器控制台输出Hello World
console.log("Hello World!");

看到这行代码是不是有点懵?别急,我来一句句解释:

  • console.log 是JavaScript的内置函数,意思是在控制台输出内容
  • 括号里的 "Hello World!" 是要输出的文本
  • 分号表示一行代码的结束(在JavaScript里分号是可选的,但建议新手养成加分的习惯)

写完了代码,怎么运行呢?最简单的方法是打开浏览器的开发者工具。按F12,找到Console(控制台)标签,把上面那行代码粘贴进去,按回车,你就能看到Hello World了!

还有一种方式是在HTML文件里写JavaScript代码:

<!DOCTYPE html>
<html>
<head>
    <title>我的第一个JS程序</title>
</head>
<body>
    <script>
        // 在页面弹出对话框显示Hello World
        alert("Hello World!");
    </script>
</body>
</html>

这段代码里的 alert 函数会在网页上弹出一个对话框。把这段代码保存为html文件,用浏览器打开,你就能看到效果了。

变量:数据的临时储物柜

写完了Hello World,我们来聊聊变量。变量说白了就是给数据起个名字,方便后面使用。就像你在超市存包,拿到一个号码牌,以后凭这个牌子就能取包。

在JavaScript里声明变量有几种方式:

// 用var声明变量(老方法,现在不太推荐)
var name = "小明";

// 用let声明变量(推荐方式)
let age = 18;

// 用const声明常量(值不会变的变量)
const PI = 3.14159;

看到这里你可能要问:var、let、const有什么区别?我来举个例子你就明白了:

// var的问题:可以重复声明,容易出错
var score = 90;
var score = 100; // 这样不会报错,但可能不是你想要的效果

// let的好处:不能重复声明
let count = 5;
// let count = 10; // 这行会报错,告诉你count已经声明过了

// const是常量,声明后不能改变值
const MAX_SIZE = 100;
// MAX_SIZE = 200; // 这行会报错,因为常量不能被重新赋值

实际开发中,我建议你这样选择:

  • 大部分时候用 let
  • 确定这个值不会改变时用 const
  • 尽量不要用 var,这是老旧的写法

数据类型:认识你的数据家庭成员

JavaScript里的数据类型就像是人的血型,决定了数据能做什么、不能做什么。主要分为两大类:基本类型和引用类型。

先来看看基本类型:

// 1. 字符串(String)- 表示文本
let username = "张三";
let message = 'Hello World';
let template = `你好,${username}`; // 模板字符串,可以插入变量

// 2. 数字(Number)- 包括整数和小数
let integer = 42;          // 整数
let float = 3.14;          // 小数
let negative = -10;        // 负数
let scientific = 1.5e3;    // 科学计数法,等于1500

// 3. 布尔值(Boolean)- 只有true或false
let isOnline = true;
let hasPermission = false;

// 4. Undefined - 变量声明了但没赋值
let undefinedVar;
console.log(undefinedVar); // 输出:undefined

// 5. Null - 表示空值
let emptyValue = null;

// 6. Symbol - 唯一且不可变的值(ES6新增)
let sym1 = Symbol("description");
let sym2 = Symbol("description");
console.log(sym1 === sym2); // 输出:false,即使描述相同也是不同的Symbol

再来看看引用类型,主要是对象和数组:

// 对象(Object)- 键值对的集合
let person = {
    name: "李四",
    age: 25,
    isStudent: true,
    sayHello: function() {
        console.log("你好!");
    }
};

// 访问对象属性
console.log(person.name);      // 输出:李四
console.log(person["age"]);    // 输出:25
person.sayHello();             // 输出:你好!

// 数组(Array)- 有序的数据列表
let fruits = ["苹果", "香蕉", "橙子"];
let mixedArray = [1, "文本", true, null];

// 访问数组元素
console.log(fruits[0]);        // 输出:苹果
console.log(fruits.length);    // 输出:3,数组长度

类型检测和转换:看清数据的真面目

有时候我们需要知道一个变量到底是什么类型,或者把一种类型转换成另一种类型。这时候就需要类型检测和转换的技巧。

先来看看怎么检测类型:

// typeof 操作符 - 检测基本类型
let str = "hello";
let num = 123;
let bool = true;

console.log(typeof str);   // 输出:string
console.log(typeof num);   // 输出:number  
console.log(typeof bool);  // 输出:boolean
console.log(typeof undefined); // 输出:undefined

// 注意:typeof null 返回 "object",这是JavaScript的历史遗留问题
console.log(typeof null);  // 输出:object

// 检测数组和对象
let arr = [1, 2, 3];
let obj = { key: "value" };

console.log(Array.isArray(arr));  // 输出:true
console.log(typeof obj);          // 输出:object

类型转换也很常见,特别是从用户输入获取数据时:

// 字符串转数字
let stringNum = "123";
let realNum = Number(stringNum);
console.log(realNum);         // 输出:123
console.log(typeof realNum);  // 输出:number

// 更简单的方法:用 + 操作符
let quickConvert = +"456";
console.log(quickConvert);    // 输出:456

// 数字转字符串
let numberValue = 789;
let stringValue = String(numberValue);
console.log(stringValue);     // 输出:"789"
console.log(typeof stringValue); // 输出:string

// 更简单的方法:用空字符串连接
let quickString = 123 + "";
console.log(quickString);     // 输出:"123"

// 布尔值转换
let truthyValue = Boolean(1);     // 输出:true
let falsyValue = Boolean(0);      // 输出:false
let emptyString = Boolean("");    // 输出:false

实战练习:做个简单的用户信息卡片

光说不练假把式,我们来写个实际的小例子,把今天学的东西都用上:

// 定义用户信息
const userName = "王五";
let userAge = 28;
const isEmployed = true;
const skills = ["JavaScript", "HTML", "CSS"];
const contact = {
    email: "wangwu@example.com",
    phone: "13800138000"
};

// 输出用户信息卡片
console.log("=== 用户信息卡片 ===");
console.log(`姓名:${userName}`);
console.log(`年龄:${userAge}`);
console.log(`就业状态:${isEmployed ? "已就业" : "待业"}`);
console.log(`技能:${skills.join("、")}`);
console.log(`邮箱:${contact.email}`);
console.log(`电话:${contact.phone}`);

// 模拟一年后的年龄变化
userAge = userAge + 1;
console.log(`一年后年龄:${userAge}`);

这段代码展示了:

  • 使用const和let声明变量
  • 字符串、数字、布尔值、数组、对象等各种数据类型
  • 模板字符串的使用
  • 基本的数学运算
  • 数组的join方法

常见坑点和避坑指南

新手写JavaScript经常会遇到一些坑,我总结了几个最常见的:

// 坑点1:变量提升
console.log(hoistedVar); // 输出:undefined,不会报错
var hoistedVar = "我被提升了";

// 如果用let就会报错
// console.log(notHoisted); // 报错
// let notHoisted = "我不会被提升";

// 坑点2:== 和 === 的区别
console.log(1 == "1");   // 输出:true,只比较值
console.log(1 === "1");  // 输出:false,比较值和类型

// 建议:总是使用 ===,避免类型转换的意外结果

// 坑点3:数字精度问题
console.log(0.1 + 0.2); // 输出:0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // 输出:false

// 解决方案:使用toFixed处理小数
let result = (0.1 + 0.2).toFixed(1);
console.log(result); // 输出:"0.3"

// 坑点4:null和undefined的区别
let testVar;
console.log(testVar);   // 输出:undefined,声明但未赋值
console.log(typeof testVar); // 输出:undefined

let nullVar = null;
console.log(nullVar);   // 输出:null,明确设置为空值
console.log(typeof nullVar); // 输出:object

学习路线和建议

学完今天的内容,你已经迈出了JavaScript学习的第一步。接下来你可以这样安排学习:

第一周:巩固基础

  • 每天写20个变量声明的练习
  • 熟悉各种数据类型的特性和用法
  • 掌握类型转换的常用方法

第二周:开始实战

  • 用学到的知识做个小项目,比如个人简介页面
  • 学习函数的基本概念
  • 了解条件判断和循环

记住,编程不是看会的,是练会的。多写代码,多调试,遇到问题先自己思考,再查资料。

学习资源推荐:

  • MDN Web Docs:最权威的JavaScript文档
  • freeCodeCamp:免费的互动式学习平台
  • 掘金、思否:有很多优质的技术文章

总结

今天我们走了JavaScript学习的第一步,从Hello World到变量和数据类型,这些都是最基础但最重要的概念。就像建房子要打好地基一样,把这些基础打牢固,后面学习更复杂的概念就会轻松很多。

记住几个关键点:

  • 变量是数据的容器,多用let和const
  • 了解每种数据类型的特点和用法
  • 掌握类型检测和转换的方法
  • 避开常见的坑点

学习编程就像学游泳,光在岸上看是学不会的,必须跳进水里多练习。现在就去打开你的代码编辑器,把今天学的例子都亲手敲一遍吧!

你在学习JavaScript的过程中遇到了什么困难?或者有什么特别想了解的话题?欢迎在评论区留言,我们一起交流进步!

❌