JS 进阶(1) ECMAScript5 — 变量、作用域、js解析机制、垃圾收集机制、释放内存
一、 变量
js中的变量就是用来保存数据的容器,是js的核心之一,如果没有变量js的操作都是扯淡。
js有两种数据类型
- 基本类型(5种)
数字 number 4,12,34,12,NaN
字符串 string '阿西吧','苟...','123','abc'
布尔 boolean ture/false
underfined underfinded
null null
- 引用类型(2种)
对象 object {}、{name:'poorpenguin',sex:'man'}
数组 array []、[1,2,3,4]
ps:NaN是一个特殊的number类型,NaN==NaN结果为false,typeof(NaN)的值为number,使用isNaN()检测
基本类型和引用类型的区别
基本类型 | 引用类型 |
---|---|
不可修改 | 可以修改 |
保存在栈内存 | 保存在堆内存 |
按值访问 | 按引用访问 |
比较时,值相等及相等 | 比较时,同一引用才相等 |
复制时,创建一个副本 | 复制时,复制的是指针(地址) |
用typeof检测类型 | 用instanceof检测类型 |
1.1 基本类型和引用类型在内存中的存储方式
内存分为栈内存和堆内存,栈内存是固定大小,堆内存是可以扩建的。
基本类型
//定义两个基本数据类型
var str = 'abc';
var num = 1;
房间名就是变量名。
总结:保存基本类型的变量保存的是值本身
引用类型
var person = {name:'poorpenguin',sex:'man'}
栈内存(变量)中存放的是指定该对象的指针(地址)
总结:引用类型保存在堆内存中,保存引用类型的变量保存的并不是对象本身,而是一个指向该对象的引用地址
1.2 基本类型和引用类型值的比较
基本类型比较
基本类型直接只用值进行比较就行。
var str1 = 'bac';
var str2 = 'abc';
str1 == str2; // false
引用类型比较
比较两个存放引用类型的变量,单纯的比较值是无用的。
var xm = {age:12,sex:'man'};
var xg = {age:12,sex:'man'};
上面两个对象很像,但是xm
和xg
这个两个变量指向不同的地址,所以
xm == xg; //false
所以要比较两个引用类型的变量的内容是否一样,要遍历里面的内容进行比较。
//遍历对象
function equalObj(a,b){
for(var p in a){
if(a[p]!==b[p]){
return true;
}
}
return true;
}
//遍历数组
function equalArr(a,b){
if(a.length!==b.length)return false;
for(var i=0; i < a.length; i++){
if(a[i]!==b[i])return false;
}
return true;
}
但是对象中的属性的值有可能还是个引用类型,上面简单的比较是无效的,需要用到递归。
1.3基本类型和引用类型的拷贝
基本类型
单纯的赋值即可。
var str1 = 'abc';
var str2;
str1 = str2;
引用类型
var xm = {age:12,sex:'man'};
var xg;
xg = xm;
对于引用类型来说,变量间的赋值只会让拷贝指针,让两个变量指向同一个对象。
所以如果要进行浅拷贝使用遍历即可。
如果要进行深度拷贝,需要用到递归,或者jquery
中的方法
//jquery
$.extend()
该方法既可以进行浅拷贝,也可以进行深拷贝。
1.4基本类型和引用类型的类型判断
基本类型
使用typeof
判断基本类型,typeof
既可以当成运算符,也可以当做方法typeof()
console.log(typeof 4); //number
console.log(typeof 'abc'); //string
console.log(typeof null); //object 特别注意
console.log(typeof underfined); //underfined
console.log(typeof true); //boolean
console.log(typeof {}); //object
console.log(typeof []); //object
console.log(typeof function(){}); //function
console.log(typeof /a/); //正则 object
可以看出typeof
判断数组和对象都是object
引用类型
引用类型使用instanceof
进行判断
console.log([] instanceof Array); //true
console.log({} instanceof Object); //true
console.log([] instanceof Object); //true
console.log(1 instanceof Number); //false
console.log(null instanceof Object); //false 特别注意
二、作用域和作用域链
作用域就是变量起作用的区域、范围。
作用域分为两种:
- 全局作用域
- 局部作用域(函数作用域)
- JS没有块级作用域(这里说的是ES5,在ES6中是有块级作用域的)
2.1 js没有块级作用域(ES5中,特别注意)
块级作用域就是被{}
里面的作用域,如C语言中
if(){
...
}
for(){
...
}
在{}
中声明的变量在外面是访问不到的。
但是 js没有块级作用域,所以{}
中声明的变量是外面是可以访问的
if(true){
var name ='poorpenguin';
}
console.log(name); //控制台打印poorpenguin
2.2 全局作用域
所有的全局空间中的属性和方法都是在window
这个对象上的属性和方法。
var name = 'poorpenguin';
function fn(){
var sex = 'male';
}
console.log(window.name === name); //true
console.log(window.fn === fn); //true
但是不存在的变量会报错,不存在的属性只会显示underfined
console.log(name); //这个会报错
console.log(window.name); //显示underfined
2.3 局部作用域(函数作用域)
函数作用域就是在函数内。
函数的参数是在局部作用域中的。
function(){
//函数作用域
}
for(函数作用域){
}
注意
for(函数作用域){
//全局作用域
}
//例如
for(var i=0;i<n;i++){
....
}
//这个i就是在函数作用域中的,随着函数的结束而消失,不会污染全局变量。
2.4 作用域链
一个补充,大家都知道在局部作用域中可是使用全局作用域中的变量,这个通过作用域链实现的。
var name = '雅典娜';
var sex = 'female';
function fn1(){
var name = '米罗';
var age = 18;
fn2();
function fn2(){
var name = '穆';
console.log(name); //穆
}
}
我们console.log(name);
,会现在当前所在的作用域下去找name
这个变量,如果有输出,没有就通过作用域链一级一级向上找。
很像dom2级事件中的冒泡机制,从目标元素一级一级向上冒泡。
三、js解析机制
看一个例子,目标是想通过作用域链去打印全局作用域下的name的值
var name = 'poorpenguin';
var sex = 'male';
function fn(arg){
console.log(name);
var name = 'mengfeng';
var sex = 'male';
}
fn();
如果看过上面作用域链的话,一般都会认为会输出poorpenguin
,但是实际输出
这就是涉及到js的解析机制了。
js解析机制
- 先 预解析
- 再 逐行解读代码(一步步执行代码)
3.1 预解析(编译阶段)
这个阶段有多种说法:预解析、编译阶段。
预解析也可以说编译阶段发生了什么?
- 找到所有的变量声明(必须var开头的)和函数声明(必须function开头的)
必须function开头的意思是,前不能有任何的符号。
function fn(){} 这种样子
如果有其他的符号,就不会声明(提升)提前。(但是这种机制在函数中有妙用见:JS进阶(2))
(function fn(){}) 这种不会提升
!function(){} 这种不会提升
(function(){}) 这种不会提升
- 将 所有的声明 提升到作用域的最顶上。(函数的优先级别变量高)
(1). 先将所有的 函数声明 提升 到 各自所在作用域 的最顶上面。
(2). 再将所有的 变量声明 提升 到 各自所在作用域 的最顶上,并 所有变量的默认值 为underfined
- 所有赋值操作、业务逻辑原地等待。(只有在执行阶段的时候才会执行)。
函数中的形参的作用域是局部作用域,在预解析阶段默认值也为underfinde
。
举个例子
var a = 1;
function fn(){
console.log(a);
var a = 2;
console.log(a);
}
fn();
console.log(a);
在预解析阶段,上面的代码相当于下面的代码。
在全局作用域下的变量声明和函数声明提升了,赋值操作,等其他逻辑操作原地等待。
function fn(){
var a; 局部作用域下的变量声明和函数声明提升了,赋值操作,等其他逻辑操作原地等待。
console.log(a);
a =2;
console.log(a);
}
var a; 全局作用域下的变量声明和函数声明提升了,赋值操作,等其他逻辑操作原地等待。
a = 1;
fn();
console.log(a);
PS:console.log(a);
这个不是函数声明,是调用;只有使用关键字function
的才是函数声明2
在预解析阶段,如果变量名和函数名冲突,会舍弃变量,保留函数;
函数名和函数名冲突,会舍弃前面的,保留后面的。(请看案例3)
3.2 逐行解读代码(执行阶段)
预解析完后,到了逐行解读代码的时候。原地等待的赋值操作和业务逻辑开始一条一条执行。
如果变量有值就将值付给变量,如果没有赋值就保持underfined
,函数该调用的调用。
3.3预解析深入理解的案例
案例1:
document.write(fn);
var fn = function(){}
这里输出的是underfined
在预解析阶段进行函数和变量的提升,上面的代码相当于
var fn;
document.write(fn);
fn = function(){}
案例2:
document.write(fn);
function fn(){}
这里输出的是function fn(){}
在预解析阶段
function fn(){}
document.write(fn);
案例3:
在预解析阶段,如果变量名和函数名冲突,会舍弃变量,保留函数;
函数名和函数名冲突,会舍弃前面的,保留后面的;
document.write(a);
var a = 1;
function a(){
document.write('a1');
}
function a(){
document.write('a2');
}
document.write(a);
a();
所以在预解析阶段,舍弃变量,保留最后的函数
fucntion a(){
document.write('a2');
}
document.write(a); 这打印fucntion a(){document.write('a2');}这个函数
a =1;
document.write(a); 这打印1
a(); 由于a在上一条被赋值为1,不是函数,所以这报错
四、垃圾收集机制
垃圾收集机制就是,释放无用的数据,回收内存。
由于js是自动收集的,所以很多人都不知道还有垃圾收集这回事,但是了解垃圾收集机制关键的时候有可能会救命。
垃圾收集分为两种:
- 自动收集
- 手动收集
垃圾收集的原理:
找出没用的数据,打上标记,释放内存。前面的过程会被垃圾收集器周期性的执行。
垃圾收集标识无用数据的策略:
1.标记清除(目前浏览器的主流的垃圾回收策略):给所有的数据打上标记,变量离开环境是清除内存。
2.引用计数(大多数浏览器不适用该策略,只有老版本IE使用):变量被其他值引用后+1,而引用的变量又引用了其他值-1,如果本身变量又引用其他值后,原来的值就变成了0,垃圾回收,缺点:相互引用的情况不能回收垃圾。
var xm = {name:'xiaoming',age:18}; //{name:'xiaoming',age:18}数据的引用次数1
var xh = xm; //{name:'xiaoming',age:18}的引用次数2
var xm = {}; //{name:'xiaoming',age:18}的引用次数1
var xh = {}; //{name:'xiaoming',age:18}的引用次数0,垃圾回收,释放占用的内存。
五、释放内存
电脑分配给web浏览器的内存少于分配给左面应用程序。
为了减少内存资源的开销,释放没用数据内存。
手动释放内存:将没用的全局变量赋值NULL。
var arr=[....] //这里的数据很大
....
...
... 到这里arr 这个全局变量已经无用的
arr = null; 这样便可以释放原数组占用的内存。
---
---
理论上来说,局部变量再函数结束的时候就已经释放内存,但是有时候也可以在函数中释放无用的局部变量。