MENU

【JavaScript】初学笔记:从作用域到事件循环

2026 年 03 月 17 日 • 文章

一直没有很系统地学习JavaScript,最近依靠AI,做了个学习路线,姑且记录一下初步学习的这段经历。

ChatGPT给我的学习路线是这样的:

  1. 学习作用域、闭包
  2. 学习对象模型和this
  3. 学习Promise基础
  4. 学习async和await
  5. 学习事件循环
  6. 写一个异步事件调度器
  7. 写一个学习记录(也就是这篇文章)

作用域

JavaScript是一个动态类型语言,其赋值关键字有varletconst三种,var用于声明全局变量、let用于声明一个局部变量,const用于说明常量,全局变量的作用域不受块级作用域(大括号括起来的内部)约束,局部变量、常量受约束。这些知识可以通过这样一个小例子来理解:

const a = 3
{
    let a = 4
    console.log(a) // 4
}
console.log(a) // 3

{
    var b = 3
}
console.log(b) // 3

{
    const b = 4
    console.log(b) // 4
}

第一次输出的a是块级作用域内部的变量a,第二次输出的是常量变量a,第三次输出的是块级作用域内部定义的全局变量b,第四次输出的是块级作用域内部的常量b

闭包

所谓闭包,朴素来理解,就是一个返回函数/对象的函数。精准一些的定义是:闭包是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。这个定义有些难以理解,看代码会好一些,一个非常简单的例子是(来自于JS MDN):

function init() {
  var name = "Mozilla"; // name 是 init 创建的局部变量
  function displayName() {
    // displayName() 是内部函数,它创建了一个闭包
    console.log(name); // 使用在父函数中声明的变量
  }
  displayName();
}
init();

这里,name作为函数的周围状态,displayName()作为返回的函数,在闭包中,返回的函数可以访问函数外的变量,也就是说,在运行的过程中,displayName()能够调用name

这里ChatGPT给了一个小练习题,写一个function counter() {},要求每次调用返回递增数字,且不能使用全局变量,这个解法其实很简单:

function counter(start) {
    let count = start
    return function() {
        return start++;
    }
}

const counter1 = counter(1)
console.log(counter1()) // 1
console.log(counter1()) // 2
console.log(counter1()) // 3

还有一个小练习题,写一个function once(fn) {},要求函数只执行一次,这个解法其实也不难:

function once(fn) {
    let called = false
    return function(...args) {
        if(!called) {
            called = true
            return fn(...args)
        } else {
            return undefined
        }
    }
}

function test(msg) {
    return msg
}

const onceTest = once(test)
console.log(onceTest("hello"))
console.log(onceTest("hello"))

这里就有一个问题,在内存中,闭包是怎么存储的?我们回到counter()的例子,在第一次调用counter()的时候,会创建一个作用域,里面保存了count,随后创建要保存的函数的信息,不仅生成函数代码本身,也生成了对于外部变量环境的引用,当执行完的时候,保存的外部信息也不会被销毁,始终保存在内存中。一句话概括就是:函数对象始终保持对外部词法环境的引用,导致该环境中的变量在外层函数返回后仍然继续存活。

this

this关键字通常表示「自己」,比如在一个Object中:

const myObj = {
    a: 3,

    fn() {
        console.log(this.a)
    }
}

myObj.fn() // 3

this就表示对象自身。

在函数中使用this,如果不是strict模式,就表示window本身,比如这样一个网页:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>this in non-strict mode</title>
</head>
<body>
    <h1>this inside a function (no strict mode)</h1>
    <p>Open the console to see the result.</p>

    <script>
        // No "use strict" — this inside a plain function refers to the global object (window in browser)
        function showThis() {
            console.log("this === window?", this === window);
            console.log("this:", this);
        }

        showThis();
    </script>
</body>
</html>

就可以在console中看到结果,而如果是strict模式,this就表示undefined

值得一提的是箭头函数的this,参见下面的例子:

