【JS红宝书学习笔记】第4章 变量、作用域和内存

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

1. 原始值和引用值(面试题)

ECMAScript 变量可以包含两种不同类型的数据:原始值和引用值。原始值(primitive value)就是最简单的数据(Undefined、Null、Boolean、Number、String 和 Symbol,其中之一),引用值(reference value)则是由多个值构成的对象(包括对象、数组、函数)。
引用值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身。为此,保存引用值的变量是按引用(by reference)访问的。

即,在JavaScript中,我们可以分成两种类型:
基本数据类型(6种):Number、String、Boolean、Null、 Undefined、Symbol(ES6),这些类型可以直接操作保存在变量中的实际值。
引用数据类型(1种):Object。

- 动态属性

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

let name1 = "Nicholas";
let name2 = new String("Matt");
name1.age = 27;
name2.age = 26;
console.log(name1.age); // undefined
console.log(name2.age); // 26
console.log(typeof name1); // string
console.log(typeof name2); // object

- 复制值

除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。

let num1 = 5;
let num2 = num1;

在这里插入图片描述
复制的值实际上是一个指针,它指向存储在堆内存中的对象。

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

变量 obj1 保存了一个新对象的实例。然后,这个值被复制到 obj2,此时两个变量都指向了同一个对象。在给 obj1 创建属性 name 并赋值后,通过 obj2 也可以访问这个属性,因为它们都指向同一个对象。图 4-2 展示了变量与堆内存中对象之间的关系。
在这里插入图片描述

- 传递参数

在按值传递参数时,局部变量的变化不会影响外部变量的值。
但是按引用传参,会改变外部变量的值

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 = "Nicholas";
	obj = new Object();
	obj.name = "Greg";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"

函数的参数是局部变量

- 确定类型

typeof 是判断一个变量是否为字符串、数值、布尔值或 undefined 的最好方式。如果该变量是object或者null,都会返回object。typeof 虽然对原始值很有用,但它对引用值的用处不大。

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

如果变量是给定引用类型(由其原型链决定,将在第 8 章详细介绍)的实例,则 instanceof 操作符返回 true。

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,因为原始值不是对象。

☆☆☆一点总结:

  • JavaScript是弱类型语言,开始的时候并不知道变量是什么类型,必须通过存储的具体的值才能判断变量的类型。
    (1)数据类型不同,内存分配不同。
    简单类型的值存放在中,在栈中存放的是对应的。数据大小固定,占用空间小。按值存放,按值访问。
    引用类型对应的值存储在中,在栈中存放的是指向堆内存的地址。数据大小不固定。
    占据空间大。引用值的变量存储的是对实际对象的引用(内存地址),而不是对象本身。
    (2)由于内存分配不同,复制变量时结果不同。
    简单类型赋值,是生成相同的值,两个对象对应不同的地址。
    复杂类型赋值,是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象。
  • JS引用值有什么副作用:
    (1)原始值会受到影响:当多个变量引用同一个引用值时,修改其中一个变量的值会影响其他变量。这可能导致意外的行为,特别是在异步操作中。
let obj1 = { name: "Alice" };
let obj2 = obj1;

obj2.name = "Bob";

console.log(obj1.name); // 输出 "Bob"

(2)对象共享:引用值的传递方式会导致对象在不同地方共享相同的引用,改动一个对象会影响到所有引用它的地方。

let arr1 = [1, 2, 3];
let arr2 = arr1;

arr2.push(4);

console.log(arr1); // 输出 [1, 2, 3, 4]

(3)隐式副作用:某些函数或方法会通过修改引用值而产生隐式副作用,这可能导致代码的可预测性降低。

let numbers = [1, 2, 3];

numbers.sort(); // 改变了原数组

console.log(numbers); // 输出 [1, 2, 3]

要避免引用值的副作用,可以使用深拷贝或者不可变对象来管理数据,以确保每个变量都有自己的副本,从而避免意外的行为和数据共享问题。

2. 执行上下文与作用域

  • 执行上下文分全局上下文、函数上下文和块级上下文。
  • 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃至全局上下文中的变量。
  • 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。
  • 变量的执行上下文用于确定什么时候释放内存。

全局上下文是最外层的上下文。根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,全局上下文就是我们常说的 window 对象。所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。
上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)

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

- 作用域链增强

