前端开发js的函数式编程和面向对象编程实例

对前端开发岗位的理解|武汉学web前端开发多少钱?|前端开发师工作内容

本章讨论的主题包括如下:

  • JavaScript如何既是FP又是OOP的?
  • JavaScript的OOP-使用原型
  • JavaScript中如何混合使用FP和OOP?
  • 函数式继承
  • 函数式混入
    写出更好的代码是目标,而FP和OOP只是为了达到这一目标的手段而已。
1.JavaScript-多范式的语言

如果说OOP是把所有的变量都当做对象,而FP是把所有的函数当作变量,那么就不能把函数当作对象吗?在JavaScript中,是可以的。
但是,“FP是把所有函数当作变量”,这一说法某种意义上是不准确的。一个更好的说法是,FP是把一切都当作“值”,尤其是函数。
一个更好的方式依旧来描述FP可能是,称其为声明式的。去表达解决问题的必要计算逻辑时,声明式编程不依赖编程的命令分支结构。FP告诉计算机问题是什么,而不是如何解决问题的处理程序。
同时,OOP源自于命令式编程风格的:告诉计算机按部就班地如何解决问题的指令。OOP要求这些计算方法和被处理的数据组成一个单元:对象。只有通过使用对象的方法才能访问数据。

那么,这两种风格如何才能整合到一起呢?
-对象方法的代码通常写成命令式风格的。但假如写成函数式风格又会怎样呢?毕竟OOP是不会把不变数据和高阶函数拒之门外的。
-也许混合使用这二者的一个更纯粹的方式,就是把对象同时既当作函数,又当作传统上的基于类的对象。(译:强调对象中的方法?)
-也许我们可以在面向对象应用程序中引入FP中的想法,比如promise和递归。
-OOP涵盖的主题,譬如封装、多态和抽象。而FP也是如此,只是表现形式不一样罢了。因此,我们或许又可以在函数式应用程序中引进OOP的想法。
重点在于:OOP和FP可以混合使用,而且还有多种方式实现。它们并不互相排斥。

2.JavaScript面向对象的实现-使用原型

JavaScript是一门class-less的语言。并不是说它与其他语言那样相比,不时髦或者低人一等。“class-less”是说它没有面向对象语言的那种类结构,它使用原型来继承。
尽管这点通常会使拥有C++或Java背景的程序设计师困惑,但基于原型的继承比传统继承更富有表现力。

2.1继承

在详细探究之前,先确保我们充分理解OOP中的继承概念。基于类的继承可以用下面的伪代码来演示:
javascript 代码

class Polygon {
intnumSides;
functioninit(n){
numSides=n;
}
}
class Rectangle inherits Polygon {
intwidth;
intlength;
functioninit(w,l){
numSides=4;
width=w;
length=l;
}
functiongetArea(){
returnwidth*length;
}
}
class Square inherits Rectangle {
functioninit(s){
numSides=4;
width=s;
length=s;
}
}

其中Polygon类是继承于它的其他类父类。它只定义了一个成员变量,多边形的边数,numSides,并在init函数中被设置。Rectangle子类继承于Polygon类,并新增了两个成员变量,length和width,和一个方法,getArea。Rectangle类不需要定义变量numSides,因为此变量已经在其继承的父类中定义过了,同时它覆盖了init函数。Square类的存在继续拓展了继承链,它继承了Rectangle类的getArea方法。简单通过覆盖init方法来使length和width相等,getArea方法就不需修改了,代码也省了。

在一个传统的OOP语言中,继承就是上面那些东西。如果我们需要给所有类添加个color属性,只需把它加入到Polygon类中,而不需要修改其他子类。

2.2JavaScript的原型链

JavaScript中的继承实际上是原型。每一个对象都有一个内部属性被称为是它的原型,此内部属性是另一个对象的引用。而这另一个对象也有它自己的原型。这种模式重复,直到某个对象的原型未定义为止。这就是广为熟知的原型链,也是JavaScript继承如何工作的原理。

