JavaScript预解释原理,看这篇就够了

预解释原理和代码分析

JavaScript预解释,也就是变量提升,将var定义的变量提前声明到代码最上层,不再赘述其定义,直接上代码。
当浏览器开始解析js代码的时候,首先看当前运行环境(作用域)内带var和function,
带var的变量会提前声明(预解释)但是不会赋值,带function的会提前声明并赋值。
带var变量提前声明的时候并不会被赋值,但是有一个默认的nudefined值。当代码执行过后才会赋值。
堆栈内存:代码运行的环境在栈内存,基本数据类型都存在栈内存里, 引用类型(对象和function)在
堆里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console.log(num);
console.log(obj);
console.log(a);
console.log(sum);
var num = 12;
console.log(num);
var obj = {'name':'tianxi', age:30}; //对象类型
console.log(obj);
console.log(haha);
var a=function(){};
function sum(num1, num2){
function haha(){

};
var total = 0;
total = num1 + num2;
}

上述代码输出

1
2
3
4
5
6
7
8
9
10
undefined
undefined
undefined
[Function: sum]
12
{ name: 'tianxi', age: 30 }
E:\jswork\js-learn\01-预解释\预解释1.js:17
console.log(haha);
^
ReferenceError: haha is not defined

上述代码在运行至console.log(haha);时会崩溃。
因为预解释(变量提升)将num,obj,以及a的声明都提升到代码最上层,所以打印num,obj,a都是undefined
预解释只关注等号左边的变量,并不关心右边是什么值,所以打印a也是undefined
sum是函数,预解释会提前声明sum,并为sum赋值,比如将sum赋值为0xfffcc2d,而0xfffcc2d地址内存储的
就是sum函数体的内容。所以打印sum会输出”Function sum”
而第二次打印num会输出12,因为此时num被赋值了
打印obj会输出obj内容,sum函数内部定义的函数haha以及变量total等只有在sum运行时才会做预解释,如果
sum不运行,则haha等不会被预解释,所以打印haha会出现 “haha is not defined”的异常。

全局作用域的细节

在全局作用域下,加var和不加var的区别
1 是否被提前声明
2 不加var那么就是一个赋值过程,相当于给window添加了一个属性并且赋值
当函数在运行的时候就会产生一个私有的作用域,并且这个作用域内的变量也是私有变量。
并且这个私有变量在外访问不到,我们把这种函数运行的时候产生的私有作用域里的私有变量
不受外界干扰的这种机制叫做闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
</html>
<script>
console.log(total);
var total = 0;
function fn(num1, num2){
console.log(total);
var total = num1 + num2;
console.log(total);
}
fn(100,200);
console.log(total);
</script>

程序输出

1
2
3
4
undefined
undefined
300
0

全局变量total会预解释,所以第一个total输出undefined
接着fn预解释并定义,然后执行fn(100,200)
执行fn时,num1,num2被视为私有变量,total也被是为私有变量,在fn私有作用域预解释
所以第二个total输出为undefined,第三个total输出为300
最后一个total因为是全局变量,所以输出是0

私有作用域和作用域链

函数的运行: 1 如果有形参是形参赋值 2 预解释 3 代码逐行执行 ..
区别私有变量还是全局变量:函数运行的时候,函数体内如有带var就是私有变量,
如果是形参也可以理解是一个私有变量。代码在执行的时候,
首先查找当前运行环境内(作用域,栈)的私有变量,如果有直接用,如果没有去上一级作用域去查找,
如果有就拿来用,如果没有一直查找到顶级的window全局作用域,如果没有就报错”not defined”,
我们把这种查找机制叫做作用域链

1
2
3
4
5
6
7
8
9
console.log(total);
var total = 0;
function fn(num1, num2) {
console.log(total);
total = num1 + num2;
console.log(total);
}
fn(100,200);
console.log(total);

程序输出

1
2
3
4
undefined
0
300
300

//全局total会预解释提前声明,fn会预解释,提前声明fn并为fn赋值地址,
//因为打印第一个total,所以为undefined
//接着执行fn,因fn内部total不是私有变量,根据作用域链继续向上查找,找到window全局作用域,
//进而找到全局total,所以第二个total输出为0,第三个为300,而最后一个total输出也为300
全局作用域如果不加var对一个变量赋值,相当于给window添加一个属性

1
2
3
4
var num = 12;
console.log(num); //12
console.log(num2); //Uncaught ReferenceError: num2 is not defined
num2 = 12;

因为num2未加var定义,所以无法预解释,输出num2会异常

