前端基础夯实--(JavaScript系列)JS变量和作用域

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

1、这篇博客将和大家分享4个内容,查漏补缺,变量,作用域,内存问题

2、查漏补缺

1、什么是变量?:在说明这个问题的时候需要先知道什么数据,在JS中,我们将4(数字),‘star’(字符串),true(布尔值),{}(对象),[](数组)这些都称之为数据。有的数据只需要使用一次,有的需要多次使用,而且数值不同,那么我们就需要使用一个容器作为变化的数据的储存之地,这个容器就是变量。所以变量就是保存数据的容器

2、变量是如何命名? 变量的命名要满足3个要求:(1)只能由$,_,数字,字母组成,数字不能开头。(2)JS是区分大小写的。(3)不能和关键字和保留字重名。建议:使用有含义的命名,建议使用下划线或者驼峰命名法。

3、数据类型和堆栈

1、数据分为基本类型引用类型

2、基本类型不能修改:这个概念很重要,很多同学会认为误解,比如下面的代码:

var num=4;
num=3;

很多人会认为,这不就是修改了值么,把4修改为3,但是这里不是修改,4是不可能突变成3的,这里只是覆盖!再比如:

var str='string';
var anotherStr=str.replace('s','');
console.log(str+' | '+anotherStr); //string | tring 

你会发现,字符串是基本类型,即使用了replace,str也没有变,所以,字符串的所有方法都不是在原有的字符串上修改,而是返回了一个新的字符串

3、引用类型就可以修改,因为引用类型可以添加属性,修改属性,删除属性,基本类型就没有办法添加属性,那你会产生疑惑,那平时用的基本类型都能调用属性方法啊,string.replace();string.indexOf()等等,其实字符串是向自己的包装对象String借的方法,数字也是一样,向Number借的方法,这些都涉及到原型链的问题,你可以下去研究一波。

4、堆栈的问题

1、栈里面的数据都是有序的,但是栈有缺陷,大小是有限的,不能扩充,就像一个小房间一样,就是作为基本数据的一个容器,基本类型保存在栈当中。而堆中数据无序,而且大小不固定,所以引用类型是数据可以变化,而变化的大小又不一样,所以保存在堆中。又因为无序,所以不可能去堆中瞎找,需要有个特定的门牌号,而门牌号就是固定大小的,所以引用类型是在栈中放地址,堆中放值

5、变量比较和值的赋值

1、变量的比较就分为基本类型的比较引用类型的比较。比较重要的就是引用类型的比较,如下面代码所示:

var xiaoming={
    age:18,
    score:4
};
var xiaohua={
    age:18,
    score:4
};
console.log(xiaoming===xiaohua); //false

上面的代码中,小明这个对象在堆中开辟了一个空间,小花这个对象在堆中另外开辟了一段空间,它们在栈中的两个xiaoming和xiaohua分别指向堆中不同的内存空间,所以就算两个对象的属性和值都一样,本质上在堆中它们分布在不同的空间中,属于不同的东西。如果两个对象相等,除非它们指向同一个引用。那么下面看看什么是同一个引用:

var xiaoming={
    age:18,
    score:4
};
var xiaohua = xiaoming;
console.log(xiaoming===xiaohua);//true

上面的代码中两个对象是相等的,因为把小明这个对象在栈中的地址,或者是引用赋值给了xiaohua,所以xiaoming和xiaohua指向的是堆中同一段内存空间,两者当然是一个东西。

2、如何比较两个对象中的值相同:使用遍历,没有别的什么神奇的方法:

function equalObj(a,b){
    for(var p in a){
    if(a[p]!==b[p])return false;
    return true;
}}

3、复制变量的值:复制变量的值也分为基本类型的复制引用类型的复制

(1)基本类型的值的复制就是简单的复制,复制后的变量和原变量没有关系:

var xmScore=4;
var xhScore=xmScore;
xhScore++;
console.log(xmScore);//4
console.log(xhScore);//5

(2)引用类型的值的复制其实也是复制,但是复制的是地址,经过复制两者拥有同一段在堆中的地址,或者说两者拥有堆中同一个房子的钥匙,修改堆中的值一定会影响另外一个引用变量

var xm={
    age:18,
    score:4
};

var xh=xm;
xh.score++;
console.log(xh.score);//5
console.log(xm.score);//5

那现在我想复制引用类型的变量,又想让复制后的变量独立怎么办?只能遍历要复制的变量的属性,比如下面这样:

var xm={
    age:18,
    score:4
};
function copyObj(a){
    var newObj={};
    for(var p in a){
        newObj[p]=a[p];
    }
    return newObj;
}
var xh=copyObj(xm);

