课程介绍
课程背景
- 前端的主要编程语言为 JavaScript
- JavaScript 作为一种融合了多种编程范式的语言,灵活性非常高
- 前端开发人员需要根据场景在不同编程范式之间 自如切换
- 进一步需要创造领域特定语言抽象业务问题
课程收益
- 了解不同编程范式的起源和适用场景
- 掌握 JavaScript 在不同的编程范式,特别是函数式编程范式的使用
- 掌握创建领域特定语言的相关工具和模式
编程语言
01_为什么需要编程语言
程序员需要通过指令输入告诉计算机需要做什么,这种指令被称为 编程语言
02_编程语言的发展史
- 最开始的时候,人们是通过 机器语言 来和机器进行交流。这种方式对机器友好,但是对人类来说十分不友好。程序员最开始的时候需要 在纸带上打孔 才能和机器进行交流。之后虽然出现了 线缆/开关 代替纸带,但是当需要输入大量内容的时候仍然很繁琐
- 之后出现了 汇编语言 代替机器语言。汇编语言是使用字母来代替数字来进行输入。在不同的机器架构下面,汇编语言可以转换成不同的数字。极大方便了代码的输入。
- 之后,为了更加方便快捷的输入,创建了 高级语言。 相较于汇编语言,高级语言更加接近于自然语言,输入也更为快捷。编辑器随之诞生。编辑器可以将 高级语言 转换成 汇编语言,然后将 汇编语言 转换成 机器语言 让计算机识读。
03_高级语言
- C:“中级语言” 过程式语言 代表
- 可对位、字节、地址 直接操作
- 代码和数据分离,倡导结构化编程
- 功能齐全:数据类型和控制逻辑多样化
- 可移植能力强
- C++:面向对象语言 代表
- C with Classes
- 继承
- 权限控制
- 虚函数
- 多态
- Lisp:函数式语言 代表
- 与机器无关
- 列表:代码即数据
- 闭包
- JavaScript:基于 原型 和 头等函数 的 多范式语言
- 过程式
- 面向对象
- 函数式
- 响应式
总结
| 编程语言 | 机器语言 | ||
|---|---|---|---|
| 汇编语言 | |||
| 中级语言 | 面向过程代表 | C | |
| 高级语言 | 面向对象代表 | C++ | |
| 函数式代表 | lisp | ||
| 多范式代表 | javascript |
编程范式
01_什么是编程范式
程序语言特性:
- 是否允许副作用
- 操作的执行顺序
- 代码组织
- 状态管理
- 语法和词法
02_常见编程范式
编程范式
- 命令式 —— 程序员如何操作机器改变状态
- 面向过程 —— 操作用 过程 进行分组
- 面向对象 —— 根据 操作和它对应的状态 进行分组
- 声明式 —— 程序员声明响应的结果而不指明具体的操作
- 函数式 —— 通过 一系列的函数 来声明逻辑
- 响应式 —— 通过 数据流 和 映射函数 来表示结果
03_过程式编程
- 自顶向下
- 设计程序的时候要自顶向下来设计程序。
- 程序
- 模块
- 变量
- 数据结构
- 函数
- 函数
- 语句
- 变量
- 模块
- 程序
- 设计程序的时候要自顶向下来设计程序。
- 结构化编程
- 顺序结构
- 选择结构 if
- 循环结构 while
JS 中的面向过程
// 可以看作 过程式编程 中的数据
export var car = {
meter:100,
speed:10
};
// 可以看作 过程式编程 中的算法
export function advanceCar(meter){
while (car < meter) {
car.meter += car.speed
}
}
import { car advanceCar } from './car';
function main(){
console.log('before',car);
advanceCar(1000);
console.log('after',car)
}
面向过程式编程的缺点
-
数据与算法关系弱
随着程序规模的增大,函数和数据之间的关系会变得难以理解,很难看出来函数的调用关系,很难看出某个数据可以用哪些函数进行修改,某个函数可以修改哪些数据。当数据的值出错的时候很难看出是哪个函数导致的,显得程序的查错会非常困难
-
不利于修改和扩充
- 当变量的定义有所改动的时候,需要把所用使用该变量的语句都找出来进行修改。
-
不利于代码重用
- 随着程序的增大,大量函数直接的关系错综复杂,在功能类似的其他地方很难复用
04_面向对象编程
将数据封装到 类 的 实例 当中,把数据藏起来,让外部不能直接操纵,只能通过类当中的实例方法来获取和修改数据,限制了数据的访问方式
- 封装
- 继承
- 多态
- 依赖注入*
封装
将一些 客观的事物 封装成 具体的类;类可以把数据和方法只让可信的类或者对象进行操作,对不可信的进行隐藏。一个类就是 封装了 数据 和 操作这些数据的代码 的逻辑形式
- 关联数据与算法
class Car {
// 数据
meter = 100;
speed = 100;
// 算法
advance(meter){
while(this.meter < meter){
this.meter += this.speed;
}
}
getSpec(){
return `meter:${this.meter};speed:${this.speed};`
}
}
function main(){
var car = new Car();
car.advance(1000);
}
继承
可以让某个类型的对象可以获得另一个类型的对象的属性和方法,可以使用现有类的所有功能,并且在无需编写原有类的情况下对这些类进行扩充
- 无需重写的情况下进行功能扩充
class FlyCar extends Car {
// 新数据
height = 100
// 新算法
fly(height){
while (this.height < height){
this.height += this.speed
}
}
}
多态
相同的方法在不同的场景有不同的表现形式
- 不同的结构可以进行接口共享,进而达到函数复用
class FlyCar extends Car {
height = 100
fly(height){
while (this.height < height){
this.height += this.speed
}
}
// 接口复用
getSpec() {
return super.getSpec() + `height:${this.height};`
}
}
// 函数复用
function showCarSpec(car){
new Model({
content:car.getSpec();
}).show();
}
依赖注入
- 去除代码耦合
class Car {
// 耦合特定实现
engine = new Engine();
whell = new Whell();
run(){
this.engine.start();
this.wheel.run();
}
}
function main(){
var car = new Car();
car.run();
}
// 需要使用Inversify.js库或者Nest.js库
class Car {
// 声明依赖
@inject('engine')
engine;
@inject('whell')
whell;
run(){
this.engine.start();
this.wheel.run();
}
}
function main(){
// 声明实现
car container = new Container();
container.bind('engine',Engine);
container.bind('car',Car);
container.bind('whell',Whell);
// 得到注入对象
var car = container.getObject('car');
car.run();
}
五大原则(SOLID原则)
- 单一职责原则(Single Responsibility Principle)
- 一个类应该只有一个引起它变化的原因。
- 例如,一个交通工具类应该只包含关于交通工具的相关属性和方法,而不应该包含其他无关的业务逻辑。
- 开放封闭原则 OCP(Open - Close Principle)
- 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。也就是说,当需要添加新功能时,应该通过增加代码来实现,而不是修改原有代码。
- 例如,通过继承和多态来实现开发一个插件系统。
- 里式替换原则 LSP(the Liskov Substitution Principle LSP)
- 子类应该能够替换掉父类并且任何使用父类的地方都可以使用子类。
- 例如,一个正方形类和矩形类,矩形类可以派生出一个正方形类,但是如果在使用正方形类的地方,矩形类不能替换正方形类。
- 接口隔离原则 ISP(Interface Segregation Principle ISP)
- 客户端不应该依赖于它不需要的接口。即一个类对另一个类的依赖应该建立在最小的接口上,不需要的接口应该剔除。
- 例如,对于一个包含多个方法的接口,在实现时可以考虑将其拆分成多个小的接口,每个类只依赖于自己需要的接口。
- 依赖倒置原则 DIP(the Dependency Inversion Principle DIP)
-
高层模块不应该依赖低层模块,它们都应该依赖于抽象。这意味着具体的实现细节应该被封装起来,而高层模块只需要面向抽象接口编程。
-
例如,通过使用接口或抽象类来定义一个通用的数据访问层,使得高层业务逻辑不需要关心具体的数据库操作。
这些原则可以帮助开发人员创建灵活、可维护和可扩展的代码。
-
- SRP 可以使类更加可读和理解
- OCP 可以避免破坏现有代码的更改
- LSP 可以避免意外的行为
- ISP 可以避免臃肿的接口
- DIP 可以减少耦合度。
面向对象编程式的缺点
面向对象编程语言的问题在于,它总是负带着所有它需要的隐含环境。你想要一个香蕉,但得到的却是一个大猩猩拿着香蕉,而且还有整个丛林 - Joe Armstrong(Erlang创始人)
因为是通过类来存储数据和操作,但是有时候我们只需要一个类的一部分功能,但是我们仍不可避免的把整个类引用过来,没办法进行新功能的导入。同时,数据的修改历史完全被隐藏。,是通过一系列补丁来编写程序。
05_函数式编程
- 函数是“第一等公民”
- 纯函数 / 无副作用
- 高阶函数 / 闭包
尽量减少变化的部分,从而使代码更为清晰
一等函数 First Class Function
// 聚合转发
const BlogController = {
index(posts) {
return Views.index(posts);
},
show(post) {
return Views.show(post);
},
create(attrs) {
return Db.create(attrs);
},
update(post, attrs) {
return Db.update(post, attrs);
},
destroy(post) {
return Db.destroy(post);
},
};
// 有了一等函数后
const BlogController = {
index: Views.index,
show: Views.show,
create: Db.create,
update: Db.update,
destroy: Db.destroy,
};
纯函数 Pure Function
优势:
- 可缓存
- 可移植
- 可测试
- 可推理
- 可并行
const retireAge = 60;
function retirePerson(p){
if(p.age > retireAge) {
p.status = 'retired';
}
}
// 修改为纯函数
function retirePerson(p){
const retireAge = 60;
if(p.age>retireAge){
return {
...p,
status:'retired'
}
}
return p;
}
柯里化 Currying
function add(a,b,c){
return a+b+c;
}
// 参数重复
add(1,2,5)
add(1,2,4)
add(1,2,7)
// 柯里化之后
const add_ = curry(add);
const add12 = add_(1, 2);
// 之前参数存入闭包
add12(5);
add12(4);
add12(7);
const add1 = add_(1);
add1(2, 5);
add1(3, 5);
function add(a, b, c) {
return a + b + c;
}
function curry(fn) {
const arity = fn.length; // 闭包
return function $curry(...args) {
if (args.length < arity) {
return $curry.bind(null, ...args); // 闭包
}
return fn.call(null, ...args);
};
}
组合 Composition
// 手动组合
const toUpperCase = x => x.toUpperCase()
const log = x => console.log;
function alertUppercase(str){
log(toUpperCase(x));
}
// Composition
const alertUppercaseFn = compose(log,toUpperCase)
const compose = (...fns)=>(...args)=>fns.reduceRight(
(res,fn)=>[fn.call(null,...res)],args
)[0];
// associativity:compose(f,compose(g,h)) === compose(compose(f,g),h);
// map's composition law: compose(map(f),map(g)) === map(compose(f,g))
Functor
可以当作容器的类型,类型支持对容器内元素进行操作
常见的 functor:Array(Iterable).map,Promise.then
// 一般判断(吐槽一下,真的会有人这么写么)
a.b != null ? (a.b.c != null ? (a.b.c.d !== null ? a.b.c.d.e : null) :null) :null;
// Functor
class Maybe{
constructor(x){
this.$value = x;
}
map(fn){
return this.$value == null ? this : new Maybe(fn(this.$value));
}
}
new Maybe(a).map(prop('b')).map(prop('c')).map(prop('d')).map(prop('e'));
Monad
可以去除嵌套容器的容器类型
常见monad:Array.flatMap Promise.then
[1,2].flatMap(()=>([1,2]));
Promise.resolve(1).then(r=>Promise.resolve(2*r))
// Monad
Maybe.prototype.flat = function(level = 1) {
if(this.$value?.constructor !== Maybe){
return new Maybe(this.$value);
}
return level ? this.$value.flat(--level) : this.$value
}
Applicative
直接对两个容器进行操作
new Maybe(2).map(two => new Maybe(3).map(add(two))).flat(); // Maybe(Maybe(5))
// Applicative
new Maybe(add).ap(new Maybe(3)).ap(new Maybe(2));
class Maybe {
constructor(x) {
this.$value = x;
}
map(fn) {
return this.$value == null ? this : new Maybe(fn(this.$value));
}
ap(other) {
return other.map(this.$value);
}
}
// Applicative 的规则
// 一致性规则 Identity: Maybe(id).ap(v) === v
// 重构性规则 Homomorphism: Maybe(f).ap(Maybe(x)) === Maybe(f(x));
// 可交换规则 Interchange: v.ap(Maybe(x)) === Maybe(f => f(x)).ap(v);
// 组合规则 Composition: Maybe(compose).ap(u).ap(v).ap(w) === u.ap(v.ap(w))
06_响应式编程
RxJS
- 异步/离散的函数式编程
- 数据流
- 操作符
- 过滤
- 合并
- 转化
- 高阶
Observable
- 观察者模式
- 需要订阅获取到数据
- 迭代器模式
- 数据会不断的推送过来,而不是一次性的
- Promise/EventTarget 超集*
const { fromEvent } = rxjs; const clicks = fromEvent(document,'click'); const sub = clicks.subscribe(x => console.log(x)) setTimeout(()=>sub.unsubscribe(),5000)
操作符
响应式编程的 “compose”
- 合并
- 过滤
- 转化
- 异常处理
- 多播
const { fromEvent, map } = rxjs;
const clicks = fromEvent(document, "click");
// 转化式编程
const positions = clicks.pipe(
map(ev => ev.clientX), // 转化操作符
map(clientX => ++clientX)
);
positions.subscribe(x => console.log(x));
//
const events = [{ clientX: 1 }, { clientX: 2 }];
compose(
map(clientX => ++clientX),
map(ev => ev.clientX)
);
Monad
去除嵌套的
const { fromEvent, flatMap, fetch } = rxjs;
const clicks = fromEvent(document, "click");
const users = clicks.pipe(
flatMap(e => fetch.fromFetch( // 嵌套 Observable
"https://api.github.com/users?per_page=5"
)),
flatMap(res => res.json()) // 嵌套Promise
);
users.subscribe(data => console.log(data));
总结
| 编程范式 | 过程式 | 自顶向下 |
|---|---|---|
| 结构化编程 | ||
| 问题 | ||
| 面向对象 | 封装 | |
| 继承 | ||
| 多态 | ||
| 原则 | ||
| 问题 | ||
| 函数式编程 | 一等函数 | |
| 纯函数 | ||
| curry/compose/functor/monard/applicative | ||
| 响应式编程 | observable | |
| 操作符 | ||
| monard |
领域特定语言 —— 了解即可
什么是领域特定语言
- Domain-specific language(DSL):应用于特定领域的语言
- HTML
- SQL
- General-purpose language :通用语言
- C/C++
- Javascript
lexer
SQL Token 分类
- 注释
- 关键字
- 操作符
- 空格
- 字符串
- 变量
Parse_语法规则
上下文无关语法规则
<selectStatement> ::= SELECT <selectList> FROM <tableName>
<selectList> ::= <selectField> [ , <selectList> ]
<tableName> ::= <tableName> [ , <tableList> ]
- 推导式:表示 非终结符 到 (非终结符或终结符) 的关系
- 终结符:构成句子的实际阵容。可以简单理解为词法分析中的token
- 非终结符:符号或变量的有限集合。他们表示在句子中不同类型的短语或子句
Parse_LL
LL:从左到右检查,从左到右构建语法树
Parse_LR
LR:从左到右检查,从右到左构建语法树
tools
npx kison -m llk -g grammar.js -o cal.js
总结
| 编程范式 | 编程语言 | 机器语言 | |
|---|---|---|---|
| 汇编语言 | |||
| 中级语言 | C | ||
| 高级语言 | C++ | ||
| Lisp | |||
| JavaScript | |||
| 编程范式 | 什么是编程范式 | ||
| 过程式编程范式 | |||
| 面向对象编程 | |||
| 函数式编程 | |||
| 响应式编程 | |||
| 领域特定语言 | 创造 DSL | lexer | |
| parser | |||
| tools | |||
| visitor |