1
2
3
4
var num = 12;
console.log(num);
num2 = 12;
console.log(num2);

两个输出都是12,
因为num2未加var,所以相当于给window增加了一个num2的属性名,属性值为12
var num = 12相当于定义了一个全局变量num,
而且也相当于给window增加了一个属性名num,属性值为12
所以当输出num时系统会优先查找num全局变量,输出num2因为没有全局变量num2,
所以会输出window的属性名,window.num2

1
2
3
4
5
6
function fn() {
console.log(total);
total = 100;
}
fn();
console.log(total);

上述代码会报错,因为fn内部输出total会根据作用域链向上查找,没有全局变量total,
全局window中没有total这个属性,所以会异常,程序运行到fn就异常终止了。

1
2
3
4
5
function fn() {
total = 100;
}
fn();
console.log(total);

上述代码会输出100
因为fn内部对total赋值会向上查找,直到window作用域,也没有找到全局变量total
所以就相当于给window添加了一个total的属性,属性值为12,所以输出100

预解释的几个坑

1
2
3
4
if (!("num" in window)) {
var num = 12;
}
console.log(num)

1 预解释不管条件是否成立,都会把var的变量提前声明
所以”num” in window 返回ture,!就是false,那么num不会赋值,所以输出undefined

1
2
3
4
fn();
var fn = function(){
console.log("ok");
};

2 匿名函数之函数表达式,把函数定义的部分当作值赋值给一个变量或者元素的一个事件
预解释的时候只预解释”=”左边的,右边的是值,不参与预解释
window下的预解释:var fn
上述代码在执行fn();时会报异常,”fn is not a function”
因为fn预解释只做声明,未作赋值。

1
2
3
4
5
fn();
function fn(){
console.log("ok");
}
fn();

因为这种方式预解释会将fn声明并赋值为对空间的地址,地址里存储的时函数体内容。
所以当执行fn时都会输出ok
3 自执行函数定义的那个function在全局作用域下不进行预解释,当代码执行到这个
位置的时候定义和执行一起完成了,自执行函数:定义和执行在一起完成了

1
2
3
~function(){
var num = 1200;
}();
1
2
3
4
5
6
7
8
function fn(){
console.log(num); //undefined
return function(){

};
var num = 100;
}
fn();

4 函数体中return下边的代码虽然不执行了,但是需要进行预解释
return后面跟着都是返回值,所以不进行预解释,也就是fn内部返回的function()不进行预解释
所以上述代码输出undefined
5 要注意变量名冲突

1
2
3
4
var fn=13;
function fn() {
console.log("ok");
}

在js中,如果变量的名字和函数的名字重复了,也算冲突
在预解释的时候如果名字已经声明过,不需要重新声明,但是需要重新赋值

1
2
3
4
5
6
7
fn();
function fn(){ console.log(1);};
fn();
var fn=10;
fn();
function fn(){console.log(2);};
fn();

上述代码会输出两个2,第三次会报异常
因为fn为函数,预解释,提前声明并赋值内存地址,fn=xxxxfff111
因为预解释不会对同一个变量重复声明,所以var fn=10不会声明
function fn(){console.log(2);};也不会声明,但是会将fn重新赋值 fn=xxxxd209
所以会连续输出两次2,fn=10,fn()会被视为10(),这样就会报错fn is not a function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var num = 12;
function fn() {
var num = 120;
return function(){
console.log(num);
};
}

var f = fn();
f();
~function(){
var num = 1200;
f();
}();

上述代码输出

1
2
120
120

因为执行f时num不是私有变量,会继续向上查找,找到fn的私有作用域num=120,所以输出两个120

如何查找当前作用域的上一级作用域?

看当前函数是在哪个作用域下定义的,那么他的上级作用域就是谁和函数在哪里执行的没有任何关系

对象数据类型和函数数据类型在定义的时候都会开辟一个堆内存,堆内存有一个引用地址。
这个地址赋值给外部变量,那么这块内存引用计数增加,就不会释放了。
可以让外部变量赋值为null,那么堆内存的引用计数就减少,当引用计数为0,则内存就会释放了。,
全局作用域只有当页面关闭的时候才会销毁
私有作用域(只有函数执行会产生私有作用域)
一般情况下,函数执行会形成一个新的私有的作用域,当私有作用域代码执行完成后,
当前作用域都会主动释放和销毁。
特殊情况:
当前私有作用域中的部分内容被作用域以外的东西占用了,那么当前的这个作用域就不能销毁了
a 函数执行返回了一个引用数据类型的值,并且在函数的外边被一个其他的东西给接受了,
//这种情况下一般形成的私有作用域都会销毁

