jQuery的extend

原文出自冴羽的博客

extend的基本用法

extend:merge the contents of two or more objects together into the first object
extend的用法

1
jQuery.extend(target [, object] [, objectN])

第一个参数target表示要拓展的目标,后面的参数都是对象,对象的内容会复制到目标对象中
打个比方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var obj1 = {
a: 1,
b: {b1: 1, b2: 2} //被下面的obj2的b直接替换了
};

var obj2 = {
b: {b1: 3, b3: 4},
c: 3
}
var obj3 = {
d: 4
}
console.log($.extend(obj1, obj2, obj3));

// {
// a: 1,
// b: { b1: 3, b3: 4 },
// c: 3,
// d: 4
// }

后面传入的参数相同时,后者会把前者覆盖掉,而不是合并。

extend 第一版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function extend(){
var target = arguments[0];
var length = arguments.length;
var i = 1;
var options;
for(; i < length; i ++) {
options = arguments[i];
if(options != null){
for(name in options){
copy = options[name]
if(name !== undefined) target[name] = copy
}
}
}
return target;
}

如何进行深拷贝的复制呢?

1
jQuery.extend([deep], target, object1 [, objectN])

也就是说通过第一参数值的状态来判断是否进行深拷贝,target被推后了一位。
同样的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var obj1 = {
a: 1,
b: { b1: 1, b2: 2 }
};

var obj2 = {
b: { b1: 3, b3: 4 },
c: 3
};

var obj3 = {
d: 4
}

console.log($.extend(true, obj1, obj2, obj3));

// {
// a: 1,
// b: { b1: 3, b2: 2, b3: 4 }, //这个时候就不是简单的覆盖了事
// c: 3,
// d: 4
// }

extend 第二版

在实现深拷贝的功能之前,需要注意的是:

  1. 需要根据第一个参数的类型,确定target和要合并的对象的下标起始值。
  2. 如果是深拷贝,根据copy的类型递归extend。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    function extend() {
    var deep = false;
    var name, options, src, copy;
    var length = arguments.length;
    var i = 1;
    var target = arguments[0] || {};


    //如果是布尔值,target就应该是第二位
    if(typeof target == 'boolean') {
    deep = target;
    target = arguments[i] || {};
    i++;
    }
    // 校验第二个参数是否是个对象
    if(typeof target !== 'object') {
    target = {}
    }

    for(; i < length; i++) {
    //获取到target之后的各个参数
    options = arguments[i];
    //参数不能为空, 避免 extend(a,,b)这种情况
    if(options != null) {
    //遍历参数
    for(name in options) {
    //添加了新属性的target的副本,用作遍历的暂存对象
    src = target[name];
    //得到参数里的参数值
    copy = options[name];
    //是深拷贝 参数值不为空 参数的值又是一个对象
    if(deep && copy && typeof copy == 'object') {
    //递归更新target
    target[name] = extend(deep, src, copy);
    }
    //浅拷贝直接赋值就可以了
    else if(copy !== undefined) {
    target[name] = copy;
    }
    }
    }
    }
    return target;
    }

target 是函数

在实现中,typeof target必须等于object,我们才会在这个target基础上进行拓展,但是typeof判断一个函数时,会返回function,也就是说,无法在一个函数上进行拓展,但是真实场景下,函数是能被扩展的

1
2
3
function a(){}
a.target = 'b';
console.log(a.target);

实际上,在underscore的实现中,underscore的各种方法便是挂在了函数上!
我们可以修改成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var class2type = {};

// 生成class2type映射-- 这里删去了 Null 和 Undefined
"Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) {
class2type["[object " + item + "]"] = item.toLowerCase();
})

function type(obj) {
// 在 == 的判断条件下 undefined == null 是 true
if (obj == null) {
// 隐式转换成了字符串
return obj + "";
}
return typeof obj === "object" || typeof obj === "function" ?
class2type[Object.prototype.toString.call(obj)] || "object" :
typeof obj;
}

function isFunction(target) {
return type(obj) === "function";
}

if(typeof target !== 'object' && !isFunction(target)) {
target = {};
}

类型不一致

目前实现的extend函数有一个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj1 = {
a: 1,
b: {
c: 2
}
}

var obj2 = {
b: {
c: [5],

}
}
var d = extend(true, obj1, obj2)
console.log(d);

我们期待的输出应该是这么一个对象

1
2
3
4
5
6
{
a: 1,
b: {
c: [5] // 这里应该是直接覆盖的
}
}

但是得到的结果确是:

1
2
3
4
5
6
7
8
{
a: 1,
b: {
c: {
0: 5
}
}
}

在函数开始添加一行 console.log(1) 发现1打印了三次,这是怎么回事呢
我们来看看每次参数是怎么变化的
第一次遍历

