前端十年:从0到资深开发者的10堂必修课【第2篇】
前端十年:从0到资深开发者的10堂必修课
第2篇:进阶篇——JavaScript 语言精髓
如果说 HTML 和 CSS 是前端的皮囊,那么 JavaScript 就是灵魂。掌握 JavaScript 的核心机制——作用域、闭包、原型、异步——是区分“会用”和“精通”的关键分水岭。本篇将深入这些精髓,让你写出更健壮、更优雅的代码。
一、作用域与闭包
作用域决定了变量在代码中的可见性和生命周期。理解作用域是理解闭包的前提,而闭包则是 JavaScript 中最强大的特性之一。
1. 全局/函数/块级作用域、词法作用域
JavaScript 中的作用域主要有三种:
- 全局作用域:在代码任何地方都能访问的变量,挂载在
window(浏览器)或global(Node.js)上。 - 函数作用域:在函数内部声明的变量,只能在函数内部及嵌套函数中访问。
- 块级作用域:由
let和const声明的变量在{}内形成块级作用域(var没有块级作用域)。
词法作用域(静态作用域):函数的作用域在定义时就已确定,而不是在调用时。这决定了变量的查找路径。
var globalVar = '全局';
function outer() {
var outerVar = '外部';
function inner() {
var innerVar = '内部';
console.log(globalVar); // 可以访问全局
console.log(outerVar); // 可以访问外部函数的变量
console.log(innerVar); // 可以访问自己的变量
}
inner();
}
outer();
// 在外部无法访问 outerVar 或 innerVar
块级作用域示例:
if (true) {
var a = 1; // 函数作用域(实际是全局,因为不在函数内)
let b = 2; // 块级作用域
const c = 3; // 块级作用域
}
console.log(a); // 1
console.log(b); // ReferenceError: b is not defined
console.log(c); // ReferenceError: c is not defined
2. 闭包的定义、常见应用(模块化、防抖节流)
闭包:当一个函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行,我们就说这个函数产生了闭包。简单说:函数 + 它对外部变量的引用 = 闭包。
经典闭包示例:
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// 外部无法直接访问 count,但闭包保留了它的引用
常见应用:
-
模块化(封装私有变量)
利用闭包实现模块模式,隐藏内部实现细节。const module = (function() { let privateVar = 0; function privateMethod() { /* ... */ } return { publicMethod: function() { privateVar++; }, getVar: function() { return privateVar; } }; })(); module.publicMethod(); console.log(module.getVar()); // 1 console.log(module.privateVar); // undefined -
防抖与节流
防抖(debounce)和节流(throttle)用于限制高频事件的触发频率,内部通过闭包保存定时器 ID 或上次执行时间。// 防抖:多次触发只执行最后一次 function debounce(fn, delay) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } // 节流:每隔一段时间执行一次 function throttle(fn, interval) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= interval) { lastTime = now; fn.apply(this, args); } }; }
闭包的注意点:由于闭包会持有外部变量的引用,可能导致内存泄漏(尤其是旧版 IE 中涉及 DOM 元素时)。现代引擎已优化,但依然要避免无意中创建大量闭包。
二、原型链与面向对象
JavaScript 的面向对象是基于原型的,而不是传统的类。理解原型链是理解对象继承、new 操作符、class 语法的关键。
1. 构造函数、原型、原型链查找机制
构造函数:通过 new 调用的函数,用于创建对象。
function Person(name, age) {
this.name = name;
this.age = age;
// 不要在这里定义方法,否则每个实例都会创建一份函数副本
// this.sayHi = function() { ... }
}
const p1 = new Person('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); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
属性查找机制:
- 检查对象自身是否有该属性。
- 如果没有,沿着
__proto__到其原型对象上查找。 - 重复直到找到或到达
null。
示例:模拟原型链
function Animal(type) {
this.type = type;
}
Animal.prototype.getType = function() { return this.type; };
function Dog(name) {
Animal.call(this, '犬科'); // 借用构造函数
this.name = name;
}
// 继承原型
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() { console.log('汪汪'); };
const d = new Dog('旺财');
console.log(d.getType()); // 犬科(来自 Animal.prototype)
d.bark(); // 汪汪(来自 Dog.prototype)
console.log(d.toString()); // 来自 Object.prototype
2. ES6 Class 语法糖与继承
ES6 的 class 本质上是基于原型的语法糖,写法更接近传统面向对象语言,但底层仍是原型链。
基本语法:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() {
console.log(`Hi, I'm ${this.name}`);
}
static info() { // 静态方法
console.log('这是一个人类');
}
}
const p = new Person('Bob', 30);
p.sayHi();
Person.info();
继承:使用 extends 和 super。
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // 必须先调用 super 才能使用 this
this.grade = grade;
}
study() {
console.log(`${this.name} 正在学习`);
}
// 重写父类方法
sayHi() {
super.sayHi(); // 调用父类方法
console.log(`我是学生,年级 ${this.grade}`);
}
}
const s = new Student('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:
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
if (成功) {
resolve('成功数据');
} else {
reject('失败原因');
}
}, 1000);
});
使用 Promise:通过 .then()、.catch()、.finally() 处理结果。
promise
.then(result => {
console.log('成功:', result);
return result + ' 处理';
})
.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。
async function fetchData() {
// 如果返回非 Promise 值,会自动包装成 Promise.resolve
return '数据';
}
fetchData().then(console.log); // '数据'
await:只能在 async 函数内部使用,等待一个 Promise 完成并返回其结果。如果 Promise 被拒绝,会抛出异常,可用 try/catch 捕获。
async function process() {
try {
const data1 = await getData1();
const data2 = await getData2(data1);
const data3 = await getData3(data2);
console.log(data3);
} catch (error) {
console.error('出错啦:', error);
}
}
process();
注意:await 会阻塞 async 函数内部的后续代码,但不会阻塞外部(因为 async 函数本身是异步的)。多个无依赖的异步操作可以并发执行,用 Promise.all 优化:
async function parallel() {
const [res1, res2] = await Promise.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'); new Promise((resolve) => { console.log('D'); resolve(); }).then(() => console.log('E')); - 使用 async/await 时,如何并行执行多个异步任务并等待所有结果?
欢迎在评论区留下你的答案和疑问,一起讨论进步!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)