1
2
3
4
5
6
7
function fn(){
var num = 100;
return function(){

}
}
var f = fn();

此时fn执行形成的私有作用域不能销毁,因为function(){}的堆内存被f占用了,
所以fn无法回收自己的私有作用域
b 在一个私有作用域中给DOM元素的事件绑定方法,一般情况下私有作用域都不销毁

1
2
3
4
5
6
var oDiv = document.getElementById("div1");
~function(){
oDiv.onclick = function(){

}
}();

当前自执行函数形成的私有作用域也不会销毁
c 下述情况不立即销毁,fn返回的函数没被其他变量占用,但是需要执行一才会释放。

1
2
3
4
5
6
7
function fn() {
var num = 100;
return function(){

}
}
fn()();

首先执行fn,返回一个小函数对应的内存地址,然后让返回的小函数再执行

1
2
3
4
5
6
7
8
9
10
11
function fn(){
var i = 10;
return function(n){
console.log(n+(++i));
}
}
var f = fn();
f(10)
f(20);
fn()(10);
fn()(20);

函数输出如下

1
2
3
4
21
32
21
31

因为fn将返回的函数地址赋值给f,所以fn私有作用域不会被回收,那么i的信息就会被记录
第一次++i ,i变为11,所以f(10)为21,接下来f(20),因为fn私有作用域没有被回收,++i
i变为12,那么f(20)就是32,而fn()(10)这种方式调用fn每次都会回收私有作用域
所以i每次都是11,那么f()(10)就是21,f()(20)就是31
类似的问题还有这个

1
2
3
4
5
6
7
8
9
10
function fn(i) {
return function(n) {
console.log(n + i++);
}
}
var f = fn(13);
f(12);
f(14);
fn(15)(12);
fn(16)(13);

程序输出

1
2
3
4
25
28
27
29

fn内部调用function(n)因为i会根据作用域链向上查找,找到fn的形参i,所以此时i为13,f(12)为25
f(14)因为fn不会释放作用域,所有i为14,此时f(14)为28
fn(15)(12)每次运行都会产生新的作用域,调用结束后释放,所以为27
fn(16)(13)结果为29

如何判断this

js中this代表的是当前行为执行的主体;
js中context代表的是当前行为执行的环境
this是谁和函数在哪定义的和在哪执行的都没有关系
1 函数执行, 首先看函数名前是否有”.”,如果有”.”前面是谁this就是谁
如果没有”.”,this就是window

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function fn() {
console.log(this);
}
var obj={
fn: fn
};
fn() //this为window
obj.fn(); //this为obj
function sum(){
fn(); //this为window
}
sum();

var oo= {
sum: function(){
//this为oo
fn(); //this 为window
}
};
oo.sum();

2 自执行函数中的this永远是window
3 给元素的某一个事件绑定方法,当事件触发时候,执行对应的方法,方法中的this就是当前元素

1
2
3
4
5
6
7
function fn() {
console.log(this);
}
document.getElementById("div1").onclick = fn; //fn中this为元素
document.getElementById("div1").onclick = function(){
fn(); //fn中的this为window
};

fn赋值给div元素的onclick,每次点击后fn打印的this都是当前元素
而将匿名函数赋值给onclick,每次点击后fn打印的this都是window

综合运用实战

接下来我们综合运用实战下,以下例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var num = 20;
var obj = {
num:30,
fn:(function (num){
this.num *= 3;
num += 15;
var num = 45;
return function(){
this.num *= 4;
num += 20;
console.log(num);
}
})(num)
};

var fn = obj.fn;
fn();
obj.fn();
console.log(window.num, obj.num);

给fn赋值的自执行函数function的形参num传递的值为全局变量num,num为20,而不是30
只有明确指明obj.num才是传递obj的num
执行自执行函数num为20,this.num中this为window所以this.num*=3所以全局num变为60,私有作用域num为20,
所以var num预解释不做声明
num+=15,num变为35,接下来num赋值为45,最后返回匿名函数function()
此时调用fn(),this.num为全局num,this.num*=4,全局变量变为240,num+=20,num会根据追踪链追踪至上层的num
此时num为45,所以num+=20后变为65,先输出65
obj.fn()中的this为obj,obj此时num为30,所以this.num*=4,obj的num变为120,num+=20会向上查找,找到自执行函数
因为匿名函数被外界引用所以无法释放,进而自执行函数num无法释放,num之前为65,num+=20,num变为85
输出window.num为240,obj.num为120

感谢关注

感谢关注我的公众号
wxgzh.jpg