摘要:会造成内存浪费的问题构造函数继承声明父类声明子类生成实例组合式继承组合式继承是汲取了两者的优点,既避免了内存浪费,又使得每个实例化的子类互不影响。
写在前面
既然是浅谈,就不会从原理上深度分析,只是帮助我们更好地理解...
面向对象与面向过程面向对象和面向过程是两种不同的编程思想,刚开始接触编程的时候,我们大都是从面向过程起步的,毕竟像我一样,大家接触的第一门计算机语言大概率都是C语言,C语言就是一门典型的面向过程的计算机语言。
面向过程主要是以动词为主,解决问题的方式是按照顺序一步一步调用不同的函数。
面向对象是以名词为主,将问题抽象出具体的对象,而这个对象有自己的属性和方法,在解决问题的时候,是将不同的对象组合在一起使用。
//面向过程装大象 1.开(冰箱) 2.(大象)装进(冰箱) 3.关(冰箱)
//面向对象装大象 1. 冰箱.开门() 2. 冰箱.装进(大象) 3. 冰箱.关门()
从这个例子可以看出,面向对象是以主谓为主,将主谓堪称一个一个的对象,然后对象有自己的属性和方法。
面向对象是以功能来划分问题的,而不是步骤。功能上的统一保证了面向对象设计的可扩展性,解决了代码重用性的问题。
这也是在漫长的程序设计的发展过程中得到的验证结果,面向对象的编程思想较之于面向过程较好一点
面向对象有封装、继承和多态三大特性。
封装:就是把事物封装成类,隐藏事物的属性和方法的实现细节,仅对外公开接口。
在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 = (() => { console.log("fish!") }) } //生成实例 var cat1 = new Cat("tom", "gray")
通过this定义的属性和方法,我们实例化对象的时候斗湖重新复制一份
在类上通过this的方式添加属性和方法会导致内存浪费的现象,有什么办法可以让实例化的类所使用的属性和方法 直接使用指针 指向同一个属性和方法。
这就是原型的方法
JavaScript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。 也就是说,对于那些不变的属性和方法,我们可以直接将其添加在类的prototype对象上。
function Cat(name,color){ this.name = name; this.color = color; } Cat.prototype.type = "英短"; Cat.prototype.eat = ( () => { alert("fish!") } ) //生成实例 var cat1 = new Cat("Tom", "gray"); var cat2 = new Cat("Kobe", "purple"); console.log(cat1.type); //英短 cat2.eat(); //fish!
这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。
但是这样做也有弊端,因为实例化的对象的原型都是指向同一内存地址,改动其中一个对象的属性可能会影响到其他的对象
es6声明一个类
①构造器:构造器内创建自有属性
②方法:声明类实例具有的方法
class Cat { //等价于Cat构造器 constructor(name) { this.name = name; } //更加简单的声明类的内部函数 //等价于 Cat.prototype.eat eat() { console.log("fish!"); } } //生成实例 var cat1 = new Cat("tom"); cat1.eat(); //fish! console.log(cat1 instanceof Cat); //true console.log(cat1 instanceof Object); //true console.log(typeof Cat); //function console.log(typeof Cat.prototype.eat); //function
从上面class声明的Cat为例:Cat类是一个具有构造函数行为的函数,其中内部方法eat实际上就是Cat.prototype.eat()
所以说es6的class封装类,本质上是es5实现方式的语法糖
最主要的区别在于,class类的属性是不可重新赋值和不可枚举的,Cat.prototype就是一个只读属性
class和自定义类型的区别
(1)class的声明不会提升,与let类似
(2)class的声明自动运行于严格模式之下
(3)class声明的方法不可枚举
(4)class的内部方法没有 constructor 属性,无法new
(5)调用class的构造函数必须new
(6)class内部方法不能同名
class类的使用
class作为js中的一级公民,可以被当作值来直接使用
//1.类名作为参数传入函数 function createObj (ClassName) { return new ClassName() } //2.立即执行,实现单例模式 let cat1 = new class{ constructor (name) { this.name = name } eat() { console.log("fish!") } }("tom”) cat1.eat() //fish!继承
继承就是子类可以使用父类的所有功能,并且对这些功能进行扩展。继承的过程,就是从一般到特殊的过程。
所谓的类式继承就是使用的原型的方式,将方法添加在父类的原型上,然后子类的原型是父类的一个实例化对象。
//声明父类 var SuperClass = function(){ let id = 1; this.name = ["java"]; this.superValue = function() { console.log("this is superValue!") } } //为父类添加共有方法 SuperClass.prototype.getSuperValue = function () { return this.superValue(); }; //声明子类 var SubClass = function() { this.subValue = (() => { console.log("this is subValue!") }) } //继承父类 SubClass.prototype = new SuperClass(); //为子类添加共有方法 SubClass.prototype.getSubValue = function() { return this.subValue() } //生成实例 var sub1 = new SubClass(); var sub2 = new SubClass(); sub1.getSuperValue(); //this is superValue! sub1.getSubValue(); //this is subValue! console.log(sub1.id); //undefined console.log(sub1.name); //["java"] sub1.name.push("php"); console.log(sub1.name); //["java", "php"] console.log(sub2.name); //["java", "php"]
其中最核心的是SubClass.prototype = new SuperClass();
类的原型对象prototype对象的作用就是为类的原型添加共有的方法的,但是类不能直接访问这些方法,只有将类实例化之后,新创建的对象复制了父类构造函数的属性和方法,并将原型 proto 指向了父类的原型对象。这样子类就可以访问父类的属性和方法,同时,父类中定义的属性和方法不会被子类继承。
but使用类继承的方法,如果父类的构造函数中有引用数据类型,就会在子类中被所有实例共用,因此一个子类的实例如果更改了这个引用数据类型,就会影响到其他子类的实例。
为了克服类继承的缺点,才有了构造函数继承,构造函数继承的核心思想就是SuperClass.call(this, id),直接改变this的指向,使通过this创建的属性和方法在子类中复制一份,因为是多带带复制的,所以各个实例化的子类互不影响。but会造成内存浪费的问题
//构造函数继承 //声明父类 var SuperClass = function(id){ var name = "java" this.languages = ["java", "php", "ruby"]; this.id = id } //声明子类 var SubClass = function(id){ SuperClass.call(this, id) } //生成实例 var sub1 = new SubClass(1); var sub2 = new SubClass(2); console.log(sub2.id); // 2 console.log(sub1.name); //undefined sub1.languages.push("python"); console.log(sub1.languages); // ["java", "php", "ruby", "python"] console.log(sub2.languages); // ["java", "php", "ruby"]
组合式继承是汲取了两者的优点,既避免了内存浪费,又使得每个实例化的子类互不影响。
//组合式继承 //声明父类 var SuperClass = function(name){ this.languages = ["java", "php", "ruby"]; this.name = name; } //声明父类原型方法 SuperClass.prototype.showLangs = function () { console.log(this.languages); } //声明子类 var SubClass = function(name){ SuperClass.call(this, name) } //子类继承父类(链式继承) SubClass.prototype = new SuperClass(); //生成实例 var sub1 = new SubClass("python"); var sub2 = new SubClass("go"); sub2.showLangs(); //["java", "php", "ruby"] sub1.languages.push(sub1.name); console.log(sub1.languages);//["java", "php", "ruby", "python"] console.log(sub2.languages);//["java", "php", "ruby"]
but警告:组合式继承方法固然好,但是会导致一个问题,父类的构造函数会被创建两次(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.languages = ["java", "php", "python"] } //定义父类原型方法 SuperClass.prototype.showLangs = function() { console.log(this.languages); } //定义子类 var SubClass = function(name) { SuperClass.call(this,name) } inheritPrototype(SubClass, SuperClass); var sub1 = new SubClass("go");
class SuperClass { constructor(name) { this.name = name this.languages = ["java", "php", "go"]; } showLangs() { console.log(this.languages); } } class SubClass extends SuperClass { constructor(name) { super(name) } //重写父类中的方法 showLangs() { this.languages.push(this.name) console.log(this.languages); } } //生成实例 var sub = new SubClass("韩二虎"); console.log(sub.name); //韩二虎 sub.showLangs(); //["java", "php", "go", "韩二虎"]多态
多态实际上是不同对象作用与同一操作产生不同的效果。多态的思想实际上是把 “想做什么” 和 “谁去做” 分开。
多态的好处在于,你不必再向对象询问“你是什么类型”后根据得到的答案再去调用对象的某个行为。你尽管去调用这个行为就是了,其他的一切可以由多态来负责。规范来说,多态最根本的作用就是通过吧过程化的条件语句转化为对象的多态性,从而消除这些条件分支语句。
由于JavaScript中提到的关于多态的详细介绍并不多,这里简单的通过一个例子来介绍就好
//非多态 var hobby = function(animal){ if(animal == "cat"){ cat.eat() }else if(animal == "dog"){ dog.eat() } } var cat = { eat: function() { alert("fish!") } } var dog = { eat: function() { alert("meat!") } } console.log(123); hobby("cat"); //fish! hobby("dog"); //meat!
从上面的例子能看到,虽然 hobby 函数目前保持了一定的弹性,但这种弹性很脆弱的,一旦需要替换或者增加成其他的animal,必须改动hobby函数,继续往里面堆砌条件分支语句。我们把程序中相同的部分抽象出来,那就是吃某个东西。然后再重新编程。
//多态 var hobby = function(animal){ if(animal.eat instanceof Function){ animal.eat(); } } var cat = { eat: function() { alert("fish!") } } var dog = { eat: function() { alert("meat!") } }
现在来看这段代码中的多态性。当我们向两种 animal 发出 eat 的消息时,会分别调用他们的 eat 方法,就会产生不同的执行结果。对象的多态性提示我们,“做什么” 和 “怎么去做”是可以分开的,这样代码的弹性就增强了很多。即使以后增加了其他的animal,hobby函数仍旧不会做任何改变。
//多态 var hobby = function(animal){ if(animal.eat instanceof Function){ animal.eat(); } } var cat = { eat: function() { alert("fish!") } } var dog = { eat: function() { alert("meat!") } } var aoteman = { eat: function(){ alert("lil-monster!") } } hobby(cat); //fish! hobby(dog); //meat! hobby(aoteman); //lil-monster!
快乐面向对象
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/99940.html
摘要:面向对象面向对象编程的全称是,简称,面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式。面向对象编程的三个主要特征是封装继承多态。 面向对象 面向对象编程的全称是Object Oriented Programming,简称OOP,面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式。面向对象编程可以看做是使用一系列对象相互协作的软件设计,面向对象程序设计的目的是在编程中促...
摘要:单一职责原则可以看做是低耦合高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。抽象的稳定性决定了系统的稳定性,因为抽象是不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心。 Java-面向对象 什么是面过程 把题分解成一个一个步骤,每个步骤用函数实现,依次调用即可。就是说,在进行面向过程 编程的时候,不需要考虑那么多,上来先定义一个...
摘要:面向对象的三大特点封装,继承,多态缺一不可。构造函数,是一种特殊的方法。特别的一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同来区分它们即构造函数的重载。 一、基本概念和背景 面向对象程序设计(OOP:Object-oriented programming)是一种程序设计范型,同时也是一种程序开发的方法。对象指的是类的实例。面向对象(Object Oriented,OO...
摘要:在中,并没有对抽象类和接口的支持。例如,当对象需要对象的能力时,可以有选择地把对象的构造器的原型指向对象,从而达到继承的效果。本节内容为设计模式与开发实践第一章笔记。 动态类型语言 编程语言按数据类型大体可以分为两类:静态类型语言与动态类型语言。 静态类型语言在编译时已确定变量类型,动态类型语言的变量类型要到程序运行时,待变量被赋值后,才具有某种类型。 而JavaScript是一门典型...
摘要:众多面向对象的编程思想虽不尽一致,但是无论哪种面向对象编程语言都具有以下的共通功能。原型编程以类为中心的传统面向对象编程,是以类为基础生成新对象。而原型模式的面向对象编程语言没有类这样一个概念。 什么是面向对象?这个问题往往会问到刚毕业的新手or实习生上,也是往往作为一个技术面试的开头题。在这里我们不去谈如何答(fu)好(yan)问(guo)题(qu),仅谈谈我所理解的面向对象。 从历...
阅读 3471·2021-11-23 10:10
阅读 3260·2019-08-30 14:03
阅读 2044·2019-08-30 13:09
阅读 3335·2019-08-29 15:29
阅读 1507·2019-08-29 11:23
阅读 1965·2019-08-28 18:28
阅读 2823·2019-08-26 13:34
阅读 2149·2019-08-26 11:32