本文会结合两种常见的编程范式,对js语言层面的一些特性进行复习,并对Composing Software中的观点加以理解。
编程范式
前文我们讨论了编程范式,这里稍微回顾一下:
在我们编程中每解决一个问题对应concept,每种paradigm是多个concepts的组合,而每种编程语言实现了一种或多种paradigms。
常见concept组合
- [record] 当只有记录(record)概念,被称为描述式声明编程(descriptive declarative programming),比如xml
- [record,procedure] 当添加上过程(procedure)被称为一等函数式编程(first-order functional programming)
- [record,procedure,cell(state)] 当添加上使用cell传递数据,则被称为命令式编程(imperative programming),比如C语言,cell在这里代表一种类似细胞之间的信息传递方式,state表示状态。
- [record,procedure,cell(state),closure] 当添加上闭包(closure)就可以称为顺序面向对象编程(sequential object-oriented programming) 或有状态的函数式编程(stateful functional programming),比如java,当加上
thread
后就有了并发的能力,这里暂且不提。 - [record,procedure,closure] 当OOP基础上减去状态,就成了函数式编程(functional programming),比如scheme,js就是参考了scheme中很多特性
面向对象和函数式
OOP和FP是两种比较常见的编程范式,也被js同时支持,相关讨论有很多,这里只代表其中一种看法,更详细的会在后面章节介绍。
核心
按照前文说法,两种范式的区别就是FP比OOP少了一个state concept,即
面向对象侧重于对数据的各种处理,数据是对象的内部状态, C++ Primer Plus 对对象的本质表述为用来设计并扩展自己的数据结构:
函数式侧重于数据流的流动,数据是外界的输入,并最终将对应结果返回,A Brief Intro to Functional Programming 对于函数式编程概括为一种数据流,而不是控制流
适用场景
以下结论参考Functional programming vs Object Oriented programming [closed]
- 当对事物有固定操作时选择面向对象,主要用来添加新的事物,可以通过添加新类实现现存的方法来完成,存在的问题是当需要添加新操作时需要编辑很多类。 在项目中的例子比如对下载器的封装,每种下载器都有开始、暂停、恢复等固定动作。
- 当事物本身固定时选择函数式编程,主要是对现有事物添加操作,可以通过添加函数处理当前数据类型,存在的问题是当需要添加新事物时,需要编辑很多参与处理的函数。最直接的例子就是编译器,输入源代码,通过一系列操作,输出目标代码;再比如webpack中的loader。
面向对象专家 Michael Feathers 也说过:
面向对象的可读性是通过封装各部分,函数式的可读性是通过最小化各部分,即前者将数据和数据的动作进行封装,后者将各种操作拆分以最小化。
在日常开发怎么选择
每一个应用都是由各种组件组合而成,只不过组件的形式不固定,比如functions,data structures,classes等。
不同编程语言倾向于使用不同的原子元素组成组件,比如java使用对象,haskell使用function等。而在js中,因为天然支持面向对象和函数式,因此在项目中往往混合使用,我们可以使用对象组合来为函数式编程生成数据类型,使用函数式编程为面向对象编程生成对象,不管怎么写,软件开发的本质就是组合。
我们的工作就是根据各种情况使用各种编程范式,像盖房子一样将各种组件组合起来,在具体讨论两种范式之前我们先多了解一下js本身,
js中的相关特性
我们需要先对js这种语言中的各种特性有所了解,才能利用范式这种工具更好的对我们开发的软件进行组合。 这一部分主要会参考ecma262。
js语言的设计
Brendan Eich开发了js。他在这篇文章提到了js设计过程中的一些问题。
Netscape需要在浏览器中内置一种脚本语言,根据领导要求,首先需要像java(Look like Java,因此很多语法和java类似),而作者本人偏向于scheme,因此最后在新的语言中选择了和Scheme一样的一等函数以及和Self一样的prototype作为主要组成。受java影响,有了原始类型和对象的区分,比如string和String。
以上设计加上后来的发展就成了现在的js。
js特性概述
在js中的数据结构分为两种,原始类型和对象,js的对象创建并不是基于class的,而是有很多方式,比如字面量或者构造函数,每个构造函数都有一个prototype
属性用于实现基于原型(prototype-based)的继承。一个构造函数的prototype
还有一个constructor
引用指向构造函数本身,当实现继承时,这个属性可能会改,按照惯例需要修正,但不是必须的(关于constructor参考这里)。
说起构造函数,这里补充一点相关概念,函数是一种特殊的对象,含有internal method [[Call]]
,因此可以通过函数调用来执行相关代码,而构造函数又是一种特殊的函数,含有internal method [[Construct]]
,可以通过new
或super
调用创建对象。
js中的对象
原型链
每一个通过构造函数创建的对象都有一个指向构造函数prototype属性的隐式引用(可以用__proto__
访问但不推荐),而这个prototype本身可能也有一个非null的引用指向它的prototype,等等,这就被称为原型链。当访问对象的一个属性时,会首先从该对象本身查找,如果找不到就会沿着原型链依次查找,直到找到或者找到尽头发现没有,原型链上的属性可以被覆盖。
相较基于class继承的语言,通常来说,状态被实例拥有,方法被class拥有,继承的只有结构和行为(behavior),而js这一切都是可以继承的。这里的行为是方法整体决定的,如果没有方法,一个class将只有结构。
原型链的尽头为null,要明确一个对象的原型链到底包括什么,这里可以大概分为以下
- 如果是个
new Object()
或字面量{}
,其原型链为
var obj={}
//原型链 obj=>Object.prototype=>null
- 如果是new调用了其他构造函数,包括自定义的或者内置的(比如Array,内置构造函数不一定需要new调用,比如也可以通过字面量或者不使用new,比如
[]
或Date()
),这里以Array为例
var arr =[]
//arr=>Array.prototype=>Object.prototype=>null
//如果是自定义构造函数也一样
var P=function(){}
var p=new P()
//p=>P.prototype=>Object.prototype=>null
- 如果在上一种情况下,延长原型链,其实就是怎么样实现继承
//1. 使用Object.create()
var q=Object.create(p)
// q=>p
//2. 直接修改构造函数的prototype
function Q(){}
Q.prototype=p
var q=new Q()
//q=>p
//3. 通过Object.setPrototypeOf(obj, prototype)设置__proto__属性,可以直接修改原型链,这个操作很浪费性能,少用
function P(){
this.b=1
}
function Q(){
this.a=2
}
var q=new Q()
Object.setPrototypeOf(q,P.prototype)
// q=>P.prototype
//4. 使用call和apply借用构造函数时,和原型无关
var P=function(v){
this.a=v
}
function Q(v){
P.call(this,v)
}
var q=new Q(2)
// q=>Q.prototype=>Object.prototype=>null
我们可以通过object instanceof constructor
判断一个构造函数的prototype是否在指定对象的原型链中
function Q(){
this.a=2
}
var q=new Q()
console.log(q instanceof Q)
console.log(q instanceof Object)
可以通过Object.getPrototypeOf(object)
获得对象的__proto__
属性
Object
Object.prototype上有一些属性和方法被其他所有对象继承,在特定对象继承过程中可能会对某些字段重写。
另外Object上还有很多静态方法用于处理关于对象的各种操作,具体请参考mdn
js中的函数
在js中所有函数都是Function的实例,包括Object和Function本身,乃至各种内置构造函数(比如Array),因此有
Function.__proto__===Function.prototype//true
Function.prototype.__proto__===Object.prototype//true,即Function instanceof Object
//原型链 Function=>Function.prototype=>Object.prototype,以下类似
Object.__proto__===Function.prototype//true,即Object instanceof Function
Array.__proto__===Function.prototype //true
function a(){}
a.__proto__===Function.prototype//true
可见,所有函数的原型链到达Object.prototype
之前需要先经过Function.prototype
,一个函数是一个对象,更是一个函数。
Function
Funciton.prototype上有一些方法值得我们关注
- func.apply(thisArg, [argsArray])
- function.call(thisArg, arg1, arg2, ...)
- function.bind(thisArg[, arg1[, arg2[, ...]]])
其中前两个在一个对象的上下文应用另一个对象的方法,第三个用于修改上下文,其余参数会在返回的函数调用时使用
js的函数式编程
在具体的了解函数式编程之前,这里先了解一些概念,参考Composing Software。
概念
Pure Function
一个纯函数是一个函数,符合以下特点
- 相同的输入总是返回相同输出
- 没有副作用
纯函数在函数式编程中很重要,但是实际的开发中,函数或多或少会有一些副作用,比如数据获取和操作dom。
Function Composition
函数复合是将两个或多个函数按照顺序生成一个函数或者执行操作。
Shared State
共享数据可以是变量、对象或内存空间。使用共享数据的一个问题是为了了解一个函数的副作用,需要知道每个共享数据的操作历史,比如对一个用户信息在不同终端的修改会发生冲突,因此在flux中要使用单向流。
另一个问题是对共享数据的操作顺序也会造成不同结果,比如四则运算。
Immutability
一个不可变对象是创建后就不能改变,但是js在语言层面只提供了原始类型的不可变性,对对象并不提供这种特性,即使使用Object.freeze()
等方法也只能冻结某个层级的对象修改,要想使用不可变数据,可以使用第三方库,比如Immer。
不可变对象是函数式编程的核心概念,没有不可变性,程序中的数据流就会不可控,应该使用原数据生成新数据,而不应该修改原来的数据。
在实际的操作中,对于一个特定的数据,不可变性和不同享,至少要满足一个。
Side Effects
副作用指的是除了对输出结果操作以外其他的操作,比如打印日志或修改dom,副作用在函数式编程中应该避免,即将副作用和数据流处理分开。
Reusability Through Higher Order Functions
高阶函数是任何以函数作为参数或返回函数的函数,经常用于
- 使用回调函数、promise或monads对动作、副作用或异步数据流进行抽象或隔离。
- 为操作各种类型的变量创建工具函数
- 为了复用或函数组合而创建偏函数或柯里化
- 将一系列输入的函数串联返回一个函数组合
Containers, Functors, Lists, and Streams
这里包括上面提到的monads,可以参考Functors, Applicatives, And Monads In Pictures
一个functor数据结构可以用于映射数据,比如 [1,2,3].map(x => x *2)
,换句话说,它是一个容器,会为内部的数据应用一个函数,当看到这个词时应该想到mappable
在这里被映射的是一个数组,只要提供map api,其他数据结构应该也可以,一个按顺序处理的list可以看作是一个stream。
Declarative vs Imperative
函数式编程是一种声明式范式,声明式编程会将流控制过程抽象,而不是用一行行代码描述怎么做,对应的是命令式。 比如函数doubleMap命令式的写法
const doubleMap = numbers => {
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
return doubled;
};
console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
声明式的写法
const doubleMap = numbers => numbers.map(n => n * 2);
console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
总结
一个函数式编程应该有以下特征
- 纯函数,而不是共享数据和副作用
- 不可变,而不是可变数据
- 函数组合,而不是命令式流控制
- 泛型工具而不是对某些数据的特定方法
- 声明式,而不是命令式
- 表达式,而不是语句
在js中的函数式应用可以参考A GENTLE INTRODUCTION TO FUNCTIONAL JAVASCRIPT系列,最终目的就是将应用中的整个逻辑切分到不同的函数中,然后将函数组合,完成最终的任务。
在具体处理过程中注意函数式编程的各种特征。
常见的函数组合方式包括
- compose,又称为pipe
const compose = (...functions) => flowIn => functions.reduceRight( ( acc,f ) => f(acc), flowIn )
- curry,这里实现一个具体的柯里化
const add = a => b => a + b;
add(1)(2)
js的面向对象编程
js的以原型为基础的继承不太适合实现面向对象的封装、继承和多态,而es6在语言层面实现了class语法,可以很方便的采用其他语言实践总结而来的设计模式和设计原则,这里建议采用ts,具体可参考ts实现的23种设计模式和设计原则
无论采用何种范式,最终都是要将各个模块组合成我们的软件。
完结撒花
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!