当查找一个对象的某个函数定义时,JavaScript会沿着其原型链进行查找,直到找到有同样的名字的第一个定义为止。因此只需在子类的原型上提供一个新的定义,就可以轻松实现覆盖。

2.3JavaScript中的继承和Object.create方法

就像有多种方式在JavaScript中创建对象那样,也有多种方式模拟基于类的经典继承。一个首选方式就是使用Object.create方法。
javascript 代码

var Polygon = function(n) {
this.numSides=n;
}
var Rectangle = function(w, l) {
this.width =w;
this.length =l;
}
// Rectangle的原型用Object.create重新定义
Rectangle.prototype = Object.create(Polygon.prototype);
// 还原constructor属性是很重要的,否则它会指向Polygon
Rectangle.prototype.constructor = Rectangle;
// 现在我们继续定义Rectangle类
Rectangle.prototype.numSides = 4;
Rectangle.prototype.getArea = function() {
returnthis.width *this.length;
}
var Square = function(w) {
this.width =w;
this.length =w;
}
Square.prototype = Object.create(Rectangle.prototype);
Square.prototype.constructor = Square;
var s = new Square(5);
console.log(s.getArea()); // 25

这种写法对很多人来说,看起来不太寻常,只需少量练习,就会变得很亲近。prototype这个单词是用来获取对象的内部属性[[Prototype]],每个对象都有这个属性的。Object.create方法用来得到一个以指定对象为其原型的新对象,以便来继承。通过这种方式,JavaScript就实现了经典继承。

我们在第5章(范畴论)构建Maybe类时已经见过这种继承。下面是Maybe、None和Just类间的继承:
javascript 代码

var Maybe = function() {}
var None = function() {}
None.prototype = Object.create(Maybe.prototype)
None.prototype.constructor = None
None.prototype.toString = function() {
return’None’
}
var Just = function(x) {
this.x =x
}
Just.prototype = Object.create(Maybe.prototype)
Just.prototype.constructor = Just
Just.prototype.toString = function() {
return’Just ‘+this.x
}

这点表明JavaScript中的类继承是FP的一个促成因素。

一个常见的错误是把一个构造器传给Object.create,而不是一个对象来作为原型。之所以会有这个问题是因为不去调用子类中继承来的方法时,浏览器是不会抛出异常的。
javascript 代码

Foo.prototype = Object.create(Parent.prototype) // 正确
Bar.prototype = Object.create(Parent) // 错误
Bar.inheritedMethod() // Error: function is undefined
3.在JavaScript中混合使用FP和OOP

OOP数十年来一直是主流的编程范式。全世界都会计算机科学101课程中传授OOP,而FP不然。软件架构师用OOP设计应用程序,而FP不然。尤其因为OOP更容易使想法概念化,让人轻松写出代码,更加剧了此现象。

因此,除非你能说服你的老板应用程序需要完全函数式的,否则我们只能在一个OOP的世界里使用FP。这一节将会探索这种做法。

3.1函数式继承

或许在JavaScript应用程序中应用FP最简单的方式,就是在OOP原则(比如继承)里写函数式风格的代码。

为了探讨可行性,我们建立一个计算产品价格的简单应用程序。首先,我们需要一些产品类:
javascript 代码

var Shirt = function(size) {
this.size =size
}
var TShirt = function(size) {
this.size =size
}
TShirt.prototype = Object.create(Shirt.prototype)
TShirt.prototype.constructor = TShirt
TShirt.prototype.getPrice = function() {
if (this.size ==’small’) {
return5
}else{
return10
}
}
var ExpensiveShirt = function(size) {
this.size =size
}
ExpensiveShirt.prototype = Object.create(Shirt.prototype)
ExpensiveShirt.prototype.constructor = ExpensiveShirt
ExpensiveShirt.prototype.getPrice = function() {
if (this.size ==’small’) {
return20
}else{
return30
}
}

我们可以在Store中如下地组织它们:
javascript 代码

