阅读视图

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

Three.js 完全学习指南(二)场景、相机、渲染器基础

场景、相机、渲染器基础

场景(Scene)详解

场景是 Three.js 中所有 3D 对象的容器,它定义了整个 3D 空间。让我们深入了解场景的配置和使用。

场景示例

图 2.1: 包含多个几何体的场景示例

1. 场景基础配置

import * as THREE from 'three';

// 创建场景
const scene = new THREE.Scene();

// 设置背景色
scene.background = new THREE.Color(0x000000); // 黑色背景

// 添加雾效果
scene.fog = new THREE.Fog(0x000000, 10, 100); // 颜色、近平面、远平面

// 设置场景环境
scene.environment = new THREE.CubeTextureLoader().load([
    'px.jpg', 'nx.jpg',
    'py.jpg', 'ny.jpg',
    'pz.jpg', 'nz.jpg'
]);

场景环境贴图

图 2.2: 使用环境贴图的场景效果

2. 场景管理

// 添加对象到场景
scene.add(mesh);

// 从场景中移除对象
scene.remove(mesh);

// 获取场景中的所有对象
const objects = scene.children;

// 遍历场景中的对象
scene.traverse((object) => {
    if (object.isMesh) {
        // 处理网格对象
    }
});
graph TD
    A[场景] --> B[几何体]
    A --> C[光源]
    A --> D[相机]
    A --> E[辅助对象]
    B --> F[网格]
    B --> G[线条]
    C --> H[环境光]
    C --> I[平行光]
    E --> J[网格辅助]
    E --> K[坐标轴]

图 2.3: 场景对象层级结构

3. 场景优化

// 设置场景自动更新
scene.autoUpdate = true;

// 手动更新场景
scene.updateMatrixWorld(true);

// 清理场景
function disposeScene() {
    scene.traverse((object) => {
        if (object.geometry) {
            object.geometry.dispose();
        }
        if (object.material) {
            if (Array.isArray(object.material)) {
                object.material.forEach(material => material.dispose());
            } else {
                object.material.dispose();
            }
        }
    });
}

相机(Camera)详解

Three.js 提供了多种相机类型,每种类型都有其特定的用途。

1. 透视相机(PerspectiveCamera)

透视相机模拟人眼视角,是最常用的相机类型。

透视相机效果

图 2.4: 透视相机的渲染效果

// 创建透视相机
const camera = new THREE.PerspectiveCamera(
    75, // 视角(FOV)
    window.innerWidth / window.innerHeight, // 宽高比
    0.1, // 近平面
    1000 // 远平面
);

// 设置相机位置
camera.position.set(0, 5, 10);

// 设置相机朝向
camera.lookAt(0, 0, 0);

// 更新相机参数
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

2. 正交相机(OrthographicCamera)

正交相机没有透视效果,适合用于 2D 场景或等距视图。

正交相机效果转存失败,建议直接上传图片文件

图 2.5: 正交相机的渲染效果

// 创建正交相机
const camera = new THREE.OrthographicCamera(
    -10, // 左
    10,  // 右
    10,  // 上
    -10, // 下
    0.1, // 近平面
    1000 // 远平面
);

// 设置相机位置
camera.position.set(0, 0, 10);
camera.lookAt(0, 0, 0);

3. 相机控制

使用 OrbitControls 实现相机控制:

相机控制效果

图 2.6: 使用 OrbitControls 的相机控制效果

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

// 创建控制器
const controls = new OrbitControls(camera, renderer.domElement);

// 配置控制器
controls.enableDamping = true; // 启用阻尼效果
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.minDistance = 10;
controls.maxDistance = 500;
controls.maxPolarAngle = Math.PI / 2;

// 在动画循环中更新控制器
function animate() {
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, camera);
}

渲染器(Renderer)详解

渲染器负责将场景和相机的内容绘制到屏幕上。

1. 基础配置

// 创建渲染器
const renderer = new THREE.WebGLRenderer({
    antialias: true, // 抗锯齿
    alpha: true,     // 透明背景
    precision: 'highp' // 精度
});

// 设置渲染器尺寸
renderer.setSize(window.innerWidth, window.innerHeight);

// 设置像素比
renderer.setPixelRatio(window.devicePixelRatio);

// 设置输出编码
renderer.outputEncoding = THREE.sRGBEncoding;

