普通视图

发现新文章,点击刷新页面。
今天 — 2025年10月25日掘金 前端

管理系统——应用初始化 Loading 动画

作者 code_YuJun
2025年10月25日 15:01

动画创建

  • 位置:index.html
  <div id="app">
      ... 动画内容 ...
  </div>
  <script type="module" src="/src/main.js"></script>

动画消失

/src/main.js 加载完毕之后,createApp(App).mount('#app') 会替换掉 #app 内的内容。

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

createApp(App).mount('#app')

完整代码

  • index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>我的网站</title>
</head>

<body>
  <div id="app">
    <style>
      * {
        /* 初始化 取消页面内外边距 */
        margin: 0;
        padding: 0;
      }

      body {
        /* 100%窗口高度 */
        height: 100vh;
        background: linear-gradient(to bottom, #2b5876, #09203f);
        /* 弹性布局 水平、垂直居中 */
        display: flex;
        justify-content: center;
        align-items: center;
      }

      .loading {
        width: 200px;
        height: 200px;
        box-sizing: border-box;
        border-radius: 50%;
        border-top: 10px solid #63A69F;
        /* 相对定位 */
        position: relative;
        /* 执行动画:动画a1 时长 线性的 无限次播放 */
        animation: a1 2s linear infinite;
      }

      .loading::before,
      .loading::after {
        content: "";
        width: 200px;
        height: 200px;
        /* 绝对定位 */
        position: absolute;
        left: 0;
        top: -10px;
        box-sizing: border-box;
        border-radius: 50%;
      }

      .loading::before {
        border-top: 10px solid #F2E1AC;
        /* 旋转120度 */
        transform: rotate(120deg);
      }

      .loading::after {
        border-top: 10px solid #F2836B;
        /* 旋转240度 */
        transform: rotate(240deg);
      }

      .loading span {
        /* 绝对定位 */
        position: absolute;
        width: 200px;
        height: 200px;
        line-height: 200px;
        text-align: center;
        color: #fff;
        /* 执行动画:动画a2 时长 线性的 无限次播放 */
        animation: a2 2s linear infinite;
      }
      /* 定义动画 */
      @keyframes a1 {
        to {
          transform: rotate(360deg);
        }
      }
      @keyframes a2 {
        to {
          transform: rotate(-360deg);
        }
      }
    </style>
    <div class="loading">
      <span>拼命加载中</span>
    </div>
  </div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>
  • main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

createApp(App).mount('#app')

动画效果

截屏2025-10-25 15.00.18.png

JavaScript 模块化演进历程:问题与解决方案。

2025年10月25日 14:44

JavaScript 模块化演进历程:问题与解决方案

JavaScript模块化的发展历程,本质上是一部解决代码组织问题的历史。下面详细介绍每个阶段的特点、代码案例、存在问题及解决方案:

一、无模块化阶段(早期时代)

特点

  • 没有模块的概念,代码直接在HTML中通过<script>标签引入
  • 所有变量和函数都在全局作用域中

代码案例

<!-- index.html -->
<script src="utils.js"></script>
<script src="app.js"></script>
// utils.js
globalCounter = 0;

function updateCounter() {
  globalCounter++;
  console.log('Counter updated:', globalCounter);
}
// app.js
name = 'App';

function init() {
  console.log('Initializing ' + name);
  updateCounter();
}

init();

存在问题

  1. 全局变量污染:所有变量都在全局作用域,容易导致命名冲突
  2. 依赖关系不明确:无法清晰看出模块间的依赖关系
  3. 加载顺序敏感:文件加载顺序必须严格控制,否则会出错
  4. 维护困难:随着代码量增加,难以维护和复用
  5. 可扩展性差:不利于大型项目开发

解决方法

引入命名空间模式或立即执行函数表达式(IIFE)来隔离变量。

二、命名空间模式

特点

  • 使用对象作为命名空间,减少全局变量数量
  • 将相关功能组织在一个对象中

代码案例

// 命名空间模式
var MyApp = MyApp || {};

// 模块A
MyApp.Utils = {
  counter: 0,
  updateCounter: function() {
    this.counter++;
    return this.counter;
  },
  formatDate: function(date) {
    return date.toLocaleDateString();
  }
};

// 模块B
MyApp.Services = {
  getData: function() {
    console.log('Getting data...');
    // 可以使用Utils模块
    return { id: MyApp.Utils.updateCounter() };
  }
};

// 使用
console.log(MyApp.Utils.formatDate(new Date()));
var data = MyApp.Services.getData();

存在问题

  1. 仍然存在全局变量:命名空间对象本身还是全局的
  2. 内部属性可被外部修改:没有真正的私有变量
  3. 依赖关系依然不明确:模块间依赖关系需要手动管理
  4. 无法按需加载:所有代码在页面加载时都会执行

解决方法

引入立即执行函数表达式(IIFE)创建私有作用域。

三、IIFE模式(立即执行函数表达式)

特点

  • 创建独立的作用域,避免全局变量污染
  • 可以模拟私有变量和方法

代码案例

// IIFE模式
var MyModule = (function() {
  // 私有变量
  var privateCounter = 0;
  
  // 私有方法
  function privateMethod() {
    console.log('This is private');
  }
  
  // 返回公共接口
  return {
    // 公共变量
    publicVar: 'Hello',
    
    // 公共方法
    incrementCounter: function() {
      privateCounter++;
      privateMethod();
      return privateCounter;
    },
    
    getCounter: function() {
      return privateCounter;
    }
  };
})();

// 使用
console.log(MyModule.publicVar); // 输出: Hello
console.log(MyModule.incrementCounter()); // 输出: This is private 和 1
console.log(MyModule.getCounter()); // 输出: 1
console.log(MyModule.privateCounter); // 输出: undefined (无法访问私有变量)

存在问题

  1. 模块依赖关系需要手动处理:如果多个模块相互依赖,需要确保加载顺序正确
  2. 无法按需加载:所有模块在页面加载时都被执行
  3. 模块之间的通信不够灵活:需要在全局作用域中暴露接口

解决方法

引入CommonJS或AMD等模块化规范。

四、CommonJS 规范

特点

  • 每个文件就是一个模块,拥有独立作用域
  • 使用module.exports导出,require()导入
  • 同步加载模块
  • 主要用于服务器端(Node.js)

代码案例

// math.js - 模块定义
const PI = 3.14159;

function add(a, b) {
  return a + b;
}

function circleArea(radius) {
  return PI * radius * radius;
}

// 导出模块
module.exports = {
  PI,
  add,
  circleArea
};
// app.js - 导入模块
const math = require('./math');

console.log(math.PI); // 输出: 3.14159
console.log(math.add(5, 3)); // 输出: 8
console.log(math.circleArea(2)); // 输出: 12.56636

存在问题

  1. 同步加载不适合浏览器环境:浏览器需要通过网络加载模块,同步加载会导致页面阻塞
  2. 无法在浏览器中直接使用:需要通过工具转换
  3. 加载顺序问题:在大型应用中可能导致性能问题

解决方法

为浏览器环境设计异步模块加载规范AMD。

五、AMD(Asynchronous Module Definition)

特点

  • 异步加载模块,不阻塞页面渲染
  • 依赖前置:定义模块时声明所有依赖
  • 使用define()定义模块,require()加载模块
  • 适合浏览器环境

代码案例

// RequireJS配置
require.config({
  baseUrl: 'js',
  paths: {
    'jquery': 'libs/jquery',
    'logger': 'modules/logger'
  }
});

// 定义logger模块
// logger.js
define([], function() {
  return {
    log: function(message) {
      console.log('[Logger]: ' + message);
    },
    error: function(message) {
      console.error('[Error]: ' + message);
    }
  };
});

// 定义依赖logger的模块
// dataService.js
define(['logger'], function(logger) {
  return {
    fetchData: function() {
      logger.log('Fetching data...');
      // 模拟异步操作
      return new Promise(function(resolve) {
        setTimeout(function() {
          const data = { id: 1, name: 'Item 1' };
          logger.log('Data fetched successfully');
          resolve(data);
        }, 1000);
      });
    }
  };
});

// 主应用
// main.js
require(['jquery', 'logger', 'dataService'], function($, logger, dataService) {
  logger.log('Application started');
  
  dataService.fetchData().then(function(data) {
    $('#result').text('Data: ' + JSON.stringify(data));
  });
});
<!-- HTML中引入RequireJS -->
<script data-main="js/main" src="js/libs/require.js"></script>
<div id="result"></div>

存在问题

  1. 依赖前置导致代码冗余:即使某些依赖暂时不用,也需要在定义时声明
  2. 代码可读性降低:回调嵌套可能导致"回调地狱"
  3. 模块定义语法冗长:相比CommonJS语法更复杂

解决方法

引入CMD规范,采用就近依赖和延迟执行策略。

六、CMD(Common Module Definition)

特点

  • 异步加载模块
  • 就近依赖:在需要使用模块时才引入
  • 延迟执行:按需加载
  • 语法更接近CommonJS

代码案例

// SeaJS配置
seajs.config({
  base: './js',
  alias: {
    'jquery': 'libs/jquery.js'
  }
});

// 定义工具模块
// utils.js
define(function(require, exports, module) {
  // 私有工具函数
  function formatNumber(num) {
    return num.toFixed(2);
  }
  
  // 导出公共方法
  exports.formatCurrency = function(amount) {
    return '$' + formatNumber(amount);
  };
});

// 定义用户模块
// userModule.js
define(function(require, exports, module) {
  // 导出用户相关方法
  exports.getUserName = function() {
    return 'John Doe';
  };
});

// 定义主模块
// main.js
define(function(require, exports, module) {
  // 在这里不引入任何模块
  
  function init() {
    console.log('Initializing...');
    
    // 就近依赖:需要时才引入
    const utils = require('./utils');
    console.log(utils.formatCurrency(100.5)); // 输出: $100.50
    
    // 条件加载
    if (needUserInfo()) {
      const userModule = require('./userModule');
      console.log('User:', userModule.getUserName());
    }
  }
  
  function needUserInfo() {
    return true; // 实际应用中可能是更复杂的判断
  }
  
  // 导出init方法
  exports.init = init;
});

// 启动应用
seajs.use('./main', function(main) {
  main.init();
});

存在问题

  1. 浏览器兼容性问题:需要额外的构建工具支持
  2. 依赖追踪困难:由于延迟加载,静态分析变得困难
  3. 生态系统不如AMD完善:主要在国内使用较多

解决方法

引入UMD模式以实现跨环境兼容,或等待ES6官方模块化规范。

七、UMD(Universal Module Definition)

特点

  • 通用模块定义,兼容多种模块规范
  • 可以在CommonJS、AMD和全局变量环境中使用
  • 跨环境兼容性强

代码案例

// UMD模式实现
(function(root, factory) {
  // 判断模块环境
  if (typeof define === 'function' && define.amd) {
    // AMD环境
    define([], factory);
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS环境
    module.exports = factory();
  } else {
    // 全局变量环境
    root.MyLibrary = factory();
  }
}(typeof self !== 'undefined' ? self : this, function() {
  // 模块实现
  const privateVar = 'private';
  
  function privateMethod() {
    return privateVar;
  }
  
  // 返回公共API
  return {
    version: '1.0.0',
    doSomething: function() {
      return 'Did something with ' + privateMethod();
    },
    utility: function(value) {
      return value.toUpperCase();
    }
  };
}));

// 在不同环境中使用:
// AMD: define(['mylibrary'], function(MyLibrary) { ... });
// CommonJS: const MyLibrary = require('mylibrary');
// 全局变量: MyLibrary.doSomething();

存在问题

  1. 代码冗余:需要额外的环境检测代码
  2. 无法利用特定环境的优势:为了兼容性而牺牲了某些环境特定的优化
  3. 加载优化困难:无法实现真正的按需加载

解决方法

等待JavaScript语言层面的模块化支持,即ES6 Module。

八、ES6 Module

特点

  • 语言层面的模块化支持
  • 静态导入导出:编译时确定依赖关系
  • 支持命名导出和默认导出
  • 浏览器和服务器端通用
  • 支持tree-shaking优化

代码案例

// math.js - 命名导出
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// 也可以批量导出
export const operations = {
  multiply: (a, b) => a * b,
  divide: (a, b) => a / b
};

// user.js - 默认导出
class User {
  constructor(name) {
    this.name = name;
  }
  
  greet() {
    return `Hello, ${this.name}!`;
  }
}

export default User;
// app.js - 导入模块
// 导入命名导出
import { PI, add, subtract } from './math.js';
import { operations } from './math.js';

// 导入默认导出
import User from './user.js';

// 使用导入的功能
console.log(PI); // 3.14159
console.log(add(5, 3)); // 8
console.log(operations.multiply(4, 5)); // 20

const user = new User('Alice');
console.log(user.greet()); // Hello, Alice!

// 导入所有命名导出
import * as mathModule from './math.js';
console.log(mathModule.PI); // 3.14159
console.log(mathModule.subtract(10, 7)); // 3
<!-- 在HTML中使用ES6模块 -->
<script type="module" src="app.js"></script>

存在问题

  1. 浏览器兼容性:旧浏览器不支持,需要转译
  2. 需要构建工具:在生产环境中通常需要打包工具
  3. 动态导入支持有限:虽然支持动态import(),但浏览器支持程度不一

解决方法

使用现代构建工具如Webpack、Rollup等进行打包和转译。

九、现代构建工具与模块化

特点

  • 支持多种模块规范混合使用
  • 提供代码分割、按需加载、tree-shaking等优化
  • 解决浏览器兼容性问题
  • 支持复杂的依赖管理

代码案例

// webpack.config.js
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: __dirname + '/dist'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  },
  mode: 'production'
};
// src/utils.js - CommonJS风格
const helper = () => {
  return 'Helper function';
};

