变量、作用域与内存

文章讨论了JavaScript中的原始值与引用值的区别,包括它们的复制和传递方式。接着介绍了作用域的概念,包括函数作用域、块级作用域以及变量声明。此外,还讲解了垃圾回收机制,如标记清理和引用计数策略。最后,提到了内存管理和性能优化,如通过const和let提升性能以及如何处理内存泄漏问题。
摘要由CSDN通过智能技术生成

目录

 原始值与引用值

动态属性

复制值

传递参数

确定类型

执行上下文与作用域 

 作用域链增强 

 变量声明

1.使用var 的函数作用域声明

2. 使用let 的块级作用域声明

3.使用const 的常量声明

 标识符查找 

垃圾回收

标记清理(最常用)

引用计数

 内存管理

通过const和let声明提升性能 

隐藏类和删除操作

内存泄漏

静态分配与对象池

总结


 原始值与引用值

ES 变量可以包含两种不同类型的数据:原始值和引用值。
原始值就是最简单的数据,保存原始值的变量是按值访问的。引用值则是由多个值构成的对象,引用值是保存在内存中的对象,JS不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用而非实际的对象本身。所以保存引用值的变量是按引用访问的。


动态属性

原始值和引用值的定义方式很类似,都是创建一个变量,然后赋值。但是变量在保存了这个值之后,可以对这个值做什么,则不相同。

  • 引用值: 可以随时添加、修改和删除其属性和方法。
  • 原始值: 不能有属性,尽管尝试给原始值添加属性不会报错。打印出现undefined 只有引用值可以动态添加后面可以使用的属性

原始类型的初始化可以只使用原始字面量的形式,如果使用的是new关键字,则JS会创建一个Object类型的实例。

let name1 = "Nick"
let name2 = new String("Amy")
name1.age = 21;
name2.age = 22; 
console.log(name1.age) //undefined  尽管尝试给原始值添加属性也不会报错
console.log(name2.age) // 22
console.log(typeof name1) // string
console.log(typeof name2) // object

复制值

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

  • 原始值:复制时,会产生一个副本,等同于被复制的值。两个变量独立使用,互不干扰。

  • 引用值:复制时,复制的是指向引用对象的指针,两个变量指向同一个对象,一个对象上面的变化会影响另一个对象。

//原始值:复制时
let num1 = 5; 
let num2 = num1;
num1 = 10;
console.log(num1,num2) //10 5
//引用值:复制时
let person1 = {name:"xx",age:21}
let person2 = person1;
person2.age = 22;
console.log(person1,person2) //{name: 'xx', age: 22} {name: 'xx', age: 22}

传递参数

ES 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,就像一个变量复制到另一个变量一样。
如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样。

//参数被赋值为原始值
function addTen(num) { 
 	num += 10; 
 	return num; 
} 
let count = 20;
let result = addTen(count); 
console.log(count); // 20,没有变化
console.log(result); // 30
//参数被赋值为引用类型
function setName(obj) {
 	obj.name = "Nicholas"; 
} 
let person = new Object(); 
setName(person); 
console.log(person.name); // "Nicholas"

注意:函数的参数虽然为对象,但是依旧是按值传递的,我们进一步理解

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

当person传入setName()时,其name属性被设置为“wwy”.然后变量obj被设置为一个新对象且name属性被设置为“xx”。如果向参数赋值是按照引用赋值,那么person的name应该变为“xx”, 因为假设形参obj拿到的是person的引用,而不是person引用的值,那么当函数内部生成新对象,并对obj 进行重新指向时,形参obj的指向改变,外部的person的指向也因该改变,但是结果证明console.log(person.name) 输出的为"wwy",所以即使函数参数是引用类型,也是按值传递。 当 obj 在函数内部被重写时,它变成了一个指向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了。


总结:所有参数传递都是按值传递,会将函数外的值复制到函数内部。对于原始值的复制会直接复制值,与原变量独立,不会修改。对于引用值,就跟引用值的复制一样,复制的是指针,因此在函数中进行对应的修改会保存,但如果将储存地址的指针初始化为别的变量(如上面一个例子)则储存地址的指针消失了,不再指向原来的变量,接下来的修改就不会影响到原来的变量。 即按值进行传递,但按照引用去访问。
 

确定类型

typeof

typeof 操作符可以用来判断一个变量是否为原始类型。更确切地说,它是判断一
个变量是否为字符串、数值、布尔值或undefined 的最好方式。

let s = "Nicholas";
let b = true;
let i = 22;
let u;
let n = null;
let o = new Object();
console.log(typeof s); // string
console.log(typeof i); // number
console.log(typeof b); // boolean
console.log(typeof u); // undefined
console.log(typeof n); // object
console.log(typeof o); // object

 instanceof

 如果想判断一个引用值是什么类型的对象就无法使用typeof了,因为它只能判断出是一个对象。
ECMAScript 提供了instanceof 操作符,语法如下:

