专业编程基础技术教程

网站首页 > 基础教程 正文

JavaScript: 面向对象编程,带你认识JS的封装、继承和多态

ccvgpt 2024-07-23 01:38:42 基础教程 25 ℃

首先,什么是面向对象编程 ?

什么是面向对象呢,用java中的一句经典语句来说就是:万事万物皆对象。面向对象的思想主要是以对象为主,将一个问题抽象出具体的对象,并且将抽象出来的对象和对象的属性和方法封装成一个类。

面向对象和面向过程的区别:

JavaScript: 面向对象编程,带你认识JS的封装、继承和多态

面向对象和面向过程是两种不同的编程思想,我们经常会听到两者的比较,刚开始编程的时候,大部分应该都是使用的面向过程的编程,但是随着我们的成长,还是面向对象的编程思想比较好一点~

其实面向对象和面向过程并不是完全相对的,也并不是完全独立的。

我认为面向对象和面向过程的主要区别是面向过程主要是以动词为主,解决问题的方式是按照顺序一步一步调用不同的函数。

而面向对象主要是以名词为主,将问题抽象出具体的对象,而这个对象有自己的属性和方法,在解决问题的时候是将不同的对象组合在一起使用。

所以说面向对象的好处就是可扩展性更强一些,解决了代码重用性的问题。

  • 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
  • 面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

有一个知乎的高票回答很有意思,给大家分享一下~

  • 面向对象: 狗.吃(屎)
  • 面向过程: 吃.(狗,屎)

具体的实现我们看一下最经典的“把大象放冰箱”这个问题:

在面向过程的编程方式中实现“把大象放冰箱”这个问题答案是耳熟能详的,一共分三步:

  1. 面向过程的解决方法
  2. 开门(冰箱);
  3. 装进(冰箱,大象);
  4. 关门(冰箱)。

2. 面向对象的解决方法

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

可以看出来面向对象和面向过程的侧重点是不同的,面向过程是以动词为主,完成一个事件就是将不同的动作函数按顺序调用。

面向对象是以主谓为主。将主谓看成一个一个的对象,然后对象有自己的属性和方法。比如说,冰箱有自己的id属性,有开门的方法。然后就可以直接调用冰箱的开门方法给其传入一个参数大象就可以了。

封装

面向对象有三大特性,封装、继承和多态。对于ES5来说,没有class的概念,并且由于js的函数级作用域(在函数内部的变量在函数外访问不到),所以我们就可以模拟 class的概念,在es5中,类其实就是保存了一个函数的变量,这个函数有自己的属性和方法。将属性和方法组成一个类的过程就是封装。

封装:把客观事物封装成抽象的类,隐藏属性和方法的实现细节,仅对外公开接口。

通过构造函数添加

javascript提供了一个构造函数(Constructor)模式,用来在创建对象时初始化对象。

构造函数其实就是普通的函数,只不过有以下的特点

  • 首字母大写(建议构造函数首字母大写,即使用大驼峰命名,非构造函数首字母小写)
  • 内部使用this
  • 使用 new生成实例

通过构造函数添加属性和方法实际上也就是通过this添加的属性和方法。因为this总是指向当前对象的,所以通过this添加的属性和方法只在当前对象上添加,是该对象自身拥有的。所以我们实例化一个新对象的时候,this指向的属性和方法都会得到相应的创建,也就是会在内存中复制一份,这样就造成了内存的浪费。

function Cat(name,color){
 this.name = name;
 this.color = color;
 this.eat = function () {
 alert('吃老鼠')
 }
 }
var cat1 = new Cat('tom','red')//生成实例 通过this定义的属性和方法,我们实例化对象的时候都会重新复制一份

通过原型prototype

