林秀栋的技术博客

设计模式(一) 面向对象

创建对象

var a = function(){
	return {
		b: function(){console.log('this is b in a');},
		c: function(){}
	}
}
var k = a();
k.b();
//上面这个方法虽然可以调用,但创建的对象k和对象a没有关系(返回出来的对象本身和a对象无关)


//所以可以用类的方法来创建,如下(需要new)
var a2 = function(){
	this.b = function(){console.log('this is b in a2');}
	this.c = function(){}
}
var k2 = new a2();
k2.b();
//每次new新对象时,都会复制一套this的属性,这样每个方法都会复制一遍,浪费大


//可以用原型,如下
var a3 = function(){}
a3.prototype = {
	b: function(){console.log('this is b in a3');},
	c: function(){}
}
var k3 = new a3();
k3.b();
//在b、c最后return this,可实现链式调用


//扩展原生对象(Function,Array等)
Function.prototype.b = function(){console.log('this is b in Function');}
var f = function(){};	//或var f = new Function()
f.b();
//但这样会污染原生对象,别人创建的函数也会被自己创建的函数污染


//可以抽象出一个统一添加方法的功能方法
Function.prototype.addMethod = function(name, fn){
	this[name] = fn;
	return this;
}
//然后通过其来添加方法
var f2 = function(){};	//或var f2 = new Function(){};
f2.addMethod('b', function(){console.log('this is b in Function by add');return this;})
	.addMethod('c', function(){console.log('this is c in Function by add');return this;})
f2.b().c();
//用上面的方法,只会给f2添加b方法,如果另外创建f3也不会有b方法


//下面f3的b方法是Function.prototype.b中扩展的,如果没有这句
//由于addMethod不会污染f3,下面运行会报错
var f3=function(){}
f3.b();


//上面是函数式编程的写法,下面是类式调用的写法
Function.prototype.addMethod = function(name, fn){
	this.prototype[name] = fn;	//这里不同
	return this;
}
var f4 = function(){};	//或var f4 = new Function(){};
f4.addMethod('b', function(){console.log('this is b in Function by add for new');return this;})
	.addMethod('c', function(){console.log('this is c in Function by add for new');return this;})
var ff4 = new f4();	//因为涉及到原型链,需要new
ff4.b().c();

面向对象

下面介绍了封装、继承、多态

//封装
var man = function (name) {
  this.name = name; //this是函数内部自带的变量,指向当前对象,在new中的构造函数中需要用this
};
man.prototype = {
  getName: function () {
    console.log(this.name);
  },
};
var bob = new man("Bob");
bob.getName();
console.log(bob.name);
//this的属性在每次new时都会得到创建,而prototype中的方法和属性不会再次创建,而是通过指向访问到
//prototype对象会像函数中创建this一样创建一个constructor属性,该属性指向拥有整个原型对象的属性或对象,如上面就是指向man
//new出的对象(如这里的bob),也会有个属性__proto__,指向类的原型所指向的对象(这里是man.prototype)

var man2 = function (age) {
  var myAge = age; //没有this的为私有属性,外界访问不到
  //这里方法没放在prototype中,因为原型中也无法访问到私有属性
  //只有在里面可以访问,这种方法称为特权方法
  this.getAge = function () {
    console.log(myAge);
  };
};
//new时是对this的不断赋值,在外面用点语法增加的属性不会加到new出的新对象里
//prototype可以是因为新对象的__proto__和类的prototype指向同一对象
//比如下面的k只能用man2.k访问,在bob2中不存在
man2.k = true;
var bob2 = new man2(18);
bob2.getAge();
console.log(bob2.k); //undefined
console.log(man2.k); //true

//闭包
var man3 = (function () {
  var sex = 0; //闭包中的sex因为不在返回的函数中,无论是否有new外界无法访问(静态私有变量)
  //下面的和普通构造函数相同,也可以把prototype写闭包外面,即man3.prototype
  //但写在里面更像一个整体
  function MAN(age) {
    var k = 1;
    this.age = age;
  }
  MAN.prototype = {};
  return MAN;
})();