// 启用阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

渲染器效果转存失败,建议直接上传图片文件

图 2.7: 不同渲染效果的对比

2. 高级配置

// 设置渲染器参数
renderer.setClearColor(0x000000, 1); // 设置清除颜色
renderer.setClearAlpha(1); // 设置清除透明度

// 配置阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

// 配置后期处理
const composer = new THREE.EffectComposer(renderer);
const renderPass = new THREE.RenderPass(scene, camera);
composer.addPass(renderPass);

后期处理效果

图 2.8: 使用后期处理的效果

3. 性能优化

// 设置渲染器参数
renderer.powerPreference = 'high-performance';
renderer.precision = 'highp';

// 自动清理
function disposeRenderer() {
    renderer.dispose();
    renderer.forceContextLoss();
    renderer.domElement.remove();
}

// 处理窗口大小变化
window.addEventListener('resize', () => {
    const width = window.innerWidth;
    const height = window.innerHeight;

    camera.aspect = width / height;
    camera.updateProjectionMatrix();

    renderer.setSize(width, height);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

实战:创建一个完整的 3D 场景

让我们结合以上知识,创建一个完整的 3D 场景:

完整场景示例

图 2.9: 完整的 3D 场景示例

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);

// 创建相机
const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
);
camera.position.set(5, 5, 5);
camera.lookAt(0, 0, 0);

// 创建渲染器
const renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
document.getElementById('app').appendChild(renderer.domElement);

// 创建控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// 添加网格
const gridHelper = new THREE.GridHelper(10, 10);
scene.add(gridHelper);

// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

// 添加平行光
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(5, 5, 5);
directionalLight.castShadow = true;
scene.add(directionalLight);

// 创建一个立方体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({
    color: 0x00ff00,
    metalness: 0.3,
    roughness: 0.4
});
const cube = new THREE.Mesh(geometry, material);
cube.castShadow = true;
cube.receiveShadow = true;
scene.add(cube);