虽然执行上下文主要有全局上下文函数上下文两种(eval()调用内部存在第三种上下文),但有其他方式来增强作用域链。某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:
(1)try/catch 语句的 catch 块
(2)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,也就是自己变量对象的属性。在引用 qs 时,引用的则是定义在 buildUrl()中的那个变量,它定义在函数上下文的变量对象上。而在 with 语句中使用 var 声明的变量 url 会成为函数上下文的一部分,可以作为函数的值被返回;但像这里使用 let 声明的变量 url,因为被限制在块级作用域(稍后介绍),所以在 with 块之外没有定义。

- 变量声明

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

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

变量 sum 在函数外部是访问不到的。如果省略上面例子中的关键字 var,那么 sum 在 add()被调用之后就变成可以访问的了

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

在调用 add()之后,sum被添加到了全局上下文,在函数退出之后依然存在,从而在后面可以访问到。
在这里插入图片描述
通过在声明之前打印变量,可以验证变量会被提升。声明的提升意味着会输出 undefined 而不是Reference Error:

        console.log(sex); // undefined
        var sex = 'male';
        function f() {
            console.log(name); // undefined
            var name = 'Jake';
        }
        f();

(2) 使用 let 的块级作用域声明
ES6 新增的 let 关键字跟 var 很相似,其作用域是块级的(var作用域:函数作用域。块级作用域由最近的一对包含花括号{}界定。if 块、while 块、function 块,甚至连单独的块也是 let 声明变量的作用域。

        if (true) {
            let a;
        }
        console.log(a); // ReferenceError: a 没有定义
        while (true) {
            let b;
        }
        console.log(b); // ReferenceError: b 没有定义
        function foo() {
            let c;
        }
        console.log(c); // ReferenceError: c 没有定义
        // 这没什么可奇怪的
        // var 声明也会导致报错
        // 这不是对象字面量,而是一个独立的块
        // JavaScript 解释器会根据其中内容识别出它来
        {
            let d;
        }
        console.log(d); // ReferenceError: d 没有定义

let 与 var 的另一个不同之处是在同一作用域内不能声明两次。重复的 var 声明会被忽略,而重复的 let 声明会抛出 SyntaxError。

var a;
var a;
// 不会报错
{
	let b;
	let b;
}
// SyntaxError: 标识符 b 已经声明过了

let 的行为非常适合在循环中声明迭代变量。使用 var 声明的迭代变量会泄漏到循环外部,这种情况应该避免。

        for (var i = 0; i < 10; ++i) { }
        console.log(i); // 10
        for (let j = 0; j < 10; ++j) { }
        console.log(j); // ReferenceError: j 没有定义

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

const a; // SyntaxError: 常量声明时没有初始化
const b = 3;
console.log(b); // 3
b = 4; // TypeError: 给常量赋值

const 除了要遵循以上规则,其他方面与 let 声明是一样的。
const 声明只应用到顶级原语或者对象。换句话说,赋值为对象的 const 变量不能再被重新赋值为其他引用值,但对象的键则不受限制。

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

如果想让整个对象都不能修改,可以使用 Object.freeze(),这样再给属性赋值时虽然不会报错,但会静默失败:

const o3 = Object.freeze({});
o3.name = 'Jake';
console.log(o3.name); // undefined

(4) 标识符查找
局部上下文(找到该标识符则停止,找不到继续找)——全局上下文(找到该标识符则停止,找不到继续找)——未声明

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

调用函数 getColor()时会引用变量 color。为确定 color 的值会进行两步搜索。第一步,搜索 getColor()的变量对象,查找名为 color 的标识符。结果没找到,于是继续搜索下一个变量对象(来自全局上下文),然后就找到了名为 color 的标识符。因为全局变量对象上有 color的定义,所以搜索结束。

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

使用块级作用域声明并不会改变搜索流程,但可以给词法层级添加额外的层次:

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

3. 垃圾回收

JavaScript 是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。通过自动内存管理实现内存分配和闲置资源回收。基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。

离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。

- 标记清理(最常用)

主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。

- 引用计数

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

- 性能

垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度很重要。尤其是在内存有限的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。开发者不知道什么时候运行时会收集垃圾,因此最好的办法是在写代码时就要做到:无论什么时候开始收集垃圾,都能让它尽快结束工作。

- 内存管理

(1) 通过 const 和 let 声明提升性能
相比于使用 var,使用 let 和 const 可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。在块作用域比函数作用域更早终止的情况下,这就有可能发生。
(2) 隐藏类和删除操作
(3) 内存泄漏
意外声明全局变量是最常见但也最容易修复的内存泄漏问题。下面的代码没有使用任何关键字声明变量:

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

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

定时器也可能会悄悄地导致内存泄漏。下面的代码中,定时器的回调通过闭包引用了外部变量:

let name = 'Jake';
	setInterval(() => {
	console.log(name);
}, 100);

只要定时器一直运行,回调函数中引用的 name 就会一直占用内存。垃圾回收程序当然知道这一点,因而就不会清理外部变量。

使用 JavaScript 闭包很容易在不知不觉间造成内存泄漏。请看下面的例子:

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

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

  • 静态分配与对象池
    压榨浏览器。一个关键问题就是如何减少浏览器执行垃圾回收的次数。理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因释放内存而损失的性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/661517.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

鸿蒙应用模型:【Stage模型开发】概述

Stage模型开发概述 基本概念 下图展示了Stage模型中的基本概念。 图1 Stage模型概念图 [AbilityStage] 每个Entry类型或者Feature类型的HAP在运行期都有一个AbilityStage类实例&#xff0c;当HAP中的代码首次被加载到进程中的时候&#xff0c;系统会先创建AbilityStage实例…

TCP/IP协议族

基于这张图片的一篇blog TCP/IP模型通常被分为四个层次&#xff1a;应用层、传输层、网络层和网络接口层。在这个模型中&#xff0c;不同的网络协议负责完成不同的任务&#xff0c;以确保数据可以在网络中高效、可靠地传输。以下是对这张图中每个协议的解释&#xff1a; 应用层…

[自动驾驶技术]-6 Tesla自动驾驶方案之硬件(AI Day 2021)

1 硬件集成 特斯拉自动驾驶数据标注过程中&#xff0c;跨250万个clips超过100亿的标注数据&#xff0c;无论是自动标注还是模型训练都要求具备强大的计算能力的硬件。下图是特斯拉FSD计算平台硬件电路图。 1&#xff09;神经网络编译器 特斯拉AI编译器主要针对PyTorch框架&am…

企业为何广泛应用数据可视化?解析其背后原因

数据可视化为何能在企业当中广泛应用&#xff1f;这是一个值得探讨的话题。在当今信息爆炸的时代&#xff0c;企业每天都会产生和处理大量的数据&#xff0c;这些数据蕴含着丰富的信息和潜在的商业价值。然而&#xff0c;面对海量数据&#xff0c;如何高效地分析、理解和利用它…

特定车型专属AI模型解决方案,高清图像,稳定输出

美摄科技凭借其对人工智能领域的深刻理解和技术积累&#xff0c;为企业带来了一项革命性的解决方案——特定车型专属AI模型。这一方案以专属车型照片为基础&#xff0c;通过先进的AI生成模型训练&#xff0c;为企业提供个性化、高清、稳定的车辆图像和视频生成服务&#xff0c;…

景源畅信电商:抖音开店步骤是什么?

随着社交媒体的兴起&#xff0c;抖音已经成为一个不可忽视的电商平台。许多人都希望通过抖音开店来实现自己的创业梦想。那么&#xff0c;抖音开店的具体步骤是什么呢?接下来&#xff0c;我们将详细阐述这一问题。 一、明确回答问题抖音开店的步骤主要包括&#xff1a;注册账号…

程序猿转型做项目经理一定要注意这 5 个坑

前言 国内的信息系统项目经理&#xff0c;很多都是从技术骨干转型的&#xff0c;我就是这样一路走过来的&#xff0c;这样有很多好处&#xff0c;比如技术过硬容易服众、熟悉开发流程更容易把控项目进度和质量、开发过程中碰到难题时更好组织攻坚等等&#xff0c;但是所谓成也…

web练习

[CISCN 2022 初赛]ezpop ThinkPHP V6.0.12LTS 反序列化漏洞 漏洞分析 ThinkPHP6.0.12LTS反序列漏洞分析 - FreeBuf网络安全行业门户 解题过程 ThinkPHP V6.0.12LTS反序列化的链子可以找到&#xff0c;找到反序列化的入口就行 反序列化的入口在index.php/index/test 链子 …

MVC架构中的servlet层重定向404小坑

servlet层中的UserLoginServlet.java package com.mhys.servlet; /*** ClassName: ${NAME}* Description:** Author 数开_11* Create 2024-05-29 20:32* Version 1.0*/import com.mhys.pojo.User; import com.mhys.service.UserService; import com.mhys.service.impl.UserSer…

【busybox记录】【shell指令】mkdir

目录 内容来源&#xff1a; 【GUN】【mkdir】指令介绍 【busybox】【mkdir】指令介绍 【linux】【mkdir】指令介绍 使用示例&#xff1a; 创建文件夹 - 默认 创建文件夹 - 创建的同时指定文件权限 创建文件夹 - 指定多级文件路径&#xff0c;如果路径不存在&#xff0c…

电脑显示不出网络

你的电脑是否在开机后显示不出网络&#xff0c;或者有网络消失的现象&#xff1f;今天和大家分享我学到的一个办法&#xff0c;希望对大家有用。 分析出现这类现象的原因&#xff1a;可能是电脑网卡松动了&#xff0c;电脑中存在静电流。 解决办法&#xff1a;先将电脑关机&am…

redis 集群 底层原理以及实操

前言 上篇我们讲解了哨兵集群是怎么回事 也说了对应的leader选举raft算法 也说了对应的slave节点是怎么被leader提拔的 主要是比较优先级 比较同步偏移量 比较runid等等 今天我们再说说,其实哨兵也有很多缺点 虽然在master挂了之后能很快帮我们选举出新的master 但是对于单个ma…

python调用阿里云通义千问(q-wen-max)API-只能总结pdf文档内容

文章目录 通义千问插件PDF解析插件调用案例通义千问插件 Dashscope插件功能能够使得大模型的生成内容与外部三方应用结合,使得模型生成的内容更加准确和丰富,模型将拥有更好的生成能力。您也可以通过开发自定义插件,来使得模型生成更符合您预期的结果。 使用插件功能,大模…

【UnityShader入门精要学习笔记】第十五章 使用噪声

本系列为作者学习UnityShader入门精要而作的笔记&#xff0c;内容将包括&#xff1a; 书本中句子照抄 个人批注项目源码一堆新手会犯的错误潜在的太监断更&#xff0c;有始无终 我的GitHub仓库 总之适用于同样开始学习Shader的同学们进行有取舍的参考。 文章目录 使用噪声上…

U盘无法打开?数据恢复与预防措施全解析

在日常生活和工作中&#xff0c;U盘已成为我们存储和传输数据的重要工具。然而&#xff0c;有时我们会遇到U盘无法打开的情况&#xff0c;这无疑给我们带来了诸多不便。本文将深入探讨U盘打不开的现象、原因及解决方案&#xff0c;并分享如何预防此类问题的发生。 一、U盘无法访…

20240529瑞芯微官方Toybrick TB-RK3588开发板的Debian11下使用SCP拷贝文件

20240529瑞芯微官方Toybrick TB-RK3588开发板的Debian11下使用SCP拷贝文件 2024/5/29 20:48 1、ADB链接异常。 2、BT打开之后找不到设备&#xff1f; 不清楚&#xff1a;是我拿到的开发板的问题&#xff0c;还是Toybrick/Rockchip官方没有做好。 3、现在最新版本的WINSCP&…

TREK高压发生器维修高压电源615-3-L-JX 615-3

美国TREK高压电源维修故障分析应注意两点&#xff1a; 故障分析检测和故障硬件更换&#xff0c;由高压电源故障和工作表现初步判断故障的类型和哪些硬件出了问题&#xff0c;初步判断缩小检测范围&#xff0c;通过排除法和更替新配件准确找到故障硬件。维修过程需要对trek电源维…

P2341 受欢迎的牛

题目描述 每一头牛的愿望就是变成一头最受欢迎的牛。现在有 N 头牛&#xff0c;给你 M 对整数&#xff0c;表示牛 A 认为牛 B 受欢迎。这种关系是具有传递性的&#xff0c;如果 A 认为 B 受欢迎&#xff0c;B 认为 C 受欢迎&#xff0c;那么牛 A 也认为牛 C 受欢迎。你的任务是…

el-upload上传文件使用http-request方法,formdata传集合List到后台

el-upload上传文件 前言1、使用el-upload上传文件1.1代码演示1.2回显列表2、formdata传集合List到Springboot后台前言 在使用el-upload上传文件,会遇到必须使用:action="upload_url"远端链接的问题,本章我们讲解怎样不适用远端链接,通过上传获取到本地的file文件…

LPDDR6带宽预计将翻倍增长:应对低功耗挑战与AI时代能源需求激增

在当前科技发展的背景下&#xff0c;低能耗问题成为了业界关注的焦点。国际能源署(IEA)近期报告显示&#xff0c;日常的数字活动对电力消耗产生显著影响——每次Google搜索平均消耗0.3瓦时&#xff08;Wh&#xff09;&#xff0c;而向OpenAI的ChatGPT提出的每一次请求则消耗2.9…