//如果使用一个构造函数时忘记new,而是直接赋值使用,里面的this会指向window,这时将取不到里面的属性
//可以用以下安全模式
var man4 = function (a) {
  if (this instanceof man4) {
    this.a = a;
  } else {
    return new man4(a);
  }
};
var m4 = man4(1); //如果没有上面的安全模式,这里不new将导致m4.a没有值,值在window.a

//继承

//类式继承
function a() {
  this.a = 1;
  this.b = [1, 2];
}
a.prototype = {}; //为父元素添加共有方法
function b() {}
b.prototype = new a();
b.prototype.constructor = b;
var kk1 = new b();
var kk2 = new b();
kk1.a = 2;
kk1.b.push(3);
console.log("kk1.a: " + kk1.a); //2
console.log("kk1.b: " + kk1.b); //1,2,3
console.log("kk2.a: " + kk2.a); //1
console.log("kk2.b: " + kk2.b); //1,2,3
//但这种方法里如果有引用类型,改变时会相互影响;且在使用时无法向父类传递参数
//为解决,可以使用构造函数继承
function a2(num) {
  this.a = [1, 2, num];
}
function b2(num) {
  a2.call(this, num);
}
var kkk1 = new b2(3);
var kkk2 = new b2(3);
kkk1.a.push(4);
console.log("kkk1.a: " + kkk1.a); //1,2,3,4
console.log("kkk2.a: " + kkk2.a); //1,2,3
//这样引用类型就不会互相影响,但没有prototype,此时父类的原型不会被子类继承,如果放在构造函数中,不符合代码复用原则
//所以出现了组合继承
function a3(index) {
  this.arr = [1, 2, index];
  console.log("看看运行了几次——组合继承");
}
a3.prototype = {};
function b3(index) {
  a3.call(this, index);
}
b3.prototype = new a3(); //类式继承
b3.prototype.constructor = b3;
b3.prototype.funA = function () {};
var kkkk1 = new b3(6);
var kkkk2 = new b3(7);
kkkk1.arr.push("x");
console.log(kkkk1.arr); //1,2,6,x
console.log(kkkk2.arr); //1,2,7
//这时引用类型不会相互影响,也可以使用父类的原型方法
//但我们会发现,'看看运行了几次'输出了3次,分别是call和两次new a3(),造成资源浪费
//于是有下面的另一种继承方式

//原型式继承
function inheritObject(o) {
  function F() {} //过渡函数对象
  F.prototype = o;
  return new F();
}
var book = {
  k: [1, 2, 3],
};
var book1 = inheritObject(book);
var book2 = inheritObject(book);
//上面相当于对类式继承的封装,也会有类式继承的问题:引用类型相互影响
//在此基础上,产生了寄生式继承
function createBook(obj) {
  var o = new inheritObject(obj); //原型继承创建对象
  o.getName = function () {}; //扩展新对象
  return o;
}
//这是对原型继承的二次封装,在其中对继承的对象进行扩展
//在这个思想上出现了最终的寄生组合式继承
function inheritPrototype(a, b) {
  //a子类,b父类
  //复制一份父类的原型副本保存在变量
  var p = inheritObject(b.prototype);
  //修正因为重写子类原型导致子类的constructor属性被修改
  p.constructor = a;
  //设置子类原型
  a.prototype = p;
}
//因为父类的构造函数在构造函数继承时就已经获取,所以这里不再获取
//而只获取父类的原型
function fa(name) {
  this.c = [1, 2, name];
  console.log("看看运行了几次——寄生组合式继承");
}
fa.prototype = {};
function ch(name) {
  fa.call(this, name);
}
inheritPrototype(ch, fa);
ch.prototype.aa = function () {};
var in1 = new ch(1);
var in2 = new ch(2);
//这样引用类型不会相互影响,父类也只调用时运行了两次
//但有个缺陷是给子类添加原型方法只能通过点语法ch.prototype.xx一个个添加,直接用赋值对象的方法会覆盖掉从父类原型继承的对象

