文章目录
一、原始值与引用值
ES 变量可以包含两种不同类型的数据:原始值和引用值。
原始值就是最简单的数据,保存原始值的变量是按值访问的。引用值则是由多个值构成的对象,引用值是保存在内存中的对象,JS不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用而非实际的对象本身。所以保存引用值的变量是按引用访问的。
1、动态属性
原始值和引用值的定义方式很类似,都是创建一个变量,然后赋值。但是变量在保存了这个值之后,可以对这个值做什么,则不相同。
引用值: 可以随时添加、修改和删除其属性和方法。
原始值: 不能有属性,尽管尝试给原始值添加属性不会报错。打印出现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
2、复制值
除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。
原始值:复制时,会产生一个副本,等同于被复制的值。两个变量独立使用,互不干扰。
引用值:复制时,复制的是指向引用对象的指针
,两个变量指向同一个对象,一个对象上面的变化会影响另一个对象。
//原始值:复制时
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}
3、传递参数⭐⭐
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 在函数内部被重写时,它变成了一个指向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了。
二、执行上下文与作用域
执行上下文(以下简称“上下文”)的概念在 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()的上下文。
用图来展示作用域之间的关系:
![在这里插入图片描述](https://img-blog.csdnimg.cn/36cb1c863474448a8b5eed56c6476aa2.png
不同颜色的矩形代表不同的上下文。内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问到内部上下文中的任何东西。
1、作用域链增强
with try…catch(这里不详细展开)
2、变量声明
var、let、const
var 局部变量,全局变量;变量提升;
let 块作用域;不能变量提升;同一块中不允许冗余声明,会报错;暂时性死区;
const 块作用域;不能变量提升;同一块中不允许冗余声明,会报错;暂时性死区;const声明的时候必须对其初始化。
三、垃圾回收
JS是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。JS通过自动内存管理实现内存分配和闲置资源回收。思路:确定哪个变量不会再使用,然后释放它占用的内存,这个过程是周期性的,即垃圾回收程序每隔一定时间就会自动运行。垃圾回收过程是一个近似且不完美的方案。
我们以函数中局部变量的正常生命周期为例。函数中的局部变量会在函数执行时存在。此时,栈(或堆)内存会分配空间以保存相应的值。函数在内部使用了变量,然后退出。此时,就不再需要那个局部变量了,它占用的内存可以释放,供后面使用。这种情况下显然不再需要局部变量了,但并不是所有时候都会这么明显。垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收内存。
垃圾回收如何标记未使用的变量的方式有:标记清理 引用计数
1、标记清理(最常用)
垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了
,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。
2、引用计数
是对每个值都记录它被引用的次数。
存在循环引用问题
,引用数永远不会变为0,如果多次调用会导致大量内存永远不会被释放。
function problem() {
let objectA = new Object();
let objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
3、 内存管理
将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据,如果数据不再必要,那么把它设置为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;
注意:解除对一个值的引用并不会自动导致相关内存被回收,解除引用的关键在于确保相关的值不在上下文里了,因此在下次垃圾回收的时候会被回收。
a、通过const和let声明提升性能
const和let有助于改善代码风格,而且同样有助于改进垃圾回收的过程。因为const和let都以块(而非函数)为作用域,所以相比于使用var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收该回收的内存。在块作用域比函数作用域更早终止的情况下,这就有可能发生。
b、隐藏类和删除操作
根据 JavaScript 所在的运行环境,有时候需要根据浏览器使用的 JavaScript 引擎来采取不同的性能优化策略。截至 2017 年,Chrome 是最流行的浏览器,使用 V8 JavaScript 引擎。V8 在将解释后的 JavaScript代码编译为实际的机器码时会利用“隐藏类”。如果你的代码非常注重性能,那么这一点可能对你很重要。运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象性能会更好,V8 会针对这种情况进行优化,但不一定总能够做到。
(不详细展开)
c、内存泄漏
意外声明全局变量是最常见但也最容易修复的内存泄漏问题。下面的代码没有使用任何关键字声明变量:
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 的内容很大(不止是一个小字符
串),那可能就是个大问题了。
4、静态分配与对象池(不展开讨论)
总结:
- 原始值可能是这6中原始数据类型之一:undefined、null、string、boolean、number、symbol
- 原始值
大小固定
,因此保存在栈内存
上 - 从一个变量到另一个变量
复制原始值
会创建
该值得第二个副本
- 引用值是对象,存储在
堆内存
上 - 包含引用值的变量实际上只包含指向相应对象的一个
指针
,而不是对象本身 - 从一个变量到另一个变量的复制
只会复制指针
,因此结果是两个变量都指向同一个对象 - typeof 操作符可以确定值的原始类型,而 instanceof 操作符用于确保值的引用类型
- 任何变量都存在于某个执行上下文中(也称为作用域)。这个上下文作用域决定了变量的生命周期,以及它们可以访问代码的哪些部分。
- 执行上下文分
全局上下文
、函数上下文
和块级上下文
。 - 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数
- 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问包含上下文乃至全局上下文中的变量。
- 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据
- 变量的执行上下文用于确定什么时候
释放内存
- JS是使用
垃圾回收
的编程语言,开发者不需要操心内存分配和回收。 - 离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。
- 主流的垃圾回收算法是
标记清理
,即先给当前不使用的值加上标记,再回来回收它们的内存。 引用计数
是另一种垃圾回收策略,需要记录值被引用了多少次。JavaScript 引擎不再使用这种算法,但某些旧版本的 IE 仍然会受这种算法的影响,原因是 JavaScript 会访问非原生 JavaScript 对象(如 DOM 元素)。引用计数
在代码中存在循环引用
时会出现问题。- 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象、全局对象的属性和循环引用都应该在不需要时解除引用