前言
面向对象相信对大家来说肯定不陌生,基本上现代web
开发不会这玩意以及没法写东西了
但是呢,用归用,相信大家平常工作中需要写原生对象的地方并没有多少,本篇文章来对面向对象
进行一个总结和梳理
概念
咱们先来说一下概念
,因为不同人叫面向对象名字也不同,这里属性
和变量
意思都是一样的,咱们不去纠结他们具体叫什么
对象组成可以分为两大类:
- 属性、变量、状态、数据
- 过程、方法、函数
- 类(class): 类本身,一般没什么功能,除非是
Math
这种基本全是静态方法的类 - 实例(instance):类被实例化之后的东西,就叫实例
- 成员(meber):包括(属性+方法)
- 实例成员(实例上才能用的属性和方法,类似
str.length
不能直接写成String.length
) - 类成员(类似
Math.xxx
),无需实例化就可以用
- 实例成员(实例上才能用的属性和方法,类似
打个比方let oDate = new Date()
这个Date
就是类,而这个oDate
就是实例
而绝大部分人再说对象的时候都是在说
实例
抽象
抽象
这个词相信大家已经快听吐了,但是又不是很好理解,这里大概说一下
- 提取过程——设计类、设计程序
- 这个其实很好理解,在写一个程序的时候,比如我们要做一个聊天的功能,整理需求,然后定义好自己需要的类,比如这个用户需要有名字,和头像等等这个过程,就叫
抽象
- 这个其实很好理解,在写一个程序的时候,比如我们要做一个聊天的功能,整理需求,然后定义好自己需要的类,比如这个用户需要有名字,和头像等等这个过程,就叫
- 抽象类
- 这个可能稍微复杂一点,有那么一种类,是不实现任何功能的
- 可能有人看到这就有点不理解,为啥我要写一个不实现任何功能的类呢,这里为了方便用
react
举个例子 - 咱们都知道
react
中写组件需要class App extnds React.Component...
而这里面必须要有一个render
方法,如果没有,它就会给你报错 - 当然了,这里主要为了说明例子,不包括
hook
什么的,别杠我,我害怕 - 而抽象类可以实现这个功能,给所有的类加一个公共的基类(也就是父类)统一处理
- 而
react
中的这个Component
就是实现了一个抽象类
面向对象思想
这个词儿也是被用烂了,大家不管是从别的文章看过也好,还是找过什么资料,看了一大堆,还是不明白啥叫面向对象思想
封装
封装的最基本的目的是——为了保护咱们类
或者实例
当中的各种成员
事实上来说咱们不管写什么
面向对象
也好,面向过程
也好,面向切片
等等等等,最终的目的就是为了让这个程序出错少,效率高,易于调试和拓展
而接下来咱们就能想到,程序都是人写的,人肯定就会犯错,容易偷懒或者侥幸心理
而咱们封装的类,往往会有很多的数据需要内部处理,比如有一个请求队列的列表,你在你内部声明好了,然后如果想取消需要用户直接调你的方法,但是如果不保护的化,可能有人犯懒直接就看到这是个数组直接就操作了,这时候如果出错了或者这个类有其他的地方没做相应的操作就很难查找
所以这个封装大概有四重目的
- 保护成员
- 数据隐藏——方法
- 强制访问权限
- 便于理解
继承
任何一个类,可以不用从零开始写,可以在一个原有的类的基础之上做一些修改等等这就叫继承
- 重用代码
- 无需修改父类
- 可以抽象类
这个继承相信大家还是没问题的,这里不多赘述
多态
多态也属于一种抽象,也是一种比较常用的思想,比如现在公司有一个转账的功能,支持多个国家互相转的,那这个钱不一样汇率也就不一样,以及其他的一些问题
当然这时候直接用if
判断肯定也是可以的,但是这不是感觉不够装*
么,这时候就可以用多态,把这个钱统一处理,至于具体汇率什么的细节,分别交给他们自己来处理,这个思想,就是多态
总结
相信看到这大家应该已经明白大概的意思了,不过对于初学者来说还是不太明白到底具体到项目能怎么写,别慌,一步一步来
有一点需要确认,大家可千万别以为
面向对象是语言所独有的东西
,比如java
是面向对象的,c
是面向过程的,其实也不是,面向对象思想几乎在所有的地方都有用
比如说数据库,也有面向对象数据库——ORM
,它里面存的就不是像excel
那样的数据了,而是一个对象,有相应匹配的操作等等一系列的
写法
首先咱们要知道,任何一种类,都需要构造函数
,什么叫构造函数
呢,很简单,就是当你这个类实例化的时候,需要做一些初始化
的工作
es6之前
在es6
之前,类和构造函数是不分的,这也是很不好的一点,现在如果想实现一个类,直接拽一个function
在这很难分辨就是是一个函数
还是类
,所以,这个函数既是构造函数也是类
咱们来直接写一个看看
function A() {
console.log('hello');
}
var a = new A();
可以看到,在咱们实例化的过程当中,就会运行函数内的代码
以及,咱们如果相加属性和方法也是很简单的
属性和es6
版本一样,直接加this
就行
方法放在prototype
上,这个没什么可说的
继承
es6
之前没给咱们提供直接继承的方法,所以得咱们手动操作,所以咱们得明白继承是干什么
继承就是把父类的方法+属性拿过来就可以了
首先,咱们先来随便写一个类出来,像这样
然后搞一个子类,并且新增一个参数,然后咱们把参数先拿过来,怎么拿呢?
现在咱们的类是一个函数,是函数就能执行,咱们只需要把A
拿过来执行一下并且call
到自己身上,再把A
需要的参数传过去,这样就能拿到所有的参数了,像这样
好的,属性咱们现在已经可以拿过来了,那么方法怎么拿过来了,方法在哪,在A
的prototype
上,那能直接B.prototype = A.prototype
么?首先肯定一点,这么写,东西肯定能出来,不过问题大家也知道,js
里存在引用
,给子类添加方法父类也被修改了这肯定不行
不啰嗦了
咱们可以直接B.prototype = new A()
咱们可以看到,东西是能出来的,父类的方法也没有被污染到,这是为什么呢,这里就不得不搞出来原型链这个概念了
有大白话讲就是 找一个实例要东西,它会先从自己实例身上找,找不到的话再找自己的prototype
,但是咱们现在这个prototype
指向的就是A
的实例,所以从B
实例找不到后去找prototype
的时候,找不到就回去找A
的实例,A
的实例找不到就回去找A
的prototype
然后就可以了
好玩不
相信看到这,大家应该已经明白es6
之前的写法缺陷了,功能肯定是都能实现,但是太乱了,一个团队好几十人,你搞你的,我搞我的就乱套了
es6
咱们es6
写class
就简单多了,直接提供了一个关键字class
,直接写就行了,不过本质上来说只是语法糖而已,所以建议大家还是看看es6
之前的写法
写法也是很简单的,注意一点,这里面的方法并不是函数的简写,写一个function
的话,反而会报错
继承
继承es6
也有专门的关键字来说明,就叫extends
很简单对不对?
注意⚠️ 在子类什么都不写的情况下,默认会给你加一个
constructor
,如果你自己写了constructor
,就相当于你需要自己来了
主要要干的就是需要把父类
的属性拿过来,es6
有一个super
关键字,就相当于直接把父类
的constructor
直接在子类里执行了一遍,不用像之前一样要么apply
,要么call
的,很乱
注意⚠️ 在咱们构建子类的时候,需要完成父类的构建,也就是那个
super
,如果先用this
后super
的时候会报错
this
this
相信大家肯定经常用,并且一会变成这个,一会变成那个,特别的乱
this
取决于谁在调用,this
就是谁
而this
是用于函数
/方法
内部,而谁在调用这个函数,this
就指向谁
js
本身又是全局的东西都属于window
,所以咱们这么写
function fn() {
console.log(this);
}
fn();
就完全等价于这么写
function fn() {
console.log(this);
}
window.fn();
所以console
出来的this
也是window
咱们可能看过很多面试题都有类似这样的题目
总的来说就是一个函数给这个对象
那个对象的,然后打印this.xxx
,其实想明白这件事,一切都会变得很简单了
现在这个arr.fn
= fn
了,而调用的时候,是arr
在调用,所以这个this
就是arr
当然了,事件
也是一样的
很简单对不对?
顺便一说,js的
this
这么乱是因为作者本身想让它变得更简单,谁调就是谁,多好呀~ 不过这往往在写一些大项目的时候会有一堆问题
严格模式
而js
作者后来又出了一个严格模式,因为全局的东西都属于window
这事儿本来也不靠谱,毕竟js
运行场景已经很多了,比如nodejs
,就没有window
的概念
而用严格模式也很简单,直接在script
里加一个"use strict"
就可以,像这样
这时候console
出来的就是undefined
了
定时器
不过这个this
还受一点影响,也就是定时器
可以看到我现在是开着严格模式
的情况下,但是console
的还是window
不过这倒是也好解释,毕竟这个定时器
是window
的,是浏览器调的,也是通过window
间接的来执行到了这个函数
操作this
js
中有两种操作this
的方法
- 函数的方法
- 咱们都知道直接写一个函数也就相当于
new
了一个Function
类,所以这么写
完全等价于function fn() { alert(this); }
var fn = new Function('alert(this)');
- 所以函数也是个对象,既然是对象,那么也就有方法
-
call
- 咱们正常运行函数可能是直接
fn();
,用了这个直接在括号前加一个call
就可以,像这样 - 这个你传什么,它的
this
就是什么,随便传,想这个例子,console
的this
就是"aaa"
- 至于参数,直接往后堆就可以了
- 咱们正常运行函数可能是直接
-
apply
- 这个
apply
就很简单了,和call
基本一摸一样,区别在于call
跟参数是直接堆在后面,apply
的其他参数是放在一个数组里的,像这样
- 这个
-
bind
- 这个
bind
跟前两者不一样,call
和apply
都是直接运行了,bind
的作用是返回一个新函数 - 可以看到,不管生成的新函数在哪运行,谁调用
this
都是不变的
- 这个
- 咱们都知道直接写一个函数也就相当于
- 箭头函数
- 箭头函数内部的
this
永远跟箭头函数外部的this
一致,也就是上下文 - 正常情况下这个
document.onclick
里的函数的this
指向HTMLDocument
,所以找不到aaa
,会console
出undefined
,这个很好理解 - 改成箭头函数之后,这回就对了,因为它的上下文环境在
class A
里,这就没问题了 - 当然了,用
bind
也是可以的,这不是方便么
- 箭头函数内部的
类型检测
typeof
typeof更适合检测基本类型:number、boolean、string、function、Object、undefined、null、bigint、symbol
这个相信大家也都用过,也就不多说了,检测基本类型代表咱们分不清一个对象到底是数组还是json
或者是map
等等
所有对象全都是object
,那这个肯定满足不了需求
instanceof
这个instanceof
是可以检测具体类型的,而且不光可以检测子类,对子类的父类也能检测到
其实这不能算是问题,因为正常来说子类本来就>=
父类
constructor
但是有时候我们就是不想对父类也有反应,就是想检测是不是属于我这个类,这是有直接用constructor
来判断就很方便
constructor
是可以用于精确匹配的
这个constructor
极少的情况会用到,本身并不是用来判断类型的,而是返回实例的构造器
,不过正好咱们可以间接做一个类型判断而已
正常情况用typeof
和instanceof
就已经足够了
当然了,可能有人看到es6
之前的class
写法已经要喷我了,因为那么写用constructor
判断又有问题,因为那么写子类
的constructor
就会变成父类
了,所以还需要重置一下
等等等等把,es6
之前的写法等等需要处理细节方面还很多,因为现在实在是不常用,所以本文就不多赘述了
高阶类-HOC
高阶类这个词可能有人没听过,这里简单描述一下
一般情况下,都是子类继承父类,然后子类可以使用父类当中的东西,而这个高阶类,可以反过来,父类使用子类的东西
顺便一说,不是只有
js
才有高阶类的概念,几乎所有语言都有,只是方不方便的问题
咱们直接来写一个
可以看到,直接在class A
上,并没有bbb
这个东西,而直接子类去继承的时候,子类身上有,间接着就可以调用了
这就是所谓的高阶类
,贼简单吧
不过直接这么说大家可能感觉不到有什么应用场景,其实也不太常用,在写一些工具或者框架的时候可能会用到,这里简单写个小例子,多个类之间如何用高阶类共享数据 当然了,高阶类的用途有很多,这只是其中一种而已
class Store {
constructor() {
this.state = {};
}
get(key) {
return this.state[key];
}
set(key, value) {
this.state[key] = value;
}
conect(cls) {
let _this = this;
return new (class extends cls {
constructor(...args) {
super(args);
this.get = _this.get.bind(_this);
this.set = _this.set.bind(_this);
}
})();
}
}
let store = new Store();
let a = store.conect(class {});
a.set('aaa', '我是aaa');
let b = store.conect(class {});
console.log(b.get('aaa'));
这个例子很简单,主要就是要分清谁是父类,咱们把数据全存到store
类里,然后接受一个类参数,内部继承一个新的类,然后添加出新的方法,而在使用者来说,也就是a
类和b
类,这些都不用管,他们自己就是父类,来调用子类的方法
是不是很简单?
大概就是这种感觉,至于工作当中怎么用,就看大家自己的业务场景了
可相应对象
所谓的可相应对象就是你操作这个对象的时候可以收到通知,比如vue
,大家应该都知道vue
页面,就算直接在浏览器f12
控制台中,直接vm.xxx=xxx
对某一项数据修改,页面中就会发生变化,这就是可相应对象
访问器
这个访问器就是在原型的方法前加get
或者set
,至于内部要干什么,你自己决定,像这样
用过ts
版的vue2.x
的大家都知道,computed
就是用这玩意的原生语法
访问器还是比较简单的,在一些小的场景中用的比较多
defineProperty
defineProperty
接受三个参数(data,key, option)
这个data
就是你的数据源,这个key
就是你要监听哪个属性,剩下的具体的操作放在option
里,比如get
和set
等等,咱们直接来试试
var json = {
_num: 0,
};
Object.defineProperty(json, 'num', {
get() {
console.log('get');
return this._num;
},
set(val) {
console.log('set');
this._num = val;
return this._num;
},
});
console.log(json.num);
json.num++;
console.log(json.num);
是不是很简单,当然了,细节上还有很多,比如delete
,咱们知道正常情况下js
中的json
是可以直接delete
某个key
的,但是咱们现在直接这么写是不行的
咱们可以给一个参数configurable: true
,默认的情况是false
更多参数介绍大家可以看MDN文档中的defineProperty,这里就不多赘述
咱们这里用的只是json
,大家可能会感觉跟访问器
区别不大,而且,平时咱们也不是直接对json
用,而是像vue 2.x
一样又能操作实例,又能this
直接访问或修改属性
类中的可相应对象
咱们都知道vue2.x
中可以这么修改属性
知道了这点之后咱们就可以干很多事儿了,咱们可以直接监听这个类
来进行return
这样咱们就可以监听到变化了,能监听到变化了自然就可以紧接着干其他的事了,比如重新渲染页面等等
当然了,真正
Vue
中可完全不是这么写的,因为它东西很多,而且所有数据都需要监听,咱们现在只是单单监听一个对象里的key
,方法咱们也知道,循环+递归
么,不过要把这些东西整理出来其实也是很复杂的,比如页面属性怎么跟你的data
绑定,组件间的传值,渲染、虚拟dom等等
等等吧,如果真拓展成一个方便的框架或者小工具还是有很多工作的, 这里只是说明方法,有什么更方便的用法和场景还需要大家自己钻研
defineProperty缺陷
用过vue
的大家都知道vue
中是不能直接用下标修改数组中的某一条数据的,vue
作者也推出了$set
解决这个问题,这个就是defineProperty
的问题
- 对数组下标直接赋值
- 对
json
对象没有的key
赋值
大家可以看到其实还好,虽然问题不大,不过没有肯定比有强,proxy
没这个问题,为啥不直接用proxy
呢
proxy
和defineProperty
有点不同,defineProperty
是操作监听的原始对象,而proxy
是操作返回出来的新对象
有两个参数,一个是数据,一个是对应的操作,这里面有几个常用的参数
- has 这个对应的就是
js
中的in
操作- 咱们知道js里可以直接
"a" in {a: 12}
这么判断 - 而这个就会触发has,像这样
- 这个
data
就是原始数据,基本上每个方法里都会把原始数据给你,很方便
- 咱们知道js里可以直接
- get 获取,这个相信不用多说,都差不多
- set 设置
- deleteProperty
- 这个其实就是
delete
,只不过delete
是关键字,所以取了个这个名字
- 这个其实就是
- apply 这个
apply
其实很有用,它可以监听一个函数,在编写一个axios这样的库 我也用到了,有兴趣大家可以看一下apply
有三个参数,第一个还是data
,但是由于咱们是监听函数,所以就是那个函数本身,第二个就是谁在调用的那个this
,上文讲过谁调用函数this
就指向谁,所以这个第二个参数就是那个this
,第三个参数就是咱们运行这个函数的时候传进来的参数
- construct 监听类
- 它有两个参数,一个是监听的那个
class
,一个是参数,其实用起来感觉跟高阶类有点相似 - 不过一般用起来,咱们还需要再配合一个
proxy
,因为要监听咱们一般都是要监听这个类上的属性有没有改变之类的,所以还要单独再监听一遍实例返回出去
- 它有两个参数,一个是监听的那个
剩下的参数大家感兴趣的话也可以去MDN中的Proxy去了解,也不多赘述
完整的监听例子
看到这大家应该都对js
中的可监听对象了解的差不多了,这里附上一个相对完成一点的监听例子
类似vue
中的data
,data
可以是值,也可以是json
,也可以是数组,数组里套json
,json
套数组等等,咱们可以分别判断一下然后套一个递归就搞定了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<script>
function createProxy(data, cb) {
let res;
if (data instanceof Array) {
res = [];
for (let i = 0; i < data.length; i++) {
if (typeof data[i] == 'object') {
res[i] = createProxy(data[i], cb);
} else {
res[i] = data[i];
}
}
} else {
res = {};
for (let key in data) {
if (typeof data[key] == 'object') {
res[key] = createProxy(data[key], cb);
} else {
res[key] = data[key];
}
}
}
return new Proxy(res, {
get(data, name) {
return data[name];
},
set(data, name, val) {
data[name] = val;
cb(name);
return true;
},
});
}
let _json = {
arr: [
{
a: [1, 2, 3],
},
321,
],
json: {
aaa: {
bbb: {
ccc: 111,
},
},
},
name: 'name',
};
var p = createProxy(_json, function(name) {
console.log('set');
});
</script>
</body>
</html>