result = variable instanceof constructor
console.log(person instanceof Object); // 变量person 是Object 吗?
console.log(colors instanceof Array); // 变量colors 是Array 吗?
console.log(pattern instanceof RegExp); // 变量pattern 是RegExp 吗?

按照定义,所有引用值都是Object 的实例,因此通过instanceof 操作符检测任何引用值和Object 构造函数都会返回true。类似地,如果用instanceof 检测原始值,则始终会返回false,因为原始值不是对象。

其实 instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。

执行上下文与作用域 

执行上下文(以下简称“上下文”)的概念在 JavaScript 中是颇为重要的。变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。
全局上下文是最外层的上下文。根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,全局上下文就是我们常说的 window 对象,因此所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。
上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。

var color = "blue";
function changeColor(){
    let anotherColor = "red";
    function swapColor(){
        let tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
        //这里能访问到tempColor、anotherColor、color
    }
    //这里能访问到anotherColor和color
    swapColor();
}
//这里只能访问到color
changeColor();


swapColors()局部上下文的作用域链中有 3 个对象:swapColors()的变量对象、changeColor()的变量对象和全局变量对象。

swapColors()的局部上下文首先从自己的变量对象开始搜索变量和函数,搜不到就去搜索上一级变量对象。

changeColor()上下文的作用域链中只有 2 个对象:它自己的变量对象和全局变量对象。因此,它不能访问 swapColors()的上下文。

不同颜色的矩形代表不同的上下文。内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问到内部上下文中的任何东西。 

 作用域链增强 

某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:

  • try/catch 语句的catch 块
  • with 语句

这两种情况下,都会在作用域链前端添加一个变量对象。对with 语句来说,会向作用域链前端添加指定的对象;对catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。

function buildUrl() {
	let qs = "?debug=true";
	with(location){
		let url = href + qs;
	}
	return url;
}

with 语句将location 对象作为上下文,因此location 会被添加到作用域链前端。
buildUrl()函数中定义了一个变量qs。当with 语句中的代码引用变量href 时,实际上引用的是location.href,也就是自己变量对象的属性。

 变量声明

1.使用var 的函数作用域声明

在使用var 声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函数的局部上下文。在with 语句中,最接近的上下文也是函数上下文。

function add(num1, num2) {
var sum = num1 + num2;
return sum;
}
let result = add(10, 20); // 30
console.log(sum); // 报错:sum 在这里不是有效变量

如果变量未经声明就被初始化了,那么它就会自动被添加到全局上下文。如下所示,没有对sum进行声明就会被添加到全局上下文,函数退出后仍然可以访问。

function add(num1, num2) {
sum = num1 + num2;
return sum;
}
let result = add(10, 20); // 30
console.log(sum); // 30

var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作“提升,

声明会得到提升,但赋值操作不会。

console.log(name); // undefined
var name = 'Jake';

2. 使用let 的块级作用域声明

块级作用域由最近的一对包含花括号{}界定。,if 块、while 块、function 块,甚至连单独的块也是let 声明变量的作用域。

3.使用const 的常量声明

使用const 声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值

const 声明只应用到顶级原语或者对象。换句话说,赋值为对象的const 变量不能再被重新赋值为其他引用值,但对象的键则不受限制。

const o1 = {};
o1 = {}; // TypeError: 给常量赋值
const o2 = {};
o2.name = 'Jake';
console.log(o2.name); // 'Jake'
o2.name = 'Bob';
console.log(o2.name); // 'Bob'

即const声明用于对象时,这个对象本身不能被赋其他值,但这个对象的属性是可以进行修改的。

 标识符查找 

查找标识符时,会按照作用域链的顺序从上往下查找,即先从当前的局部上下文开始,找到就结束,没找到就去作用域链中的下一个上下文,直到搜索至全局上下文的变量对象为止。

对这个搜索过程而言,引用局部变量会让搜索自动停止,而不继续搜索下一级变量对象。也就是说,如果局部上下文中有一个同名的标识符,那就不能在该上下文中引用父上下文中的同名标识符,如下面的例子所示,在函数上下文中定义同名变量就无法引用全局中的变量了:
 

var color = 'blue';
function getColor() {
	let color = 'red';
	return color;
}
console.log(getColor()); // 'red'

垃圾回收

JS是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。JS通过自动内存管理实现内存分配和闲置资源回收。思路:确定哪个变量不会再使用,然后释放它占用的内存,这个过程是周期性的,即垃圾回收程序每隔一定时间就会自动运行。垃圾回收过程是一个近似且不完美的方案。
我们以函数中局部变量的正常生命周期为例。函数中的局部变量会在函数执行时存在。此时,栈(或堆)内存会分配空间以保存相应的值。函数在内部使用了变量,然后退出。此时,就不再需要那个局部变量了,它占用的内存可以释放,供后面使用。这种情况下显然不再需要局部变量了,但并不是所有时候都会这么明显。垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收内存。
垃圾回收如何标记未使用的变量的方式有:标记清理 引用计数


标记清理(最常用)

垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。


引用计数

