资讯专栏INFORMATION COLUMN

JavaScript学习笔记 - 变量、作用域与内存问题

lavnFan / 3039人阅读

摘要:语句中的块语句对来说,将会指定对象添加到作用域链中。在严格模式下,初始化未经声明的变量会导致错误。查询标识符搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符。

本文记录了我在学习前端上的笔记,方便以后的复习和巩固。

4.1基本类型和引用类型的值

ECMAScript变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象。

数据类型:

基本类型值:Undefined、Null、Boolean、Number、String;

引用类型值,也就是对象类型:Object、Array、Function、Date等;

声明变量时不同的内存分配

基本类型值:存储在栈(stack)中的简单数据段,它们的值直接存储在变量访问的位置。这是因为这些基本类型占据的空间是固定的,所以可以将它们存储在较小的内存区域 - 栈中。这样存储更便于迅速查寻变量的值。

引用值:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。这是因为:引用值的大小会改变,所以不能把它放在中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在中的地址。地址的大小是固定的,所以把它存储在中对变量性能无任何负面影响。

不同的内存分配机制也带来了不同的访问机制

javascript中是不允许直接访问保存在堆内存中的对象的,也就是说不能直接操作对象的内存空间。所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,这就是传说中的按引用访问。而原始类型的值则是可以直接访问到的。

注意:当复制保存着对象的某个变量时,操作的事对象的引用。但在为对象添加属性时,操作的是实际的对象

复制变量的不同

基础类型值:在将一个保存着基础类型值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。

function addTen(num) {
    num += 10;
    return num;
}
var count = 20;
var result = addTen(count);
console.log(count); //20 没有变化
console.log(result); //30

引用值:在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量,也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。(这里要理解的一点就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了)

function setName(obj) {
    obj.name = "Nicholas";
}
var person = new Object();
setName(person);
console.log(person.name); //"Nicholas"
参数传递的不同

首先我们应该明确一点:ECMAScript中所有函数的参数都是按值来传递的。但是为什么涉及到基础类型与引用类型的值时仍然有区别呢,还不就是因为内存分配时的差别。

基础类型值:只是把变量里的值传递给参数,之后参数和这个变量互不影响。

引用类型值:对象变量它里面的值是这个对象在堆内存中的内存地址,这一点你要时刻铭记在心!因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因了,因为它们都指向同一个对象呀。或许我这么说了以后你对书上的例子还是有点不太理解,那么请看图吧:

所以,如果是按引用传递的话,是把第二格中的内容(也就是变量本身)整个传递进去(就不会有第四格的存在了)。但事实是变量把它里面的值传递(复制)给了参数,让这个参数也指向原对象。因此如果在函数内部给这个参数赋值另一个对象时,这个参数就会更改它的值为新对象的内存地址指向新的对象,但此时原来的变量仍然指向原来的对象,这时候他们是相互独立的;但如果这个参数是改变对象内部的属性的话,这个改变会体现在外部,因为他们共同指向的这个对象被修改了呀!来看下面这个例子吧:(传说中的call by sharing)

var obj1 = {
  value:"111"
};
 
var obj2 = {
  value:"222"
};
 
function changeStuff(obj){
  obj.value = "333";
  obj = obj2;       
  return obj.value;
}
 
 
var foo = changeStuff(obj1);
 
console.log(foo);// "222" 参数obj指向了新的对象obj2
console.log(obj1.value);//"333"

obj1仍然指向原来的对象,之所以value改变了,
是因为changeStuff里的第一条语句,这个时候obj是指向obj1的 .
再啰嗦一句,如果是按引用传递的话,这个时候obj1.value应该是等于"222"

可以把ECMAScript函数的参数想象成局部变量

4.1.4 检测类型

如果变量的值是一个对象null,则typeof操作符会返回"object".

通常我们并不是想知道某个值是对象,而是想知道它是什么类型的对象。为此,ECMAScript提供了instanceof操作符;

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

