【javascript】javascrpt高级教程第3版-更新...深浅拷贝 下

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 【javascript】javascrpt高级教程第3版-更新...深浅拷贝

六、垃圾收集

Javascript具有自动垃圾收集机制。在编写javascript程序时,开发人员不用再关心内存使用问题,所需内存的分配以及无用内存的回收完全实现了自动管理。

原理 :找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。

垃圾收集器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。用于编辑无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略:

(1)标记清除

这是各浏览器最常用的方式。可使用任何方式来标记变量。比如,可通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。说到底,如何标记变量其实并不重要,关键在于采取什么策略。

(2)引用计数

这是一种不常见的方式。引用计数的含义:跟踪记录每个值被引用的次数。当声明一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1.如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得另一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。

但是,引用计数会遇到一个严重的问题:循环引用——指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。

(3)性能问题

说到垃圾收集器多长时间运行一次,不禁让人联想到IE因此而声名狼藉的性能问题。IE的垃圾收集器是根据内存分配量运行的,具体一点就是256个变量、4096个对象或数组字面量和数组元素或者64KB的字符串。达到上述任何一个临界值,垃圾收集器就会运行。

这种实现方式的问题在于,如果一个脚本中包含那么多变量,那么该脚本很可能会在其生命周期中一直保有那么多的变量。这样一来,垃圾收集器就不得不频繁地运行。结果,由此引发的严重性能问题促使IE7重写了其垃圾收集例程。

在IE7中的垃圾收集机制有所改变的是:如果垃圾收集例程回收的内存分配量低于15%,则变量、字面量和(或)数组元素的临界值就会加倍。如果例程回收了85%的内存分配量,则将各种临界值重置回默认值。这看似简单的调整,却极大提升IE在运行包含大量javascript的页面时的性能。

(4)管理内存

javascript中内存管理面临的最主要的一个问题,就是分配给Web浏览器的可用内存数量通常要比分配给桌面应用程序的少。这样做的目的主要是出于安全考虑,目的是防止运行javascript的网页耗尽全部系统内存而导致系统奔溃。内存限制问题不仅会影响给变量分配内存,同时还影响调用栈以及在一个线程中能同时执行的语句数量。

因此,确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为null来释放其引用——即“解除引用”,该方法适用于大多数全局变量和全局对象的属性,而局部变量会在它们离开执行环境时自动被解除引用。例如:

function createPerson(name){
  var localPerson=new Object();
  localPerson.name=name;
  return localPerson;
}
var globalPerson=createPerson("Nicholas")
//手工解除globalPerson的引用
globalPerson = null;

注解:由于localPerson在createPerson()函数执行完毕后就离开了其执行环境,因此无需显示地为其解除引用。而全局变量globalPerson则需要我们在不使用它的时候手工为它解除引用。

不过,解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

七、引用类型

ECMAScript中,引用类型是一种数据结构,用于将数据和功能组织在一起。引用数据类型有时候也被称为对象定义,因为它们描述的是一类对象所具有的属性和方法。

对象是某个特定引用类型的实例。新对象是使用new操作符后跟一个构造函数来创建的。构造函数本身就是一个函数,只不过该函数是出于创建新对象的目的而定义的。例如:

var person=new Object();

这行代码创建了Object引用类型的一个新实例,然后把该实例保存在了变量person中。使用的构造函数是Object,它只为新对象定义了默认的属性和方法。

1、Object类型

创建Object实例的方式有两种。第一种:

使用new 操作符后跟Object构造函数。

var person=new Object();

第二种:使用对象字面量表示法。对象字面量是对象定义的一种简写形式,目的在于简化创建包含大量属性的对象的过程。例如:

var person={
  name:"jack",
  age:20
}

注:使用对象字面量语法时,属性名也可以使用字符串。且通过该方法,实际上不会调用Object构造函数。通常也是开发人员青睐的方法。

一般而言,访问对象属性时使用的是点表示法。不过,在javascript中也可以使用方括号表示法来访问对象的属性。在使用方括号语法时,应该将要访问的属性以字符串的形式放在方括号中。如:

alert(person["name"]);\\方括号语法
alert(person.name);\\点表示法

建议使用点表示法。

2、Array类型

