10种JavaScript代码复用模式

web前端开发

共 11267字,需浏览 23分钟

 ·

2020-09-25 10:00

作者 | 汤姆大叔

前言
任何编程都提出代码复用,否则话每次开发一个新程序或者写一个新功能都要全新编写的话,那就歇菜了。
但是代码复用也是有好要坏,今天这篇文章我们将针对代码复用来进行讨论,里面一种介绍了10种模式,其中前面4种模式是推荐使用的,是推荐大家使用的模式,一般不会有什么问题。
后面6种模式是要尽量避免使用的,因为后面6种模式或多或少会带来一些问题。
下面我们就进入今天的内容吧。

模式1:原型继承

原型继承是让父对象作为子对象的原型,从而达到继承的目的:
function object(o) { function F() { }
F.prototype = o; return new F();}
// 要继承的父对象var parent = { name: "Papa"};
// 新对象var child = object(parent);
// 测试console.log(child.name); // "Papa"

// 父构造函数function Person() { // an "own" property this.name = "Adam";}// 给原型添加新属性Person.prototype.getName = function () { return this.name;};// 创建新personvar papa = new Person();// 继承var kid = object(papa);console.log(kid.getName()); // "Adam"

// 父构造函数function Person() { // an "own" property this.name = "Adam";}// 给原型添加新属性Person.prototype.getName = function () { return this.name;};// 继承var kid = object(Person.prototype);console.log(typeof kid.getName); // "function",因为是在原型里定义的console.log(typeof kid.name); // "undefined", 因为只继承了原型
同时,ECMAScript5也提供了类似的一个方法叫做Object.create用于继承对象,用法如下:
/* 使用新版的ECMAScript 5提供的功能 */var child = Object.create(parent);
var child = Object.create(parent, { age: { value: 2} // ECMA5 descriptor});console.log(child.hasOwnProperty("age")); // true
而且,也可以更细粒度地在第二个参数上定义属性:
// 首先,定义一个新对象manvar man = Object.create(null);
// 接着,创建包含属性的配置设置// 属性设置为可写,可枚举,可配置var config = { writable: true, enumerable: true, configurable: true};
// 通常使用Object.defineProperty()来添加新属性(ECMAScript5支持)// 现在,为了方便,我们自定义一个封装函数var defineProp = function (obj, key, value) { config.value = value; Object.defineProperty(obj, key, config);}
defineProp(man, 'car', 'Delorean');defineProp(man, 'dob', '1981');defineProp(man, 'beard', false);
所以,继承就这么可以做了:
var driver = Object.create( man );defineProp (driver, 'topSpeed', '100mph');driver.topSpeed // 100mph
但是有个地方需要注意,就是Object.create(null)创建的对象的原型为undefined,也就是没有toStringvalueOf方法,所以alert(man);的时候会出错,但alert(man.car);是没问题的。

模式2:复制所有属性进行继承

这种方式的继承就是将父对象里所有的属性都复制到子对象上,一般子对象可以使用父对象的数据。
先来看一个浅拷贝的例子:
/* 浅拷贝 */function extend(parent, child) { var i; child = child || {}; for (i in parent) { if (parent.hasOwnProperty(i)) { child[i] = parent[i]; } } return child;}
var dad = { name: "Adam" };var kid = extend(dad);console.log(kid.name); // "Adam"
var dad = { counts: [1, 2, 3], reads: { paper: true }};var kid = extend(dad);kid.counts.push(4);console.log(dad.counts.toString()); // "1,2,3,4"console.log(dad.reads === kid.reads); // true
代码的最后一行,你可以发现dad和kid的reads是一样的,也就是他们使用的是同一个引用,这也就是浅拷贝带来的问题。
我们再来看一下深拷贝:
/* 深拷贝 */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);console.log(kid.counts.toString()); // "1,2,3,4"console.log(dad.counts.toString()); // "1,2,3"
console.log(dad.reads === kid.reads); // falsekid.reads.paper = false;
深拷贝以后,两个值就不相等了,bingo!

模式3:混合(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( { eggs: 2, large: true }, { butter: 1, salted: true }, { flour: '3 cups' }, { sugar: 'sure!' } );
console.dir(cake);
mix函数将所传入的所有参数的子属性都复制到child对象里,以便产生一个新对象。
那如何我们只想混入部分属性呢?该个如何做?其实我们可以使用多余的参数来定义需要混入的属性,例如mix(child,parent,method1,method2)这样就可以只将parent里的method1和method2混入到child里。上代码:
// Car var Car = function (settings) { this.model = settings.model || 'no model provided'; this.colour = settings.colour || 'no colour provided';};
// Mixinvar Mixin = function () { };Mixin.prototype = { driveForward: function () { console.log('drive forward'); }, driveBackward: function () { console.log('drive backward'); }};

// 定义的2个参数分别是被混入的对象(reciving)和从哪里混入的对象(giving)function augment(receivingObj, givingObj) { // 如果提供了指定的方法名称的话,也就是参数多余3个 if (arguments[2]) { for (var i = 2, len = arguments.length; i < len; i++) { receivingObj.prototype[arguments[i]] = givingObj.prototype[arguments[i]]; } } // 如果不指定第3个参数,或者更多参数,就混入所有的方法 else { for (var methodName in givingObj.prototype) { // 检查receiving对象内部不包含要混入的名字,如何包含就不混入了 if (!receivingObj.prototype[methodName]) { receivingObj.prototype[methodName] = givingObj.prototype[methodName]; } } }}
// 给Car混入属性,但是值混入'driveForward' 和 'driveBackward'*/augment(Car, Mixin, 'driveForward', 'driveBackward');
// 创建新对象Carvar vehicle = new Car({ model: 'Ford Escort', colour: 'blue' });
// 测试是否成功得到混入的方法vehicle.driveForward();vehicle.driveBackward();
该方法使用起来就比较灵活了。

模式4:借用方法

一个对象借用另外一个对象的一个或两个方法,而这两个对象之间不会有什么直接联系。不用多解释,直接用代码解释吧:
var one = { name: 'object', say: function (greet) { return greet + ', ' + this.name; }};
// 测试console.log(one.say('hi')); // "hi, object"
var two = { name: 'another object'};
console.log(one.say.apply(two, ['hello'])); // "hello, another object"
// 将say赋值给一个变量,this将指向到全局变量var say = one.say;console.log(say('hoho')); // "hoho, undefined"
// 传入一个回调函数callbackvar yetanother = { name: 'Yet another object', method: function (callback) { return callback('Hola'); }};console.log(yetanother.method(one.say)); // "Holla, undefined"
function bind(o, m) { return function () { return m.apply(o, [].slice.call(arguments)); };}
var twosay = bind(two, one.say);console.log(twosay('yo')); // "yo, another object"

// ECMAScript 5给Function.prototype添加了一个bind()方法,以便很容易使用apply()和call()。
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);console.log(twosay2('Bonjour')); // "Bonjour, another object"
var twosay3 = one.say.bind(two, 'Enchanté');console.log(twosay3()); // "Enchanté, another object"

模式5:默认模式

代码复用大家常用的默认模式,往往是有问题的,该模式使用Parent()的构造函数创建一个对象,并且将该对象赋值给Child()的原型。我们看一下代码:
function inherit(C, P) { C.prototype = new P();}
// 父构造函数function Parent(name) { this.name = name || 'Adam';}// 给原型添加say功能Parent.prototype.say = function () { return this.name;};// Child构造函数为空function Child(name) {}
// 执行继承inherit(Child, Parent);
var kid = new Child();console.log(kid.say()); // "Adam"
var kiddo = new Child();kiddo.name = "Patrick";console.log(kiddo.say()); // "Patrick"
// 缺点:不能让参数传进给Child构造函数var s = new Child('Seth');console.log(s.say()); // "Adam"
这种模式的缺点是Child不能传进参数,基本上也就废了。

模式6:借用构造函数

该模式是Child借用Parent的构造函数进行apply,然后将child的this和参数传递给apply方法:
// 父构造函数function Parent(name) { this.name = name || 'Adam';}
// 给原型添加say功能Parent.prototype.say = function () { return this.name;};
// Child构造函数function Child(name) { Parent.apply(this, arguments);}
var kid = new Child("Patrick");console.log(kid.name); // "Patrick"
// 缺点:没有从构造函数上继承say方法console.log(typeof kid.say); // "undefined"
缺点也很明显,say方法不可用,因为没有继承过来。

模式7:借用构造函数并设置原型

上述两个模式都有自己的缺点,那如何把两者的缺点去除呢,我们来尝试一下:
// 父构造函数function Parent(name) { this.name = name || 'Adam';}
// 给原型添加say功能Parent.prototype.say = function () { return this.name;};
// Child构造函数function Child(name) { Parent.apply(this, arguments);}
Child.prototype = new Parent();
var kid = new Child("Patrick");console.log(kid.name); // "Patrick"console.log(typeof kid.say); // functionconsole.log(kid.say()); // Patrickconsole.dir(kid);delete kid.name;console.log(kid.say()); // "Adam"
运行起来,一切正常,但是有没有发现,Parent构造函数执行了两次,所以说,虽然程序可用,但是效率很低。

模式8:共享原型

共享原型是指Child和Parent使用同样的原型,代码如下:
function inherit(C, P) { C.prototype = P.prototype;}
// 父构造函数function Parent(name) { this.name = name || 'Adam';}
// 给原型添加say功能Parent.prototype.say = function () { return this.name;};
// Child构造函数function Child(name) {}
inherit(Child, Parent);
var kid = new Child('Patrick');console.log(kid.name); // undefinedconsole.log(typeof kid.say); // functionkid.name = 'Patrick';console.log(kid.say()); // Patrickconsole.dir(kid);
确定还是一样,Child的参数没有正确接收到。

模式9:临时构造函数

首先借用构造函数,然后将Child的原型设置为该借用构造函数的实例,最后恢复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; }} ());
function Parent(name) { this.name = name || 'Adam';}
// 给原型添加say功能Parent.prototype.say = function () { return this.name;};
// Child构造函数function Child(name) {}
inherit(Child, Parent);
var kid = new Child();console.log(kid.name); // undefinedconsole.log(typeof kid.say); // functionkid.name = 'Patrick';console.log(kid.say()); // Patrickvar kid2 = new Child("Tom");console.log(kid.say()); console.log(kid.constructor.name); // Childconsole.log(kid.constructor === Parent); // false
问题照旧,Child不能正常接收参数。

模式10: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(i)) { Child.prototype[i] = props[i]; } }
// return the "class" return Child;};
var Man = klass(null, { __construct: function (what) { console.log("Man's constructor"); this.name = what; }, getName: function () { return this.name; }});
var first = new Man('Adam'); // logs "Man's constructor"first.getName(); // "Adam"
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"
console.log(clark instanceof Man); // trueconsole.log(clark instanceof SuperMan); // true
怎么样?看着是不是有点晕,说好点,该模式的语法和规范拧得和别的语言一样,你愿意用么?咳。。。

总结

以上就是我要给大家分享的10个代码复用模式,前面4种模式推荐使用。后面6种模式尽量避免使用。虽然后面6种模式,在某种特殊情况下实现了某些功能,但是都存在各自的缺点,所以一般情况,请大家要避免使用。
本文完~
参考:http://shichuan.github.com/javascript-patterns/#code-reuse-patterns

浏览 33
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报