面向对象(OOP)
一、什么是面向对象?
1.概念
- 面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
- 万事万物皆对象。面向对象的思想主要是以对象为主,将一个问题抽象出具体的对象,并且将抽象出来的对象和对象的属性和方法封装成一个类。
在 OOP 中,每个对象能够接收消息,处理数据和发送消息给其他对象。每个对象都可以被看作是一个拥有清晰角色或责任的独立小机器。
2.面向对象和面向过程的区别?
我们常提到的两种编程方式就是面向过程和面向对象,面向过程的有C,面向对象的有java、C#、C++、JavaScript等。
面向对象和面向过程是两种不同的编程思想。举个例子形象说明一下:
在面向过程的编程方式中实现“把大象放冰箱”这个问题答案是耳熟能详的,一共分三步:
- 开门(冰箱);
- 装进(冰箱,大象);
- 关门(冰箱)。
而面向对象中呢?
- 冰箱.开门()
- 冰箱.装进(大象)
- 冰箱.关门()
总结:
- 面向过程的特点在于逻辑性强,符合思维方式和解决问题的流程。
- 面向对象的特点就是可扩展性更强一些,解决了代码重用性的问题。
二、为什么要使用面向对象编程?
我们知道,js中对象,对象下的方法和属性,都是储存在内存中的,调用的时候,是可以随时拿到这些属性和方法的。
在编程中使用OOP就是为了提高代码的复用性,而提高代码复用性的根本原因是为了降低内存的使用率。
举个简单的例子:
在同一个家里,只要有一把螺丝刀就可以了,大家都可以使用,而不是每人都配一把螺丝刀,这样没有必要,而且浪费家里的空间。在这个例子中,家就是内存,而每个人就是一个变量,螺丝刀是一个方法,如果很多人想用螺丝刀,那么就创建一个类,由这个类创建出多个实例,所有的实例都共用一个螺丝刀,而不是每个人一把螺丝刀,这样太浪费了。
面向对象程序设计的目的是在编程中促进更好的灵活性和可维护性,在大型软件工程中广为流行。即代码各部分相对独立,耦合性低,且功能明确,遇到bug或者更改需求,都可以直接针对特定的对象进行修改,便于维护。
另外,面向对象凭借其对模块化的重视,面向对象的代码开发更简单,更容易理解,相比非模块化编程方法 , 它能更直接地分析, 编码和理解复杂的情况和过程。
三、面向对象相关术语
术语
- Namespace 命名空间
允许开发人员在一个独特, 应用相关的名字的名称下捆绑所有功能的容器。 - Class 类
定义对象的特征。它是对象的属性和方法的模板定义. - Object 对象
类的一个实例。 - Property 属性
对象的特征,比如颜色。 - Method 方法
对象的能力,比如行走。 - Constructor 构造函数
对象初始化的瞬间, 被调用的方法. 通常它的名字与包含它的类一致. - Inheritance 继承
一个类可以继承另一个类的特征。 - Encapsulation 封装
一种把数据和相关的方法绑定在一起使用的方法. - Abstraction 抽象
结合复杂的继承,方法,属性的对象能够模拟现实的模型。 - Polymorphism 多态
多意为‘许多’,态意为‘形态’。不同类可以定义相同的方法或属性。
四、实现面向对象编程
面向对象有三大特性,封装、继承和多态
封装
封装主要实现的功能就是将数据隐藏,只暴露出有限的接口。
在js中万物皆对象,字符串、数值、数组、函数都属于Object。因此js基本的创建对象的方法有两种:
- 对象字面量法
- new对象
//对象字面量
var person = {
name:"chen",
age:22,
sayname:function (){
alert(this.name);
}
}
//new对象
var person = new Obejct();
person.name = "chen";
person.age = 22;
person.sayname = function (){
alert(this.name);
}
但这两种方法都会产生大量重复代码,基于面向对象思想,我们使用新的方式创建对象。
1. 工厂模式
工厂模式如同它的名字一般,将对象从原料加工、制作、最后出厂,可实现大批量的功能相似产品对象!
function createPerson(name){
//1、原料
var obj=new Object();
//2、加工
obj.name=name;
obj.showName=function(){
alert(this.name);
}
//3、出场
return obj;
}
var p1=createPerson('小米');
var p1=createPerson('小红');
p1.showName();//小米
p2.showName();//小红
工厂模式的优缺点:虽然解决了创建相似对象的问题,但是却没有解决对象识别问题(即怎样知道一个对象的类型)。
2.构造函数模式
构造函数其实就是普通的函数,只不过有以下的特点:
- 首字母大写(建议构造函数首字母大写,即使用大驼峰命名,非构造函数首字母小写)
- 内部使用this
- 使用 new生成实例
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);
其中new操作符内部会经以下四个步骤:
1、创建一个新对象;
2、将构造函数的作用域赋给新对象(因此这个this就指向这个新对象);
3、执行构造函数中的代码(为这个新对象添加属性);
4、返回新对象。
构造函数模式的优缺点:
1、优点:创建自定义函数意味着将来可以将它的实例标识为一种特定的类型,这是构造函数胜过工厂模式的地方
2、缺点:每个方法都要在每个实例上重新创建一遍
3.原型(类)模式
辨析prtotype
、_proto_
、constructor
◆每一个构造函数,都有一个原型[[prototype]]属性 指向构造函数的原型对象
◆每一实例化对象都有一个隐式原型_proto_ 指向构造函数的原型对象
◆每一个原型对象都有一个默认的constructor属性,指向对象的构造函数
解释:
- 每个函数都有一个 prototype 属性,记住,只有函数才有,对象没有,而每个对象都会拥有一个 proto 属性(后面会对 proto 属性详细讲解)。因为在 JS 中,函数也是对象,所以,函数也会拥有一个 proto 属性。也就是说函数会同时拥有 prototype 和 proto 属性,而对象只有 proot 属性。
- 此外,每个由构造函数生成的实例都会拥有的是一个 [[prototype]]的属性,但是规范中并没有明确定义这样一个默认的属性,所以实际上是无法获取这个属性的,不过 FireFox, Safari, Chrome 都会支持通过 proto 来获取这个属性,所以才会有 proto 这样一个属性。也就是说 [[prototype]] == _ proto_ 。
- 构造器作为一个普通函数,如上面所说,会拥有一个 prototype 的属性。这个 prototype 属性是个对象,这个对象就是原型对象(下文说原型对象都指的是这个对象)。原型对象会拥有很多属性和方法,而一旦使用 new 操作符调用构造器函数的时候,所有的实例就会拥有构造器函数的 prototype 属性上的所有属性和方法,也就是原型对象上的所有属性和方法,这样就实现了原型继承。
<script type="text/javascript">
// 1. 创建一个构造器
function Person() {}
// 2. 创建一个构造器原型
Person.prototype.name = 'jizq';
Person.prototype.age = 28;
Person.prototype.sayName = function() {
console.log(this.name)
};
// 3. 由构造器创建两个实例
var p1 = new Person(),
p2 = new Person();
p1.sayName(); // jizq
p2.sayName(); // jizq
// 4. 查看这两个实例之间的关系
console.log(p1.sayName == p2.sayName); // true
// Problems with prototypes
// 当一个属性包含的是一个引用值的时候,使用 prototype 会出现问题。例如:
Person.prototype.friends = ['Shelby', 'Court'];
p1.friends.push('Van');
console.log(p2.friends); // ['Shelby', 'Court', 'Van']
// 因为 friends 这个属性并不是存在在 p1 上,而是存在于 Person.prototype 上面。通常,实例需要拥有自己的属性。
</script>
p1.sayName 和 p2.sayName 是同一个 sayName。当通过 Person.prototype 来修改 sayName 属性的时候,所有的实例都会受到影响,因为所有的实例都是指向这个方法的。
下图为网上最经典的原型链的图解:
原型模式的优缺点:
1、优点:可以让所有的对象实例共享它所包含的属性和方法
2、缺点:原型中是所有属性都是共享的,但是实例一般都是要有自己的单独属性的。所以一般很少单独使用原型模式。
4.混合模式(常用作封装类)
通过构造函数模式定义实例(私有)属性,而原型模式用于定义方法和共享的属性
function Person (name,age){//定义私有属性
this.name = name;
this.age = age;
}
Person.prototype.sayname = function (){//定义公有属性
console.log(this.name)
}
var person1 = new Person("chen",22);
var person2 = new Person("qian",21);
console.log(person1.age);//22
console.log(person2.age);//21
person1.sayname()//chen
person2.sayname()//qian
console.log(person1.sayname===person2.sayname);//true
person1.age = 26;
//改变person1的属性和方法
person1.sayname = function (){console.log("change")};
console.log(person1.age);//26
console.log(person2.age);//21 person2不受影响
person1.sayname()//change
person2.sayname()//qian person2访问原型的方法,不受影响
继承
1.类式继承(原型继承)
所谓的类式继承就是使用的原型的方式,将方法添加在父类的原型上,然后子类的原型是父类的一个实例化对象。
function Person(name){
this.name=name;
this.className="person"
}
Person.prototype.getClassName=function(){
console.log(this.className)
}
function Man(){
}
Man.prototype=new Person();//1
//Man.prototype=new Person("Davin");//2
var man=new Man;
>man.getClassName()
>"person"
>man instanceof Person
>true
其中最核心的一句代码是Man.prototype=new Person() ;
这种继承方式下,所有的子类实例会共享一个父类对象的实例,这种方案最大问题就是子类无法通过父类创建私有属性。比如每一个Person都有一个名字,我们在初始化每个Man的时候要指定一个不同名字,然后子类将这个名字传递给父类,对于每个man来说,保存在相应person中的name应该是不同的,但是这种方式根本做不到。所以,这种继承方式,实战中基本不用!
2. 构造函数继承
function Person(name){
this.name=name;
this.className="person"
}
Person.prototype.getName=function(){
console.log(this.name)
}
function Man(name){
Person.apply(this,arguments)
}
var man1=new Man("Davin");
var man2=new Man("Jack");
>man1.name
>"Davin"
>man2.name
>"Jack"
>man1.getName() //1 报错
>man1 instanceof Person
>true
Person.apply(this,arguments)
:子类的在构造函数里用子类实例的this去调用父类的构造函数,从而达到继承父类属性的效果。
这样一来,每new一个子类的实例,构造函数执行完后,都会有自己的一份资源(name),不过会造成 内存浪费。但是这种办法只能继承父类构造函数中声明的实例属性,并没有继承父类原型的属性和方法,所以就找不到getName方法,所以1处会报错。为了同时继承父类原型,从而诞生了组合继承的方式:
3.组合继承
function Person(name){
this.name=name||"default name"; //1
this.className="person"
}
Person.prototype.getName=function(){
console.log(this.name)
}
function Man(name){
Person.apply(this,arguments)
}
//继承原型
Man.prototype = new Person();
var man1=new Man("Davin");
> man1.name
>"Davin"
> man1.getName()
>"Davin"
组合式继承就是汲取两者的优点,即避免了内存浪费,又使得每个实例化的子类互不影响。
4.寄生组合继承
组合式继承的方法固然好,但是会导致一个问题,父类的构造函数会被创建两次(call(),apply()的时候一遍,new的时候又一遍),所以为了解决这个问题,又出现了寄生组合继承。
也就是说,我们只需要先给父类的原型创建一个副本,然后修改子类constructor属性,最后在设置子类的原型就可以了!
function Person(name){
this.name=name; //1
this.className="person"
}
Person.prototype.getName=function(){
console.log(this.name)
}
function Man(name){
Person.apply(this,arguments)
}
//注意此处
Man.prototype = Object.create(Person.prototype);
var man1=new Man("Davin");
> man1.name
>"Davin"
> man1.getName()
>"Davin"
这里用到了Object.creat(obj)
方法,该方法会对传入的obj对象进行浅拷贝。和上面组合继承的主要区别就是:将父类的原型复制给了子类原型。这种做法很清晰:
- 构造函数中继承父类属性/方法,并初始化父类。
- 子类原型和父类原型建立联系。
另外我们发现,Person和Man实例的constructor指向都是Person,当然,这并不会改变instanceof的结果,但是对于需要用到construcor的场景,就会有问题。所以一般我们会加上这么一句:
Man.prototype.constructor = Man
综合来看,es5下,这种方式是首选,也是实际上最流行的。
5. ES6 中的 class继承
es6引入了class、extends、super、static
class Person{
//static sCount=0 //1
constructor(name){
this.name=name;
this.sCount++;
}
//实例方法 //2
getName(){
console.log(this.name)
}
static sTest(){
console.log("static method test")
}
}
class Man extends Person{
constructor(name){
super(name)//3
this.sex="male"
}
}
var man=new Man("Davin")
man.getName()
//man.sTest()
Man.sTest()//4
输出结果:
Davin
static method test
最后再说一下 ES6 中 class 实现原型继承。ES6 中 class 其实是原型继承的语法糖而已!
es6继承的不足:
- 不支持静态属性(除函数)。
- class中不能定义私有变量和函数。class中定义的所有函数都会被放倒原型当中,都会被子类继承,而属性都会作为实例属性挂到this上。如果子类想定义一个私有的方法或定义一个private 变量,便不能直接在class花括号内定义,这真的很不方便!
多态
多态(Polymorphism)按字面的意思就是“多种状态”,在面向对象语言中,接口的多种不同的实现方式即为多态。
可以解释为:同样的操作作用于不同对象上面,可以产生不同的解释和不同的运行结果。换句话说,,给不同对象发送统一消息的时候,这些对象会根据这个信息分别给出不同的反馈。
下面看一个例子:
function Person(name,age){
this.name=name
this.age=age
}
Person.prototype.toString=function(){
return "I am a Person, my name is "+ this.name
}
function Man(name,age){
Person.apply(this,arguments)
}
Man.prototype = Object.create(Person.prototype);
Man.prototype.toString=function(){
return "I am a Man, my name is"+this.name;
}
var person=new Person("Neo",19)
var man1=new Man("Davin",18)
var man2=new Man("Jack",19)
> person+""
> "I am a Person, my name is Neo"
> man1+""
> "I am a Man, my name isDavin"
> man1 false
总的来说,JavaScript的多态的思想实际上就是把“做什么”和“谁去做”分离开来,要实现这一点,归根结底就是解耦。
五、OOP 的典型案例
实现一个js插件
见另一篇文章:
参考文章:
MDN面向对象
JS中面向对象
阮一峰js面向对象
github 理解面向对象
面向对象编程(最系统)
JavaScript继承与多态