与其他语言不同的是,ECMAScript数组的每一项可以保存任何类型的数据。即,可以用数组的第一个位置来保存字符串,用第二位置来保存数值,用第三个位置来保存对象,以此类推。而且,ECMAScript数组的大小是可以动态调整的,即可以随着数据的添加自动增加以容纳新增数据。

创建数组有两种方式:

(1)、使用Array构造函数

var colors=new Array();//new可省略

(2)、数组字面量表示法

数组字面量由一对包含数组项的方括号表示,多个数组项之间以逗号隔开

var colors=["red","blue","green"];

注意:

使用数组字面量创建数组优于数组构造函数。

主要原因:[]与new Array() ,前者2个字符,后者11个字符。
此外,由于javascript的高度动态特性,无法阻止修改内置的Array构造函数,
即new Array()创建的不一定是数组。
因此,推荐使用数组字面量。

数组的length属性很有特点——它不是只读的。通过设置这个属性,可以从数组的末尾移除项或向数组中添加新项。

var colors=["red","blue","green"];
colors.length=2;
alert(colors[2]);//undefined

注:数组最多可以包含4294967295个项,这几乎已经能够满足任何编程需求了。如果想添加的项数超过这个上限值,就会发生异常 。

上面的结果也表面,javascript中数组是对象。如果访问不存在的对象,会返回undefined。访问不存在的数组索引,也会返回undefined。

2.1 检测数组

从ECMAScript3之后,出现了确定某个对象是不是数组的经典问题。对于一个网页或者全局作用域而言,使用instanceof操作符就能得到满意的结果:

if(value instanceof Array){
//对数组执行某些操作
}

instanceof的问题在于:它假定只有一个全局执行环境。如果网页中包含对个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的Array构造函数。

为了解决这个问题,ECMAScript5新增了Array.isArray()方法。这个方法的目的是最终确定某个值到底是不是数组,而不管它是在哪个全局执行环境中创建的。

if(Array.isArray(value)){
//对数组执行某些操作
}

2.2 转换方法

所有对象都具有toLocaleString()、toString()和valueOf()方法。其中,调用数组的toString()方法会返回由数组中每个值的字符串形式拼接而成的一个以逗号分隔的字符串。而valueOf()返回的还是数组。

eg:

var colors=["red","blue","green"];
alert(colors.toString());//red,blue,green
alert(colors.valueOf());//red,blue,green
alert(colors);//red,blue,green

数组继承的toLocaleString()、toString()和valueOf()方法,在默认情况下都会以逗号分隔的字符串的形式返回数组项。而如果使用join()方法,则可以使用不同的分隔符来构建这个字符串。join()方法只接受一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串。

eg:

var colors=["red","green","blue"];
alert(colors.join(","));//red,green,blue
alert(colors.join("||"));//red||green||blue

如果不给join()方法传入任何值,或者给它传入undefined,则使用逗号作为分隔符。IE7及更早版本会错误的使用字符串“undefined”作为分隔符。

注:如果数组中的某一项的值是null或者undefined,那么改值在join()、toLocaleString()和valueOf()方法返回的结果中以空字符串表示。

2.3栈方法

ECMAScript为数组专门提供了push()和pop()方法,以便实现类似栈的行为。

  • push()方法可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。
  • pop()方法则从数组末尾移除最后一项,减少数组的Length值,然后返回移除的项。

2.4队列方法

ECMASCript也提供了shift()和unshift()方法。

  • shift()能够移除数组中第一个项并返回该项,同时将数组长度减1。结合使用shift()和 push()方法,可以像使用队列一样使用数组。
  • unshift()与shift()用途相反,它能在数组前端添加任意个项并返回新数组的长度。因此,同时使用unshift()和pop()方法,可以从相反的方向来模拟队列,即在数组的前端添加项,从数组末端移除项。

2.5操作方法

  • concat() :
    会基于当前数组创建一个副本,然后将接收到的参数添加到这个副本末尾,最后返回新构建的数组。在没有给concat()方法传递参数的情况下,它只是复制当前数组并返回副本。
