前端十年:从0到资深开发者的10堂必修课
第2篇:进阶篇——JavaScript 语言精髓
如果说 HTML 和 CSS 是前端的皮囊,那么 JavaScript 就是灵魂。掌握 JavaScript 的核心机制——作用域、闭包、原型、异步——是区分“会用”和“精通”的关键分水岭。本篇将深入这些精髓,让你写出更健壮、更优雅的代码。
一、作用域与闭包
作用域决定了变量在代码中的可见性和生命周期。理解作用域是理解闭包的前提,而闭包则是 JavaScript 中最强大的特性之一。
1. 全局/函数/块级作用域、词法作用域
JavaScript 中的作用域主要有三种:
- 全局作用域:在代码任何地方都能访问的变量,挂载在
window(浏览器)或global(Node.js)上。 - 函数作用域:在函数内部声明的变量,只能在函数内部及嵌套函数中访问。
- 块级作用域:由
let和const声明的变量在{}内形成块级作用域(var没有块级作用域)。
词法作用域(静态作用域):函数的作用域在定义时就已确定,而不是在调用时。这决定了变量的查找路径。
varglobalVar='全局';functionouter(){varouterVar='外部';functioninner(){varinnerVar='内部';console.log(globalVar);// 可以访问全局console.log(outerVar);// 可以访问外部函数的变量console.log(innerVar);// 可以访问自己的变量}inner();}outer();// 在外部无法访问 outerVar 或 innerVar块级作用域示例:
if(true){vara=1;// 函数作用域(实际是全局,因为不在函数内)letb=2;// 块级作用域constc=3;// 块级作用域}console.log(a);// 1console.log(b);// ReferenceError: b is not definedconsole.log(c);// ReferenceError: c is not defined2. 闭包的定义、常见应用(模块化、防抖节流)
闭包:当一个函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行,我们就说这个函数产生了闭包。简单说:函数 + 它对外部变量的引用 = 闭包。
经典闭包示例:
functioncreateCounter(){letcount=0;returnfunction(){count++;returncount;};}constcounter=createCounter();console.log(counter());// 1console.log(counter());// 2// 外部无法直接访问 count,但闭包保留了它的引用常见应用:
模块化(封装私有变量)
利用闭包实现模块模式,隐藏内部实现细节。constmodule=(function(){letprivateVar=0;functionprivateMethod(){/* ... */}return{publicMethod:function(){privateVar++;},getVar:function(){returnprivateVar;}};})();module.publicMethod();console.log(module.getVar());// 1console.log(module.privateVar);// undefined防抖与节流
防抖(debounce)和节流(throttle)用于限制高频事件的触发频率,内部通过闭包保存定时器 ID 或上次执行时间。// 防抖:多次触发只执行最后一次functiondebounce(fn,delay){lettimer=null;returnfunction(...args){clearTimeout(timer);timer=setTimeout(()=>fn.apply(this,args),delay);};}// 节流:每隔一段时间执行一次functionthrottle(fn,interval){letlastTime=0;returnfunction(...args){constnow=Date.now();if(now-lastTime>=interval){lastTime=now;fn.apply(this,args);}};}
闭包的注意点:由于闭包会持有外部变量的引用,可能导致内存泄漏(尤其是旧版 IE 中涉及 DOM 元素时)。现代引擎已优化,但依然要避免无意中创建大量闭包。
二、原型链与面向对象
JavaScript 的面向对象是基于原型的,而不是传统的类。理解原型链是理解对象继承、new操作符、class语法的关键。
1. 构造函数、原型、原型链查找机制
构造函数:通过new调用的函数,用于创建对象。
functionPerson(name,age){this.name=name;this.age=age;// 不要在这里定义方法,否则每个实例都会创建一份函数副本// this.sayHi = function() { ... }}constp1=newPerson('Alice',25);原型(prototype):每个函数都有一个prototype属性,指向一个对象。通过该函数创建的实例会继承这个原型对象上的属性和方法。
Person.prototype.sayHi=function(){console.log(`Hi, I'm${this.name}`);};p1.sayHi();// Hi, I'm Alice原型链:每个对象都有一个隐式原型__proto__(标准中为[[Prototype]]),指向其构造函数的prototype。当访问对象属性时,如果对象本身没有,就会沿着__proto__向上查找,直到Object.prototype甚至null。
console.log(p1.__proto__===Person.prototype);// trueconsole.log(Person.prototype.__proto__===Object.prototype);// trueconsole.log(Object.prototype.__proto__);// null属性查找机制:
- 检查对象自身是否有该属性。
- 如果没有,沿着
__proto__到其原型对象上查找。 - 重复直到找到或到达
null。
示例:模拟原型链
functionAnimal(type){this.type=type;}Animal.prototype.getType=function(){returnthis.type;};functionDog(name){Animal.call(this,'犬科');// 借用构造函数this.name=name;}// 继承原型Dog.prototype=Object.create(Animal.prototype);Dog.prototype.constructor=Dog;Dog.prototype.bark=function(){console.log('汪汪');};constd=newDog('旺财');console.log(d.getType());// 犬科(来自 Animal.prototype)d.bark();// 汪汪(来自 Dog.prototype)console.log(d.toString());// 来自 Object.prototype2. ES6 Class 语法糖与继承
ES6 的class本质上是基于原型的语法糖,写法更接近传统面向对象语言,但底层仍是原型链。
基本语法:
classPerson{constructor(name,age){this.name=name;this.age=age;}sayHi(){console.log(`Hi, I'm${this.name}`);}staticinfo(){// 静态方法console.log('这是一个人类');}}constp=newPerson('Bob',30);p.sayHi();Person.info();继承:使用extends和super。
classStudentextendsPerson{constructor(name,age,grade){super(name,age);// 必须先调用 super 才能使用 thisthis.grade=grade;}study(){console.log(`${this.name}正在学习`);}// 重写父类方法sayHi(){super.sayHi();// 调用父类方法console.log(`我是学生,年级${this.grade}`);}}consts=newStudent('Charlie',18,12);s.sayHi();s.study();注意:class只是语法糖,内部依然通过原型链实现。例如Student.prototype的原型是Person.prototype。
三、异步编程
JavaScript 是单线程语言,但通过事件循环机制实现了非阻塞的异步操作。掌握异步编程是现代前端开发的必备技能。
1. 回调地狱、Promise 原理与 API
回调函数是 JavaScript 最早的异步处理方式。但当多个异步操作嵌套时,容易形成“回调地狱”(callback hell),代码难以阅读和维护。
// 回调地狱示例getData1(function(data1){getData2(data1,function(data2){getData3(data2,function(data3){console.log(data3);});});});Promise是 ES6 引入的解决方案,代表一个异步操作的最终完成(或失败)及其结果值。Promise 有三种状态:
pending:进行中fulfilled(或resolved):已成功rejected:已失败
创建 Promise:
constpromise=newPromise((resolve,reject)=>{// 异步操作setTimeout(()=>{if(成功){resolve('成功数据');}else{reject('失败原因');}},1000);});使用 Promise:通过.then()、.catch()、.finally()处理结果。
promise.then(result=>{console.log('成功:',result);returnresult+' 处理';}).then(processed=>{console.log('再次处理:',processed);}).catch(error=>{console.error('失败:',error);}).finally(()=>{console.log('无论成功失败都会执行');});Promise 静态方法:
Promise.resolve(value):返回一个成功状态的 Promise。Promise.reject(reason):返回一个失败状态的 Promise。Promise.all(iterable):所有 Promise 成功则返回所有结果数组,任一失败则立即失败。Promise.race(iterable):返回最先完成的 Promise 结果(无论成功或失败)。Promise.allSettled(iterable):返回所有 Promise 的结果(包含状态和值),不会因某个失败而终止。Promise.any(iterable):返回第一个成功的 Promise,如果全部失败则返回 AggregateError。
2. async/await 与错误处理
async/await是 ES2017 引入的语法糖,基于 Promise 使异步代码看起来像同步代码。
async 函数:声明一个函数,使其自动返回一个 Promise。
asyncfunctionfetchData(){// 如果返回非 Promise 值,会自动包装成 Promise.resolvereturn'数据';}fetchData().then(console.log);// '数据'await:只能在 async 函数内部使用,等待一个 Promise 完成并返回其结果。如果 Promise 被拒绝,会抛出异常,可用try/catch捕获。
asyncfunctionprocess(){try{constdata1=awaitgetData1();constdata2=awaitgetData2(data1);constdata3=awaitgetData3(data2);console.log(data3);}catch(error){console.error('出错啦:',error);}}process();注意:await会阻塞 async 函数内部的后续代码,但不会阻塞外部(因为 async 函数本身是异步的)。多个无依赖的异步操作可以并发执行,用Promise.all优化:
asyncfunctionparallel(){const[res1,res2]=awaitPromise.all([getData1(),getData2()]);// 使用 res1, res2}3. 事件循环(宏任务、微任务)
JavaScript 的事件循环(Event Loop)是其异步并发的核心机制。它负责协调执行栈、任务队列(宏任务)和微任务队列。
宏任务(MacroTask):由宿主环境(浏览器/Node)发起,包括:
setTimeout、setIntervalsetImmediate(Node)- I/O 操作
- UI 渲染(浏览器)
postMessage、MessageChannel等
微任务(MicroTask):由 JavaScript 自身发起,优先级高于宏任务,包括:
Promise.then/catch/finally的回调MutationObserver(浏览器)queueMicrotaskprocess.nextTick(Node,但优先级高于微任务)
事件循环流程:
- 执行一个宏任务(从宏任务队列中取一个)。
- 执行过程中产生的微任务会被依次加入微任务队列。
- 宏任务执行完后,清空微任务队列中的所有微任务(按添加顺序执行)。
- 如果需要渲染,执行渲染。
- 开始下一轮循环,执行下一个宏任务。
示例分析:
console.log('1');// 同步代码setTimeout(()=>{console.log('2');// 宏任务},0);Promise.resolve().then(()=>{console.log('3');// 微任务});console.log('4');// 同步代码// 输出顺序:1 4 3 2解释:
- 执行全局脚本(一个宏任务),输出
1、4。 - 遇到
setTimeout,回调被放入宏任务队列。 - 遇到
Promise.then,回调被放入微任务队列。 - 全局脚本执行完毕,清空微任务队列,输出
3。 - 下一轮循环,执行宏任务
setTimeout回调,输出2。
理解事件循环有助于避免异步执行顺序的陷阱,并优化代码性能。
总结
本篇深入剖析了 JavaScript 的核心精髓:
- 作用域与闭包:理解了词法作用域和闭包的形成机制,学会了用闭包实现模块化、防抖节流等高级模式。
- 原型链与面向对象:掌握了构造函数、原型、原型链的查找规则,以及 ES6
class的用法与本质。 - 异步编程:从回调地狱到 Promise,再到 async/await,并理清了事件循环中的宏任务与微任务。
这些知识点是 JavaScript 进阶的必经之路,也是面试中的高频考点。下一篇我们将进入浏览器篇,探索渲染原理、事件机制与 DOM 性能优化,敬请期待!
思考题:
- 闭包一定会造成内存泄漏吗?如何避免?
Function.prototype.call、apply、bind与原型链有什么关系?- 以下代码的输出顺序是什么?为什么?
setTimeout(()=>console.log('A'),0);Promise.resolve().then(()=>console.log('B'));console.log('C');newPromise((resolve)=>{console.log('D');resolve();}).then(()=>console.log('E')); - 使用 async/await 时,如何并行执行多个异步任务并等待所有结果?
欢迎在评论区留下你的答案和疑问,一起讨论进步!