js的面向对象(oop)

面向对象(OOP)

一、什么是面向对象?

1.概念

  1. 面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
  2. 万事万物皆对象。面向对象的思想主要是以对象为主,将一个问题抽象出具体的对象,并且将抽象出来的对象和对象的属性和方法封装成一个类。
在 OOP 中,每个对象能够接收消息,处理数据和发送消息给其他对象。每个对象都可以被看作是一个拥有清晰角色或责任的独立小机器。

2.面向对象和面向过程的区别?

我们常提到的两种编程方式就是面向过程和面向对象,面向过程的有C,面向对象的有java、C#、C++、JavaScript等。
面向对象和面向过程是两种不同的编程思想。举个例子形象说明一下:
在面向过程的编程方式中实现“把大象放冰箱”这个问题答案是耳熟能详的,一共分三步:

  1. 开门(冰箱);
  2. 装进(冰箱,大象);
  3. 关门(冰箱)。

而面向对象中呢?

  1. 冰箱.开门()
  2. 冰箱.装进(大象)
  3. 冰箱.关门()

总结:

  • 面向过程的特点在于逻辑性强,符合思维方式和解决问题的流程。
  • 面向对象的特点就是可扩展性更强一些,解决了代码重用性的问题。

二、为什么要使用面向对象编程?

我们知道,js中对象,对象下的方法和属性,都是储存在内存中的,调用的时候,是可以随时拿到这些属性和方法的。

在编程中使用OOP就是为了提高代码的复用性,而提高代码复用性的根本原因是为了降低内存的使用率。

举个简单的例子:

在同一个家里,只要有一把螺丝刀就可以了,大家都可以使用,而不是每人都配一把螺丝刀,这样没有必要,而且浪费家里的空间。在这个例子中,家就是内存,而每个人就是一个变量,螺丝刀是一个方法,如果很多人想用螺丝刀,那么就创建一个,由这个类创建出多个实例,所有的实例都共用一个螺丝刀,而不是每个人一把螺丝刀,这样太浪费了。

面向对象程序设计的目的是在编程中促进更好的灵活性和可维护性,在大型软件工程中广为流行。即代码各部分相对独立,耦合性低,且功能明确,遇到bug或者更改需求,都可以直接针对特定的对象进行修改,便于维护。
另外,面向对象凭借其对模块化的重视,面向对象的代码开发更简单,更容易理解,相比非模块化编程方法 , 它能更直接地分析, 编码和理解复杂的情况和过程。

三、面向对象相关术语

术语

  1. Namespace 命名空间
    允许开发人员在一个独特, 应用相关的名字的名称下捆绑所有功能的容器。
  2. Class 类
    定义对象的特征。它是对象的属性和方法的模板定义.
  3. Object 对象
    类的一个实例。
  4. Property 属性
    对象的特征,比如颜色。
  5. Method 方法
    对象的能力,比如行走。
  6. Constructor 构造函数
    对象初始化的瞬间, 被调用的方法. 通常它的名字与包含它的类一致.
  7. Inheritance 继承
    一个类可以继承另一个类的特征。
  8. Encapsulation 封装
    一种把数据和相关的方法绑定在一起使用的方法.
  9. Abstraction 抽象
    结合复杂的继承,方法,属性的对象能够模拟现实的模型。
  10. 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继承与多态

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值