var colors=["red","green","blue"];
var colors2=colors.concat("yellow",["black","brown"]);
console.log(colors);//red,green,blue
console.log(colors2);//red,green,blue,yellow,black,brown
  • slice() :
    它能基于当前数组中的一个或多个项创建一个新数组;
    (1)接收一个参数时:slice()返回从该参数指定位置开始到当前数组末尾的所有项;
    (2)接收2个参数时,返回起始和结束位置之间的项,但不包括结束位置。
 var colors=["red","green","blue","yellow","purple"];
 (1)
 var colors2=colors.slice(1);
 console.log(colors2);//green,blue,yellow,purple
 (2)
 var colors3=colors.slice(1,4);
 console.log(colors3);//green,blue,yellow
  • splice()—最强大的数组方法
   删除:指定2个参数---要删除的第一项的位置和要删除的项数;
    var colors=["red","green","blue"];
    var removed=colors.splice(0,1);
    console.log(colors);//green,blue
    插入:指定3个参数---起始位置、0(要删除的项数)、要插入的项,要插入的项可多个;
    inserts=colors.splice(1,0,"yellow","orange");
    console.log(colors);//green,yellow,orange,blue
    替换:指定3个参数---起始位置、要删除的项数、要插入的任意数量的项
    replaces=colors.splice(1,1,"red","purple");
    console.log(colors);//green,red,purple,orange,blue

2.6 位置方法

indexOf()和lastIndexOf()分别从数组开头向后与末尾开始向前查找;

两个方法,都返回要查找的项在数组中的位置,或者在没找到的情况下返回-1.

注意:在比较第一个参数与数组中每一项时,会使用全等操作符,即要求查找的项必须严格相等。

2.7 迭代方法

  • every()与some():用于查询数组中的项是否满足某个条件。
  • every():传入的函数必须对每一项都返回true,才返回true。
var numbers=[1,2,3,4,5,4,3,2,1];
var everyResult=numbers.every(function(item,index,array)){
 return (item>2);
});
alert(everyResult);//false

some():转入的函数对数组中的某一项返回true,就会返回true;

var someResult=numbers.some(function(item,index,array)){
 return (item>2);
});
alert(someResult);//true

filter():利用指定的函数确定是否在返回的数组中包含某一项

var numbers=[1,2,3,4,5,4,3,2,1];
var filterResult=numbers.filter(function(item,index,array)){
 return (item>2);
});
alert(someResult);//[3,4,5,4,3]

map():返回一个数组,这个数组的每一项都是在原始数组中的对应项上运行传入函数的结构;

var numbers=[1,2,3,4,5,4,3,2,1];
var mapResult=number.map(function(item,index,array)){
 return item*2;
});
alert(mapResult);//[2,4,6,8,10,8,6,4,2]

forEach():对数组中的每一项运行传入的函数,没有返回值;

2.8归并方法

reduce()和reduceRight(),迭代数组的所有项,然后构建一个最终返回的值。分别从数组第一项与最后一项开始,向前与向后遍历。

接收4个参数:前一个值、当前值、项的索引和数组对象,函数返回的任何值都会作为第一个参数自动传给下一项。

var values=[1,2,3,4,5];
var sum=values.reduce(function(prev,cur,index,array){
  return prev+cur;
});
alert(sum);//15

3、Function类型

ES中最有意思的莫过于函数,因为函数实际上是对象。

每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法。

函数是-对象,函数名是-指向函数对象的指针,不会与某个函数绑定。

(1)、3种函数定义方法:

  • 函数声明语法定义 (推荐)
 function sum(num1,num2){
 return num1+num2;
 }
  • 函数表达式定义
 var sum=function(num1,num2){
    return num1+num2;
 }
  • Function构造函数(不推荐)
    该方法不推荐使用,因为这种语法会导致解析两次代码,第一次是解析常规ES代码,第二次解析传入构造函数中的字符串,从而影响性能。
 var sum=new Function("num1","num2","return num1+num2");
 //Function构造函数可以接收任意数量的参数,但最后一个参数始终都被看成是函数体

(2)、没有重载

ES中没有函数重载的慨念。如果同时声明了两个同名函数,那么后一个会覆盖前一个。

(3)、函数声明与函数表达式

解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。

注意:

  • 解析器会率先读取函数声明,并使其在执行任何代码之前可用于访问;
  • 而函数表达式,必须等到解析器执行到它所在的代码行,才会真正被解析执行。
例如:
alert(sum(10,10));
var sum=function(num1,num2){
    return num1+num2;
}
//上述代码,在第一行就开始报错-"unexcepted identifier(意外标识符)错误",
并且不会执行到下一行。

(4)、作为值的函数

