JavaScript设计模式之一:面向对象的Javascript

(本节内容摘自:Javascript设计模式与开发实践一书,作为自己的笔记保存,希望对有需要的朋友有用)

JavaScript没有提供传统面向对象语言中的类式继承,而是通过原型委托的方式来实现对象与对象之间的继承。

一、动态类型语言

编程语言按数据类型分类,大致可以分为静态类型语言和动态类型语言。

静态类型语言在编译时已确定变量类型,而动态语言类型的变量类型要到程序运行时被赋值后才能确定,Javascript是一门典型的动态类型语言。

鸭子类型(duck typing),关于这个有一个故事:从前有个国王,他觉得这个世界上鸭子的叫声很美妙,于是召集大臣要组建一个1000只鸭子组成的合唱团,大臣们找遍了全国却只有999只,最后大臣们发现有一只鸡,它的叫声跟鸭子一模一样,于是这只鸡成为了鸭子合唱团的最后一员。

下面我们用代码来模拟上面的这个故事:


var duck = {
    duckSinging: function() {
        console.log('嘎嘎嘎');
    }
};
var chicken = {
    duckSinging: function() {
        console.log('咯咯咯');
    }
};

var choir = [];  //合唱团

var joinChoir = function(animal) {
    if(animal && typeof animal.duckSinging === 'function') {
        choir.push(animal);
        console.log('恭喜加入合唱团');
        console.log('合唱团已有成员数量:' + choir.length);
    }
};

joinChoir(duck);    //恭喜加入合唱团
joinChoir(chicken);	//恭喜加入合唱团


鸭子类型的概念在动态类型语言的面向对象设计中非常重要,利用它我们可以在动态类型语言中实现“面向接口编程”,而不是“面向实现编程”。

二、多态

多态(polymorphism),它的含义是同一操作作用于不同的对象上,可以产生不同的解释和不同的执行效果,换句话说,给不同的对象发送同一消息时,这些对象会根据这个消息分别给出不同的反馈,下面举个栗子:

有一只鸭和一只鸡,它们都会叫,当主人向它们发出“叫”的指令时,鸭会“嘎嘎嘎”的叫,而鸡会“咯咯咯”的叫,两只动物会根据主人发出的同一指令,发出各自不同的声音。

下面我们来看一段多态的Javascript代码:


var makeSound = function(animal) {
    if(animal instanceof Duck) {
        console.log('嘎嘎嘎');
    }else if(animal instanceof Chicken) {
        console.log('咯咯咯');
    }
};
var Duck = funcdtion(){};
var Chicken = function(){};

makeSound(new Duck());        //嘎嘎嘎
makeSound(new Chicken());     //咯咯咯


多态背后的思想就是把“做什么”和“谁去做及怎样去做”分离开,也就是将“不变的事”和“可能改变的事”分离开。很显然,上面的代码有问题,如果我们再增加一只狗,就要改动makeSound函数,修改代码是危险且不可取的,我们要让代码变得可扩展,我们将上面的代码进行改动,如下:


//将不变的部分分离出来,这里就是所有的动物都会叫
var makeSound = function(animal) {
    animal.sound();
};

//将可变的部分封装起来
var Duck = function(){};
Duck.prototype.sound = function(){
    console.log('嘎嘎嘎');
}

var Chicken = function(){};
Chicken.prototype.sound = function(){
    console.log('咯咯咯');
}

makeSound(new Duck());            //嘎嘎嘎
makeSound(new Chicken());         //咯咯咯


如果我们需要增加一只动物,那么我们只需要增加代码即可,而不需要去改动makeSound函数


var Dog = function(){};
Dog.prototype.sound = function(){
    console.log('汪汪汪');
}
makeSound(new Dog());        //汪汪汪


由此可见,Javascript的多态性是与生俱来的,它作为一门动态类型语言,既不会检查对象类型,也不会检查参数类型,从上面的例子看出,我们既可以往makeSound函数里传递duck参数,也可以传递chicken参数,所以,一种动物是否能发出声音,只取决于它有没有makeSound方法,而不取决于它是否是某种类型的对象。

下面我们再来看一个在实际项目中可能会遇到的例子,假设我们要编写一个地图应用,有两家地图API可供选择,他们都提供了show方法,代码如下:


var googleMap = {
    show: function(){
        console.log('开始渲染谷歌地图');
    }
};
var renderMap = function(){
    googleMap.show();
};

renderMap();        //开始渲染谷歌地图


现在我们需要把谷歌地图换成百度地图


var googleMap = {
    show: function(){
        console.log('开始渲染谷歌地图');
    }
};

var baiduMap = {
    show: function(){
        console.log('开始渲染百度地图');
    }
};

var renderMap = function(type) {
    if(type === 'google') {
        googleMap.show();
    }else if(type === 'baidu'){
        baiduMap.show();
    }
};

renderMap('google');            //开始渲染谷歌地图
renderMap('baidu');             //开始渲染百度地图


OK,现在问题来了,如果我再增加一个搜搜地图呢?那就要改动renderMap函数,继续在里面添加条件分支语句,所以,看下面的代码:


var googleMap = {
    show: function(){
        console.log('开始渲染谷歌地图');
    }
};

var baiduMap = {
    show: function(){
        console.log('开始渲染百度地图');
    }
};

//把相同的部分抽象出来,也就是显示地图
var renderMap = function(map){
    if(map.show instanceof Function){
        map.show();
    }
};

