JavaScript设计模式之面向对象编程

为了深刻地学习 javascript ,奔着一名标准 Web 开发人员的标准,想要深刻了解一下面向对象的编程思想,提升本身模块化开发的能力,编写可维护、高效率、可拓展的代码,最近一直拜读 《JavaScript设计模式》 ,对其重点内容作了概括与总结,若有总结的不详细或者理解不透彻的,还望批评斧正~javascript

什么是面向对象编程(OOP)?

简单来讲,面向对象编程就是将你的需求抽象成一个对象,而后对这个对象进行分析,为其添加对应的特征(属性)与行为(方法),咱们将这个对象称之为 。 面向对象一个很重要的特色就是封装,虽然 javascript 这种解释性的弱类型语言没有像一些经典的强类型语言(例如C++,JAVA等)有专门的方式用来实现类的封装,但咱们能够利用 javascript 语言灵活的特色,去模拟实现这些功能,接下里咱们就一块儿来看看~java

封装

  • 建立一个类

javascript 中要建立一个类是很容易的,比较常见的方式就是首先声明一个函数保存在一个变量中(通常类名首字母大写),而后将这个函数(类)的内部经过对 this 对象添加属性或者方法来实现对类进行属性或方法的添加,例如:编程

//建立一个类
var Person = function (name, age ) {
	this.name = name;
	this.age = age;
}
复制代码

咱们也能够在类的原型对象(prototype)上添加属性和方法,有两种方式,一种是一一为原型对象的属性赋值,以一种是将一个对象赋值给类的原型对象:设计模式

//为类的原型对象属性赋值
Person.prototype.showInfo = function () {
    //展现信息
    console.log('My name is ' + this.name , ', I\'m ' + this.age + ' years old!');
}

//将对象赋值给类的原型对象
Person.prototype = {
    showInfo : function () {
	    //展现信息
	    console.log('My name is ' + this.name , ', I\'m ' + this.age + ' years old!');
	}
}
复制代码

这样咱们就将所须要属性和方法都封装在 Person 类里面了,当咱们要用的时候,首先得须要使用 new 关键字来实例化(建立)新的对象,经过 . 操做符就可使用实例化对象的属性或者方法了~数组

var person = new Person('Tom',24);
console.log(person.name)        // Tom
console.log(person.showInfo())  // My name is Tom , I'm 24 years old!
复制代码

咱们刚说到有两种方式来添加属性和方法,那么这两种方式有啥不一样呢?安全

经过 this 添加的属性和方法是在当前对象添加的,而 javascript 语言的特色是基于原型 prototype 的,是经过 原型prototype 指向其继承的属性和方法的;经过 prototype 继承的方法并非对象自身的,使用的时候是经过 prototype 一级一级查找的,这样咱们经过 this 定义的属性或者方法都是该对象自身拥有的,咱们每次经过 new 运算符建立一个新对象时, this 指向的属性和方法也会获得相应的建立,可是经过 prototype 继承的属性和方法是每一个对象经过 prototype 访问获得,每次建立新对象时这些属性和方法是不会被再次建立的,以下图所示:bash

其中 constructor 是一个属性,当建立一个函数或者对象的时候都会给原型对象建立一个 constructor 属性,指向拥有整个原型对象的函数或者对象。框架

若是咱们采用第一种方式给原型对象(prototype)上添加属性和方法,执行下面的语句会获得 true模块化

console.log(Person.prototype.constructor === Person ) // true
复制代码

那么好奇的小伙伴会问,那我采用第二种方式给原型对象(prototype)上添加属性和方法会是什么结果呢?函数

console.log(Person.prototype.constructor === Person ) // false
复制代码

卧槽,什么鬼,为何会产生这种结果?

缘由在于第二种方式是将一整个对象赋值给了原型对象(prototype),这样会致使原来的原型对象(prototype)上的属性和方法会被所有覆盖掉(pass: 实际开发中两种方式不要混用),那么 constructor 的指向固然也发生了变化,这就致使了原型链的错乱,所以,咱们须要手动修正这个问题,在原型对象(prototype)上手动添加上 constructor 属性,从新指向 Person ,保证原型链的正确,即:

Person.prototype = {
		constructor : Person ,
		showInfo : function () {
			//展现信息
			console.log('My name is ' + this.name , ', I\'m ' + this.age + ' years old!'); } } console.log(Person.prototype.constructor === Person ) // true 复制代码
  • 属性与方法的封装

在大部分面向对象的语言中,常常会对一些类的属性和方法进行隐藏和暴露,因此就会有 私有属性、私有方法、公有属性、公有方法等这些概念~

