浅拷贝与深拷贝
浅拷贝
function extend(target, options) {
for(name in options) {
target[name] = options[name];
}
}
如果对象是引用,则两者互通,改一个,另一个也跟着变:
var target = {};
var options = {
a: {a: 1, b: 2, c: 3},
b: 2,
c: 3
};
extend(target, options);
target.a.a = 12;
console.log(options);
深拷贝
function extend(target, options) {
for(name in options) {
if(isPlainObject(options[name]))
target[name] = extend(target[name], options[name]);
else
target[name] = options[name];
}
return target;
}
判断其是不是一个纯粹的对象(通过 {} 或者 new Object 创建的),如果是,则递归拷贝。
纯粹对象
通用的判断数据类型的方式 Object.prototype.toString.call() 判断是否是 [object Object],调用原型方法最简单的方式就是实例化后再调用:
var object = new Object();
object.toString.call({});
而 new Object() 跟 {} 又算是同一回事儿,下面这个代码也应该是正确的:
{}.toString.call({});
实际上,这个跑不通,js 把行首的 {} 当作空语句块来处理了,必须得这样:
({}).toString.call({});
最简单的能识别出的就是没有原型的对象,单纯的只是存储数据,没有原型链,而Object.getPrototypeOf(object) 可以返回对象的原型:
var object1 = Object.create(null);
console.log(Object.getPrototypeOf(object1));
var object2 = {};
console.log(Object.getPrototypeOf(object2));
而我们自定义的构造函数,也会同样返回 [object Object],这并不是我们想要的纯粹对象:
function A(){}
var a = new A();
({}).toString.call(a);
要把这一类的都排除出去:
Object.toString();
Object.toString.call(function(){});
可以看出两者生成的字符串并不一致,所以只要比较下字符串就可以看出是由 Object 直接实例化的对象还是通过其它自定义行为实例化的,而通过实例化对象找出构造函数可以通过 constructor:
Object.toString.call(Object.getPrototypeOf({}).constructor);
实际在 jQuery 源码中用了一种非常绕的表达方式:
Object.prototype.hasOwnProperty.toString.call(Object);
综上,判断纯粹的对象:
var getProto = Object.getPrototypeOf;
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;
var fnToString = hasOwn.toString;
var ObjectFunctionString = fnToString.call( Object );
function isPlainObject(obj) {
var proto, Ctor;
if(!obj || toString.call(obj) !== "[object Object]") {
return false;
}
proto = getProto(obj);
if(!proto) {
return true;
}
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && fnToString.call(Ctor) === ObjectFunctionString;
}
当然,还有一个小的错误,extend(target[name], options[name]) 递归调用的时候 target[name] 并一定就存在,修改如下:
function extend(target, options) {
for(name in options) {
target[name] = target[name]? target[name] : {};
if(isPlainObject(options[name]))
target[name] = extend(target[name], options[name]);
else
target[name] = options[name];
}
return target;
}
jQuery 中的类型判断
在 jQuery 中使用了一个对象 class2type 来存储各种类型的字符串表达形式。
var class2type = {};
var arrType = "Boolean Number String Function Array Date RegExp Object Error Symbol".split(" ");
for(var i = 0; i < arrType.length; i++) {
class2type["[object " + arrType[i] + "]"] = arrType[i].toLowerCase();
}
有了这个类型字符串存储对象,判断类型就很简单了:
function type(obj) {
if(obj == null) {
return obj + "";
}
return typeof obj === "object" || typeof obj === "function" ?
class2type[({}).toString.call(obj)] || "object" :
typeof obj;
}
类数组
可以直接通过 [] 访问就可以称之为类数组:
-
本身就是数组;
-
存在 length 属性,且其为 0;
-
length 是数字,且 length - 1(最后一个元素) 存在。
function isArrayLike(obj) {
var length = !!obj && "length" in obj && obj.length,
type = type(obj);
if (type === "function" || (obj != null && obj === obj.window)) {
return false;
}
return type === "array" || length === 0 ||
typeof length === "number" && length > 0 && (length - 1) in obj;
}
each 方法
遍历,而后挨个执行传入的函数,分了类数组与非类数组,不过内部处理逻辑是一致的:
function each(obj, callback) {
var length, i = 0;
if (isArrayLike(obj)) {
length = obj.length;
for(; i < length; i++) {
if (callback.call(obj[i], i, obj[i] ) === false) {
break;
}
}
} else {
for(i in obj) {
if(callback.call(obj[i], i, obj[i] ) === false) {
break;
}
}
}
return obj;
}
组合起来的 extend
有了上面那些大大小小的拆分开的小方法,就能组合成 jQuery 中的 extend 方法了:
(function(window) {
"use strict";
var getProto = Object.getPrototypeOf;
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;
var fnToString = hasOwn.toString;
var ObjectFunctionString = fnToString.call( Object );
var jQuery = function() {
return new jQuery.fn.init();
};
jQuery.fn = jQuery.prototype;
jQuery.extend = jQuery.fn.extend = function() {
var options, name, src, copy, copyIsArray, clone,
target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false;
if(typeof target === "boolean") {
deep = target;
target = arguments[i] || {};
i++;
}
if(typeof target !== "object" && !jQuery.isFunction(target)) {
target = {};
}
if(i === length) {
target = this;
i--;
}
for(; i < length; i++) {
if((options = arguments[i]) != null) {
for(name in options) {
src = target[name];
copy = options[name];
if(target === copy) {
continue;
}
if(deep && copy && (jQuery.isPlainObject(copy) ||
(copyIsArray = Array.isArray(copy)))) {
if (copyIsArray) {
copyIsArray = false;
clone = src && Array.isArray(src) ? src : [];
} else {
clone = src && jQuery.isPlainObject(src) ? src : {};
}
target[name] = jQuery.extend(deep, clone, copy);
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
return target;
};
jQuery.extend({
isFunction: function(obj) {
return jQuery.type(obj) === "function";
},
isWindow: function(obj) {
return obj != null && obj === obj.window;
},
isPlainObject: function(obj) {
var proto, Ctor;
if(!obj || toString.call(obj) !== "[object Object]") {
return false;
}
proto = getProto(obj);
if (!proto) {
return true;
}
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && fnToString.call(Ctor) === ObjectFunctionString;
},
type: function(obj) {
if (obj == null) {
return obj + "";
}
return typeof obj === "object" || typeof obj === "function" ?
class2type[toString.call(obj)] || "object" :
typeof obj;
},
each: function(obj, callback) {
var length, i = 0;
if (isArrayLike(obj)) {
length = obj.length;
for(; i < length; i++) {
if (callback.call(obj[i], i, obj[i] ) === false) {
break;
}
}
} else {
for(i in obj) {
if(callback.call(obj[i], i, obj[i] ) === false) {
break;
}
}
}
return obj;
}
});
jQuery.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "), function(i, name) {
class2type[ "[object " + name + "]" ] = name.toLowerCase();
});
function isArrayLike(obj) {
var length = !!obj && "length" in obj && obj.length,
type = jQuery.type(obj);
if (type === "function" || jQuery.isWindow(obj)) {
return false;
}
return type === "array" || length === 0 ||
typeof length === "number" && length > 0 && (length - 1) in obj;
}
var init = jQuery.fn.init = function() {};
init.prototype = jQuery.fn;
window.jQuery = window.$ = jQuery;
return jQuery;
})(window);
小小的吐槽
jQuery 扩展并没有防覆盖措施,很轻松就能通过外部扩展来覆盖原先定义好的方法,可能这就是前端最大的无奈,一切都放在明面上,反正我的东西你都能看到,源码都能直接改,何必费劲多写东西来防止你覆盖我原先的方法呢?
代码地址
https://github.com/oldmanscode/jq_step_by_step/blob/master/step3.js
浙公网安备 33010602011771号