关于函数和变量的处理机制,属于我们学习js的必备技能,
也是面试经常会遇到的问题
分享一点自己整理的知识点,加个人理解
- 浏览器和NODE运行JS代码的时候,会提供一个供代码赖以执行的环境,我们把这个环境称之为“全局作用域”
浏览器:window
NODE:global
window和global代表全局作用域,他们也是全局对象(在JS代码的任何位置,我们都可以基于window.xxx或者global.xxx调取全局对象中的属性和方法[这些属性方法一般都是浏览器内置的])
//=>例如:
window.setTimeout(function(){},1000);
window.alert();
...
- 环境创建完成后,JS代码没有立即执行,而是做一些词法解析,其中词法解析中有一个重要的操作就是“变量提升” => 在变量提升这个操作阶段,渲染引擎会把“当前作用域”下所有“带VAR和FUNCTION关键字”的变量进行提前的“声明或者定义”
[声明:declare]
//=>var [variable name];
var xxx; //=>声明就是在创建变量或者函数
function fn;
[定义:defined]
//=>[variable name]=[value];
xxx=100; //=>定义就是在给声明的变量赋值
fn(){};
//=>只声明未定义,默认值是:undefined(未定义)
- 带VAR关键字的在变量提升阶段只声明
带FUNCTION关键字的在这个阶段完成了“声明+定义(赋值)”
- 引用数据类型的操作步骤
- 引用数据类型
- 对象
- 普通对象
- 数组对象
- 正则对象
- 日期对象
- Math
- 函数
- 对象
由于引用数据类型存储的值过于复杂(结构复杂及内容较多),渲染引擎会开辟一个新的内存空间,单独来存储这些值,最后把空间的引用地址赋值给对应的变量,后期所有的操作都是基于地址先找到空间,然后对空间中的内容进行操作
[对象数据类型]
1. 开辟一个内存空间(有一个16进制的地址)
2. 把对象中的属性名和属性值(键值对)依次存储到内存空间中
3. 把内存空间的地址赋值给变量
/*
* [变量提升]
* var obj;
*
* [代码执行]
* 1.开辟一个内存空间(bbbfff000)
* 2.把键值对存储到空间中
* + x : 10
* + y : obj.x*3 此时的obj还没有赋值呢,键值对存储完成,才会把地址赋值给obj,此时的obj依然是undefined,undefined是无法调取属性x的,所以报错了
* obj =
*/
var obj={
x:10,
y:obj.x*3 //=>Uncaught TypeError: Cannot read property 'x' of undefined 说明此时的obj是undefined
};
console.log(obj.y);
[函数数据类型:创建一个函数]
1. 开辟一个内存空间(有一个16进制的地址)
2. 把函数体中的代码当做“字符串”存储到内存空间中
+ 函数创建的时候,存储的都是字符串,所以说函数只创建不执行是毫无意义的
+ 变量提升只对当前作用域下的var/function处理,主要原因是函数中存的都是字符串,我们看到的函数中的var/function此时都还是字符呢
+ 当函数执行的时候目的就是要把这堆字符串拿出来执行的
3. 把内存空间的地址赋值给变量
/*
* [全局作用域:变量提升]
* function fn = AAAFFF111;
* //=>var x; var y; 这两步没有,原因是此时的他们都还是无意义的字符
*/
function fn(){
var x=100;
var y=200;
console.log(x+y);
}
fn();
代码执行过程中的一些细节点
代码执行的时候,如果当前的某个操作在变量提升阶段已经处理过了,渲染引擎很懒,不会进行重复的处理,所以:遇到 var xxx=xxx 只需要给变量赋值即可,不需要重新声明;遇到 function xxx(){…} 直接的跳过即可,因为变量提升阶段函数已经声明+定义了;
函数执行
目的:把之前创建函数在内存空间中存储的“代码字符串”变为真正的JS代码执行,从而实现相关的效果
- 第一步:首先形成一个私有的作用域(给接下来的代码执行提供一个私有的环境)
- 第二步:到私有作用域中依然要先进性词法解析
- 先给形参赋值
- 私有作用域中的变量提升
- 第三步:在私有作用域中把代码自上而下执行
重要的知识点:
- 在每一个作用域(全局和函数执行产生的私有的)的词法解析阶段(变量提升或者形参赋值都在这个阶段),声明过的变量或者函数都是当前作用域私有的
+ 全局作用域下声明的变量是“全局变量”:在JS任何位置都可以使用(但是用多了会产生一些冲突和冗余“全局变量污染”)
+ 在私有作用域下,“形参”以及“声明的变量函数”都是“私有变量”:只能在当前作用域下使用,和外界的变量(即使重名)是互不干扰的
- 私有作用域代码执行过程中,遇到一个变量或者函数,但它不是当前作用域私有的变量,此时相当于在私有作用域中“操作上级作用域中的变量”
+ 代码执行遇到变量,首先验证是否是自家私有的,是私有的则和外面没有半毛钱关系,不是私有的进行第二步
+ 第二步是向当前作用域的上级作用域查找,看是否是上级作用域中私有的,如果是,那么我们此处操作的就是上级作用域中的变量,如果不是,则继续向上查找...
+ 如果找到window都没有发现哪个作用域有这个变量,说明当前变量是不存在,如果是获取这个变量值则会报错,如果是设置变量的值,相当于给window全局对象设置了一个新的属性
=>我们把这种向上级作用域一级级查找的过程称为“作用域链”
- 变量提升的细节和意义
[意义]
可以让我们开发者在代码执行之前使用变量或者函数(尤其是函数:代码执行之前已经创建完成了),不会发生错误
//=>创建变量带VAR和不带VAR的区别
1.在私有作用域中,带VAR的变量会在变量提升阶段进行声明,属于私有变量(和外界无关联);如果不带VAR也不是形参,则它不是私有的变量,这样和外界有关了!
var x=10,
y=20;
function fn(){
var x=100; //=>[私有变量]
y=200; //=>[全局变量]
}
fn();
console.log(x,y); //=>10,200
2.在全局作用域下,带VAR和不带VAR有一些区别
+ 带VAR的是全局变量(也相当于给全局对象增加了一个属性),存在变量提升
+ 不带VAR的只是给全局对象设置一个属性而已,没有所谓的变量提升
//=>在变量提升阶段,遇到相同的变量名(函数名),不会重复声明,但是需要重复赋值,最后赋的值会替换掉之前赋的值
作用域
1.如何查找当前作用域的上级作用域
函数执行会形成私有作用域(A),A的上级作用域是谁和他在哪执行没关系,只和在哪定义的有关系;在哪个作用域下定义的,A的上级作用域就是谁!
2.堆栈内存
JS中有两大内存:堆内存(Heap)、栈内存(Stack)
堆内存作用:用来存储内容的(对象存储的是键值对,函数存储的是代码字符串)
栈内存作用:也可以被称为作用域,是代码解析和执行的环境
3.堆栈内存释放问题
栈内存的释放:函数执行会形成一个私有的栈内存,一般函数执行完成,栈内存会自己释放(优化性能),除非栈内存中的某一个东西(例如:栈中开辟的堆内存)被栈内存以外的变量(或者是其它的东西)占用了,此时的栈内存就不能被释放掉;全局作用域(栈内存)是在一开始加载JS的时候诞生的,当页面关闭的时候会自动释放掉!
堆内存释放:只要没有变量占用这个堆内存,浏览器就会在空闲的时候把它释放掉,所以项目中我们尽可能把不被使用的堆内存手动释放掉,例如:obj=null;
- 函数执行会形成一个私有的作用域(栈内存),私有栈中的私有变量被作用域保护起来(和外界的变量互不干扰),而且一旦当前栈内存中的某个东西被外面占用了,当前栈内存就不会被释放掉,我们可以在栈内存中存储一些信息(这些信息也不会被销毁)… 我们把函数执行的这种“保护”、“保存”特性称之为“闭包”!!!
综上 : 我们来看两道题(附答案)
var i= 2,
x = 5;
var fn = function(x){
x+=3;
return function(y){
console.log((x++)+y+(--i));
}
};
var f = fn(1);
f(2); //7
fn(3)(4); //10
f(5); //9
为了帮助大家理解,画了一张图,看图能更清晰的理解整个题的运行过程
第二题
console.log(x, y);//=>undefined undefined
var x = 10,
y = 20;
function fn() {
console.log(x, y, z);//=>undefined 20 报错 (假如没有报错,下面继续执行)
var x = y = 100;
z = 30;
console.log(x, y, z);//=>100 100 30
}
x = fn(20);
console.log(x, y, z);//=>undefined 100 30
希望本篇博文能更好的帮助大家学习~~~ q(≧▽≦q)