ES6 以前, javascript 是没有块级做用域的,有函数级做用域,即声明在函数内部的变量和方法在外部是没法访问的,能够经过这个特性模拟建立类的 私有变量私有方法 ,而函数内部经过 this 建立的属性和方法,在类建立对象的时候,每一个对象都会建立一份并可让外界访问,所以咱们能够将经过 this 建立的属性和方法看做是 实例属性实例方法,然而经过 this 建立的一些方法们不但能够访问对象公有属性和方法,还能访问到类(建立时)或对象自身的私有属性和私有方法,因为权力这些方法的权力比较大,所以成为 特权方法 ,经过 new 建立的对象没法经过 . 运算符访问类外面添加的属性和和方法,只能经过类自己来访问,所以,类外部定义的属性和方法被称为类的 静态公有属性静态公有方法 , 经过类的原型 prototype 对象添加的属性和方法,其实例对象都是经过 this 访问到的,因此咱们将这些属性和方法称为 公有属性公有方法,也叫 原型属性原型方法

//建立一个类
	var Person = function (name, age ) {
    	    //私有属性
    	    var IDNumber = '01010101010101010101' ;
    	    //私有方法
            function checkIDNumber () {}
            //特权方法
            this.getIDNumber = function () {}
            //实例属性
            this.name = name;
            this.age = age;
            //实例方法
            this.getName = function () {}
	}

	//类静态属性
        Person.isChinese = true;
	//类静态方法
        Person.staticMethod = function () {
            console.log('this is a staticMethod')
        }

        //公有属性
	Person.prototype.isRich = false;
	//公有方法
        Person.prototype.showInfo = function () {}
复制代码

经过 new 建立的对象只能访问到对应的 实例属性 、实例方法 、原型属性 和 原型方法 ,而没法访问到类的静态属性和私有属性,类的私有属性和私有方法只能经过类自身方法,即:

var person = new Person('Tom',24);

        console.log(person.IDNumber) // undefined
        console.log(person.isRich)  // false
        console.log(person.name) // Tom
        console.log(person.isChinese) // undefined
        
        console.log(Person.isChinese) // true
        console.log(Person.staticMethod()) // this is a staticMethod
复制代码
  • 建立对象的安全模式

咱们在建立对象的时候,若是咱们习惯了 jQuery 的方式,那么咱们极可能会在实例化对象的时候忘记用 new 运算符来构造,而写出来下面的代码:

//建立一个类
	var Person = function (name, age ) {
		this.name = name;
		this.age = age;
	}
	
	var person = Person('Tom',24)
复制代码

这时候 person 已经不是咱们指望的那样,是 Person 的一个实例了~

console.log(person)  // undifined
复制代码

那么咱们建立的 nameage 都不知去向了,固然不是,他们被挂到了 window 对象上了,

console.log(window.name)  // Tom
    console.log(window.age)   // 24
复制代码

咱们在没有使用 new 操做符来建立对象,当执行 Person 方法的时候,这个函数就在全局做用域中执行了,此时 this 指向的也就是全局变量,也就是 window 对象,因此添加的属性都会被添加到 window 上,而咱们的 person 变量在获得 Person 的执行结果时,因为函数中没有 return 语句, 默认返回了 undifined

为了不这种问题的存在,咱们能够采用安全模式解决,稍微修个一下咱们的类便可,

//建立一个类
	var Person = function (name, age) {
		// 判断执行过程当中的 this 是不是当前这个对象 (若是为真,则表示是经过 new 建立的)
		if ( this instanceof Person ) {
			this.name = name;
			this.age = age;
		} else {
			// 不然从新建立对象
			return new Person(name, age)
		}
	}
复制代码

ok,咱们如今测试一下~

var person = Person('Tom', 24)
	console.log(person)         // Person
	console.log(person.name)    // Tom
	console.log(person.age)     // 24
	console.log(window.name)    // undefined
	console.log(window.age)     // undefined
复制代码

这样就能够避免咱们忘记使用 new 构建实例的问题了~

pass:这里我用的 window.name ,这个属性比较特殊,它是 window 自带的,用于设置或返回存放窗口的名称的一个字符串,注意更换~

继承

继承也是面型对象的一大特征,可是 javascript 中没有传统意义上的继承,可是咱们依旧能够借助 javascript 的语言特点,模拟实现继承

类式继承

比较常见的一种继承方式,原理就是咱们是实例化一个父类,新建立的对象会复制父类构造函数内的属性和方法,并将圆形 __proto__ 指向父类的原型对象,这样就拥有了父类原型对象上的方法和属性,咱们在将这个对象赋值给子类的原型,那么子类的原型就能够访问到父类的原型属性和方法,进而实现了继承,其代码以下:

