JavaScript高级程序设计学习(3)

本文详细阐述了JavaScript中的变量类型(原始值和引用值)、值传递与引用传递、执行上下文和作用域链、垃圾回收机制(包括标记清理和引用计数),以及如何通过对象池优化内存管理,以提高性能和避免内存泄漏。
摘要由CSDN通过智能技术生成

其实在之前的JavaScript高级程序设计学习(2)文章内讲完let,var,const声明之后就戛然而止了,后面应当有数据类型,操作符,控制流语句等等,我个人觉得在学习数据类型前应当先了解下变量,作用域与内存这块相关的知识,至于控制流语句,个人还在考虑要不要写出来。所以这里的顺序和红宝书上面略有偏差。

四,变量、作用域与内存

4.1 原始值与引用值

ECMAScript 变量可以包含两种不同类型的数据:原始值和引用值。
原始值(primitive value)就是最简单的数据,引用值(reference value)则是由多个值构成的对象。
在把一个值赋给变量时,JavaScript 引擎必须确定这个值是原始值还是引用值。 6 种原始值:Undefined、Null、Boolean、Number、String 和 Symbol。保存原始值的变量是按值(byvalue)访问的,因为我们操作的就是存储在变量中的实际值。
引用值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身。为此,保存引用值的变量是按引用(by reference)访问的。
简单来说就在JavaScript中传值有两种,一个是值传递,一个是地址值传递。

4.1.1 动态属性

原始值和引用值的定义方式很类似,都是创建一个变量,然后给它赋一个值。不过,在变量保存了这个值之后,可以对这个值做什么,则大有不同。对于引用值而言,可以随时添加、修改和删除其属性和方法。
注意这里不管是原始值和引用值都可以添加动态属性,只不过原始值添加的动态属性获取的时候是undefined。

let person = new Object();
person.name = "Nicholas";
console.log(person.name); // "Nicholas"

let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined

4.1.2 复制值

除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。

let num1 = 5;
let num2 = num1;
//这里,num1 包含数值 5。当把 num2 初始化为 num1 时,num2 也会得到数值 5。
//这个值跟存储在num1 中的 5 是完全独立的,因为它是那个值的副本。

在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。注意这个地方是位置,而不是值

let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); // "Nicholas" 

4.1.3 传递参数

ECMAScript 中所有函数的参数都是按值传递的。这就代表着不论是基本数据类型还是引用数据类型,在作为参数传递给函数的时候都是值传递,看到这个地方之前我一直认为函数的参数如果是对象的话是引用传递。

function addTen(num) {
 num += 10;
 return num;
}
let count = 20;
let result = addTen(count);
console.log(count); // 20,没有变化
console.log(result); // 30 

这里,函数 addTen()有一个参数 num,它其实是一个局部变量。在调用时,变量 count 作为参数传入。count 的值是 20,这个值被复制到参数 num 以便在 addTen()内部使用。在函数内部,参数 num的值被加上了 10,但这不会影响函数外部的原始变量 count。参数 num 和变量 count 互不干扰。

