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写起来还是有点小意思的。

添加新评论

已有 3 条评论
  1. ia2222 ia2222

    我发现你最近好像太去Lain化了 你去年不是还去酒店用大电视看了Lain吗
    当然我不知道这是不是我的幻觉 我也有很久没有接触有关Lain的东西了

  2. ia2222 ia2222

    我快被AI气死了 我觉得当今社会还是过于高估AI了

    你就是 技术栈稍微复杂一些 这AI就彻底没有辨明是非的能力了 写代码也一个逼样

    就是这玩意 对的就是很对 错的地方就是 一错再错 错到底 而且有时候还会坚信自己是正确的 会在互联网上找一些自己所谓的资料来证明 在我看来 AI的思考就是不是真思考 他是被答案推着走 而不是推着答案走 无论是最新的Gemini Pro,Grok,ChatGPT都一个逼样

    指望AI代替人类短时间不太可能 除非是一些比较低智的CRUD

    人话就是 我快被这傻逼AI弄疯了 这回答让我又气人又降智

  3. ia2222 ia2222

    还有 我最近从vim更换neovim了

    我先说说emacs的特点吧 首先一个问题就是复杂 你折腾emacs 就像在折腾 一个有严重历史包袱的 很多功能冗余的 复杂的 操作系统

    里面很多包 都已经很多年都没有更新了 生态不怎么样 支持少 其次 有很多机制是完全冗余并且没有必要的 总之一只手数不过来
    elisp本身和emacs很多机制强绑定 并且有一大堆理论等着你去学习 你需要为了一个简单东西 花费大量实现学习elisp陌生的语法 最后只能写出来一个胶水代码

    是 我不得不承认 emacs是给黑客的 给那些愿意把大把时间交给emacs 去花费大量时间看文档 对现代特性Doesn't give a shit的人 他在某些情况确实有超强的灵活性 还有Geeker的风格
    But, I don't give a fuck
    我的要求只有一个 极简 键盘流 按我所需 而不是像emacs一样 去对着一堆强耦合的组件花费大量时间钻研最后写出几行胶水代码

    最后我还是选择了neovim