Web前端的知识之旅哟——克隆与数组

在开始介绍克隆的代码和数组的相关知识点之前,我在前面先介绍一下arguments.callee方法func.caller属性

arguments.callee

这个方法是代指函数本身。

当我们在一些匿名函数或者立即执行函数里面进行递归调用函数本身的时候,由于这个函数没有名字,我们不能用函数名的方式调用,就可以用arguments.callee来调用。

一般当我们需要通过计算来进行初始化的时候,我们写一个立即执行函数,当这个立即执行函数还需要递归调用自身的时候,我们就是用arguments.callee方法。

func.caller


1. function test () {
2.       console.log(test.caller);
3. }
4. function demo () {
5.       test()
6. }
7. demo(); // function demo(){}
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这是函数本身自带的一个属性,可以指出当前函数ude运行环境的函数引用,即这个函数是在哪个函数体里面执行的。

个人感觉这个东西并没有什么用……

克隆

克隆和我们前面所讲的继承有一些区别,克隆是复制出来一个一模一样的目标对象,而克隆又分为浅层克隆和深层克隆。

浅层克隆

克隆大致就是我们的源对象里面有什么属性,目标文件就有什么属性,依照这个原理,我们可以写出下面的代码:


1. function clone(src, tar) {
2.       var tar = tar || {}; 如果没有tar则默认是一个空对象
3.       for(var prop in src) {
4.             if(src.hasOwnProperty(prop)){
5.                   tar[prop] = src[prop];
6.             }
7.        }
8.       return tar;
9. }
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

当我们我的源对象src有一个自身的属性,就会在目标对象tar克隆一个对应的属性。

下面我们来测试一下:


1. var obj = {
2.       name: ‘scarlett’,
3.       age: 123
4. }
5. var obj2 = {}
6. clone(obj, obj2);
7. console.log(obj2.name);// scarlett
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

然后我们改变一下obj这个源对象的name:


1. obj.name = ‘test’;
2. console.log(obj2.name); // scarlett
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

我们可以发现,克隆出来的目标对象和本来的源对象之间没有关系了。

但是,当我们有一个属性是引用值(数组或者对象)的时候,按照我们这种克隆方式,只是把这个引用值的指向赋给了新的目标对象,也就是说,我们一旦改变了源对象或者目标对象的引用值属性,另一个也会跟着改变,这一点就是浅层克隆的缺点。

深层克隆

为了解决浅层克隆的引用值的问题,我们又需要一种深层克隆。

其实深层克隆的原理也很简单,我们只要不克隆引用值的引用,而是吧引用值也当做一个源对象,把里面的值一个一个的克隆进目标对象里面,不就解决了他们二者相同指向的问题了吗。


1. function deepCopy(src,tar){
2.       var tar= tar|| {} ;
3.       for (var prop in src){
4.             if (typeof (src[prop]) == ‘object’){
5.                    tar[prop] = (src[prop].constructor === Array ) ? [] : {};
6.                   deepCopy(src[prop],tar[prop]);
7.             }else {
8.                    tar[prop] = src[prop];
9.             }
10.       }
11.  
12.       return tar;
13. }
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这里我们运用了递归调用的方法,当我们检测到源对象里面的这个属性值是引用类型的,那么就在目标对象里面也创建一个引用类型的属性,如果原来是数组就创建数组,是对象就创建对象,然后分别将源对象里面的这个引用值和目标对象里面的引用值分别当做新的源对象和目标对象进行克隆,这样就是克隆的里面每一个值了,而不是把整个引用都克隆过去。

现在我们来测试一下:


1. var parent = {
2.       name : ‘ScarLet’,
3.       age : 123,
4.       sex : ‘male’,
5.       height : 190,
6.       money: [1,2,3,4,5]
7. }
8. var child = {};
9. deepCopy(parent,child);
10. console.log(child.money); //1 2 3 4 5
11. parent.money.push(10);
12. console.log(child.money); // 1 2 3 4 5
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这个时候目标对象和源对象的引用值之间就没有了关系,自己都是独立值,可以进行修改了。

数组

数组的声明

首先来介绍一下数组的声明方式,一共有两种:

1.字面量方式声明数组。


1. var arr [];
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

2.通过数组构造函数构造数组。


1. var arr = new Array(1, 2, 3, 4);
2. console.log(arr); // 1 2 3 4
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这种方式和字面量方式没有区别,但是要注意的是,如果我们在构造函数里面只写一个数字new Array(5);这个时候这个数字就不是第一个值是5的意思了,而是我们新创建的这个数组长度是5。