var Store = function(products) {
this.products=products;
}
Store.prototype.calculateTotal = function() {
returnthis.products.reduce(function(sum,product){
returnsum+product.getPrice();
},10) *TAX;// 10美元的加价,最后乘以税率TAX
};
var TAX = 1.08;
var p1 = new TShirt(‘small’);
var p2 = new ExpensiveShirt(‘large’);
var s = new Store([p1, p2]);
console.log(s.calculateTotal()); // 48.6

calculateTotal方法中使用了数组的reduce方法用来计算各产品的价格总和。

这么写是可以的,但如果我们需要一个动态的方式来计算价格时,又会怎样呢?为此,我们可以转向一个概念,策略模式。

3.1.1策略模式

策略模式是一种方式定义了一系列相互可替换的算法。OOP程序设计师经常用它来操作运行时行为,但是他基于几个FP原则的:
[quote]-逻辑和数据的分离
-函数的组合
-函数是一等对象[/quote]
同时也基于几个OOP原则的:
[quote]-封装
-继承[/quote]

在我们计算产品价格的应用程序中,像先前说的那样,假如说我们想给一些消费者提供优惠,那么不得不变动计算价格方法来反映这一点。
因此,让我们创造几个消费者类
javascript 代码

var Customer = function() {}
Customer.prototype.calculateTotal = function(products) {
return (
products.reduce(function(total,product){
returntotal+product.getPrice()
},10) *TAX
)
}
var RepeatCustomer = function() {}
RepeatCustomer.prototype = Object.create(Customer.prototype)
RepeatCustomer.prototype.constructor = RepeatCustomer
RepeatCustomer.prototype.calculateTotal = function(products) {
return (
products.reduce(function(total,product){
returntotal+product.getPrice()
},5) *TAX
)
}
var TaxExemptCustomer = function() {}
TaxExemptCustomer.prototype = Object.create(Customer.prototype)
TaxExemptCustomer.prototype.constructor = TaxExemptCustomer
TaxExemptCustomer.prototype.calculateTotal = function(products) {
returnproducts.reduce(function(total,product){
returntotal+product.getPrice()
},10)
}

每一个Customer类都封装了算法。现在我们需要在Store类中调用Customer类的calculateTotal方法。
javascript 代码

var Store = function(products) {
this.products=products
this.customer=newCustomer()
}
Store.prototype.setCustomer = function(customer) {
this.customer=customer
}
Store.prototype.getTotal = function() {
returnthis.customer.calculateTotal(this.products)
}
var p1 = new TShirt(‘small’)
var p2 = new ExpensiveShirt(‘large’)
var s = new Store([p1, p2])
var c = new TaxExemptCustomer()
s.setCustomer(c)
console.log(s.getTotal()) // 45

Customer类做计算,Product类保存数据(价格),而Store类维系上下文。这就做到了高内聚和混合使用了OOP和FP。JavaScript高水平的表现力使这点成为可能,并相当简单。

3.2混入(mixin)

一句话说,mixin就是类,可以允许其他类使用它们的方法。其方法旨在单独地被其他类使用,mixin类本身不该实例化的。这有助于避免继承的不确定性。而且mixin是混合使用FP和OOP的重要方式。

在不同语言中实现mixin的方式也不同。多亏了JavaScript的灵活性和表现力,可以通过只有方法的对象来实现mixin。尽管mixin可以被定义为函数,比如var mixin = function() {…},为了代码结构化原则,最好把它们定义成对象字面量,比如var mixin = {…}。这样就会帮助我们更好地好地区分类和mixin。毕竟,mixin应该被当成处理程序而不是对象。

让我们先声明一些mixin作为开始。我们会拓展上节Store应用程序,使用mixin对类进行拓展。
javascript 代码

var small = {
getPrice:function(){
returnthis.basePrice+6
},
getDimensions:function(){
return [44,63]
},
}
var large = {
getPrice:function(){
returnthis.basePrice+10
},
getDimensions:function(){
return [84,100]
},
}

当然,我们并不局限于此。更多mixin可以添加进来,比如颜色或布料等。我们不得不稍微重写下我们的Shirt类:
javascript 代码