module.exports = { helper };

// src/api.js - ES6模块风格
export const fetchUser = async (id) => {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
};

// src/index.js - 混合使用
// 导入CommonJS模块
const { helper } = require('./utils');

// 导入ES6模块
import { fetchUser } from './api';

// 动态导入(代码分割)
const loadAdminPanel = () => {
  import('./admin').then((adminModule) => {
    adminModule.init();
  });
};

console.log(helper());

// 使用
document.getElementById('loadAdmin').addEventListener('click', loadAdminPanel);

存在问题

  1. 构建配置复杂:配置Webpack等工具需要一定学习成本
  2. 构建过程增加开发时间:大型项目构建可能较慢
  3. 调试不便:需要使用source maps等工具辅助调试

解决方法

使用零配置工具如Vite,或使用框架提供的脚手架工具简化配置。

演进总结

阶段 主要问题 解决方案 关键特点
无模块化 全局变量污染、依赖混乱 引入命名空间和IIFE 简单但问题多
命名空间 仍有全局变量、无真正私有性 IIFE模式 创建独立作用域
IIFE 依赖管理困难、无法按需加载 模块化规范 私有变量模拟
CommonJS 同步加载不适合浏览器 AMD规范 服务器端标准
AMD 依赖前置、语法冗长 CMD规范 异步加载
CMD 静态分析困难、生态不完善 UMD或ES6 Module 就近依赖
UMD 代码冗余、优化困难 ES6 Module 跨环境兼容
ES6 Module 浏览器兼容性、动态导入限制 现代构建工具 语言级支持
现代构建工具 配置复杂、构建速度 优化工具链 多种优化功能

JavaScript模块化的演进历程反映了前端工程化的不断发展,从最初简单的代码分割到现在的规范化、标准化模块系统,每一步都解决了前一阶段的核心问题,使得代码组织更加清晰,依赖管理更加精确,开发效率和代码质量得到了极大提升。

循环背后的魔法:Lua 迭代器深度解析

作者 烛阴
2025年10月25日 14:08

一、泛型 for 循环的解构

要理解迭代器,我们首先必须拆解泛型 for 循环的语法: for var_1, var_2, ..., var_n in <表达式列表> do ... end

这里的关键是 <表达式列表>。当循环开始时,Lua 会对这个列表求值一次,并期望它返回三个值

  1. 迭代器函数(Iterator Function):一个函数,for 循环在每次迭代时都会调用它来获取下一个(或下一组)值。
  2. 不变的状态(Invariant State):一个值(通常是表),它会在整个循环过程中被传入迭代器函数,用于维持状态。它本身通常不改变。
  3. 初始控制变量(Initial Control Variable):第一个要传入迭代器函数的值。

实际上,一个泛型 for 循环:

for var_1, var_2 in explist do
    -- 循环体
end

在 Lua 内部,等价于下面这段 while 循环代码:

-- 1. 求值,获取三个关键部分
local _iterator, _state, _control_var = explist

-- 2. 循环开始
while true do
    -- 3. 调用迭代器函数获取新值
    local var_1, var_2 = _iterator(_state, _control_var)
  
    -- 4. 更新控制变量为第一个返回值
    _control_var = var_1

    -- 5. 如果第一个返回值为 nil,则循环结束
    if var_1 == nil then
        break
    end

    -- 6. 执行循环体
    -- 循环体
end

这就是 for 循环的全部秘密!它只是一种 while 循环的语法糖,遵循着这个“三值协议”。

二、迭代器的两种核心模式

理解了协议后,我们来看看两种最主要的迭代器实现模式。

1. 无状态迭代器(Stateless Iterator)

“无状态”指的是不变的状态_state),它通常就是我们正在遍历的那个对象(例如一个表)。下一次迭代所需的一切信息都包含在控制变量(_control_var)中。

Lua 内置的 ipairspairs 就是典型的无状态迭代器。pairs(t) 实际上等价于 return next, t, nil

  • 迭代器函数next,Lua 内置的用于遍历表的函数。
  • 不变的状态t,我们正在遍历的表。
  • 初始控制变量nil,告诉 next 函数从头开始。

让我们亲手实现一个 ipairs 来加深理解:

local function my_ipairs_iterator(tbl, index)
    index = index + 1
    local value = tbl[index]
    if value then
        return index, value
    end
end

function my_ipairs(tbl)
    -- 返回三元组:迭代器函数, 状态, 初始控制变量
    return my_ipairs_iterator, tbl, 0
end

-- 使用我们自己的 ipairs
local days = { "Monday", "Tuesday", "Wednesday" }
for index, day in my_ipairs(days) do
    print(index, day)
end
-- 输出:
-- 1   Monday
-- 2   Tuesday
-- 3   Wednesday

你看,my_ipairs 完美地遵循了三值协议,for 循环也因此能正确地与它协作。

2. 有状态迭代器(Stateful Iterator)

“有状态”指的是遍历的对象无法作为一个不变的状态_state),需要我们添加状态构建一个新的不变的状态_state

-- 这是我们的迭代器函数。
-- 它接收不变的状态表和当前的控制变量。
local function range_iterator(state, current_val)
    -- 1. 检查当前值是否已经超出限制
    if current_val >= state.limit then
        return nil -- 返回 nil 来结束循环
    end

    -- 2. 计算下一个值
    local next_val = current_val + state.step

    -- 3. 返回下一个值,它将成为下一次循环的控制变量
    return next_val
end

