☰
JavaScript完整学习笔记:从运行机制到浏览器实战
2026/9/24 22:35:57 网站建设 项目流程

很多自学前端的朋友找我聊JavaScript,聊来聊去发现大家的问题其实都一样:不是看不懂语法,而是不知道每个知识点到底用在哪、为什么这么设计、遇到报错怎么排查。前阵子整理自己的学习笔记时,我把JavaScript从运行环境到数据类型、函数逻辑、浏览器实操、错误处理,再到扩展插件和WebGPU这类进阶方向,完整串了一遍。这份笔记适合两类人:一类是正在系统学JavaScript的初学者,另一类是平时写业务代码但没时间梳理底层知识点的开发者。看完之后,你会对JavaScript的整个知识版图有一个清晰的坐标感,以后再遇到奇怪的问题,至少知道该往哪个方向找答案。

1. 先解决“JavaScript能干什么”的困惑:运行环境与生态版图

1.1 运行环境不只是浏览器:V8、Node.js与嵌入式场景

我见过不少初学者把一个概念记死了:JavaScript只能在浏览器里跑。实际上,JavaScript的核心规范是ECMAScript,它只定义了语法、类型、对象、关键字这些语言本身的东西,至于能不能操作页面、能不能读写文件,全靠宿主环境提供能力。

最常见的宿主是浏览器,在浏览器里JavaScript通过BOM和DOM两类API和外界交互。BOM(Browser Object Model)提供window、navigator、location、history、screen这些对象,解决“浏览器窗口是什么状态”“地址栏地址是什么”“浏览器历史怎么跳”这类问题。DOM(Document Object Model)是操作页面上元素的标准接口,所有document.querySelector、getElementById、createElement都是DOM API的范畴。很多初学者把BOM和DOM混着用,其实很简单:BOM管浏览器本身,DOM管页面内容。

在不同环境里的JavaScript还有各自的运行时扩展,这套机制不仅限于浏览器。常见的还有:

  • Node.js:把V8引擎搬到服务端,提供fs、http、path等模块,JavaScript因此能写后端服务。
  • 嵌入式脚本:比如Adobe Acrobat的PDF文档里就内置了JavaScript引擎,PDF表单校验、文件属性操作都可以用app.trustedFunction这类API实现。
  • iOS/Android原生组件里通过JavaScriptCore、WKWebView等桥接层,实现原生代码和JavaScript互相调用。

我当年第一次接触“JavaScript还能改PDF?”时也非常震惊,到现在我依然觉得,理解“JavaScript是语言,环境决定API”这件事,是学习整个语言最重要的一把钥匙。所以别再问“JavaScript能干什么”了,应该问“你打算在哪个环境里用它”。

1.2 “JS是解释型语言”这句话需要修正:JIT编译与运行机制

我经常看到教学帖说JavaScript是解释型执行、性能不如编译型语言。这个说法在十年前还有道理,放在今天早就过时了。现代JavaScript引擎(比如Chrome的V8)是一个典型的JIT(Just-In-Time,即时编译)架构:代码先被解析成抽象语法树,然后由Ignition解释器逐行执行,同时TurboFan编译器会监测热点代码,发现某段函数被反复执行,就把它编译成机器码,下次直接跑编译后的版本。

这个机制解释了几个日常现象。为什么同一个函数第一次调用慢、后面调用快?因为预热的JIT在起作用。为什么动态改变对象结构会导致性能下降?因为引擎内部已经基于你原先生成的对象形状做了优化,给对象加一个之前没有的属性,优化就直接失效回退到解释执行。

JavaScript文件虽然没有传统意义的“编译步骤”,但在真实项目里还是离不开构建工具。TypeScript要编译成JavaScript,ES Module要合并打包,代码要压缩混淆,这个环节经常被叫“编译环境”。热词里看到有人搜“javascript编译环境”,大概率就是在配这种构建工具链。别把这个跟语言的运行机制搞混。

1.3 扩展插件:JavaScript能力的边界延伸