// 创建地面
const planeGeometry = new THREE.PlaneGeometry(10, 10);
const planeMaterial = new THREE.MeshStandardMaterial({
    color: 0x808080,
    side: THREE.DoubleSide
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2;
plane.position.y = -0.5;
plane.receiveShadow = true;
scene.add(plane);

// 动画循环
function animate() {
    requestAnimationFrame(animate);

    // 更新控制器
    controls.update();

    // 旋转立方体
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

    // 渲染场景
    renderer.render(scene, camera);
}

// 开始动画
animate();

性能优化建议

  1. 场景优化

    • 使用适当的几何体复杂度
    • 及时清理不需要的对象
    • 使用对象池管理频繁创建的对象
  2. 相机优化

    • 设置合适的视锥体范围
    • 使用适当的相机类型
    • 优化控制器参数
  3. 渲染器优化

    • 使用适当的像素比
    • 启用必要的渲染特性
    • 及时释放资源

练习

  1. 实现相机的自动旋转
  2. 添加多个光源并观察效果
  3. 实现场景的昼夜变化
  4. 添加后期处理效果

下一步学习

在下一章中,我们将学习:

  • 几何体的创建和使用
  • 材质系统的详细配置
  • 纹理的应用
  • 对象的变换和组合

JavaScript作用域和作用域链

在JavaScript中,作用域和作用域链是理解代码执行和变量访问的关键概念。它们决定了变量和函数在代码中的可见性和生命周期。

一、作用域(Scope)

(一)什么是作用域?

作用域是在运行时代码中的某些特定部分中变量、函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。

作用域的主要作用是隔离变量,防止不同作用域下的同名变量发生冲突。例如:

function outFun2() {
    var inVariable = "内层变量2";
}
outFun2();
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined

在上面的例子中,变量inVariable在全局作用域中没有声明,因此在全局作用域下访问它会报错。

(二)全局作用域和函数作用域

1. 全局作用域

全局作用域是指在代码中任何地方都能访问到的对象。以下几种情形拥有全局作用域:

  • 最外层函数和在最外层函数外面定义的变量拥有全局作用域。
  • 所有未定义直接赋值的变量自动声明为拥有全局作用域。
  • 所有window对象的属性拥有全局作用域。
var outVariable = "我是最外层变量"; // 最外层变量
function outFun() { // 最外层函数
    var inVariable = "内层变量";
    function innerFun() { // 内层函数
        console.log(inVariable);
    }
    innerFun();
}
console.log(outVariable); // 我是最外层变量
outFun(); // 内层变量
console.log(inVariable); // inVariable is not defined

全局作用域的弊端是容易污染全局命名空间,引起命名冲突。因此,通常建议将代码封装在函数中,避免全局变量的滥用。

2. 函数作用域

函数作用域是指声明在函数内部的变量,这些变量只能在函数内部访问。例如:

function doSomething() {
    var stuName = "zhangsan";
    function innerSay() {
        console.log(stuName);
    }
    innerSay();
}
console.log(stuName); // 脚本错误
innerSay(); // 脚本错误

函数作用域的一个重要特点是内层作用域可以访问外层作用域的变量,但外层作用域不能访问内层作用域的变量。

(三)块级作用域

ES6引入了块级作用域,通过letconst关键字声明的变量具有块级作用域。块级作用域在以下情况被创建:

  1. 在一个函数内部。
  2. 在一个代码块(由一对花括号包裹)内部。

块级作用域的特点包括:

  • 声明变量不会提升到代码块顶部。
  • 禁止重复声明。
  • 循环中的绑定块作用域的妙用。
for (let i = 0; i < 10; i++) {
    console.log(i); // i 在循环内部有效
}
console.log(i); // ReferenceError: i is not defined

二、作用域链

(一)什么是自由变量?

自由变量是指在当前作用域中没有定义的变量。例如:

var a = 100;
function fn() {
    var b = 200;
    console.log(a); // 这里的 a 是一个自由变量
    console.log(b);
}
fn();

fn函数中,a是一个自由变量,因为它在fn函数的作用域中没有定义。

(二)什么是作用域链?

作用域链是指当访问一个变量时,编译器会从当前作用域开始,逐层向上查找,直到找到该变量或到达全局作用域。例如:

var a = 100;
function f1() {
    var b = 200;
    function f2() {
        var c = 300;
        console.log(a); // 100
        console.log(b); // 200
        console.log(c); // 300
    }
    f2();
}
f1();

f2函数中,ab是自由变量,它们的值通过作用域链从外层作用域中获取。

(三)关于自由变量的取值

自由变量的值是在函数定义时确定的,而不是在函数调用时确定的。例如:

var x = 10;
function fn() {
    console.log(x);
}
function show(f) {
    var x = 20;
    (function () {
        f(); // 输出 10,而不是 20
    })();
}
show(fn);

fn函数中,x的值是在fn函数定义时确定的,因此输出的是全局作用域中的x,而不是show函数中的x

三、作用域与执行上下文

许多开发人员经常混淆作用域和执行上下文的概念。虽然它们都与变量的访问和函数的执行有关,但它们是不同的概念。

  • 作用域:作用域是在函数定义时确定的,它决定了变量的可见性和生命周期。
  • 执行上下文:执行上下文是在函数执行时创建的,它包括变量对象、作用域链和this的指向。

(一)执行上下文的生命周期

执行上下文的生命周期分为两个阶段:

  1. 创建阶段:当代码执行进入一个环境时,会创建一个执行上下文。在这个阶段,执行上下文会进行以下操作:

    • 创建变量对象(Variable Object,VO):包括函数的形参、arguments对象、函数声明和变量声明。
    • 确定this的指向。
    • 确定作用域链。
  2. 执行阶段:在执行阶段,代码开始执行,变量被赋值,函数被调用,其他代码按顺序执行。

四、总结

理解作用域和作用域链的工作原理和实际应用,可以帮助你更好地理解代码的执行流程和变量的访问机制。如果你对本文的内容有任何疑问或补充,欢迎在评论区留言讨论。

面试官:讲一下HTPP缓存...

前言 不知道各位有没有在面试中被拷打HTTP缓存策略的经历,那到底什么是HTTP缓存? HTTP缓存具体有什么作用? HTTP缓存的策略是什么? 今天我们就来简单了解一下HTTP缓存。

猜成语小游戏

这是一个猜成语的网页游戏,用户根据显示的成语 首字母 来猜测对应的四字成语。 例如 ABBS = 哀兵必胜 ABJB = 按部就班 ##功能特点 随机生成四字成语的首字母缩写 用户输入猜测的成语并提交

使用 Harmony ArkTS 开发数字钱包应用(Dompet App)

Dompet.webp


推荐

  1. 使用 Flutter 开发数字钱包应用(Dompet App)
  2. 开发 ArkTS 版 HarmonyOS 日志库 —— logger

前言

在之前的 Flutter 版本中,我们探索构建了一个现代化的混合应用框架,专注于 状态管理路由管理国际化 ,并整合了 网络请求本地存储WebView 等核心能力,以便快速适配多种业务场景。

如今随着 HarmonyOS 兴起,我们将这一探索和搭建应用开发框架的方式延续到鸿蒙的设备和平台上,实现基于 ArkTS 构建现代化的 HarmonyOS 应用 开发方案。

  1. 基于 @Local@Param@Event@ComponentV2 装饰器实现数据与UI双向交互
  2. 基于 @ohos/axios 第三方库提供 Restful 范式网络请求,并实现全局的请求和响应拦截器
  3. 基于 preferencesrelationalStore 等 API 构建高效的本地数据持久化储存和读取方案
  4. 基于 ArkWebWebviewController 实现 Web 页面渲染,并支持 Web 与 App 端交互
  5. ......

选型

在构建 Harmony App 过程中,我们从 状态管理路由管理国际化 以及 日志库 等方面考虑,引入使用了 @hadss/hmrouter@ohos/axios@ohos/imageknife@hitro/arklogger 等插件。

在 UI 设计方面,我们延续了 Flutter 版本设计,继续采用 Pixso 社区 提供的 《Dompet 数字钱包》 设计风格。不过,就快捷登录部分,我们不再使用 GoogleGithub 第三方登录,而是采用了 华为账号一键登录 的方式。(需是企业开发者账号上架应用!!!)

image.png


特性

在技术上,我们选择了 @hadss/hmrouter@ohos/axiospreferencesrelationalStore 以及 ArkWeb 作为构建框架的核心基础设施,而在 UI 设计方面,则是选择了 《Dompet 数字钱包》 设计稿作为我们 App 用户界面。

在确定了 ArkTS/ArkUI 技术栈、核心插件以及 UI 设计稿后,我们开始着手梳理 Dompet 数字钱包 的 UI 界面和功能需求,以确保高效的开发效率,并成为一个用户体验极佳的 App。

  1. 使用 @hadss/hmrouter 来接管 App 的路由管理、权限拦截/认证、以及转场动画。
  2. 定义 entry/src/main/resources 中不同目录 (eg. zh_CN),实现中英文语言和资源的切换
  3. 使用 @ComponentV2 装饰器 (eg. @Local@Param@Event) 实现 UI 和数据的双向驱动
  4. 使用 relationalStore 数据库,实现用户、账单、消息、银行卡的数据流转,模拟业务处理
  5. 对于 华为本机账号一键登录 我们通过 Authentication kit 来实现其账号一键登录的流转和处理
  6. 对于 UI 设计稿中的折线图表,我们通过 ArkWebEChart 来实现其复杂的图表交互功能和效果
  7. 定义 size 单位转换/绘制 API (eg. size.vpsize.vw ...),支持自动适配不同设备的屏幕以及翻转
  8. 借助 intlsetAppPreferredLanguage 自动处理不同语言和区域的格式 (eg. 日期本地化)
  9. 通过 photoAccessHelper 插件,实现 App 上传来自相册或拍照而的得图片,进而更新用户头像
  10. 虽然 relationalStore 模拟了业务的处理和流转,但网络请求作为 App 基础设施,我们依旧对 Axios 进行了封装,完善了 Request 和 Response 拦截处理 (携带 Token、异常处理等)
  11. ......

重要插件依赖如下:

  • @ohos/axios: 一个运行在 HarmonyOS 的网络请求库,支持 Axios 原有用法和特性
  • @hadss/hmrouter: 一款功能强大的路由框架,支持自定义拦截器、生命周期、转场动画...
  • @ohos/imageknife: 一款为 OpenHarmony 打造的图像加载缓存库,提供更高效、更轻便的 API
  • @ohos/crypto-js: 一款加密算法类库,可以非常方便地在 HarmonyOS 中进行加解密 (eg. MD5)
  • @hitro/ark: 一个基于 ArkTS 实现工具函数库 (eg. 数据类型转换、debounce、Signal 等 API)
  • logger: 一个简单、实用的 HarmonyOS 应用日志框架,支持多种数据类型和日志上报 详情

源码 - Configure 配置

  • configure/axios: 基于 Axios 封装 Request 和 Response 拦截处理 (Token/异常处理、日志埋点等)
  • configure/context: 存储 App 各类运行上下文,支持跨线程引用和传输 (eg. 为 Toast 提供上下文)
  • configure/device: 存储 App 设备相关信息,例如 设备宽高、设备翻转、设备安全区、设备状态等
  • configure/network: 存储 App 设备网络、wifi 连接状态,支持自动更新网络断开、网络重连时情况
  • configure/persistent: 基于 sendablePreferences 封装,支持数据持久化存储,支持跨线程使用
  • configure/scheme: 一个 App 路由调度器,接收并处理来 Want 的信息,使其访问正确 UI 页面
  • configure/socket: 封装了 WebSocket 系统,已实现网络异常处理、断开重连、ping 机制等功能
  • configure/sqlite: 封装了 Sqlite 数据库的初始化和管理,提供了数据库的创建、关闭、销毁等操作

源码 - Components 组件

  • components/Dialog: 提供基于 ArkTS 统一封装的对话框,并扩展了 cancelconfirm 对应事件
  • components/Imager: 封装了 imageknife 图像加载缓存库,预设了 loadingerror 默认图片
  • components/Loading: 封装了一个通用的加载组件 <Loading>,支持 LoadingModifier 自定义
  • components/Toast: 结合 CtxManager 上下专注于 Toast 提示,适用于无 UI 情况 (例如 Axios)

源码 - globals 全局服务

  • globals/store: 全局响应式状态,提供了用户登录登出、语言切换、消息/订单等数据的存取和清理
  • globals/event: 全局统一事件,提供用户登录登出、业务数据同步与更新,协调数据库与状态管理

源码 - utils 工具库

  • utils/crypto: 基于 @ohos/crypto-js 封装,支持对称密钥加解密处理,例如 AES-ECBMD5
  • utils/grant: 封装权限管理逻辑,支持全局开关申请、动态权限请求、权限检测及引导跳转设置页
  • utils/size: 封装了设备尺寸相关的转换和渲染方法,用于自动适配不同屏幕尺寸和分辨率
  • utils/unit: 提供了多种单位之间的转换方法,如 lpx2vpvp2lpxany2vpany2px

注意

虽然我们设计了 华为账号一键登录 以及 获取本机手机号 的 UI 页面和功能,但是调用这些 API 却是需要企业开发者账号才能申请其相关权限,所以比较可惜,目前 Dompet App 暂时无法使用一键登录。不过我们提供了 访客模式 ,无需注册便可进行登录演示。

image.png


演示

https://linpengteng.github.io/resource/dompet-app/hap.gif 前往


GitHub

Dompet App: https://github.com/DompetApp/Dompet.harmony 前往
Webview SDK: https://github.com/DompetApp/Dompet.webview 前往

前往点个赞 Star 👍

你不知道的Javascript(上卷) | 第二章难点与细节解读(词法作用域)

作为《你不知道的Javascript》忠实读者,多次拜读该著作,本专栏用来分享我对该书的解读,适合希望深入了解这本书的读者阅读 电子书下载网址:zh.101-c.online

第二章——词法作用域

本章在《你不知道的Javascript(上卷)》中并没用较大的篇幅去描述,书中所讲主要聚焦在定义阶段,以及欺骗词法,我们的解读将由“就是这样”向“为什么是这样”转变。下面我们去讨论一些问题。

一、为何是词法作用域?

原文表述

image.png Javascript便是使用的词法作用域而非动态作用域,那么什么是词法作用域,什么是动态作用域呢?(这里做一个总结,有书中内容,也有总结内容)

词法作用域(Lexical Scope)

定义:词法作用域(也称为静态作用域)是由代码的书写结构决定的,在代码编译阶段(词法分析时)就确定了变量的作用域,不会在运行时改变。

特点

  1. 由代码结构决定:作用域在代码编写时就固定,函数的作用域取决于它被定义的位置,而不是调用的位置。
  2. JavaScript 采用词法作用域:JavaScript 的作用域规则是词法作用域。
  3. 闭包的基础:由于词法作用域的存在,函数可以访问定义时的外层变量,即使在外层函数执行完毕后仍然有效(闭包)。

示例

var a = 10;
function foo() {
    console.log(a); // 10(查找 foo 定义时的作用域,而非调用时的作用域)
}
function bar() {
    var a = 20;
    foo(); // 仍然输出 10,因为 foo 的词法作用域在全局
}
bar();

由于 foo 在全局定义,它的作用域链在编译时就确定了,即使 bar 内部有同名变量 afoo 仍然访问全局的 a


动态作用域(Dynamic Scope)

定义:动态作用域是在运行时根据调用栈决定的,函数的作用域取决于它被调用的位置,而不是定义的位置。

特点

  1. 由调用链决定:变量的查找基于函数的调用顺序,而不是代码结构。
  2. JavaScript 默认不支持动态作用域,但 this 的绑定机制(如 callapplybind)有些类似动态作用域的行为。
  3. Bash、Perl 等语言支持动态作用域

示例(假设 JavaScript 是动态作用域)

var a = 10;
function foo() {
    console.log(a); // 如果是动态作用域,这里会输出 20
}
function bar() {
    var a = 20;
    foo(); // 动态作用域下,foo 会查找 bar 的 a
}
bar();

如果 JavaScript 是动态作用域,foo 会查找调用它的 bar 的作用域,输出 20。但实际 JavaScript 是词法作用域,仍然输出 10

那么之后我们就进入本篇文章的第一个要点 “为什么Javascript要使用词法作用域,而不使用动态作用域?”,想必各位也能想出其中一二,比如动态作用域会使语言的作用域不清晰,不便于开发;动态作用域使代码性能下降等,这里我们做一个系统的分析和总结

1. 可预测性与代码可维护性

  • 词法作用域 在代码 编写时 就确定了变量的作用域,开发者能清晰知道一个变量来自哪里(例如函数定义时的外层作用域)。
  • 动态作用域 的变量查找依赖运行时调用链,导致代码行为难以预测,尤其是大型项目或嵌套调用时。

示例对比

// 词法作用域(可预测)
var x = 10;
function foo() { console.log(x); }
function bar() { var x = 20; foo(); }
bar(); // 输出 10(foo 始终访问定义时的 x)

// 如果是动态作用域(不可预测)
bar(); // 会输出 20(foo 访问调用时的 bar 的 x)

动态作用域下,foo 的输出会因调用位置不同而变化,增加调试难度。


2. 性能优化

  • 词法作用域 在编译阶段即可确定变量引用,引擎可以优化作用域链的查找(如静态分析、内联缓存)。
  • 动态作用域 需要在运行时动态解析变量,每次调用都可能重新计算作用域链,显著降低性能。

底层优化
V8 引擎通过 隐藏类(Hidden Classes) 和 内联缓存(Inline Caches) 加速词法作用域的变量访问,而动态作用域无法应用这些优化。


3. 闭包的支持

  • 词法作用域是闭包的基础:函数可以记住并访问定义时的作用域,即使在外层函数执行完毕后仍然有效。
  • 动态作用域无法实现闭包,因为变量的绑定在运行时才确定。

闭包示例

function outer() {
    var x = 10;
    function inner() { console.log(x); } // 记住 outer 的 x
    return inner;
}
var fn = outer();
fn(); // 输出 10(词法作用域允许闭包)

如果 JavaScript 是动态作用域,inner 无法可靠访问 x,因为它的值取决于调用时的上下文。


4. 模块化的天然支持

  • 词法作用域 允许通过函数嵌套和闭包实现模块化(如 IIFE 模式)。
  • 动态作用域 的变量容易受外部调用污染,难以隔离作用域。

模块化示例

// 模块模式(依赖词法作用域)
var module = (function() {
    var privateVar = 1;
    return { get: function() { return privateVar; } };
})();
module.get(); // 1(privateVar 被保护)

动态作用域下,privateVar 可能被外部代码意外修改。


5. 历史与语言设计哲学

  • JavaScript 受 Scheme/Lisp 影响:Brendan Eich 在设计 JavaScript 时借鉴了 Scheme 的词法作用域特性,强调函数式编程的简洁性。
  • 动态作用域更适合脚本语言:如 Bash、Perl,它们需要频繁依赖运行时上下文,但牺牲了可维护性。

至此,我们的第一部分完结

二、欺骗词法的性能问题

书中欺骗词法的讲述较为清晰,这里我只提一下 “欺骗词法的性能问题”

先来看一下原文表述

image.png

image.png 原文的表述主要聚焦在欺骗词法破坏了代码词法的静态分析,在一定程度上出现了“动态作用域的性能问题”,经管Javascript是词法作用域,但是欺骗词法是词法作用域的优势不再。那有没有更多的角度去分析呢?——

JavaScript 引擎(如 V8)在编译阶段会进行 静态作用域分析,优化变量访问,而 evalwith 等动态作用域操作会破坏这种优化,导致性能下降。具体原因如下:

1. 破坏作用域静态分析

(1) 词法作用域的优化机制

  • 编译阶段:引擎在代码执行前就能确定变量属于哪个作用域,生成高效的字节码或机器码。

    function foo() {
        var a = 1;
        console.log(a); // 引擎知道 a 是局部变量,直接访问栈内存
    }
    
  • 优化手段

    • 内联缓存(Inline Cache):缓存变量位置,避免重复查找。
    • 隐藏类(Hidden Class):快速定位对象属性。

(2) 动态作用域的破坏

eval 或 with 会让引擎无法在编译阶段确定变量来源,必须 运行时动态解析

function riskyEval(str) {
    eval(str); // 可能插入新变量,如 eval("var b = 2;")
    console.log(b); // 引擎无法提前知道 b 是否存在
}
riskyEval("var b = 3;");

后果

  • 引擎无法预分配变量存储位置(栈/堆)。
  • 所有变量访问退化为 慢速的动态查找(类似哈希表查询)。

2. 禁用 JIT 优化

(1) 去优化(Deoptimization)

现代 JavaScript 引擎(如 V8)会先快速生成未优化的字节码,运行中如果发现热点代码(频繁执行),则用 JIT(Just-In-Time)编译 生成优化后的机器码。
动态作用域操作会触发去优化

function unstable(x) {
    with ({ x: 1 }) {
        return x; // 引擎无法静态确定 x 的来源
    }
}
// 首次执行:生成未优化代码
unstable(10); 
// 后续执行:发现 with 无法优化,回退到解释执行

后果

  • 优化后的机器码被丢弃,回退到慢速的解释执行模式。
  • 性能可能下降 10~100 倍

(2) 无法内联缓存(Inline Cache)

  • 正常情况:引擎会缓存对象属性的内存偏移量,加速访问:

    obj.a; // 第一次访问记录位置,后续直接跳转
    
  • 动态作用域下

    with (obj) {
        console.log(a); // 无法缓存,每次都要查找
    }
    

    每次访问都需重新计算属性位置,类似 HashMap.get("a"),速度极慢。


3. 内存与安全开销

(1) 作用域链膨胀

eval 可能意外注入变量,污染作用域:

function leaky() {
    eval("var secret = 123;");
    // secret 泄漏到函数作用域,可能被闭包长期持有
}

后果

  • 变量无法被及时垃圾回收,增加内存占用。
  • 作用域链变长,变量查找时间增长。

(2) 安全风险

动态代码执行(如 eval)可能引发 XSS 攻击或意外行为,引擎会启用额外的安全检查,进一步拖慢速度。

结语

JavaScript 的词法作用域设计体现了语言在灵活性与性能之间的精妙平衡。通过静态作用域规则,JavaScript 既保证了代码的可预测性和可维护性,又为引擎优化提供了坚实基础。 这种设计让开发者能够构建复杂的模块化系统,同时享受现代 JIT 编译器带来的极致性能。词法作用域不仅是 JavaScript 的核心特性,更是理解闭包、this 绑定等高级概念的关键入口。

然而,这种优雅的设计也划定了明确的边界。任何试图"欺骗"词法作用域的操作(如 eval 和 with)都会破坏引擎的静态分析能力,导致严重的性能惩罚。 这提醒我们:在追求动态灵活性的同时,必须尊重语言的核心设计哲学。理解这些底层机制,不仅能帮助我们写出更高效的代码,更能深入体会 JavaScript 作为一门精心设计的语言所蕴含的智慧。

第二章的内容还是太少了,我更期待第三章的内容......

❌