"use strict"

const obj = {
    name: "myObj",
    regular() {
        console.log("regular this.name:", this.name)
    },
    arrow: () => {
        console.log("arrow this.name:", this?.name)
    },
}

obj.regular()  // "myObj"
obj.arrow()    // undefined

这是因为,箭头函数中的this,通常指向定义它时所在环境(obj)的外层(undefined)。而一般函数的this,就指向调用它的东西:

const obj = {
  name: "A",
  f() {
    console.log(this.name)
  }
}

const obj2 = {
  name: "B",
  f: obj.f
}

obj.f()   // A
obj2.f()  // B

Class

JS的类和其它语言没有什么太大差别,这里ChatGPT给了一个小练习题,编写class TaskManager {},实现addTasklistTask,一个解法就是:

class TaskManager {
    task_list = []
    addTask(task) {
        this.task_list.push(task)
    }
    listTasks() {
        return this.task_list
    }
}

const taskManager = new TaskManager()
taskManager.addTask("Task 1")
taskManager.addTask("Task 2")
taskManager.addTask("Task 3")
console.log(taskManager.listTasks())

class的本质其实是function,这一点要记住:

"use strict"

// A class is a function (the constructor). Visualize it:

class Counter {
  constructor() {
    this.count = 0
  }
  inc() {
    this.count++
  }
}

// 1) typeof class is "function"
console.log("typeof Counter:", typeof Counter)  // "function"

// 2) You can call it with new (like a constructor function)
const c = new Counter()
console.log("new Counter() works:", c instanceof Counter)  // true

// 3) It has a .prototype (like any constructor function)
console.log("Counter has prototype:", "prototype" in Counter)  // true
console.log("Counter.prototype.constructor === Counter:", Counter.prototype.constructor === Counter)  // true

// 4) Compare with a plain function used as constructor — same idea
function CounterFn() {
  this.count = 0
}
CounterFn.prototype.inc = function () {
  this.count++
}
const c2 = new CounterFn()
console.log("CounterFn behaves the same:", c2.inc && typeof c2.inc === "function")  // true

输出:

typeof Counter: function
new Counter() works: true
Counter has prototype: true
Counter.prototype.constructor === Counter: true
CounterFn behaves the same: true

Promise

这一点是我学JS最难以理解的一个部分。Promise是什么,字面意思上来理解,就是「承诺」,表示「马上要执行的函数会返回一个值,可要做好准备啦」的意思。Promise有三种状态——Pending(正在处理……)、Fulfilled(……成功啦!)、Rejected(……失败啦!)。如果Fulfilled,就可以通过then()处理结果,否则就是catch()来处理。看几个例子:

console.log("--- 1. Executor runs now, .then/catch run later ---")
const p1 = new Promise((resolve, reject) => {
  console.log("  [executor] Promise executor runs immediately")
  resolve(42)
})
console.log("  [sync] After new Promise(...), p1 is still pending until microtask runs")
p1.then((v) => console.log("  [microtask] .then got value:", v))

这里,p1计算之后,通过resolve把值传给了then里面的函数,进行后续的操作,这里用return是不行的,算是Promise的一个约定。又有这样一个例子:

console.log("\n--- 2. Chaining and value flow ---")
Promise.resolve(1)
  .then((x) => {
    console.log("  first .then received:", x)
    return x + 1
  })
  .then((x) => {
    console.log("  second .then received:", x)
    return x * 2
  })
  .then((x) => console.log("  final value:", x))

这里,then里面就不能用resolve了,因为then里面的东西就是普通的回调函数——除非then里也返回一个Promise。又有这样一个例子:

console.log("\n--- 3. Reject and .catch() ---")
const pReject = new Promise((resolve, reject) => {
  console.log("  [executor] about to reject")
  reject(new Error("Something went wrong"))
})
pReject
  .then((v) => console.log("  [skip] fulfilled:", v))
  .catch((err) => console.log("  [microtask] .catch got reason:", err.message))
  .then(() => console.log("  [after catch] chain is fulfilled again; can keep .then-ing"))