浏览器扩展能改页面样式、拦截请求、注入脚本,本质上是宿主动态开放了更多能力。Chrome扩展现在以Manifest V3为主,核心模块包括三个:

  • content_scripts内容脚本:在匹配的页面里注入并执行JavaScript,可以操作页面DOM。
  • background service worker后台服务:常驻的脚本环境,处理扩展的全局逻辑和事件监听。
  • popup弹窗:点扩展图标弹出的HTML页面。

三者之间通过消息传递APIchrome.runtime.sendMessage和chrome.tabs.sendMessage通信。写扩展最常见的坑是权限配置,一定要在manifest.json的permissions和host_permissions里声明清楚,否则API会直接报undefined。

学JavaScript到后期,建议拿扩展做一次练手,因为你一旦能自己给浏览器写装备,对这个平台的理解深度就不一样了。

2. 类型系统与数据结构的底层认知:从变量到对象/数组

2.1 七个基础类型与一堆判断陷阱

JavaScript的数据类型分为两大类:基础类型(primitive value)和引用类型。基础类型一共七种:string、number、boolean、null、undefined、symbol、bigint。引用类型最典型的就是对象(Object),数组是特殊的对象,函数也是对象。

很多人背过这个列表,但实际操作里到处都是坑。第一个坑是typeof null === 'object',这个结果来源于早期引擎的实现,按底层二进制位判断类型时,对象类型标记以000开头,而null的存储值正好全是0,所以被误判成object。这个bug因为历史兼容性问题一直没修,你只能自己记住。

第二个坑是typeof无法区分数组和普通对象,两者都返回'object'。判断数组的正确方式是Array.isArray(arr),或者用Object.prototype.toString.call(arr) === '[object Array]'。

第三个坑是浮点数精度问题。0.1 + 0.2的结果是0.30000000000000004而不是0.3,所有浮点数计算都会遇到IEEE 754二进制浮点的精度误差。做金额计算时别直接加减小数,要么先乘100转成整数,要么引入decimal.js这种库。

对象相关还有一个隐藏坑:对象的键名会被自动转成字符串,所以obj[1]和obj['1']访问的是同一个属性;obj[true]和obj['true']也是同一个属性。当你想用对象当字典时,如果键名混入数字或布尔值,很容易出现数据覆盖的现象。原始对象属性赋值时,符号类型除外,普通对象键都遵循这套规则。

2.2 对象、数组与字符串:三种高频数据结构的操作要点

对象是描述“某一个事物”的,数组是描述“一组事物”的,字符串是描述“一段文本”的。三种结构相互配合,几乎覆盖前端日常开发的所有数据操作。

对象操作的核心是增删改查和遍历。遍历对象有几种方式:Object.keys(obj)拿到键数组、Object.values(obj)拿到值数组、Object.entries(obj)拿到键值对二维数组。for...in也能遍历对象的键,它会把原型链上可枚举的属性一起遍历出来,所以遍历时最好先用Object.prototype.hasOwnProperty.call(obj, key)过滤一下。

数组操作的核心是map、filter、reduce这三个方法。map对每个元素做映射,返回等长新数组;filter筛选出符合条件的元素,返回子集;reduce把整个数组归并成一个值。三者可以链式调用:arr.filter(fn).map(fn).reduce(fn)。链式调用的每次方法都会生成一个新数组,数据量大时会产生额外内存开销,不过日常场景完全能接受。

数组有个经典坑:用delete arr[2]删掉元素后,数组长度不会变,那个位置变成一个empty hole,遍历时forEach会跳过它但for...of会输出undefined。想真正删除某个元素应该用splice(start, count)。另外,arr.sort()默认把元素转成字符串再按字典序排序,排数字时必须传比较函数(a, b) => a - b。

字符串相关的操作有两个高频场景:切割与合并。str.split('')把字符串拆成字符数组,arr.join('')把数组拼回字符串。ES6之后拼接字符串推荐用模板字符串,比如`用户${name}的年龄是${age}`,比一串加号连接阅读性高得多。字符串是不可变的,所有方法(slice、substring、replace等)都返回新字符串,原串不受影响。

2.3 剩余参数与展开运算符:处理参数的优雅方式