//多继承
//由于js的继承是通过原型链实现的,但原型链只有一条,理论上无法实现对多个父类的继承
//但可以通过其他方法继承多个对象的属性来实现类似多继承
//这里把函数绑定到原生对象上,这样对象也就有这一方法
Object.prototype.mix = function () {
  var i = 0,
    len = arguments.length,
    arg;
  for (; i < len; i++) {
    //缓存当前对象
    arg = arguments[i];
    for (var property in arg) {
      this[property] = arg[property];
    }
  }
};
var sa = {},
  sb = {},
  sc = {};
sa.mix(sb, sc);

//多态
//普通写法就是感觉参数switch,下面是面向对象的类形式的写法
function Add() {
  function a() {}
  function b() {}
  this.add = function () {
    var arg = arguments;
    var len = arg.length;
    switch (len) {
      case 0:
        return a();
      case 1:
        return b(1);
    }
  };
}
var A = new Add();
A.add(1, 2);

再谈寄生组合式继承

上面寄生组合式基础看的还是比较懵,这里再贴上《javascript 高级程序设计》的实现

其实和上面的差不多,主要说明文字不太一样

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}
//上面的函数得到的对象F,拥有了对象o的全部属性(在原型链上),而修改F的属性,不会影响到o,相当于把o复制了一份。
//使用object方法,就可以将Super.prototype的属性「复制」到Sub.prototype上了,当然这儿还需要修正一下constructor的指向。

function inheritPrototype(subType, superType){
	var o = object(superType.prototype); //创建对象
	o.constructor = subType; //增强对象
	subType.prototype = o; //指定对象
}
/*
    这个示例中的 inheritPrototype()函数实现了寄生组合式继承的最简单形式。
    这个函数接收两个参数:子类型构造函数和超类型构造函数。
    在函数内部,第一步是创建超类型原型的一个副本。
    第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。
    最后一步,将新创建的对象(即副本)赋值给子类型的原型。
    这样,我们就可以用调用 inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了
*/


function SuperType(name){
	this.name = name;
	this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
	alert(this.name);
};
function SubType(name, age){
	SuperType.call(this, name);

	this.age = age;
}
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function(){
	alert(this.age);
};

var k = new SubType();

为什么要改变 constructor 的指向

下面是一个使用 prototype 模式的继承

如果”猫”的 prototype 对象,指向一个 Animal 的实例,那么所有”猫”的实例,就能继承 Animal 了。

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物

代码的第一行,我们将 Cat 的 prototype 对象指向一个 Animal 的实例。

它相当于完全删除了 prototype 对象原先的值,然后赋予一个新值。

关键在第二行

原来,任何一个 prototype 对象都有一个 constructor 属性,指向它的构造函数。如果没有”Cat.prototype = new Animal();”这一行,Cat.prototype.constructor 是指向 Cat 的;加了这一行以后,Cat.prototype.constructor 指向 Animal。

alert(Cat.prototype.constructor == Animal); //true

更重要的是,每一个实例也有一个 constructor 属性,默认调用 prototype 对象的 constructor 属性。

alert(cat1.constructor == Cat.prototype.constructor); // true

因此,在运行”Cat.prototype = new Animal();”这一行之后,cat1.constructor 也指向 Animal!

alert(cat1.constructor == Animal); // true

这显然会导致继承链的紊乱(cat1 明明是用构造函数 Cat 生成的),因此我们必须手动纠正,将 Cat.prototype 对象的 constructor 值改为 Cat。这就是第二行的意思。

这是很重要的一点,编程时务必要遵守。下文都遵循这一点,即如果替换了 prototype 对象,

o.prototype = {};

那么,下一步必然是为新的 prototype 对象加上 constructor 属性,并将这个属性指回原来的构造函数。

o.prototype.constructor = o;