-- 这是我们的迭代器构造函数
function range(start, finish, step)
    -- 设置默认值
    start = start or 1
    finish = finish or 10
    step = step or 1

    -- 创建包含不变信息的状态表
    local state = {
        limit = finish,
        step = step
    }

    -- **关键**:返回迭代器三元组
    -- 1. 迭代器函数: range_iterator
    -- 2. 状态表: state
    -- 3. 初始控制变量: 从 start 开始,所以初始值为 start
    --   (但为了让第一个返回的值就是 start 本身,
    --    初始控制变量需要是 start - step,这样第一次相加后正好是 start)
    return range_iterator, state, start - step
end

print("从 1 到 5,步长为 1:")
for i in range(1, 5) do
    print(i)
end
-- 输出:
-- 1
-- 2
-- 3
-- 4
-- 5

在lua中,常用闭包(Closure)来实现状态的保存,所以有状态迭代器的实现通常使用闭包实现,闭包实现过程中,没有显式地利用 for 循环传递的 statecontrol_var 参数

function close_iterator(start, finish, step)
    start = start - 1
    finish = finish or 10
    step = step or 1
    local current = start -- 保存的状态
    return function ()
        current = current + step
        if current <= finish then
            return current
        else
            return nil
        end
    end
end

for i in close_iterator(1, 5, 2) do
    print(i)
end

结语

点个赞,关注我获取更多实用 Lua 技术干货!如果觉得有用,记得收藏本文!

JavaScript 变量声明报错指南:var、let、const 常见错误解析

作者 烟袅
2025年10月25日 13:48

大家好,在上一篇文章中,我们深入探讨了 varletconst 的区别与最佳实践。今天,我们来聚焦一个更实际的主题——当你在使用这些关键字时,JavaScript 引擎会抛出哪些错误?它们背后的原因是什么?

理解这些错误信息,能让你在调试代码时事半功倍。我们直接来看几个最常见的报错场景。


🚨 1. ReferenceError: height is not defined

场景: 试图访问一个从未声明过的变量,或在变量作用域之外访问它。

console.log(height); // 报错: ReferenceError: height is not defined

原因分析:

  • height 这个变量名在当前作用域及其外层作用域中都找不到。
  • 这是最典型的“未定义”错误。它与“变量提升”不同——var 声明的变量会提升为 undefined,而完全未声明的变量则会直接抛出 ReferenceError

对比 var 的行为:

console.log(width); // 输出: undefined (不会报错!)
var width = 100;

看,这就是 var 的“提升”特性:即使你还没写 var width,引擎也已经知道有 width 这个变量了,只是值为 undefined。而完全未声明的变量,连“提升”的机会都没有。


🚨 2. TypeError: Assignment to constant variable.

场景: 尝试修改一个用 const 声明的变量的值。

const PI = 3.14159;
PI = 3; // 报错: TypeError: Assignment to constant variable.

原因分析:

  • const 的核心含义是“常量”,它声明的变量绑定是不可变的(immutable binding)。
  • 一旦 PI 被赋值为 3.14159,你就不能再用 = 操作符给它赋新值。
  • 注意: 这个错误是 TypeError,因为它是一个类型或操作上的错误(试图改变常量),而不是引用错误。

常见误区:

很多人以为 const 声明的“对象”是完全不可变的,其实不然:

const person = { name: "Alice" };
person.name = "Bob"; // ✅ 合法!修改对象内部属性
// person = {}; // ❌ 报错!试图改变 person 的指向

const 保证的是 person 这个变量名始终指向同一个对象,但对象内部的数据是可以改变的。


🚨 3. ReferenceError: Cannot access 'PI' before initialization

场景:constlet 变量声明之前访问它。

console.log(PI); // 报错: ReferenceError: Cannot access 'PI' before initialization
const PI = 3.14159;

原因分析:

这就是我们常说的“暂时性死区”(Temporal Dead Zone, TDZ)。

  • 虽然 constlet 的声明也会被“提升”,但它们在声明语句执行之前,是处于一个“不可访问”的状态。
  • 在这个“死区”内访问变量,JavaScript 会直接抛出 ReferenceError,而不是像 var 那样返回 undefined
  • 这个设计是有意为之的,它强制开发者遵循“先声明,后使用”的良好习惯,避免了 var 提升带来的潜在 bug。

对比 var

console.log(version); // 输出: undefined
var version = "1.0.0";

var 的行为在这里显得“宽容”,但这种宽容往往是 bug 的温床。


📊 错误类型对比

错误信息 错误类型 触发条件
ReferenceError: X is not defined ReferenceError 变量从未声明或作用域外访问
TypeError: Assignment to constant variable. TypeError 试图修改 const 变量的值
ReferenceError: Cannot access 'X' before initialization ReferenceError let/const 声明前访问(TDZ)

💡 如何避免这些错误?

  1. 使用 constlet,远离 var 这样你就能利用“暂时性死区”来及早发现错误,而不是让 undefined 静静地破坏你的逻辑。
  2. 遵循“先声明,后使用”的原则: 养成良好的代码组织习惯,把变量声明放在使用之前。
  3. 善用开发工具: 现代编辑器(如 VS Code)和 ESLint 规则可以帮你提前发现作用域和提升相关的问题。

✅ 总结

理解这些错误背后的机制,能让你从“被动修复”转向“主动预防”。记住:

  • is not defined → 检查变量名拼写和作用域。
  • Assignment to constant variable → 检查是否误改了 const 变量。
  • Cannot access ... before initialization → 检查代码顺序,确保在声明后再使用。

掌握这些,你就能更自信地驾驭 JavaScript 的变量系统了!

你在开发中还遇到过哪些有趣的变量相关错误?欢迎在评论区分享!

告别 var!深入理解 JavaScript 中 var、let 和 const 的差异与最佳实践

作者 烟袅
2025年10月25日 13:44

今天我们来聊聊 JavaScript 中一个看似基础却极其重要的知识点——变量声明。你是否曾经被 var 的“变量提升”搞得一头雾水?是否在 for 循环中因作用域问题而踩过坑?今天,我们就来彻底搞懂 varletconst 这三剑客,让你的代码从此远离“直觉不符”的陷阱!


📌 为什么 var 被认为是“Bad”?

在 ES6 之前,var 是声明变量的唯一方式。然而,随着 JavaScript 的发展,var 的一些特性被证明是“反直觉”且容易引发 bug 的。我们先来看一个经典的例子:

console.log(myVar); // 输出什么?是 undefined 还是报错?
var myVar = "Hello, var!";

// 答案是:undefined

发生了什么?这就是“变量提升”(Hoisting)。

在 JavaScript 的执行过程中,引擎会先进行一个“编译”阶段,将所有 var 声明的变量提升到其作用域的顶部。但注意,只有声明被提升,赋值不会提升。上面的代码在执行时,其行为等价于:

var myVar; // 声明被提升
console.log(myVar); // 此时 myVar 存在但未赋值,所以是 undefined
myVar = "Hello, var!"; // 赋值在原位置执行

var 的两大“痛点”:

  1. 变量提升带来的困惑: 你可以在声明前访问变量,得到 undefined 而不是报错,这容易掩盖未定义变量的错误。
  2. 缺乏块级作用域:
if (true) {
  var message = "I'm inside an if block!";
}
console.log(message); // 输出: "I'm inside an if block!"
// 糟糕!message 在 if 块外竟然还能访问!

这完全违背了我们对“块作用域”的直觉,极易导致变量污染和命名冲突。


let:块级作用域的救星

ES6 引入了 let 来解决 var 的问题。let 的核心优势在于块级作用域

什么是块级作用域?

简单说,用 {} 包裹的代码块(如 ifforwhile、函数体等)就是一个独立的作用域。

if (true) {
  let message = "Hello, let!";
  console.log(message); // 输出: "Hello, let!"
}
// console.log(message); // 报错: ReferenceError: message is not defined
// 完美!message 只在 if 块内有效

let 的“暂时性死区”(Temporal Dead Zone, TDZ)

let 也有变量提升,但它引入了“暂时性死区”的概念。在声明语句执行之前,访问该变量会直接抛出 ReferenceError,而不是返回 undefined

// console.log(greeting); // 报错: Cannot access 'greeting' before initialization
let greeting = "Hi!";
console.log(greeting); // 输出: "Hi!"

这比 var 更加安全,因为它强制你必须先声明再使用,避免了因提升导致的逻辑错误。


🔒 const:不可变的常量

const 用于声明一个常量,其值在声明后不能被重新赋值。

const PI = 3.14159;
// PI = 3; // 报错: TypeError: Assignment to constant variable.

关键点:

  • 必须初始化: const 声明时就必须赋值。
  • 不可重新赋值: 不能用 = 改变 const 变量的值。
  • “常量”不等于“不可变”: const 保证的是变量指向的内存地址不变。如果变量是一个对象或数组,你仍然可以修改其内部的属性或元素。
const user = { name: "Alice", age: 25 };
user.age = 26; // 合法!修改对象的属性
user.city = "Beijing"; // 合法!添加新属性
console.log(user); // { name: "Alice", age: 26, city: "Beijing" }

// user = {}; // 报错!试图改变 user 的指向

🆚 三者对比总结

特性 var let const
作用域 函数作用域 块级作用域 块级作用域
变量提升 是 (值为 undefined) 是 (有 TDZ) 是 (有 TDZ)
可重新赋值
必须初始化

🚀 最佳实践:拥抱现代 JavaScript

基于以上分析,我强烈推荐你在日常开发中遵循以下原则:

  1. 优先使用 const 对于所有不会被重新赋值的变量,都用 const 声明。这不仅能防止意外修改,还能让代码的可读性和可维护性大幅提升。研究表明,大部分变量在声明后其引用都不会改变。
  2. 其次使用 let 只有当你明确需要改变变量的值时(例如循环计数器 i),才使用 let
  3. 彻底告别 var 在现代项目中,尽量避免使用 var。它的作用域规则和提升机制是历史遗留问题,继续使用只会增加代码的复杂性和出错概率。