1
2
3
4
5
src = {c: 2}, target = {a: 1, b: {...}}  name = "b"
copy = {c: Array(1)}, options = {b: {...}}, name = "b"
deep = true, copy = {c: Array(1)}

target[name] = extend(deep, src, copy)

第二次遍历

1
2
3
4
5
scr = 2 , target = {c: 2}, name = "c"
copy = [5], options = {c: Array(1)}, name = "c"
deep = true, copy = [5]

target[name] = extend(deep, src, copy)

第三遍进行最终的赋值,因为 src 是一个基本类型,我们默认使用一个空对象作为目标值,所以最终的结果就变成了对象的属性!
也就是这一段代码

1
2
3
4
// 校验第二个参数是否是个对象
if(typeof target !== 'object') {
target = {}
}

为了解决这个问题,我们就需要同时对目标属性和待复制对象的属性值进行判断:
判断目标属性值要跟复制的对象的属性值类型是否一致:

  1. 如果待复制对象属性值是一个数组,目标属性值类型不为数组的话,目标属性值就设置为 []
  2. 如果待复制对象属性值类型为对象,目标属性值类型不为对象的话,目标属性值就设置为{}

结合javaScript之类型判断中的isPlainObject函数,我们可以对类型进行更细致的划分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var clone, copyIsArray;

...

if(deep && copy && (isPlainObject(copy) || (copyIsArray = Array.isArray(copy)) )) {
if(copyIsArray) {
copyIsArray = false;
clone = src && Array.isArray(src) ? src : [];
} else {
clone = src && isPlainObject(src) ? src : {};
}

target[name] = extend(deep, clone, copy);
}else if(copy !== undefined) {
target[name] = copy;
}

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
function extend() {
var deep = false;
var name, options, src, copy;
var length = arguments.length;
var i = 1;
var target = arguments[0] || {};
var clone, copyIsArray;

//如果是布尔值,target就应该是第二位
if(typeof target == 'boolean') {
deep = target;
target = arguments[i] || {};
i++;
}
// 校验第二个参数是否是个对象
if(typeof target !== 'object') {
target = {}
}

for(; i < length; i++) {
//获取到target之后的各个参数
options = arguments[i];
//参数不能为空, 避免 extend(a,,b)这种情况
if(options != null) {
//遍历参数
for(name in options) {
//添加了新属性的target的副本,用作遍历的暂存对象
src = target[name];
//得到参数里的参数值
copy = options[name];
//是深拷贝 参数值不为空 参数的值又是一个对象
if (deep && copy && ($.isPlainObject(copy) ||
(copyIsArray = Array.isArray(copy)))) {

if (copyIsArray) {
copyIsArray = false;
clone = src && Array.isArray(src) ? src : [];

} else {
clone = src && $.isPlainObject(src) ? src : {};
}

target[name] = extend(deep, clone, copy);

}
//浅拷贝直接赋值就可以了
else if(copy !== undefined) {
target[name] = copy;
}
}
}
}
return target;
}

循环引用

在实际中可能会遇到一个循环引用的问题
var a = {name : b};
var b = {name : a};
var c = extend(a, b);
console.log(c);
结果就成了下图所示:
img
为了避免这个问题,我们需要判断要复制的对象属性是否等于target,如果等于,就跳过

1
2
3
4
5
6
src = target[name];
copy = options[name];

if(target === copy) {
continue;
}

如果加上这句话,结果就会是:

1
{name: undefined}

最终代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// isPlainObject 函数来自于  [JavaScript专题之类型判断(下) ](https://github.com/mqyqingfeng/Blog/issues/30)
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;

function isPlainObject(obj) {
var proto, Ctor;
if (!obj || toString.call(obj) !== "[object Object]") {
return false;
}
proto = Object.getPrototypeOf(obj);
if (!proto) {
return true;
}
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
}


function extend() {
// 默认不进行深拷贝
var deep = false;
var name, options, src, copy, clone, copyIsArray;
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" && !isFunction(target)) {
target = {};
}

// 循环遍历要复制的对象们
for (; i < length; i++) {
// 获取当前对象
options = arguments[i];
// 要求不能为空 避免 extend(a,,b) 这种情况
if (options != null) {
for (name in options) {
// 目标属性值
src = target[name];
// 要复制的对象的属性值
copy = options[name];

// 解决循环引用
if (target === copy) {
continue;
}

// 要递归的对象必须是 plainObject 或者数组
if (deep && copy && (isPlainObject(copy) ||
(copyIsArray = Array.isArray(copy)))) {
// 要复制的对象属性值类型需要与目标属性值相同
if (copyIsArray) {
copyIsArray = false;
clone = src && Array.isArray(src) ? src : [];

} else {
clone = src && isPlainObject(src) ? src : {};
}

target[name] = extend(deep, clone, copy);

} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}

return target;
};