有一个很有意思的热搜问题:为什么很少人愿意手写JS。我的答案可能和你想的不一样——不是大家懒,而是大部分人的原生JS底子没打牢,一遇到问题就下意识“先上网搜个框架”,结果搜出来的东西越看越糊涂。这篇JS速通简要版,不打算把文档再抄一遍,而是把我在写代码和带新人过程中被卡得最狠、被搜索最多的十几个知识点集中梳理一遍:函数与对象的暧昧关系、原型链的查找逻辑、闭包、事件循环、this指向,再到三级联动、iframe通信、日期格式化这类具体业务片段。适合刚学完语法但写不出业务的读者,也适合准备面试想快速把知识串起来的老手。
1. 先把JS的根基聊透:对象、函数与原型链
1.1 函数也是对象,这句话到底意味着什么
刚刚接触JS的人,十有八九都被“js中函数是对象吗”搞懵过。我的答案是:函数是对象,而且是Function构造器创建出来的特殊对象。你可以把函数当普通值用——存到变量里、塞进数组里、当作参数传出去、再当作返回值带回来,这就是所谓的一等公民。
function add(a, b) { return a + b; } add.customProperty = '给函数挂属性'; console.log(add.name); // "add" console.log(add.length); // 2,形参个数 console.log(add.toString()); // 函数源码字符串 const fn = add; fn(1, 2); // 正常调用为什么这个认知很重要?因为如果你不理解“函数是对象”,你就理解不了高阶函数、柯里化、闭包,更理解不了现代框架里到处都在用的回调函数和装饰器。面试官问这个,不是考你冷知识,而是看你能不能打通“函数即值”这条线。我见过不少写过两三年业务的人,一提到函数还是只能想到function和(),这种人对框架源码基本是望而生畏的。
理解“函数是对象”之后,JS很多行为的理由就浮出水面了。函数自带name、length、prototype等属性;函数可以动态添加属性;函数可以像对象一样被Object.assign拷贝。反过来,普通对象却不能像函数那样被调用。所以在JS的这一套体系里,函数和对象不是对立关系,函数是对象的一个子集,只不过它多了一个“可调用”的能力。
1.2 原型链:属性找不到时的“向上搜索路径”
说完了函数和对象,就得说它们之间的纽带——prototype。我把原型链总结成一句话:当你访问某个属性时,JS会先看对象自身有没有;没有就顺着__proto__去构造函数的prototype上找;再找不到就继续往上,直到撞上Object.prototype,再往上就是null,搜索结束。
function Person(name) { this.name = name; } Person.prototype.sayHi = function() { return 'Hi, ' + this.name; }; const p = new Person('张三'); console.log(p.name); // "张三",来自对象自身 console.log(p.sayHi()); // "Hi, 张三",来自Person.prototype console.log(p.toString()); // 来自Object.prototype console.log(p.__proto__ === Person.prototype); // true console.log(Person.prototype.__proto__ === Object.prototype); // true很多人知道数组有map、filter,但不知道这些方法其实是Array.prototype上的属性。let arr = [1, 2, 3]; arr.map(...)之所以能用,是因为arr自身没有map,于是JS沿着arr.__proto__找到了Array.prototype.map。明白这个搜索过程后,很多“为什么能用”的问题就不需要死记硬背了。
原型链最有用的实操场景,是给内置对象扩展方法,或者做基础的面向对象复用。比如你想给数组加一个去重方法:
Array.prototype.unique = function() { return [...new Set(this)]; }; console.log([1, 1, 2, 3].unique()); // [1, 2, 3]但我要提醒一句:给内置原型挂方法在公司项目里是禁忌,因为团队里其他人可能遍历属性或者用了同名方法,容易互相污染。个人项目和教学练手随意,写生产代码千万别这么干。
1.3 继承别背套路,理解背后的对象关系
ES6的class出来后,很多人以为JS终于变成了正经的面向对象语言。其实class只是语法糖,底层还是原型链。速通阶段你需要掌握的是两种最常见的写法。
ES6类的写法:
class Animal { constructor(name) { this.name = name; } speak() { return `${this.name} 在叫`; } } class Dog extends Animal { constructor(name, breed) { super(name); // 调用父类构造器,先完成父类初始化 this.breed = breed; } speak() { return `${this.name}在汪汪叫`; } }使用Object.create的写法:
const animal = { init(name) { this.name = name; return this; }, speak() { return `${this.name} 在叫`; } }; const dog = Object.create(animal); dog.init('小黑').speak(); // "小黑 在叫"面试的时候如果被问“class和原型链有什么关系”,你就回答“class的extends最终会被转成原型链上的链接,子类实例找不到方法时,会顺着子类原型链去父类原型上找”。这句话一说,懂行的面试官就知道你搞懂了幕后机制。学习继承时不要沉溺于“模式大全”,先把“子类实例->子类原型->父类原型->Object.prototype”这条线走通,所有继承写法在你眼里都只是实现这条链的不同姿势。
2. 运行时那点事:作用域、闭包、事件循环和this
2.1 闭包就是“函数记住了自己出生时的周围环境”
闭包被讲得神乎其神,其实它的定义就一句:函数在定义时,会把当时所在的作用域“打包”保存下来,哪怕后来这个函数被拿到别处执行,它依然能访问当初作用域里的变量。怎么验证?我工作中最常用的计数器例子:
function createCounter() { let count = 0; return function() { count += 1; return count; }; } const counterA = createCounter(); console.log(counterA()); // 1 console.log(counterA()); // 2 const counterB = createCounter(); console.log(counterB()); // 1,和counterA互不干扰四个变量count在这里没有被销毁,正是因为内部返回的函数“包住”了它。这也是闭包最常见的用途之一:创建私有状态。另一个高频场景是防抖——搜索框输入时不想每次按键都发请求,就利用闭包保存定时器变量:
function debounce(fn, delay = 300) { let timer = null; return function(...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }这里timer就是闭包包裹的变量,多次调用触发的是同一个范围。防抖函数几乎每个前端项目都写过,面试题里出现频率极高,值得反复手写。
闭包最容易翻车的点是for循环配合var。看这段:
for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 0); } // 输出:3 3 3问题在于var是函数级作用域,循环里三个异步回调共用了同一个i,循环结束时i已经是3。解决方式有两种:把var换成let,让每次循环产生新的块级作用域;或者用闭包立刻执行函数传值。理解了闭包,你就能理解为什么老代码里那么多(function(i){...})(i),也能理解为什么现在大家无脑用let——问题在底层被语法解决了。
2.2 事件循环:单线程如何不卡死
JS是单线程语言,但它能一边处理点击、一边发请求、一边跑动画,靠的是事件循环机制。速通版本的理解方式是:JS把代码分成同步任务和异步任务,同步任务先执行,执行完后再看任务队列里排队的异步任务。异步任务内部还分微任务和宏任务,微任务比宏任务先执行。
console.log(1); setTimeout(() => { console.log(2); }, 0); Promise.resolve().then(() => { console.log(3); }); console.log(4); // 输出顺序:1 4 3 2这行代码解释了三个关键结论:setTimeout回调不会立刻执行,哪怕延时是0,也得等当前同步代码跑完;微任务(Promise.then)优先于宏任务(setTimeout);JS在执行顺序上的确定性,让异步代码的可预测性比想象中高。你不需要背复杂的任务队列模型,但至少要能预测上面这段代码的输出,因为这是面试和团队协作中用来判断你会不会写异步代码的试金石。
事件循环还解释了“为什么大量同步计算会卡页面”。如果你在浏览器里跑一个大循环,同步代码把主线程占满,后面的点击、滚动事件全部排队,页面就表现为“卡死”。这时候要么用Web Worker把计算拆出去,要么把大任务拆成小分片放进宏任务队列。看见事件循环不只是为了面试,它直接决定了你怎么设计耗时任务的调度方式。
2.3 event对象里那几个属性,别再傻傻分不清
搜索词里有“js 事件中的 event”,说明这块确实迷惑了不少人。事件处理函数收到的event对象,常用的其实就这几样:type(事件类型)、target(实际触发事件的最深层元素)、currentTarget(当前绑定监听的元素)、preventDefault()(阻止默认行为)、stopPropagation()(阻止冒泡)。
document.querySelector('.outer').addEventListener('click', function(e) { console.log('target:', e.target); // 我点的是谁 console.log('currentTarget:', e.currentTarget); // 监听器绑在谁身上 });target和currentTarget是最容易被搞混的一对。我在实际调试中经常看到同事写业务代码,判断“点的是不是这个按钮”,结果用了e.currentTarget,在多层结构里判断失误。记住:如果你需要知道用户真正点到的是哪个元素,用e.target;如果你需要知道当前处理函数挂在哪里,用e.currentTarget。
preventDefault的用处是你不想让表单提交刷新页面的时候,不想让a标签跳转的时候;stopPropagation的用处是点击弹窗内部时不想让事件冒泡到遮罩层从而触发关闭。这两个方法在三级联动、弹窗组件、表单校验里几乎天天用。
2.4 this的四条规则,五分钟不再犯迷糊
this是JS里最玄学的话题,但总结下来其实只有四条规则,按优先级排序:
- new调用:this指向新创建的对象
- call/apply/bind显式绑定:this指向传入的第一个参数
- 对象方法调用:this指向调用方法的对象
- 独立函数调用:非严格模式下指向window,严格模式下是undefined
const user = { name: '张三', say() { console.log(this.name); } }; const fn = user.say; fn(); // 严格模式下报错或undefined,独立调用 user.say(); // 张三,对象方法调用 user.say.call({ name: '李四' }); // 李四,显式绑定箭头函数比较特殊,它没有自己的this,体内的this完全继承自定义时所在的外层作用域。这也是为什么在setTimeout回调里,传统函数拿不到外层this,箭头函数却可以:
const user = { name: '张三', sayLater() { setTimeout(() => { console.log(this.name); // 箭头函数继承了sayLater的this,输出“张三” }, 100); } };判断this的实操方法是:先看是不是箭头函数,不是的话看前面有没有“点”,有“点”就指向点前面的对象;没有就再想有没有call/bind;最后只剩独立调用和严格模式的问题。有了这个顺序,绝大部分this相关题目都能秒解。
3. 业务代码里十次有八次会踩的小坑速查
3.1 判断字符串是否包含,到底该用哪个方法
“js判断字符串是否包含”是高频搜索词。旧时代大家习惯用indexOf,因为返回的是位置,需要判断不等于-1。ES6之后有了includes,语义更直白,直接在流程里做判断。
const str = 'Hello JavaScript World'; console.log(str.includes('JavaScript')); // true console.log(str.indexOf('JavaScript') !== -1); // true,老写法 console.log(str.startsWith('Hello')); // true console.log(str.endsWith('World')); // true console.log(str.search(/java/i)); // 6,支持正则,返回索引三个方法的取舍很简单:想检查“包含不包含”,用includes;想拿到具体位置做截取,用indexOf;想按正则匹配,用search或者test。需要注意,includes、startsWith、endsWith都区分大小写,要忽略大小写就先把两边都转成.toLowerCase()再比较,或者用正则的i标志。这些API看起来简单,但选错了,代码读起来会非常别扭。
3.2 判断“空对象”,一行代码真的够吗
“判断是不是空对象 js”搜索量常年居高不下。最常看到的答案是Object.keys(obj).length === 0,但这个答案有一个漏洞:如果对象的属性值是symbol或者不可枚举属性,Object.keys根本看不见。
const obj1 = {}; const obj2 = { a: 1 }; console.log(Object.keys(obj1).length === 0); // true console.log(Object.keys(obj2).length === 0); // false const obj3 = {}; Object.defineProperty(obj3, 'hidden', { value: 1, enumerable: false }); console.log(Object.keys(obj3).length === 0); // true,但并不是真正意义上的空如果写通用工具函数,我建议补上Symbol的判断:
function isEmptyObject(obj) { return ( Object.keys(obj).length === 0 && Object.getOwnPropertySymbols(obj).length === 0 ); }另外别忘了,null和undefined不能直接过这个函数,要先做判空。普通业务项目里Object.keys版本足够用,但你要是面试遇到这道题,补上Symbol和不可枚举属性的考虑,会比标准答案高一个档次。
3.3 循环里break/continue/return到底能不能用
“js for循环跳出循环”能成为热搜词,说明有太多人在forEach、map这些数组方法里栽过跟头。先说结论:在forEach里return只能结束本轮迭代,相当于continue;forEach里break会直接抛语法错误;map同理,它存在的意义是返回一个新数组,不是你用来提前停循环的工具。
要想跳出所有迭代,有几个办法。一个是改成传统for循环或for...of,直接break:
for (const item of list) { if (item === target) { break; // 跳出整个循环 } }另一个是“借用”数组的some或every方法,利用它们的短路特性提前终止:
list.some((item) => { if (item === target) { return true; // 这里return true会让some立即停止 } return false; });如果你遇到多层嵌套循环想要一起跳出,也要用标签语句,但标签语法容易让人读不懂,我不太推荐在公司代码里用它。更优雅的做法是把循环体抽成函数,用return从函数层面提前返回。顺带说一句,性能敏感的大数组遍历,传统for循环通常比forEach快不少,这也是为什么很多开源库的内部实现还坚持用for循环的原因。
3.4 一组容易看懵的比较逻辑
速通再快,也得把这几个基础概念捋平:null是“空值”,它被设计成指向空对象;undefined是“未定义”,通常表示变量声明了但没赋值。用==的时候,null和undefined会被当成相等,所以if (x == null)能同时覆盖两种空值;用===时必须严格区分。
typeof null的结果是"object",这是一个历史遗留bug,不用跟它较劲,知道就行。数组判断用Array.isArray(arr),不要用typeof arr === 'object',因为对象和数组都是object。整数判断也别用x === parseInt(x),直接用Number.isInteger(x)更干净。
console.log(null == undefined); // true,全等是false console.log(typeof null); // "object",历史遗留 console.log(Array.isArray([1])); // true console.log(Number.isInteger(1.0)); // true这些知识点单看每个都很简单,但它们恰恰是“看不懂老代码”的根本原因。老项目里那些写着玩的==、奇怪的typeof判断,会让你在维护的时候一脸问号。把底层规则吃透,读代码的速度至少快一倍。
4. 拿这几个业务场景练手:三级联动、日期格式化与iframe通信
4.1 省市区三级联动:看上去很复杂,核心就两步
“js三级联动”是前端入门的老牌实战题。它背后考察的其实是两个能力:数据结构和事件驱动的配合。本质上就是:省份变化时,根据省份的key重新渲染城市下拉框;城市变化时,再根据城市的key重新渲染区县下拉框。我通常用嵌套对象作为数据源:
const areaData = { 北京市: { 北京市: ['东城区', '西城区', '海淀区'] }, 广东省: { 广州市: ['天河区', '越秀区', '海珠区'], 深圳市: ['福田区', '南山区', '宝安区'] } };初始渲染省份下拉框后,监听province的change事件,把对应城市数据取出并填充city下拉框,同时清空area下拉框;再监听city的change,填充area下拉框。
provinceSelect.addEventListener('change', function() { const cities = areaData[this.value] || {}; citySelect.innerHTML = '<option value="">请选择城市</option>' + Object.keys(cities).map(c => `<option value="${c}">${c}</option>`).join(''); areaSelect.innerHTML = '<option value="">请选择区县</option>'; }); citySelect.addEventListener('change', function() { const province = provinceSelect.value; const districts = areaData[province]?.[this.value] || []; areaSelect.innerHTML = '<option value="">请选择区县</option>' + districts.map(d => `<option value="${d}">${d}</option>`).join(''); });这个场景里的坑在“回显”。如果编辑页面里已经保存了省市区,你在初始化时不能只填充三个下拉框,还得在填充后手动触发change事件,或者把change事件写成函数单独调用。我当时在做后台管理系统时,被这个回显bug卡过整整一个下午,后来总结出的经验是:三级联动一定要把“数据渲染”和“事件触发”拆开,初始化时直接调用渲染函数,而不是模拟用户点击。另外注意,真实项目的数据结构不会像我上面写得那么整齐,很多后台接口返回的是一维数组,需要你先把数组转换成树形结构,这里考验的就是数组reduce和对象映射的基本功了。
4.2 日期格式化:为什么我不建议你直接toLocaleString
“js格式化”这个热搜词下,最常被问到的是日期格式化。很多新手图省事,直接new Date().toLocaleString(),结果在不同浏览器和系统语言环境下,输出格式五花八门:有的是“2026/03/15 14:30:00”,有的是“2026年3月15日”,还有的带星期几。业务要的是稳定格式,所以正确姿势是自己封装一小段格式化函数,或者用dayjs这类库。
function formatDate(date, fmt = 'YYYY-MM-DD HH:mm:ss') { const pad = (n) => String(n).padStart(2, '0'); const map = { YYYY: date.getFullYear(), MM: pad(date.getMonth() + 1), DD: pad(date.getDate()), HH: pad(date.getHours()), mm: pad(date.getMinutes()), ss: pad(date.getSeconds()) }; return fmt.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) => map[match]); } console.log(formatDate(new Date())); // 2026-03-15 14:30:00 console.log(formatDate(new Date(), 'YYYY/MM/DD')); // 2026/03/15这段代码里最值得学习的是思路:先把需要替换的字段放进一个映射表,再用正则匹配模板字符串,用回调函数逐项替换。这种“模板+映射表”的模式在导出报表、生成文件名、拼接日志前缀时都很好使。遇到月份或日期只有一位数的情况,padStart补零的小细节直接决定输出是否符合预期。如果你连padStart都不熟,这段函数就是最好的复习材料。
除此之外,Date对象还有很多坑:getMonth()从0开始,所以月份要加1;getDay()返回的是星期几的数字,不是文本;两个日期相减得到的是毫秒数,要换算成天数得除以86400000。这些细节面试时经常混在一起考,建议自己动手写一个完整的格式化函数,把Date对象所有API过一遍,比看十篇教程都扎实。
4.3 iframe里关掉自己并刷新父页面:老场景背后的新姿势
搜索词里“iframe+关闭+jquery+并刷新+父页面+js”摆在一起,说明这是个真实存在了很久的需求。在jQuery时代,大家习惯直接用window.parent操作父页面的DOM:
// 子页面里(jQuery老写法) window.parent.document.getElementById('myIframe').style.display = 'none'; window.parent.location.reload();这种写法确实能用,但它有两个问题:一是子页面直接操作父页面DOM,耦合度太高;二是如果父页面和子页面的域名不一致,浏览器会出于安全策略拦截跨域访问,直接报错。现代推荐的方案是postMessage,父页面和子页面都只暴露一个“消息接口”,关闭和刷新通过消息传递,互不直接碰对方的DOM:
// 子页面:发消息,要求父页面关闭iframe并刷新 window.parent.postMessage({ type: 'close-and-reload' }, 'https://parent.example.com');// 父页面:监听消息,处理关闭与刷新 window.addEventListener('message', function(e) { if (e.origin !== 'https://parent.example.com') return; // 校验来源 if (e.data.type === 'close-and-reload') { document.getElementById('myIframe').style.display = 'none'; window.location.reload(); } });postMessage里的第二个参数和父页面里的e.origin校验,是防止其他网站伪造消息往你页面里塞内容的护城河。开发时图省事写成*或者不校验来源,上线后容易被恶意页面钻空子,这点务必留个心眼。如果用的是iframe UI组件库,比如一些弹窗iframe方案,一般组件内部封装好了close方法,优先看组件文档,别自己再去碰parent。即使是老项目里的jQuery,我也建议逐渐把这种通信方式改成postMessage,至少耦合度低,后面替换框架时改动面小。
4.4 异步请求的进化史:从回调地狱到async/await
“js函数”“异步”相关的搜索词背后,藏着大量写不好异步的代码。我用一个最简单的fetch用户数据场景,展示这套进化:
回调用法,层层嵌套,一旦业务复杂,就会变成面试题里臭名昭著的“回调地狱”:
fetch('/api/user') .then(res => res.json()) .then(user => { fetch(`/api/order?userId=${user.id}`) .then(res => res.json()) .then(orders => { console.log(orders); }); });改用async/await后,代码看起来跟同步逻辑一样:
async function loadUserOrders() { const res = await fetch('/api/user'); const user = await res.json(); const orderRes = await fetch(`/api/order?userId=${user.id}`); const orders = await orderRes.json(); console.log(orders); }关键要理解await到底做了什么:它把后面的Promise“摊平”了,当前函数会暂停在这个位置,等Promise解决后再继续往下走。所以async函数里的try/catch能捕获await抛出的异常,这比.then的链式错误处理直观很多。要注意的是,async/await并不是把异步变成了同步,它只是语法层面的转换,事件循环里的微任务机制并没有消失。如果你在循环里写await,要注意串行和并行的差别:for循环一个个等是串行,用Promise.all一次性等是并行。
async function loadAll(ids) { const results = await Promise.all( ids.map(id => fetch(`/api/item/${id}`).then(r => r.json())) ); return results; }这里Promise.all能并发发起所有请求,整体耗时取决于最慢的那个而不是所有请求之和。错误处理时用try/catch包住Promise.all,任何一个请求挂了都会走catch。如果只要“全部成功”的场景,Promise.all最合适;如果希望“谁先完成谁先渲染”,可以把Promise.all换成Promise.allSettled或者逐条处理。
5. 从写脚本到建项目:Node环境与常用库速览
5.1 Node.js装好了以后,你的JS才能跑出浏览器
“node js安装”上来就是热搜词,说明越来越多的人想用JS写点后端或工具脚本。Node.js装好之后,你在终端里敲node -v能看到版本号,敲npm -v能看到包管理器版本号。npm install就是你的“插件市场入口”,装完的依赖放在node_modules目录里。
入门阶段最容易忽略的是package.json的作用。它不只是给项目取名用的,它记录了你项目的依赖列表、脚本命令,比如你用npm init -y生成一个默认配置后,可以给package.json加scripts:
{ "scripts": { "start": "node index.js", "dev": "node --watch index.js" } }之后在终端里npm start就能跑起来。node --watch是Node 18之后的原生热重载能力,改完代码自动重启,小项目完全不需要再引入nodemon。我见过不少新手,装了Node之后第一反应是去搜“怎么让JS文件双击就能运行”,其实正确姿势是打开终端,cd到项目目录,用node xxx.js执行。这样你才能看到控制台输出和报错信息,这也是Node学习的第一步。
5.2 想用JS写后端,框架到底怎么选
“想用js写一个后端项目,用什么框架好”这个问题,答案取决于你的项目规模。我给一个不绕弯子的建议:
- Express:最老牌,文档多,中间件模型简单直接,中小型项目、快速原型首选。
- Koa:由Express原班人马打造,基于async/await,中间件是洋葱圈模型,现代感更强,适合追求简洁的个人项目。
- NestJS:基于TypeScript,自带依赖注入、模块化、装饰器,是大团队和复杂业务的首选,学习曲线也最陡。
- 不加框架,只用Node内置的http模块写一个几十行的接口,用来验证思路、写小工具,完全可行。
// 用Node内置http模块写一个极简接口 const http = require('http'); http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ message: 'Hello JS' })); }).listen(3000, () => console.log('running at http://localhost:3000'));初学者选框架最大的误区是“别人说哪个火就学哪个”,结果被NestJS的依赖注入绕晕,连接口都没写出一个就放弃了。我的建议是先从Express写起,把路由、中间件、请求参数这几个概念跑通,再去看Koa或者NestJS,你会发现框架的核心逻辑都是一脉相承的。作为全栈方向的第一步,甚至可以先不碰框架,用http模块写一个待办事项的接口,把CRUD和JSON响应踩一遍,然后再换成框架,你会深刻理解框架到底帮你省了什么活。
5.3 办公和项目里会用到的JS库速查
搜“js生成word文档有哪些js库”这类词的朋友,多半是在做报表、导出、文档处理类的活。我把自己常用的库整理成一张表,按需取用:
| 需求 | 推荐库 | 适用场景 |
|---|---|---|
| 日期处理 | dayjs | 体积小,API友好,格式化、加减日期很方便 |
| 表格增强 | DataTables | 给普通HTML表格加排序、搜索、分页,几行配置就能上线 |
| 生成Word | docx | 在Node端或浏览器端生成、编辑.docx文档,支持段落、表格、图片 |
| PDF预览/缩略图 | pdf.js | 浏览器内渲染PDF,可以做缩略图和水印 |
| PDF导出 | jsPDF | 把页面内容或文字导出成PDF文件 |
| Excel读写 | xlsx / exceljs | 解析和生成Excel文件,适合批量导入导出 |
这些库的使用套路几乎一样:先npm install,再看官方文档示例,最后把示例里的数据结构换成你自己的。不要指望记住所有API,能把示例代码跑通、会查文档索引就够了。我用docx生成合同文件时,最有效的路径就是先拿官方的“Hello World”示例跑通,再逐段替换内容。没有哪个库你只看文档就能精通,上手跑一遍永远是最高效的。
还有一类JS应用场景是办公自动化,比如WPS的JS宏,语法本质就是JS,你可以用它在WPS里批量处理表格和文字。这类脚本的价值被很多人低估了,实际上处理重复性办公任务时,一段几十行的JS宏能顶一上午的人工操作。原理和Node脚本并无区别,只换了运行环境。
5.4 搜索资料时的一个安全提醒
你在搜索引擎里可能经常看到“js逆向”“js反爬实战”这类热搜词,我特别想多说一句:学习JS的语法和原理没有错,但“逆向”和“反爬”这两个词背后,大多涉及绕过他人网站系统防护的技术。没有明确授权就去分析和破解别人的系统,轻则账号被封,重则惹上法律麻烦。我不是假清高,是真见过有人因为毕业设计里用了类似技术,最后答辩没通过还被平台发了律师函。学框架、学算法、学网络安全攻防原理都可以,但请在合法授权范围内做实验,比如自己搭一个靶场来练手,别把技术在别人家的系统上试。
最后再分享一点我的个人经验
总有人问我:“JS速通是不是把语法书看一遍就行?”我的答案从来不是。速通的本质是“高频卡点集中扫盲”,语法看十遍,不如把原型链、闭包、事件循环、this指向这四件事用代码亲手验证一遍,再拿三级联动、iframe通信、日期格式化、异步请求这几个场景练手。我带了几年新人,发现能把“函数是对象”“原型链是搜索路径”“await是语法糖”这三句话讲清楚的人,通常三个月内就能独立接项目;反之一头扎进框架源码的人,反而容易越学越没底。
最后给你一个我一直在用的小技巧:每天晚上花十五分钟手写一个mini实现,比如手写bind、手写new、手写防抖节流、手写一个简单的Promise。这个习惯不需要很聪明,只需要坚持。三个月后你再回头看那些曾经看不懂的代码和热搜词,会发现大部分问题都不再是问题了。JS这条路的秘诀,从来都是动手。