console.log(person instanceof Object);  //变量person是Object吗?
console.log(colors instanceof Array);   //变量colors是Array吗?
console.log(pattern instanceof RegExp); //变量pattern是RegExp吗?

根据规定,所有引用类型的值都是Object的实例。在检查一个引用类型值和Object构造函数时,instanceof操作符始终会返回true

4.2执行坏境和作用域

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

每个环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它

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

4.2.1 延长作用域链

有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。有两种情况下会发生这种现象。

try-catch 语句中的 catch 块

with 语句

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

var oMyself = {
    sFirstname: "Aidan",
    sLastName: "Dai"
}

function create(){
    var sLastName = "Wen"
    with(oMyself){
        //将oMyself作为自己的执行环境
        sAllName = sFirstname +" " + sLastName;
    }
    return sAllName;
}
var sMyName = create();
console.log(sMyName); //Aidan Dai
4.2.2 没有块级作用域

对于有块级作用域的语言来说,for语句初始化变量的表达式所定义的变量,只会存在于循环的环境之中。而对于JavaScript来说,由for语句创建的变量i即使在for循环执行结束后,也依旧会存在于循环外部的执行环境中。

1. 声明变量
使用var声明的变量会自动被添加到最接近的环境中。在函数内部,最接近环境的就是函数的局部环境;在with语句中,最接近的环境就是函数环境。如果初始化变量时没有使用var声明,该变量会自动被添加到全局环境。

注意:在编写JavaScript中,不声明而直接初始化变量时一个错误的做法,因为这样可能会导致意外。在严格模式下,初始化未经声明的变量会导致错误。

2.查询标识符
搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符。如果在局部环境找到,搜索过程停止,变量就绪。如果在局部环境中没有找到该变量名,则继续沿作用域向上搜索。搜索过程将一直追溯到全局环境的变量对象。在全局环境也没找到的话则说明该变量尚未声明。

var color = "blue";

function getColor() {
    return color;
}

console.log(getColor()); //"blue";
4.3 垃圾收集

JavaScript具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。
JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是时时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

变量生命周期