剩余参数(rest parameters)和展开运算符(spread syntax)长得很像,都是三个点...,作用刚好相反。剩余参数用于把多个参数收集成一个数组,展开运算符用于把一个数组或对象拆成独立值。

剩余参数最常见的场景是处理不定数量的入参:

function sum(...numbers) { return numbers.reduce((total, num) => total + num, 0); } console.log(sum(1, 2, 3, 4)); // 10

注意剩余参数必须是函数参数列表的最后一个,后面不能再跟其他参数。

展开运算符最常见的场景是复制和合并:

const original = [1, 2, 3]; const copy = [...original]; // 浅拷贝 const merged = [...original, 4, 5]; const user = { name: '张三', age: 18 }; const userWithEmail = { ...user, email: 'zhangsan@example.com' };

对象展开属于浅拷贝,嵌套对象内部还是引用关系,这是很多面试爱考的点。如果想深拷贝,有人会直接JSON.parse(JSON.stringify(obj)),这个方法够快但会对函数、Date、undefined等类型做特殊处理,遇到循环引用还会直接抛错,适合纯数据结构的拷贝,不适合携带方法的对象。

3. 把逻辑写干净的三大支柱:条件、循环与函数

3.1 条件语句的写法与短路求值技巧

if/else是条件逻辑的地基,但JavaScript里条件判断的写法弹性非常大。首先是条件表达式里真假值的判断,六个值为falsy:false、0、''、null、undefined、NaN,其余所有值(包括空数组[]和空对象{})都是truthy。写判断时,从语义上想清楚是“有没有值”还是“是不是指定的值”,例如判断数组是否为空可以用arr.length === 0,而不要用if (arr),因为空数组也是truthy,这个判断毫无意义。

除了if/else,还有三元运算符condition ? a : b和逻辑运算符短路求值。短路求值利用&&和||的返回值特性,a && b在a为假时直接返回a,否则返回b;a || b在a为真时直接返回a,否则返回b。这意味着可以用一行代码做默认值处理:

const nickname = user.name || '默认用户'; // 上面的写法等价于 const nickname = user.name ? user.name : '默认用户';

这种写法简洁,但嵌套过多会让代码完全不可读,个人建议最多嵌套一层。ES2020还引入了空值合并运算符??,它只对null和undefined做兜底,不会把0或''也替换掉,这个细节在处理用户输入时很有用。

条件语句还有一个优化点:多个判断条件之间尽量把最可能成立的先写,利用短路特性减少后面的计算;分支特别多的时候,优先考虑用对象映射或switch而不是一长串else if,代码的可维护性会好很多。

3.2 循环语句的选择:for、forEach、for...of、map/reduce

循环是程序重复执行某段逻辑的基本手段,JavaScript提供多种循环方式,关键是知道什么时候用哪种:

  • 传统for循环:最灵活,可以控制起始条件、退出条件、步长。性能上通常也是最优的,因为没有任何额外函数调用。
  • for...in:用来遍历对象的可枚举字符串键,不推荐遍历数组,因为会把数组的非索引属性和原型链属性也遍历出来。
  • for...of:遍历可迭代对象的每个值,适用于数组、Set、Map、字符串、arguments等。需要拿到索引时配合entries(),例如for (const [index, value] of arr.entries())。
  • forEach:数组上直接调用的遍历方法,代码写起来最平和,但它的回调里无法用break或return中断整个循环,想提前退出还是得用普通for循环。
  • map/filter/reduce:这套函数式方法不只是遍历,更强调“把数组通过变换得到新结果”,上面讲数据类型时已经说过。

从实际项目中我自己的偏好排序是:能写map/filter这类链式方法时优先用它们,目的性强;遇到需要break或复杂控制流时,回退到for...of或传统for循环。切忌在forEach里通过抛异常来中断循环,这是有名的坏味道,会让后续维护的人非常痛苦。

3.3 函数是一等公民:普通函数、箭头函数与通过字符串调用函数

JavaScript里函数是“一等公民”,意思是可以像普通值一样被赋值给变量、作为参数传给另一个函数、作为返回值从函数里返回。这个特性支撑了高阶函数、闭包、柯里化等一系列高级玩法。

