《JavaScript模式》第6章 代码复用模式

@by Ruth92(转载请注明出处)

第6章:代码复用模式

GoF 在其著作中提出的有关创建对象的建议原则:

—— 优先使用对象组合,而不是类继承。

  • 传统模式:使用类继承;
  • 现代模式:“类式继承”,不以类的方式考虑。

代码重用才是最终目的,继承只是实现这一目标的方法之一。

☞ 使用类式继承时的预期结果
// 父构造函数
function Parent(name) {
  this.name = name || 'Adam';
}

Parent.prototype.say = function() {
  return this.name;
}

// 空白的子构造函数
function Child(name) {}

// 继承
inherit(Child, Parent);	// 需要自己实现

类式继承#1——默认模式

function inherit(C, P) {
  C.prototype = new P();
}

var kid = new Child();
kid.name = 'Patrick';
kid.name;   // 'Patrick'
kid.say();  // 'Adam'

需要记住:原型属性应该指向一个对象,而不是一个函数,因此它必须指向一个由父构造函数所创建的实例(一个对象),而不是指向构造函数本身。

缺点:1)同时继承了两个对象的属性,即添加到 this 的属性以及原型属性。在绝大多数时候,并不需要这些自身的属性,因为它们很可能是指向一个特定的实例,而不是复用。2)不支持参数传递。

6-2 调用继承函数后的原型链

类式继承#2——借用构造函数

// 父构造函数
function Parent(name) {
  this.name = name || 'Adam';
}

Parent.prototype.say = function() {
  return this.name;
}

// 空白的子构造函数
function Child(name) {
    Parent.apply(this, arguments);
}

var kid = new Child('Patrick');
kid.name;   // 'Patrick'
typeof kid.say;  // 'undefined'

优点:解决了从子构造函数到父构造函数的参数传递问题,可获得父对象自身成员的真实副本。

缺点:无法从原型中继承任何东西,并且原型也仅是添加可重用方法及属性的位置,它并不会为每个实例重新创建原型。

图6-4 借用构造函数时断开的原型链

☞ 通过借用构造函数实现多重继承

function Cat() {
    this.legs = 4;
    this.say = function() {
        return "meaowww";
    }
}

function Bird() {
    this.wings = 2;
    this.fly = true;
}

function CatWings() {
    Cat.apply(this);
    Bird.apply(this);
}

var jane = new CatWings();
console.log(jane);

类式继承#3——借用和设置原型

主要思想:结合前两种模式,先借用构造函数,然后设置子构造函数的原型使其指向一个构造函数创建的新实例。

function Chlid(name) {
  Parent.apply(this, arguments);
}

Child.prototype = new Parent(); // 第一次调用

var kid = new Chlid('Patrick'); // 第二次调用
kid.name; // 'Patrick'
kid.say();  // 'Patrick'
delete kid.name;
kid.say();  // 'Adam'