不过上面的代码是很简单的例子,只涉及到浅拷贝,也就是只有基本类型的拷贝,深拷贝我们后面再说。

6、参数的传递和类型检测

1、参数的传递和变量的复制是密不可分的,基本类型的参数的传递就是变量的复制,实参和形参没有什么关系:

function addTen(num){
    return num+10;
}
var score=10;
console.log(addTen(score));//20
console,log(score);//10

2、引用类型的参数传递:其实还是传递的值,不是引用,只不过此时的值就不是基本类型的那种数值,而是地址,地址也是基本的数据类型,是放在栈中的,所以不管是什么参数传递,传递的都是值。只不过这个值有可能是数值,也可能是地址。来看下面的例子:

这个例子比较容易被搞错结果:很多人认为结果是'xh',那我们分析一下,假如person在栈中的地址为‘123abc’,那么按照之前说的参数传递都是值传递,那么obj也是‘123abc’,此时两者在栈中存放的是同一段堆中内存空间的地址‘123abc’,那么函数setName中的第一行:obj.name='xm';就是修改了person的属性。但是第二行,obj={},这句话就将obj在栈中存的地址的值‘123abc’修改为了其他堆中一个新的对象的地址,反正不是‘123abc’,所以此时person和obj就不指向堆中同一段内存了。后面的obj.name的修改就和person没关系了,所以整个setName中对person进行操作的就只有第一行。

function setName(obj){
    obj.name='xm';
    obj={};
    obj.name='xh';
}
var person={};
setName(person);
console.log(person.name);//'xm'

3、类型检测

(1)typeof作为加测数据类型的关键字,返回的是字符串:

console.log(typeof 4);//number
console.log(typeof 'str');//string
console.log(typeof true);//boolean
console.log(typeof undefined);//undefined
console.log(typeof null);//object
console.log(typeof []);//object
console.log(typeof {});//object
console.log(typeof function(){});//function
console.log(typeof /a/);//object(有的浏览器将正则表达式返回的是function)

(2)instanceof关键字,XX instanceof YY表示XX是 YY的实例,判断具体的引用类型,这里注意instanceof只能用于引用类型

console.log([] instanceof Array);//true
console.log({} instanceof Object);//true
console.log(1 instanceof Number);//false,不能判断基本类型

7、全局作用域和局部作用域

1、什么是作用域?作用域有两层含义,一层就是变量的生命周期,第二层就是哪里能够访问到变量。

2、全局作用域是存在于整个程序当中,生命周期和程序的执行和结束相关,而局部作用域也就叫做函数作用域,变量的生命周期和函数执行和结束相关。局部作用域为什么叫做函数作用域,因为没有块级作用域这个概念

3、全局作用域的变量在局部可以访问的到,但是局部作用域的变量是在全局访问不到的,原因我们下面在作用域链中说。

8、变量对象和作用域链

1、什么是变量对象,变量对象就是每个作用域中的老大。如下代码:

(1)在全局作用域中,有name 和fn两个变量,所以在全局作用域中存在window这个变量对象负责帮JS引擎来调用存在全局作用域中的变量,所以我们在全局作用域中,使用name可以直接使用,也可以使用window.name,同理window.fn和fn是一样的。

(2)在fn这个局部作用域里存在sex和fn2,所以这个局部作用域中存在变量对象fn来帮JS引擎调用fn局部作用域中的变量,比如fn.sex和fn.fn2(就是这个意思,fn局部作用域中的变量对象未必就叫fn)

var name='xm';
function fn(){
    var sex='male';
    function fn2(){
        var age=18;
    }
}

2、作用域链:先写出一段简单的代码:作用域链其实方便我们查找变量

var name='xm';                     //全局作用域:name fn
function fn(){                     //fn局部作用域:name sex fn2
    var name='xh';
    var sex='male';
    function fn2(){                //fn2局部作用域:name age
        var name='xhei';
        var age=18;
    }
}

可以看到有三个作用域,全局作用域,fn作用域和fn2作用域形成了一条作用域链,当我们在fn2中寻找name这个变量时,如果有直接用,如果当前作用域没有这个变量,查找并不会结束,沿着作用域链往上层作用域继续找,直到作用域链的顶端全局作用域。所以这个就是为什么内部作用域可以访问外部作用域,而外部作用域访问不到内部作用域的原因。

9、JS解析机制

9.1预解析(变量提升)

1、JS解析过程分为预解析和解析过程,所谓预解析是在所有的作用域中分别同时进行的,