在类上通过 this的方式添加属性和对象会导致内存浪费的问题,我们就考虑,有什么方法可以让实例化的类所使用的方法直接使用指针指向同一个方法。于是,就想到了原型的方式

Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。也就是说,对于那些不变的属性和方法,我们可以直接将其添加在类的prototype 对象上。

 function Cat(name,color){
    this.name = name;
    this.color = color;
  }
  Cat.prototype.type = "猫科动物";
  Cat.prototype.eat = function(){alert("吃老鼠")};
 var cat1 = new Cat("大毛","黄色");
  var cat2 = new Cat("二毛","黑色");
  alert(cat1.type); // 猫科动物
  cat1.eat(); // 吃老鼠
// 这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。

在类的外部通过.语法添加

我们还可以在类的外部通过. 语法进行添加,因为在实例化对象的时候,并不会执行到在类外部通过. 语法添加的属性,所以实例化之后的对象是不能访问到. 语法所添加的对象和属性的,只能通过该类访问。

三者的区别

通过构造函数、原型和. 语法三者都可以在类上添加属性和方法。但是三者是有一定的区别的。

构造函数:通过this添加的属性和方法总是指向当前对象的,所以在实例化的时候,通过this添加的属性和方法都会在内存中复制一份,这样就会造成内存的浪费。但是这样创建的好处是即使改变了某一个对象的属性或方法,不会影响其他的对象(因为每一个对象都是复制的一份)。

原型:通过原型继承的方法并不是自身的,我们要在原型链上一层一层的查找,这样创建的好处是只在内存中创建一次,实例化的对象都会指向这个prototype 对象,但是这样做也有弊端,因为实例化的对象的原型都是指向同一内存地址,改动其中的一个对象的属性可能会影响到其他的对象

. 语法:在类的外部通过. 语法创建的属性和方法只会创建一次,但是这样创建的实例化的对象是访问不到的,只能通过类的自身访问

new的实质

虽然很多人都已经了解了new的实质,但是还是要再说一下new 的实质

var o = new Object()

  1. 新建一个对象o
  2. o. __proto__ = Object.prototype 将新创建的对象的__proto__属性指向构造函数的prototype
  3. 将this指向新创建的对象
  4. 返回新对象,但是这里需要看构造函数有没有返回值,如果构造函数的返回值为基本数据类型string,boolean,number,null,undefined,那么就返回新对象,如果构造函数的返回值为对象类型,那么就返回这个对象类型

继承

继承:子类可以使用父类的所有功能,并且对这些功能进行扩展。继承的过程,就是从一般到特殊的过程。

其实继承都是基于以上封装方法的三个特性来实现的。

类式继承:所谓的类式继承就是使用的原型的方式,将方法添加在父类的原型上,然后子类的原型是父类的一个实例化对象。

//声明父类
 var SuperClass = function () {
 var id = 1;
 this.name = ['javascript'];
 this.superValue = function () {
 console.log('superValue is true');
 console.log(id)
 }
 };
 //为父类添加共有方法
 SuperClass.prototype.getSuperValue = function () {
 return this.superValue();
 };
 //声明子类
 var SubClass = function () {
 this.subValue = function () {
 console.log('this is subValue ')
 }
 };
 //继承父类
 SubClass.prototype = new SuperClass() ;
 //为子类添加共有方法
 SubClass.prototype.getSubValue= function () {
 return this.subValue()
 };
 var sub = new SubClass();
 var sub2 = new SubClass();
 sub.getSuperValue(); //superValue is true
 sub.getSubValue(); //this is subValue
 console.log(sub.id); //undefined
 console.log(sub.name); //javascript
 sub.name.push('java'); //["javascript"]
 console.log(sub2.name) //["javascript", "java"]

其中最核心的一句代码是SubClass.prototype = new SuperClass() ;

类的原型对象prototype对象的作用就是为类的原型添加共有方法的,但是类不能直接访问这些方法,只有将类实例化之后,新创建的对象复制了父类构造函数中的属性和方法,并将原型__proto__ 指向了父类的原型对象。这样子类就可以访问父类的public 和protected 的属性和方法,同时,父类中的private 的属性和方法不会被子类继承。

