Ext.apply & Ext.Object.merge & Ext.clone
前两天写Ext的时候,碰到对象引用的问题,本想Ext有自己的拷贝对象的方法,Ext.apply(),那就用呗~~ 然,问题依然存在啊。于是,猜想:Ext.apply不能拷贝深层对象,深层对象依然是引用。
先看源码:
/** * Copies all the properties of config to the specified object. * Note that if recursive merging and cloning without referencing the original objects / arrays is needed, use * {@link Ext.Object#merge} instead. * @param {Object} object The receiver of the properties * @param {Object} config The source of the properties * @param {Object} [defaults] A different object that will also be applied for default values * @return {Object} returns obj */ Ext.apply = function(object, config, defaults) { if (defaults) { Ext.apply(object, defaults); } if (object && config && typeof config === 'object') { var i, j, k; for (i in config) { object[i] = config[i]; // 复制对象,这里显然是浅复制。 } if (enumerables) { for (j = enumerables.length; j--;) { k = enumerables[j]; if (config.hasOwnProperty(k)) { object[k] = config[k]; } } } } return object; };
嗯,果然,人家已经写的很清楚了:如果不想引用原对象或数组,可以使用 Ext.Object.merge方法(还是看源码好啊!!),一下就找到问题的根源了。
源码里面还有个新鲜的玩意儿:enumerables 这是什么呢? 看源码:
enumerables = ['hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'constructor']; /** * An array containing extra enumerables for old browsers * @property {String[]} */ Ext.enumerables = enumerables;
'old browsers' 很明显嘛,兼容ie6用的。但是如果还不明白为什么要兼容ie6,那就先用ie6测试一下吧,
var a = { years: [20013, 20012], toString: 'this year is 2013' } var b = {}; Ext.apply(b, a); if(b.hasOwnProperty('toString')){ alert(1); }else{ alert(2); }
// 在ie6下,弹出 2
// ie7,ie8,ie9 及 chrome firefox弹出的都是 1
// 即 在ie6下,对象包含的像 toString 这样的原生属性不会被拷贝,也不能被 for in 遍历到
在ie6下,复制对象后,不会将enumerables列举的这些属性复制到目标对象中,因此这里需要手动添加!(万恶的ie6啊)。
题外话,hasOwnProperty() 这个方法只会检查该对象本身的属性,不会检查该对象的原型链中是否具有该属性。如果需要检查一个对象的原型链上是否包含了另一个对象,可以使用isPrototypeOf方法。
下面我们再看下Ext.Object.merge
/** @param {Object} destination The object into which all subsequent objects are merged. * @param {Object...} object Any number of objects to merge into the destination. * @return {Object} merged The destination object with all passed objects merged in. */ merge: function(destination) { var i = 1, ln = arguments.length, mergeFn = ExtObject.merge, // ExtObject = Ext.Object 这里指代 this cloneFn = Ext.clone, // 真正干活的函数 object, key, value, sourceKey; for (; i < ln; i++) { object = arguments[i]; for (key in object) { value = object[key]; if (value && value.constructor === Object) { // 判断是否是深层对象 sourceKey = destination[key];
// 判断是复制还是合并 if (sourceKey && sourceKey.constructor === Object) { mergeFn(sourceKey, value); // 合并 递归 } else { destination[key] = cloneFn(value); // 复制 } } else { destination[key] = value; } } } return destination; }
其实,看到这里,才发现原来真正干活的是Ext.clone(),
/** * Clone simple variables including array, {}-like objects, DOM nodes and Date without keeping the old reference. * A reference for the object itself is returned if it's not a direct decendant of Object. For model cloning, * see {@link Ext.data.Model#copy Model.copy}. * * @param {Object} item The variable to clone * @return {Object} clone */ clone: function(item) { var type, i, j, k, clone, key; if (item === null || item === undefined) { return item; } // DOM nodes // TODO proxy this to Ext.Element.clone to handle automatic id attribute changing // recursively if (item.nodeType && item.cloneNode) { return item.cloneNode(true); } type = toString.call(item); // 这里 toString = Object.prototype.toString
// 这里是一个细节的地方
// String.prototype.toString 和 Array.prototype.toString 都重写了 Object.prototype.toString 的方法
// So, 这两个其实是不一样的方法.
// String.prototype.toString.call('str') -> 'str'
// Array.prototype.toString.call('str') -> '[object String]'
// Array.prototype.toString.call([1,2]) -> '1,2'
// Object.prototype.toString.call('str') -> '[object String]'
// So 这个方法其实是在变相的判断数据类型 !!! 又是一个小技巧 MARK
// Date if (type === '[object Date]') { return new Date(item.getTime()); } // Array if (type === '[object Array]') { i = item.length; clone = []; while (i--) { clone[i] = Ext.clone(item[i]); } } // Object else if (type === '[object Object]' && item.constructor === Object) { clone = {}; for (key in item) { clone[key] = Ext.clone(item[key]); } if (enumerables) { for (j = enumerables.length; j--;) { k = enumerables[j]; clone[k] = item[k]; } } } return clone || item; }
Ext为复制对象还提供了一个十分实用的函数 Ext.applyIf(),这个方法只拷贝源对象中存在,目标对象中不存在的属性。
/** * Copies all the properties of config to object if they don't already exist. * @param {Object} object The receiver of the properties * @param {Object} config The source of the properties * @return {Object} returns obj */ applyIf: function(object, config) { var property; if (object) { for (property in config) { if (object[property] === undefined) { //目标对象中不存在该属性 object[property] = config[property]; } } } return object; }
嗯,到这里,整个Ext复制对象的过程已然明了。花花再也不用担心对象引用的问题啦!