JS 面向对象程序设计

阅读 1277
收藏 107
2017-08-14
原文链接:www.jianshu.com

面向对象(Object-Oriented, OO)的语言有一个标志,那就是都有类的概念,通过类可以创建任意多个具有相同属性和方法的对象。而ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或函数”。其对象的创建主要有两种方式,对象字面量和原型链,一般程序员比较倾向于使用前者。其实现如下:

var object = {  //对象字面量
  name1: value,
  name3: {
    name_1: value
  }
}

function object() {  //原型链
}//对象默认带有prototype这样一个属性
object.prototype.name1 = value;

1、理解对象属性、创建对象

JavaScript 中的所有事物都是对象(Object):字符串、数值、数组、函数。此外,JavaScript 允许自定义对象,而创建自定义对象的最简单方式就是就是创建Object实例,再为它添加属性和方法,对象内允许定义多个属性,也允许定义多种类型的属性。

1.1属性类型

ECMAScript中有数据属性和访问属性两种.。
数据属性:[[Configurable]]可配置、[[Enumerable]]可枚举、[[Writable]]可写。

Object.defineProperty(person, "name", {  
  configurable: false,  // 将configurable配置为false在进行配置将会抛出错误
  value: "Nicholas"
});   
 //throws error
 Object.defineProperty(person, "name", {
   configurable: true,
  value: "Nicholas"
});

其他两个属性也与configurable类似
访问器属性:[[Configurable]]可配置、[[Enumerable]]可枚举、[[Get]]在读取属性时调用的函数、[[Set]]在写入属性时调用的函数。其中,在ES6之前的版本中属性前面带有下划线的,用于表示只能通过对象方法访问的属性。
读取属性的特性在ECMAScript5中使用的是Object.getOwnPropertyDescriptor()方法,API的使用详见手册。

1.1创建对象

虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象会产生大量重复的代码。这时,就有了工厂模式的用武之地。
工厂模式
工厂模式是软件工程领域一宗广为人知的设计模式,这种模式抽象了创建具体对象的过程。以下为实例代码:

function createDog(varieties, age){
    var d = new Object();
    d.varieties = varieties;
    d.age = age;
    d.sayName = function(){
        alert(this.name);
    };    
    return d;
}
var dog1 = createPerson("Husky", 2);
var dog2 = createPerson("Alaska", 3);

工行模式的实现解决了多个相似对象的问题,但是对象识别的问题并没有的到解决,这时,构造函数模式出现了。
构造函数模式
构造函数模式与工厂模式有个显著的不同,其没有显示的创建对象,直接将属性和方法赋给了this对象,没有return语句,代码示例如下:

function Dog(name, age, job){
    this.varieties = varieties;
    this.age = age;
    this.sayName = function(){
        alert(varieties, age);
    };     
}        
var dog1 = new Dog("Husky", 2);
var dog2 = new Dog("Alaska", 3);

创建一个Dog新实例,使用new操作符,这种方式调用构造函数会经历以下4个步骤:
1、创建一个新对象;
2、将构造函数的作用域赋给新对象(因此这个this就指向这个新对象);
3、执行构造函数中的代码(为这个新对象添加属性);
4、返回新对象。
另还有特殊的将构造函数当做函数,其中构造函数与其他普通函数最大的区别就在于调用它们的方式不同。
原型模式
我们没创建的一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途包含可以由特定类型的所有实例共享的属性和方法。同时对象实例的信息就可以通过该原型链添加到原型对象中,而不必在构造函数中定义,如下:

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var person1 = new Person();
person1.sayName();   //"Nicholas"
var person2 = new Person();
person2.sayName();   //"Nicholas"
alert(person1.sayName == person2.sayName);  //true
alert(Person.prototype.isPrototypeOf(person1));  //true
alert(Person.prototype.isPrototypeOf(person2));  //true

而在ES6以前的版本,每个对象都支持一个属性__proto____,同时明确一个重要的信息就是这个实例存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
还有需要注意圆形的动态性,因为实例与原型之间的连接只不过是一个指针,而非一个副本。我们知道调用构造函数时会为实例添加一个指向最初原型的[[prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型之间的联系,重写了原型对象。需要注意的是:实例中的指针仅指向原型而非构造函数。例子如下:

function Person(){}
var friend = new Person();       
Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};
friend.sayName();   //error
原型链重写前后
原型链重写前后
当然,还有原生对象的原型,原生模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型都在其构造函数的原型上定义了方法。下面两个例子:
alert( typeof Array.prototype.sort);  //"function"
alert( typeof String.prototype.substring);  //"function"

讲到这里再说一下typeof与instenceof的区别:typeof操作符是确定一个变量是字符串,数值,布尔值还是undefine或者是object的最佳工具;instanceof操作符是确定一个对象是什么类型的对象的工具。 同时可以组合使用构造函数与原型模式。这组合了两者的长处,同时这种混成模式还支持想构造函数传递参数。下面代码用于解释这种混成模式:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}
Person.prototype = {
    constructor: Person,
    sayName : function () {
        alert(this.name);
    }
};
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends);    //"Shelby,Court,Van"
alert(person2.friends);    //"Shelby,Court"
alert(person1.friends === person2.friends);  //false
alert(person1.sayName === person2.sayName);  //true