构造函数继承:构造函数继承的核心思想就是SuperClass.call(this,id),直接改变this的指向,使通过this创建的属性和方法在子类中复制一份,因为是单独复制的,所以各个实例化的子类互不影响。但是会造成内存浪费的问题

//构造函数继承
 //声明父类
 function SuperClass(id) {
 var name = 'javascript'
 this.books=['javascript','html','css'];
 this.id = id
 }
 //声明父类原型方法
 SuperClass.prototype.showBooks = function () {
 console.log(this.books)
 }
 //声明子类
 function SubClass(id) {
 SuperClass.call(this,id)
 }
 //创建第一个子类实例
 var subclass1 = new SubClass(10);
 var subclass2 = new SubClass(11);
 console.log(subclass1.books);
 console.log(subclass2.id);
 console.log(subclass1.name); //undefined
 subclass2.showBooks();

组合式继承:

我们先来总结一下类继承和构造函数继承的优缺点:

类继承 / 构造函数继承

核心思想 :子类的原型是父类实例化的对象 / SuperClass.call(this,id)

优点 :子类实例化对象的属性和方法都指向父类的原型 / 每个实例化的子类互不影响

缺点 :子类之间可能会互相影响 / 内存浪费

所以组合式继承就是汲取两者的优点,即避免了内存浪费,又使得每个实例化的子类互不影响。

//组合式继承
 //声明父类
 var SuperClass = function (name) {
 this.name = name;
 this.books=['javascript','html','css']
 };
 //声明父类原型上的方法
 SuperClass.prototype.showBooks = function () {
 console.log(this.books)
 };
 //声明子类
 var SubClass = function (name) {
 SuperClass.call(this, name)
 };
 //子类继承父类(链式继承)
 SubClass.prototype = new SuperClass();
 //实例化子类
 var subclass1 = new SubClass('java');
 var subclass2 = new SubClass('php');
 subclass2.showBooks();
 subclass1.books.push('ios'); //["javascript", "html", "css"]
 console.log(subclass1.books); //["javascript", "html", "css", "ios"]
 console.log(subclass2.books); //["javascript", "html", "css"]

寄生组合继承:那么问题又来了~组合式继承的方法固然好,但是会导致一个问题,父类的构造函数会被创建两次(call()的时候一遍,new的时候又一遍),所以为了解决这个问题,又出现了寄生组合继承。

刚刚问题的关键是父类的构造函数在类继承和构造函数继承的组合形式中被创建了两遍,但是在类继承中我们并不需要创建父类的构造函数,我们只是要子类继承父类的原型即可。所以说我们先给父类的原型创建一个副本,然后修改子类constructor属性,最后在设置子类的原型就可以了~

//原型式继承
 //原型式继承其实就是类式继承的封装,实现的功能是返回一个实例,改实例的原型继承了传入的o对象
 function inheritObject(o) {
 //声明一个过渡函数对象
 function F() {}
 //过渡对象的原型继承父对象
 F.prototype = o;
 //返回一个过渡对象的实例,该实例的原型继承了父对象
 return new F();
 }
 //寄生式继承
 //寄生式继承就是对原型继承的第二次封装,使得子类的原型等于父类的原型。并且在第二次封装的过程中对继承的对象进行了扩展
 function inheritPrototype(subClass, superClass){
 //复制一份父类的原型保存在变量中,使得p的原型等于父类的原型
 var p = inheritObject(superClass.prototype);
 //修正因为重写子类原型导致子类constructor属性被修改
 p.constructor = subClass;
 //设置子类的原型
 subClass.prototype = p;
 }
 //定义父类
 var SuperClass = function (name) {
 this.name = name;
 this.books = ['javascript','html','css']
 };
 //定义父类原型方法
 SuperClass.prototype.getBooks = function () {
 console.log(this.books)
 };
 //定义子类
 var SubClass = function (name) {
 SuperClass.call(this,name)
 }
 inheritPrototype(SubClass,SuperClass);
 var subclass1 = new SubClass('php')

Tags:

最近发表
标签列表