1. var arr = new Array(10);
2. console.log(arr); //10
3. document.write(arr); // 9个逗号 这里用console的话是一个空数组,看不到长度
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

数组的读写

js的数组是弱数据类型的数组,不像其他语言那样严格。

我们不可以溢出读


1. var arr = [1, 2];
2. console.log(arr[3]); // undefined
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

但是我们可以溢出写


1. arr[5] = 5;
2. console.log(arr); // 1,2,,,,5
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

数组的常用方法

数组的方法大致可以分类两类:不改变原数组的和改变原数组的。

1.改变原数组的

改变原数组的方法主要有:reverse,sort,push,pop,shift,unshift,splice

reverse

reverse是使数组倒叙。

push

push在数组最后的位置增加数据。


1. push(4, 5, 1);
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

pop

pop从数组后面删除一位数据,同时返回这个被删除的数据,没有参数。

shift

从数据的最前面删除一位数据,同时返回这个数据,没有参数。

unshift

在数据的最前面添加数据,和push一样的用法。

splice

这个方法是截取的意思,它有三个参数,第一个是截取开始的位置,第二个是截取的长度,第三个是参数是一组数据,代表我们要在截取的位置添加的数据。


1. var arr = [1, 2, 3, 4, 5];
2. arr.splice(1, 2, 100,188);
3. console.log(arr); // 1 100 188 4 5
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这样2和3就被截掉了。

如果我们不写要添加的数据的话,这个方法就变成了在数组中删除数据的作用了。

如果我们截取的长度是0,然后添加数据的话,这个方法就变成了在数据的特定位置添加数据的作用了。

sort

这个方法是快速排序的意思,它将一种算法封装好了给我们使用。

我们可以在这个方法中传入一个参数,这个参数是一个函数,规定了我们的排序规则,否则就按照ASC码来排序。


1. arr -> 1 2 5 4 3
2. arr.sort -> 1 2 3 4 5
3. arr -> a c b d
4. arr.sort -> a b c d
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

如果我们数组中的元素是比较复杂的数据的话,我们就需要自己来定义排序的规则了。


1. arr.sort(function (x, y) {
2.       return x.age < y.age;
3. })
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这里的x、y代表的是数组里面任意的两位数据。

无论中间的规则怎么写,系统只关注函数最后的返回值是正数还是负数。

负数的时候,表示a在前面,b在后面。

正数的时候,表示a在后面,b在前面。

乱序排序:


1. function (a, b) {
2.       var num = Math.random() - 0.5;
3.      return num;
4. }
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

不改变原数组的

改变原数组的方法主要有:

concat,join

concat

这个方法是连接的数组的作用。


1. arr1 = [1, 2];
2. arr2 = [2, 3];
3. arr = arr1.concat(arr2);
4. console.log(arr, arr1, arr2);// [1, 2, 2, 3],  [1, 2], [2, 3];
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

我们发现arr现在是1和2连接之后的数组,而且1和2本来的数组都没有改变。

当然我们如果要连接多个数组的话,那么concat里面的数组之间用逗号分隔即可。


1. arr3 = [5, 5];
2. arr = arr1.concat(arr2, arr3);
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

join

这个方法是让数组的每一个数据以什么方式连接成字符串。


1. var arr = [‘a’, ‘b’, ‘c’];
2. var str = arr.join(‘-‘);
3. console.log(str); // a-b-c
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

我们可以用这个方法来进行大量字符串的连接工作,如果我们用+运算符去连接字符串的话,因为是栈操作,所以很消耗性能,我们可以先放进数组里面,然后用join连接成字符串即可。

同时,字符串中以一个split操作刚好和join操作相反。

split是把字符串以什么方式分割成数组


1. var str = ‘a-b-c-d’;
2. var arr = str.split(-‘);
3. console.log(arr); // a b c d
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

数组去重问题

这里有一个我们必须要掌握的方法——数组去重方法。

把数组中重复的元素去掉。


1. Array.prototype.unique = function () {
2.       var len = this.length,
3.       arr = [],
4.       obj = {};
5.       for (var i = 0; i < len; i++) {
6.             if (!obj[this[i]]) {
7.                   obj[this[i]] = 1;
8.                   arr.push(this[i]);
9.             }
10.       }
11.       return arr;
12. }
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