是对每个值都记录它被引用的次数。
存在循环引用问题,引用数永远不会变为0,如果多次调用会导致大量内存永远不会被释放。

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

 内存管理

将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据,如果数据不再必要,那么把它设置为null,从而释放其引用。这也可以叫做解除引用。这个最适合全局对象的属性。局部变量在超出作用域后会被自动解除引用。

function createPerson(name){ 
	 let localPerson = new Object(); 
	 localPerson.name = name; 
	 return localPerson; 
} 
let globalPerson = createPerson("Nicholas"); 

// 解除 globalPerson 对值的引用
globalPerson = null;

在上面代码中,变量globalPerson保存着createPerson()函数调用的返回值。在createPerson()内部,localPerson创建了一个对象,并添加了一个name属性,最后localPerson作为函数的返回值被返回,然后被赋值给了globalPerson。localPerson这个局部变量在函数执行完之后就会被解除引用,垃圾回收。但是globalPerson是一个全局变量,则不会被处理,需要显示的去处理,应该在不再需要时手动解除其引用。比如最后一行使 globalPerson = null;

注意:解除对一个值的引用并不会自动导致相关内存被回收,解除引用的关键在于确保相关的值不在上下文里了,因此在下次垃圾回收的时候会被回收。

通过const和let声明提升性能 

const和let有助于改善代码风格,而且同样有助于改进垃圾回收的过程。因为const和let都以块(而非函数)为作用域,所以相比于使用var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收该回收的内存。在块作用域比函数作用域更早终止的情况下,这就有可能发生。

隐藏类和删除操作


根据 JavaScript 所在的运行环境,有时候需要根据浏览器使用的 JavaScript 引擎来采取不同的性能优化策略。截至 2017 年,Chrome 是最流行的浏览器,使用 V8 JavaScript 引擎。V8 在将解释后的 JavaScript代码编译为实际的机器码时会利用“隐藏类”。如果你的代码非常注重性能,那么这一点可能对你很重要。运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象性能会更好,V8 会针对这种情况进行优化,但不一定总能够做到。
 

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

V8 会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原型。

内存泄漏

意外声明全局变量是最常见但也最容易修复的内存泄漏问题。下面的代码没有使用任何关键字声明变量:

function setName() { 
 	name = 'Jake'; 
}

 

此时,解释器会把变量 name 当作 window 的属性来创建(相当于 window.name = ‘Jake’)。可想而知,在 window 对象上创建的属性,只要 window 本身不被清理就不会消失。这个问题很容易解决,只要在变量声明前头加上 var、let 或 const 关键字即可,这样变量就会在函数执行完毕后离开作用域。
使用 JavaScript 闭包很容易在不知不觉间造成内存泄漏。请看下面的例子:

let outer = function() { 
	let name = 'Jake'; 
 	return function() { 
 		return name; 
 	}; 
 }; 

调用 outer()会导致分配给 name 的内存被泄漏。以上代码执行后创建了一个内部闭包,只要返回
的函数存在就不能清理 name,因为闭包一直在引用着它。假如 name 的内容很大(不止是一个小字符串),那可能就是个大问题了。

静态分配与对象池


为了提升JavaScript 性能,最后要考虑的一点往往就是压榨浏览器了。此时,一个关键问题就是如何减少浏览器执行垃圾回收的次数。开发者无法直接控制什么时候开始收集垃圾,但可以间接控制触发垃圾回收的条件。理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因释放内存而损失的性能。

总结

  • 原始值可能是这6中原始数据类型之一:undefined、null、string、boolean、number、symbol
  • 原始值大小固定,因此保存在栈内存上
  • 从一个变量到另一个变量复制原始值会创建该值得第二个副本
  • 引用值是对象,存储在堆内存上
  • 包含引用值的变量实际上只包含指向相应对象的一个指针,而不是对象本身
  • 从一个变量到另一个变量的复制只会复制指针,因此结果是两个变量都指向同一个对象
  • typeof 操作符可以确定值的原始类型,而 instanceof 操作符用于确保值的引用类型
  • 任何变量都存在于某个执行上下文中(也称为作用域)。这个上下文作用域决定了变量的生命周期,以及它们可以访问代码的哪些部分。
  • 执行上下文分全局上下文、函数上下文和块级上下文。
  • 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数
  • 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问包含上下文乃至全局上下文中的变量。
  • 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据
  • 变量的执行上下文用于确定什么时候释放内存
  • JS是使用垃圾回收的编程语言,开发者不需要操心内存分配和回收。
  • 离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。
  • 主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。
  • 引用计数是另一种垃圾回收策略,需要记录值被引用了多少次。JavaScript 引擎不再使用这种算法,但某些旧版本的 IE 仍然会受这种算法的影响,原因是 JavaScript 会访问非原生 JavaScript 对象(如 DOM 元素)。
  • 引用计数在代码中存在循环引用时会出现问题。
  • 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象、全局对象的属性和循环引用都应该在不需要时解除引用
     
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值