JavaScript中的变量、作用域、和内存问题

前言

最近开始入坑js“圣经”之一的《JavaScript高级程序设计》,准备深入学习这门“博大精深”的语言,下面我学习第四章的总结及拓展。

变量

js中声明变量的几种方式

下面是es5以及之前的方式(var,function):

  • var:可以声明任意类型的变量,js会根据变量的值来自动判定这个值是什么类型。
  • function:主要是声明函数类型的变量,如定义一个函数。
  • 啥也不加直接写变量。这种方式是创建了一个全局变量,而且无法被垃圾回收机制回收,只有退出这个变量所在的环境后,这个变量才会被销毁,如关闭浏览器

下面是es6声明变量的方式(let,const,import,class)

  • let:和var作用一样,只是该变量被限制在了当前代码块(当前作用域)

基本用法:

// var是没有块级作用域的概念
{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

还有一个更经典的例子:

for (var i = 0; i < 10; i++) {
	doSomething(i);
}

alert(i);// 10

如果将 var 替换成 let ,那么就会报“ ReferenceError: i is not defined ”,因为循环结束后,i 这个变量就被回收了
如果上面的两个例子都不足以说服你,那么下面的这个例子会更有说服力。

var a = [];
for (var i = 0; i < 10; i++) {
	a[i] = function () {
		console.log(i);
	}
}

a[6]();// 10

同样,把 var 替换成 let 那么,a[6]() 的结果就是 6 ,原因是使用var声明的变量是没有块级作用域的,循环变量和循环内部的i是同一个i,处于一个作用域,循环外变化,里面的也会同时改变。而 let 就不一样,i 只在本轮循环有用,循环后就被回收,下一次是重新定义的 i ,该轮循环的 i 是多少,循环里面的 i 就是多少。

注意:let 方式定义的变量是不存在变量提升的情况的。而 var 定义的变量存在变量提升,可以称之为 预解析。

// var 的情况:先解析声明,在执行语句
console.log(foo ); // 输出undefined
var foo = 2 ;

// 等同于
var foo;
console.log(foo);
foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError,还未声明
let bar = 2 ;

总结来说,使用 let 声明的变量会绑定这个区块,也就是说,只要在声明 let 的变量之前使用这个变量,就会报错,有个术语可以描述在 let 之前的区域,称之为“暂定性死区”(temporal dead zone,简称 TDZ)

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

除此之外,let不允许在相同作用域内,重复声明同一个变量,这样会报错。

  • const:const声明一个只读的常量。一旦声明,常量的值就不能改变,所以必须一声明就必须初始化。它和 let 一样,具有块级作用域,不存在变量提升,存在“暂定性死区”,不允许重复声明的特点。不同的是,const 在声明的同时必须赋值,不然会报错。

const的本质:const 保证的是变量指向的那个内存地址所保存的数据不得改动。对于基本类型来说,值就保寸在变量指向的那个内存地址中,无法再更改。对于复杂类型来说,变量里存储的并不是真正的对象,而是指向这个对象内存地址的指针,const 保证就是这个指针一直指向该对象而已,无法保证这个对象的数据结构是否发生改变。

const a = {};

a.name = "laocao";
console.log(a.name);// "laocao"

a = {};
console.log(a.name);// TypeError: Assignment to constant variable.

不过有一种办法可以保证,对象的数据结构无法被更改,那就是 Object.freeze(obj),obj 就是要冻结的对象。详细内容可以参考 MDN

  • import:引入模块变量
  • class:定义类名

变量的类型有哪些

  • 基本类型:Number,String,Boolean,Null,Undefined,Symbol(es6)
  • 引用类型:Object

如何判断变量的类型

  • 基本类型的判断:typeof
    原理:识别变量机器码的低位1-3位来判断该变量是什么类型。
    – 000:对象
    – 010:浮点数
    – 100:字符串
    – 110:布尔值
    – 1:整数
    而 null 的机器码是全 0 ,undefined 用 −2^30 整数来表示,所以判断 null 这个原始值类型的时候就被识别成对象。

  • 引用类型的判断:instanceof
    原理:主要是判断左边的对象是不是右边对象的实例对象,这里其实就是 return obj1.proto === obj2.prototype 的布尔值,缺点就是无法判断具体的对象。这里涉及到原型的知识,具体可以看一下我写的文章

  • 通用方法:Object.prototype.toString.call()
    在这里插入图片描述

作用域

作用域其实就是变量和函数所在的执行环境

三种作用域

  • 局部作用域:一般是指函数执行的环境,函数外无法访问函数里面的变量,除非是没有声明的变量
  • 全局作用域:无论函数内还是函数外都可以访问的变量的环境(这里涉及到顶级对象与全局变量的歧义)
  • 块级作用域:es5是没有块级作用域的,但是可以模拟块级作用域。
    (1):使用es6中 let 和 const 关键字
    (2):使用IIFE(立即调用函数表达式)

作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。

也就是说,你在函数中使用了某个变量,那么js引擎就会去找这个变量的值,从当前环境到一直到全局环境,找到就停止,没找到就报错。

var a = 10;
function fn1 () {
	var b = 5;
	function fn2 () {
		var c = a + b;
		return c;
		// 这层作用域可以访问a,b,c三个变量
	}
	fn2();
	//这层只能访问a,b
}
fn1();
// 这层只能访问a

在这里插入图片描述

内存问题

JavaScript 具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。

堆和栈

栈:是一种线性的数据结构,自动分配固定大小的内存,在js中主要存储基本类型的值以及指向引用类型地址的指针,由系统自动释放,具有先进后出的特点
堆:是一种杂乱无章的数据结构,没有固定大小,在js中主要存储引用类型的数据
在这里插入图片描述

浅拷贝和深拷贝

浅拷贝就是复制一个指针地址,然后指向同一块堆空间,那么不管哪个指针发生变化,堆中存储的对象都会跟着变化。

 var obj1 = {
     age:12,
     car:["奥迪", "奔驰", "特斯拉"],
 };
 var obj2 = obj1;

 console.log(obj1);
 console.log(obj2);
 

在这里插入图片描述
深拷贝:把一个对象中所有的属性或者方法,一个一个的找到,并且在另一个对象中开辟相应的空间,一个一个的存储到另一个对象中,深拷贝效率极其低下,很少使用。

const obj1 = {
            age:10,
            name:"老曹",
            car:["奔驰","宝马"],
            books:{
                size:"4k",
                pages:300
            }
        };
        const obj2 = {};
        //定义一个函数将对象a的属性拷贝到b
        function copy(obj1, obj2) { 
            //遍历obj1所有属性
            for (let key in obj1) {
                //保存每一个属性
                let item = obj1[key];
                //判断属性是什么类型,如果是对象或者数组类型则将里面的属性一个一个拷贝,使用递归
                if (item instanceof Array) {
                    //如果obj1某属性的类型为数组,则在我obj2开辟一个空间存储该数组
                    obj2[key] = [];
                    copy(item, obj2[key]);
                } else if (item instanceof Object) {
                    //如果obj1某属性的类型为对象,则在obj2开辟一个空间存储该对象
                    obj2[key] = {};
                    copy(item, obj2[key]);
                } else {
                    //如果是基本类型的值,则直接赋值
                    obj2[key] = item;
                }
            }
         }
         
         copy(obj1, obj2);
         console.log(obj1,obj2);
         
         obj2.age = 20;
         console.log(obj1,obj2);

在这里插入图片描述

垃圾回收机制(了解)

  • 标记清除(常见)

这是js中最常用的垃圾收集方式。我的理解是在一个环境中如函数声明了一个变量,函数执行完毕后,该变量就会被标记,在一定的周期内回收掉该变量所占用的空间。不同浏览器垃圾收集的间隔是不一样的。

  • 引用计数

引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0 时,则说明没有办法访问这个值了,因而就可以将其占用的内存空间回收回来。

引用计数存在一定的性能问题,比如两个相互引用的变量存在循环引用,就无法被回收,此时需要手动设置为 null 才能断开循环。这种问题主要存在于IE浏览器,其BOM 和DOM 中的对象就是
使用C++以COM(Component Object Model,组件对象模型)对象的形式实现的,而COM 对象的垃圾收集机制采用的就是引用计数策略

参考文献

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值