//声明父类
    function Super () {
        this.superValue = 'super';
    }
    //为父类添加原型方法
    Super.prototype.getSuperValue = function () {
        return this.superValue;   
    }
	
    //声明子类
    function Child () {
        this.childValue = 'child';
    }
	
    //继承父类
    Child.prototype = new Super();
    //为子类添加原型方法
    Child.prototype.getChildValue = function () {
        return this.childValue;
    }
复制代码

咱们测试一下~

var child = new Child();
    console.log(child.getSuperValue());  // super
    console.log(child.getChildValue());  // child
复制代码

可是这种继承方式会有两个问题,第一因为子类经过其原型 prototype 对其父类实例化,继承父类,只要父类的公有属性中有引用类型,就会在子类中被全部实例共用,若是其中一个子类更改了父类构造函数中的引用类型的属性值,会直接影响到其余子类,例如:

//声明父类
    function Super () {
    	this.superObject = {
    		a: 1,
    		b: 2
    	}
    }


    //声明子类
    function Child () {}
    
    //继承父类
    Child.prototype = new Super();
    }

    var child1 = new Child();
    var child2 = new Child();
    console.log(child1.superObject);    // { a : 1 , b : 2 }
    child2.superObject.a = 3 ;
    console.log(child1.superObject);    // { a : 3,  b : 2 }
复制代码

这会对后面的操做形成很大困扰!

第二,因为子类是经过原型 prototype 对父类的实例化实现的,因此在建立父类的时间,没法给父类传递参数,也就没法在实例化父类的时候对父类构造函数内部的属性进行初始化操做。

为了解决这些问题,那么就衍生出其余的继承方式。

构造函数继承 利用 call 这个方法能够改变函数的做用环境,在子类中调用这个方法,将子类中的变量在父类中执行一遍,因为父类中是给 this 绑定的, 所以子类也就继承了父类的实例属性,即:

//声明父类
    function Super (value) {
    	this.value = value;
    	this.superObject = {
    		a: 1,
    		b: 2
    	}
    }

    //为父类添加原型方法
    Super.prototype.showSuperObject = function () {
    	console.log(this.superValue);
    }

    //声明子类
    function Child (value) {
    	// 继承父类
        Super.call(this,value)
    }
    
    var child1 = new Child('Tom');
    var child2 = new Child('Jack');

    child1.superObject.a = 3 ;
    console.log(child1.superObject);    // { a : 3 , b : 2 }
    console.log(child1.value)           // Tom
    console.log(child2.superObject);    // { a : 1,  b : 2 }
    console.log(child2.value);          // Jack
复制代码

Super.call(this,value) 这段代码是构造函数继承的精华,这样就能够避免类式继承的问题了~

但这种继承方式没有涉及到原型 prototype , 因此父类的原型方法不会获得继承,而若是要想被子类继承,就必需要放到构造函数中,这样建立出来的每一个实例都会单独拥有一份,不能共用,为了解决这个问题,有了 组合式继承。

组合式继承

咱们只要在子类的构造函数做用环境中执行一次父类的构造函数,在将子类的原型 prorotype 对父类进行实例化一次,就能够实现 组合式继承 , 即:

//声明父类
    function Super (value) {
    	this.value = value;
    	this.superObject = {
    		a: 1,
    		b: 2
    	}
    }
    
    //为父类添加原型方法
    Super.prototype.showSuperObject = function () {
    	console.log(this.superObject);
    }
    
    //声明子类
    function Child (value) {
    	// 构造函数式继承父类 value 属性
        Super.call(this,value)
    }
    
    //类式继承
    Child.prototype = new Super();
    
    var child1 = new Child('Tom');
    var child2 = new Child('Jack');
    
    child1.superObject.a = 3 ;
    console.log(child1.showSuperObject());      // { a : 3 , b : 2 }
    console.log(child1.value)                   // Tom
    child1.superObject.b = 3 ;
    console.log(child2.showSuperObject());      // { a : 1,  b : 2 }
    console.log(child2.value);                  // Jack
复制代码

这样就能融合类式继承和构造函数继承的有点,而且过滤掉其缺点。 看起来是否是已经很完美了,NO , 细心的同窗能够发现,咱们在使用构造函数继承时执行了一遍父类的构造函数,而在实现子类原型的类式继承时又调用了一父类的构造函数,那么父类的构造函数执行了两遍,这一点是能够继续优化的。

寄生组合式继承