renderMap(googleMap);        //开始渲染谷歌地图
renderMap(baiduMap);         //开始渲染百度地图


这时,我们如果需要添加其他的地图API


var sosoMap = {
    show: function(){
        console.log('开始渲染搜搜地图');
    }
};

renderMap(sosoMap);


在Javascript中,函数是一等对象,函数本身也是对象,函数用来封装行为并能被四处传递,当我们向函数发出“调用”消息时,这些函数会返回不同的执行结果。

二、封装

封装的目的就是将信息隐藏,一般我们讨论的是对数据和实现进行封装,除此之外更广泛的是对封装类型和封装变化。

1、封装数据

在其他许多编程语言中提供了private、public、protected等关键字来实现封装,但Javascript没有,我们只有依靠变量的作用域来实现,而且只能模拟出public和private这两种封装特性


var myObject = (function(){
    var _name = 'sven';        //私有(private)变量
    return {
        getName: function(){        //公开(public)方法
            return _name;
        }
    }
})();

console.log(myObject.getName());        //sven
console.log(myObject._name);            //undefined


另外,在ES6中,可以通过Symbol来创建私有属性。 

2、封装实现

3、封装类型

4、封装变化

三、继承

Javascript中继承是基于原型模式的,而像Java、C++等这些是基于类的的面向对象语言,我们要创建一个对象,必须先定义一个Class,然后从这个Class里实例化一个对象出来。然而Javascript中并没有类,所以,在JavaScript中对象是被克隆出来的,也就是一个对象通过克隆另一个对象来创建自己。

我们假设编写一个网页版的飞机大战游戏,这个飞机拥有分身技能,当使用这个技能时,页面上会出现多个同样的飞机,这时我们就需要使用到原型模式来克隆飞机。ES5提供了Object.create方法来克隆对象,看如下代码:


var Plane = function(){
    this.blood = 100;
    this.attackLevel = 1;
    this.defenseLevel = 1;
};
var plane = new Plane();
plane.blood = 500;
plane.attackLevel = 10;
plane.defenseLevel = 7;

var clonePlane = Object.create(plane);
console.log(clonePlane);                //Object{blood: 500, attatckLevel: 10, defenseLevel: 7}

//在不支持Object.create方法的浏览器中,使用以下代码:
Object.create = Object.create || function(obj){
    var F = function(){};
    F.prototype = obj;
    return new F();
}


原型继承遵循以下原则,Javascript也不例外:

a、所有的数据都是对象

b、要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它

c、对象会记住它的原型

d、如果对象无法响应某个请求,它会把这个请求委托给它自己的原型

Javascript中存在一个根对象Object.prototype,它是一个空对象,所有的对象都是从这个根对象中克隆而来的,Object.prototype就是它们的原型。


var obj1 = new Object();
var obj2 = {};

//利用ES5提供的Object.getPrototypeOf方法来查看它们的原型
console.log(Object.getPrototypeOf(obj1) === Object.prototype);    //true
console.log(Object.getPrototypeOf(obj2) === Object.prototype);    //true


通过new运算符从构造器中得到一个对象,看下面的代码:


function Person(name){
    this.name = name;
};

Person.prototype.getName = function(){
    return this.name;
};

var a = new Person('Kaindy');

console.log(a.name);            //Kaindy
console.log(a.getName());        //Kaindy
console.log(Object.getPrototypeOf(a) === Person.prototype);            //true


上面代码中的Person并不是一个类,而是函数构造器,Javascript的函数既可以作为普通函数使用,也可以作为构造器调用,当使用new运算符时,函数就成了构造器,这个创建对象的过程,也就是先克隆了Object.prototype,然后再做其他的一些操作。

在Javascript中,每个对象都会记住它的原型,准确的说,应该是对象的构造器有原型。每个对象都有一个名为__proto__的隐藏属性,这个属性会指向它的构造器的原型对象


var a = new Object();
console.log(a.__proto__ === Object.prototype);        //true


实际上,每个对象就是通过自身隐藏的__proto__属性来记住自己的构造器原型.

如果对象无法响应请求,它会把这个请求委托给它的构造器的原型,我们来看下面的代码:


var obj = {name: 'Kaindy'};

var A = function(){};
A.prototype = obj;

var a = new A();
console.log(a.name);        //Kaindy


我们来看下引擎做了什么,

首先,我们需要打印出对象a的name属性,尝试遍历对象a的所有属性,但没找到name

接着,对象a把查找name属性这个请求委托给了它自己的构造器原型,也就是a.__proto__,而a.__proto__指向了A.prototype,A.prototype被设置为了对象obj。

最后在obj中找到了name属性,并返回它的值。


结束:Object.create是原型模式的天然实现,目前大多数主流浏览器都支持此方法,但它效率并不高,比通过构造函数创建对象要慢,最新的ES6带来了Class语法,看起来像一门基于类的语言,但原理还是通过原型机制来创建对象,看下面的代码:


class Animal {
    constructor(name) {
        this.name = name;
    }
    getName() {
        return this.name;
    }
}

class Dog extends Animal {
    constructor(name) {
        super(name);
    }
    speak() {
        return "woof";
    }
}

var dog = new Dog("Scamp");
console.log(dog.geName() + ' says ' + dog.speak());


转载于:https://my.oschina.net/u/2399867/blog/631701

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值