前端十年从0到资深开发者的10堂必修课第2篇进阶篇——JavaScript 语言精髓如果说 HTML 和 CSS 是前端的皮囊那么 JavaScript 就是灵魂。掌握 JavaScript 的核心机制——作用域、闭包、原型、异步——是区分“会用”和“精通”的关键分水岭。本篇将深入这些精髓让你写出更健壮、更优雅的代码。一、作用域与闭包作用域决定了变量在代码中的可见性和生命周期。理解作用域是理解闭包的前提而闭包则是 JavaScript 中最强大的特性之一。1. 全局/函数/块级作用域、词法作用域JavaScript 中的作用域主要有三种全局作用域在代码任何地方都能访问的变量挂载在window浏览器或globalNode.js上。函数作用域在函数内部声明的变量只能在函数内部及嵌套函数中访问。块级作用域由let和const声明的变量在{}内形成块级作用域var没有块级作用域。词法作用域静态作用域函数的作用域在定义时就已确定而不是在调用时。这决定了变量的查找路径。varglobalVar全局;functionouter(){varouterVar外部;functioninner(){varinnerVar内部;console.log(globalVar);// 可以访问全局console.log(outerVar);// 可以访问外部函数的变量console.log(innerVar);// 可以访问自己的变量}inner();}outer();// 在外部无法访问 outerVar 或 innerVar块级作用域示例if(true){vara1;// 函数作用域实际是全局因为不在函数内letb2;// 块级作用域constc3;// 块级作用域}console.log(a);// 1console.log(b);// ReferenceError: b is not definedconsole.log(c);// ReferenceError: c is not defined2. 闭包的定义、常见应用模块化、防抖节流闭包当一个函数能够记住并访问其词法作用域即使该函数在其词法作用域之外执行我们就说这个函数产生了闭包。简单说函数 它对外部变量的引用 闭包。经典闭包示例functioncreateCounter(){letcount0;returnfunction(){count;returncount;};}constcountercreateCounter();console.log(counter());// 1console.log(counter());// 2// 外部无法直接访问 count但闭包保留了它的引用常见应用模块化封装私有变量利用闭包实现模块模式隐藏内部实现细节。constmodule(function(){letprivateVar0;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){lettimernull;returnfunction(...args){clearTimeout(timer);timersetTimeout(()fn.apply(this,args),delay);};}// 节流每隔一段时间执行一次functionthrottle(fn,interval){letlastTime0;returnfunction(...args){constnowDate.now();if(now-lastTimeinterval){lastTimenow;fn.apply(this,args);}};}闭包的注意点由于闭包会持有外部变量的引用可能导致内存泄漏尤其是旧版 IE 中涉及 DOM 元素时。现代引擎已优化但依然要避免无意中创建大量闭包。二、原型链与面向对象JavaScript 的面向对象是基于原型的而不是传统的类。理解原型链是理解对象继承、new操作符、class语法的关键。1. 构造函数、原型、原型链查找机制构造函数通过new调用的函数用于创建对象。functionPerson(name,age){this.namename;this.ageage;// 不要在这里定义方法否则每个实例都会创建一份函数副本// this.sayHi function() { ... }}constp1newPerson(Alice,25);原型prototype每个函数都有一个prototype属性指向一个对象。通过该函数创建的实例会继承这个原型对象上的属性和方法。Person.prototype.sayHifunction(){console.log(Hi, Im${this.name});};p1.sayHi();// Hi, Im 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.typetype;}Animal.prototype.getTypefunction(){returnthis.type;};functionDog(name){Animal.call(this,犬科);// 借用构造函数this.namename;}// 继承原型Dog.prototypeObject.create(Animal.prototype);Dog.prototype.constructorDog;Dog.prototype.barkfunction(){console.log(汪汪);};constdnewDog(旺财);console.log(d.getType());// 犬科来自 Animal.prototyped.bark();// 汪汪来自 Dog.prototypeconsole.log(d.toString());// 来自 Object.prototype2. ES6 Class 语法糖与继承ES6 的class本质上是基于原型的语法糖写法更接近传统面向对象语言但底层仍是原型链。基本语法classPerson{constructor(name,age){this.namename;this.ageage;}sayHi(){console.log(Hi, Im${this.name});}staticinfo(){// 静态方法console.log(这是一个人类);}}constpnewPerson(Bob,30);p.sayHi();Person.info();继承使用extends和super。classStudentextendsPerson{constructor(name,age,grade){super(name,age);// 必须先调用 super 才能使用 thisthis.gradegrade;}study(){console.log(${this.name}正在学习);}// 重写父类方法sayHi(){super.sayHi();// 调用父类方法console.log(我是学生年级${this.grade});}}constsnewStudent(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已失败创建 PromiseconstpromisenewPromise((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{constdata1awaitgetData1();constdata2awaitgetData2(data1);constdata3awaitgetData3(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、setIntervalsetImmediateNodeI/O 操作UI 渲染浏览器postMessage、MessageChannel等微任务MicroTask由 JavaScript 自身发起优先级高于宏任务包括Promise.then/catch/finally的回调MutationObserver浏览器queueMicrotaskprocess.nextTickNode但优先级高于微任务事件循环流程执行一个宏任务从宏任务队列中取一个。执行过程中产生的微任务会被依次加入微任务队列。宏任务执行完后清空微任务队列中的所有微任务按添加顺序执行。如果需要渲染执行渲染。开始下一轮循环执行下一个宏任务。示例分析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 的核心精髓作用域与闭包理解了词法作用域和闭包的形成机制学会了用闭包实现模块化、防抖节流等高级模式。原型链与面向对象掌握了构造函数、原型、原型链的查找规则以及 ES6class的用法与本质。异步编程从回调地狱到 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 时如何并行执行多个异步任务并等待所有结果欢迎在评论区留下你的答案和疑问一起讨论进步
前端十年:从0到资深开发者的10堂必修课【第2篇】
前端十年从0到资深开发者的10堂必修课第2篇进阶篇——JavaScript 语言精髓如果说 HTML 和 CSS 是前端的皮囊那么 JavaScript 就是灵魂。掌握 JavaScript 的核心机制——作用域、闭包、原型、异步——是区分“会用”和“精通”的关键分水岭。本篇将深入这些精髓让你写出更健壮、更优雅的代码。一、作用域与闭包作用域决定了变量在代码中的可见性和生命周期。理解作用域是理解闭包的前提而闭包则是 JavaScript 中最强大的特性之一。1. 全局/函数/块级作用域、词法作用域JavaScript 中的作用域主要有三种全局作用域在代码任何地方都能访问的变量挂载在window浏览器或globalNode.js上。函数作用域在函数内部声明的变量只能在函数内部及嵌套函数中访问。块级作用域由let和const声明的变量在{}内形成块级作用域var没有块级作用域。词法作用域静态作用域函数的作用域在定义时就已确定而不是在调用时。这决定了变量的查找路径。varglobalVar全局;functionouter(){varouterVar外部;functioninner(){varinnerVar内部;console.log(globalVar);// 可以访问全局console.log(outerVar);// 可以访问外部函数的变量console.log(innerVar);// 可以访问自己的变量}inner();}outer();// 在外部无法访问 outerVar 或 innerVar块级作用域示例if(true){vara1;// 函数作用域实际是全局因为不在函数内letb2;// 块级作用域constc3;// 块级作用域}console.log(a);// 1console.log(b);// ReferenceError: b is not definedconsole.log(c);// ReferenceError: c is not defined2. 闭包的定义、常见应用模块化、防抖节流闭包当一个函数能够记住并访问其词法作用域即使该函数在其词法作用域之外执行我们就说这个函数产生了闭包。简单说函数 它对外部变量的引用 闭包。经典闭包示例functioncreateCounter(){letcount0;returnfunction(){count;returncount;};}constcountercreateCounter();console.log(counter());// 1console.log(counter());// 2// 外部无法直接访问 count但闭包保留了它的引用常见应用模块化封装私有变量利用闭包实现模块模式隐藏内部实现细节。constmodule(function(){letprivateVar0;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){lettimernull;returnfunction(...args){clearTimeout(timer);timersetTimeout(()fn.apply(this,args),delay);};}// 节流每隔一段时间执行一次functionthrottle(fn,interval){letlastTime0;returnfunction(...args){constnowDate.now();if(now-lastTimeinterval){lastTimenow;fn.apply(this,args);}};}闭包的注意点由于闭包会持有外部变量的引用可能导致内存泄漏尤其是旧版 IE 中涉及 DOM 元素时。现代引擎已优化但依然要避免无意中创建大量闭包。二、原型链与面向对象JavaScript 的面向对象是基于原型的而不是传统的类。理解原型链是理解对象继承、new操作符、class语法的关键。1. 构造函数、原型、原型链查找机制构造函数通过new调用的函数用于创建对象。functionPerson(name,age){this.namename;this.ageage;// 不要在这里定义方法否则每个实例都会创建一份函数副本// this.sayHi function() { ... }}constp1newPerson(Alice,25);原型prototype每个函数都有一个prototype属性指向一个对象。通过该函数创建的实例会继承这个原型对象上的属性和方法。Person.prototype.sayHifunction(){console.log(Hi, Im${this.name});};p1.sayHi();// Hi, Im 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.typetype;}Animal.prototype.getTypefunction(){returnthis.type;};functionDog(name){Animal.call(this,犬科);// 借用构造函数this.namename;}// 继承原型Dog.prototypeObject.create(Animal.prototype);Dog.prototype.constructorDog;Dog.prototype.barkfunction(){console.log(汪汪);};constdnewDog(旺财);console.log(d.getType());// 犬科来自 Animal.prototyped.bark();// 汪汪来自 Dog.prototypeconsole.log(d.toString());// 来自 Object.prototype2. ES6 Class 语法糖与继承ES6 的class本质上是基于原型的语法糖写法更接近传统面向对象语言但底层仍是原型链。基本语法classPerson{constructor(name,age){this.namename;this.ageage;}sayHi(){console.log(Hi, Im${this.name});}staticinfo(){// 静态方法console.log(这是一个人类);}}constpnewPerson(Bob,30);p.sayHi();Person.info();继承使用extends和super。classStudentextendsPerson{constructor(name,age,grade){super(name,age);// 必须先调用 super 才能使用 thisthis.gradegrade;}study(){console.log(${this.name}正在学习);}// 重写父类方法sayHi(){super.sayHi();// 调用父类方法console.log(我是学生年级${this.grade});}}constsnewStudent(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已失败创建 PromiseconstpromisenewPromise((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{constdata1awaitgetData1();constdata2awaitgetData2(data1);constdata3awaitgetData3(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、setIntervalsetImmediateNodeI/O 操作UI 渲染浏览器postMessage、MessageChannel等微任务MicroTask由 JavaScript 自身发起优先级高于宏任务包括Promise.then/catch/finally的回调MutationObserver浏览器queueMicrotaskprocess.nextTickNode但优先级高于微任务事件循环流程执行一个宏任务从宏任务队列中取一个。执行过程中产生的微任务会被依次加入微任务队列。宏任务执行完后清空微任务队列中的所有微任务按添加顺序执行。如果需要渲染执行渲染。开始下一轮循环执行下一个宏任务。示例分析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 的核心精髓作用域与闭包理解了词法作用域和闭包的形成机制学会了用闭包实现模块化、防抖节流等高级模式。原型链与面向对象掌握了构造函数、原型、原型链的查找规则以及 ES6class的用法与本质。异步编程从回调地狱到 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 时如何并行执行多个异步任务并等待所有结果欢迎在评论区留下你的答案和疑问一起讨论进步