本文介紹的四種代碼復用模式都是最佳實踐,推薦大家在編程的過程中使用。
原型繼承是讓父對象作為子對象的原型,從而達到繼承的目的:
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; }; // 創(chuàng)建新person var 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
而且,也可以更細粒度地在第二個參數上定義屬性:
// 首先,定義一個新對象man var man = Object.create(null); // 接著,創(chuàng)建包含屬性的配置設置 // 屬性設置為可寫,可枚舉,可配置 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)創(chuàng)建的對象的原型為undefined,也就是沒有toString和valueOf方法,所以alert(man);的時候會出錯,但alert(man.car);是沒問題的。
這種方式的繼承就是將父對象里所有的屬性都復制到子對象上,一般子對象可以使用父對象的數據。
先來看一個淺拷貝的例子:
/ 淺拷貝 / 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
代碼的最后一行,你可以發(fā)現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); // false kid.reads.paper = false;
深拷貝以后,兩個值就不相等了,bingo!
混入就是將一個對象的一個或多個(或全部)屬性(或方法)復制到另外一個對象,我們舉一個例子:
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'; }; // Mixin var 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'); // 創(chuàng)建新對象Car var vehicle = new Car({ model: 'Ford Escort', colour: 'blue' }); // 測試是否成功得到混入的方法 vehicle.driveForward(); vehicle.driveBackward();
該方法使用起來就比較靈活了。
一個對象借用另外一個對象的一個或兩個方法,而這兩個對象之間不會有什么直接聯(lián)系。不用多解釋,直接用代碼解釋吧:
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" // 傳入一個回調函數callback var 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"
就不用總結了吧。
更多建議: