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

1、基本类型和引用类型

ECMAScript变量可能包含两种不同数据类型的值:基本类型和引用类型
基本类型:简单的数据段,是按值访问的,有Undefined、Null、Boolean、Number、String。
引用类型:值保存在内存中,JavaScript不允许直接访问内存中的位置,不能直接操作对象的内存空间。因此在操作对象时,实际是操作对象的引用。因此是按引用访问的。

动态的属性

当创建一个变量并为其赋值后,对于引用类型的值可以为其添加删除属性和方法,而不能给基本类型进行此操作。

//引用类型
var person = new Object();
person.name = "lili";
alert(person.name);//"lili"
//基本类型
var name = "lili";
name.age = 27;
alert(name.age)//undefined

复制变量值

在进行变量复制操作时,基本类型和引用类型也存在不同。如果从一个变量向两一个变量赋值基本类型的值,会在变量对象上创建一个新值,然后将该值复制到新变量的位置上。例如:

var num1 = 5;
var num2 = num1;//num2 = 5
num2 = 10;
console.log(num1,num2)//5,10

从上述案例代码来看,可以知道num2与num1完全独立,num2可以看作是num1的一个副本,num1和num2进行任何操作都不会互相影响。

而当一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象的值复制一份到新分配的空间中,但此时变量对象所储存的值是一个指针,这个指针指向存储在堆中的一个对象。此时两个变量有同样的指针指向,并且指向的是同一个对象。因此两个变量实际上将引用同一个对象,因此改变其中一个变量会影响另一个变量。例如:

var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Lili";
alert(obj2.name);//Lili
obj2.name = "tom";
alert(obj1.name,obj2.name)//"tom tom"

传递参数

ECMAScript中所有函数的参数都是按值传递的,也就是说将函数外部的值复制给函数内部的参数,就把值从一个变量复制到另一个变量一样。基本类型值的传递等同于基本类型变量的复制一样,而引用类型值的传递如同引用类型变量的复制一样。参数只能按值传递。

向参数传递基本类型的值时,被传递的值会被复制给一个局部变量。

function addTen(num){
	num+= 10;
	return num;
}
var count = 20;
var result = addTen(count);
alert(count);//20,无变化
alert(result);//30

函数内部参数的变化不会影响函数外部的count变量。因为参数的传递是将外部的值20复制给了函数的参数count

在向参数传递引用类型的值时,会把这个值在内存中的地址赋值给一个局部变量,因此这个局部变量的变化会反应在函数外部。

function setName(obj){
	obj.name = "Lili";
}
var person = new Object();
setName(person);
alert(person.name)//"Lily"

以上代码中创建一个对象,并将其保存在了object中,将object的值传递给函数的参数,实际上传递的是一个指针,而指针的指向仍是堆中的对象。因此函数内外的变量引用的是同一个对象。

function setName(obj){
	obj.name = "Lili";
	obj = new Object();
	obj.name = "Tom";
}
var person = new Object();
setName(person);
alert(person.name);//"Lili"

上述代码在函数中新增了两行代码,一行为obj重新定义了一个对象,另一行则设置了一个不同的属性值。如果person是按引用传递的,那此时person就会自动被修改为指向新对象的对象。但是最后的输出结果并没有改变,说明原始的引用仍然保持未变,更加的说明了参数的传递是按值传递的。

检测类型

typeof
typeof可以判断基本数据的类型,对一个对象或null,typeof会返回“object”;

var s = '11';
var b = true;
var i = 22;
var u;
var n = null;
var o = new Object();
var a = [];

console.log(typeof s);//string
console.log(typeof b);//boolean
console.log(typeof i);//number
console.log(typeof u);//undefined
console.log(typeof n);//object
console.log(typeof o);//object
console.log(typeof a);//object

因此可以看出在检测引用类型的值时,这个操作符用处不大。

instanceof
其语法如下:

result = variable instanceof constructor