var Shirt = function() {
this.basePrice=1
}
Shirt.prototype.getPrice = function() {
returnthis.basePrice
}
var TShirt = function() {
this.basePrice=5
}
TShirt.prototype = Object.create(Shirt.prototype)
TShirt.prototype.constructor = TShirt

现在我们做好使用mixin的准备了。

3.2.1经典混入

你可能会好奇,这些mixin是怎么会和类混合在一起的。经典的实现方式是通过把minin里的函数拷贝到目标上。可以对Function原型进行如下的拓展:
javascript 代码

Function.prototype.addMixin = function(mixin) {
for (varpropinmixin) {
if (mixin.hasOwnProperty(prop)) {
this.prototype[prop] =mixin[prop]
}
}
}

然后,现在minxin可以通过如下的方式添加进去:
javascript 代码

TShirt.addMixin(small)
var p1 = new TShirt()
console.log(p1.getPrice()) // 11
TShirt.addMixin(large)
var p2 = new TShirt()
console.log(p2.getPrice()) // 15

然而,这会有一个很大的问题。当我们再一次计算p1的价格时,结果是15,是large那项的价格。本该是small那项的价格。
javascript 代码

console.log(p1.getPrice()); // 15

问题在于每次添加mixin时,Shirt的prototype.getPrice方法都被重写;这一点并不很函数式,也不是我们想要的。

3.2.2函数式混入

还有另外一种方式使用mixin,更与FP相匹配。

不通过拷贝mixin的方法到目标对象的那种方式,我们创建一个新的对象,是目标对象的副本,并把混入的方法添加进去。如何克隆对象?这可以通过创建一个新的对象继承它来做到。这种方式这里称为plusMixin。
javascript 代码

Function.prototype.plusMixin = function(mixin) {
varnewObj=this
newObj.prototype =Object.create(this.prototype)
newObj.prototype.constructor =newObj
for (varpropinmixin) {
if (mixin.hasOwnProperty(prop)) {
newObj.prototype[prop] =mixin[prop]
}
}
returnnewObj
}
var SmallTShirt = TShirt.plusMixin(small) // creates a new class
var smallT = new SmallTShirt()
console.log(smallT.getPrice()) // 11
var LargeTShirt = TShirt.plusMixin(large)
var largeT = new LargeTShirt()
console.log(largeT.getPrice()) // 15
console.log(smallT.getPrice()) // 11 (不受第二次混入的影响)

有趣的部分来了。现在我们可以通过mixin让应用很函数式,可以创建产品和mixin所有的可能组合。
javascript 代码

// 真实世界中可能会有更多产品和mixin
var productClasses = [ExpensiveShirt, TShirt]
var mixins = [small, medium, large]
// 混合它们
products = productClasses.reduce(function(previous, current) {
varnewProduct=mixins.map(function(mxn){
varmixedClass=current.plusMixin(mxn)
vartemp=newmixedClass()
returntemp
})
returnprevious.concat(newProduct)
}, [])
products.forEach(function(o) {
console.log(o.getPrice())
})

为个更加面向对象,我们可以重写Store,给Store类(而不是产品类)添加了一个展示价格函数,保持逻辑和数据分离。
javascript 代码