普通函数和箭头函数在写法上有一个关键差异:箭头函数没有自己的this,它继承的是定义时所在作用域的this。这既是优点也是坑。优点在于回调函数里不需要再const that = this这种hack,缺点在于如果你期望箭头函数的this在执行时动态绑定,那它做不到。另外,箭头函数没有自己的arguments对象,不能当构造函数,不能使用new。

函数还有一个值得聊的实用场景:通过字符串调用函数。有时后端返回的函数名是字符串,或者你从配置项里拿到一个方法名,需要动态执行它,常见的实现方式有几种:

// 方式一:通过全局对象访问 window['sayHello']('张三'); // 方式二:eval eval('sayHello("张三")'); // 方式三:new Function const fn = new Function('name', 'console.log("你好," + name)'); fn('张三');

三种方式里,eval和new Function都会把字符串当代码执行,存在代码注入风险,能不用尽量别用;在浏览器环境中,通过window[fnName]()这种方式最直接也最安全。如果在模块作用域里定义函数,它并不挂载到window上,这时候可以自己维护一个函数索引对象,把函数名和函数引用做映射:

const handlers = { edit: editFunction, delete: deleteFunction, }; handlers['edit'](id);

这种映射方式在表单提交、按钮点击这类动态分发场景里非常常见,比一串if-else判断好看得多。

4. 文本与数据处理的实战工具箱:字符串、正则、JSON与日期

4.1 正则表达式:用来匹配字符串的微型语言

正则表达式是很多人的老大难,但如果你理解了它的本质——描述字符串模式的一种微型语言——就迈过心理门槛了。JavaScript里正则的基本用法包括:

  • 创建正则字面量:const re = /abc/i,i是忽略大小写的标志。
  • 常用方法:re.test(str)判断是否匹配,返回布尔值;str.match(re)返回匹配结果数组;str.replace(re, replacement)替换匹配部分。
  • 元字符:\d数字、\w字母数字下划线、\s空白符、.任意字符。
  • 量词:*零次或多次、+一次或多次、?零次或一次、{n,m}次数范围。
  • 位置锚点:^开头、$结尾。
  • 捕获组:用圆括号()把匹配内容分组,str.match的返回数组里可以通过索引取到分组内容。

最容易踩的坑是贪婪匹配:默认情况下量词会尽可能匹配更多字符,比如/<.+>/g匹配<div>class</div>会直接吃掉整个<div>class</div>,而不是只匹配<div>。需要非贪婪匹配则在量词后加?,写成/<.+?>/g,才能匹配到第一个>为止。

另一个高频需求是合并字符串和字符串替换。你可以在replace的回调里拿到捕获组再决定替换结果,比如把日期格式从2024-01-15转成2024年1月15日:

'2024-01-15'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1年$2月$3日');

写正则建议从小到大地量测试,推荐在调试工具里写一个案列再逐步增加复杂条件,比如先只匹配数字看能不能测通,再加边界条件。在交互式终端里反复测试,比一次写一长串再去猜快得多。

4.2 JSON:数据交换的事实标准

JSON这个词几乎天天见,但很多人只停留在JSON.stringify和JSON.parse两个方法上。理解JSON的关键在于:它是一套数据序列化格式,而不是JavaScript的对象语法。JSON要求键名和字符串必须用双引号,不支持undefined、函数、Symbol、循环引用,这是与普通JS对象最大的不同。

JSON.stringify做序列化时有几个容易被忽略的行为:

  • 值为undefined、函数、Symbol的属性会被直接忽略。
  • 数组里的undefined和函数会被转成null,不是删除。
  • NaN和Infinity会被转成null。
  • 日期对象会调用自己的toJSON()方法转成ISO字符串。
  • 循环引用会抛错:TypeError: Converting circular structure to JSON。

想精细控制序列化结果,可以传第二个参数(replacer),传一个函数逐层处理键值,或传一个数组当白名单。第三参是美化缩进的空格数,比如JSON.stringify(obj, null, 2),调试时很常用。

