前端十年:从0到资深开发者的10堂必修课

第2篇:进阶篇——JavaScript 语言精髓

如果说 HTML 和 CSS 是前端的皮囊,那么 JavaScript 就是灵魂。掌握 JavaScript 的核心机制——作用域、闭包、原型、异步——是区分“会用”和“精通”的关键分水岭。本篇将深入这些精髓,让你写出更健壮、更优雅的代码。


一、作用域与闭包

作用域决定了变量在代码中的可见性和生命周期。理解作用域是理解闭包的前提,而闭包则是 JavaScript 中最强大的特性之一。

1. 全局/函数/块级作用域、词法作用域

JavaScript 中的作用域主要有三种:

  • 全局作用域:在代码任何地方都能访问的变量,挂载在 window(浏览器)或 global(Node.js)上。
  • 函数作用域:在函数内部声明的变量,只能在函数内部及嵌套函数中访问。
  • 块级作用域:由 letconst 声明的变量在 {} 内形成块级作用域(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,但闭包保留了它的引用

常见应用

  1. 模块化(封装私有变量)
    利用闭包实现模块模式,隐藏内部实现细节。

    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
    
  2. 防抖与节流
    防抖(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

属性查找机制

  1. 检查对象自身是否有该属性。
  2. 如果没有,沿着 __proto__ 到其原型对象上查找。
  3. 重复直到找到或到达 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();

继承:使用 extendssuper

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)发起,包括:

  • setTimeoutsetInterval
  • setImmediate(Node)
  • I/O 操作
  • UI 渲染(浏览器)
  • postMessageMessageChannel

微任务(MicroTask):由 JavaScript 自身发起,优先级高于宏任务,包括:

  • Promise.then/catch/finally 的回调
  • MutationObserver(浏览器)
  • queueMicrotask
  • process.nextTick(Node,但优先级高于微任务)

事件循环流程

  1. 执行一个宏任务(从宏任务队列中取一个)。
  2. 执行过程中产生的微任务会被依次加入微任务队列。
  3. 宏任务执行完后,清空微任务队列中的所有微任务(按添加顺序执行)。
  4. 如果需要渲染,执行渲染。
  5. 开始下一轮循环,执行下一个宏任务。

示例分析

console.log('1'); // 同步代码

setTimeout(() => {
    console.log('2'); // 宏任务
}, 0);

Promise.resolve().then(() => {
    console.log('3'); // 微任务
});

console.log('4'); // 同步代码

// 输出顺序:1 4 3 2

解释:

  • 执行全局脚本(一个宏任务),输出 14
  • 遇到 setTimeout,回调被放入宏任务队列。
  • 遇到 Promise.then,回调被放入微任务队列。
  • 全局脚本执行完毕,清空微任务队列,输出 3
  • 下一轮循环,执行宏任务 setTimeout 回调,输出 2

理解事件循环有助于避免异步执行顺序的陷阱,并优化代码性能。


总结

本篇深入剖析了 JavaScript 的核心精髓:

  • 作用域与闭包:理解了词法作用域和闭包的形成机制,学会了用闭包实现模块化、防抖节流等高级模式。
  • 原型链与面向对象:掌握了构造函数、原型、原型链的查找规则,以及 ES6 class 的用法与本质。
  • 异步编程:从回调地狱到 Promise,再到 async/await,并理清了事件循环中的宏任务与微任务。

这些知识点是 JavaScript 进阶的必经之路,也是面试中的高频考点。下一篇我们将进入 浏览器篇,探索渲染原理、事件机制与 DOM 性能优化,敬请期待!


思考题

  1. 闭包一定会造成内存泄漏吗?如何避免?
  2. Function.prototype.callapplybind 与原型链有什么关系?
  3. 以下代码的输出顺序是什么?为什么?
    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'));
    
  4. 使用 async/await 时,如何并行执行多个异步任务并等待所有结果?

欢迎在评论区留下你的答案和疑问,一起讨论进步!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