【JavaScript高程第4版精读】变量、作用域与内存

变量

原始值和引用值

ESMAScript有两种类型的数据:原始值和引用值。

  • 原始值:Undefined、Null、Boolean、Number、String和Symbol;
  • 引用值:由多个值构成的对象;
    下面详细分析一下他们的不同之处。

原始值和引用值的区别

1.动态属性

引用值可以随时增加、修改和删除其属性和方法;

js 复制代码
let person = new Object()
person.name = "张三"
console.log(person.name) // 张三

原始值不能有属性,虽然给原始值增加属性不会报错,但是获取不到该属性;

js 复制代码
let person = "张三"
person.age = 30
console.log(person.age) // undefined

2. 复制值

原始值复制:

js 复制代码
let num1 = 5
let num2 = num1

引用值复制:

js 复制代码
let obj1 = new Object()
let obj2 = obj1
obj1.name = "张三"
console.log(obj2.name) // 张三

3.传递参数

ECMAScript中所有函数的参数都是按值传递的,就像从一个变量复制到另一个变量一样。变量有按值和按引用访问,而传参只有按值传递。

如果参数是原始值会好理解一些,函数内部的num加10,不会影响函数外部的count变量。num和count是不会互相影响的。

js 复制代码
function addTen(num) {
  num += 10;
  return num;
}
let count = 20;
let result = addTen(count);
console.log(count); // 20
console.log(result); // 30

如果参数是引用值,person和obj都指向同一个对象。看起来好像参数是按引用传递的,那我们再看一个例子;

js 复制代码
function setName(obj){
  obj.name = "张三"
}
let person = new Object()
setName(person)
console.log(person.name) // 张三

如果参数是按引用传递的,person应该改为李四啊,但我们访问person.name,它还是张三。当函数中的参数改变时,原始的引用依然没有变。在obj被重写成新的对象,这个新的对象在函数执行完成时就销毁了。

js 复制代码
function setName(obj){
  obj.name = "张三"
  obj = new Object()
  obj.name = "李四"
}
let person = new Object()
setName(person)
console.log(person.name) // 张三

4.确定类型typeof

typepf能判断出String、Number、Boolean、undefined;如果是object或是null,它就会都返回Object。typeof对原始值很有用,但它对引用值用处不大。

如果变量是引用类型,可以用instanceof判断,它会返回true。

执行上下文和作用域

执行上下文和作用域是什么?

每个函数都有自己的上下文;当代码执行进入函数时,函数的上下文被推到上下文栈上;函数执行完之后,上下文栈会弹出该函数的上下文,将控制权返还给之前的执行上下文。

js 复制代码
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
changeColor();
  • 上面的🌰涉及到3个上下文:全局上下文,changeColor()的局部上下文和swapColors()的局部上下文。
  • 全局上下文有一个变量color和函数changeColor()。
  • changeColor()的局部上下文中有一个变量anotherColor和swapColors(),但在这里可以访问全局上下文中的变量color。
  • swapColors()的局部上下文中有一个变量tempColor,只能在当前这个上下文中被访问,全局上下文和changeColor()局部上下文中都无法访问到变量tempColor。
  • 在swapColors()中可以访问另外两个上下文中的变量。

上图展示了这个🌰的作用域链。内部上下文能通过作用域链访问外部上下文,但外部上下文无法访问到内部上下文中的任何东西。

变量声明

var

先来看一个🌰,在函数中使用var声明变量,它会被添加到函数的局部上下文中。函数add()定义了一个局部变量sum,这个值作为函数的值被返回,但是变量sum在函数外部是访问不到的。

js 复制代码
function add(num1, num2) {
  var sum = num1 + num2;

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

我们把var去掉呢。

js 复制代码
function add(num1, num2) {
  sum = num1 + num2;

  return sum;
}
let result = add(10, 20);
console.log(sum); // 30

上面的🌰中,如果变量未经声明就被初始化了,它就会被自动添加到全局上下文中,因此sum在add()函数之外可以被访问了。

var声明会被放到函数或者全局作用域的顶部,这个现象叫做"提升"(Hoisting)

js 复制代码
var name = "张三"

// 等价于

name = "张三"
var name
js 复制代码
functin fn1(){
    var name = "张三"
}

// 等价于

function fn2(){
    var name
    name = "张三"
}

如果我们在变量声明之前打印变量,会发生什么呢?变量提升之后会打印出undefined

js 复制代码
console.log(name) // undefined
var name = "张三"

function fn1(){
    console.log(name) // undefined
    var name = "张三"
}

let

let和var很相似,但它的作用域是块级的。

js 复制代码
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) // ReferenceEffor: c没有定义
// 这里如果用var,也会报错


{
  let d
}
console.log(d) // ReferenceEffor: d没有定义

let和var还有一个不同之处就是在同一作用域中let不能声明两次。重复的var声明会被忽略,而重复的let会抛出SyntaxError。

let在JavaScript运行时也会被提升,但是由于"暂时性死区"(temporal dead zone)的缘故,并不能在声明之前使用let变量。let的提升和var是不一样的。

const

const声明变量的时候就得初始化,我是说原始值类型,如果重新赋予新值就会报错。 但如果const声明了引用类型变量,对象的健是不受限制的。

js 复制代码
const o1 = {}
o1 = {} // TypeError: 給常量赋值

const o2 = {}
o2.name = "张三"
console.log(o2.name) // 张三

标识符查找

js 复制代码
var color = "blue";
function getColor() {
  return color;
}

console.log(getColor()); // blue

调用函数getColor()时会引用变量color。为确定color的值会分两步搜索。第一步在函数getColor()内部的变量color;如果没找到就会去全局上下文中查找,因为全局有color,所以就找到了。

js 复制代码
var color = "blue";
function getColor() {
  let color = "red";
  return color;
}

console.log(getColor()); // red

在上面这个🌰中,在函数getColor()中找到了color,搜索就停止了。

js 复制代码
var color = "blue";
function getColor() {
  let color = "red";
  {
    let color = "green";
    return color;
  }
}

console.log(getColor()); // green

调用getColor()时会引用变量color,于是在局部上下文中搜索color,结果找到了值为"green"的color;因为变量color找到了,所以停止搜索。块级作用域不会改变搜索的流程,但是可以改变词法层级;

垃圾回收

在C和C++中需要开发者自己跟踪内存使用,这对开发工作带来了麻烦。JavaScript通过自动内存管理实现内存分配和垃圾回收,为开发工作减轻了负担。垃圾回收会定期查找不再使用的变量,然后释放它们的内存空间。因此垃圾回收需要去标记未使用的变量,这里有两种主要的方式。

两种标记未使用变量方式

标记清理(常用)

当变量进入上下文,比如在函数内部声明一个变量,这个变量会被加上存在上下文的标记。只要上下文中的代码在运行,就不会释放它们的内存。当变量离开上下文,就会被加上离开上下文的标记。

js 复制代码
var m = 0, n = 19 //把m, n , add()标记为进入环境
add(m, n) // 把a, b, c标记为进入环境
console.log(n); // 把a, b, c标记为离开环境,等待垃圾回收
function add(a, b){
  a++
  var c = a + b
  return c
}

引用计数

引用计数:每个值都记录它被引用的次数。声明a变量并给它赋一个引用值,引用值为1;如果a变量又被赋值给另一个变量b,那么引用数➕1;如果a变量被其他值覆盖了,那么引用数➖1;当变量的引用数为0时,就说明没法再访问这个值了;因此可以被垃圾回收了;

js 复制代码
let a = new Object(); // 此对象的引用计数为1(a 引用)
let b = a; // 此对象的引用计数为2(a, b引用)
a = null // 此对象的引用计数为1(b引用)
b = null // 此对象的引用计数为0(无引用)
... // GC回收此对象

很快又有一个严重问题:循环引用;当对象A有一个指针指向对象B,而对象B也引用了对象A。这时objectA和objectB的引用数都是2;当函数结束后还会存在,它们的引用数永远不会变成0;如果函数被多次调用,就会导致大量内存永远不会被释放。

js 复制代码
function problem() {
  let objectA = new Object();
  let objectB = new Object();

  objectA.someOtherObject = objectB;
  objectB.anotherObject = objectA;
}

内存管理

1.通过const和let声明提升性能

let和const都是块作用域,相比于使用var,使用let和const能更早地让垃圾回收程序介入,尽早回收应该回收的内存。

2.隐藏类

a2.author = "张三" 这种操作的频率和隐藏类的大小,会对性能产生明显的影响。

js 复制代码
function Article() {
  this.title = "hidden class";
}
let a1 = new Article();
let a2 = new Article();
// a1和a2会共享相同的隐藏类
a2.author = "张三"
// 此时a1和a2会对应两个不同的隐藏类
delete a2.author
// 使用delete也会使a1和a2不再共享相同的隐藏类

因此要避免a2.author = "张三" 这种操作,尽量在constructor中一次性声明所有的属性像这样;也要避免delete a2.author操作,把它改成a2.author = null像这样。

js 复制代码
function Article() {
  this.title = "hidden class";
  this.author = "张三";
}
let a1 = new Article();
let a2 = new Article("李四");

a2.author = null

3.内存泄漏

以下几种操作会有内存泄漏的问题:

1.意外声明全局变量

name前面没有任何关键字,此时会把name当作window的属性来创建(相当于window.name = "张三"),window不被清理name就不会消失。

js 复制代码
function setName(){
  name = "张三"
}

2.定时器

只要定时器一直运行,回调函数中的name就会一直占用内存。

js 复制代码
let name = "张三";
setInterval(() => {
  console.log(name);
}, 1000);

3.使用闭包

outer中创建一个内部闭包,只要返回的函数存在就不能清理name。

js 复制代码
let outer = function () {
  let name = "张三";
  return function () {
    return name;
  };
};

4.静态分配与对象池 (选修内容)

当你的应用程序被垃圾回收严重拖了后腿,可以利用它提升性能。大多数情况下,这都属于过早优化,因此不用考虑。 如果有很多对象被初始化,然后又一下子超出了作用域,那么浏览器就会采用更加激进的方式垃圾回收,这样当然会影响性能。来看一个例子:

js 复制代码
function addVector(a, b) {
  let resultant = new Vector();
  resultant.x = a.x + b.x;
  resultant.y = a.y + b.y;
  return resultant;
}

上面这个例子,调用这个函数时,会在堆上创建一个新对象,然后修改它,最后再返回给调用者。假如addVector方法被频繁调用,这里的对象更新速度很快,从而会频繁地进行垃圾回收。那么有更好的解决方法吗?

js 复制代码
function addVector(a, b, resultant) {
  resultant.x = a.x + b.x;
  resultant.y = a.y + b.y;
  return resultant;
}

更好的解决方法就是不要动态创建矢量对象,让它使用一个已有的矢量对象。

相关推荐
不爱说话郭德纲6 分钟前
还在等后端接口?自己写得了
前端·javascript·后端
m0_7388202012 分钟前
vue生命周期
前端·javascript·vue.js
我爱学习_zwj29 分钟前
前端面试题-1(详解事件循环)
前端·javascript·面试·浏览器
小马哥编程1 小时前
【前端Vue】day02
前端·javascript·vue.js
zpjing~.~1 小时前
Vue3 调用子组件的方法和变量
前端·javascript·vue.js
大臣不想在月亮上上热搜1 小时前
黑马2024AI+JavaWeb开发入门Day02-JS-VUE飞书作业
javascript·vue.js·飞书
萝卜快乐晶1 小时前
Vue+Element Plus实现自定义表单弹窗
前端·javascript·vue.js·elementui·前端框架
秀儿y1 小时前
vue3-setup基本使用(非响应式数据)
开发语言·前端·javascript·vue.js
sunly_1 小时前
Flutter:encrypt插件 AES加密处理
java·javascript·flutter
Lee_Yu_Fan1 小时前
vue3 + vite + antdv 项目中自定义图标
前端·vue.js·svg图标