JSON.parse的第二个参数是reviver函数,可以在反序列化时对每个键值做处理。例如把字符串形式的日期还原成Date对象:

const data = JSON.parse(text, (key, value) => { if (key === 'createdAt') return new Date(value); return value; });

用JSON.parse(JSON.stringify(obj))做深拷贝前面提过,可以用于纯数据场景,但不能有函数和循环引用。做项目时如果后端返回的对象字段可能缺失,尽量在解析后做一次字段校验或默认定值,这样可以避免“Cannot read properties of undefined”这类运行时报错。

4.3 Math、日期和异常处理的常见坑

Math对象是个静态对象,包含数学常用操作:Math.max、Math.min、Math.floor、Math.ceil、Math.round、Math.abs、Math.random等。有个细节:Math.max.apply(null, arr)可以拿到数组的最大值,但数组元素特别多时容易爆栈,ES6后写成Math.max(...arr)更整洁。

Math.floor和parseInt都像是向下取整,但行为有区别。Math.floor(2.9)返回2,parseInt(2.9)也返回2;负数时Math.floor(-2.1)返回-3,parseInt(-2.1)返回-2。因为parseInt本质是“解析字符串从开头到第一个非数字字符的部分”,第二个参数是进制,不传时遇到'0x'前缀还会按16进制解析,所以取整时优先用Math.floor。

日期操作这块,最反直觉的坑是月份从0开始。new Date(2025, 0, 1)表示2025年1月1日,getMonth()返回值0也表示1月,所以展示月份时一定记得date.getMonth() + 1。日期比较可以直接用时间戳,例如date1.getTime() > date2.getTime(),也可以直接用date1 > date2,内部会调用valueOf转数字。格式化日期时,toISOString()返回的UTC时间字符串,如果你在东八区直接截取日期,会发现差8小时,这是非常经典的时区坑。

异常处理的基本结构是try/catch/finally,finally无论是否抛错都会执行,适合做清理工作。捕获到的错误对象有message和stack两个重要属性,排查问题时看stack堆栈信息比看message有用得多,它能告诉你调用链是怎么走到的。主动抛异常用throw new Error('描述'),但注意不要抛字符串或普通对象,最好统一包装成Error实例,这样可以保证调用方捕获到的对象结构一致。

5. 浏览器里的真实世界:BOM、事件与页面调试

5.1 用document.querySelector操作页面元素:从事件派发到video旋转

document.querySelector('video')是现在最常用的元素选择方法,它接收CSS选择器字符串,返回第一个匹配元素。querySelectorAll则返回所有匹配元素的NodeList。NodeList不是数组,不能直接用数组方法,可以Array.from()转换,或者直接使用forEach方法。

浏览器热词里有两个例子很有代表性,一个是视频播放完后派发事件,另一个是旋转视频。第一个例子的代码是用document.querySelector("video")拿到视频元素,然后调用dispatchEvent(new Event("ended"))。这个操作的目的通常是想骗过播放器的事件机制:当视频真正播放到结束时,浏览器会自动触发ended事件,某些场景需要手动模拟一下来触发后续逻辑。new Event('ended')创建的是一个没有冒泡、没有默认行为的基础事件,如果需要冒泡,传入参数{ bubbles: true }。

第二个例子:

const v = document.querySelector('video'); v.style.rotate = '-90deg';

这是用CSS的rotate属性直接旋转元素。注意这里的rotate是CSS Transforms Level 2里新提供的独立属性,可以直接赋字符串带角度单位,比用transform: rotate(-90deg)更简洁。多视频同时旋转时,所有元素都以自己中心为旋转圆心。如果想以其他点为圆心,还是要回到transform-origin加transform的组合。

这两个例子说明,很多浏览器里的“奇技淫巧”,背地里都是浏览器原生API的基本组合:查元素、改样式、派发事件。把BOM和DOM的底子打牢,看到任何类似的小技巧都能一眼看穿原理。

5.2 模拟input输入的两种思路及背后的机制

模拟输入是自动化测试和油猴脚本里的高频需求。但直接给input元素赋值是不会触发响应式框架的更新机制的,因为Vue、React这类框架监听的是input事件,而不是元素value属性变化。正确做法是分两步:第一步用原生的value setter去修改值,第二步派发input事件。