// ✅ 好的做法
const API_URL = "https://api.example.com";
const users = fetchUsers();
let currentUserIndex = 0;

for (let i = 0; i < users.length; i++) {
  const user = users[i];
  console.log(`Processing user: ${user.name}`);
  // ... 处理逻辑
}

// ❌ 避免的做法
var API_URL = "https://api.example.com";
var users = fetchUsers();
var currentUserIndex = 0;

for (var i = 0; i < users.length; i++) {
  var user = users[i]; // 在旧版浏览器中,这可能会导致闭包问题
  console.log(`Processing user: ${user.name}`);
}

💡 结语

理解 varletconst 的差异,不仅仅是掌握语法,更是培养良好的编程习惯和对作用域的深刻理解。从今天开始,用 constlet 武装你的代码,告别 var 带来的“惊喜”,写出更健壮、更清晰的 JavaScript 吧!

你的项目还在用 var 吗?欢迎在评论区分享你的看法和经验!

Electron 应用自动更新方案:electron-updater 完整指南

2025年10月25日 12:47

1. 概述

electron-updater 是 Electron 社区广泛采用的自动更新解决方案,通常与 electron-builder 配合使用。该库封装了跨平台的更新逻辑(支持 macOS、Windows、Linux),并通过事件回调机制让主进程能够在不同更新阶段向用户提供反馈或自动执行安装操作。

核心特性:

  • 提供完整的更新流程 API(autoUpdater),包括检查更新、下载更新、提示安装等功能
  • 支持多种发布方式:GitHub Releases、通用静态服务器、自建更新服务等
  • electron-builderpublish 配置无缝集成,自动生成更新元数据(如 latest.ymlRELEASES 等文件)

适用场景:需要自动分发新版本并降低用户升级成本的桌面应用程序。

2. 工作原理

  1. 检查更新:应用在启动或用户触发时调用 checkForUpdates()checkForUpdatesAndNotify()
  2. 版本比对electron-updater 向发布服务器请求元数据文件(如 latest.yml),与本地版本进行比对
  3. 下载更新:如果发现远程版本更高,则开始下载更新包(支持差分更新或完整包下载,取决于发布配置)
  4. 安装准备:下载完成后触发 update-downloaded 事件,可在适当时机调用 autoUpdater.quitAndInstall() 完成安装(支持立即重启或下次启动时安装)

3. 平台与打包器支持

  • Windows:NSIS、Squirrel(Squirrel 正逐步被 NSIS 等其他方案替代)
  • macOS:dmg、zip、mas(上架 Mac App Store 需要特殊处理)
  • Linux:AppImage、deb 等格式(支持程度取决于目标格式)

推荐组合:使用 electron-builder 构建安装包并生成更新元数据,electron-updater 负责运行时的更新检查和下载安装。

4. 完整配置流程

4.1 环境准备与安装

确保项目已安装必要的依赖:

# 在项目根目录执行
npm install --save-dev electron-builder
npm install --save electron-updater

4.2 配置更新服务器

package.json 中配置更新服务器的地址:

{
  "build": {
    "publish": [{
      "provider": "generic",
      "url": "http://your-update-server.com/updates"  // 替换为实际的更新服务器地址
    }]
  }
}

在 electron-updater 中,publish 配置是连接应用与更新服务器的桥梁,其核心意义体现在:

  1. 指定更新文件的存储位置(URL),使 electron-updater 能精确获取元数据文件(如 latest.yml)和安装包。
  2. 替代 Electron 原生 autoUpdater 的碎片化实现,提供跨平台统一的更新接口(支持 Windows/macOS/Linux)

4.3 主进程更新逻辑实现

在 Electron 的主进程文件(如 main.js)中实现更新检测与处理逻辑:

const { autoUpdater } = require('electron-updater');

// 在窗口创建后调用更新检测
function createWindow() {
  mainWindow = new BrowserWindow({ /* 窗口配置 */ });
  setupAutoUpdater(); // 初始化自动更新
}

function setupAutoUpdater() {
  // 自动检查更新并通知用户
  autoUpdater.checkForUpdatesAndNotify();
  
  // 监听更新可用事件
  autoUpdater.on('update-available', () => {
    mainWindow.webContents.send('update-status', '检测到新版本,正在下载...');
  });
  
  // 监听更新下载完成事件
  autoUpdater.on('update-downloaded', () => {
    mainWindow.webContents.send('update-status', '更新下载完成,准备安装');
    // 退出应用并安装更新
    autoUpdater.quitAndInstall();
  });
}

这段代码在应用窗口创建后自动启动更新检查,并通过事件机制向渲染进程发送更新状态信息。

4.4 渲染进程通信集成

如需在渲染进程中触发更新或显示更新状态,需要设置 IPC 通信:

主进程添加事件监听

// main.js
const { ipcMain } = require('electron');

// 监听渲染进程的更新请求
ipcMain.on('trigger-update', (event) => {
  setupAutoUpdater(); // 调用更新函数
});

渲染进程发送事件

// 在 Vue 组件或普通 HTML 页面中
const { ipcRenderer } = require('electron');

// 为更新按钮添加点击事件
document.getElementById('update-button').addEventListener('click', () => {
  ipcRenderer.send('trigger-update');
});

4.5 应用构建与分发

使用 electron-builder 打包应用并发布到更新服务器:

# 构建应用并自动发布
electron-builder build --publish always

构建完成后,将生成的文件(包括 latest.yml 等元数据文件和可执行文件)上传到配置的更新服务器。

当执行命令:electron-builder build -p always

1、自动生成版本元数据文件:

  • latest.yml → 通用版本描述
  • latest-mac.yml → macOS 专用
  • .blockmap → 增量更新支持文件

2、文件内容示例(latest.yml)

version: 2.1.0
files:
  - url: YourApp-Setup-2.1.0.exe
    size: 58451392
    sha512: xZYfE...  # 文件哈希值
path: YourApp-Setup-2.1.0.exe
sha512: xZYfE...
releaseDate: '2024-06-15T12:00:00.000Z'

3、需要手动/CICD,上传到文件服务器:(示例:your-server.com/updates/)

https://your-server.com/updates/
├── latest.yml              # 核心元数据
├── latest-mac.yml
├── YourApp-Setup-2.1.0.exe # 安装包
└── YourApp-2.1.0.dmg

4.6 更新功能测试

测试自动更新功能时,可按照以下步骤:

  1. 运行旧版本应用
  2. 确保应用能正确检测到服务器上的新版本
  3. 验证下载和安装流程是否正常

调试技巧:在开发阶段,可通过 autoUpdater.logger = console 启用详细日志输出,便于排查问题。


通过以上步骤,您可以为 Electron 应用实现完整的自动更新功能,为用户提供无缝的升级体验。

🇨🇳 Next.js 在国内场景下的使用分析与实践指南

作者 LeonGao
2025年10月25日 11:31

一、前言:从“Hello World”到“你好,延迟”

当我们第一次运行 npx create-next-app,心中闪过的不仅是期待,还有一种在国内运行国外框架的勇气

Next.js 就像那位旅居海外的天才诗人:文采斐然,思想先进,但落地到国内网络环境时,时常因为“回不来”而丢了一行韵脚。

于是,本文将带你回到代码和底层原理的层层结构中,分析 Next.js 如何在 中国网络与业务场景下 优雅地生存。


二、Next.js 是个什么“角色”?

用一句话:Next.js 是 React 世界的“服务端骑士”。

在传统的 React 应用中,浏览器负责渲染,这一路从客户端加载、执行、展示,堪比独自爬长城。但 Next.js 出场后,部分工作被放回到服务器端完成,生成已经“预烤好”的 HTML——这样用户加载时速度极快。

Next.js 的三种渲染模式是它在不同场景下的招数:

模式 特点 适合场景
静态生成(SSG) 构建时生成 HTML 博客、文档类网站
服务端渲染(SSR) 每次请求都渲染 动态内容、个性化页面
客户端渲染(CSR) 前端异步加载数据 管理后台、纯前端页面

但在国内使用时,这三种模式都需要考虑一个现实问题:服务器在国内还是国外?


三、网络与部署的“玄学”

在国内使用 Next.js 最大的挑战往往不是代码,而是流量的跨境延迟与资源托管问题

1. 若服务器在国外

用户访问国内网站,但要加载国外服务器的页面。这时网页就像跨洋通信:

  • 延迟高
  • 静态资源被墙
  • 服务端渲染时间拉长

你看到的不是“白屏时间”,而是“国际沉默”。

2. 若部署在国内

则问题缓解许多,但随之而来的新问题是 —— Node.js 服务端渲染的资源消耗。SSR 模式会在每次请求时唤醒服务器的“脑袋”去计算页面渲染结果,流量高峰时 CPU 直逼天花板。

国内主流云厂商的推荐方案是:

  • 使用 阿里云函数计算腾讯云 SCF 部署 SSR;
  • 使用 CDN 缓存静态内容(比如图片与 SSG 页面);
  • 对接口请求进行负载均衡与流量分配。

四、依赖下载:npm install 的炼狱

在国内执行 npm install,你常常能看到玄幻场景

npm install --save next
# 一分钟……
# 十分钟……
# Repository timed out.

此时你需要明白,不是你电脑慢,也不是网络差,而是 npm 默认仓库在太平洋的另一端。

解决方案显然是使用国内镜像源:

npm config set registry https://registry.npmmirror.com

或者直接用 pnpm + 国内源:

pnpm set registry https://registry.npmmirror.com
pnpm install next react react-dom

如果想在根本上解决依赖地狱问题,可以考虑构建内部私有 npm 仓库(如 Verdaccio),让 Team 的依赖永远不出境。


五、渲染模式选择建议