动态原型模式,它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型,又保持了同时使用构造函数和原型的优点。还有寄生构造函数模式稳妥构造函数模式,就不一一列举解释了。

2、继承

继承是面向对象语言中一个最为人津津乐道的概念,而许多的面向对象语言都支持两种继承方式:接口继承和实现继承。而接口继承只继承方法的签名;实现继承才继承实际的方法,ECMAScript中将描述的原型链作为实现集成的主要方法。实现原型链有一种基本的方法,其代码致如下:

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

//inherit from SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function (){
    return this.subproperty;
};

var instance = new SubType();
alert(instance.getSuperValue()); 

以下是这个例子及构造函数源性之间的关系


原型链实现
原型链实现

有一点特别需要注意的就是:在通过原型链继承时,不能使用对象字面量创建原型方法,因为这样做会重写原型链,下面为例子:

function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function SubType(){            
}

//inherit from SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);    //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors);    //"red,blue,green,black"

借用构造函数,在子类兴的构造函数内部调用超类型构造函数。还有组合继承,它还有个名字伪经典继承,指的是原型链和借用构造函数的技术组合到一块,从而发挥二者职场的一种集成模式,再上例子

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function(){
    alert(this.name);
};

function SubType(name, age){  
    SuperType.call(this, name);
    
    this.age = age;
}

SubType.prototype = new SuperType();

SubType.prototype.sayAge = function(){
    alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors);  //"red,blue,green,black"
instance1.sayName();      //"Nicholas";
instance1.sayAge();       //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors);  //"red,blue,green"
instance2.sayName();      //"Greg";
instance2.sayAge();       //27

原生是继承偶那个的不多就不列举了。
寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,在函数在内部以某种方式来增强对象,最后再想真正地是它做了所有工作一样返回对象。
寄生组合式继承,组合式继承是JavaScript最常用的继承模式,而现在的寄生组合式继承有个缺陷,即无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function(){
    alert(this.name);
};

function SubType(name, age){  
    SuperType.call(this, name);
    
    this.age = age;
}
SuperType.prototype = new SuperType();

JavaScript主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样子类就能够访问超类的所有属性和方法,这一点和类的继承很相似,原型链的问题是对象实例共享所有继承的属性和方法,因此不适合单独使用。 解决这个问题的技术是借用构造函数,在子类型的内部调用超类的构造函数。 这样就能做到每个实例都有自己的属性,同时还能保证只是用构造函数模式来定义类型。 使用最多的继承模式是组合继承,这种模式使用原型链来继承共享的属性和方法,通过借用构造函数继承实例属性。

2、多态

“多态”一词源于希腊文ploymorphism,拆开来看是ploy(复数)+morph(形态)+ism,从字面上我们可以理解为复数形态。
堕胎的实际含义是同意操作作用于不同对象上面,可以产生不同的解释和不同的运行结果。换句话说,,给不同对象发送统一消息的时候,这些对象会根据这个信息分别给出不同的反馈。
上一段非多态和多态代码:

var makeSound = function(animal) {  //非多态代码示例
    if(animal instanceof Duck) {
        console.log('嘎嘎嘎');
    } else if (animal instanceof Chicken) {
        console.log('咯咯咯');
    }
}
var Duck = function(){}
var Chiken = function() {};
makeSound(new Chicken());
makeSound(new Duck());

var makeSound = function(animal) {  //多态的代码示例
    animal.sound();
}
var Duck = function(){}
Duck.prototype.sound = function() {
    console.log('嘎嘎嘎')
}
var Chiken = function() {};
Chiken.prototype.sound = function() {
    console.log('咯咯咯')
}
makeSound(new Chicken());
makeSound(new Duck());

JS的函数重载是多态的基础,同时函数又是对象,就可以延伸为对象的多态性,上面对象的多态性把不变的部分隔离开来,那就是所有的动物都会发出叫声。
还有使用继承得到多态性,这是让对象表现多态性最常用手段,而实现继承和接口继承是本质,前面已经介绍过实现继承和接口继承,这里就不细说了。
总的来说,JavaScript的多态的思想实际上就是把“做什么”和“谁去做”分离开来,要实现这一点,归根结底就是解耦。

3、封装

封装主要实现的功能就是将数据隐藏
最通俗最常见的封装数据,这是由最简单的语法解析来实现,这就是我们通常所认知的,但这所谓的把封装等同于封装数据这是一种非常狭隘的认知。封装还有封装类型的不同,封装的变化等,文字解说再多可能理解的还是不够,上一段代码帮助理解,下面给出一段封装的代码实例:

function Dog(varieties,age) {  //函数封装,解决代码的重复
  return {
    varieties: varieties,
    age: age
  }
}
var d1 = Cat("Husky",2);  //然后生成实例对象,就等于是在调用函数
var d2 = Cat("Alaska",3);

同时还有构造函数的实现,上面对象属性,创建对象也有解释,不过还是上段代码帮助理解吧

function Dog(varieties,age){
  this.varieties = varieties;
  this.age = age;
  this.type = "猫科动物";
  this.eat = function(){
    alert("吃骨头");
  };
}
var d1 = new Cat("Husky",2);
var d2 = new Cat ("Alaska",3);
alert(d1.type); // 猫科动物
d1.eat(); // 吃骨头

以上就是个人对JS面向对象程序设计的理解。

评论
说说你的看法