咱们上面学习了 组合式继承 ,也看出了这种方式的缺点,因此衍生出了 寄生组合式继承 ,其中 寄生 是寄生式继承 ,而寄生式继承依托于原型式继承,所以学习以前,咱们得了解一下 原型式继承寄生式继承

原型式继承跟类式继承相似,固然也存在一样的问题,代码以下:

//原型式继承
    function inheritObject (o) {
        // 声明一个过渡函数对象
        function F () {}
    	// 过渡对象的原型继承父对象
    	F.prototype = o;
        // 返回过渡对象的一个实例,该实例的原型继承了父对象
    	return new F();
    }
    var Super = {
    	name : 'Super' ,
        object : {
    		a : 1 ,
            b : 2
        }
    }
    var child1 = inheritObject(Super);
    var child2 = inheritObject(Super);
    console.log(child1.object) // { a : 1 , b : 2 }
    child1.object.a = 3 ;
    console.log(child2.object) // { a : 3 , b : 2 }
复制代码

寄生式继承是对原型继承的第二次封装,并在封装过程当中对对象进行了拓展,新对象就有了新增的属性和方法,实现方式以下:

//原型式继承
    function inheritObject (o) {
        // 声明一个过渡函数对象
        function F () {}
    	// 过渡对象的原型继承父对象
    	F.prototype = o;
        // 返回过渡对象的一个实例,该实例的原型继承了父对象
    	return new F();
    }
    
    // 寄生式继承
    // 声明基对象
    var Super = {
    	name : 'Super' ,
        object : {
    		a : 1 ,
            b : 2
        }
    }
    
    function createChild (obj) {
    	// 经过原型继承建立新对象
        var o = new inheritObject(obj);
        // 拓展新对象
        o.getObject = function () {
            console.log(this.object)
    	}
    	return o;
    }
复制代码

咱们将二者的特色结合起来就出现了寄生组合式继承,经过借用构造函数来继承属性,经过原型链的混成形式来继承方法,

/**
    * 寄生组合式继承
    * 传递参数
    *   childClass 子类
    *   superClass 父类
    * */
    
    //原型式继承
    function inheritObject (o) {
       // 声明一个过渡函数对象
       function F () {}
       // 过渡对象的原型继承父对象
       F.prototype = o;
       // 返回过渡对象的一个实例,该实例的原型继承了父对象
       return new F();
    }
    
    function inheritPrototype (childClass , superClass) {
        // 复制一份父类的原型保存在变量中
        var p = inheritObject(superClass.prototype);
        // 修复子类的 constructor
        p.constructor = childClass;
        // 设置子类的原型
        childClass.prototype = p;
    }
复制代码

咱们须要继承父类的原型,不须要在调用父类的构造函数,咱们只须要父类原型的一个副本,而这个副本咱们是能够经过原型继承拿到,若是直接赋值给子类对象,会致使子类的原型错乱,由于父类的原型对象复制到 P 中的 constructor 指向的不是子类的对象,因此经行了修正,并赋值给子类的原型,这样子类也就继承了父类的原型,可是没有执行父类的构造方法。

ok,测试一下:

// 定义父类
    function SuperClass (name) {
    	this.name = name;
    	this.object = {
    		a: 1,
    		b: 2
    	}
    }
    // 定义父类的原型
    SuperClass.prototype.showName = function () {
        console.log(this.name)
    }
    
    // 定义子类
    function ChildClass (name,age) {
        // 构造函数式继承
        SuperClass.call(this,name);
        // 子类新增属性
        this.age = age;
    }
    
    // 寄生式继承父类原型
    inheritPrototype(ChildClass,SuperClass);
    // 子类新增原型方法
    ChildClass.prototype.showAge = function () {
        console.log(this.age)
    }
    
    //
    var child1 = new ChildClass('Tom',24);
    var child2 = new ChildClass('Jack',25);
    
    console.log(child1.object)  // { a : 1 , b : 2 }
    child1.object.a = 3 ;
    console.log(child1.object)  // { a : 3 , b : 2 }
    console.log(child2.object)  // { a : 1 , b : 2 }
    
    console.log(child1.showName())  // Tom
    console.log(child2.showAge())   // 25
复制代码

如今没问题了哈,以前的问题也都解决了,大功告成~

多继承

JavaC++ 面向对象中会有多继承你的概念,可是 javascript 的继承是依赖原型链实现的,可是原型链只有一条,理论上是不能实现多继承的。可是咱们能够利用 javascript 的灵活性,能够经过继承多个对象的属性来实现相似的多继承。

首先,咱们来看一个比较经典的继承单对象属性的方法 —— extend

