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

·  阅读 5063

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

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

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

封装

  • 创建一个类

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 访问得到,每次创建新对象时这些属性和方法是不会被再次创建的,如下图所示:

其中 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 中不同的设计模式,欢迎喜欢的小伙伴持续关注~