const input = document.querySelector('input'); const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; nativeSetter.call(input, '模拟输入的内容'); input.dispatchEvent(new Event('input', { bubbles: true }));

为什么要用Object.getOwnPropertyDescriptor拿到setter?因为现在大多数浏览器和框架都重写了HTMLInputElement的value属性,用input.value = 'xxx'这种直接赋值方式写在控制台里看起来生效了,但内部的原生setter不一定被触发,在自动化环境里很容易出现“值变了但框架没感知”的情况。通过原生的value setter绕过重写,再显式dispatchEvent,是Shadydom里比较稳妥的模拟方案。

对于React应用,有时候还需要额外处理InputEvent而不是普通Event,且事件类型可能区分input和change,建议两步都派发一次,避免某些组件只监听了change导致状态不刷新。

5.3 检查静态资源是否加载完成的常用思路

前端经常需要确认图片、脚本、样式等静态资源是否加载成功,尤其是做预加载和懒加载时。最基础的做法是给资源元素绑load和error事件:

const img = document.querySelector('img.logo'); img.addEventListener('load', () => console.log('图片加载成功')); img.addEventListener('error', () => console.log('图片加载失败'));

如果要检查页面上所有已完成加载的资源,用performance.getEntriesByType('resource')可以查到所有资源条目的加载耗时、大小、初始化时间、requestStart等指标,这是Performance API提供的强大能力,比逐个挂载事件高效得多。想判断某个具体的资源是否被缓存,可以检查该条目的transferSize是否为0,为0说明走的是缓存没有网络传输。

另外一个判断页面加载状态的基础方法是document.readyState,它有三种状态:loading表示文档加载中、interactive表示DOM解析完成但子资源还在加载、complete表示所有资源都加载完成。监听DOMContentLoaded事件时readyState通常是interactive,window.onload触发时readyState是complete。需要尽早执行逻辑监听前者,需要等所有资源都就绪监听后者,两者别搞混。

5.4 javascript:void(0)是什么?为什么地址栏有时会拒绝执行

javascript:开头的东西叫伪协议,常见两种用法:一种是<a href="javascript:;">,目的是让点击链接时不做页面跳转;另一种是直接在浏览器地址栏输入javascript:...来执行一段代码。void运算符的语义是“执行后面的表达式,但永远返回undefined”,void 0在所有JavaScript版本里都可靠地表示undefined,所以javascript:void(0)字面意思就是“执行一个空操作并返回undefined”。

早期网页里大量使用javascript:void(0)阻止a标签默认行为,现在不推荐了。原因有几个:伪协议会把代码直接暴露在href里,不方便维护也容易引入XSS;更好的方式是给事件处理函数里event.preventDefault(),或者在HTML里就用<a role="button">这样的语义标签。Chrome等浏览器出于安全考虑,目前禁止在地址栏里通过粘贴方式执行JavaScript代码,输入历史中留存的javascript:伪协议也大多不再执行,这正是很多人在谷歌浏览器里发现javascript:void(o)没反应的原因。

顺带一提,你可能会看到复制公众号名称javascript:;这种搜索词,本质上是一些网站把复制操作绑定到了javascript:;伪协议的链接上,通过点击事件回调里执行复制逻辑。这是历史遗留写法,了解原理即可。

6. 从学习笔记走向真实项目:运行时报错、资源检测与进阶方向

6.1 运行时报错的类型与完整排查链路

JavaScript的报错不是玄学,几乎都能通过控制台堆栈找到根因。常见的错误类型分成几类:

  • SyntaxError:语法错误,代码根本没能解析执行,通常是少括号、多逗号、使用了非法语法。
  • ReferenceError:引用错误,访问了一个不存在的变量,最常见的场景是拼写错误或者变量还没初始化。
  • TypeError:类型错误,比如undefined is not a function、Cannot read property 'xxx' of undefined,这是日常开发里最多的错误。
  • RangeError:范围错误,比如数组长度超出限制、递归调用栈溢出。