函数本身就是变量,所以,函数也可以作为值来使用。即,不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。

function callSomeFunction(someFunction,someArgument){
   return someFunction(someArgument);
}

可以从一个函数中返回另一个函数,而且这也是极为有用的一种技术。

function createComparisonFunction(propertyName){
  return function(object1,object2){
    var value1=object1[propertyName];
    var value2=object2[propertyName];
    if(value1<value2){
      return -1;
    }else if(value1>valu2){
      return 1;
    }else {
      return 0;
    }
  };
}

(4)、函数内部属性

函数内部,有两个特殊的对象:arguments和this。其中,arguments是一个类数组对象,包含着传入函数中的所有参数。虽然,arguments的主要用途是保存函数参数,但这个对象还有一个名叫callee的属性。

callee属性,是一个指针。指向拥有这个arguments对象的函数。

function factorial(num){
  if(num<=1){
    return 1;
  }else {
    return num*arguments.callee(num-1)
  }
}
var trueFactorial=factorial;
factorial=function(){
  return 0;
};
alert(trueFactorial(5));//120
alert(factorial(5));//0

//变量trueFactorial获得了factorial的值,实际上是在另一个位置上保存了一个函数的指针。

函数内部的另一个特殊对象是this。其行为与java和C#中的this大致类似。this引用的是函数剧以执行的环境对象—或者也可以说是this值(当在网页的全局作用域中调用函数时this对象引用的就是window)。

window.color="red";
var o={color:"blue"};
function sayColor(){
  alert(this.color);
}
sayColor();//"red"
o.sayColor=sayColor();
o.sayColor();//"blue"

//sayColor()是在全局作用域中定义的,它引用this对象。由于在调用函数之前,this的值并不确定,因此this可能会在代码执行过程中引用不同的对象。当在全局作用域中调用sayColor()时,this引用的就是全局对象window。即,this.color会转换为window.color求值。而把这个函数赋给对象o并调用o.sayColor()时,this引用的是对象o,因此this.color会转换成对o.color求值。

函数名仅仅是一个包含指针的变量。即使是在不同的环境中执行,全局的sayColor()函数与o.sayColor()指向的仍然是同一个函数。

caller属性:这个属性保存着调用当前函数的函数的引用。如果在全局作用域中调用当前函数,它的值为Null。

function outer(){inner();}
function inner(){alert(inner.caller);}
//inner.caller指向outer(),等价于alert(arguments.callee.caller);
outer();

注意,当函数在严格模式下执行,arguments.callee会导致错误。ES5还定义了arguments.caller属性,但在严格模式下访问它也会错误,在非严格模式下这个属性始终是undefined。

另外,严格模式下,不能为函数的caller属性赋值,否则会导致错误。

深拷贝与浅拷贝

基本数据类型(undefined,boolean,number,string,null)

存放在中,值不可变,可直接访问。即使是动态修改了基本数据类型的值,它的原始值也是不会改变。

如:

var str = "imaginecode";
console.log(strp[1]="f");
console.log(str);   //imaginecode

其次,基本类型的比较是值的比较,只要它们的值相等,就认为相等。

所以,通常为了避免值类型的转换,我们使用严格等 a===b进行比较。

引用类型(object)

存放在中,值可变。引用类型的变量,实际上是一个存放在栈内存的指针,指向堆内存中的地址。

如:

var a = [1,2,3];
a[1] = 6;
console.log(a); //[6,2,3]

其次,引用类型的比较是引用的比较。所以每次我们对 js 中的引用类型进行操作的时候,都是操作其对象的引用(保存在栈内存中的指针),所以比较两个引用类型,是看其的引用是否指向同一个对象。例如:

var a = [1,2,3];
var b = [1,2,3];
console.log(a===b); //false

虽然变量 a 和变量 b 都是表示一个内容为 1,2,3 的数组,但是其在内存中的位置不一样,也就是说变量 a 和变量 b 指向的不是同一个对象,所以他们是不相等的。

基于以上,由此可见:

基本类型的赋值是传值,而引用类型的赋值的传地址。

例如,也就是说引用类型的赋值是对象保存在栈中的地址的赋值,这样的话两个变量就指向同一个对象,因此两者之间操作互相有影响。

浅拷贝

先看一段代码:

var obj1 = {
    'name' : 'zhangsan',
    'age' :  '18',
    'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
var obj3 = shallowCopy(obj1);
function shallowCopy(src) {
    var dst = {};
    for (var prop in src) {
        if (src.hasOwnProperty(prop)) {
            dst[prop] = src[prop];
        }
    }
    return dst;
}
obj2.name = "lisi";
obj3.age = "20";
obj2.language[1] = ["二","三"];
obj3.language[2] = ["四","五"];
console.log(obj1);  
//obj1 = {
//    'name' : 'lisi',
//    'age' :  '18',
//    'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj2);
//obj2 = {
//    'name' : 'lisi',
//    'age' :  '18',
//    'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj3);
//obj3 = {
//    'name' : 'zhangsan',
//    'age' :  '20',
//    'language' : [1,["二","三"],["四","五"]],
//};

先定义个一个原始的对象 obj1,然后使用赋值得到第二个对象 obj2,然后通过浅拷贝,将 obj1 里面的属性都赋值到 obj3 中。也就是说:

  • obj1:原始数据
  • obj2:赋值操作得到
  • obj3:浅拷贝得到

然后我们改变 obj2 的 name 属性和 obj3 的 age 属性,可以看到,改变赋值得到的对象 obj2 同时也会改变原始值 obj1。

而改变浅拷贝得到的的 obj3 则不会改变原始对象 obj1

这就可以说明赋值得到的对象 obj2 只是将指针改变,其引用的仍然是同一个对象,
而浅拷贝得到的的 obj3 则是重新创建了新对象。

然而,我们接下来来看一下改变引用类型会是什么情况呢,我又改变了赋值得到的对象 obj2 和浅拷贝得到的 obj3 中的 language 属性的第二个值和第三个值(language 是一个数组,也就是引用类型)。结果见输出,可以看出来,无论是修改赋值得到的对象 obj2 和浅拷贝得到的 obj3 都会改变原始数据。

因为浅拷贝只复制一层对象的属性,并不包括对象里面的为引用类型的数据。
所以就会出现改变浅拷贝得到的 obj3 中的引用类型时,会使原始数据得到改变。

由此,我们可以知道深拷贝与浅拷贝的一个区别:

  1. 深拷贝:将 B 对象拷贝到 A 对象中,包括 B 里面的子对象
  2. 浅拷贝:将 B 对象拷贝到 A 对象中,但不包括 B 里面的子对象

深拷贝:

深拷贝是对对象以及对象的所有子对象进行拷贝。

思路就是递归调用刚刚的浅拷贝,把所有属于对象的属性类型都遍历赋给另一个对象即可。

相关文章
|
4月前
|
JavaScript
vue中使用 HotKeys.js 教程(按键响应、快捷键开发)
vue中使用 HotKeys.js 教程(按键响应、快捷键开发)
204 0
|
1月前
|
JavaScript 前端开发
js教程——函数
js教程——函数
36 4
|
1月前
|
JavaScript 前端开发 Java
Node.js 教程
10月更文挑战第1天
41 0
|
3月前
|
JavaScript NoSQL 前端开发
|
4月前
|
JSON JavaScript 数据格式
vue 绘制波形图 wavesurfer.js (音频/视频) 【实用教程】
vue 绘制波形图 wavesurfer.js (音频/视频) 【实用教程】
421 3
|
4月前
|
JavaScript
vue 农历日期转公历日期(含插件 js-calendar-converter 使用教程)
vue 农历日期转公历日期(含插件 js-calendar-converter 使用教程)
237 0
|
4月前
|
开发框架 监控 JavaScript
企业级node.js开发框架 【egg.js】 实用教程
企业级node.js开发框架 【egg.js】 实用教程
70 0
|
4月前
命令行加载特效 【cli-spinner.js】 实用教程
命令行加载特效 【cli-spinner.js】 实用教程
48 0
|
4月前
|
JavaScript
文件查询匹配神器 【glob.js】 实用教程
文件查询匹配神器 【glob.js】 实用教程
70 0
|
4月前
|
JavaScript 数据安全/隐私保护
node.js 命令行的命令注册和配置工具(最新版) commander.js 实用教程(含自研脚手架的创建流程)
node.js 命令行的命令注册和配置工具(最新版) commander.js 实用教程(含自研脚手架的创建流程)
185 0
下一篇
无影云桌面