function setName(obj) {
 obj.name = "Nicholas";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"

这个地方该如何理解呢,创造了一个person对象,然后把person对象的地址值赋值给了形参obj,注意是地址值(值而不是引用),此时形参obj和对象person指向同一个地址,在函数内obj. name = “Nicholas”,即在该地址的对象内添加name属性,值为“Nicholas”,此时该地址有了name属性,外部的person由于指向的是该地址,所以person对象也可以访问到name属性。请你始终记住这个过程仍是值传递,只不过这个地方是把地址值传过去。
为了让大家更好理解,请看下面的例子。

function setName(obj) {
 obj.name = "Nicholas";
 obj = new Object();
 obj.name = "Greg";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas" 

和上面的代码一样,不一样的是多了两行代码,让我们看看这两行代码内发生了什么事情。
在函数内创建了一个新的对象,即开辟了新的内存空间,并把该内存空间的地址值赋值给obj,此时内存里做的事情,就是把地址值赋值给obj,让obj从指向person的地址值变为了新的地址,此时并不会影响person,obj.name = “Greg”,这一步的操作就是在新的内存空间了,不再是之前的name的内存空间了。
通过上面的例子,可以很清晰的了理解为什么函数的参数都是值传递。
在JavaScript中,只有ES6的模块化用到了引用传递。
这里说一下题外话,引用传递和值传递的区别是什么,值传递他会额外的开辟内存空间,相当于复制了一个房卡,而引用传递是两个人共用一个房卡。

4.2 执行上下文与作用域

变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每一个上下文都有一个关联的变量对象,上下文中所有的变量和方法都存在这个对象上面,我们可以理解成每个上下文都有一个存放需要用到变量和方法的书包。书包包括书包内的东西,虽然无法通过代码访问变量对象,但后台处理数据会用到它。
全局上下文是最外层的上下文。在浏览器中,全局上下文就是我们常说的 window 对象。
上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。
ECMAScript程序的执行流就是通过上下文栈进行控制的。在代码进行的时候,首先栈的最底部是全局上下文,然后在执行某个方法的时候,把这个方法压栈,优先执行该方法,当该方法执行完毕的时候,弹栈,把控制权再返回给上一个上下文。
上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。
代码正在执行的上下文的变量对象始终位于作用域链的最前端,然后是上一个上下文的变量对象,以此类推直至全局上下文的变量对象,全局上下文的变量对象始终是作用域链的最后一个变量对象。所以作用域链就是指向上一个上下文栈的变量对象的链条。下面看一个例子。

var color = "blue";
function changeColor() {
 let anotherColor = "red";
 function swapColors() {
 let tempColor = anotherColor;
 anotherColor = color;
 color = tempColor;
 // 这里可以访问 color、anotherColor 和 tempColor
 }
 // 这里可以访问 color 和 anotherColor,但访问不到 tempColor
 swapColors();
}
// 这里只能访问 color 

内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西。

4.3 垃圾回收机制

我们以函数中局部变量的正常生命周期为例。函数中的局部变量会在函数执行时存在。此时,栈(或堆)内存会分配空间以保存相应的值。函数在内部使用了变量,然后退出。此时,就不再需要那个局部变量了,它占用的内存可以释放,供后面使用。
在浏览器的发展史上,用到过两种主要的标记策略:标记清理和引用计数

4.3.1 标记清理

官方的解释比较晦涩,我这里通过自己的理解总结了一下,就是在垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内
存清理,销毁带标记的所有值并收回它们的内存。标记清理的频率每个浏览器有自己的频率(JavaScript宿主环境)。

4.3.2 引用计数

其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存。
但引用计数有个严重的bug,就是两个变量互相引用,那么他们的计数就永远不可能为0,这样就会导致内存泄漏的情况,如下面代码

function problem() {
 let objectA = new Object();
 let objectB = new Object();
 objectA.someOtherObject = objectB;
 objectB.anotherObject = objectA;
}

4.3.3 性能

书上说了一大堆,可以总结为一句话:为了更好的性能应当减少垃圾回收的次数和垃圾回收的时间。换句话来说就是不用的对象要及时赋值为null解除引用(释放内存)。这里再简单的提一下浏览器对开启垃圾回收机制的策略,在早期的IE中它的策略是根据分配数,比如分配了 256 个变量、4096 个对象/数组字面量和数组槽位(slot),或者 64KB 字符串。只要满足其中某个条件,垃圾回收程序就会运行。这样实现的问题在于,分配那么多变量的脚本,很可能在其整个生命周期内始终需要那么多变量,结果就会导致垃圾回收程序过于频繁地运行。由于对性能的严重影响,IE7最终更新了垃圾回收程序,如果垃圾回收程序回收的内存不到已分配的 15%,这些变量、字面量或数组槽位的阈值就会翻倍。如果有一次回收的内存达到已分配的 85%,则阈值重置
为默认值。
有些浏览器可以主动触发垃圾回收,在 IE 中,window.CollectGarbage()方法会立即触发垃圾回收。在 Opera 7 及更高版本中,调用 window.opera.collect()也会启动垃圾回收程序。

4.3.4 内存管理

这一条主要是对上一条性能的补充,我们该如何去做可以提高代码的性能?
1. 通过 const 和 let 声明提升性能
ES6 增加这两个关键字不仅有助于改善代码风格,而且同样有助于改进垃圾回收的过程。因为 const和 let 都以块(而非函数)为作用域,所以相比于使用 var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。在块作用域比函数作用域更早终止的情况下,这就有可能发生。
2. 隐藏类和删除操作
V8 在解释后的 JavaScript代码编译为实际的机器码时会利用“隐藏类”。如果你的代码非常注重性能,那么这一点可能对你很重要。运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象性能会更好。
这里面书上解释的比较晦涩

function Article() {
 this.title = 'Inauguration Ceremony Features Kazoo Band';
}
let a1 = new Article();
let a2 = new Article(); 

V8 会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原型。假设之后又添加了下面这行代码:

a2.author = 'Jake';

此时就生成了两个隐藏类,因为你给Article的实例动态添加了author属性,但在原有的类中并没有该属性,此时就要新增一个隐藏类,所以为了避免生成多个隐藏类,在编码的时候尽量避免“先创建再补充”(ready-fire-aim)式的动态属性赋值。如

function Article(opt_author) {
 this.title = 'Inauguration Ceremony Features Kazoo Band';
 this.author = opt_author;
}
let a1 = new Article();
let a2 = new Article('Jake'); 

这样,两个实例对应的就是同一个隐藏类了。
上面说的是添加,当然删除delete也会导致相同的问题

function Article() {
 this.title = 'Inauguration Ceremony Features Kazoo Band';
 this.author = 'Jake';
}
let a1 = new Article();
let a2 = new Article();
delete a1.author; 

这里正确的操作应当是

function Article() {
 this.title = 'Inauguration Ceremony Features Kazoo Band';
 this.author = 'Jake';
}
let a1 = new Article();
let a2 = new Article();
a1.author = null; 

3. 内存泄漏
导致内存泄漏的情况有很多,比如在块级作用域内没有用任何声明方式,这样就相当于声明了一个全局变量、创建了事件监听或计时器没有清除,闭包等等。
这里很多初学者不理解闭包,所以额外谈一下闭包。
什么是闭包:一个函数对周围的状态捆绑在一起,内层函数访问到外层函数的作用域就会形成闭包,当然综合上面的概念闭包不是一定需要return才会形成,而闭包也不一定会导致内存泄漏。
那怎么样闭包才会导致内存泄漏?当return的函数内有外部函数变量,且全局作用域里面调用外部函数并通过变量接收,这样就导致了内存泄漏,为什么会这样呢?
其实通过上面的学习我们也应该可以猜到,按照原本的执行顺序,当函数结束后,函数作用域内已经子作用域内的变量就在内存中清除掉了,但由于使用了闭包把内部函数return到了全局作用域,那么该函数内用到的变量的生命周期延长了,也就是引用次数加1或标记没有被清除掉,这样就导致了内存泄漏。

4. 静态分配与对象池
浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度。如果有很多对象被初始化,然后一下子又都超出了作用域,那么浏览器就会采用更激进的方式调度垃圾回收程序运行,这样当然会影响性能。
池化技术就是把同质化的资源放到同一个池子里面,就比方说筷子筒,一家有三口人,那么筷子筒里面有三双筷子,假如来了客人,那么就要往筒里面添加筷子,那么此时筒里就有之前加添加后的筷子数量,当然如果来的是外国人,那么不能肯定是不能往筷子筒里面放刀叉的,所以对象池一定是同质化的资源。池化技术应用广泛,如线程池,内存池,连接池等。
下面写一个关于池化技术的优化策略
假如需要设计地图搜索打上mark点的例子,我们肯定不会每次搜索得到的位置,再重新绘制mark点,因为mark点是可以复用的,只需要改一下位置即可,这里可以这么优化

var tooltipFactory =(function(){var toolTipPool =[];
return {
create: function(){
if(!toolTipPool.length){var div = document.createElement(div');document.body.appendchild(div);return div;
else {
return toolTipPool.pop()};
recover:function(tooltipDom){return toolTipPool.push(tooltipDom)
})();

如果池子里(toolTipPool)有,那么我们就用池子里的,而不用再去初始化创建,如果没有我们再去创建。所以代码的操作流程就是,先把上一次的池子结果通过recover回收一下,然后根据后端返回的结果判断池子内是否有该元素,如果没有那么我们就通过div去创建,如果有的话那么就用池子内的,这样就减少了初始化的操作,提高了性能。
总结就是用池化技术,减少创建新对象。当减少了对象的更替,垃圾回收程序就不会那么频繁地运行。但池子(数组)也会带来一个问题,就是我们一般在编程的时候数组由于 JavaScript 数组的大小是动态可变的,引擎会删除大小为 100 的数组,再创建一个新的大小为200 的数组。垃圾回收程序会看到这个删除操作,说不定因此很快就会跑来收一次垃圾。要避免这种动
态分配操作,可以在初始化时就创建一个大小够用的数组,从而避免上述先删除再创建的操作。不过,必须事先想好这个数组有多大。

  • 50
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值