【重学Javascript】变量、作用域与内存

348 阅读7分钟

1. 原始值与引用值

 JavaScript中变量可以分为两种类型,一种是原始值,另外一种是引用值。原始值就是基础类型的数据,引用值就是类似数组,对象,方法等由多个值构成的对象。

1.1 值的存储以及栈内存 / 堆内存

 原始值包含有null, undefined, string, number, boolean, symbol等六种,而引用值则包含有object, array, function, Date, Math等... 。且原始值和引用值的存储位置也不同,原始值是存储在栈内存中,而引用值则存储在堆内存中,而栈内存和堆内存的引用方式也不一样。首先我们来说一下栈内存。

栈内存: 我们在声明使用原始值变量时,栈内存会分配一块区域来存这个变量名和变量值,使用时也会在内存中直接调用这个变量。原始值在复制变量时是重新拷贝一份变量来的,就是在内存中再次分配一块儿区域给新复制的变量使用

let num1 = 5
let num2 = num1

如图所示,首先声明了一个变量num1他的值为5,之后又声明了一个新的变量num2,并将num1的值赋给num2,而在内存中展示的就是新开了一块内存空间,这里有点深拷贝的味道了 image.png堆内存: 将一个引用值赋值给一个变量名时,这是数据存储在堆内存中,而且此时变量名的实际值并不是该引用数据,而是一个地址。为什么呢?是因为存储的时候引用值是存储在堆内存中,而同时生成了一个地址指向该引用值在堆内存中的位置,而变量名的值就是这个地址,每次访问数据或者修改引用值的时候都是通过这个地址的。

let obj = {name: '楚泽'}
let obj1 = obj

结合上面的代码和下面的图来说:声明了一个引用值,变量名是obj,之后将obj的值赋给了obj1,但是实际上赋 obj1 的并不是引用值本身,而是它对应的保存在栈内存中的引用地址。引用地址不变,所以只想的引用值也是同一个不会变化的

image.png

1.2 值传递和引用传递

值传递: 就是在函数调用时传递一个基本类型的值,这个值发生变化后不会影响原本的值,如下面代码:

let num = 12
function fun(a){
    a = 13
    console.log(a)
}
fun()  // 13
console.log(num)  12

代码中函数fun执行过程中将传进来的参数a重新赋值为13,但是在函数外面打印num时值时没有发生变化的。是因为函数体内a重新赋值时,它的值是保存在栈内存中的,所以就重新分配了一个新的值,新值和外面的旧值时没有关联的。

引用传递: 引用传递就是在函数调用时使用参数传递进去一个引用值,但是这里在函数体内改变引用值属性的时候,作为参数传进去的引用值也会发生变化,但是这分为两种情况,一种是修改引用值,另一种是完全覆盖引用值,采用完全覆盖的时候效果和值传递的效果是一样的,看代码

// 1. 修改引用值
let num = {}
function fun(a) {
  a.name = '楚泽'
  console.log(a)
}
fun(num)  // {name: '楚泽'}
console.log(num)   // {name: '楚泽'}

// 2. 覆盖引用值
let num = {}
function fun(a) {
  a = []
  console.log(a)
}
fun(num)  // []
console.log(num)   // {}

如代码所示,第一种情况是把变量 num 当作参数传到了方法中,但是仅对其添加了一个 name 属性,所以后面打印的时候两者都打印了相同的结果。

第二种情况是在方法体内直接将参数值给覆盖掉了,之后打印出来的结果和值传递的结果是一样的。这点以后日常开发需要注意到

1.3 变量类型

1.3.1 typeof

 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

typeof虽然用来判断原始值很有作用,但是判断引用类型值的时候却不准确,引用类型的值以及null在使用typeof来判断时返回的结果都是object,所以这个时候可以使用instanceof操作符来判断变量的类型

1.3.2 instanceof

 instanceof主要用来判断某个变量是否是某个引用类型的实例,同时用来判断变量是否是某个构造函数的实例的话也是可以的

let str = []
let fun = function () { }
console.log(str instanceof Array)
console.log(fun instanceof Function);

// 判断是否是某个构造函数的实力
function Person() { }
const person1 = new Person()
console.log(person1 instanceof Person)

2. 执行上下文与作用域

2.1 执行栈

 首先先来大致了解下什么是执行栈是一个后进先出的数据结构,javascript在执行的过中首先会将全局执行上下文放到执行栈的底部,然后执行代码的过程职工遇到A函数执行时会将该函数的执行上下文放到执行栈中,然后执行,在执行A函数的过程中如果再遇到B函数执行,则会再将B函数的执行上下文放到执行栈中,之后继续执行函数B,B函数执行完之后执行栈会将B函数的执行上下文弹出执行栈,然后继续执行,A函数执行完成后会将A函数的执行上下文弹出调用栈,直到所有的代码执行完成,最后将全局上下文的弹出调用栈,可以结合下面的代码和图片来梳理

function cFn() {}
function pFn() {
    cFn()
}
pFn()

201942155230826.jpeg

2.2 执行上下文

什么是执行上下文? 执行上下文就是执行javascript时的环境。只要javascript代码执行,那么它就是在上下文中执行的
执行上下文的三种类型:

  • 全局执行上下文
  • 函数执行上下文
  • eval函数执行上下文
2.2.1 作用域链

 每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文会被推到一个上下文栈中,当函数执行完成之后,这个函数执行上下文栈会被弹出上下文栈,并将执行权还给之前的执行上下文,js的执行流就是靠这个执行上下文来控制的。

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

2.2.2 作用域链增强

 虽然执行上下文有全局执行上下文和函数执行上下文,但是还有其他的方式来增强作用域链。某些语句会导致在作用域链前端临时增加一个上下文,这个上下文在代码执行完成后会被删除。通常在以下两种情况会出现作用域链增强的情况

  • 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,也就是自己变量对象的属性。在引用 qs 时,引用的则是定义在 buildUrl()中的那 个变量,它定义在函数上下文的变量对象上。而在 with 语句中使用 var 声明的变量 url 会成为函数 上下文的一部分,可以作为函数的值被返回;但像这里使用 let 声明的变量 url,因为被限制在块级作 用域(稍后介绍),所以在 with 块之外没有定义