如果变量是给定的引用类型的实例,那么instanceof操作符就会返回true。例如:

alert(person instanceof Object);
alert(colors instanceof Array);
alert(pattern instanceof RegExp);

如果使用instanceof操作符检查测基本类型值,会返回false,因为基本类型不是对象。

执行环境及作用域

执行环境是JavaScript中一个最重要的概念,定义了变量或函数有权访问当代其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

全局执行环境是最外围的一个执行环境。在web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法来创建的。

每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端是当前执行代码所在环境,作用域链的最后一个对象是全局执行环境变量对象。

标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链前端开始逐级向后回溯,直到找到标识符为止。
示例代码:

var color = "blue";
function changeColor(){
    var anotherColor = "red";
    function swapColors(){
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
        //这里可以访问 color tempColor anotherColor
    }
    //这里可以访问 color anotherColor 不可以访问tempColor
    swapColors();
}

上述代码涉及三个执行环境:全局环境、changeColor()环境以及swapColor()环境。

window
|
|----color
|----changeColor()
        |
        |---anotherColor
        |---swapColor()
       		 |
             |---tempColor

可见内部环境可以通过作用域链访问所有的外部环境,而外部环境不能访问内部环境中的变量和函数。

延长作用域链

虽然执行环境类型只有全局和局部两种,但是还有其他方法来延长作用域链。因为有些语句可以在作用域链前端临时增加一个变量对象,该变量对象会在代码执行后被移除。具体来说,当执行流进入下列任何一个语句时,作用域链就会加长:

  • try-catch语句的catch块
  • with语句

这两个语句都会在作用域前端添加一个变量对象。对with语句来说,会将指定的对象添加到作用域中。对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。

function buildUrl(){
    var qs = "?debug=true";
    with(location){
        var url = href + qs
    }
    return url
}

在此处,with语句接收的是location对象,因此其变量对象中就包括了location对象的所有属性和方法,而这个变量对象被添加到作用域链的前端。

变量声明

ES6之前,JavaScript是没有块级作用域的。也就是说var声明的变量不存在块级作用域。ES6中新增了let、const关键字来解决了这一问题。

var声明变量
使用var声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函数的局部上下文。如果变量未经声明就初始化,那么它将自动添加到全局上下文。例如:

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

上一段代码中,sum为局部变量,下面一段代码中sum为全局变量

同时,使用var声明的变量会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前,这个叫做变量提升

使用let的块级作用域声明

let的作用域是块级的,块级作用域由最近的一对{}界定。
同时let与var另一个不同点是在同一作用域内,不能重复进行let声明

使用const的常量声明

使用const声明的变量必须同时初始化为某个值,一经声明,不能再重新赋值。但是对引用对象的属性值的更改不受影响。

标识符查找

在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。搜索开始于作用链前端,以给定的名称搜索对应的标识符。找到则停止搜索,否则继续沿着作用域链搜索。

垃圾回收

JavaScript是使用垃圾回收的语言,执行环境负责在代码执行时管理内存。通过自动内存管理实现内存分配和闲置资源回收。

基本思路:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,每隔一段时间自动运行。

垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便于回收内存。如何标记未使用的变量有不同的实现方式,在浏览器发展史上,用到过两种主要的标记策略:标记清理、引用计数。

  • 标记清理:是JavaScript最常用的垃圾回收策略。当变量进入上下文时会被加上存在于上下文的标记,而在上下文中的变量逻辑上讲永远不该被释放。当变量离开上下文时,也会被加上离开上下文的标记。垃圾回收程序运行时,会标记内存中存储的所有变量,然后它会讲所有在上下文中的变量,以及被在上下文中变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的变量,因为在任何上下文中的变量都访问不到他们。随后垃圾回收程序做一次内存清理,销毁带标记的所有制并收回内存/
  • 引用计数:对每一个值都记录它被引用的次数。当一个值的引用数为0时,就无法访问到这个值,因此就可以安全地回收其内存。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值