var name='xm';
var age=18;
function fn(){
    console.log(name);
    var name='xh';
    var age=10;
}
fn(); //undefined

下面这段代码的预解析分为两步,在全局作用域和fn局部作用域中进行,在全局作用域中对fn函数,变量name和变量age先声明,并且把fn全部拿了过去,所以说对函数的声明就是将整体拿到了前面。而name和age被赋值为undefined,接着在fn中进行函数和变量的声明,此时fn中只有name和age,就声明并赋值为undefined。所以上述代码是按照下面这样的顺序执行的:

function fn(){
    var name=undefined;
    var age=undefined;
    console.log(name);
    name='xh';
    age=10;
}
var name=undefined;
var name=undefined;
name='xm';
name=18;

fn(); //undefined

2、预解析中函数和变量名称冲突:函数优先于变量先声明,所以假如在代码中存在变量name,又存在函数name,那么变量name都就干掉了,不存在了。

3、预解析中函数和函数名称冲突:谁写在前面谁倒霉,只保留后面写的

9.2、解析

1、说通俗一点,解析和预解析,就是JS的编译和运行。

2、经典题目1:有关未对变量x进行声明会在程序运行的时候在全局作用域中声明名称为x的变量

console.log(a);//报错
a=1;
a=1;
console.log(a);//1

结果如注释所示,,为什么?因为第一种因为编译(预解析)时候就没有识别到a,没有看见var,所以没有对a进行提升和声明。第二种,在编译(预解析)的时候也没有对a提升和申明,但是在程序运行(解析)的时候先看到a=1,发现未声明,就在全局作用域中声明了a,然后再执行打印,a在打印之前既声明了也赋值了,当然能打印出对的值。

3、经典题目2:结果是:a() 1 1 3 3 报错

console.log(a);
var a=1;
console.log(a);
function a(){
    console.log(2);
}
console.log(a);
var a=3;
console.log(a);
function a(){
    console.log(4);
}
console.log(a);
a();

解答:预解析后,a声明为一个函数,函数内部内容是console.log(4);然后后面的就按照程序一步步执行了。

4、经典题目3:结果是 :1 2

 var a=1;
 function fn(){
     console.log(a);
     a=2;
 }
 fn();
 console.log(a);

5、经典题目4:结果:1 1 

 var a=1;
 function fn(a){
     console.log(a);
     a=2;
 }
 fn(a);
 console.log(a);

10、垃圾收集机制

1、垃圾收集就是释放无用的数据,回收内存

2、垃圾收集分为自动收集手动收集,以前我们学的Object是需要手动回收垃圾的,但是JS是自动回收垃圾的

3、JS垃圾回收的原理:找出没用的数据,打上标记,释放内存

4、标识无用数据的策略:

(1)标记清除:垃圾回收器在运行的时候会给所有变量一次性都加上标记,然后会去掉环境中的变量以及别环境中的变量所引用的变量它们上的标记,什么是环境中的变量?程序运行时,还处在自己所属的环境中的变量。那些已经离开环境的变量没有被去掉标记,就会被清楚掉。

(2)引用计数:这个并不常用,通过跟踪原始对象的引用次数来判断是否该被清除,当引用次数为0,则会被清除。比如说下例:

var xm={
    name:'xm',
    age:18
};// {name:'xm',age:18}这个原始对象引用次数现在为1
var xh=xm; //引用次数为2
xh={}; //引用次数为1
xm={}; //引用次数为0

引用计数不常用是因为循环引用,比如a和b都是对象,但是a中有的属性的值为b,b中有个属性的值为a,你中有我,我中有你,当执行函数完毕后,计数不会变成0,所以就导致无法回收这段内存,如下:

function foo(){
    var a={..叫做A的空对像..};   //这个A对象被a引用1次
    var b={..叫做B的空对像..};   //这个B对象被b引用1次
    a.wife=b;    //A被引用2次
    b.husband=a;  //B被引用2次
}

foo();
xm=null;  此时A还被引用1次
xh=null;  此时B还被引用1次

//最终A B两个空对像无法被回收

11、内存管理

1、首先要知道:计算机分配的内存:web浏览器<桌面应用程序。原因是因为安全考虑,不能给浏览器太多内存,防止消耗太多内存导致卓明应用程序无法运行。

2、释放内存最简单的方法就是将变量设置为null,一般用于全局变量,因为局部变量离开环境就自动被干掉了。所以我们对于一些全局变量,什么时候不用了,什么时候就手动设置为null,因为全局变量在整个程序运行的过程中都是要占据内存的。

猜你喜欢

转载自blog.csdn.net/weixin_37968345/article/details/82595633
今日推荐