0%

JS的OOP与函数式编程

函数式编程

函数式编程是一种编程范式,它将电脑运算视为函数的计算。

编程范式(programming paradigm),指的是计算机编程的基本风格或典范模式。

常见的编程范式有:函数式编程、程序编程、面向对象编程、指令式编程等,其他的,如,声明式编程(eg. SQL)、泛型编程。
其中,程序编程 也称 命令式编程 或 过程化编程(所以,面向过程属于程序编程范式?)。
函数式编程语言一般有如下几个特性:

函数是“第一等公民”

即函数可以出现在任何地方,比如可以把函数作为参数传递给另一个函数,不仅如此还可以将函数作为返回值。

闭包(Closure) 和 高阶函数

闭包可以理解成“定义在一个函数内部的函数”。
本质上,闭包是将函数内部和函数外部连接起来的桥梁,就是能在一个函数外部执行这个函数内部定义的方法,并访问这个函数内部定义的变量。
举个栗子:

1
2
3
4
5
6
7
8
function func1() {
var num = 100;
function func2() {
return num;
}
return func2;
}
console.log(func1()());// 100

上例中,func2 即为闭包。一般这种情况下,可以用匿名函数的写法:

1
2
3
4
5
6
7
function func1() {
var num = 100;
return function() {
return num;
};
}
console.log(func1()());// 100

高阶函数 指 接受函数作为参数 或 将函数作为返回值 的 函数。上例中,func1 的返回值是一个函数,func1 即为高阶函数。

Lambda演算 与 函数柯里化

所谓柯里化就是把具有较多参数的函数转换成具有较少参数的函数的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
function sum(a, b, c) {
return a + b + c;
}
sum(1, 2, 3);// 6
// 柯里化
function sum(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}
sum(1)(2)(3);// 6

可以看出,柯里化是以闭包为基础的。
上例中,柯里化反而增加了代码复杂度,但实际上,柯里化是有一些好处的,比如:延迟计算、动态创建函数、参数复用。
https://www.zhihu.com/question/37774367/answer/192978122

面向对象还是基于对象?

先说下结论:JS 中有对象的概念,但又不是纯粹的面向对象编程语言。
怎么理解?我们知道,面向对象的特性有 封装、继承、多态。

没有类的概念,而是直接使用对象

虽然 ECMAScript 2015 中引入了类,但实质上是JS现有的基于原型的继承的语法糖
类语法不会为JS引入新的面向对象的继承模型。
JS中的函数其实是函数对象,JS中的一切皆对象
一些函数语法或对象语法,本质上都是创建 Function 对象 和 Object 对象。

1
2
3
4
var x = new Function();
var x = function() {};console.log(x instanceof Function);// true
function x() {};console.log(x instanceof Function);// true
var o = {};// 等价于 var o = new Object();

通过原型实现继承

alt
原型 是一种管理对象继承的机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 父对象
function Person(name) {
this.name = name;
}
Person.prototype.say = function(message) {
console.log(this.name + ': ' + message);
};
// 子对象
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}

function inherits(Child, Parent) {
// 空对象
var F = function () {};
// 把空对象的原型指向父对象
F.prototype = Parent.prototype;
// 把子对象的原型指向一个空对象的实例
Child.prototype = new F();
// 修复子对象的原型的构造函数
Child.prototype.constructor = Child;
}
// 实现原型继承
inherits(Student, Person);

// 自此达成原型继承, 接下来可以定义子对象原型(就是new F()对象)的方法
Student.prototype.getGrade = function() {
return this.grade;
};

// 使用看看
var hanmeimei = new Student('韩梅梅', '一年级');
console.log(hanmeimei.name, hanmeimei.grade);
hanmeimei.say('Hello World.');
console.log(hanmeimei instanceof Student);
console.log(hanmeimei instanceof Person);// 验证继承关系
console.log(hanmeimei.__proto__ === Student.prototype);
console.log(hanmeimei.__proto__.__proto__ === Person.prototype);// 验证原型

上例中的原型链(红色箭头部分)如图所示:
alt
借助空对象实现原型链继承。
ES6引入了关键字class,让定义“类”更简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person {
constructor(name) {
this.name = name;
}
say(message) {
console.log(this.name + ': ' + message);
}
}
class Student extends Person {
constructor(name, grade) {
super(name);// 用super调用父类的构造函数
this.grade = grade;
}
getGrade() {
return this.grade;
}
}

// 使用看看
var hanmeimei = new Student('韩梅梅', '一年级');
console.log(hanmeimei.name, hanmeimei.grade);
hanmeimei.say('Hello World.');
console.log(hanmeimei instanceof Student);
console.log(hanmeimei instanceof Person);// 验证继承关系
console.log(hanmeimei.__proto__ === Student.prototype);
console.log(hanmeimei.__proto__.__proto__ === Person.prototype);// 验证原型

在原型继承基础上实现多态

继续上面的示例:

1
2
3
4
5
6
7
8
9
10
// 省略之前的代码...
Student.prototype.say = function(message) {
console.log(this.name + '(' + this.grade + '): ' + message);
};

// 使用看看
var lilei = new Person('李磊');
lilei.say('How are you?');// 李磊: How are you?
var hanmeimei = new Student('韩梅梅', '一年级');
hanmeimei.say('Fine, thank you, and you?');// 韩梅梅(一年级): Fine, thank you, and you?

Student覆盖Person的say方法,使得say方法在不同对象下调用有不同的逻辑。

所以,JS不是纯粹的面向对象(或者说不是我们理解的面向对象实现),而是结合自身语言特性来实现面向对象的特性。

既然没有类,为什么创建对象时要用new操作符?

要创建对象的新实例,必须使用new操作符,以这种方式调用构造函数实际上会经历4个步骤:

  • 创建一个新对象。
  • 将构造函数的作用域赋给新对象(因此 this 就指向了这个对象)。
  • 执行构造函数中的代码(为这个新对象添加属性)。
  • 返回新对象。

用构造函数new对象的代码大致如下:

1
2
3
4
5
6
7
function Person(name, age) {
this.name = name;
this.age = age;
}
var person = new Person('Hanmeimei', 21);
console.log(person.name, person.age);// Hanmeimei 21
console.log(person instanceof Person);// true

那么我们是否可以模拟new的过程来自主实现同等的效果呢:

1
2
3
4
5
6
7
8
9
function newFunc(constructor) {
var o = {};
o.__proto__ = constructor.prototype;
constructor.apply(o, Array.prototype.slice.call(arguments, 1));
return o;
}
var person1 = newFunc(Person, 'Hanmeimei', 22);
console.log(person1.name, person1.age);// Hanmeimei 22
console.log(person1 instanceof Person);// true

参考资料

JS中new操作符的工作原理是什么
JavaScript的原型继承
JavaScript的class继承
JavaScript常见的六种继承方式