阅读视图
俄“进步MS-31”货运飞船与国际空间站对接
时隔两年 三大运营商重启eSIM业务
日本鹿儿岛县近海发生4.8级地震
2025暑期档总票房破25亿
我国在建最大火电厂新机组投产 单台年发电量60亿度
安普瑞斯74张3C证书被暂停或撤销
日本鹿儿岛县附近海域发生3.7级地震
东方电气:获中东客户数亿元沙漠钻机订单
深圳口岸上半年出入境1.3亿人次 同比增长16.2%
马斯克宣布成立“美国党” 后回应“参选时间”:明年
国家发展改革委与巴西有关部门签署合作文件
四叉树:二维空间的 “智能分区管理员”
想象一下,你手里有一张巨大的城市地图,上面密密麻麻地分布着十万个路灯。现在老板突然让你找出某条小巷里的三个路灯 —— 如果像翻字典一样逐个排查,恐怕下班前都完不成任务。但如果这张地图早被划分成了街道片区,每个片区又细分出街区,街区再分成小巷,你就能像剥洋葱一样层层定位,这就是四叉树的核心智慧。
从像素格子到数学魔法
在计算机图形学的世界里,二维空间就像一块等待切割的披萨。四叉树这位 “披萨大师” 有个怪癖:每次都要把当前区域切成大小相等的四份 —— 左上、右上、左下、右下,如同给正方形蛋糕划十字刀。这种划分不是随机的,而是遵循着简单又精妙的规则:当某个区域里的 “居民”(可以是点、图形或像素)数量超过阈值,就必须分家。
让我们用坐标系理解这个过程。假设初始空间是一个从 x0 到 x1、y0 到 y1 的正方形,就像一个边长为 100 的方盒子。当里面的点超过 4 个时,四叉树会找出这个正方形的中心 ——x 中点是(x0 加 x1)除以 2,y 中点同理。这两条中线如同魔术师的魔杖,瞬间把一个大盒子变成四个小盒子,每个孩子盒子的边长都是原来的一半。
这种划分会递归进行,直到每个小盒子里的 “居民” 数量都小于等于设定的阈值。就像俄罗斯套娃,每个盒子里可能藏着更小的盒子,也可能直接住着几个 “居民”。
JavaScript 中的四叉树实现
让我们用代码给这位 “分区管理员” 编写工作手册。下面的 JavaScript 类就像四叉树的身份证,记录着它的管辖范围和家庭成员:
class Quadtree {
// 构造函数:初始化一个区域
constructor(x, y, width, height, capacity) {
this.x = x; // 区域左上角x坐标
this.y = y; // 区域左上角y坐标
this.width = width; // 区域宽度
this.height = height; // 区域高度
this.capacity = capacity; // 最大容纳数量
this.points = []; // 当前区域的居民
this.children = null; // 四个子区域(初始为空)
}
// 划分区域:生四个"孩子"
subdivide() {
const halfW = this.width / 2;
const halfH = this.height / 2;
// 左上孩子
this.children = [
new Quadtree(this.x, this.y, halfW, halfH, this.capacity),
// 右上孩子
new Quadtree(this.x + halfW, this.y, halfW, halfH, this.capacity),
// 左下孩子
new Quadtree(this.x, this.y + halfH, halfW, halfH, this.capacity),
// 右下孩子
new Quadtree(this.x + halfW, this.y + halfH, halfW, halfH, this.capacity)
];
}
// 插入新居民
insert(point) {
// 如果点不在当前区域,直接拒绝
if (!this.contains(point)) return false;
// 还有空位且没生孩子,直接入住
if (this.points.length < this.capacity && !this.children) {
this.points.push(point);
return true;
}
// 人满为患,赶紧分家
if (!this.children) this.subdivide();
// 让四个孩子决定谁接收这个新居民
for (let child of this.children) {
if (child.insert(point)) return true;
}
return false; // 理论上不会走到这步
}
// 检查点是否在当前区域内
contains(point) {
return (point.x >= this.x &&
point.x <= this.x + this.width &&
point.y >= this.y &&
point.y <= this.y + this.height);
}
// 查找区域内的所有居民
query(range, found) {
// 如果当前区域和查询范围不搭界,直接返回
if (!this.intersects(range)) return found;
// 收集当前区域里的居民
for (let p of this.points) {
if (range.contains(p)) {
found.push(p);
}
}
// 让孩子们也交出符合条件的居民
if (this.children) {
for (let child of this.children) {
child.query(range, found);
}
}
return found;
}
// 检查两个区域是否重叠
intersects(range) {
return !(this.x > range.x + range.width ||
this.x + this.width < range.x ||
this.y > range.y + range.height ||
this.y + this.height < range.y);
}
}
像侦探一样高效搜索
假设我们要在游戏地图中检测碰撞 —— 比如找出玩家角色周围 50 像素内的所有敌人。如果遍历整个地图的 1000 个角色,每次检测都要做 1000 次计算;而有了四叉树,我们只需:
- 找到玩家所在的最小区域
- 检查相邻的几个兄弟区域
- 最多只需查询几十个角色
这种效率提升在图形渲染中更明显。当你缩放地图时,远处的细节不需要渲染 —— 四叉树会告诉你:“这个区域太小了,里面的东西合并成一个点就行”,就像地图上远处的城市只用一个圆点表示。
生活中的四叉树哲学
其实四叉树的智慧早就渗透在生活里:图书馆的书架先按学科分类,再分大类,最后到具体书目;快递网点先按城市分区,再到街道,最后到小区。这种 “分而治之” 的思想,让计算机在处理海量空间数据时,从 “愚公移山” 变成了 “庖丁解牛”。
下次当你在地图软件上缩放查看路况时,不妨想想背后可能有一棵四叉树正在默默工作 —— 它可能正把你当前视野里的车辆、行人、红绿灯,都妥善地放进不同的 “小盒子” 里,等待你的每一次点击查询。
Three.js 深度冲突:当像素在 Z 轴上玩起 "挤地铁" 游戏
想象一下,在地铁站台,两列列车同时到站,车门完美对齐,乘客们挤在同一平面想下车 —— 这混乱的场景,正是 Three.js 里深度冲突(Z-fighting)的真实写照。作为计算机图形学里最让人头疼的 "小摩擦" 之一,深度冲突就像两个过于亲密的舞者,在 Z 轴上争抢同一个舞台位置,最终让我们的 3D 画面变得支离破碎。
深度冲突的本质:像素级的 "领土争端"
要理解深度冲突,我们得从图形渲染的底层逻辑说起。当 Three.js 绘制 3D 场景时,每个像素都需要回答一个关键问题:"在当前视角下,我应该显示哪个物体的颜色?" 这个判断依靠的是深度缓冲区(depth buffer),它就像一本记录着每个像素 "海拔高度" 的账本。
深度缓冲区里的每个值都代表着对应像素在 Z 轴上的距离,范围通常是 0 到 1(近平面到远平面)。当两个物体的表面在 Z 轴上靠得太近,近到它们的 Z 值差异小于深度缓冲区的精度时,麻烦就来了 —— 缓冲区无法分辨谁前谁后,只能随机选择一个显示。这就像用毫米尺去测量两张叠在一起的薄纸,根本分不清哪张在前哪张在后。
在 Three.js 中,这种精度限制源于浮点数的存储特性。深度缓冲区通常是 16 位、24 位或 32 位的,位数越高精度越高,但即便是 32 位,在处理远距离场景时也会力不从心。比如当远平面设置得很远时,近距离内的微小 Z 值差异就会被 "淹没" 在浮点精度的误差里。
如何检测深度冲突:那些 "闪烁" 的警示灯
深度冲突最明显的特征是画面上出现不规则的闪烁斑块,就像老式电视机信号不良时的雪花点。这些闪烁区域其实是两个重叠表面在每一帧渲染中 "轮流掌权" 的结果 —— 上一帧显示 A 物体,这一帧显示 B 物体,肉眼看起来就成了闪烁。
在 Three.js 中,我们可以用一个简单的测试场景来复现这种现象:
// 创建两个几乎重叠的平面
const geometry1 = new THREE.PlaneGeometry(10, 10);
const geometry2 = new THREE.PlaneGeometry(10, 10);
const material = new THREE.MeshBasicMaterial({
color: 0xff0000,
side: THREE.DoubleSide
});
const plane1 = new THREE.Mesh(geometry1, material);
const plane2 = new THREE.Mesh(geometry2, material);
// 让它们在Z轴上非常接近但不重合
plane1.position.z = 0;
plane2.position.z = 0.000001; // 微小的距离
scene.add(plane1);
scene.add(plane2);
运行这段代码,你会看到红色平面上出现诡异的闪烁区域 —— 这就是典型的深度冲突。两个平面距离太近,超出了深度缓冲区的分辨能力,导致像素所有权在每一帧随机切换。
解决深度冲突:给像素们 "划清界限"
解决深度冲突的核心思路很简单:让原本挤在一起的表面保持适当距离,或者增强系统的 "分辨能力"。在 Three.js 中,我们有多种实用技巧可以采用:
1. 增加物理距离:给舞者们更多舞台空间
最直接的方法是增大两个表面之间的实际距离,就像在拥挤的地铁里喊一声 "请大家散开一点"。调整物体的 position 属性,让它们在 Z 轴上保持足够间隙:
// 从0.000001增加到0.01,提供足够的深度差异
plane2.position.z = 0.01;
这个值需要根据场景尺度调整,原则是既要明显大于深度缓冲区的精度误差,又要小到不影响视觉效果。
2. 调整相机参数:优化深度缓冲区的 "视力"
相机的近平面(near)和远平面(far)设置对深度缓冲区精度影响巨大。就像望远镜的焦距,调得合适才能看得更清楚。理想情况下,近平面应尽可能远,远平面应尽可能近,形成一个 "紧凑" 的观察范围:
// 不好的设置:近平面太近,远平面太远
camera.near = 0.0001;
camera.far = 10000;
// 更好的设置:根据实际场景调整范围
camera.near = 0.1;
camera.far = 100;
camera.updateProjectionMatrix(); // 重要:更新相机投影矩阵
这个技巧的原理是,深度缓冲区的精度在近平面附近最高,随着距离增加而降低。缩小观察范围能让有限的精度分布在更有用的区间内。
3. 使用多边形偏移:给表面添加 "虚拟垫片"
Three.js 提供了 polygonOffset(多边形偏移)功能,就像给其中一个表面垫上看不见的垫片,在不改变实际位置的情况下解决冲突。这特别适合处理网格线、阴影等必须与表面贴合但又不能重叠的元素:
// 创建带偏移的材质
const materialWithOffset = new THREE.MeshBasicMaterial({
color: 0x00ff00,
polygonOffset: true,
polygonOffsetFactor: 1, // 偏移因子
polygonOffsetUnits: 1 // 偏移单位
});
const plane2 = new THREE.Mesh(geometry2, materialWithOffset);
polygonOffsetFactor 控制基于多边形斜率的偏移量,polygonOffsetUnits 控制基于屏幕像素的偏移量。通常从 (1,1) 开始尝试,逐渐调整到合适值。
4. 启用 logarithmicDepthBuffer:给远距离场景配 "老花镜"
对于大型场景(如室外建筑、地形),可以启用相机的对数深度缓冲区,它能在远距离保持更高的深度精度,就像老花镜帮助看清远处物体:
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.logarithmicDepthBuffer = true; // 启用对数深度缓冲
注意这个属性需要 WebGL 2.0 支持,并且会略微增加性能消耗。
深度冲突的哲学思考:精度与效率的永恒平衡
深度冲突本质上是计算机图形学中 "精度有限" 这一根本矛盾的体现。我们永远在精度和性能之间寻找平衡 —— 更高位的深度缓冲区(如 32 位)能减少冲突,但会消耗更多显存;更大的物体间距能避免冲突,但会影响视觉真实性。
作为 Three.js 开发者,理解深度冲突背后的原理能帮助我们写出更健壮的代码。记住,当你的场景出现神秘的闪烁和条纹时,不妨先检查那些看似亲密无间的表面 —— 也许只是需要给它们一点呼吸的空间。
就像现实世界中解决拥挤问题的方法永远是合理规划空间和流量,在 Three.js 的 3D 世界里,优雅解决深度冲突的关键也在于:理解你的场景尺度,优化相机参数,给每个表面合适的 "生存空间"。
5400余人被遣返 中缅泰:继续联手打击妙瓦底等地电诈
深入理解JavaScript闭包:从入门到精通的实战指南
碎碎念
你是不是也有过这样的困扰?面试官问起闭包,你能说出个大概,但总感觉说不到点子上?或者在实际开发中,明明知道要用闭包解决问题,但写出来的代码总是有各种奇怪的bug?
别担心,今天我们就来彻底搞懂JavaScript中这个既神秘又实用的概念——闭包。通过实际代码示例和踩坑经验,让你从"知其然"到"知其所以然"。
叠个甲:本文基于阮一峰老师的经典教程,结合实际开发经验,用最接地气的方式带你理解闭包。如果你觉得某些地方讲得不够深入,欢迎在评论区讨论!
一、作用域:理解闭包的基石
1.1 作用域链的奥秘
在深入闭包之前,我们先来回顾一下JavaScript的作用域机制。看看这段代码:
// 全局作用域
var n = 999;
function f1() {
// 没有使用var声明,变成了全局变量
b = 123;
// 函数作用域
{
// 块级作用域
let a = 1;
}
console.log(n); // 可以访问全局变量
}
f1();
console.log(b); // 123,意外的全局变量
关键点解析:
- 作用域链:内部作用域可以访问外部作用域的变量,这就是作用域链的核心
-
意外的全局变量:不使用
var
、let
或const
声明的变量会意外成为全局变量,这是JavaScript的一个"坏零件"(The Bad Parts) -
块级作用域:ES6的
let
和const
引入了块级作用域概念
1.2 函数外部无法读取内部变量的困境
正常情况下,函数外部是无法访问函数内部变量的:
function f1() {
var n = 999; // 局部变量
}
console.log(n); // ReferenceError: n is not defined
那么问题来了:如果我们确实需要在函数外部访问函数内部的变量怎么办?
这就是闭包要解决的核心问题!
二、闭包的本质:连接内外的桥梁
2.1 什么是闭包?
闭包就是将函数内部和函数外部连接起来的桥梁
用更技术化的语言描述:闭包是指有权访问另一个函数作用域中变量的函数。
让我们看一个最简单的闭包例子:
// 让局部变量可以在全局访问
function f1() {
// 局部变量
var n = 999; // 自由变量
function f2() {
// 自由变量
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
代码解析:
-
f2
函数定义在f1
函数内部 -
f2
函数访问了f1
函数的局部变量n
-
f1
函数返回了f2
函数 - 在全局作用域中,我们通过
result
变量保存了f2
函数的引用 - 调用
result()
时,依然能够访问到n
变量
这就是闭包的神奇之处:即使f1
函数已经执行完毕,但n
变量并没有被销毁,而是被"保存"在了闭包中。
2.2 自由变量的生命周期
你可能会好奇:为什么n
变量没有被垃圾回收机制回收?
答案是:引用计数。
function f1() {
var n = 999;
nAdd = function () {
n += 1;
}
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000
关键观察:
- 第一次调用
result()
输出999 - 调用
nAdd()
修改了n
的值 - 第二次调用
result()
输出1000
这说明什么?n
变量一直存活在内存中,没有被销毁!
这就是闭包的核心特性:让变量的值始终保持在内存中。
三、闭包的经典应用场景
3.1 解决this指向问题
在实际开发中,闭包最常见的应用场景之一就是解决this
指向问题:
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
var that = this; // 保存this引用
return function(){
return that.name; // 通过闭包访问外部的this
}
}
}
console.log(object.getNameFunc()()); // "My Object"
为什么需要这样做?
如果直接返回function(){ return this.name; }
,那么this
会指向全局对象(在浏览器中是window
),而不是object
对象。
通过闭包,我们巧妙地"捕获"了正确的this
引用。
3.2 模块化编程
闭包还可以用来实现模块化编程,创建私有变量:
var module = (function(){
var privateVar = 0;
return {
increment: function(){
privateVar++;
},
getCount: function(){
return privateVar;
}
};
})();
module.increment();
module.increment();
console.log(module.getCount()); // 2
console.log(module.privateVar); // undefined,无法直接访问
这种模式在ES6模块化普及之前,是JavaScript实现模块化的主要方式。
四、闭包的陷阱与注意事项
4.1 内存泄漏的隐患
闭包虽然强大,但也带来了内存管理的挑战:
function createClosure() {
var largeData = new Array(1000000).fill('data');
return function() {
console.log('闭包函数被调用');
// 即使不使用largeData,它也不会被回收
};
}
var closure = createClosure();
// largeData数组会一直占用内存
解决方案:
function createClosure() {
var largeData = new Array(1000000).fill('data');
return function() {
console.log('闭包函数被调用');
};
}
var closure = createClosure();
// 使用完毕后,手动清理
closure = null; // 这样largeData才能被回收
4.2 循环中的闭包陷阱
这是一个经典的面试题:
// 错误的写法
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出三次3
}, 100);
}
为什么输出三次3?
因为setTimeout
中的回调函数形成了闭包,它们都引用了同一个变量i
。当定时器执行时,循环已经结束,i
的值已经变成了3。
解决方案1:使用立即执行函数
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 输出0, 1, 2
}, 100);
})(i);
}
解决方案2:使用let声明
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出0, 1, 2
}, 100);
}
五、闭包的性能考量
5.1 内存消耗
闭包会导致额外的内存消耗,因为:
- 外部函数的变量不能被垃圾回收
- 闭包函数本身也占用内存
- 如果创建大量闭包,可能导致内存压力
5.2 最佳实践
- 及时清理不需要的闭包引用
var closure = createClosure();
// 使用完毕后
closure = null;
- 避免在循环中创建大量闭包
// 不好的做法
for (let i = 0; i < 10000; i++) {
element.addEventListener('click', function() {
// 创建了10000个闭包
});
}
// 更好的做法
function handleClick() {
// 只创建一个函数
}
for (let i = 0; i < 10000; i++) {
element.addEventListener('click', handleClick);
}
- 在退出函数之前,将不使用的局部变量设为null
function createClosure() {
var largeObject = {};
var smallData = 'needed';
// 使用完largeObject后
largeObject = null;
return function() {
return smallData;
};
}
六、现代JavaScript中的闭包
6.1 箭头函数与闭包
const createCounter = () => {
let count = 0;
return () => ++count;
};
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
箭头函数同样可以形成闭包,而且语法更加简洁。
6.2 Promise与闭包
function createAsyncCounter() {
let count = 0;
return function() {
return new Promise(resolve => {
setTimeout(() => {
resolve(++count); // 闭包访问count
}, 1000);
});
};
}
const asyncCounter = createAsyncCounter();
asyncCounter().then(console.log); // 1
asyncCounter().then(console.log); // 2
七、总结与思考
7.1 闭包的核心价值
- 数据封装:创建私有变量,实现信息隐藏
- 状态保持:让变量在函数执行完毕后依然存活
- 回调函数:在异步编程中保持上下文
- 模块化:在ES6之前实现模块化编程的重要手段
7.2 使用闭包的原则
- 明确目的:确实需要保持状态或封装数据时才使用
- 注意内存:及时清理不需要的闭包引用
- 性能考虑:避免在性能敏感的场景中过度使用
- 代码可读性:确保团队成员都能理解闭包的使用意图
7.3 闭包与现代前端开发
在现代前端开发中,虽然有了ES6模块、React Hooks、Vue Composition API等新特性,但闭包依然是理解这些概念的基础。比如:
- React的
useState
本质上就是利用闭包来保持状态 - Vue的响应式系统也大量使用了闭包
- 各种状态管理库都离不开闭包的概念
小贴士
闭包不仅仅是一个技术概念,更是JavaScript语言设计哲学的体现。它体现了"函数是一等公民"的理念,让我们能够以更加灵活和强大的方式组织代码。
虽然闭包可能带来一些性能和内存方面的考虑,但只要我们理解其原理,合理使用,它就是我们手中的利器。
记住:闭包的自由是有代价的,这个代价就是生命周期的延长和内存的占用。但正是这种"不确定性"的自由,给了JavaScript无限的可能性。
希望这篇文章能帮你彻底理解闭包,在面试和实际开发中都能游刃有余。如果你有任何问题或者想法,欢迎在评论区讨论!
参考资料:
- 阮一峰《JavaScript教程》
- 《JavaScript语言精粹》
- MDN Web Docs
罗马仕客服回应停工:内部正常组织架构调整 召回方案长期有效
Vue.js 入门指南:从零开始构建你的第一个应用
Vue.js 入门指南:从零开始构建你的第一个应用
什么是 Vue.js?
Vue.js(通常简称为 Vue)是一个渐进式 JavaScript 框架,用于构建用户界面。由前 Google 工程师尤雨溪于 2014 年创建,如今已成为全球最流行的前端框架之一。Vue 的核心特点包括:
- 渐进式架构:可以逐步采用,从增强静态页面到构建复杂单页应用
- 响应式系统:自动追踪数据变化并更新视图
- 组件化开发:将 UI 拆分为独立可复用的组件
- 易学易用:基于标准 HTML、CSS 和 JavaScript,学习曲线平缓
- 轻量高效:运行时仅 20KB 左右,性能优异
为什么选择 Vue?
- 简单易上手:对初学者友好,文档完善
- 灵活性强:可根据需求选择使用方式
- 社区活跃:拥有庞大开发者社区和丰富生态
- 性能出色:虚拟 DOM 和智能优化策略
- 现代工具链:Vite 构建工具提供极速开发体验
搭建第一个 Vue 应用
安装 Vue
方法一:CDN 引入(最简单方式)
<!DOCTYPE html>
<html>
<head>
<title>第一个 Vue 应用</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
{{ message }}
</div>
<script>
const { createApp } = Vue
createApp({
data() {
return {
message: '你好,Vue!'
}
}
}).mount('#app')
</script>
</body>
</html>
方法二:使用 Vite 创建项目(推荐)
npm create vite@latest my-vue-app -- --template vue
cd my-vue-app
npm install
npm run dev
方法三:使用 Vue CLI 创建项目
npm install -g @vue/cli
# 或
yarn global add @vue/clinpm
vue --version # 验证安装是否成功
vue create my-project # 创建项目
cd my-project
npm run serve
Vue 应用基本结构
// main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
<!-- App.vue -->
<template>
<div>
<h1>{{ title }}</h1>
<p>计数器: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script>
export default {
data() {
return {
title: '我的第一个Vue应用',
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
<style scoped>
h1 {
color: #42b983;
}
</style>
Vue 核心概念详解
1. 模板语法
Vue 使用基于 HTML 的模板语法,允许你声明式地将数据绑定到 DOM:
<template>
<!-- 文本插值 -->
<p>{{ message }}</p>
<!-- 原始 HTML -->
<p v-html="rawHtml"></p>
<!-- 属性绑定 -->
<a :href="url">链接</a>
<!-- 条件渲染 -->
<div v-if="isVisible">可见内容</div>
<div v-else>其他内容</div>
<!-- 列表渲染 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }}. {{ item.name }}
</li>
</ul>
</template>
2. 响应式数据
Vue 自动跟踪 JavaScript 对象的变化并更新 DOM:
export default {
data() {
return {
message: 'Hello Vue!',
counter: 0,
user: {
name: '张三',
age: 30
},
todos: [
{ id: 1, text: '学习Vue' },
{ id: 2, text: '构建应用' }
]
}
},
methods: {
updateUser() {
// Vue 会自动检测这些变化并更新视图
this.user.age = 31
this.todos.push({ id: 3, text: '部署项目' })
}
}
}
3. 指令系统
指令是带有 v-
前缀的特殊属性:
指令 | 说明 | 示例 |
---|---|---|
v-bind |
动态绑定属性 | :class="{ active: isActive } |
v-on |
绑定事件监听器 | @click="handleClick" |
v-model |
表单输入双向绑定 | <input v-model="message"> |
v-if/v-show |
条件渲染 | <p v-if="isVisible">显示</p> |
v-for |
列表渲染 | <li v-for="item in items"> |
v-html |
输出原始 HTML | <div v-html="rawHtml"> |
4. 组件基础
组件是 Vue 的核心概念,允许你将 UI 拆分为独立可复用的部分:
<!-- 父组件 ParentComponent.vue -->
<template>
<div>
<child-component
:title="parentTitle"
@child-event="handleChildEvent"
/>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
},
data() {
return {
parentTitle: '来自父组件的消息'
}
},
methods: {
handleChildEvent(data) {
console.log('收到子组件事件:', data)
}
}
}
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
<div>
<h2>{{ title }}</h2>
<button @click="sendToParent">通知父组件</button>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true
}
},
emits: ['child-event'],
methods: {
sendToParent() {
this.$emit('child-event', { message: '来自子组件的数据' })
}
}
}
</script>
5. 计算属性和侦听器
计算属性:基于响应式数据派生值 侦听器:响应数据变化执行操作
<template>
<div>
<p>原始价格: {{ price }} 元</p>
<p>折扣后价格: {{ discountedPrice }} 元</p>
<input v-model="discountPercent" placeholder="输入折扣百分比">
</div>
</template>
<script>
export default {
data() {
return {
price: 100,
discountPercent: 10
}
},
computed: {
// 计算属性 - 自动缓存结果
discountedPrice() {
return this.price * (1 - this.discountPercent / 100)
}
},
watch: {
// 侦听器 - 响应特定数据变化
discountPercent(newVal, oldVal) {
if (newVal > 50) {
alert('折扣不能超过50%!')
this.discountPercent = 50
}
}
}
}
</script>
6. 生命周期钩子
Vue 组件有多个生命周期阶段,你可以在特定阶段执行代码:
export default {
data() {
return {
data: null
}
},
// 组件初始化后,数据观测已建立
created() {
console.log('组件已创建')
this.fetchData()
},
// 组件挂载到 DOM 后
mounted() {
console.log('组件已挂载')
this.setupEventListeners()
},
// 组件更新后
updated() {
console.log('组件已更新')
},
// 组件卸载前
beforeUnmount() {
console.log('组件即将卸载')
this.cleanup()
},
methods: {
fetchData() { /* 获取数据 */ },
setupEventListeners() { /* 设置事件监听 */ },
cleanup() { /* 清理操作 */ }
}
}
实战:构建待办事项应用
<template>
<div class="todo-app">
<h1>待办事项清单</h1>
<!-- 添加新任务 -->
<div class="add-todo">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="添加新任务..."
>
<button @click="addTodo">添加</button>
</div>
<!-- 过滤选项 -->
<div class="filters">
<button
:class="{ active: filter === 'all' }"
@click="filter = 'all'"
>
全部
</button>
<button
:class="{ active: filter === 'active' }"
@click="filter = 'active'"
>
未完成
</button>
<button
:class="{ active: filter === 'completed' }"
@click="filter = 'completed'"
>
已完成
</button>
</div>
<!-- 任务列表 -->
<ul class="todo-list">
<li v-for="todo in filteredTodos" :key="todo.id">
<input
type="checkbox"
v-model="todo.completed"
>
<span :class="{ completed: todo.completed }">
{{ todo.text }}
</span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
<!-- 统计信息 -->
<div class="stats">
<p>剩余任务: {{ activeTodosCount }} / 总任务: {{ todos.length }}</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
newTodo: '',
todos: [
{ id: 1, text: '学习Vue基础', completed: false },
{ id: 2, text: '构建第一个Vue应用', completed: true },
{ id: 3, text: '探索Vue高级特性', completed: false }
],
filter: 'all'
}
},
computed: {
activeTodosCount() {
return this.todos.filter(todo => !todo.completed).length
},
filteredTodos() {
switch (this.filter) {
case 'active':
return this.todos.filter(todo => !todo.completed)
case 'completed':
return this.todos.filter(todo => todo.completed)
default:
return this.todos
}
}
},
methods: {
addTodo() {
if (this.newTodo.trim() === '') return
this.todos.push({
id: Date.now(),
text: this.newTodo,
completed: false
})
this.newTodo = ''
},
removeTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
}
}
}
</script>
<style>
.todo-app {
max-width: 600px;
margin: 20px auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.add-todo {
display: flex;
margin-bottom: 20px;
}
.add-todo input {
flex-grow: 1;
padding: 10px;
font-size: 16px;
}
.add-todo button {
margin-left: 10px;
padding: 10px 15px;
background: #42b983;
color: white;
border: none;
cursor: pointer;
}
.filters {
margin-bottom: 20px;
}
.filters button {
padding: 5px 10px;
margin-right: 5px;
background: #f0f0f0;
border: 1px solid #ddd;
cursor: pointer;
}
.filters button.active {
background: #42b983;
color: white;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-list input[type="checkbox"] {
margin-right: 10px;
}
.todo-list span.completed {
text-decoration: line-through;
color: #888;
}
.todo-list button {
margin-left: auto;
background: #ff6b6b;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
}
.stats {
margin-top: 20px;
text-align: center;
color: #666;
}
</style>
下一步学习路径
掌握了 Vue 基础后,你可以继续深入学习:
- Vue Router:构建单页面应用(SPA)的路由管理
- 状态管理:使用 Pinia 管理全局状态
- 组合式 API:Vue 3 的现代代码组织方式
- 服务端渲染:使用 Nuxt.js 提升应用性能
- UI 框架:Element Plus、Vuetify 等流行 UI 库
- 测试:使用 Vitest 和 Vue Test Utils 进行组件测试
学习资源推荐
- Vue 官方文档 - 最权威的学习资源
- Vue Mastery - 优质视频教程
- Vue School - 专业 Vue 课程平台
- Vue.js 中文社区 - 中文开发者社区
- Awesome Vue - Vue 生态资源大全
结语
Vue.js 以其简洁的设计和强大的功能,已成为现代前端开发的首选框架之一。通过本指南,你已经掌握了 Vue 的核心概念和基本用法。现在,是时候动手实践,构建你自己的 Vue 应用了!
记住,学习编程最好的方法就是不断实践。从简单的项目开始,逐步挑战更复杂的应用,你将很快成为一名熟练的 Vue 开发者。祝你编程愉快!
🔥JavaScript 入门必知:代码如何运行、变量提升与 let/const🔥
前言:为什么 JavaScript 如此“奇怪”?
如果你是从其他编程语言(比如 Python、Java 或 C++)转到 JavaScript 的,你可能会对它的某些行为感到困惑:
console.log(name); // 输出:undefined,而不是报错!
var name = "Alice";
{
let age = 25;
console.log(age); // 输出:25
}
console.log(age); // 报错:age is not defined
- 为什么
var
变量可以在声明前访问? - 为什么
let
和const
又不行? - 为什么
{}
块能影响变量的作用域?
这些现象背后,是 JavaScript 独特的 编译与执行机制,以及 作用域管理方式。在传统语言中,变量通常需要先声明再使用,而 JavaScript 的 var
却允许“先使用后声明”,这源于它的 变量提升(Hoisting) 特性。而 let
和 const
的出现,则修复了 var
的缺陷,引入了更严格的 块级作用域(Block Scope) 和 暂时性死区(TDZ)。
本章将深入剖析:
✅ JavaScript 代码的执行流程(编译 vs. 执行阶段)
✅ 变量提升的本质(var
与函数声明的特殊行为)
✅ let
和 const
如何避免变量提升问题
✅ 作用域链与闭包的底层机制
无论你是 JavaScript 新手,还是想彻底理解它的运行原理,这篇文章都会让你豁然开朗!
🚀 现在,让我们开始探索 JavaScript 的独特世界!
First:JavaScript代码是如何跑起来的
想要了解这些现象的本质,我们第一个需要了解的是,JavaScript的代码是怎么跑的,如何运行的,究竟是什么神奇的妙妙♂工具能让它的规则如此灵动~
JavaScript代码运行的基本过程:
解析阶段
JavaScript代码在运行时的第一阶段,在这一阶段中浏览器的引擎会进行解析(Phrasing),在这一阶段会进行词法分析和语法分析。
词法分析:会将代码字符串拆分成有意义的“单词”或“符号”,称为 Token
例如:let x = 5 + 3;
会被拆分成:[let, x, =, 5, +, 3, ;]
语法分析:根据 JavaScript 的语法规则,将这些 Token 组织成一个树状结构,称为 抽象语法树 (Abstract Syntax Tree - AST)
- AST 代表了代码的结构和逻辑关系。
- 例如:
let x = 5 + 3;
的 AST 会表示:声明一个变量x
,它的值是一个加法表达式(操作数是 5 和 3)
此阶段的重点:
- 检查代码是否有语法错误 (Syntax Errors)。如果代码写得不合语法规则(比如缺少括号、错误的关键字),解析阶段就会失败并报错。
- 只关心代码的结构和形式,不关心变量具体代表什么值、函数具体做什么操作。
- 输出:AST (抽象语法树) 。这是代码的“结构化蓝图”。
编译阶段
编译阶段会 静态分析 作用域关系(但不创建运行时词法环境):
-
确定变量和函数的作用域归属:
- 识别全局作用域、函数作用域、块级作用域(
let/const
)。 - 标记变量声明(
var
、let
、const
)和函数声明(function
)的作用域。
- 识别全局作用域、函数作用域、块级作用域(
-
处理变量提升(Hoisting) :
-
var
和function
声明会被记录到作用域顶部(但未赋值)。 -
let/const
也会被记录,但不会提前绑定(形成暂时性死区)。
-
-
建立作用域链的静态结构:
- 确定嵌套作用域的引用关系(闭包的基础)。
什么是作用域?
看到上面编译阶段的小伙伴们可能会有疑问:什么叫作用域啊?作用域链是什么东西嘞?
作用域,作用域,就是变量能发挥作用的区域,变量能在某个区域耀武扬威,但是到了别的地方,就得喝肾宝咯~
比如以下例子:
var a = 1;
function add(a1,a2){
var c = 2;
return a1+a2+c;
}
{
let f = 1;
const g = 2;
var h = 3;
}
这个例子的作用域就是这么组成的:
对应的变量只会在相应的作用域中发挥作用,在其他的作用域中不可发挥作用,图中,c,a1,a2仅仅能在add函数中被使用,f,g仅能在块级作用域中被使用,而a,h处于全局作用域,可以在任何地方被使用。
这个时候就有人要说了:“主播主播,h不是在块级作用域中被定义的吗?怎么跑到了全局作用域了呢?”
答案其实就是提升(hoistings)。
提升(Hoisting)
提升是JavaScript中一个比较重要的特性,它决定了我们利用定义的变量和普通函数后的,它们的特殊行为:即无论在哪里定义变量(var)和函数,都会在编译阶段提升到当前顶层
通俗一点讲,就是一个人在刷抖音,他的抖音里有各种各样的内容,他会总览一下,把所有看到的美女都先收藏一下,就算不知道她们的ID,也要收藏一下,毕竟感觉来了谁会管她ID是啥呢0v0.
而在代码编译阶段,所有变量和函数就是美女,让编译器离不开眼,把她们全部拉到顶层了,不管它们是什么值,不管他们有没有值,全部拉到顶部!
就像下面这个例子:
console.log(a); // 输出 undefined
var a = 3;
var c = 114514;
add(1,3);
function add(a,b){
console.log(a,b); // 输出 1,3
console.log(c); // 输出 undefined
var c = 4;
}
这一段代码在编译后就相当于以下内容:
var a; // 全局作用域内变量提升至顶部
var c;
function add(a,b){ // 函数提升到顶部
var c; // 函数作用域内变量提升
console.log(a,b); // 输出 1,3
console.log(c); // 输出 undefined
c = 4;
}
console.log(a);
a = 3;
c = 114514;
add(1,3);
在编译器的法眼下,所有var变量和函数都被标记出来了,提升到了对应的顶部,这也就是为什么这一串看似反人类的代码可以执行而不报错的原因,就在于提升(Hoisting)
!!!!提醒:
能做到提升的变量只有 var ,但是如果var存在于函数中,那么只能提升到函数作用域的顶部,而不会溢出到全局作用域。
let和const均不可以提升,这是由于其设计造成的,JavaScript仅仅用了一周的时间就被设计了出来,var是最早表示变量的标识,后来在ES6中为了消除这种反人类的设计才引入了let和const,用了let和const的变量拥有和其他语言一样的特性,不再有提升。
这个时候有的童鞋可能想问了:如果我在add函数中没有定义var c,那么c会输出什么呢?
这个就和作用域链有关系了
作用域链
作用域链(Scope Chain) 是 JavaScript 在执行过程中寻找变量的机制,它决定了 当前作用域 可以访问哪些变量,以及它们的查找顺序。
当访问一个变量时,JavaScript 引擎会 沿着作用域链逐级向上查找,直到找到该变量或到达全局作用域(未找到则报 ReferenceError
)。
例如上方的例子中,要访问c变量时,就会先访问函数作用域中的c,如果没有,就会沿着作用域链一步步向外寻找。就像小孩子自己被困在家里,睡醒了找不到妈妈一样,先从房间找,又跑到客厅,最后跑出房子找是一样的....(大家一定要照顾好孩子233333~)
执行阶段
编译阶段的几个问题解答完了,我们来解释一下执行阶段。
目的: 真正逐行运行代码,产生程序的实际效果(计算、修改数据、操作 DOM、网络请求等)。
-
过程:
-
JavaScript 引擎从上到下、逐行执行编译阶段准备好的代码(字节码或机器码),创建执行上下文。
-
赋值操作: 给变量赋予实际的值(覆盖掉编译阶段
var
变量的undefined
初始值)。 -
函数调用:
- 遇到函数调用时,会为该函数创建一个新的函数执行上下文,同样经历编译(创建) 和 执行 阶段。
- 执行函数体内部的代码。
- 函数执行完毕后,其执行上下文通常会被销毁(闭包除外)。
-
表达式求值: 计算表达式的结果(如
5 + 3
,x > y
)。 -
逻辑控制: 执行条件判断 (
if/else
)、循环 (for
,while
) 等逻辑流程。
-
其中,创建执行上下文是最重要的部分。
执行上下文
简单来说,执行上下文就是记录了各种信息的载体,方便对应规则来运行代码。
执行上下文一般有三种:
- 全局执行上下文(Global Execution Context) → 代码首次运行时创建。
- 函数执行上下文(Function Execution Context) → 每次调用函数时创建。
-
eval
执行上下文(较少用,不推荐)。
执行上下文都包含了啥呢?
V8引擎就是根据这些信息来对应着一定的规则,来运行了代码。
Second:let?const?var?有啥区别?
JavaScript 有三种变量声明方式:const
、let
和 var
,它们在 作用域、提升(Hoisting)、重复声明、TDZ(暂时性死区) 等方面有显著区别。
首先来说说Var,这个是JavaScript最初设计用来表示变量的标识,它具有提升的特性,这一点与其他编程语言不同,比较反人类,所以呢,后来在ES6中就设计了let和const,用来替代var,去除这一反人类的特性。那为什么后来没有废除var呢,我猜可能是用var的代码已经太多了,如果删除掉var,那么开发者们就要掀桌子了哈哈哈哈哈哈哈哈。
const
、let
、var
核心区别
特性 | var |
let |
const |
---|---|---|---|
作用域 | 函数作用域(function-scoped ) |
块级作用域(block-scoped ) |
块级作用域(block-scoped ) |
声明提升 | ✅(初始化为 undefined ) |
✅(未初始化,访问报错) | ✅(未初始化,访问报错) |
重复声明 | ✅(可以多次声明) | ❌(同一作用域禁止重复声明) | ❌(同一作用域禁止重复声明) |
重新赋值 | ✅ | ✅ | ❌(常量,不能重新赋值) |
TDZ(暂时性死区) | ❌(无 TDZ) | ✅(进入块级作用域到声明前不可访问) | ✅(同 let ) |
TDZ(Temporal Dead Zone,暂时性死区)
简单来讲呢,就是当你定义一个let
或者const
变量的时候,不能在没有声明的情况下提前访问它,这一点和其他编程语言一致.
TDZ 是 let
和 const
特有的行为:
-
在变量声明之前,如果访问它,会抛出
ReferenceError
(而不是得到undefined
)。 - TDZ 的范围:变量所在的作用域顶部 → 变量声明的位置。
示例 1:let
的 TDZ
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 10; // 声明前访问会报错(TDZ 区域)
执行过程:
- 进入作用域 →
a
被提升(let a
) - 但
a
未被初始化(处于 TDZ) - 执行
console.log(a)
→ 报错 - 执行
a = 10
→ TDZ 结束,可以访问a
示例 2:const
的 TDZ
console.log(b); // ReferenceError: Cannot access 'b' before initialization
const b = 20; // 声明前访问会报错(TDZ 区域)
const
和 let
的 TDZ 行为一致。
示例 3:var
没有 TDZ
console.log(c); // undefined(不会报错)
var c = 30; // var 提升并初始化为 undefined
var
不会进入 TDZ,因为它在编译阶段已经被初始化为 undefined
。
全局作用域下的绑定
-
var
声明的全局变量 → 会 绑定到window
(浏览器)或global
(Node.js) -
let
和const
声明的全局变量 → 不会绑定到window
/global
示例
// 浏览器环境
var globalVar = "var 变量";
let globalLet = "let 变量";
const globalConst = "const 变量";
console.log(window.globalVar); // "var 变量"(绑定到 window)
console.log(window.globalLet); // undefined(不绑定)
console.log(window.globalConst); // undefined(不绑定)
为什么 let
和 const
不绑定到 window
?
-
历史遗留问题:
var
是 ES5 的写法,会污染全局对象(window
)。 -
块级作用域优化:
let
和const
是 ES6 引入的,设计初衷是避免全局污染。
使用场景总结
声明方式 | 适用场景 |
---|---|
var |
旧代码兼容 / 不需要块级作用域的场景(但现代 JS 基本不用)。 |
let |
需要重新赋值的变量(如循环计数器 for (let i = 0; ...) )。 |
const |
常量(如 API 密钥、数学常量 PI )、对象/数组引用(可修改属性但不能重新赋值)。 |
总结
关键点 | var |
let |
const |
---|---|---|---|
作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
提升(Hoisting) | ✅ (undefined ) |
✅ (TDZ) | ✅ (TDZ) |
重复声明 | ✅ | ❌ | ❌ |
重新赋值 | ✅ | ✅ | ❌ |
TDZ(暂时性死区) | ❌ | ✅ | ✅ |
全局绑定 window |
✅ | ❌ | ❌ |
最佳实践
-
默认使用
const
(避免意外修改)。 -
需要重新赋值时用
let
(如循环变量)。 -
避免使用
var
(除非维护旧代码)。
总结
好了,这一篇文章就到这里吧,我们在这里讲解了JS代码运行的原理,详细介绍了作用域和执行上下文等各种概念,还是希望大家能够有一些收获,能够更好的了解JavaScript,如果你觉得我写的好,那么请给我一个免费的赞吧嘻嘻嘻》》》