业务类型 推荐渲染模式 理由
企业官网/营销页 SSG + CDN 缓存 访问量高但更新频率低
中后台系统 CSR 安全要求高、数据实时交互
商品详情页 SSR + 缓存 内容动态且需 SEO
CMS / 博客系统 ISR(增量静态再生成) 更新灵活、流量友好

ISR 是 Next.js 的神之一笔 —— 它能在运行时增量生成页面,就像咖啡机自动补充库存,不打扰现有用户体验。


六、示例:国内优化思路

一个典型的国内优化配置实例(示例代码仅展示思路):

// next.config.js
const isProd = process.env.NODE_ENV === 'production';

module.exports = {
  reactStrictMode: true,
  output: 'standalone',
  images: {
    domains: ['cdn.yourdomain.cn'], // 走国内 CDN
  },
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'Cache-Control', value: 'public, max-age=3600' }
        ],
      },
    ];
  },
  assetPrefix: isProd ? 'https://cdn.yourdomain.cn' : '',
  compiler: {
    removeConsole: isProd,
  },
};

小技巧:

  • assetPrefix 可将静态文件托管到国内 CDN;
  • output: 'standalone' 方便在云函数环境(如阿里云 FC)下快速部署;
  • 合理地剥离 .next/cache 并利用增量渲染减少重建时间。

七、SEO 与国际搜索隔离

中国的搜索引擎(如百度、360)对动态内容的识别相对弱,而 SSR 的优势在此凸显。

  • 若你面向中文受众,SSR 模式几乎必选;
  • 若你只做管理系统,可完全抛弃 SEO,采用 CSR。

不过要注意:Next.js SSR 的请求性能在 Node 环境下并不及 Go 或 Rust,那些语言在渲染速度上压你三倍不止。


八、总结:让 Next.js 成为“本地化居民”

归根结底,Next.js 在国内使用的关键是“本地化”:

  1. 流量走国内 CDN,映射静态资源;
  2. 依赖切向国内镜像,解决安装问题;
  3. 渲染策略分层,不同页面使用不同模式;
  4. 部署方式函数化,利用 Serverless 优化弹性;
  5. 缓存优先,用时间换算力。

这就像让一位外籍诗人学会中文俳句,虽然路途坎坷,但结果动人。


九、后记:

在这个 AI 全栈时代,Next.js 的意义不仅在于页面渲染,更在于它让“前端”这个概念重新与“服务器”对话。

它提醒我们,现代前端工程师,不只是写界面的人,而是构建体验的诗人。

Tenorshare 4DDiG(数据恢复软件) 最新版

作者 非凡ghost
2025年10月25日 10:57

Tenorshare 4DDiG 是一款专业的数据恢复软件,旨在帮助用户从各种存储设备中恢复丢失或误删除的数据。无论是因为误操作、格式化、系统崩溃还是病毒感染导致的数据丢失,4DDiG都能提供有效的解决方案。它支持多种文件类型和存储介质,包括硬盘驱动器、USB闪存盘、SD卡、数码相机等。

软件功能

全面的数据恢复:可以从计算机硬盘、外部硬盘、USB驱动器、SD卡、数码相机等多种设备中恢复丢失的照片、视频、文档、音频文件等。
深度扫描与快速扫描:提供深度扫描模式以找回更多难以恢复的文件,同时也支持快速扫描来迅速定位最近删除的文件。
分区恢复:支持恢复因分区损坏或丢失而无法访问的数据。
格式化恢复:能够从被格式化的磁盘或分区中恢复数据。
原始文件恢复:即使没有文件系统信息,也可以通过识别文件签名来恢复数据。
预览与选择性恢复:在正式恢复之前可以预览找到的文件,并选择需要恢复的具体文件,避免不必要的数据覆盖。

软件特点

高成功率:采用先进的算法和技术提高数据恢复的成功率。
用户友好的界面:设计简洁直观的操作界面,使得即使是技术新手也能轻松使用。
多语言支持:支持包括中文在内的多种语言,方便全球用户使用。
高效处理:优化了扫描和恢复过程,提高了处理速度,减少了等待时间。
安全可靠:保证数据恢复过程中不会对原始数据造成二次损害。
实时更新:定期更新以适应最新的操作系统和技术变化,确保最佳兼容性和性能。

「Tenorshare 4DDiG(数据恢复软件) v10.6.1.1 最新版」 链接:pan.quark.cn/s/3195b13c7…

深入理解JavaScript变量声明:var、let与const的全面解析

作者 www_stdio
2025年10月25日 10:47

深入理解JavaScript变量声明:var、let与const的全面解析

前言

在JavaScript的学习和使用过程中,变量声明是最基础也是最重要的概念之一。从最初的var到ES6引入的letconst,JavaScript的变量声明机制经历了重要演进。本文将深入探讨这三种声明方式的特性、区别以及最佳实践,帮助开发者避免常见的陷阱。

一、var声明:灵活但充满陷阱

1.1 var的基本用法

var是JavaScript中最原始的变量声明方式,其基本语法非常简单:

var a = 1;
var name = "JavaScript";
var isValid = true;

1.2 变量提升(Hoisting)

var声明最特殊的特性就是变量提升。这意味着不管在作用域的哪个位置使用var声明变量,这个声明都会被提升到作用域的顶部。

console.log(a); // 输出:undefined,而不是报错
var a = 1;
console.log(a); // 输出:1

上述代码在JavaScript引擎中的实际执行顺序是这样的:

var a; // 声明提升到顶部,初始值为undefined
console.log(a); // undefined
a = 1; // 赋值操作保留在原地
console.log(a); // 1

1.3 var的作用域问题

var声明的变量只有函数作用域,没有块级作用域,这经常导致不符合直觉的行为:

for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 三次都输出3
    }, 100);
}
console.log(i); // 输出3,变量i在循环外仍然可访问

这种特性在循环和条件语句中经常引发问题,因为变量会泄露到外部作用域。

二、let声明:块级作用域的解决方案

2.1 let的基本用法

ES6引入的let提供了块级作用域,解决了var的许多问题:

let a = 1;
let name = "JavaScript";

2.2 块级作用域

let声明的变量只在当前的代码块内有效:

for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 依次输出0, 1, 2
    }, 100);
}
console.log(i); // ReferenceError: i is not defined

在条件语句中也是如此:

if (true) {
    let message = "Hello";
    console.log(message); // 输出"Hello"
}
console.log(message); // ReferenceError: message is not defined

2.3 暂时性死区(Temporal Dead Zone)

let声明的变量存在"暂时性死区",在声明之前访问变量会报错:

console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 1;

这与var的变量提升形成鲜明对比,使得代码更加可预测。

三、const声明:不可变的绑定

3.1 const的基本用法

const用于声明常量,一旦赋值就不能重新赋值:

const PI = 3.14159;
const API_URL = "https://api.example.com";

3.2 const的不变性

const创建的是不可变的绑定,而不是不可变的值:

const person = {
    name: "John",
    age: 30
};

person.age = 31; // 这是允许的,修改对象属性
console.log(person.age); // 输出31

person = { name: "Jane" }; // TypeError: Assignment to constant variable

对于基本数据类型,const确实创建了不可变的值:

const MAX_SIZE = 100;
MAX_SIZE = 200; // TypeError: Assignment to constant variable

3.3 const的块级作用域

let一样,const也具有块级作用域和暂时性死区的特性。

四、三种声明方式的对比

4.1 特性对比表

特性 var let const
作用域 函数作用域 块级作用域 块级作用域
变量提升 否(存在TDZ) 否(存在TDZ)
重复声明 允许 不允许 不允许
全局对象属性
是否需要初始值

4.2 错误类型分析

在实际开发中,理解不同声明方式导致的错误类型很重要:

// ReferenceError: height is not defined
// 在作用域外访问变量
function test() {
    let height = 100;
}
console.log(height); // ReferenceError

// TypeError: Assignment to constant variable
// 尝试修改const声明的变量
const PI = 3.14;
PI = 3.14159; // TypeError

// ReferenceError: Cannot access 'PI' before initialization
// 在暂时性死区内访问变量
console.log(PI); // ReferenceError
const PI = 3.14;

五、最佳实践和建议

5.1 现代JavaScript的声明策略

  1. 默认使用const

    // 好的做法
    const user = getUser();
    const items = [];
    const config = { ... };
    
    // 只有在确实需要重新赋值时才使用let
    let count = 0;
    count = count + 1;
    
  2. 避免使用var 在现代JavaScript开发中,应该尽量避免使用var,除非有特殊的兼容性需求。

  3. 命名约定

    // 常量使用全大写
    const MAX_USERS = 100;
    const API_BASE_URL = "https://api.example.com";
    
    // 普通变量使用驼峰命名
    const userName = "John Doe";
    let itemCount = 0;
    

5.2 实际应用场景

// 函数内的变量声明
function processUserData(userId) {
    const user = getUserById(userId); // 使用const,因为user不会重新赋值
    let isValid = false; // 使用let,因为验证状态可能会改变
    
    if (user && user.age >= 18) {
        isValid = true;
    }
    
    return isValid;
}

// 循环中的变量声明
for (let i = 0; i < items.length; i++) { // 使用let,每次迭代都有新的绑定
    const item = items[i]; // 使用const,当前项不会改变
    processItem(item);
}

// 异步操作中的变量声明
async function fetchData() {
    const response = await fetch('/api/data'); // 使用const
    const data = await response.json(); // 使用const
    
    return data;
}

六、深入理解作用域和闭包

6.1 词法作用域

JavaScript采用词法作用域,意味着变量的作用域在代码书写时就已经确定:

function outer() {
    const outerVar = "I'm outside";
    
    function inner() {
        console.log(outerVar); // 可以访问外部变量
    }
    
    return inner;
}

const innerFunc = outer();
innerFunc(); // 输出:"I'm outside"

6.2 闭包的现代用法