function extend (target,source) {
    //遍历源对象中的属性
    for( var property in source ){
    	//将源对象中的属性复制到目标对象
        target[property] = source[property]
    }
     // 返回目标对象
    return target;
}
复制代码

可是这个方法是一个浅复制过程,也就是说只能复制基本数据类型,对于引用类型的数据达不到预期效果,也会出现数据篡改的状况:

var parent = {
	name: 'super',
	object: {
		a: 1,
		b: 2
	}
}

var child = {
	age: 24
}

extend(child, parent);

console.log(child);     //{ age: 24, name: "super", object: { a : 1 , b : 2 } }
child.object.a = 3;
console.log(parent);    //{ name: "super", object: { a : 3 , b : 2 } }
复制代码

顺着这个思路,要实现多继承,就要将传入的多个对象的属性复制到源对象中,进而实现对多个对象的属性继承,咱们能够参考 jQuery 框架中的 extend 方法,对咱们上面的函数进行改造~

//判断一个对象是不是纯对象
function isPlainObject(obj) {
    var proto, Ctor;

    // (1) null 确定不是 Plain Object
    // (2) 使用 Object.property.toString 排除部分宿主对象,好比 window、navigator、global
    if (!obj || ({}).toString.call(obj) !== "[object Object]") {
        return false;
    }

    proto = Object.getPrototypeOf(obj);

    // 只有从用 {} 字面量和 new Object 构造的对象,它的原型链才是 null
    if (!proto) {
        return true;
    }

    // (1) 若是 constructor 是对象的一个自有属性,则 Ctor 为 true,函数最后返回 false
    // (2) Function.prototype.toString 没法自定义,以此来判断是同一个内置函数
    Ctor = ({}).hasOwnProperty.call(proto, "constructor") && proto.constructor;
    return typeof Ctor === "function" && Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object);
}

function extend() {
    var name, options, src, copy, clone, copyIsArray;
    var length = arguments.length;
    // 默认不进行深拷贝
    var deep = false;
    // 从第二个参数起为被继承的对象
    var i = 1;
    // 第一个参数不传布尔值的状况下,target 默认是第一个参数
    var target = arguments[0] || {};
    // 若是第一个参数是布尔值,第二个参数是 target
    if (typeof target == 'boolean') {
        deep = target;
        target = arguments[i] || {};
        i++;
    }
    // 若是target不是对象,咱们是没法进行复制的,因此设为 {}
    if (typeof target !== "object" && !( typeof target === 'function')) {
        target = {};
    }

    // 循环遍历要复制的对象们
    for (; i < length; i++) {
        // 获取当前对象
        options = arguments[i];
        // 要求不能为空 避免 extend(a,,b) 这种状况
        if (options != null) {
            for (name in options) {
                // 目标属性值
                src = target[name];
                // 要复制的对象的属性值
                copy = options[name];

                // 解决循环引用
                if (target === copy) {
                    continue;
                }

                // 要递归的对象必须是 plainObject 或者数组
                if (deep && copy && (isPlainObject(copy) ||
                    (copyIsArray = Array.isArray(copy)))) {
                    // 要复制的对象属性值类型须要与目标属性值相同
                    if (copyIsArray) {
                        copyIsArray = false;
                        clone = src && Array.isArray(src) ? src : [];

                    } else {
                        clone = src && isPlainObject(src) ? src : {};
                    }

                    target[name] = extend(deep, clone, copy);

                } else if (copy !== undefined) {
                    target[name] = copy;
                }
            }
        }
    }

    return target;
};
复制代码

该方法默认是浅拷贝,即:

var parent = {
    name: 'super',
    object: {
        a: 1,
        b: 2
    }
}
var child = {
    age: 24
}

extend(child,parent)
console.log(child); // { age: 24, name: "super", object: { a : 1 , b : 2 } }
child.object.a = 3;
console.log(parent) // { name: "super", object: { a : 3 , b : 2 } }
复制代码

咱们只须要将第一个参数传为 true , 就能够深复制了,即:

extend(true,child,parent)
    console.log(child); // { age: 24, name: "super", object: { a : 1 , b : 2 } }
    child.object.a = 3;
    console.log(parent) // { name: "super", object: { a : 1 , b : 2 } }
复制代码

ok~这些就是 javascript 中面向对象的一些知识,能仔细看到这里的小伙伴,相信大家对 javascript 中面向对象编程有了进一步的认识和了解,也为后面的设计模式的学习奠基了基础,接下来也会继续分享 javascript 中不一样的设计模式,欢迎喜欢的小伙伴持续关注~

相关文章
相关标签/搜索