JS 面向对象详解

写在前面

既然是浅谈,就不会从原理上深度分析,只是帮助我们更好地理解...

面向对象与面向过程

面向对象和面向过程是两种不同的编程思想,刚开始接触编程的时候,我们大都是从面向过程起步的,毕竟像我一样,大家接触的第一门计算机语言大概率都是C语言,C语言就是一门典型的面向过程的计算机语言。面向过程主要是以动词为主,解决问题的方式是按照顺序一步一步调用不同的函数。面向对象是以名词为主,将问题抽象出具体的对象,而这个对象有自己的属性和方法,在解决问题的时候,是将不同的对象组合在一起使用。

//面向过程装大象1.开(冰箱)2.(大象)装进(冰箱)3.关(冰箱)

//面向对象装大象1. 冰箱.开门()2. 冰箱.装进(大象)3. 冰箱.关门()

从这个例子可以看出,面向对象是以主谓为主,将主谓堪称一个一个的对象,然后对象有自己的属性和方法。面向对象是以功能来划分问题的,而不是步骤。功能上的统一保证了面向对象设计的可扩展性,解决了代码重用性的问题。这也是在漫长的程序设计的发展过程中得到的验证结果,面向对象的编程思想较之于面向过程较好一点。

封装

面向对象有封装、继承和多态三大特性。
封装:就是把事物封装成,隐藏事物的属性和方法的实现细节,仅对外公开接口。

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

1.通过构造函数添加

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定义的属性和方法,我们实例化对象的时候斗湖重新复制一份

2.通过原型prototype封装

在类上通过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中的类和封装

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!"

继承

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

1.类式继承

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

//声明父类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');

es6中的继承

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!

感兴趣的小伙伴,可以关注公众号【grain先森】,回复关键词 “vue”,获取更多资料,更多关键词玩法期待你的探索~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 继承的概念:子类可以使用父类共享的属性和方法,避免重复代码提高代码复用性。 原型链:子类可以共享父类的实例对象和实...
    浅秋_6672阅读 393评论 0 0
  • 单例模式 适用场景:可能会在场景中使用到对象,但只有一个实例,加载时并不主动创建,需要时才创建 最常见的单例模式,...
    Obeing阅读 2,056评论 1 10
  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,718评论 2 17
  • > 此篇总结与《JavaScript设计模式》github地址 [YOU-SHOULD-KNOW-JS](http...
    Nealyang阅读 306评论 0 1
  • 陈老师说,莫问山路凶吉,但求落幕无悔 人生成功的三要素可以用三个单词来形容 positive soluble co...
    臭臭和狗剩阅读 274评论 0 0