海阔凭鱼跃,天高任鸟飞。Hey 你好!我是猫力Molly
一万个人心中有一万个哈姆雷特,一万个开发者心中便有一万种对面向对象思想的理解。这里我只粗浅的阐述一下我对面向对象思想的理解,不喜轻喷,欢迎大家评论区留言讨论。
基本概念
在程序里,我们通过使用对象去构建现实世界的模型,把原本很难(或不可)能被使用的功能,简单化并提供出来,以供访问
这段解释摘抄自MDN,读起来甚是绕口。
这里我们可以采用填鸭法来理解面向对象,大致是说看起来像只鸭子,那么它就是一只鸭子。人与机器不同的是,人类具备主观意识而机器没有,一个具备尖尖的嘴,扁扁的脑袋,嘎嘎嘎的叫声并且还会游泳的生物,那么这便是我们使用对象思想去构建了一只鸭子的类。机器不会像人一样主观意识去设想这其实是一只大鹅。
对象的组成
一个基本的对象由若干个数据类型组成,往大的类别上划分的话,可以分为行为和属性
属性:所有的数据类型都可以认为是对象的属性(小鸭子的体重,翅膀,脚丫子等等都是属性)
行为:一般指函数,赋予对象能力(小鸭子会游泳,那么游泳这个行为就是小鸭子的能力)
实例化对象
至此,我们已经创建了一只鸭子类,这时候鸭子仅仅是一个初始化状态,相当于被冰封。我们需要将鸭子解封,可使用new
关键字实例化鸭子对象。这样我们便得到一个全新的鸭子对象。
new
关键字具体干了啥? 可参考如下代码:
var obj = {};
//取得该方法的第一个参数(并删除第一个参数),该参数是构造函数
var Constructor = [].shift.apply(arguments);
//将新对象的内部属性__proto__指向构造函数的原型,这样新对象就可以访问原型中的属性和方法
obj.__proto__ = Constructor.prototype;
//取得构造函数的返回值
var ret = Constructor.apply(obj, arguments);
//如果返回值是一个对象就返回该对象,否则返回构造函数的一个实例对象
return typeof ret === "object" ? ret : obj;
对象中的this
对于this
问题,很多初学者被这个this
指向搞得晕头转向。其实搞懂this
我们只需要记住一句话谁在调用它,它就指向谁,this指向当前调用它的执行环境
经典例子:
var obj = {
foo: function () { console.log(this.bar) },
bar: 1
};
var foo = obj.foo;
var bar = 2;
obj.foo() // 1
foo() // 2
js中的数据类型分为基本数据类型和引用数据类型。基本数据类型是按值访问,引用数据类型是按引用访问。对象将所有的引用放入栈将所有的值放入堆,要获取一个对象值,需要先获取对象引用,然后根据引用找到对应的值。如果引用对应的值是一个函数,由于函数是一个单独的值,可以存在不同的执行上下文环境。那么问题来了,同样的函数在不同的环境下调用,我们如何在函数内部获取当前执行环境呢?没错,this
的出现正是为了解决此类场景问题。
总结:一堆属性和行为聚合到一起便构成了一个最基本的对象。可通过new关键字来实例化一个对象,由于执行环境的不同,对象内部的this指向也不同。在调用对象方法时,需要注意一下this的指向问题。
对象系统
上面我们提到了,一个基础对象的构成。但是在我们实际开发当中远远比这复杂的多,往往是多层对象的嵌套或者多个对象通过某个映射文件相互关联又或者一个对象继承自另一个对象... 从而去构建一个更庞大的对象世界,解决更复杂的应用场景,我们把这种复杂对象称之为 对象系统 ,把这种思想称之为 面向对象编程
显式原型(prototype)
概念:每个函数上都有一个默认的prototype属性使您有能力向对象添加属性和方法。
function people(name) {
this.name = name;
this.say = function () {
console.log(`hello!我是${name}`);
};
}
people.prototype.kungfu = function () {
console.log(`我是${this.name},我会中国功夫`);
};
const qad = new people('秦爱德');
const zs = new people('张三');
console.log(qad);
console.log(qad.say());
console.log(qad.kungfu());
console.log(zs);
console.log(zs.say());
console.log(zs.kungfu());
以上代码创建了一个people
构造函数,在它内部添加了一个name
属性和say
方法,在它的原型上添加了一个kungfu
方法
如何理解内部属性和原型属性呢?
这里我们可以借助css
样式来便于理解
哈哈
以上我们创建了一个标签,并向标签添加了一个内联样式和外部样式,对齐构造函数的话,内联样式对应内部属性,是跟随函数独有的,外部样式对应原型属性,可以是公共的,可在多处使用。
由于每次new
一个新的构造函数,内部属性都会重新生成,而原型属性则不会,所以这也避免了内存上的浪费。并且可以基于原型实现原型继承操作。
构造器(constructor)
概念:每个对象都会默认一个contructor
,并指向当前原型对象的构造函数。
console.log(qad.__proto__.constructor === people); // true
一图胜千言
总结:每个函数上都会自带一个prototype原型,在原型上添加的属性可以共用,函数即对象,对象自带属性constructor指向了这个构造函数
隐式原型(proto)
概念:每个对象都有一个_proto_
属性,指向了创建该对象的构造函数的原型。
console.log(qad.__proto__ === people.prototype); // true
万事万物皆对象,函数也是一个对象,只要是对象,就拥有_proto_
属性,所以_proto_
在构造器和原型之间建立了一个连接,通过由内向外在构造器中找到原型的属性和方法。
原型链
当我们创建了一个构造函数,并访问里面的某一个属性时。会先从构造函数自身去找,再从显式原型(prototype
)上去找,再从隐式原型(__proto__
)上去找,再从object
的__proto__
上去找,直到null
。有值就返回相应的值,没有就返回undefined
,我们把这个由内向外的查找过程称之为原型链
一图胜千言
用好面向对象思想
上面提到我们可以通过使用对象去构建现实世界的模型,并将复杂问题简单化。要想运用好面向对象思想,我们需要牢记面向对象的三大特征和几个原则
三大特征
1:封装
中华文化博大精深,将词语拆分之后,发现更好理解了
封:封存(将一系列行为、属性、业务逻辑等等封存起来)
装:包装(提供一个容器来存放封存起来的代码,包装之后,对外输出)
封装里面还有一个概念叫做抽象,拆分之后也很好理解(把”像“的东西抽出来)
结合起来就是:我们把相似雷同的一堆属性、行为、逻辑抽离出来,存放到一个包装对象里面,控制好入参和出参便于他人调用,这就是封装。
2:继承
继:继续(继续延续下去)
承:承担(承担延续下来的重任,并发扬光大)
结合起来就是:子类继续沿用父类的行为或属性,并合理改造拓展业务,输出新的对象。颇有点子承父业,青出于蓝的意思。
3:多态
多:多种
态:状态 / 形态
结合起来就是:同一个实例对象在多种状态下有不同的展示形态
简单理解就是一个函数通过入参不同,可以得到不同的输出结果
几个原则
1:单一职责原则
一个类或者一个函数实现功能要单一,不能杂乱无章,越纯粹越好。一旦函数变得不纯粹了,内部实现多个功能。当我们在多处地方使用这个函数的时候往往会因为不够纯粹而多写很多兼容代码。
2:开放封闭原则
一个类在拓展性方便应该是保持开放的,对更改性应该是封闭的。比如我们封装了一个函数,应该尽量预留好口子,以便日后新功能迭代,而避免直接更改之前已经写好的代码。
3:里氏替换原则
里氏替换原则主要是用来约束继承的,子类可以扩展父类的功能,但不能改变父类原有的功能。如果子类不能完整地实现父类方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系来代替继承。
4:依赖倒置原则
上层模块不应该依赖于下层模块,两者都应该依赖其抽象。简而言之就是面向接口开发,每个类都提供接口或者抽象类,抽象类往往是比较稳定的,当下层细节发生变化时,不应该直接影响上层。细节依赖于抽象,只要抽象不变,程序就不要变化。
5:组合聚合复用原则
在代码复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
6:高类聚低耦合
顾名思义就是高度类似的东西要聚集起来,低相似的东西不要将它们耦合到一起
js本身就是一门面向对象编程的语言,在我们的日常开发中,每时每刻都在享受着面向对象给我们带来的编程体验。
感谢
欢迎关注我的个人公众号点击查看:前端有猫腻每天给你推送新鲜的优质好文。回复 “福利” 即可获得我精心准备的前端知识大礼包。愿你一路前行,眼里有光!
感兴趣的小伙伴还可以加我点击查看:微信:yuyue540880拉你进群,一起交流前端技术,一起玩耍!