排查运行时错误的完整链路,我的习惯是四步走。第一步先看控制台错误信息的第一行,那行字通常已经说明了错误类型和出错文件:行号。第二步展开堆栈(stack),看清调用链,也就是这个错误是被谁调起来的。第三步根据错误信息里的变量名,在代码里搜索该变量的定义处和使用处,重点检查它是否在异步回调、事件处理函数里已经变成undefined。第四步在可疑代码处打断点,刷新页面逐步查看变量实时值。

遇到“第3关:javascript错误”这类教程关卡,本质就是让你练习这个排查链路。错误信息只是结果,真正关键的是通过堆栈定位到出错的上下文,然后思考“为什么这个变量在这里会是undefined”,而不是对着错误信息发呆。

6.2 浏览器扩展插件:把页面变成你的试验场

前面提到扩展的模块结构,这里再补充一个实战细节:内容脚本运行在一个隔离环境中,它访问到的window和页面本身的window并不是同一个对象。脚本里定义的全局变量不会泄漏到页面,页面里已有的全局变量也未必在内容脚本中直接可见。这也是很多扩展遇到的“为什么注入那么多次都没反应”的原因。

想在内容脚本里调用页面自己的函数,可以通过注入一个<script>标签的方式,把代码插到页面的主world执行,或者用window.postMessage与页面脚本通信。扩展之间的消息走chrome.runtime,扩展与页面之间走window.postMessage加上事件监听,这套机制是扩展开发绕不开的核心知识点。

另外,MV3的service worker不是常驻的,浏览器会在空闲时把它终止,所以不要在全局状态里保存重要信息,要用chrome.storage持久化。这个特性会让很多习惯写常驻后台脚本的开发者踩坑,但习惯了之后会发现整体内存占用确实低很多。

6.3 边界拓展:从OC与JavaScript互相调用到WebGPU与3D高斯泼溅

学习JavaScript到后期,你会发现它的身影无处不在。“OC和JavaScript互相调用”是iOS开发里的经典话题:UIWebView/WKWebView通过WKScriptMessageHandler让JavaScript调用原生方法,原生通过evaluateJavaScript调用网页里的JavaScript函数。双方传递的数据必须是能序列化成JSON的简单类型,传不了复杂对象和函数引用,这是桥接层的基础限制。

“PDF JavaScript”则是Adobe Acrobat扩展的JavaScript API,可以在PDF文档里写脚本实现表单校验、域联动、文档打开时自动执行动作。app.trustedFunction用于提升函数的信任级别,让一些受限操作在非特权上下文里也能执行。这套东西虽然小众,但足以说明JavaScript的宿主环境有多丰富。

前端领域里的重头戏是WebGPU。以热词里出现的splat.js为例,它是一个纯JavaScript加WebGPU实现的3D高斯泼溅渲染方案。3D高斯泼溅是近年三维重建和实时渲染的热门方向,思路是用大量三维高斯函数表示物体表面的空间分布,而不是像传统网格那样用三角形顶点去描述。WebGPU能为这种并行计算提供着色器管线和计算管线,让浏览器里直接跑密集型渲染成为可能。

这套方案的特别之处在于它不需要任何后端计算支持,全部工作在GPU里完成,高斯点的排序、投影、渲染都能靠WebGPU的compute shader并行处理。虽然WebGPU的环境支持还不够全面,但它是浏览器图形能力的未来方向。

如果你已经熟练掌握了前面这些基础,完全可以按这个方向去延伸:先弄清WebGPU的适配器、设备、缓冲区、绑定组这些核心概念,再试着跑通一个最小例程,最后再去看splat.js这种完整方案,收获会非常大。JavaScript的学习没有天花板,但它永远是从数据类型、函数、正则、JSON、DOM这套基础开始生长的。

最后分享一点个人体会:学JavaScript千万别沉迷于刷语法题和背面试题,最好的方式永远是带着一个具体的小目标去写——哪怕是写一个旋转视频的书签脚本、一个自动填表的扩展、一个PDF表单校验的脚本。写完发现问题,再回来看这份笔记找对应的原理,你就把知识真正变成自己的了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询