什么叫不再使用的变量?不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后再函数中使用这些变量,直至函数结束(闭包中由于内部函数的原因,外部函数并不能算是结束

一旦函数结束,局部变量就没有存在必要了,可以释放它们占用的内存。貌似很简单的工作,为什么会有很大开销呢?这仅仅是垃圾回收的冰山一角,就像刚刚提到的闭包,貌似函数结束了,其实还没有,垃圾回收器必须知道哪个变量有用,哪个变量没用,对于不再有用的变量打上标记,以备将来回收。用于标记无用的策略有很多,常见的有两种方式

4.3.1 标记清除

这是JavaScript最常见的垃圾回收方式,当变量进入执行环境的时候,比如函数中声明一个变量,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”。至于怎么标记有很多种方式,比如特殊位的反转、维护一个列表等,这些并不重要,重要的是使用什么策略,原则上讲不能够释放进入环境的变量所占的内存,它们随时可能会被调用的到。

垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了,因为环境中的变量已经无法访问到这些变量了,最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

4.3.2 引用计数

在低版本IE中经常会出现内存泄露,很多时候就是因为其采用引用计数方式进行垃圾回收。引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加1,如果该变量的值变成了另外一个,则这个值得引用次数减1,当这个值的引用次数变为0的时候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为0的值占用的空间。

4.3.3 性能问题

垃圾收集器是周期性运行的,而且如果为变量分配的内存数量很可观,那么回收工作量也是相当大的。在这种情况下,确定垃圾收集的时间间隔是一个非常重要的问题。

事实上,在有的浏览器中可以触发垃圾收集过程,但我们不建议这样做。在IE中调用window.CollectGarbage()方法会立即执行垃圾收集。在Opera7及更高版本中,调用window.opera.collect()也会启动垃圾收集例程。

4.3.4管理内存

确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要数据。一旦数据不再可用,最好通过将其值设置为null来释放其引用——这个方法叫做解除引用(dereferencing)。这一做法适用于大多数全局变量和全局对象属性。局部变量会在它们离开执行环境时自动被解除引用。

function createPerson(name) {
     var localPerson = new Object();
     localPerson.name = name;
     return localPerson;
}
var globalPerson = createPerson("Nicholas");

//手工解除globalPerson的引用

globalPerson = null;
4.4 小结

基本类型值和引用类型值具有以下特点:

基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中;

从一个变量向另一个变量复制基本类型的值,会创建这个值得一个副本;

引用类型的值是对象,保存在堆内存中;

包含引用类型值得变量实际上包含的并不是对象本身,而是指向该对象的指针;

从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同一个对象;

确定一个值是哪种基本类型可以使用typeof操作符,而确定一个值是哪种引用类型可以使用instanceof操作符。
所有变量(包括基本类型和引用类型)都存在于一个执行环境(也称为作用域)当中,这个执行环境决定了变量的生命周期,以及哪一部分代码可以访问其中的变量。

最后,如有错误和疑惑请指出,多谢各位大哥

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82338.html

相关文章

  • 《你不知道的javascript笔记_作用域与闭包

    摘要:建筑的顶层代表全局作用域。实际的块级作用域远不止如此块级作用域函数作用域早期盛行的立即执行函数就是为了形成块级作用域,不污染全局。这便是闭包的特点吧经典面试题下面的代码输出内容答案个如何处理能够输出闭包方式方式下一篇你不知道的笔记 下一篇:《你不知道的javascript》笔记_this 写在前面 这一系列的笔记是在《javascript高级程序设计》读书笔记系列的升华版本,旨在将零碎...

    galaxy_robot 评论0 收藏0
  • 你不知道的JavaScript上卷之作用域与闭包·读书笔记

    摘要:的分句会创建一个块作用域,其声明的变量仅在中有效。而闭包的神奇作用是阻止此事发生。依然持有对该作用域的引用,而这个引用就叫做闭包。当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。 date: 16.12.8 Thursday 第一章 作用域是什么 LHS:赋值操作的目标是谁? 比如: a = 2; RHS:谁是赋值操作的源头? 比如: conso...

    Raaabbit 评论0 收藏0
  • 先有蛋还是先有鸡?JavaScript 作用域与闭包探析

    摘要:而闭包的神奇之处正是可以阻止事情的发生。拜所声明的位置所赐,它拥有涵盖内部作用域的闭包,使得该作用域能够一直存活,以供在之后任何时间进行引用。依然持有对该作用域的引用,而这个引用就叫闭包。 引子 先看一个问题,下面两个代码片段会输出什么? // Snippet 1 a = 2; var a; console.log(a); // Snippet 2 console.log(a); v...

    elisa.yang 评论0 收藏0
  • javaScript作用域与闭包

    摘要:闭包里面保存的变量只有被方法引用了的变量这个例子里,闭包里只有并没有。那最后来说说的问题闭包到底是什么闭包是一个作用域。鉴于在的调试窗口,是放在下面的那闭包这个作用域是个什么范围被后代方法子方法,孙子方法。。。 首先给js的作用域这个话题打标签:2,var, 全局变量,局部变量,函数,undefined, 作用域提升,赋值不会提升,ReferenceError, 同名覆盖。打完标签之后...

    Reducto 评论0 收藏0
  • JavaScript语法理论知识点

    摘要:方法用于删除原数组中的一部分元素,并可以在被删除的位置添加新数组成员,返回值是被删除的元素。递归函数函数内部调用函数自身,称之为递归。在函数内使用声明的变量是局部变量,是人为的声明。 前言 这个笔记不知道什么时候记下的反正很有意思,很基础,很理论。 JavaScript学习笔记1 第一章 JavaScript概述 1.1 什么是JavaScript JavaScript是一种轻量级的脚...

    Amos 评论0 收藏0

发表评论

0条评论

lavnFan

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<