缺点:需要两次调用父构造函数(类式继承模式#3)。

类式继承#4——共享原型

【经验法则】:可复用成员应该转移到原型中而不是放置在 this 中。

因此,出于继承的目的,任何值得继承的东西都应该放置在原型中实现。所以,可以仅将子对象的原型与父对象的原型设置为相同的即可。

function inherit(C, P) {
  C.prototype = P.ptototype;
}

优点:不需要两次调用父构造函数。这种模式能够提供简短而迅速的原型链查询,这是由于所有的对象实际上共享了同一个原型。

缺点:如果在继承链下方的某处存在一个子对象或者孙子对象修改了原型,它将会影响到所有的父对象和祖先对象。

图6-7 共享同一个原型时对象之间的关系

类式继承#5——临时构造函数(代理函数或代理构造函数模式)

通过断开父对象与子对象的原型之间的直接链接关系,从而解决共享同一个原型所带来的问题,同时还能够继续受益于原型链带来的好处。

function inherit(C, P) {
	// 空白函数F():充当子对象和父对象之间的代理
	var F = function() {};
	F.prototype = P.prototype;
	C.prototype = new F();
	
	// 存储超类
	C.uber = P.ptototype;
	
	// 重置构造函数指针
	C.prototype.constructor = C;
}

var kid = new Child();

/**
 * 优化:避免在每次需要继承时都创建临时(代理)构造函数
 * 使用即时函数,并且在比闭包中存储代理函数
 */
var inherit = (function() {
	var F = function() {};

	return function(C, P) {
		F.prototype = P.prototype;
		C.prototype = new F();
		C.uber = P.prototype;	// 原始父对象的引用
		C.prototype.constructor = C;	// 重置构造函数的指针
	}
}());

图6-8 通过使用临时(代理)构造函数F()的类式继承

☞ Klass
var klass = function(Parent, props) {
  var Child, F, i;

  // 1. 新构造函数
  Child = function() {
    if (Child.uber && Child.uber.hasOwnProperty("__construct")) {
      Child.uber.__construct.apply(this, arguments);
    }
    if (Child.prototype.hasOwnProperty("__construct")) {
      Child.prototype.__construct.apply(this, arguments);
    }
  };

  // 2. 继承
  Parent = Parent || Object;
  F = function() {};
  F.prototype = Parent.prototype;
  Child.prototype = new F();
  Child.uber = Parent.prototype;
  Child.prototype.constructor = Child;

  // 3. 添加实现方法
  for (i in props) {
    if (props.hasOwnProperty) {
      Child.prototype[i] = props[i];
    }
  }

  // 返回该 'class'
  return Child;
}

// 测试
var Man = klass(null, {
  __construct: function(what) {
    console.log("Man's constructor");
    this.name = what;
  },
  getName: function() {
    return this.name;
  }
});

var SuperMan = klass(Man, {
  __construct: function(what) {
    console.log("SuperMan's constructor");
  },
  getName: function() {
    var name = SuperMan.uber.getName.call(this);
    return "I am " + name;
  }
});

var clark = new SuperMan('Clark Kent');
clark.getName();  // 'I am Clark Kent'
☞ 原型继承
/**
 * 使用字面量创建父对象
 */
var parent = {
  name: 'Papa'
};

function object(o) {
  function F() {};
  F.prototype = o;
  return new F();
}

// 新对象
var child = object(parent);

/**
 * 使用构造函数创建父对象
 */
function Person() {
  this.name = 'Adam';
}

Person.prototype.getName = function() {
  return this.name;
}

/**
 * 继承方法1
 */
var papa = new Person();
var kid = object(papa);
kid.getName();  // 'Adam'

/**
 * 继承方法2:
 * 仅继承现有构造函数的原型对象
 */
var kid2 = object(Person.prototype);

typeof kid.getName;   // 'function'
typeof kid.name;  // 'undefined'

/**
 * 继承方法3:
 * 使用 Object.create(),可以扩展新对象自身的属性,并返回该新对象
 */
var child = Object.create(parent);
var child2 = Object.create(parent, {
  age: {value: 2}
})
child2.hasOwnProperty('age'); // true

通过复制属性实现继承

由于 JavaScript 中的对象时通过引用传递的,在使用 浅复制 的时候,如果改变了子对象的属性,并且该属性恰好是一个对象,那么这种操作表示也正在修改父对象。

使用 深复制 可以创建对象的真实副本。jQuery库中的 extend() 可创建深度复制的副本。

/**
 * 浅复制
 */ 
function extend(parent, child) {
  var i;
  child = child || {};
  for (i in parent) {
    if (parent.hasOwnProperty(i)) {
      child[i] = parent[i];
    }
  }
  return child;
}

var dad = {
  counts: [1, 2, 3],
  reads: {paper: true}
};
var kid = extend(dad);
kid.counts.push(4);
dad.counts.toString();	// '1,2,3,4'
dad.reads === kid.reads;	// true

/**
 * 深复制
 */ 
function extendDeep(parent, child) {
  var i,
      toStr = Object.prototype.toString,
      astr = '[object Array]';

  child = child || {};

  for (i in parent) {
    if (parent.hasOwnProperty(i)) {
      if (typeof parent[i] === 'object') {
        child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
        extendDeep(parent[i], child[i]);
      } else {
        child[i] = parent[i];
      }
    }
  }
  return child;
}

// 测试
var dad = {
  counts: [1, 2, 3],
  reads: {paper: true}
};
var kid = extendDeep(dad);

kid.counts.push(4);
kid.counts.toString();  // '1,2,3,4'
dad.counts.toString();  // '1,2,3'

dad.reads === kid.reads;	// false
kid.reads.paper = false;

kid.reads.web = true;
dad.reads.paper;	// true
☞ 混入——针对通过属性复制实现继承的思想做的进一步扩展

mix-in 模式并不是复制一个完整的对象,而是从多个对象中复制出任意的成员并将这些成员组合成一个新的对象。

实现:遍历每个参数,并复制出传递给该函数的每个对象中的每个属性。

function mix() {
  var arg, prop, child = {};
  for (arg = 0; arg < arguments.length; arg += 1) {
    for (prop in arguments[arg]) {
      if (arguments[arg].hasOwnProperty(prop)) {
        child[prop] = arguments[arg][prop];
      }
    }
  }
  return child;
}

var cake = mix(
  {egg: 2, large: true},
  {butter: 1, salted: true},
  {flour: '3 cups'},
  {sugar: 'sure!'}
);
☞ 借用方法
/**
 * 借用数组的方法
 */
function f() {
  var args = [].slice.call(arguments, 1, 3);
  // 或者
  // var args = Array.prototype.slice.call(arguments, 1, 3);
  return args;
}

f(1, 2, 3, 4, 5, 6);  // [2, 3] 

/**
 * 借用和绑定
 */
var one = {
  name: 'object',
  say: function(greet) {
    return greet + ', ' + this.name;
  }
};

one.say('hi');  // 'hi, object'

var two = {
  name: 'another object'
};

one.say.apply(two, ['hello']);  // 'hello, another object'
one.say.call(two, 'bye'); // "bye, another object"
var say = one.say.bind(two, 'bind');
say();  // "bind, another object"

/**
 * this都指向了全局对象
 */
// 给变量赋值
// `this` 将指向全局变量
var say = one.say;
say('hoho'); // 'hoho, '

// 作为回调函数
var yetanother = {
  name: 'Yet another object',
  method: function(callback) {
    return callback('Hola');
  }
};
yetanother.method(one.say); // 'Hola, '

/**
 * 解决办法:bind()函数
 */
function bind(o, m) {
  return function() {
    return m.apply(o, [].slice.call(arguments));
  };
}

var twosay = bind(two, one.say);
twosay('yo'); // 'yo, another object';

/**
 * Function.prototype.bind()实现
 */
if (typeof Function.prototype.bind === 'undefined') {
  Function.prototype.bind = function(thisArg) {
    var fn = this, 
        slice = Array.prototype.slice,
        args = slice.call(arguments, 1);

    return function() {
      return fn.apply(thisArg, args.concat(slice.call(arguments)));
    };
  };
}

var twosay2 = one.say.bind(two);
twosay2('Bonjour'); // 'Bonjour, another object'

var twosay3 = one.say.bind(two, 'Enchante');
twosay3(); // 'Enchante, another object'

原文地址:https://www.cnblogs.com/Ruth92/p/5937945.html