var Store = function() {
varproductClasses= [ExpensiveShirt,TShirt]
varproductMixins= [small,medium,large]
this.products=productClasses.reduce(function(previous,current){
varnewObJavaScript=productMixins.map(function(mxn){
varmixedClass=current.plusMixin(mxn)
vartemp=newmixedClass()
returntemp
})
returnprevious.concat(newObJavaScript)
}, [])
}
Store.prototype.displayProducts = function() {
this.products.forEach(function(p){
$(‘ul#products’).append(‘<li>’+p.getTitle() +’: $’+p.getPrice() +'</li>’)
})
}

接下来要创建一个Store对象,然后调用它的displayProducts方法,来展示产品和价格列表。
html 代码

<ul id=”products”>
    <li>small premium shirt: $16</li>
    <li>medium premium shirt: $18</li>
    <li>large premium shirt: $20</li>
    <li>small t-shirt: $11</li>
    <li>medium t-shirt: $13</li>
    <li>large t-shirt: $15</li>
</ul>

下面的代码需要被添加到产品类中:
javascript 代码

Shirt.prototype.title = ‘shirt’;
TShirt.prototype.title = ‘t-shirt’;
ExpensiveShirt.prototype.title = ‘premium shirt’;
// 然后mixin还需要添加额外的getTitle方法
var small = {
getTitle: function(){
return’small ‘+this.title;// small or medium or large
}
}

这样,我们就得到了一个高度模块化和可拓展的电子商务网站应用程序了。添加新种类衬衫相当容易,只需定义一个新的Shirt子类并添加到Store类中的数组productClasses中即可。添加新的mixin也是一样。假如现在你的boss说,“嘿,现在有一种新型的衬衫和外套,各种标准颜色都有的,在你回家之前需要添加到我们的网站上。”我们可以确信无疑,自己不会在公司停留太长时间。
译:完整案例如下:
html 代码

<ul id=”products”></ul>
<script src=”http://libs.baidu.com/jquery/1.9.1/jquery.min.js”></script>
<script>
    var small = {
        getPrice: function () {
            return this.basePrice + 6;
        },
        getDimensions: function () {
            return [44, 63]
        },
        getTitle: function () {
            return ‘small ‘ + this.title;
        }
    }
    var medium = {
        getPrice: function () {
            return this.basePrice + 8;
        },
        getDimensions: function () {
            return [64, 83]
        },
        getTitle: function () {
            return ‘medium ‘ + this.title;
        }
    }
    var large = {
        getPrice: function () {
            return this.basePrice + 10;
        },
        getDimensions: function () {
            return [84, 100]
        },
        getTitle: function () {
            return ‘large ‘ + this.title;
        }
    };
    var Shirt = function () {
        this.basePrice = 1;
    };
    Shirt.prototype.getPrice = function () {
        return this.basePrice;
    }
    Shirt.prototype.title = ‘shirt’;
    var TShirt = function () {
        this.basePrice = 5;
    };
    TShirt.prototype = Object.create(Shirt.prototype);
    TShirt.prototype.constructor = TShirt;
    TShirt.prototype.title = ‘t-shirt’;
    var ExpensiveShirt = function () {
        this.basePrice = 10;
    };
    ExpensiveShirt.prototype = Object.create(Shirt.prototype);
    ExpensiveShirt.prototype.constructor = ExpensiveShirt;
    ExpensiveShirt.prototype.title = ‘premium shirt’;
    Function.prototype.plusMixin = function (mixin) {
        var newObj = this;
        newObj.prototype = Object.create(this.prototype);
        newObj.prototype.constructor = newObj;
        for (var prop in mixin) {
            if (mixin.hasOwnProperty(prop)) {
                newObj.prototype[prop] = mixin[prop];
            }
        }
        return newObj;
    };
    // the store
    var Store = function () {
        var productClasses = [ExpensiveShirt, TShirt];
        var productMixins = [small, medium, large];
        this.products = productClasses.reduce(function (previous, current) {
            var newObJavaScript = productMixins.map(function (mxn) {
                var mixedClass = current.plusMixin(mxn);
                var temp = new mixedClass();
                return temp;
            });
            return previous.concat(newObJavaScript);
        }, []);
    }
    Store.prototype.displayProducts = function () {
        this.products.forEach(function (p) {
            $(‘ul#products’).append(‘<li>’ + p.getTitle() + ‘: $’ + p.getPrice() + ‘</li>’);
        });
    }
    var store = new Store();
    store.displayProducts();
</script>
小结

JavaScript富有很高表现力。这一点使混合使用FP和OOP称为可能。现代JavaScript不单单是OOP或FP,它是二者的混合物。譬如策略模式和混入的概念,对于JavaScript原型结构来说是完美的,表明JavaScript的FP和OOP如今有着等量的最佳实践。

前端开发 岗位描述|对前端开发岗位的认识|web前端开发现在就业情况

赞(0)
前端开发者 » 前端开发js的函数式编程和面向对象编程实例
64K

评论 抢沙发

评论前必须登录!