结合letconst,可以更好地控制闭包行为:

function createCounter() {
    let count = 0; // 使用let,因为count需要改变
    
    return {
        increment: () => {
            count++;
            return count;
        },
        getValue: () => count
    };
}

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

七、常见陷阱和解决方案

7.1 循环中的闭包问题

问题代码:

for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 输出3次3
    }, 100);
}

解决方案:

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

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

7.2 条件声明的问题

// 不好的做法
if (condition) {
    var result = "success";
} else {
    var result = "failure";
}

// 好的做法
let result;
if (condition) {
    result = "success";
} else {
    result = "failure";
}

// 或者更好的做法
const result = condition ? "success" : "failure";

八、总结

JavaScript的变量声明从varletconst的演进,体现了语言设计的成熟过程。理解这三种声明方式的差异对于编写可维护、可预测的代码至关重要:

  1. 优先使用const,确保变量的不可变性,提高代码的可预测性
  2. 需要重新赋值时使用let,提供明确的变量修改意图
  3. 避免使用var,除非有特殊的兼容性需求

通过掌握这些声明方式的特性和最佳实践,开发者可以避免许多常见的JavaScript陷阱,编写出更加健壮和可维护的代码。

现代JavaScript开发已经形成了明确的最佳实践:默认使用const,需要重新赋值时使用let,尽量避免使用var。这种模式不仅使代码更加安全,也提高了代码的可读性和可维护性。

延伸学习

  • 了解JavaScript的执行上下文和变量环境
  • 深入学习作用域链和闭包机制
  • 探索模块作用域和全局作用域的管理
  • 学习TypeScript的类型声明系统

掌握变量声明只是JavaScript深入学习的第一步,但这是构建坚实基础的关键环节。

WinMute(自动锁屏静音软件) 中文绿色版

作者 非凡ghost
2025年10月25日 10:39

WinMute 是一款轻量级的应用程序,设计用于在特定事件发生时自动静音您的计算机。它特别适合那些需要在离开电脑时避免打扰他人的人群,比如在办公室环境中。该软件可以在系统锁定、屏幕保护启动或蓝牙设备断开等情况下自动将系统音量设置为静音,并在这些状态解除后恢复原始音量。

软件功能

自动静音触发:当检测到预设的系统事件(如锁屏、屏保激活、蓝牙设备断开等)时,WinMute 会自动将系统的音量静音。
智能触发条件:支持多种触发条件,包括但不限于屏幕保护启动、工作站锁定、设备状态变化等。
音量恢复:一旦触发条件不再存在(例如解锁计算机),WinMute 可以自动恢复之前的音量设置,确保用户的使用体验不被打断。
任务模式:可以设置定时任务,在指定时间自动静音和恢复音量。
轻量级与高效性:占用硬盘空间几乎可以忽略不计,对系统资源的影响极小,保证了高效的性能表现。

软件特点

用户友好的界面:尽管功能强大,但其操作简单直观,易于用户快速上手。
高度可定制化:允许用户根据自己的需求调整各种参数,例如选择不同的触发条件和设置蓝牙/WiFi模式下的行为。
无广告干扰:WinMute 设计为无广告,确保用户体验的纯净性。
跨版本兼容性:无论是Windows 10还是其他Windows版本,WinMute都能良好运行,适应不同用户的需求。

「WinMute(自动锁屏静音软件) v2.5.3.0 中文绿色版」 链接:pan.quark.cn/s/6d069f5d0…

JavaScript数据类型

作者 w2sfot
2025年10月25日 10:29

一、原始类型

JavaScript中的原始类型是不可变的数据类型,它们直接存储在栈内存中。

1. Undefined类型

let notDefined;
console.log(notDefined); // undefined
console.log(typeof notDefined); // "undefined"

// 未声明的变量与undefined的区别
// console.log(neverDeclared); // ReferenceError

2. Null类型

let emptyValue = null;
console.log(emptyValue); // null
console.log(typeof emptyValue); // "object" (这是JavaScript的历史遗留问题)

// null与undefined的区别
console.log(null == undefined); // true
console.log(null === undefined); // false

3. Boolean类型

let isActive = true;
let isCompleted = false;

//  truthy和falsy值
console.log(Boolean('')); // false
console.log(Boolean('hello')); // true
console.log(Boolean(0)); // false
console.log(Boolean(1)); // true

4. Number类型

let integer = 42;
let float = 3.14;
let scientific = 2.5e3; // 2500
let hex = 0xFF; // 255
let binary = 0b1010; // 10
let octal = 0o744; // 484

// 特殊数值
let infinity = Infinity;
let negativeInfinity = -Infinity;
let notANumber = NaN;

console.log(typeof NaN); // "number"

5. BigInt类型

// 超过Number安全整数范围的数字
const bigNumber = 9007199254740991n;
const huge = BigInt(123456789012345678901234567890);

console.log(bigNumber + 1n); // 9007199254740992n

6. String类型

let singleQuote = 'Hello';
let doubleQuote = "World";
let templateLiteral = `Hello ${doubleQuote}`;

console.log(templateLiteral); // "Hello World"

// 字符串方法
let message = "JavaScript数据类型";
console.log(message.length); // 11
console.log(message.includes('数据类型')); // true

7. Symbol类型

const uniqueKey = Symbol('description');
const anotherKey = Symbol('description');

console.log(uniqueKey === anotherKey); // false

// 全局Symbol注册表
const globalSymbol = Symbol.for('globalKey');
const sameGlobalSymbol = Symbol.for('globalKey');
console.log(globalSymbol === sameGlobalSymbol); // true

二、引用数据类型

1.Object类型

对象是JavaScript中最复杂的数据类型,用于存储键值对集合。

// 对象字面量
let person = {
    name: '张三',
    age: 25,
    isStudent: true,
    greet: function() {
        return `你好,我是${this.name}`;
    }
};

console.log(person.name); // "张三"
console.log(person.greet()); // "你好,我是张三"

// 动态添加属性
person.country = '中国';
delete person.isStudent;

2.Array类型

数组是特殊的对象,用于存储有序的数据集合。

let numbers = [1, 2, 3, 4, 5];
let mixed = [1, 'hello', true, { name: '李四' }];

// 数组方法
numbers.push(6); // 末尾添加
numbers.pop();   // 末尾删除
numbers.unshift(0); // 开头添加
numbers.shift(); // 开头删除

// 遍历数组
numbers.forEach((item, index) => {
    console.log(`索引${index}: ${item}`);
});

3.Function类型

函数在JavaScript中也是一等公民,可以作为参数传递和返回值。

// 函数声明
function add(a, b) {
    return a + b;
}

// 函数表达式
const multiply = function(a, b) {
    return a * b;
};

// 箭头函数
const divide = (a, b) => a / b;

// 高阶函数
function createCalculator(operation) {
    return function(a, b) {
        return operation(a, b);
    };
}

安卓旧机变服务器,KSWEB部署Typecho博客并实现远程访问:cpolar内网穿透实验室第645个成功挑战

软件名称:KSWEB(安卓Web服务器)+ Termux终端模拟器 操作系统支持 安卓5.0及以上版本,适配老旧机型(如小米红米系列、华为畅享等)。 软件介绍 KSWEB是安卓端的轻量级Web服务器,

Vue3 - Reactivity的核心流程

作者 7ayl
2025年10月25日 07:11

前言

这篇文章主要简述了vue框架在模块运行时如何做到的响应式的,我先阐述一下文章中会用到的相关概念

副作用函数(Effect)  

是指那些会"依赖"响应式数据,并且当这些数据变化时需要重新执行的函数。 常见的有:模版渲染数据、计算属性、Watch、自定义 Effect

流程

创建一个响应式对象

// 表面
const a = reactive({ num: 1 })

这里这时候reactive函数会将目标对象 { num: 1 } 转换 为Proxy代理对象,

这时候reactive函数到底干了什么?

image.png

看图可知,这时候 reactive 函数给 createReactiveObject 函数传了对象,以及用于普通对象的可变响应式代理的处理器 mutableHandlers,

而这整一个过程主要是对象和baseHandlers(处理器)传给Proxy类,创建了相应的Proxy代理对象

那么这个处理器给proxy带来了什么方法呢?

image.png 在baseHandlers中调用了createGetter和createSetter分别创建了代理对象的get和set函数,

get函数的作用是返回当前需要的值以及收集依赖,

set函数是用来设置并返回对象更新的值的,以及触发依赖

对象数据变动

let dummy // 用来创建副作用函数的相关变量

// 表面
const a = reactive({ num: 1 })

// 手动创建的副作用函数
effect(() => {
  console.log(`num 发生变化: ${a.num}`)
  dummy = a.num
})

// 会触发 Effect 执行
a.num++ 

响应式数据变更之前的准备操作

创建副作用函数(fn),它的内部需要访问num的,这时就会触发get中的依赖收集,

fn就会被传入给ReactiveEffect类创建出一个_effect实例【用来注册响应数据变化时需要执行的函数】,并用实例的方法run执行fn,

image.png 而run内部还会将这次创建的_effect实例赋值给全局的activeEffect,方便代理对象收集依赖 image.png 然后get操作会进行依赖收集,触发track操作 image.png track操作用来找寻代理对象(a)的依赖合集,再通过代理对象的依赖合集找到对应的key(num)的依赖合集,并将当前的 activeEffect 添加到对应的对象的dep容器里去,完成依赖收集 image.png

响应式数据改变时

如果这时候对象的数据发生变更,那么会触发set操作,继而用trigger()触发依赖,即执行存在key的dep里的的依赖,即num的依赖

image.png

image.png

实践

image.png 第一次初始化的时候执行副作用函数,第二次trigger触发依赖执行副作用函数

JavaScript变量声明:var、let、const的完整指南

作者 Yira
2025年10月25日 00:19

