-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
JavaScript专题之从零实现jQuery的extend #33
Comments
var isObject = function (data) {
return Object.prototype.toString.call(data) === '[object Object]'
}
var extend = function(deep) {
var sources = typeof deep === 'boolean' && deep ? Array.prototype.slice.call(arguments, 1) : Array.prototype.slice.call(arguments)
var i = 0;
var obj = {};
for (; i < sources.length; i++) {
if (!isObject(sources[i])) {
console.error("Function[extend] parmas must be Object")
return false
}
for (var key in sources[i]) {
if (deep === true && isObject(sources[i][key]) && obj[key]) {
obj[key] = extend(deep, obj[key], sources[i][key])
continue
}
if (sources[i].hasOwnProperty(key)) {
obj[key] = sources[i][key]
}
}
}
return obj;
} 我没有考虑数组对象的情况,只是一个简单的对象的复制。我只是觉得没必要写的你那么繁琐,比如 |
@TaurusWood 我觉得写得很好呀,文章中的写法是抽自 jQuery 的 extend 的写法,考虑到了很多情况,比如不同类型之间的拷贝、循环引用,甚至是性能上的考虑,比如 for 循环高于 for in ,直接遍历 arguments 对象而非先转换成数组再遍历等,你的写法相对简化,但相对的,实现的功能也少了一点。我觉得这个看需求吧,如果现在的可以满足你的需求,那就是很好的~ |
楼主的深复制方法只支持json对象,并不完美啊。可以参考http://jerryzou.com/posts/dive-into-deep-clone-in-javascript/ |
为什么
感觉应该是
|
@fondadam 单独看这个结果,当然是 |
@mqyqingfeng 哎呀 掉坑里了 话说你的blog都讲的好详细,谢谢分享。 |
extend 第一版中的 |
@cobish 哈哈,确实如此,感谢指出~ |
function extend() {
// 默认不进行深拷贝
var deep = false;
var name, options, src, copy;
var length = arguments.length;
// 记录要复制的对象的下标
var i = 1;
// 第一个参数不传布尔值的情况下,target默认是第一个参数
var target = arguments[0] || {};
// 如果第一个参数是布尔值,第二个参数才是target
if (typeof target == 'boolean') {
deep = target;
target = arguments[i] || {};
i++;
}
// 如果target不是对象,我们无法进行复制的所以设为{}
if (typeof target !== 'object') {
target = {}
}
for (; i < length; i++) { // 从i 开始 没有deep i = 1 有deep i = 2
options = arguments[i]
if (options !== null) { //不为null和undefined
for (name in options) {
src = target[name]
copy = options[name]
if (deep && copy && typeof copy == 'object') {
console.log(deep, src, copy)
src = extend(deep, src, copy)
// target[name] = extend(deep,src,copy)
} else if (copy !== undefined) {
target[name] = copy
}
}
}
}
return target
}
var obj1 = {
a: 1,
b: {
c: 2
}
}
var obj2 = {
b: {
c: [5],
}
}
var d = extend(true, obj1, obj2)
console.log(d); // {a:1,b:{c:2}} 只是在深拷贝的时候把 target[name]换成了 src |
@1391020381 我搞错了,第二版是没有这个问题的,这是因为: if (deep && copy && typeof copy == 'object') {
console.log(deep, src, copy)
// wrong 这样并不会修改 target[name]的值
src = extend(deep, src, copy)
// right
target[name] = extend(deep,src,copy)
} else if (copy !== undefined) {
target[name] = copy
} |
@mqyqingfeng 是不是当 target[name]是对象的时候,src保存的是target[name]的指针副本, |
@1391020381 正是如此,不过无论 target[name] 是不是对象,修改 src 的值都不会改变 target[name] 的值,举个例子: // 不是对象
var value = {
a: 1
}
var src = value.a;
src = 2;
console.log(value.a); // 1
// 是对象
var value = {
a: {
num: 1
}
}
var src = value.a;
src = 2;
console.log(value.a); // {num: 1} |
对的不是对象的时候,也是重新给src赋值,不会影响value.a。大神回复,我受宠若惊,我会一直关注大神的 @mqyqingfeng |
@1391020381 过奖了,大家一起相互交流讨论,有的时候,我可能因为某些原因没能及时回复,还请不要介意~ |
// 要求不能为空 避免 extend(a,,b) 这种情况 |
@zzzzzyb 因为 |
不严格的话,undefined==undefined,也是true,就结果来说用undefined来判断也是一样的吧@mqyqingfeng |
@zzzzzyb 所以也可以啦~ 不过 null 更短呀~ <( ̄︶ ̄)> window.undefined = null;
console.log(window.abc === undefined); // false 当然大多时候,不会出现这样的问题,但是有可能曲折的出现: var n = window.abc; // 实际上 abc 并没有被设置,n 为 undefined
window[n]='text'; // 不小心更改了 undefined 的值
console.log(window.abc === undefined); // false 所以有些开发者可能更倾向于使用 null 进行判断 |
@mqyqingfeng 确实更严谨点,谢谢大佬指点 |
感觉ES6的Object.assign函数和extend很相似,不过assign使用的也是浅拷贝 |
@liujuntao123 确实是这样的,我觉得正是 extend 这种函数的广泛应用才导致了 assign 函数的诞生~ |
非常感谢作者的讲解,一开始看有点不懂,后来带着敲,自己理解懂了点,然后自己仿着写了一遍,也弄了个深拷贝,只支持数组和普通对象,循环引用没过滤,技术比较菜写这个都写了很久,害羞的丢个链接让前辈们帮我看看不足之处,我感觉我写的 i f判断太多。 |
@lizhongzhen11 写得很不错哈~ 直接看确实会有点难度啦,最好还是自己敲一遍,边敲边理解,效果会更好。写代码的时候注意哪些东西是重复书写的,就可以将其提取出来,也算是一个优化,比如 Object.prototype.toString.call(source) === '[object Object]' 和 Object.prototype.toString.call(source) === '[object Array]' 被重复的用到,就可以提取成 isObject 和 isArray 函数,这样代码的语义会更好~ |
哈哈哈哈哈,好开心能得到前辈的点评,我会继续努力的!!! |
jQuery中 /**
* obj: 待拷贝的对象
* target: 目标对象
* parent: target的各个父级
* _target: 用来分解target,如果有值,必有_target.target[_target.key] === target,主要用来while赋值用。(出现自引用)
**/
function deepCopy(obj, target, parent = null, _target = null) {
target = target || {};
let _parent = parent;
while (_parent) {
if (_parent.parentObj === obj) {
_target.target[_target.key] = obj;
// 不能直接给target赋值,否则引用的指针丢失,只能给target的属性赋值
// target = obj (错!!!)
// 所以才要把target[key]拆分成target和key
return;
}
_parent = _parent.parent;
}
Object.keys(obj).forEach(function(key) {
let currentCopy = obj[key];
if (typeof currentCopy === 'object' && currentCopy !== null) {
target[key] = currentCopy.constructor === Array ? [] : {};
deepCopy(
currentCopy,
target[key],
{
parentObj: obj, // 当前target的直接父级
parent: parent // 保存target的所有非直接父级
},
{
// 把target[key]拆分成target和key,供while赋值,否则对传入的target直接赋值会导致指针丢失
target: target,
key: key
}
);
} else {
target[key] = currentCopy;
}
});
return target;
} |
@DEVINNN dalao的思路非常赞~不过这种实现的话,每次只能从一个对象继承属性吧?需要继承多个对象的时候需要多次执行该方法。有个简单的修改:既然已经用了es6的默认参数功能,那么 |
@Tan90Qian 大佬谈不上,你写的文章质量都很高😄 |
@DEVINNN 认错人了吧233 我只是一个读者,不是作者,还没毕业的萌新一枚 |
@Tan90Qian 同,大三哈~ |
@Tan90Qian @StevenXN 后生可畏呀,两位同学!(๑•̀ㅂ•́)و✧ |
求解释一下最终版里的clone有什么用直接这样写不行吗
|
对于这句注释,如果第一个参数传入false,得到的结果是{},所以是不是可以理解成如果传入false,就不会把第二个参数当成target,而是直接使用{},然后从第二个参数开始copy呢。 |
if (copyIsArray) {
copyIsArray = false;
src = src && Array.isArray(src) ? src : [];
} 大佬 这段代码中 copyIsArray 重新赋值false是 有必要的吗? 求解 |
@gxr404 要的,如果这里不重新赋值的话会影响下一次for循环数组的判断 |
使用Object.create({})创建的对象通过isPlainObject函数后返回false,也就会导致这个对象不会递归调用extend方法。这样是不是就没有达到深度拷贝的效果呢? |
|
我的理解是 var a = {
b: 1
}
console.log(a.c) // undefined 对象中没有赋值的属性,取值都为 undefined |
var a = {name : b}; 原本不是循环引用嘛,name都是有值的,extend后name都变成undefined了,它们是在哪里变成undefined的呢? |
大佬,你的循环引用的例子有点问题: |
大佬,这篇所讲的extend函数 感觉和上篇所说的深拷贝是一回事吧,这样理解对吧 |
ES6的Object.assign好像就是extend 第一版的实现 |
前言
jQuery 的 extend 是 jQuery 中应用非常多的一个函数,今天我们一边看 jQuery 的 extend 的特性,一边实现一个 extend!
extend 基本用法
先来看看 extend 的功能,引用 jQuery 官网:
翻译过来就是,合并两个或者更多的对象的内容到第一个对象中。
让我们看看 extend 的用法:
第一个参数 target,表示要拓展的目标,我们就称它为目标对象吧。
后面的参数,都传入对象,内容都会复制到目标对象中,我们就称它们为待复制对象吧。
举个例子:
当两个对象出现相同字段的时候,后者会覆盖前者,而不会进行深层次的覆盖。
extend 第一版
结合着上篇写得 《JavaScript专题之深浅拷贝》,我们尝试着自己写一个 extend 函数:
extend 深拷贝
那如何进行深层次的复制呢?jQuery v1.1.4 加入了一个新的用法:
也就是说,函数的第一个参数可以传一个布尔值,如果为 true,我们就会进行深拷贝,false 依然当做浅拷贝,这个时候,target 就往后移动到第二个参数。
还是举这个例子:
因为采用了深拷贝,会遍历到更深的层次进行添加和覆盖。
extend 第二版
我们来实现深拷贝的功能,值得注意的是:
在实现上,核心的部分还是跟上篇实现的深浅拷贝函数一致,如果要复制的对象的属性值是一个对象,就递归调用 extend。不过 extend 的实现中,多了很多细节上的判断,比如第一个参数是否是布尔值,target 是否是一个对象,不传参数时的默认值等。
接下来,我们看几个 jQuery 的 extend 使用效果:
target 是函数
在我们的实现中,
typeof target
必须等于object
,我们才会在这个target
基础上进行拓展,然而我们用typeof
判断一个函数时,会返回function
,也就是说,我们无法在一个函数上进行拓展!什么,我们还能在一个函数上进行拓展!!
当然啦,毕竟函数也是一种对象嘛,让我们看个例子:
实际上,在 underscore 的实现中,underscore 的各种方法便是挂在了函数上!
所以在这里我们还要判断是不是函数,这时候我们便可以使用《JavaScript专题之类型判断(上)》中写得 isFunction 函数
我们这样修改:
类型不一致
其实我们实现的方法有个小 bug ,不信我们写个 demo:
我们预期会返回这样一个对象:
然而返回了这样一个对象:
让我们细细分析为什么会导致这种情况:
首先我们在函数的开始写一个 console 函数比如:console.log(1),然后以上面这个 demo 为例,执行一下,我们会发现 1 打印了三次,这就是说 extend 函数执行了三遍,让我们捋一捋这三遍传入的参数:
第一遍执行到递归调用时:
第二遍执行到递归调用时:
第三遍进行最终的赋值,因为 src 是一个基本类型,我们默认使用一个空对象作为目标值,所以最终的结果就变成了对象的属性!
为了解决这个问题,我们需要对目标属性值和待复制对象的属性值进行判断:
判断目标属性值跟要复制的对象的属性值类型是否一致:
如果待复制对象属性值类型为数组,目标属性值类型不为数组的话,目标属性值就设为 []
如果待复制对象属性值类型为对象,目标属性值类型不为对象的话,目标属性值就设为 {}
结合着《JavaScript专题之类型判断(下)》中的 isPlainObject 函数,我们可以对类型进行更细致的划分:
循环引用
实际上,我们还可能遇到一个循环引用的问题,举个例子:
我们会得到一个可以无限展开的对象,类似于这样:
为了避免这个问题,我们需要判断要复制的对象属性是否等于 target,如果等于,我们就跳过:
如果加上这句,结果就会是:
最终代码
思考题
如果觉得看明白了上面的代码,想想下面两个 demo 的结果:
专题系列
JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog。
JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered: