JavaScript - 变量 & 作用域

167 阅读10分钟

概述

相比较其他语言,JavaScript 中的变量可谓独树一帜。正如ECMA-262所规定的,JavaScript 变量是松散类型的,而且变量只不过就是特定时间点一个特定值的名称而已。 由于没有规则定义变量必须包含什么类型数据,变量的值和数据类型在脚本生命周期内可以改变。这样的变量很有意思,很强大,但也引申出一些问题。

变量相关的概念

什么是变量?

正如ECMA-262所规定的,JavaScript 变量是松散类型的,而且变量只不过就是特定时间点一个特定值名称而已。
所以变量只是一个值的标识符,在JavaScript 中,它的类型是可以动态变化的。能够引起它的类型的变化的因素有2个:时间点,值的类型

变量的类型 & 变量类型的决定权

ECMAScript 变量可以包含两种不同类型的数据:原始值和引用值。所以我们可以确定的说,变量有两种类型,一个是原始值变量类型,一个是引用值类型。 变量的类型是由赋值给这个变量的值的类型决定的
在把一个值赋值给变量时,JavaScript 引擎必须确定这个值时原始值还是引用值。

原始值 & 引用值

原始值就是最简单的数据,引用值则是由多个值构成的对象。

在JavaScript 数据类型中,我们总结了6种原始值:Undefined,Null,Boolean,Number,String和Symbol。保存原始值的变量是按值访问的,因为我们操作的就是存储在变量中的实际值。

引用值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用而非实际的对象本身为此保存引用值的变量是按引用访问的。

变量的动态属性

原始值和引用值的定义方式很类似,都是创建一个变量,然后给他赋一个值。不过变量保存了这个值之后,可以对这个值做什么,则大有不同。对于引用值而言,可以随时添加,修改和删除其属性和方法。原始值则没有动态属性的特点。

let person = new Object();
person.name = "Wang";
console.log(person.name);//Wang

变量的复制

原始值的复制

除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。这两个变量可以独立使用,互不干扰。

let num1 = 5;
let num2 = num1;

截屏2022-07-13 下午6.29.42.png

引用值的复制

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

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

截屏2022-07-13 下午6.39.17.png

变量作为函数参数的传递

ECMAScript 中所有的函数的参数都是按值传递的。

给函数传递原始值的参数

function addTen(num) {
    num += 10;
    return num;
}

let count = 10;
let result = addTen(count);
console.log(count); // 20 没有变化
console.log(result);// 30

由于所有的函数的参数都是按值传递的,addTen函数的参数的num 和 函数外部的count 是两个相互独立的变量,互不干扰。

给函数传递引用值的参数

function setName(obj) {
    obj.name = "zhang";
}
let person = new Object();
setName(person);
console.log(person.name);//zhang

由于所有的函数的参数都是按值传递的,setName的obj 参数 和函数外部的person 变量也是两个独立的变量,互不干扰,但是由于person是引用值,所以虽然参数是按值传递的,但是obj在函数中是一个对象指针,且在内存中obj和person都是指向同一个对象,obj这个参数对对象的修改,就会体现到函数外部,但这并不意味着函数是按引用传递参数的。我们做如下修改:

function setName(obj) {
    obj.name = "zhang";
    obj = new Object();
    obj.name = "li";
}
let person = new Object();
setName(person);
console.log(person.name);//zhang

在这个例子中,假如函数对引用值的参数传递是按引用传递的,那么obj这个引用变量,再经过setName 函数的操作后,obj的指针会指向一个新的对象,这个对象的name是“li”,但结果显然不是这样,所以函数对引用值的参数传递也是按值传递的。这段代码的内存图如下:

截屏2022-07-13 下午7.26.24.png

怎么查看变量的类型?

