今天打算和大家聊聊面向对象的事,问题得从程序员的对象开始说起,说,某程序员在北京上班
第一天:
——-8:50起床,9:00出门
——-骑电动车25分钟到公司
——-花十分钟吃早餐
——-摸鱼一上午,吃中午饭
——-下午工作了三个小时,接着摸鱼
——-晚上8点下班,回家,睡觉
第二天:
——-8:40起床,自己做了个早餐在家吃,9:30出门
——-骑电动车多等了一个红绿灯,30分钟到公司
——-摸鱼一上午,吃中午饭
——-下午工作了四个小时,接着摸鱼
——-晚上9点下班,回家,睡觉
第三天:…
这是他没有对象的状态,虽然每天都有一些细微的差别,但是过程其实大同小异,但是一个变量发生变化,必然会导致其他的后续的事情跟着发生变化。
后来他有对象了,既然有对象了,对他的生活肯定有一些要求和改变咯
10点必须到公司!
晚上9点必须到家!
于是乎他也成为了别人眼中的对象了,他会吃饭,会上班挣钱,会摸鱼,会骑电动车。至于这些事情怎么安排怎么组合,已经不再重要了,重要的是,他有对象了。
至于以后他还会添加其他的功能:照顾孩子,做家务,买纸尿裤… 功能随意增加,怎么组合依然不重要。
这其实就是面向对象的原理解析了,至于这个故事有没有看懂,其实无所谓,今天也没打算跟你们掰扯面向对象的思想,我们还是来看看具体要怎么做吧。
创建对象
在创建对象之前,大家一定要明白这么一个事情:我们,也可以写一个window对象,比如把window对象打印出来:
window{ alert: ƒ alert() atob: ƒ atob() blur: ƒ blur() btoa: ƒ btoa() caches: CacheStorage {} cancelAnimationFrame: ƒ cancelAnimationFrame() cancelIdleCallback: ƒ cancelIdleCallback() captureEvents: ƒ captureEvents() chrome: {loadTimes: ƒ, csi: ƒ} devicePixelRatio: 2 document: document external: External {} fetch: ƒ fetch() find: ƒ find() getComputedStyle: ƒ getComputedStyle() getSelection: ƒ getSelection() history: History {length: 1, scrollRestoration: "auto", state: null} indexedDB: IDBFactory {} innerHeight: 939 innerWidth: 647 isSecureContext: true length: 0 ··· }
比如我们熟悉的alert方法,你看不就是这个在这里面么?如果我们自己写,要怎么写呢?
window = { alert:function(){ doSomething... } } window.alert();//我们自己写的方法也可以这么调用,所以对象平平无奇
因为要面向对象嘛,所以第一步当然是要创建对象咯,js有这么几个方法可以创建对象
1,字面量
let str = 'name'; let obj = { [str]:"无忧", age:"18", hobby:function(){ console.log("爱好羽毛球"); } }
这个很直接,简单粗暴,没啥可说的,唯一的点就是我把对象里的属性名用了个变量,只是想告诉大家这里是可以用变量的,就和调用一样:obj.age
可以调用,obj['age']
一样可以调用。
2,构造函数:Object;
平时总挺大家说没有对象? new
一个不就好了?这里指的就是用构造函数来创建一个对象。
let obj = new Object(); obj.name = "无忧"; obj.age = 18; obj.hobby = function(){ console.log("爱好羽毛球"); }
3,调用构造函数的方法 Object.create();
let obj = Object.create({ name:"无忧", age:18, hobby(){ console.log("爱好羽毛球"); } })
三个方法都可以创建一个对象,不过呢,最后这个方法创造出来的对象会把里面的属性和方法都放到原型上面,也就是说打印出来是一个空对象
可以看到,把属性和方法都丢到 __proto__
里面去了。
至于这东西是什么,估计你们一定有所耳闻,我们后面再细说。
现在,创建对象的方法也有了,那我们是不是就可以happy coding
了?
工厂模式
以前是没有什么面向对象啊之类的概念的,但是大家在想啊,对象这东西还是很好用的,那如果说我要创造很多对象怎么办呢? 然后每个对象肯定有自己的属性呀,有自己的方法,怎么办呢?
总不能一个个去写吧,虽然没啥问题,但是效率着实有点低啊,于是想到了传参,用一个函数来创建对象,把每个对象的不同属性用参数传进来,不就可以了么?
于是工厂模式就诞生了。
function Person(name,age,hobby){ let obj = {};//创建对象 obj.name = name;//给对象赋值 obj.age = age; obj.hobby = function(){ console.log(hobby); } return obj; //出厂; } let wuyou = Person("无忧",18,"爱好羽毛球"); let lisa = Person("李四",21,"喜欢足球");
其实工厂模式就是“类”的前身。
类,就像一个工厂,用来创建对象。
New运算符
那既然有工厂模式了,这个New
又是用来干啥的呢?为什么没有对象了非得New
一个对象呢?这样更有逼格么?
其实也不是,观察可以发现工厂模式其实需要自己手动创建一个对象,然后给对象赋值,然后还得自己return
出来,但是如果有New运算符的话,就可以省去这些步骤,并且,New
一个对象这个过程看起来也更加语义化一些。
如果把上面工厂模式的代码用New运算符改造一下的话:
function Person(name,age,hobby){ //let obj = {};//创建对象 this.name = name;//给对象赋值 this.age = age; this.hobby = function(){ console.log(hobby); } //return obj; //出厂; } let wuyou = new Person("无忧",18,"爱好羽毛球"); let lisa = new Person("李四",21,"喜欢足球");
可以看到,我们不需要自己再去创建一个对象,然后给它赋值了,New
运算符会帮我们做这件事情。
并且可以发现里面全都变成了this
, 说道面向对象,大家都知道一个概念,那就是构造函数里面的this
, 指向了被创建出来的对象,这个其实也是New
运算符的功劳,这样的话我们可以更方便的在构造函数里去做事情。
总结一下,New运算符做了这么几件事
- 1.执行函数;
- 2.自动创建一个空对象;
- 3.把空对象和this绑定
- 4.如果没有返还,隐式返还this;
构造函数
在上面我提到了一个词:构造函数,其实,构造函数就是上面我们改造过后的工厂模式函数
,只是现在它经过New
运算符的改造,叫构造函数
了,另外New一个对象这个过程也换了个称呼,叫实例化对象
。
所以很多时候我都觉得程序员很恶心,换一个叫法而已,搞那么一大堆概念对吧。
但是没办法,已经踏入这个坑了,只能跟着主流走。
针对构造函数,大家提出了一些要求,得把它和普通的函数区分开对吧,于是乎就把构造函数的首字母都变成了大写,常见的构造函数其实有很多,比如我想要一个字符串:
let str = 'aihsdiahd'; let str2 = new String('adadiaushdia');
可以通过这两种方式,其中,String其实就是一个构造函数,只不过它是内置的构造函数,还有Array,Number等等。
另外,还有一个很重要的一点:构造函数本身也是一个对象,所谓面向对象,一切皆对象就是这么来的。
所以构造函数自身也会有一些方法,这些方法不是实例化对象的,而是构造函数本身的,这些方法我们称之为构造函数的静态方法。
比如上面的代码中,String的静态方法就有String.fromCharCode(),String.fromCodePoint()等。
这个方法只能通过String进行调用,而不能这么用:
let str = 'aihsdiahd'; str.fromCharCode(9731, 9733, 9842, 0x2F804) //报错 //str.fromCharCode is not a function String.fromCharCode(9731, 9733, 9842, 0x2F804) //☃★♲你
上面我们知道了:构造函数的静态方法不能被实例化对象调用,那,什么方法可以被实例化对象调用呢?
其实如果没有提出这个问题,你可能还很清楚,而问题一旦提出来了,往深了去想,你会发现突然很绕,但是反过来去看看工厂模式的代码,你会瞬间明白:构造函数不就是用来创建对象的么?写在对象身上的函数,自然就可以被创建出来的对象(所谓的实例化对象)调用了。
为了加深印象,我们再来看看这段代码:
function Person(name,age,hobby){ let obj = {};//创建对象 obj.name = name;//给对象赋值 obj.age = age; obj.hobby = function(){ console.log(hobby); } return obj; //出厂; } let wuyou = Person("无忧",18,"爱好羽毛球"); wuyou.hobby(); //爱好羽毛球
那prototype又是怎么回事呢?
上面说了半天都没有说到面向对象中,可能是大家最头疼的东西 prototype。这东西吧,说简单其实很简单。
我们还是回头去看看工厂模式的代码,一切从头出发
function Person(name,age,hobby){ let obj = {};//创建对象 obj.name = name;//给对象赋值 obj.age = age; obj.hobby = function(){ console.log(hobby); } return obj; //出厂; } let wuyou = Person("无忧",18,"爱好羽毛球"); wuyou.hobby(); //爱好羽毛球 let shine = Person("阳光",20,"喜欢韩剧"); shine.hobby(); //喜欢韩剧 console.log(wuyou.hobby===shine.hobby);//false
上面的代码中,我们用 Person
去创建了一个对象并且赋给 wuyou
,于是 wuyou
便有了属性name
和age
,并且还有了一个hobby
方法。
但是这个hobby
方法是属于wuyou
这个家伙的,它的hobby
方法在内存中有独立的地址。
如果再创建一个对象,那么新对象也会有一个hobby
方法,并且在内存中也会有独立的地址。两个hobby
方法是不相等的。
| 既然不相等,代表这两个方法不一样,不一样为什么要放到一个公有的工厂里去创造呢? 对不对?
| 通过构造函数构造出来的类,应该具有通用性,独特的东西后天再加
| 这样,大家估计应该知道以后什么方法放到构造函数里去,什么方法单独添加了。
但是,公共的方法,放在哪里呢? 于是prototype
就出来了。
一切都是为了性能着想。
prototype提供了一个公共的空间,可以把对象的公用方法都放在这里面
这就像是造汽车,造汽车需要匹配的维护技术,但是有没有必要说每造一辆车就去定制一套匹配的维护技术呢?显然是没有必要的,造出来的汽车一样,那么维护技术也是一样的,就没有必要说专车专用。
有了prototype这个公共空间,大家就可以随意的去研究更多的公用函数,为什么数组这玩意儿有那么多的方法?
concat(),copyWithin(),entries(),every(),fill(),filter(),find(),findIndex(),flat(),flatMap(),forEach(),includes(),indexOf(),join(),keys(),lastIndexOf(),map(),pop(),push(),reduce(),reduceRight(),reverse(),shift(),slice(),some(),sort(),splice(),toLocaleString(),toSource(),toString(),unshift(),values()。
大家可以感受一下,就是因为公共空间的存在,有了公共空间之后,并不会对创建出来的对象有任何的影响,而且创建出来的对象都可以去使用这些方法,所以这才是prototype
的强大之处
然后还有一个东西:__proto__
呵呵,这就更简单了,前面说了,构造函数提供了公共空间去存放公用的方法,而__proto__
就是实例化对象身上的一把钥匙,可以去访问这个公共空间。
举例:
String.prototype.trim = function () { return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); }; //这是字符串 trim 方法的实现,于是: let str = ' asid '; str.trim();//'asid' str.__proto__ === String.prototype;//true
但是大家始终记住,一切皆对象,prototype其实也是一个对象,上面的方法我们可以这样写:
String.prototype = { trim:function(){ return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); } } //这是字符串 trim 方法的实现,于是: let str = ' asid '; str.trim();//'asid' str.__proto__ === String.prototype;//true
只不过这样的话我们就把整个prototype
给改写了,prototype
身上有一些固有属性存在的。
为什么会有__proto__
这个属性的存在呢? 就是为了拓展,我们可以通过实例化对象给构造函数添加一些公用的方法,就像Vue里面的父子级之间的通信一样。
既然prototype也是一个对象,于是它的身上也有__proto__
属性,于是就形成了所谓的原型链
ok,以上就是关于面向对象的一些东西了,enjoy···