这里,第一个then会被跳过,直接从catch开始执行,然后再执行最后一个then

这里的练习是写一个function delay(ms) {},可以这样使用delay(1000).then(() => console.log("done"));,解法比较简单:

function delay(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(ms)
        }, ms)
        setTimeout(() => {
            reject("Timeout")
        }, ms + 100)
    })
}

delay(1000).then(() => console.log("done"));

通过修改ms+100,可以指定两个任务谁先完成,从而产生不同的结果。

async和await

这俩其实是Promise的语法糖。和其他语言的用法倒也差不多。

事件循环

我们知道,JS是和网页开发息息相关的,那么,JS最好是单线程执行,这样的话,就不会对浏览器的渲染产生干扰,从而导致race condition或其他多线程常见的问题。那么,JS对于任务的处理方式,又是怎样的呢?这里一句话理解就是:先执行同步代码 → 再清空微任务 → 再执行一个宏任务 → 再清空微任务 → 如此循环。

我们都知道代码是从上往下执行的,这个时候就出现了三种情况:

  1. 遇到同步代码。比如console.log()之类。
  2. 遇到宏任务。比如setTimeout()之类。
  3. 遇到微任务。比如Promise。

一个例子:

setTimeout(() => {
    console.log("macro1")
  
    Promise.resolve().then(() => console.log("micro1"))
    Promise.resolve().then(() => console.log("micro2"))
    Promise.resolve().then(() => console.log("micro3"))
  }, 0)
  
  setTimeout(() => {
    console.log("macro2")
  }, 0)

输出为:

macro1
micro1
micro2
micro3
macro2

为什么?我们首先看到了两个Timeout,创建了两个宏任务,然后在执行第一个宏任务的时候,需要把其产生的所有微任务——里面的所有Promise——执行完,然后再执行第二个宏任务。

另一个例子:

console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
console.log(4);

输出为:

1
4
3
2

这里,主程序也可以看作一个宏任务,所以先执行同步代码,然后执行微任务,再执行另一个宏任务。

异步事件调度器

这个要求很简单,编写一个async function runTasks(tasks, { parallel }) {},然后支持顺序、支持并行,初版解法如下:

async function runTasks(tasks, { parallel }) {
    let result = []
    for(let task of tasks) {
        if(!parallel) {
            result.push(await task())
        } else {
            result.push(task())
        }
    }
    if(parallel) {
        return Promise.all(result)
    }
    return result
}

function task1() {
    return new Promise(resolve => setTimeout(() => resolve("done in 1s"), 1000))
}

function task2() {
    return new Promise(resolve => setTimeout(() => resolve("done in 2s"), 2000))
}

const tasks = [task1, task2]

console.log(await runTasks(tasks, { parallel: true }))
console.log(await runTasks(tasks, { parallel: false }))

如果要支持错误catching,就可以:

"use strict"

async function runTasks(tasks, { parallel }) {
    if (parallel) {
        const wrapped = tasks.map(task =>
            task()
                .then(value => ({ status: "fulfilled", value }))
                .catch(error => ({ status: "rejected", error }))
        );
        return await Promise.all(wrapped);
    } else {
        const results = [];
        for (const task of tasks) {
            try {
                const value = await task();
                results.push({ status: "fulfilled", value });
            } catch (error) {
                results.push({ status: "rejected", error });
            }
        }
        return results;
    }
}

const tasks = [
    () => new Promise(resolve => setTimeout(() => resolve("done in 1s"), 1000)),
    () => Promise.reject(new Error("error in 2s")),
];

const results = await runTasks(tasks, { parallel: true });
console.log(results);
const results2 = await runTasks(tasks, { parallel: false });
console.log(results2);

后记

And that's all for today!

JS写起来还是有点小意思的。