JavaScript 提供了两个操作符供开发者判断变量的类型:typeof 操作符和instanceof 操作符typeof 操作符用于判断 字符串,数值,布尔值,undefined,symbolinstanceof 判断null,引用值类型

      let a = "wang";
      let b = true;
      let c = undefined;
      let x = Symbol();
      let d = null;
      let e = new Object();
      let f = [];
      let g = new RegExp();
      console.log(typeof a); //string
      console.log(typeof b); //boolean
      console.log(typeof c); //undefined
      console.log(typeof x); //symbol
      console.log(d instanceof Object); // false 用instanceof 检测原始值,则始终会返回false
      console.log(e instanceof Object); // true
      console.log(f instanceof Array);//true
      console.log(g instanceof RegExp);//true

变量的访问范围

JavaScript 中变量的访问范围和变量所处的作用域相关,而作用域是作用域链的一部分,作用域链是执行上下文的一部分,执行上下文是在上下文栈中执行的,所以要弄清楚变量的访问范围首先 要弄清楚这几个概念。

上下文栈

上下文栈是个栈数据结构类型的数据结构。ECMAScript 程序的执行流程就是通过这个上下文栈进行控制的。上下文栈遵循栈结构的先入后出的特点。可以用下图来理解上下文栈这个概念。

截屏2022-07-13 下午9.11.27.png

执行上下文

执行上下文(简称上下文)的概念在JavaScript中是颇为重要的。变量或函数的上下文决定了它们可以访问那些数据,以及他们的行为。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。 

在ES6 以后,javaScript 中存在三种上下文,全局上下文局部上下文(又称函数上下文),局部上下文又可以分为函数上下文和块级上下文。其对象的作用域为全局作用域,函数作用域,块级作用域。

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

函数上下文
每个函数调用都有自己的上下文。当代码执行流入函数时,函数的上下文被推到一个上下文栈上。在函数执行完毕后,上下文栈会弹出该上下文栈,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流程就是通过这个上下文栈进行控制的。

一个上下文中包含如下几个部分: 截屏2022-07-13 下午9.41.28.png

作用域

作用域是可访问变量的集合,我们把一个上下文中所有的变量的集合称为这个上下文的作用域。如果一个变量在这个集合中,我们也称这个变量的作用域是这个上下文的作用域。 在JavaScript 中,函数也是一个变量,所以我们可以简单理解一个上下文中的所有变量和函数的集合是这个上下文的作用域。

var 关键字声明的作用域

var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫做作用域的提升。提升让同一作用域中的代码不必考虑变量是否已经声明就可以直接使用。
在函数中省略var 声明一个变量,它将会被自动添加到全局上下文。

使用let 的块级作用域声明

ES6 新增的let 关键字跟var 很类似,但它的作用域是块级的,这也是JavaScript 中的新概念。块级作用域由最近的一对花括号{}界定。换句话说,if块,while块,function块,甚至连单独的块也是let 声明变量的作用域。

使用const 的块级作用域声明

const 声明的作用域同let一致。需要注意的是const 声明指应用到顶级原域或对象。换句话说,赋值为对象的const 不能再被重新赋值为其他引用值,但对象的键则不受限制。
如果想让整个对象都不能修改,可以使用Object.freeze(),这样再给属性赋值时虽然不会报错,但会静默失败。

const o3 = Object.freeze();
o3.name = "wang";
console.log(o3.name);// undefined

作用域链

上下文的代码在执行的时候,会创建变量对象的一个作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象(activation object AO)用作变量对象。活动对象最初只有一个定义变量arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上下文,再洗一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对象。

作用域链增强

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

  • try/catch语句的catch块

  • with 语句

标识符的查找

代码执行时的标识符解析时通过沿作用域链逐级搜索标识符名称完成。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到表示符。
当特定上下文中为读取或写入而引用一个表示符时,必须通过搜索确定这个标识符表示什么。搜索开始域作用域链前端,以给定的名称搜索对应的标识符.(注意,作用域链中的对象也有一个原型链,因此搜索课程涉及每个对象的原型链。)这个过程一直持续到搜索至全局上下文的变量对象。

变量的回收

请查看js的内存管理