JavaScript变量声明:var、let、const的完整指南

第一章 JavaScript变量声明的发展历程

1.1 从var到let/const的演进

JavaScript作为一门动态类型语言,其变量声明机制经历了重要的演进过程。在ES6(ECMAScript 2015)之前,开发者只能使用var关键字来声明变量。然而,var存在一些设计上的缺陷,这些缺陷常常导致不符合直觉的行为和难以调试的bug。

// ES5及之前的变量声明方式
var name = "张三";
var age = 25;

随着ES6的发布,JavaScript引入了letconst两种新的变量声明方式,为开发者提供了更精确的变量作用域控制和更严格的变量管理机制。

第二章 var关键字:历史遗留问题分析

2.1 变量提升(Hoisting)机制

var最著名的特性就是变量提升,这也是它最不符合直觉的地方。理解变量提升需要了解JavaScript代码的执行过程:

console.log(a); // 输出:undefined,而不是报错
var a = 1;
console.log(a); // 输出:1

代码执行的两个阶段

  1. 编译阶段:JavaScript引擎会检测语法错误,并将所有var声明的变量提升到作用域顶部
  2. 执行阶段:按照代码顺序逐行执行

实际上,上面的代码相当于:

var a; // 变量声明被提升到作用域顶部
console.log(a); // 此时a已声明但未赋值,值为undefined
a = 1; // 变量赋值
console.log(a); // 输出1

2.2 var的作用域问题

var声明的变量具有函数作用域,而不是块级作用域,这常常导致意外的行为:

function example() {
    if (true) {
        var x = 10;
    }
    console.log(x); // 输出10,变量x在if块外仍然可访问
}
example();
console.log(x); // ReferenceError: x is not defined

第三章 let关键字:现代变量声明方式

3.1 块级作用域

let引入了真正的块级作用域,解决了var的作用域问题:

function letDemo() {
    if (true) {
        let y = 20;
        console.log(y); // 输出20
    }
    console.log(y); // ReferenceError: y is not defined
}

3.2 暂时性死区(Temporal Dead Zone)

let声明的变量存在暂时性死区,从块开始到变量声明之间的区域无法访问该变量:

console.log(z); // ReferenceError: Cannot access 'z' before initialization
let z = 30;

这与var的变量提升形成鲜明对比,这种设计有助于在开发阶段发现潜在的错误。

第四章 const关键字:常量声明

4.1 不可重新赋值

const用于声明常量,其值在声明后不能被重新赋值:

const PI = 3.14159;
PI = 3.14; // TypeError: Assignment to constant variable

4.2 const与对象和数组

需要注意的是,const保证的是变量引用的不变性,而不是值的不变性:

const person = { name: "李四", age: 30 };
person.age = 31; // 这是允许的,修改对象属性
console.log(person); // { name: "李四", age: 31 }

person = { name: "王五" }; // TypeError: Assignment to constant variable

const numbers = [1, 2, 3];
numbers.push(4); // 允许,修改数组内容
numbers = [5, 6, 7]; // TypeError: Assignment to constant variable

第五章 JavaScript错误类型详解

5.1 ReferenceError(引用错误)

ReferenceError: height is not defined

  • 原因:尝试访问未声明的变量
  • 解决方案:确保变量在使用前已正确声明
// 错误示例
console.log(height); // ReferenceError: height is not defined

// 正确做法
let height = 180;
console.log(height); // 输出180

ReferenceError: Cannot access 'PI' before initialization

  • 原因:在暂时性死区内访问let/const声明的变量
  • 解决方案:确保在变量声明后使用
// 错误示例
console.log(PI); // ReferenceError: Cannot access 'PI' before initialization
const PI = 3.14159;

// 正确做法
const PI = 3.14159;
console.log(PI); // 输出3.14159

5.2 TypeError(类型错误)

TypeError: Assignment to constant variable

  • 原因:尝试给const声明的常量重新赋值
  • 解决方案:使用let声明需要重新赋值的变量
// 错误示例
const MAX_SIZE = 100;
MAX_SIZE = 200; // TypeError: Assignment to constant variable

// 正确做法
let maxSize = 100;
maxSize = 200; // 允许重新赋值

第六章 三种声明方式的对比与选择

6.1 特性对比表

特性 var let const
作用域 函数作用域 块级作用域 块级作用域
变量提升 否(存在TDZ) 否(存在TDZ)
重复声明 允许 不允许 不允许
重新赋值 允许 允许 不允许

6.2 使用场景建议

优先使用const

  • 声明不会被重新赋值的变量
  • 导入的模块、配置常量等
const API_BASE_URL = 'https://api.example.com';
const CONFIG = { timeout: 5000 };

其次使用let

  • 需要重新赋值的变量
  • 循环计数器、临时变量等
for (let i = 0; i < 10; i++) {
    console.log(i);
}

let isLoading = true;
// ...后续操作
isLoading = false;

避免使用var

  • 在新的项目中尽量避免使用var
  • 只在维护遗留代码时使用

第七章 实际开发中的最佳实践

7.1 变量声明规范

// 好的实践
const DEFAULT_SETTINGS = {
    theme: 'dark',
    language: 'zh-CN'
};

let userPreferences = { ...DEFAULT_SETTINGS };

function updatePreferences(newPrefs) {
    userPreferences = { ...userPreferences, ...newPrefs };
}

// 避免的做法
var count = 0; // 使用let代替
var MAX_COUNT = 100; // 使用const代替

7.2 作用域管理技巧

// 使用IIFE创建私有作用域(ES6之前的方式)
(function() {
    var privateVar = 'secret';
    // 私有变量不会污染全局作用域
})();

// ES6使用块级作用域
{
    let privateVar = 'secret';
    // 块结束后privateVar自动销毁
}

第八章 常见陷阱与调试技巧

8.1 闭包中的变量捕获

// 经典的闭包问题
for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 输出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);
}

8.2 严格模式下的行为差异

'use strict';

// 严格模式下,未声明的变量赋值会报错
undeclaredVar = 10; // ReferenceError

// 非严格模式下,这会创建全局变量(应避免)

第九章 现代JavaScript开发规范

9.1 ESLint规则配置

在现代JavaScript项目中,通常使用ESLint来强制变量声明的最佳实践:

{
    "rules": {
        "no-var": "error",
        "prefer-const": "error",
        "no-undef": "error"
    }
}

9.2 TypeScript中的变量声明

在TypeScript中,变量声明还包含类型注解:

const name: string = "张三";
let age: number = 25;
let isActive: boolean = true;

第十章 总结

JavaScript的变量声明机制从varlet/const的演进,体现了语言设计的成熟和对开发者友好性的提升。理解这三种声明方式的差异,对于编写可维护、可预测的JavaScript代码至关重要。

核心要点总结

  1. 优先使用const,其次使用let,避免使用var
  2. 理解变量提升和暂时性死区的概念
  3. 掌握块级作用域与函数作用域的区别
  4. 熟悉常见的错误类型及其解决方案
  5. 在实际开发中遵循现代JavaScript的最佳实践

通过合理运用letconst,开发者可以写出更加健壮、易于维护的代码,减少因变量作用域和重复声明导致的潜在bug。这种对变量声明方式的精细控制,是现代JavaScript开发的重要基础。

**

JS 变量那些坑:从 var 到 let/const 的终极解密

作者 gustt
2025年10月24日 22:39

JavaScript 深度解析varletconst变量声明

在 JavaScript 的学习过程中,变量声明是最基础却也是最容易踩坑的知识点。尤其是当你从传统的 Java/C++ 等语言背景过渡到 JavaScript 时,varletconst 的行为差异可能让你一头雾水。本文将结合示例和原理,带你深入理解 JavaScript 中变量的声明与作用域规则,并总结最佳实践。


1. 早期的 JavaScript 与 var

在 ES5(ECMAScript 5,即 2011 年标准)之前,JavaScript 只有 var 一种声明变量的方式。var 看似简单,但隐藏了很多坑。有些老师叫你直接弃用 var 转而使用 let ,但是并没有告诉你为什么,现在我来解释解释。

var age = 18; // js 是弱类型语言,变量的类型由值决定
age++;
var PI = 3.1415926; // 变量名大写通常表示常量
console.log(age); // 19

注意:虽然我们用大写 PI 表示“常量”,但是 var 并不能阻止你修改它。

PI = 3.14;
console.log(PI); // 3.14

这意味着,使用 var 时,变量既可以被重复声明,也可以被重新赋值,这在大型项目中很容易引起混乱。


1.1 var 的变量提升(Hoisting)

var 的最大特点之一就是变量提升。即便你在声明之前使用变量,代码也不会报 ReferenceError,而是返回 undefined

console.log(age); // undefined
var age = 18;

这里的执行顺序可以理解为:

  1. 编译阶段:JavaScript 引擎把所有 var 声明提前(提升)到函数或全局的最顶端,但不会赋值。
  2. 执行阶段:按原代码顺序执行,进行赋值。

这种机制虽然允许代码运行,但会降低代码可读性,容易导致 BUG。在现代开发中,建议尽量避免使用 var


1.2 var 的作用域问题

var函数作用域,不支持块级作用域。这意味着它在大括号 {} 内部声明时,依然属于外层函数或全局作用域:

{
    var age = 18;
}
console.log(age); // 18,仍然能访问

在复杂逻辑中,这种行为会让变量名冲突和状态管理变得非常麻烦。


2. ES6 引入的 letconst

从 ES6(2015 年)开始,JavaScript 引入了 letconst,提供了块级作用域和常量声明,从根本上解决了 var 带来的诸多问题。

let height = 188;
height++;
console.log(height); // 189

const key = 'abc123';
key = 'abc234'; // TypeError: Assignment to constant variable.

2.1 块级作用域

