目录
1、什么是预编译
JavaScript是解释性语言,解释性是指逐行解析、逐行执行。
在JavaScript真正被解析之前,JS解析引擎会把整个文件进行预处理,以消除一些歧义。这个预处理的过程就称为预编译。
2、全局对象(GO对象)、全局变量、全局函数
什么是全局对象?
在浏览器环境中,JS引擎整合所有 script 标签中的内容,产生了统一的 Window 对象。这个对象就是全局对象(GO对象)。
全局对象的作用?
在 JavaScript 中,全局变量是挂载在 window 对象下的变量,所以在网页中的任何位置都可以使用并且访问到这个全局变量。
举例:
globalName 变量在任何地方都是可以被访问到的,所以它就是全局变量。而在 getName 函数中作为局部变量的 name 变量是不具备这种能力的。
var globalName = 'global'; function getName() { console.log(globalName) // global var name = 'inner' console.log(name) // inner } getName(); console.log(name); console.log(globalName); //global
注意:
在 JavaScript 中,所有没有经过定义而直接被赋值的变量默认就是一个全局变量,比如下面代码中 setName 函数里面的 vName:
function setName(){
vName = 'haha';
}
setName();
console.log(vName); // haha
console.log(window.vName) // haha
缺点:
当定义很多全局变量时,可能会引起变量命名的冲突,所以在定义变量时应该注意作用域的问题。
3、活动对象(AO对象)、局部变量、局部函数
什么是局部对象/活动对象?
活动对象:也就激活对象(AO对象)。在函数被调用时产生,用来保存当前函数内部的执行环境,也叫执行期上下文。在函数调用结束时销毁。
在函数内部声明的变量,叫局部变量。
局部变量作为 AO 对象的属性存在。局部变量只在函数作用域内有效。
在函数内部声明的函数,叫局部函数。
局部函数作为 AO 对象的方法存在。局部函数只在函数作用域内有效。
function a() {
var b = 1
console.log(b) // 1
}
a()
console.log(b) // b is not defined
这里的b是作为活动对象中的属性存在,也就是说 b 是局部变量,只在函数 a 中有效。
除了这个函数内部,其他地方都是不能访问到它的。同时,当这个函数被执行完之后,这个局部变量也相应会被销毁。所以会看到在 a 函数外面的 b 是访问不到的。
4、JS预解析的过程
① 浏览器什么时候进入作用域?
- 当看到script标签的时候
- 调用方法的时候
② 进入作用域之后,进行什么操作?
- 首先进行 js 的预解析:搜寻预解析关键字,执行预解析。
接着对 js 逐行执行:找有没有表达式,如果有的话就把js预解析里面的内容进行修改。
③ js预解析的过程:
1)全局预编译
查找变量声明(通过关键字var声明的变量),作为GO对象的属性名,值为undefined;
var a // 变量声明
var a = 111 // 变量声明+变量赋值
查找函数声明(通过关键字function声明的函数),作为GO对象的属性名,值为function;
function a() {} // 函数声明
var a = function() {} // 函数表达式,不是函数声明
例子解析:
2)局部/函数预编译
在函数被调用时,为当前函数产生AO对象;
查找形参和变量声明作为AO对象的属性名,值为undefined;
使用实参的值改变形参的值;
查找函数声明,作为AO对象的属性名,值为function;
例子1:
例子2:
答案: function 0 undefined
3)局部变量和形参同名,哪个优先级更高?
只要声明了局部函数,函数的优先级最高;
没有声明局部函数,实参的优先级高;
整体来说,局部函数 > 实参 > 形参
function fn(a) {
var a;
console.log(a) // 1
}
fn(1)
4)预解析过程总结:
首先,JS引擎会整合所有script标签中的内容,然后生成全局对象Window对象;
其次,寻找作用域中的变量声明,赋值为undefined,还有函数声明(var 和 function)(匿名函数没有function声明,所以不会提升),赋值为function;
然后,将其声明提升,也就是提升到作用域的最顶端;
赋值操作依然留在原地不会提升,再从上到下执行;
先是全局预编译,然后是函数预编译。这就是预解析的过程。
局部函数的优先级高于实参,实参高于形参
④ 重复声明 var 变量如何编译?
var 关键词对同一个标识符重复使用时,除了第一次有效外,其他均做忽略处理;
⑤ 变量提升和函数提升的优先级:
函数提升优先级高于变量提升;预解析时,先处理变量声明,再处理函数声明;
5、关于JS预解析的题目
① 题目一
var foo;
function foo(){}
console.log(foo);
预解析过程:
var foo; // 提升变量声明的var,赋值为undefined
var foo; // 提升函数声明抽出的var
foo=function (){}; // 函数声明抽出的赋值
console.log(foo); //function foo(){}
② 题目二
function foo(){}
var foo;
console.log(foo);
预解析过程:
var foo; // 提升变量声明的var,赋值为undefined
var foo; // 提升函数声明抽出的var
foo=function (){}; // 函数声明抽出的赋值
console.log(foo); //function foo(){}
③ 题目三
var a=1;
function a(){}
console.log(a);
预解析过程:
var a; // 提升变量声明的var,赋值为undefined
var a; // 提升函数声明抽出的var
a=function (){}; // 函数声明抽出的赋值a=1;
console.log(a); //1
④ 题目四
var a = 10;
function f1() {
var b = 2 * a;
var a = 20;
var c = a + 1;
console.log(b)
console.log(c)
}
f1()
答案:NaN 21
6、JS作用域和作用域链
① 作用域
在JavaScript中,作用域分为全局作用域和局部作用域。
全局作用域:在<script>标签产生的区域,从计算机的角度可以理解为Window对象;
局部作用域:由函数产生的区域,从计算机的角度可以理解为函数的AO对象;
拓展:在ES6之前,只有函数能产生作用域,ES6中有了let之后,就有了块级作用域。
② 作用域链
在JavaScript中,函数存在一个隐式属性 [[ scopes ]],这个属性用来保存当前函数在执行时的环境(上下文),由于在数据结构上是链式的,也称为作用域链。我们可以把它理解为一个数组。
function fn() {}
console.dir(fn) // 打印内部结构
内部结构如下:
scopes 属性在函数声明时产生,在函数被调用时更新,记录当前函数的执行环境。
在函数被调用时,将该函数的AO对象压入到[[ scopes ]]中
梳理作用域链:
function a() {
console.dir(a)
function b() {
console.dir(b)
function c() {
console.dir(c)
}
c()
}
b()
}
a()
调试和打印结果如下:
③ 作用域链的作用
在访问变量或者函数的时候,会在作用域链上依次查找。先查找自己的AO是否存在变量和函数,不存在的话,再一层一层向上查找,直到找到GO;
外层的函数无法访问内部函数的变量;而内部函数可以访问外部函数的变量。
第一个例子的aa打印的是 111
第二个例子的aa打印的是 222
7、关于JS作用域的案例和解析
① 案例1
function test(num1, num2) {
console.log(num1)
console.log(num2)
var num2 = 234
console.log(num2)
num1 = 123
console.log(num1)
function num1() {}
var num1
num2 = 125
var num2 = function() {}
console.log(num1)
console.log(num2)
}
test(1)
解析:
// 执行函数fn前预编译:
// num2: undefined,
// num1: function num1() {}
function test(num1, num2) {
console.log(num1) // function num1(){}
console.log(num2) // undefined
var num2 = 234
console.log(num2) // 234
num1 = 123
console.log(num1) // 123
function num1() {}
var num1
num2 = 125
var num2 = function() {}
console.log(num1) // 123
console.log(num2) // f(){}
}
test(1)
② 案例2
function test(a, b) {
console.log(a)
a = 3
console.log(b)
b = 2
console.log(b)
function b() {}
function d() {}
console.log(b)
console.log(d)
}
test(1)
解析:
// 预编译:GO -> {test: functiong () {}}
// 解释执行:{test()}
// 执行函数test前预编译:
// a: 1[因为外部实参传入]
// b: function () {}
// d: function () {}
function test (a, b) {
console.log(a); // 控制台显示1
a = 3;
console.log(b); // 控制台显示function b () {}
b = 2;
// 解释执行函数test:{ b: 2 }
console.log(b); // 控制台显示2
function b () {};
function d () {};
console.log(b); // 控制台显示2,因为b已是变量并被赋值
console.log(d); // 解释执行函数test:{d: function () {}}
}
test(1);
③ 案例3
var glob = 100
function test() {
console.log(glob)
glob = 200
function glob() {}
console.log(glob)
var glob
console.log(glob)
}
test()
解析:
// 预编译:
// glob: undefined
// test: function () {}
var glob = 100; // 解释执行:glob = 100; test()
function test () {
// 执行函数test前开始预编译
// glob = function () {}
console.log(glob); // 控制台显示function glob () {};
glob = 200
function glob() {};
// 解释执行函数test:{glob = 200}
console.log(glob); // 控制台显示200
var glob;
console.log(glob); // 原理同4,控制台显示200
}
test();
④ 案例4
function bar() {
return foo
foo = 10
function foo() {}
var foo = 11
}
console.log(bar())
解析:
// 预编译:GO -> {bar: function () {}}
// 解释执行:{bar()}
// 执行函数bar()前预编译:
// foo: function () {}
function bar () {
return foo; // 解释执行函数bar():{foo()}
foo = 10;
function foo () {}
var foo = 11;
}
console.log(bar());// 控制台显示function foo () {}
⑤ 案例5
function Bar() {
foo = 10
function foo() {}
var foo = 11
return foo
}
console.log(Bar())
解析:
// 预编译:GO -> bar: function () {}
// 解释执行:{bar()}
// 执行函数bar()前预编译:
// foo: function () {}
function Bar () {
foo = 10; // 解释执行函数bar():{foo: 10}
function foo () {}
var foo = 11; // 解释执行函数bar():{foo: 11}
return foo;
}
console.log(Bar()); //控制台显示11
⑥ 案例6
function fn (a) {
console.log(a);
var a = 123;
console.log(a);
function a () {};
console.log(a);
console.log(b);
var b = function () {};
console.log(b);
console.log(d);
function d () {};
}
fn(1);
解析:
// 预编译:GO -> {fn: function () {}}
// 解释执行:{fn()}
// 执行函数fn前预编译:
// a: function () {},
// b: undefined[因为函数b()为函数表达式]
// d: function () {}
function fn (a) {
console.log(a); // 控制台显示function a () {}
var a = 123;
console.log(a); // 控制台显示123
function a () {};
console.log(a); // 控制台显示123
console.log(b); // 控制台显示undefined
var b = function () {};
console.log(b); // 控制台显示function () {}
console.log(d); // 控制台显示function d () {}
function d () {};
}
fn(1);