一、什么是预解释
js代码执行之前,浏览器会首先默认把所有带var和function的进行提前的声明和定义。
1. 理解声明和定义
声明:告诉浏览器在全局作用域中有该变量。eg:var a;function fn;
定义:对变量进行赋值。eg:a = 1;fn = xxxfff111(该地址指向内存中的函数体字符串)
注意:变量只声明时,默认值是undefined。
2. 带var和function关键字的在预解释时操作是不一样的
var:在预解释时只是提前声明(只有当代码执行的时候才会完成赋值操作)。
function:在预解释时声明和定义一起完成(在代码执行的时候,遇到定义的代码直接跳过)。
//预解释:var num; plus = xxxfff111;
console.log(num); //undefined
var num= 100;
console.log(plus); //plus(num1,num2){var total = num1 + num2;console.log(total);}
//代码执行到这一行会直接跳过,因为在预解释时声明和定义已经完成
function plus(num1,num2){
var total = num1 + num2;
console.log(total);
}
//代码执行到这行时,给num赋值100
console.log(num); //100
3. 预解释只发生在当前的作用域下
eg:开始只对window下进行预解释,只有函数执行的时候才会对函数中的进行预解释。
二、作用域和闭包
1. 全局变量和私有变量
1.1 概念
全局变量:在全局作用域下声明的变量。
私有变量:在私有作用域中声明的var变量和函数的形参。
1.2 如何区分变量是私有的还是全局的?
在私有作用域中,代码执行时遇到变量,看是否为形参,看在私有作用域中是否有var声明,两者有其一则为私有变量,私有的变量和外面的没有任何关系;如果不是私有的,则往当前作用域的上级作用域进行查找,如果上级作用域也没有则继续查找,一直找到window为止,在window下的为全局变量。
注意:只有函数执行会产生私有的作用域,比如for(){}、if(){}和switch(){}都不会产生私有作用域。
//全局作用域下的预解释:var a,b,c; test = xxxfff111;
var a = 1, b = 2, c = 3;
function test(a){
//私有作用域
//形参赋值:a = 10
//预解释:var b
a = 4; //私有变量 a = 4
var b = 6; //私有变量 b = 6
c = 5; //全局变量 c = 5
}
test(10);
console.log(a);//1
console.log(b);//2
console.log(c);//5
1.3 带var和不带var的区别
本质区别:带var的会进行预解释,不带var的不能预解释。
//例题1
console.log(num);//undefined
var num = 12;
//例题2
console.log(num);//Uncaught ReferenceError:num is not defined
num = 12;
说明:var num和 num=12,JavaScript实际上会将其看成两个声明;第一个声明是在预解释阶段进行的。第二个声明赋值会被留在原地等待执行阶段。例1中带var的变量进行了提前声明,所以输出undefined;例2中不带var的是不能进行预解释的,所以在前面执行就会报错。
//例题1
var total=0;
function plus(){
console.log(total);//undefined
var total=100;
}
plus();
console.log(total);//0
//例题2
var total=0;
function plus(){
console.log(total);//0
total=100;
}
plus();
console.log(total);//100
说明:例1中var total在私有作用域中进行预解释,所以第一个console打印出来的值为undefined,又因为其为私有变量,不会更改全局作用域下声明的var total,所以第二个console打印出来的值为0。例2中判断私有作用域中total是否为私有变量,不是则往上级作用域进行查找,所以第一个console打印出来的值为0;代码执行到total=100时,该total为全局变量且被重新声明赋值,所以第二个console打印出来的值为100。
2. 闭包
闭包是一种机制,函数执行时形成一个新的私有作用域保护了里面的私有变量不受外界的干扰(外面修改不了私有的,私有的也修改不了外面的)。这是因为当函数执行时,首先会形成一个新的私有作用域,然后按以下步骤执行:
1)如果有形参,先给形参赋值
2)进行私有作用域中的预解释
3)私有作用域中的代码从上到下执行
var total = 0;
function plus(num1,num2){
console.log(total);//undefined,total是私有变量,外面的修改不了私有的
var total = num1 + num2;
console.log(total);//35
}
plus(20,15);
console.log(total);//0,私有的也修改不了外面的
3. JS中的内存分类
栈内存:用来提供一个供JS代码执行的环境,即作用域(全局作用域和私有作用域)。
堆内存:用来存储引用数据类型的值。
注意:引用类型中,对象和函数的操作过程是有差别的
对象操作过程:1.开辟内存空间 2.把对象的属性名和属性值存储起来 3.把地址赋值给变量名
函数操作过程:1.开辟内存空间 2.把函数体中的代码当作字符串存储起来 3.把地址赋值给变量名
4. 内存释放和作用域销毁
堆内存:对象数据类型或者函数数据类型在定义的时候都会开辟一个堆内存,堆内存有一个引用的地址,如果外面有变量引用了该地址,意味着该内存被占用了,就不能被销毁。要销毁堆内存只要把变量都赋值为null,即空对象指针,谁都不指向了,浏览器在空闲的时候会把内存释放。
栈内存:栈内存释放,即作用域销毁,JS中只存在全局作用域和私有作用域。
- 全局作用域:只有当页面关闭的时候全局作用域才会销毁。
- 私有作用域:正常情况下,函数执行会形成一个新的私有作用域,当私有作用域中的代码执行后,当前的作用域都会主动地进行释放和销毁,但还有以下几个特殊情况
1)当前私有作用域中的部分内存被作用域以外的变量占用了,那么当前这个作用域就不能销毁了
functions fn(){
var num = 100;
return function(){}
}
//fn函数执行返回了一个引用数据类型的值,
//并且在函数外被变量(或其它东西)给接收了,
//这种情况下fn执行形成的私有作用域就不会被销毁
var f = fn();
2)在一个私有的作用域中给DOM元素的事件绑定方法,这种情况下的私有作用域一般都不销毁
//通过DOM方法获取的元素/元素集合都是对象数据类型的值
//eg:oDiv -> xxxfff000: {id:"content",style:[object],onclick:null}
//自执行函数形成的私有作用域中oDiv.onclick被赋值一个函数,即oDiv.onclick->xxxfff111
//自执行函数执行完毕后不会被销毁,因为xxxfff111被当前作用域以外的onclick引用了
var oDiv = document.getElementById("content");
+function(){
oDiv.onclick = function(){}
}();
3)下述情况属于不立即销毁:fn返回的函数没有被其它的东西占用,但是还需要执行一次呢,所以暂时不销毁,当返回的值执行完成后,浏览器会在空闲的时候把它销毁
function fn(){
var num = 100;
return function(){}
}
fn()();
三、预解释五大机制
1. 预解释的时候不管条件是否成立,都要把带var的进行提前的声明
if(!("num" in window)){
//这句话会被提到大括号之外的全局作用域:var num;->window.num
var num = 12;
}
console.log(num);//undefined
2. 预解释的时候只解释“=”左边的,右边的值不参与预解释
fn();//Uncaught TypeError: fn is not a function
//window(全局作用域)下的预解释:var fn;
var fn = function(){
console.log("ok");
}
3. 自执行函数在全局作用域下不进行预解释,当代码执行到这个位置时定义和执行一起完成
自执行函数常见的有以下几种形式:
(function(num){})()
+function(num){}();
-function(num){}();
~function(num){}();
!function(num){}();
4. 函数体中return下面的代码不执行,但还是需要预解释;return后是返回值,所以不进行预解释
function fn(){
//私有作用域下预解释:var num;
console.log(num);
return function(){};
var num = 1;
}
fn() //undefined
5. 在预解释的时候,如果名字已经声明过了,不需要重新声明,但需要重新赋值
注意:函数声明和变量声明都会被提升。但是函数会首先被提升,然后才是变量。
function a() {}
var a;
//打印出来是function,函数的声明提升优先级高于变量,变量声明会被函数声明覆盖
console.log(typeof a)
//不会重新声明,但是可以重新赋值
//预解释:c = xxxfff111;
//代码执行:c = 1; c(2);
var c = 1
function c(c) {
console.log(c);
var c = 3;
}
c(2)//Uncaught TypeError: c is not a function
//分析步骤:
//1.预解释:fn = xxxfff111;(fn被声明和定义,当遇到var fn时不会重新声明,再次遇到fn时会被重新赋值)
//2.代码执行:fn()->2; fn()->2; fn = 10; fn()->Uncaught TypeError:fn is not function; fn();->该代码不执行
fn();
//fn -> xxxfff000
function fn(){console.log(1);};
fn();
var fn=10;
fn();
//fn -> xxxfff111
function fn(){console.log(2);};
fn();