这是一篇视频稿(Final Cut 爆炸了所以没去薅叔叔)

孔子曾说:

“温故而知新,可以为师矣”

Confucius 曾经也说过:

“When reviewing old knowledge, you can have new experiences and discoveries, and you can be a teacher.”

意思就是:经常翻看以往学过的知识,一般总能学到新知识。这样就可以可持续性当你爹(?

想一想,从 16 年到现在一直用 TypeScript,至今已经成为主流。在今天这个大好日子,我决定听孔子一回,重新学习 JavaScript

并且,孔子还说过:

知之者不如好之者,好之者不如乐之者

所以我决定邀请各位,请让你身边的人一起学

环境

虽然直接使用浏览器方便一些,但我们现在应该本地都有一个 node,这可能对我们来说更为方便。顺便的可以温习一遍简单的 File IO,毕竟这是几乎所有语言入门都需要接触的部分

本文环境配置如下:

  • OS:最新最时尚最卡的 MacOS Ventura
  • node: v16.13.2

基本类型

JavaScript 是一款“函数式” [1] 解释型脚本语言,所以这部分我们可以使用 Node REPL

打开终端,输入这个

node

首先是基本类型:JavaScript三种基本类型:string, number, boolean

string 叫字符串,如果有其他编译型语言经验的话可能一时会觉得奇怪,但在 JavaScript 没那么讲究,字符串就是基础类型

字符串由一对引号包起来,也没那么多讲究 —— 单引号双引号都可以

> 'I love you'
'I love you'
> "I love you, too"
'I love you, too'

如果你的字符串出现了必要的单引号(例如 do not 的缩写),这时为了避免歧义,应该用一种引号(双引号)来作为语法识别,这样字符串内就可以使用另一对引号(单引号)

> "I don't hate you"
"I don't hate you"

number 叫 number,数字,同样也没那么多讲究 —— 不分 32 位 64 位,不分整浮点,甚至双精,甚至 decimal

配合一些基本的操作符(operators),我们可以把 REPL 作为计算器使用

> 1+1
2
> 1*1
1

计算机的世界,计算没有大括号({}),没有中括号([]),只用小括号表示优先级,嵌套越深则越优先。举个例子

> 4 * (40 - (2 - 12) + 40 * 2)
520

这个时候你再试试除以 0。不过我们小学都学过,除法里是不能除以 0 的。但是学编程,就是要敢猜敢试。放心,不会起火

> 520 / 0
Infinity

看,尝试得到了新的理解。这个式子也许对你还有另一种含义:爱与无限之间,只有一个「无条件」

boolean 指布尔值,指一对真实反义词,例如对和错,真和假,0和1。通常用在逻辑分支

计算机是严格的,对就是对的,错就是错的。但你们之间,没有绝对对错

懂行的人别急,确实还有两种:undefinednull,它们一般情况下指「没有」,但他们严格来说不是一个意思:null 指这个值是空的,但本身也是一个值;undefined 是真的没有,且未知

就像你现在问你身旁的人问你今天有没有想TA,你回答 “没有”。虽说可能真的没想你,但其实心里还有你

而有些人想都没得想!

函数

在复习函数之前先随便复习一下定义变量

严格 ES6 之前我们只会用 var,但如今我们都默认有两种定义方式:var, let,还有一种定量定义方式:const

变量指定义之后,值还能改;定量指定义之后,值无法修改

var lovedGame = 'League of Legends'
lovedGame = 'JX Online 3' // 世界上最好的 MMORPLG

const lovedOne = 'You'
lovedOne = 'Her'
// Uncaught TypeError: Assignment to constant variable.

严格讲,varlet 有一点区别,let 有声明使用顺序(死区)[2]和作用域限制,换句话说更“安全”,所以现在对于新手来说,无论如何优先使用 let

接下来到函数,一般声明一个函数会有两种方式,一种是正常的函数声明,一种是基于语言特性的声明

// 1.
function fn() {}
// 2.
const fn = () => {}

第二种其实就是一种「把函数也当做一个变量/定量」的思路,具体区别不展开[3]

调用一个函数很简单

fn() // 函数有参数就照着传

Ok,小节实践,尝试以下函数并调用

function calc(n) {
  return ((n + 52.8) * 5 - 3.9343) / 0.5 - 10 * n
}

// 也可以这么写,是不是更帅了
const calc = n => ((n + 52.8) * 5 - 3.9343) / 0.5 - 10 * n

(可以在 REPL 运行)

> const calc = n => ((n + 52.8) * 5 - 3.9343) / 0.5 - 10 * n
undefined

REPL 是这样的,输入的下一行永远是返回值。接下来输入调用看看

> calc(1)
520.1314
> calc(51)
520.1314
> calc(564)
520.1314

你们也是这么简单纯粹吧 —— 不要总问是不是变丑了,不管你变成怎样,都会一直爱你

实践

接下来就不用 REPL 了,因为接下来想整 NetworkFile IO。纯 JavaScript 这个语言是不带 IO 实现的

这一次就随便练一下写文件好了,我们的目的是:获取一个网络上的资源,并写下来

首先创建一个文件,就叫 love.js,随便放在你能找到的地方

先定义一些变量,照着复制就完事了

const fileName = 'love.txt'

接下来是引入一个fs

由于现在已经是 import/export stable 的时代,所以包引用哪种都可以

但如果使用 module 模式的话需要将 package.json 的类型更改为 module

我们直接执行,也不引入 npm 概念,所以我们这里用 CommonJS

在最顶上(fileName 的上方)写入

// 文件系统
const fs = require('fs')
// 路径模块(用于系统各种跟路径相关的工具)
const path = require('path')
// 网络
const http = require('https')

接着我们在最开始写的变量(fileName)下面新增一行

// 将要保存文件的路径(自动拼接绝对路径)
const currentDir = path.resolve(__dirname, fileName)

然后是网络请求,我们一般都会封装成一个函数

function request() {
  http.get(
    'https://colmugx.github.io/blog/file/h.txt',
    res => {
      let data = [];
      res.on('data', chunk => {
        data.push(chunk);
      });
      res.on('end', () => {
        return data.toString()
      });
    })
}

最后一行写上文件写入就行啦(注:这个示例跳过错误边界)

fs.writeFileSync(currentDir, request())

接下来在终端运行它吧!

node ./love.js

然后报错了!

这个时候我们应该学会看错误,这也是编程的基础。有错就说明清楚,然后调整,这样才能和谐相处

The “data” argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received undefined

这里说的是获取到一个 undefined,这个是不支持写入的

Actually,我们会给一个未知变量做边界控制,这里简单的直接短路吧[4]

fs.writeFileSync(currentDir, request() || '')

这个时候不报错了,我们去看一下这个目录下生成的 love.txt 文件吧!

当我们兴奋地打开之后发现,这是一个空文件

千万不要想着「我的努力我的付出,得不到回应」

不要灰心,学习编程的过程就是不断发现问题,不断解决问题。感情也是如此

但我们要的不是对「你错在哪儿了?」进行一个精准的回答,而是 code review

通过 review 我们会发现,request 写错了 —— 异步函数在这里不能直接获取返回值

异步编程技术使你的程序可以在执行一个可能长期运行的任务的同时继续对其他事件做出反应而不必等待任务完成。与此同时,你的程序也将在任务完成后显示结果。

而网络对于一个 node 程序来说,就是一个异步工作

发现问题,开始解决问题

对于异步工作,我们总会很习惯的使用 Promise 来解决「回调嵌套」问题,使得代码看起来「同步」,而且可以非常直觉的处理「返回值」。换句话说,就是把整个函数的调用都放进了异步队列

Promise 是现代 JavaScript 中异步编程的基础,是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象

所以,对 request 进行修改吧

function request() {
  return new Promise(resolve => {
     http.get(
      'https://colmugx.github.io/blog/file/h.txt',
      res => {
        let data = [];
        res.on('data', chunk => {
          data.push(chunk.toString())
        });
        res.on('end', () => {
          // 文件里有被转义的换行符,这里转回来
          resolve(data.toString().replace(/\\n/g, '\n'))
        });
      })
  })
}

同时,「文件写入」语句也要做相应的修改

fs.writeFileSync(currentDir, await request() || '')

这个时候再执行一遍!同样没有报错

但这一次不同了,当我们再一次打开 love.txt 文件时,我们会看到

heart

所以说,爱有时候也应该像这样:长久的爱与互相理解,建立在承诺(promise)和等待(await)

我们已经约好了一直走下去的呀,等一会又怎样嘛

END -

(其实如果你有留意到这篇文章分类是在「动物园」下,或许早就知道了)

真是温故知新啊,看看这个引用区 ↓

全都是年轻不懂事的自己,瞎几把写


  1. JavaScript 可以是函数式语言,但是极端还是存在争议,这里过分严谨一些 ↩︎

  2. 我今天给 let 安排了 · Colmugx 's Blog ↩︎

  3. 我就写个方法,这么麻烦? · Colmugx 's Blog ↩︎

  4. 剧本味也太重了草,事已至此只能做一个违背祖宗的决定了 ↩︎