一直没有很系统地学习JavaScript,最近依靠AI,做了个学习路线,姑且记录一下初步学习的这段经历。
ChatGPT给我的学习路线是这样的:
- 学习作用域、闭包
- 学习对象模型和this
- 学习Promise基础
- 学习async和await
- 学习事件循环
- 写一个异步事件调度器
- 写一个学习记录(也就是这篇文章)
作用域
JavaScript是一个动态类型语言,其赋值关键字有var、let和const三种,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() // 3this就表示对象自身。
在函数中使用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() // BClass
JS的类和其它语言没有什么太大差别,这里ChatGPT给了一个小练习题,编写class TaskManager {},实现addTask和listTask,一个解法就是:
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: truePromise
这一点是我学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对于任务的处理方式,又是怎样的呢?这里一句话理解就是:先执行同步代码 → 再清空微任务 → 再执行一个宏任务 → 再清空微任务 → 如此循环。
我们都知道代码是从上往下执行的,这个时候就出现了三种情况:
- 遇到同步代码。比如
console.log()之类。 - 遇到宏任务。比如
setTimeout()之类。 - 遇到微任务。比如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写起来还是有点小意思的。