letconst 支持块级作用域,即变量只在 {} 内有效:

{
    let height = 188;
    const PI = 3.141592;
}

console.log(height); // ReferenceError
console.log(PI);     // ReferenceError

这与 var 的行为截然不同,能有效避免变量污染全局的问题。


2.2 暂时性死区(Temporal Dead Zone, TDZ)

letconst 有一个重要特点:暂时性死区。在变量声明之前访问,会报错 ReferenceError暂时性死区(Temporal Dead Zone, TDZ) 是指:

当你用 letconst 声明一个变量时,从代码块开始变量声明的位置之间的这段区域,变量处于一种“已声明但不可访问”的状态。如果在这段区域内访问变量,就会抛出 ReferenceError

换句话说:

  • 变量在编译阶段就已经知道存在,但在声明之前不能访问。
  • 直到变量真正执行声明(并赋值)之后,才能正常使用。
// 实例1.
const foo = () => {
    console.log('///')
    console.log(a)
    let a = 3;
}

foo()  //ReferenceError

或者

// 实例2.
const foo = () => {
    let a;
    console.log('///')
    console.log(a)
    a = 3;
}
foo();  //undefined

letconst可以说会变量提升但是被 TDZ 阻挡了访问,看起来就像没有提升

这种机制避免了 var 的变量提升带来的困扰,让代码更安全、更易读。


2.3 const 的使用注意点

const 声明的变量不能被重新赋值(简单数据类型):

简单数据类型包括:

  • Number
  • String
  • Boolean
  • undefined
  • null
  • Symbol(ES6 新增)
const PI = 3.1415926;
PI = 3.14; // TypeError

但是,const对象和数组是浅冻结的:

const person = { name: "gg", age: 20 };
person.age = 21; // 可以修改对象属性
console.log(person); // { name: "gg", age: 21 }

const wes = Object.freeze(person); // 深冻结对象
wes.age = 17; // 无效
console.log(wes); // { name: "gg", age: 21 }

简单类型(number、string、boolean)的 const 完全不可变,而复杂类型(对象、数组)引用地址不可变,但属性可变。用 Object.freeze 可以实现深层不可变。


3. 函数与变量提升的差异

在 JavaScript 中,函数也是一等公民,可以像变量一样被提升,但提升行为与 var 不完全相同。

setWidth(); // 运行正常

function setWidth() {
    var width = 100;
    console.log(width); // 100
}

// console.log(width); // ReferenceError

特点总结:

  • 函数声明会连同函数体一起提升。
  • var 声明的变量只会提升声明,不会提升赋值。
  • 函数内部使用 varletconst,遵循各自的作用域规则。

因此,函数提升可以在调用前使用函数,而 var 变量提升只能得到 undefined


4. 总结 varletconst

特性 var let const
作用域 函数作用域 块级作用域 块级作用域
变量提升 是(赋值未提升) 是,但有 TDZ 是,但有 TDZ
可重复声明
可修改值 否(对象属性可改,但引用不可改)

4.1 开发实践建议

  1. 不再使用 var
    let + const 结合使用,保证代码安全和可读性。
  2. 默认使用 const
    只有在需要重新赋值时才使用 let
  3. 对象或数组尽量用 Object.freeze
    保证数据不可变,避免副作用。
  4. 理解 TDZ 与作用域
    有助于减少 ReferenceError,让调试更简单。

5. 常见错误集合

  • ReferenceError: height is not defined
    访问未声明的变量。
  • TypeError: Assignment to constant variable.
    const 变量重新赋值。
  • ReferenceError: Cannot access 'PI' before initialization
    暂时性死区内访问 letconst 变量。

理解这些错误,能够帮助你快速定位问题,并加深对作用域和提升机制的理解。


6. 代码示例总结

6.1 块级作用域

块级作用域只在大括号 {} 内有效,并且主要影响 letconst

  • 普通块 {}
  • if / else
  • switch case
  • try / catch
  • for / while 本身不是块级作用域,它们只是语句。
  • var 只遵循函数作用域(或全局作用域),忽略块级作用域
{
    var age = 18;    // 不支持块级作用域或忽略块级作用域
    let height = 188;  // 支持块级作用域
}
console.log(age);    // 18
console.log(height); // ReferenceError

6.2 对象常量

const person = { name: "gg", age: 20 };
person.age = 21; // 可修改属性
console.log(person);

const wes = Object.freeze(person);
wes.age = 17; // 无效
console.log(wes);

6.3 函数提升

setWidth(); // 可以调用

function setWidth() {
    var width = 100;
    console.log(width); // 100
}
// console.log(width); // ReferenceError

7. 总结

  1. var 适合老旧代码维护,现代开发不推荐使用。
  2. let 提供块级作用域,解决 var 的变量提升问题。
  3. const 用于不可变值或引用,结合 Object.freeze 可实现深冻结。
  4. 理解 TDZ 和提升机制,能够避免常见的 ReferenceErrorTypeError
  5. 在企业级开发中,合理使用 letconst 能显著提升代码质量和可维护性。

【鸿蒙】【毛豆工具集】【图片】【编缉】图片增加水印(通过组件的Overlay方法增加水印)

作者 俩毛豆
2025年10月24日 22:19
对图片增加水印经常在分享,版权保护等场景使用,在鸿蒙系统中提供了多种方,本篇介绍通过组件的Overlay方法增加水印的方法,是对组件进行的水印操作。代码如下,主要分为两步 第一步,实现增加水印的组件,

从零学 JavaScript:彻底搞懂 var、let、const(上篇)——告别变量提升的坑

作者 Zyx2007
2025年10月24日 22:05

前言:为什么你必须重新理解 JavaScript 的变量声明?

你是否曾遇到过这样的困惑?

console.log(age); // 输出 undefined,而不是报错?
var age = 18;

console.log(height); // 直接报错!
let height = 1.88;

明明都是声明变量,为什么 varlet 的行为完全不同?
为什么 const 声明的“常量”对象,属性还能改?

如果你也曾被这些问题困扰,那么恭喜你,你即将揭开 JavaScript 中最经典、最易错的一块拼图:varletconst 的本质区别


一、早期 JavaScript:var 的“糟粕”时代

在 ES6(2015 年)之前,JavaScript 只有一个关键字来声明变量:var

var age = 18;

var 的设计存在严重问题,最典型的就是 变量提升(Hoisting)

什么是变量提升?

JavaScript 的执行分为两个阶段:

  1. 编译阶段:检查语法,提升 var 和函数声明
  2. 执行阶段:逐行执行代码

var 声明的变量会在编译阶段就被提升到作用域顶部,但赋值仍然留在原地。

实例说明:

console.log(age); // 输出:undefined
var age = 18;
console.log(age); // 输出:18

这段代码的实际执行过程是:

// 编译阶段:var age 被提升
var age; // 此时 age = undefined

// 执行阶段:
console.log(age); // undefined
age = 18;
console.log(age); // 18

这种行为完全违背直觉:还没声明就能访问,但值却是 undefined


⚠️ 二、var 的三大“坏味道”

1. 变量提升导致逻辑混乱

function example() {
  console.log(a); // undefined
  var a = 10;
}

你本意是先声明再使用,但 JavaScript 却允许你在声明前访问,只是值为 undefined。这极易导致 bug。

2. 不支持块级作用域

if (true) {
  var age = 18;
}
console.log(age); // 18!竟然能访问到

iffor 等块级作用域中声明的 var 变量,会泄露到外部作用域。这在大型项目中非常危险。

3. 没有常量的概念

var PI = 3.1415926;
PI = 3.14; // 竟然可以修改!

虽然开发者约定“大写字母命名表示常量”,但 JavaScript 本身不强制,容易被误改。


三、ES6 的救赎:letconst

2015 年,ES6 正式发布,带来了 letconst,彻底解决了 var 的问题。

let:现代变量声明的首选

let height = 1.88;
height++; // 可以修改

let 的特点:

  • 不提升:不能在声明前访问
  • 支持块级作用域:只在 {} 内有效
  • 不允许重复声明

实例对比:

// var 的问题
console.log(age); // undefined
var age = 18;

// let 的正确行为
console.log(height); // 报错!Cannot access 'height' before initialization
let height = 1.88;

const:真正的常量

const PI = 3.1415926;
PI = 3.14; // 报错!Assignment to constant variable.

const 声明的变量一旦赋值就不能再修改,否则会抛出 TypeError

注意:const 保证的是引用地址不变,而不是值不可变。


四、常见的错误与报错解析

1. ReferenceError: height is not defined

console.log(height); // 报错

原因:变量 height 根本不存在,或在作用域外访问。

2. TypeError: Assignment to constant variable.

const PI = 3.14;
PI = 3.15; // 报错

原因:尝试修改 const 声明的变量。

3. ReferenceError: Cannot access 'PI' before initialization

console.log(PI);
const PI = 3.14; // 报错

原因:constlet 存在暂时性死区(Temporal Dead Zone),不能在声明前访问。


五、暂时性死区:letconst 的安全机制

“暂时性死区”听起来很玄乎,其实很简单:

从进入作用域到变量声明完成之前,该变量都不可访问。

示例:

{
  console.log(a); // 报错!TDZ
  let a = 10;
}

这与 var 的“提升但值为 undefined”完全不同,它强制你先声明再使用,提高了代码的可读性和安全性。


六、最佳实践建议

场景 推荐使用
普通变量 let
不会重新赋值的变量 const
不再使用 var

建议:从今天起,彻底告别 var,全面使用 letconst


JavaScript 正在变得越来越“严谨”

var 是 JavaScript 早期设计不完善的产物,而 letconst 的出现,标志着 JavaScript 正在向更严谨、更适合大型项目开发的语言演进。

理解它们的区别,不仅能避免常见 bug,更能写出更清晰、更安全的代码。

❌
❌