第 4 章 变量、作用域与内存



前言

虽然之前在课堂上已经学过 JavaScript 的相关用法了,但是现在要开始找工作的时候才发现好多知识点都忘记了(正所谓基础不牢,地动山摇😂),而且有些知识点学校教材上都没有,便有了要写这个 JavaScript 学习系列文章的想法。

由于博主只是做知识点总结与学习笔记分享,导致本系列文章对于 JavaScript 的使用方面介绍较少,还望读者能够自己多勤学苦练。废话不多说,让我们开始第 4 章 变量、作用域与内存 的学习吧。


一、原始值与引用值

ECMAScript 变量可以包含两种不同类型的数据:原始值和引用值。原始值(primitive value)就是最简单的数据,大小固定因此保存在栈内存上,引用值(reference value)则是由多个值构成的对象,存储在堆内存上。

保存原始值的变量是按值(by value)访问的,因为我们操作的就是存储在变量中的实际值。在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身。为此,保存引用值的变量是按引用(by reference)访问的。

1.动态属性

原始值和引用值的定义方式很类似,都是创建一个变量,然后给它赋一个值。

// 原始值不能有属性,尽管尝试给原始值添加属性不会报错
let name = "Nicholas"; 
name.age = 27; 
console.log(name.age); // undefined

// 对于引用值而言,可以随时添加、修改和删除其属性和方法
let person = new Object(); 
person.name = "Nicholas"; 
console.log(person.name); // "Nicholas"

2.复制值

在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。

let num1 = 5; 
let num2 = num1; 

在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来,如下面的例子所示:

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

3.传递参数

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

注意:ECMAScript 中函数的参数就是局部变量

4.确定类型

typeof 虽然对原始值很有用,但它对引用值的用处不大。我们通常不关心一个值是不是对象,而是想知道它是什么类型的对象。为了解决这个问题,ECMAScript 提供了 instanceof 操作符,语法如下:

result = variable instanceof constructor

二、执行上下文与作用域

执行上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。

1.作用域链增强

虽然执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但有其他方式来增强作用域链。某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:

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

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

2.变量声明

  1. 使用 var 的函数作用域声明
    在使用 var 声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函数的局部上下文。在 with 语句中,最接近的上下文也是函数上下文。如果变量未经声明就被初始化了,那么它就会自动被添加到全局上下文,如下面的例子所示:

    function add(num1, num2) { 
     var sum = num1 + num2; 
     return sum; 
    } 
    let result = add(10, 20); // 30 
    console.log(sum); // 报错:sum 在这里不是有效变量
    
  2. 使用 let 的块级作用域声明
    ES6 新增的 let 关键字跟 var 很相似,但它的作用域是块级的,这也是 JavaScript 中的新概念。块级作用域由最近的一对包含花括号{}界定。换句话说,if 块、while 块、function 块,甚至连单独的块也是 let 声明变量的作用域。

  3. 使用 const 的常量声明
    除了 let,ES6 同时还增加了 const 关键字。使用 const 声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值。

  4. 标识符查找
    当在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。搜索开始于作用域链前端,以给定的名称搜索对应的标识符。如果在局部上下文中找到该标识符,则搜索停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。(注意,作用域链中的对象也有一个原型链,因此搜索可能涉及每个对象的原型链。)这个过程一直持续到搜索至全局上下文的变量对象。如果仍然没有找到标识符,则说明其未声明。

三、垃圾回收

垃圾回收基本思路:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。

1.标记清理

JavaScript 最常用的垃圾回收策略是标记清理(mark-and-sweep)。当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。

2.引用计数

另一种没那么常用的垃圾回收策略是引用计数(reference counting)。其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存。

引用计数在代码中存在循环引用时会出现问题。解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象、全局对象的属性和循环引用都应该在不需要时解除引用。

3.性能

垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度很重要。

4.内存管理

  1. 使用 const 和 let 声明提升性能
  2. 隐藏类和删除操作
  3. 内存泄漏
  4. 静态分配与对象池

总结

JavaScript 变量可以保存两种类型的值:原始值和引用值。原始值可能是以下 6 种原始数据类型之一:Undefined、Null、Boolean、Number、String 和 Symbol。

任何变量(不管包含的是原始值还是引用值)都存在于某个执行上下文中(也称为作用域)。这个上下文(作用域)决定了变量的生命周期,以及它们可以访问代码的哪些部分。

JavaScript 是使用垃圾回收的编程语言,开发者不需要操心内存分配和回收。

以上就是本篇文章的全部内容了,下一篇文章我们将学习“第 5 章 基本引用类型”,如果想要第一时间获取相关文章,欢迎关注博主,并订阅专栏。您的支持与鼓励将成为我不断前行地动力!

最后,如果本篇文章对正在阅读的您有所帮助或启发,请不要吝啬您的点赞收藏评论及分享,这样就有可能帮助到更多的人了。同时也欢迎留下您遇到的问题,让我们一起探讨学习,共同进步!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值