0. 概念
在JavaScript中所有的数据都可以看做是对象,但是这并不意味着我们之前已经采用了面型对象编程了。JavaScript中的面面向对象编程和Java、c++这些编程语言中的面向对象编程还是有些有一些不一样的,即使在ES6之后,在对象的定义、新建以及使用上并没有太大的区别。在后者是通过对象模板类型模板类和根据类创建的对象实例组成,而在JavaScript中,并没有特意的区分类和原型二者,而是通过一个原型对象实现面型对象编程。与其说JavaScript中的面向对象编程,不如说它是基于对象编程。
JavaScript的原型链和其他语言的class之间的区别就在,它没有class的概念,所有的对象都是实例,所谓的继承关系只不过是把一个对象的原型指向了另一个对象而已。
接下来,先从工厂函数说起,然后引入面型对象编程,其中面向对象编程分为ES5和ES6两个版本,即使在工作中,我们大多数时候都是采用ES6的方式进行的,但我们需知道,ES6中只是对ES5进行了进一步封装而已,ES5才能真正的体会到JavaScript编程的独特之处。
1. 工厂模式
首先我们先叙述下Java中最常用设计模式之一。这种模式下,创建对象时不会对客户端创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
例如:
// 定义工厂函数
function factory(){
let obj = {};
obj.attr = 'value';
obj.func = () => {};
return obj;
}
// 创建对象
let o = new factory();
o.attr;
o.func();
2. 面向对象ES5
前面叙述了JavaScript语言中是通过函数对象以及函数原型链实现面向对象的。
function Es5Obj(options){
// 构造函数,每次创建新的对象都会为其单独开辟空间,对象之间不相互影响
this.options = Object.assign({/* 默认参数 */},options);
this.func = () => {};
}
// 原型,每次创建新的对象时不会单独创建新的空间,即同类型的对象共享空间
Es5Obj.prototype.arrt = 'value' ;
Es5Obj.prototype.funcP = function(){} ; // 原型函数声明需要使用function命令,不能使用箭头函数,因为this指向问题,这样做是为了保证原型的中this指向类对象
// new指令,会实现:执行函数(构造函数中的代码,非原型属性和方法);自动创建一个新对象;将空指针指向新创建对象;将this绑定到这个对象;隐式返回this
/*
new 执行底层实现运行原理:
function new (contructor, ...arg){
let obj = {};
constructor.call(obj, ...arg);
obj.__proto__ = constructor.prototype;
}
*/
let es5Obj = new Es5Obj();
es5Obj.func();
es5Obj.funcP();
// 实现继承
function Es5Obj2(options){
Es5Obj.call(this.options); // call这种方式的继承只会继承父类构造函数中的属性和方法,原型并不能继承
}
// 原型继承方式1:引用继承
Es5Obj2.prototype = Es5Obj.prototype; // 继承父类原型属性和方法,这种方式属于引用继承,改变子类继承属性和方法影响到父类的原型和属性
Es5Obj2.prototype.construtor = Es5Obj2; // 每个函数都会有原型指向构造函数自身
//原型继承方式2:组合继承
function Link(){}
Link.prototype = Es5Obj.prototype; // 将父类原型指向间接类原型上
Es5Obj2.prototype = new Link(); // 将携带父类原型的间接类创建的对象绑定到子类原型上,子类更改继承的原型属性不会影响父类
Es5Obj2.prototype.constructor = Es5Obj2;
// 原型继承方式3:深拷贝
function deepCopy(obj){
let newObj = Array.isArray(obj) ? [] : {};
for (let i in obj){
if(obj.hasOwnPrototype(i)){
if(typeof obj[i] === 'object'){ newObj[i] = obj[i];deepCopy(obj[i])}
else newObj[i] = obj[i];
}
}
return newObj;
}
Es5Obj2.prototype = deepCopy(Es5Obj.prototype);
Es5Obj2.prototype.constructor = Es5Obj2;
// 无论是深拷贝还是组合继承都是为解决子类原型改变导致父类原型改变的影响。
案例:使用构造函数的方式实现拖拽案例,且使用继承方式实现拖拽对象的范围限定。
注意:
instanceof运算符,用来判断对对象在原型链上是否存在关系;
typeof运算符,无法判断是对象还是数组;
Object.prototype.toString.call(obj) == ‘[object Array]’; // 精确判断数据类型,其他数据类型类似
3. 面向对象ES6
上面对ES5实现面向对象编程进行了介绍,但是 这种方式太过于复杂。为了更加简便的实现面向对象编程,ES6使用class,estends,super关键字对ES5面向对象编程实现了封装。
class Es6Obj {
constructor(options){
this.options = Object.assign({/*默认参数*/},options);
this.func = ()=>{};
}
attr = 'value';
funcP = function(){};
}
let es6Obj = new Es6Obj(options);
es6Obj.funcP();
// 继承
class Es6Obj2 extends Es6Obj { // extends命令会继承父类构造函数和原型,且子类原型改变不会改变父类原型
constructor(options){
super(options); // super()函数会将调用父类构造函数
this.function = () => {};
}
funcP2(){
// 操作
super.funcP(); // 调用父类原型方法
}
funcP(){
// 改写父类原型方法
super.superP(); // super.attr会调用父类的原型属性或方法或者是父类的其他属性和方法
}
}
let es6Obj2 = new Es6Obj2(options);
es6Obj2.funp2(); // 调用子类原型方法
es6Obj2.funp(); // 调用子类改写父类原型方法
es6Obj3.attr; // 调用父类原型属性
案例:使用ES6提供的面向对象编程实现拖拽案例重写。
4. 组件开发
组件开发,实现类似UI库的组件开发,本篇以开发弹框组件为例。
class Dialog extends EventTaget{ // 继承EventTarget类是为了添加自定义事件
constructor(options){
super();
this.options = Object.assign({/*默认配置*/},options);
this.init(); // 调用自执行函数
}
init(){ // 初始化函数
this.render(); // 调用组件渲染函数
this.showDialog() // 调用显示组件代码,实质上是添加监听事件
this.cancel(); // 调用隐藏组件代码,事实上是添加监听事件
this.dragDialog(); // 调用拖拽组件代码,实质上是添加监听事件
}
render(){
/* 组件渲染代码*/
}
showDialog(){ // 显示组件函数
/* 显示组件代码*/
}
cancel(){ // 隐藏组件函数
/* 隐藏组件代码*/
}
dragDialog(){ // 拖拽组件函数
/* 拖拽组件代码*/
}
class InputDialog extends Dialog{ // 为原始组件添加新的代码和功能
constructor(options){
super(options); //
this.inputRender(); // 渲染输入框代码函数,注意子类的构造函数执行顺序在父类构造函数执行之后
}
inputRender(){ // 新组件代码的新功能
/* 渲染输入框代码*/
}
enter(){
/* 重写父类的确定按钮事件逻辑*/
super.ensure(); // 调用父类的原型方法
}
}
let dialog = new Dialog();
let inputDialog = new InputDialog();
}
5. 编写自己的工具库
尽管jquery在当今的编程中已经很少用到了,但是它所体现的思想却还是指的学习的。这里只实现了部分代码,如获取元素属性,设置元素属性,添加元素事件,获取元素。
需要我们学习到的点有:
- 封装一个函数用来返回一个对象,且使用$()、eq(index)多种方式实现获取元素;
- 使用on、eventName多种方式实现事件绑定;
- 使用css、setStyle、getStyle多种方式实现操作元素样式;
- 使用传入元素选择器、原始元素、函数多种方式
- 借助$函数的cssNumber对象属性的方式设置一些属性的样式规则,并允许用户自定义规则;
- 借助$函数的cssHooks对象属性的方式设置一些用户自定义的属性操作规则,且能够使用内置的css操作方式方式操作
class myjQuery extends EventTarget {
constructor(arg,root){
super();
if(typeof root == 'undefined'){
// 第一次调用,没有记录上次操作元素
this.prevObject = new myjQuery(document,null);
}
if(root){
// 记录上次操作的元素
this.prevObject = root;
}
switch(typeof arg){
case 'string':
let eles = document.querySelectorAll(arg);
this.addEvent(eles);break;
case 'function':
window.addEventListener('DOMContentLoaded',arg);
break;
default:
if(typeof arg.length == 'undefined'){
this[0] = arg;
this.length = 1;
}else{
this.addEvent(arg);
}
}
}
// 添加元素到对象实例上
addEvent(eles){
eles.forEach((item,index) => {
this[index] = item;
});
this.length = eles.length;
}
// 添加选择元素
eq(index){
return new Jq(this[index], this);
}
// 获取上次操作的对象
end(){
return this.prevObject;
}
// 为元素添加点击事件
click(fn){
for(let i = 0;i < this.length; i++){
this[i].addEventListener('click',fn);
}
}
// 使用on函数为元素添加多个事件
on(eventName,fn){
let reg = /\s+/g;
eventName = eventName.replace(reg,' ');
let eventArr = eventName.split(' ');
eventArr.forEach((item,index) => {
for(let i=0; i<this.length;i++){
this[i].addEventListener(item,fn);
}
});
}
// 修改元素样式
css(){
if(arguments.length > 1){
// 设置样式
if(arguments[0] in $.cssHooks){
// this.addEventListener('wh',$.cssHooks['wh'].set);
// this.dispatchEvent(new CustomEvent('wh',{
// ele:this[0],
// value:arguments[1]
// }));
// return this[0]
}else{
for(let i =0; i< this.length ; i++){
this.setStyle(this[i],arguments[0],arguments[1]);
}
}
} else{
switch(typeof arguments[0]){
case 'string':
// 获取元素样式
if(arguments[0] in $.cssHooks){
let attrs = $.cssHooks['wh'].get();
let obj = {};
for(let i = 0; i< attrs.length;i++){
obj[attrs[i]] = this.getStyle(this[0],attrs[i]);
}
return obj;
}else{
return this.setStyle(this[0],arguments[0]);
}
break;
default :
// 对象设置样式
for(let i =0;i<this.length;i++){
for(let j in arguments[0]){
this.setStyle(this[i],j,arguments[0][j]);
}
}
}
}
}
setStyle(ele,attr,styleValue){
if( attr in $.cssNumber){
ele.style[attr] = styleValue;
}else{
styleValue = parseFloat(styleValue) +'px';
ele.style[attr] = styleValue;
}
}
getStyle(ele,attr){
return window.getComputedStyle(ele,null)[attr];
}
}
function $(arg){
return new myjQuery(arg);
}
$.cssHooks = {};
$.cssNumber = {
animationIterationCount: true,
columnCount: true,
fillOpacity: true,
flexGrow: true,
flexShrink: true,
fontWeight: true,
gridArea: true,
gridColumn: true,
gridColumnEnd: true,
gridColumnStart: true,
gridRow: true,
gridRowEnd: true,
gridRowStart: true,
lineHeight: true,
opacity: true,
order: true,
orphans: true,
pm: true,
widows: true,
zIndex: true,
zoom: true
};
自定义是事件详解:
所谓的自定事件就是,把函数当做事件执行,其中常用封装操作包括添加事件、执行事件、删除事件
- 添加事件:继承自定义事件类,新建自定义事件类对象,调用对象的addEvent(‘eventName’,事件处理函数);
- 执行事件:在恰当的时机调用事件触发函数,即对象的trigger(‘eventName’);
- 删除事件:调用对象的removeEvent(‘eventname’,原事件处理函数);
// 自定义 自定义事件类
class MyEvent {
constructor() {
this.handle = {};
}
addEvent(evnetName, fn) {
if (typeof this.handle[evnetName] == "undefined") {
this.handle[evnetName] = [];
}
this.handle[evnetName].push(fn);
}
trigger(evnetName) {
this.handle[evnetName].forEach(v => {
v();
})
}
removeEvent(eventName, fn) {
if (!fn in this.handle[eventName]) {
return;
}
for (let i = 0; i < this.handle[eventName].length; i++) {
if (this.handle[eventName][i] === fn) {
this.handle[eventName].splice(i, 1);
break;
}
}
}
}
class Test extends MyEvent{
constructor(){
this.event = new MyEvent(); // 创建自定义事件类对象
}
addEvent(){
this.event,addEvent('eventName',fn);
}
triggle(){
this.event.trigger('eventName');
}
remove(){
this.evnet.removeEvent('eventName',fn);
}
}
class test2 extends EventTarget{
constructor(){}
event1(){
this.addEventListener('eventName',fn); // 添加自定义事件监听,绑定事件处理函数
let eventName = new Event('eventName'); // 创建自定义事件类对象
this.dispatchEvent(eventName); // 触发自定义事件
this.removeEventListener('eventName',fn); // 删除自定义事件
}
event2(){
this.addEventListener('eventName',fn); // 添加自定义事件监听,绑定事件处理函数
let eventName = new CustomEvent('eventName',{/* 回调参数*/}); // 创建自定义事件类对象
this.dispatchEvent(eventName); // 触发自定义事件
this.removeEventListener('eventName',fn); // 删除自定义事件
}
}
Event对象和CustomEvent对象区别:
二者都是系统自带类,且均能添加自定义事件,前者不能添加回调参数,后者可以,且回调参数会显示在回调函数的e对象中。
6. Next
ES6高阶,nodejs。