这里我们运用了一个简单的哈希结构。当我们数组中的这个数据出现过一次之后,我们就在obj中将这个元素的值的位置标记成1,后面如果出现相同的属性值,因为这个位置已经是1了,所以就不会添加到新数组里面,从而达到了去重的效果。


1. var arr = [1,1 ,1 ,2 ,3 ,4 ,2, 5, 6, undefined, undefined, null, 9, null];
2. console.log(arr.unique());//[1, 2, 3, 4, 5, 6, undefined, null, 9]
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

ES5的数组方法

刚才我们介绍的那些常用的数组方法都是ES3中定义的,ES5中出现了一些新的方法。

forEach

这个方法会改变原数组,它让数组中的元素从头到尾遍历一遍,每一个都调用一下我们在forEach里面传递的方法,中间不会停止。


1. var arr = [1, 2, 3, 4];
2. arr.forEach(function () {
3.       arr[i] += 1;
4. })
5. console.log(arr); // 2 3 4 5
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

map

这个方法和forEach很像,只不过forEach会改变原数组,而map不会改变原数组,而是返回一个新的数组,它也是让传递一个指定的方法,让数组中的每一个元素都调用一遍这个方法。不过记得map方法最后有返回值。


1. var arr = [1, 2, 3];
2. var test = arr.map(function (x) {
3.       return x * x;
4. });
5. console.log(test); // 1 4 9
6. console.log(arr); // 1 2 3
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

filter

这个方法是过滤的作用,它同样不会改变原数组,而是返回一个原数组的子集。我们同样会传递一个方法,每一个元素都会调用一下这个方法,但是只有返回true的元素才会被添加到新数组里面,返回false的不会被添加到新数组里面。


1. var a = [1 ,2 , 3, 4, 5];
2. var b = a.filter(function (x) {
3.       return x > 2
4. });
5. console.log(b); // 3 4 5
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

同时,filter()会跳过稀疏数组里面缺少的元素,它的返回数组总是稠密的


1. var arr = [1,,,,,,3,4];
2. var b = arr.filter(function () {
3.       return true;
4. })
5. console.log(arr); // 1 3 4
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

every 和 some

这两个方法是数组的逻辑判定,他们对数组应用指定的函数进行判定,返回true或者false。

every是如果每一个元素经过传递的方法的判定之后都返回true,那么最后才返回true。

some是只要有一个元素返回true,那么就返回true。


1. var arr = [1 ,2 ,3];
2. console.log(arr.some(function (x) {return x<3;})); true
3. console.log(arr.every(function (x) {return x<3;})); false
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

reduce 和 reduceRight

reduce()和reduceRight()方法使用指定的函数将数组元素进行组合,最后变成一个值,reduce是从左向右,reduceRight是从右向左。有两个参数,第一个是方法,第二个是可选参数,即我们最后的这个值的初始值。

当我们没有设置初始值的时候,用数组的第一个元素的值作为初始值。不过当数组为空的时候,不带初始值就会报错。

当我们的数组只有一个元素并且没有指定初始值,或者有一个空数组并且指定一个初始值的情况下,reduce只是简单地返回那个值,而不会调用函数


1. var arr = [1, 2, 3];
2. var sum = a.reduce(function (x, y) { return x + y}, 0);// 0 + 1 + 2 + 3 = 6;
3. var temp = [1];
4. var temoOut = a.reduce(function (x, y) {return x x y}); // 1 不会调用这个函数,因为数组只有一个值,除非我们设置一个初始值
JavaScript; “复制代码”); “查看纯文本代码”); “返回代码高亮”)

数组类型的检测

在ES5中,我们有一个isArray()方法来检测是否是数组。

但是在es5之前,我们要检测数据是否是数组类型是很麻烦的。

typeof运算符,数组和对象都会返回object,因此无法区分数组和对象。

constructor和instanceof操作符到现在为止是好用的,但是它们都存在潜藏问题:

我们的web浏览器中可能有多个窗口或者窗体,每一个窗体都有自己的js环境,有自己的全局对象。并且,每个全局对象有自己的构造函数,因此一个窗体中的对象将不可能是另外窗体中的构造函数的实例。窗体之间的混淆并不常发生,但这个问题已经证明constructor和instanceof都不能真正可靠的检测数组类型。

这个时候我们就需要这样的代码来检测了:

Object.prototype.toString.call(arr) === ‘[object Array]’ 

这是最可靠的检测是否是数组类型的方法。

今天的知识点就到这里哟~