JavaScript预解析
JS的解析和执行过程
代码案例 – 思考:下列代码是否会报错,区别于其他语言。
1
2
3
4
5 1console.log(a);
2var a = 2;
3conosle.log(a);
4
5
该代码的输出结果为undefined 2(在c++中这样的代码是会报错的,但是在JavaScript中不会报错,由于发生了预解析的过程)
1
2
3
4
5
6
7 1// 从解析器角度看到的代码
2var a;
3console.log(a);//undefined
4a = 2;
5console.log(a);// 2
6
7
JS的解析和执行过程
-
全局预解析阶段(全局变量和函数声明前置)
-
全局顺序执行阶段(变量赋值、函数调用等操作)
-
当遇到函数调用时,在执行函数内代码前,进行函数范围内的预解析
-
当存在函数嵌套时,以此类推,会进行多次函数预解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 1// example1
2console.log(a, b); //undefined undefined
3var b = 23;
4console.log(a, b); //undefined 23
5var a = b;
6console.log(a, b); //23 23
7
8// example2
9console.log(obj1, obj2); //undefined undefined
10var obj1 = { x: 23 };
11console.log(obj1, obj2); //{x:23} undefined
12var obj2 = obj1;
13console.log(obj1, obj2); //{x:23} {x:23}
14obj2.x = 25;
15console.log(obj1, obj2); //{x:25} {x:25}
16
17// example3
18function foo() {
19 console.log("j:", j); // undefined
20 var j = 10;
21 console.log("j:", j); // 10
22}
23foo();
24console.log("j:", j); // error
25
26
预解析主要工作(变量声明和函数声明提升)
-
解析器在执行代码前的进行代码扫描(var、function)
-
将变量和函数声明在当前作用域(全局、函数)内进行提升
注:ES5中函数及变量声明重复的话,后面的函数或者变量会将前面的进行覆盖
同时有var和function关键字
-
情形1:函数表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 1// 当function前有运算符的话,认定为表达式,不提升
2foo();
3var foo = function() {
4 console.log("foo");
5};
6# Uncaught TypeError: foo is not a function
7# at <anonymous>:1:1
8
9// 上述代码等价于
10var foo;
11foo();// error
12foo = function(){
13 console.log("foo");
14};
15
16
练习:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1console.log(foo);// undefined
2var foo = function() {
3 console.log("foo");
4};
5foo();// foo
6
7// 上述代码等价于
8var foo;
9console.log(foo);// undefined
10foo = function(){
11 console.log("foo");
12};
13foo();// 函数会将变量进行覆盖 foo
14
15
-
情形2:变量名同函数名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 1AA();
2function AA() {
3 console.log("AA_1");
4}
5var AA = function() {
6 console.log("AA_2");
7};
8AA();
9
10// 上述代码等价于
11function AA() {
12 console.log("AA_1");
13}
14var AA;// 在最顶端和在这是等效的 函数会将变量进行覆盖
15AA();// AA_1;
16AA = function(){
17 console.log("AA_2");
18};
19AA();// AA_2
20
21
预解析与作用域
静态词法作用域(重要)
1
2
3
4
5
6
7
8
9
10
11 1var name = "Jack";
2function echo() {
3 console.log(name); // 此时的name指向了Jack之后不会再改变
4}
5function foo() {
6 var name = "Bill";
7 echo();
8}
9foo(); //Jack
10
11
JS采用的是静态词法作用域,代码完成后作用域链就已形成,与代码的执行顺序无关
全局变量与局部变量
-
全局变量:拥有全局作用域的变量(JS代码中任何地方都可以访问)
-
全局变量是跨域了所有函数自身作用域的自由变量,可以在函数内和函数外直接访问
-
局部变量:函数内声明的变量,只在函数体内有定义,作用域是局部性的
-
在函数外不能直接访问函数的局部变量,但可以通过闭包来访问
-
函数内访问同名变量时,局部变量会覆盖全局变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 1//example1 访问全局变量 x === window.x
2var x = "outside f1";
3var f1 = function() {
4 console.log(x);
5};
6f1(); // outside f1
7console.log(x); // outside f1
8
9//example2 局部变量会覆盖全局变量
10var x = "outside f1";
11var f1 = function() {
12 var x = "inside f1";
13 console.log(x);
14};
15f1(); // inside f1
16console.log(x); // outside f1
17
18//example3 局部变量的作用域就在局部
19var f2 = function() {
20 var y = "局部";
21 console.log(y);
22};
23f2(); // '局部'
24console.log(y); //报错
25
26//example4 在函数里不加var声明的变量默认都是全局变量
27var f2 = function() {
28 y = "全局";
29 console.log(y);
30};
31f2(); // "全局"
32console.log(y); //"全局"
33
34
声明前置与作用域的关系(全局作用域、函数作用域)
1
2
3
4
5
6
7
8
9
10
11
12
13 1// 声明前置与作用域的关系
2if (true) {
3 var i = 0;
4}
5console.log(i);
6// 等价于
7var i;
8if (true) {
9 i = 0;
10}
11console.log(i); //ES5 中没有块级作用域 0
12
13
JS作用域及其特点
JS作用域
-
作用域就是变量与函数的可访问范围(变量生效的区域范围,即在何处可以被访问到)
-
作用域控制着变量与函数的可见性和生命周期,它也是根据名称查找变量的一套规则
上述实例(嵌套作用域)中:
变量d只能在bar作用域中被访问到,变量c只能在fn和bar作用域中被访问到
在bar中访问a时为500(覆盖性)在bar中访问c时为200(链式关系)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 1var a = 10,
2 b = 20;
3
4function fn() {
5 var a = 100,
6 c = 200;
7 //console.log(a,b,c,d); 报错由于此时的d还没有定义
8 function bar() {
9 //bar局部作用域
10 var a = 500,
11 d = 600; // 若此句该为var a = 500;b = 600;则此时的b相当于是全局变量
12 console.log(a, b, c, d); // 500 20 200 600
13 }
14 bar();
15}
16fn();
17//console.log(a,b,c,d); // 报错 c d此时都没有定义
18
19
JS作用域特点
词法作用域
-
JS采用的是词法作用域(静态性),这种静态结构决定了一个变量的作用域
-
词法作用域不会被函数从哪里调用等因素影响,与调用形式无关(体现了静态性)
注意:词法作用域主要确定变量的具体内容
-
通过new Function创建的函数对象不一定遵从静态词法作用域
-
对比下边两个例子(通过不同形式定义的函数对象,访问到的scope的区别)
关于块作用域
-
大多数语言都有块级作用域,变量“存活”在最近的代码块中,比如Java中
-
JS(ES5)采用的是函数级作用域,没有块作用域
-
无块作用域的问题(变量污染、变量共享问题)变量污染、变量共享问题,尤其是异步执行的情况下。如果是两个单独的文件的情况下,更容易造成变量污染
JS执行上下文与调用栈(Call Stack)
执行上下文
-
执行上下文指代码执行时的上下文环境(包括局部变量、相关的函数、相关自由变量等)
-
JS运行时会产生多个执行上下文,处于活动状态的执行上下文环境只有一个
理解执行上下文
JS运行时会产生多个执行上下文,处于活动状态的执行上下文环境只有一个
练习:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 1//理解执行上下文(通俗的例子),嵌套的情况
2var xx = ["书桌","书包","铅笔盒"];//小明家中
3console.log("在家-做作业中 1 ...");
4function goToStore(){
5 var yy = ["文具店老板","出售的文具"];//文具商店中
6 console.log("在文具店-买文具中 ...");
7 function goToBank(){
8 var zz = ["银行职员","柜员机"];//银行中
9 console.log("在银行-取钱 ... 返回文具店");
10 }
11 console.log("在文具店-买文具中 ... 发现没带钱");
12 goToBank();
13 console.log("在文具店-买好文具 ... 返回家");
14}
15console.log("在家-做作业中 2 ... 发现笔没油了");
16goToStore();//笔没油了,去商店买笔
17console.log("在家-继续做作业...");
18
19
调用栈(Call Stack)
-
代码执行时JS引擎会以栈的方式来处理和追踪函数调用(函数调用栈 Call Stack)
-
栈底对应的是全局上下文环境,而栈顶对应的是当前正在执行的上下文环境
作用域链与执行上下文
理解代码执行时形成的作用域链(继续小明的例子)
-
如果有多个文具店和多个银行,那么执行就有多种可能,形成不同的链式关系
-
依然要遵从静态词法作用域(在A文具店,应该有A店老板,而不应有B店老板)
作用域链与执行上下文
-
执行时,当前执行上下文,对应一个作用域链环境来管理和解析变量和函数(动态性)
-
变量查找按照由内到外的顺序(遵循词法作用域),直到完成查找,若未查询到则报错
-
当函数执行结束,运行期上下文被销毁,此作用域链环境也随之被释放
JS的立即执行表达式IIFE
什么是IIFE以及其使用方式
-
IIFE英文全称:Immediately-InvokedFunction Expression即立即执行的函数表达式
-
IIFE的作用(建立函数作用域,解决ES5作用域缺陷所带来的问题,如:变量污染、变量共享等问题)
IIFE的写法
使用小括号的写法(最常见的两种)
-
(function foo( x,y){ … }(2,3)); //2,3为传递的参数
-
(function foo(x,y){ … })(2,3);
1
2
3
4
5
6
7
8
9
10 1(function count(arr){
2 console.log(arr.length);
3})([1,2,3,4]); // 4
4
5// 括号位置的不同
6(function count(arr){
7 console.log(arr.length);
8}(1,2,3,4));// 4
9
10
注意:IIFE是表达式,要注意使用分号结尾,否则可能出现错误
1
2
3
4
5
6
7
8
9
10 1(function() {
2 console.log("111");
3})()//没有分号的话会报错
4(function () {
5 console.log("222");
6})()
7# Uncaught TypeError: (intermediate value)(...) is not a function
8# at <anonymous>:4:1
9
10
与运算符结合的写法(执行函数、进行运算)
1
2
3
4
5
6 1var i = function( ){ return 10; }( ); //i为10
2true&& function( ){ ... }( );
3~function(arg1,arg2){ ... }(x,y); //x,y为传递参数 位运算非操作符
4!function( ){ ... }( );//思考!function(){return 2; }( ); 与 !function(){return 0; }( );
5
6
在上述的式子中,IIFE是一个表达式,类似于我们所说的a=2;执行的时候要注意是立即执行,因此function( ){ … }( );的结果就是返回值
1
2
3
4 1! function() { return2; }(); // fasle
2! function() { return0; }(); // true
3
4
练习下列例子:
通过IIFE来解决的问题(JS缺陷)
变量污染问题
-
JS(ES5)中没有块作用域,容易造成js文件内或文件间的同名变量互相污染
-
我们往往会通过IIFE引入一个新的作用域来限制变量的作用域,来避免变量污染
变量共享错误
- 当程序运行到变量所在作用域时,变量被创建,JS(ES5)没有块作用域,变量可能会共享
如下例:
在函数作用域中创建的变量 i 只有一个,出现了变量 i 共享问题,可通过IIFE解决
在程序的执行过程中,程序会先执行循环,之后i的值就变为了10,再每一次调用函数数组的函数时,输出的结果都是10,因此需要使用立即执行表达式来解决这个问题
代码调试过程图:
通过立即执行表达式解决问题:
1
2
3
4
5
6
7
8
9
10
11
12
13 1function f(){
2 var getNumFuncs = [];//函数数组
3 for(var i=0;i<10;i++){
4 (function (j) {
5 getNumFuncs[j] = function(){return j;};
6 })(i);
7 }
8 return getNumFuncs;//设置断点,查看变量共享问题
9}
10var tmp = f();
11tmp[3]();
12
13
注意:上述的代码中的也是先执行循环,但是在执行循环的时候,程序将每一个i的值都传入到了创建的是个作用域的一个中,实现了i值的传入,i为实参,j为形参。
IIFE实际应用案例
案例一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 1var tabs = document.getElementsByClassName('tabs')[0].children;
2var contents = document.getElementsByClassName('show')[0];
3
4for(var i=0;i<tabs.length;i++) {
5 //(function (i) { //IIFE start
6 tabs[i].onclick=function(){
7 for (var j = 0; j < tabs.length; j++) {
8 tabs[j].className = '';
9 }
10 this.className = "active";
11 contents.innerHTML = "导航" + i + "内容";
12 };
13 //}(i)); //IIFE end
14}
15
16
tab的length为4,由于变量共享在同一个作用域下,所以变量 i 只有一个,并最终i为4,所以点击任何标签,都输出“点击了4”
避免闭包中非期望的变量共享问题,解决方式 IIFE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 1var tabs = document.getElementsByClassName('tabs')[0].children;
2var contents = document.getElementsByClassName('show')[0];
3
4for(var i=0;i<tabs.length;i++) {
5 (function (i) { //IIFE start
6 tabs[i].onclick=function(){
7 for (var j = 0; j < tabs.length; j++) {
8 tabs[j].className = '';
9 }
10 this.className = "active";
11 contents.innerHTML = "导航" + i + "内容";
12 };
13 }(i)); //IIFE end
14}
15
16
tab的length为4,立即执行了4次函数,有4个函数作用域,所以变量 i 生成了4次,所以点击时能正常输出1到4
案例二
通过立即执行表达式可以实现
1
2
3
4
5
6
7
8
9 1for (var i = 0; i < 5; i++) {
2 (function(j) { // j = i
3 setTimeout(function() {
4 console.log(new Date, j);
5 }, 1000*i);
6 })(i);
7}
8
9
JS闭包
闭包的概念
闭包(引入案例)
思考:函数内的局部变量,是否能在函数外得到(按照常理来说是没有办法进行访问的)
有什么方法能读写函数内部的局部变量?
闭包(closure)的概念
-
闭包是由函数和与其相关的引用环境组合而成的实体(函数和它所在的作用域的变量)
-
闭包是词法作用域中的函数和其相关变量的包裹体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 1function createInc(startValue){
2 return function(step){
3 startValue+=step;
4 return startValue;
5 }
6}
7var inc = createInc(5);
8console.log(inc(1));//输出多少?
9console.log(inc(2));//输出多少?
10inc = createInc(5);
11console.log(inc(1));//输出多少?
12
13# 输出结果
146
158
166
17
18
1
2
3
4
5
6
7
8
9
10
11
12
13
14 1function createInc(startValue){
2 return function(step){
3 startValue+=step;
4 return startValue;
5 }
6}
7var inc = createInc(5);
8console.log(inc(1));//输出多少? 6
9console.log(inc(2));//输出多少? 8
10var inc2 = createInc(5);
11console.log(inc(1));//输出多少? 9
12console.log(inc2(1));//输出多少? 6
13
14
根据上述代码,可以写出下列的图示:
若一个函数离开了它被创建时的作用域,它还是会与这个作用域的变量相关联
闭包是一个函数外加上该函数创建时所建立的作用域
1
2
3
4
5
6
7
8
9
10
11
12
13
14 1function foo() {
2 var i = 0;
3 function bar() {
4 console.log(++i);
5 }
6 return bar;
7}
8var a = foo();
9var b = foo();
10a();//输出多少? 1
11a();//输出多少? 2
12b();//输出多少? 1
13
14
- 函数bar和其相关词法上下文中的变量i,构成了一个闭包
- 返回的函数bar,依然能够访问到变量i(藕断丝连)
a和b对应的是否为同一个闭包? // a 和 b对应的不是同一个闭包
思考:foo和它相关作用域的变量是否形成闭包?//foo和它相关的作用域的变量也会形成一个闭包
闭包的常见形式
以函数对象形式返回
1
2
3
4
5
6
7
8
9
10
11
12
13 1var tmp = 100;// 注意:词法作用域,形成的闭包是否包含此行的tmp 不包含
2function foo(x){
3 var tmp = 3;// 思考:若屏蔽此行,则又会输出多少?
4 return function(y){
5 console.log(x + y + (++tmp));
6 }
7}
8var fee = foo(2);
9fee(10);// 16
10fee(10);// 17
11fee(10);// 18
12
13
上述代码中fee 形成了一个闭包,闭包包含一个函数对象以及tmp(其中tmp是3,不是100,tmp = 100是自由变量,全局变量)和x变量
-
思考:若屏蔽此行,则又会输出多少?
1
2
3
4
5 1113
2114
3115
4
5
-
思考:此实例中fee函数对象相关作用域的变量都有哪些?形成的闭包是否包含foo函数之外(即第一行)的自由变量tmp?foo中的tmp是否调用后就释放?
1
2
3 1答:包含tmp和x 不包含自由变量tmp
2
3
1
2
3
4
5
6
7
8
9
10
11
12
13
14 1function foo(x){
2 var tmp = 3;
3 return function(y){
4 x.count = x.count ? x.count+1 :1;
5 console.log(x + y + tmp,x.count);
6 }
7}
8var age = new Number(2);
9var bar = foo(age);
10bar(10);// 15 1
11bar(10);// 15 2
12bar(10);// 15 3
13
14
- 思考:此实例中bar函数对象相关作用域的变量都有哪些?(tmpx)foo中的tmp是否调用后就释放?(不会释放,会常驻内存中)
作为对象的方法返回
-
对于c来说的count和reset和n都是构成了两个闭包
-
对于d来说的count和reset和n都是构成了两个闭包
闭包的作用及常用场景
闭包的作用
-
可通过闭包来访问隐藏在函数作用域内的局部变量
-
使函数中的变量被保存在内存中不被释放(单例模式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1var n = 10;
2function f1(){
3 var n = 999;
4 nAdd = function(){n+=1;};
5 function f2(){
6 console.log(n);
7 }
8 return f2;
9}
10var result = f1();
11result();// 999
12nAdd();
13result();// 999
14
15
上述实例中,无法在f1函数外直接得到局部变量n的999的值,可以通过闭包间接的在f1函数外访问和修改n
注意:上述中nAdd不在result对应的闭包里面!
因此将代码进行修改可得:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 1var n = 10;
2function f1(){
3 var n = 999;
4 nAdd = function(){n+=1;};
5 function f2(){
6 console.log(n);
7 }
8 return f2;
9}
10var result = f1();
11var result1 = f1();
12result();// 999
13nAdd();
14result1();// 1000
15result();// 999
16
17
其中nAdd不在f1所对应的闭包里面,因此在对result1()进行调用的时候,则会对n的值进行改变,但是对于result()的调用结果不会发生改变。
闭包的实际应用案例
- 案例分析一:创建结点(单例模式)
比如说我现在的需求是这样的,在网页中有时候会需要遮罩层,调用的时候我就创建一个,但是你不可能每次调用创建,所以如果存在就用以前的,如果不存在就创建新的
1
2
3
4
5
6
7
8
9
10 1function fn(){
2 var a;
3 return function(){
4 return a || (a = document.body.appendChild(document.createElement('div')));
5 }
6};
7var f = fn();
8f();
9
10
定时与节点,闭包应用案例 2秒后执行,由于闭包所以objID此时还存在
虽然有时没有直接用,但闭包无时无刻不存在
1
2
3
4
5
6
7
8 1function closureExample(objID,text,timedelay){
2 setTimeout(function(){
3 console.log(objID,text);
4 },timedelay);
5}
6closureExample("mydiv","Closure is Create",2000);
7
8
闭包的注意事项
-
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包
-
使用闭包时要注意不经意的变量共享问题,可以通过立即执行表达式来解决