1. 解析与执行
一般js代码执行需要先经过解析,生成计算机能解析的语言再执行。
1.1解析
var a = 100;
- 词法分析:分析token,把语句解析成记号,如 var , a , = , 100 , ;
- 语法/解析分析:将token数组 根据一定的语法规则 转化成AST(抽象语法树)
- 代码生成:生成浏览器js引擎能解析的底层代码。
1.2执行
在js引擎中执行代码分为:分析阶段和执行阶段
1.2.1分析阶段
根据分析创建对应的信息
-
VO(Variable Object)对象:该对象保存了当前执行上下文中的变量和函数地址(也就是当前作用域)。 -
作用域链:VO(当前作用域) + ParentScope(父级作用域) -
this的指向: 视情况而定。注意:变量作用域通过词法作用域确定,也就是定义的时候已经确定一些变量的值。
1.2.2执行阶段
根据对应的作用域访问变量,同时创建执行的上下文。
- 引擎询问作用域,作用域中是否有这个叫X的变量
- 如果作用域有X变量,引擎会使用这个变量
- 如果作用域中没有,引擎会继续寻找(向上层作用域),如果到了最后都没有找到这个变量,引擎会抛出错误。
执行阶段的核心就是找,使用LHS查询与RHS查询。
var a = 2; // LHS 查询
LHS (Left-hand Side) : (=)的左侧,变量赋值或写入内存。想象为将文本文件保存到硬盘中。
RHS (Right-hand Side) : (=)的右侧,变量查找或从内存中读取。想象为从硬盘打开文本文件。
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
直接看引擎在作用域找这个过程: LSH(写入内存):
c=, a=2(隐式变量分配), b=
RHS(读取内存) :
读foo(2), = a, a ,b
(return a + b 时需要查找a和b)
2.作用域
2.1定义
- 程序中定义变量的区域,它决定了当前执行代码对变量的访问权限。限制谁能否访问该作用域的变量。
- 也可以理解为:作用域就是对象,对应的变量就是对象属性。
作用:
防止不同范围的变量互相干扰。
2.2分类
2.2.1 全局作用域:
- 程序的最外层作用域
- 不在任何函数内容
- 会随容器一直存在。浏览器上就是window对象,所有的变量和函数都在window上创建。
全局变量 : 保存在全局的变量
优点
- 可以反复使用,访问方便
缺点
- 会造成全局污染
- 占用空间,页面不关闭,不被释放
2.2.2 函数/局部作用域:
只在函数被定义时才会创建,不同函数之间互相独立。函数结束就销毁。
//全局作用域
var a = 10
function myFun1() {
//这里是函数myFun1作用域,当方法调用的时候会动态创建,是独立的作用域
var b = 20
console.log(b) //访问函数myFun1作用域的 b 这里是20
var b2 = 200
}
function myFun2() {
//这里是函数myFun2作用域
var b = 30 //这里与myFun1中的b 不冲突,由于互相独立,所以可以同时定义
console.log(b) //访问函数myFun2作用域的 b,成功, 这里是30
console.log(b2) //访问函数myFun1作用域的 b2,失败, 函数的作用域互相独立。
}
console.log(a) //访问全局作用域的 a 成功
局部变量 : 保存在函数作用域下的变量,包括传入的形参
function myfun(a,b){
var c = 10;
}
//这里的 内部参数 c 和 形参 a,b 都是局部变量
优点:
- 数据隔离,不会被污染
缺点:
- 无法反复使用
注意
只有函数的 {} 包裹的区域才是局部作用域,其他都不是 (箭头函数也没有作用域和this概念)
var obj = {
a:"1",
b:"2"
}
这里 {} 就不是一个作用域, a也不是作用域变量
2.3 es6的块级作用域
js的块级: 花括号内 {...} 内的代码片段。如 if,for,switch,while的语句
js实际是没有块级作用域,会导致变量的内容超出{}的范围,影响到外部。
console.log(a)
if(false) { //即使条件为false ,内容的定义的 var a 依然能影响到外部,做声明提前。
var a = 10
}
console.log(a)
//这里正常打印
undefined
10
在ES6引进了 let和const声明 实现块级作用域,但是底层还是匿名函数自调
for (let i =0; i<10;i++){
console.log(i)
}
//等价于
for (let i =0; i<10;i++){
function(i) {
console.log(i)
}(i); // 定义 匿名函数+执行
}
2.3.1 不会变量提升
console.log(bar);//抛出`ReferenceError`异常: 某变量 `is not defined`,这里不存在变量提升
let bar=2;
for (let i =0; i<10;i++){
console.log(i)
}
console.log(i);//抛出`ReferenceError`异常: 某变量 `is not defined` 这里访问不了块作用域的i
2.3.2 不能重复声明
// var
function test(){
var name = 'aaaa';
var name = 'bbbb';
console.log(name); // bbbb
}
// let || const
function test2(){
var name ='aaaa';
let name= 'bbbb';
// Uncaught SyntaxError: Identifier 'count' has already been declared
}
2.3作用域链(scope chain)
函数作用域优先访问自己内部的变量,没有再往上继续查找,由此形成了作用域链。
注意只有往上查找,没有往下的逻辑
访问的这个变量也叫:自由变量
//全局
var a = 10
function myFun1() {
var b = 20
}
function myFun2() {
var c = 30
console.log(a) //访问全局作用域的 a 成功 通过作用域链一直往上找,自由变量a
console.log(b) //访问函数作用域的 b 报错
}
console.log(a) //访问全局作用域的 a 成功
console.log(b) //访问函数myFun1作用域的 b 报错,不能父级往子访问。
console.log(c) //访问函数myFun2作用域的 c 报错,不能父级往子访问。
嵌套的列子
//这里是全局作用域
var a = 10
function myFun1(p1) {
//这里是myFun1函数作用域
var b = 20
function myFun2(p2) {
//这里是myFun2函数作用域
console.log(a) //访问全局作用域的 a 成功 通过作用域链一直往上找
console.log(b) //访问函数myFun1作用域的 b 成功
}
myFun2()
}
myFun1() //
嵌套的关系
全局作用域 -> myFun1作用域 -> myFun2作用域
-
全局作用域 :标识符包含 a 和 myFun1
-
myFun1作用域 :标识符包含 p1 , b , myFun2
-
myFun2作用域 :标识符包含 p2
从未声明变量赋值 会提升到全局,不会报错
function a() {
function b(){
age = 10; //这里会被提升到全局 ,不会报错
}
b()
}
a()
console.log(age) //这里输出10
如果是访问,则还是会报错
function a() {
function b(){
console.log(age); //age is not defined
}
b()
}
a()
2.4词法作用域
定义:函数被定义的时候,它的作用域就已经确定了,跟执行没有关系,也可以叫 “静态作用域”。
JS使用的是词法作用域
var a = 1;
function fun1() {
console.log(a);
}
function fun2() {
var a = 2;
fun1();
}
fun2();
// 结果是 1 访问的是全局的变量 1
这里的fun1在定义的时候,已经根据定义时的关系,把a分析出来,自由变量a取得是全局的a = 1,已经预设好了,所以在执行的过程中即使在fun2里面执行了 var a = 2, 也不影响
例子
1.全局和参数命名一样
var a = 10
function myfun(a){
a++ //这里从作用域链查找,优先找到的是参数a,而不是全局的a
console.log(a) //这样也是就近原则
}
myfun(a)//输出11
console.log(a)//输出10
js里面 参数是都是拷贝一份,因为a是基本数据类型,赋值多一份10在方法里操作,方法结束后,方法参数a也会销毁
作用域是对象
var age = 10;
function fun() {
var age = 100;
age++;
console.log(age);
}
fun();
console.log(age);
- 代码执行前 scope只包含全局Global(Window) ,里面有一个age全局变量
- 当代码执行到 函数内的 console.log(age);会看到scope 一个数组
里面有两个scope
作用域链式对象的集合数组 scope = [Local,Global]
- Local 函数作用域
- Global(Window) 全局作用域
各自存储了自己的作用域
window里包含fun的 等价于 Local
当函数fun调用结束后,作用域链变回scope只包含全局Global(Window)
2.7 函数执行的三个步骤
- 备料:创建作用域对象+局部变量
- 执行代码
- 垃圾回收
函数的作用域链
一个是自己,另外一个是外部
2.6其他知识
2.5.1声明提前 hoisting
在任意代码执行前处理的,会把未声明的变量做提升处理,放置到当前作用域的最前面。
var命令会发生“声明提前”现象,即变量可以在声明之前使用,值为undefined。let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
2.5.1.1 未声明
var tmp = new Date();
function f() {
console.log(tmp);
if(false) {
var tmp='hello';
}
}
f()//输出undefined
结果为undefined
因为js是词法作用域,所以在静态解析时候会忽略false判断,然后把var tmp 提升到console.log(tmp);前声明 var tmp;
2.5.1.2 重复声明-变量
当提升的变量已经存在声明,提升的动作会被忽略,只做赋值操作。即按顺序至上而下,第一个声明了后面都忽略。
var name = 'aaa';
console.log(name); // aaa
if(true){
var name = 'bbb';
console.log(name); // bbb
}
console.log(name); // bbb
2.5.1.3 重复声明-变量+函数
当出现变量和函数命名一样的情况,声明函数最优先提升(并且包含声明的内容)。函数表达式则跟变量效果一样。
console.log(name)
var name = "aaa"
function name() { //声明函数方式
console.log("bbb")
}
//输出name
//undefined
console.log(name)
var name = "aaa"
var name = function() { //函数表达式
console.log("bbb")
}
//输出函数
//ƒ name() {
// console.log("bbb")
//}
2.5.2作用域冲突
全局与局部重复变量名冲突
解决办法
使用立即执行函数IIFE(Immediately Invoked Function Expression)
其实所有的es6的块作用域最终还是通过IIFE实现,并没真正意义上的块级作用域,只是通过语法糖模拟。
// module1.js
(function () {
var a = 1;
console.log(a);
})();
// module2.js
(function () {
var a = 2;
console.log(a);
})();
2.5.3 创建作用域的方式
- 函数
function foo () {
}
2.使用 let 和 const
for (let i = 0; i < 5; i++) {
console.log(i);
}
console.log(i); // ReferenceError
try catch创建作用域(不推荐)
try {
undefined(); // 强制产生异常
}
catch (err) {
console.log( err ); // TypeError: undefined is not a function
}
console.log( err ); // ReferenceError: `err` not found
- 使用
eval
function foo(str, a) {
eval( str );
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1 3
- 使用
with
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2
例子
阿里 作用域-面试题
function fn(a, c) {
console.log(a) // function a
var a = 123
console.log(a) // 123
console.log(c) // function c
function a() {}
if (false) {
var d = 678 //这里会被申请提前
}
console.log(d) // undefined
console.log(b) // undefined
var b = function () {} //这是表达式 ,不算是函数声明,所以不在ao创建逻辑里
console.log(b) // function b
function c() {}
console.log(c) // function c
}
fn(1, 2)
输出结果
[Function: a]
123
[Function: c]
undefined
undefined
[Function: b]
[Function: c]
AO创建与初始化的过程
- 创建ao对象
- 找形参和变量声明 并当做 AO对象的属性名 值为undefined
- 实参和形参相统一
- 在函数体里面找函数声明 值赋予函数体
// 第一步 创建
ao : {
}
// 第二步 确定属性
ao : {
a: undefined
c: undefined
d: undefined
b: undefined
}
// 第三步 实参赋值
ao {
a: 1
c: 2
d: undefined
b: undefined
}
// 第四步 函数声明赋值
ao {
a: 1
c: function c() {}
d: undefined
b: function c() {}
}
3.执行上下文
3.1定义
执行上下文(Execution context)
遇到函数执行的时候,就会创建一个执行上下文。执行上下文是当前 js 代码被解析和执行时所在环境的抽象概念。
3.2分类
- 全局执行上下文:这是默认的上下文,任何不在函数内部的代码都在全局上下文中。由浏览器主动调用根函数,包含全局的window对象和this指向这个window ,一个程序中只会有一个全局执行上下文。
- 函数执行上下文:在函数执行的时候,都会动态创建的执行上下文,跟函数作用域类似。
- Eval 函数执行上下文: 执行在
eval函数内部的代码也会有它属于自己的执行上下文。
3.3执行3个阶段
3.3.1创建阶段
ExecutionContext = { // 执行上下文
ThisBinding = <this value>, // this绑定
LexicalEnvironment = { ... }, // 词法环境
VariableEnvironment = { ... }, // 变量环境
}
- this绑定:在全局上下文,this指向window,函数调用this指向调用的对象,否则会指到全局或undefined
- 词法环境
- 回收阶段
3.3.2执行阶段
执行变量赋值、代码执行。
3.3.3回收阶段
执行上下文出栈等待虚拟机回收执行上下文,在执行阶段
3.4执行上下文栈
3.4.1定义
执行上下文栈 (Execution context stack),调用栈,执行栈。 用来维护执行代码时候创建的上下文,是栈的数据结构,后进先出。
3.4.2执行流程
- 在执行script代码时,由浏览器主动调用根函数G,创建默认全局执行上下文G,并然后押入栈顶。
- 根函数里,调用方法a,创建的函数执行上下文a,并把a押入栈顶,在G的上面。
- a函数里,调用方法b,创建的函数执行上下文b,并把b押入栈顶,在a的上面。
- 方法a调用完毕,同时执行上下文a也出栈。
- 方法b调用完毕,同时执行上下文b也出栈。
- 根函数G也执行完毕,全局上下文G也出栈。
//根路径默认会创建一个全局的上下执行栈
function a (){
b() //函数调用时,创建b的上下文
}
function b (){
}
a() //函数调用时,创建a的上下文
3.5词法环境
3.5.1定义
相应代码块内标识符与变量值、函数值之间的关联关系的一种体现
3.5.2环境类型
- 全局环境
- 函数环境
3.5.3环境的两个组件
3.5.3.1 环境记录器:
是存储变量和函数声明的实际位置。不同的环境对应不同的结构
- 全局环境 使用
Type: "Object"对象环境记录器,描述 变量和函数的关系。 - 函数环境 使用
Type: "Declarative"声明式环境记录器,描述 存储变量、函数和参数。
//伪代码
// GlobalExectionContext // 全局执行上下文
EnvironmentRecord: { // 环境记录器:存储变量和函数声明的实际位置
Type: "Object",
// 在这里绑定标识符
}
//FunctionExectionContext // 函数执行上下文
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
}
3.5.3.2. 外部环境的引用:
用于关联外面的环境,形成嵌套关系
//伪代码
GlobalExectionContext = { // 全局执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录器:存储变量和函数声明的实际位置
Type: "Object",
// 在这里绑定标识符
}
outer: <null> // 对外部环境的引用:可以访问其父级词法环境
}
}
FunctionExectionContext = { // 函数执行上下文
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
}
outer: <Global or outer function environment reference>
}
}
3.6变量环境
跟词法环境结构一致,区别在于
- 词法环境 记录的是
let和const绑定 - 变量环境 记录的是
var的绑定 在es5之前只有一个词法环境处理var ,到了es6为了区分let 和const ,多出一个变量环境。把原来词法环境调整为 let 和const ,变量环境处理var。
- 词法环境:处理let 和 const
- 是变量环境:处理var
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
对应的 环境伪代码是
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: { // 词法环境
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
a: < uninitialized >, // let、const声明的变量
b: < uninitialized >, // let、const声明的变量
multiply: < func > // 函数声明
}
outer: <null>
},
VariableEnvironment: { // 变量环境
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
c: undefined, // var声明的变量
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: { // 词法环境
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
Arguments: {0: 20, 1: 30, length: 2}, // arguments对象
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: { // 变量环境
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
g: undefined // var声明的变量
},
outer: <GlobalLexicalEnvironment>
}
}
- 可以看出 在词法环境中的 a: < uninitialized >, 被定义为未初始化。
- 而在变量环境 g: undefined 被声明并赋值为undefined。
4.执行上下文与作用域的区别
-
作用域是编译阶段就已经确定的,因为使用的是词法作用域,不受运行时影响。
-
但是执行上下文,是代码执行阶段动态分配的。调用了函数就创建,没有则重来没发生,而且执行过程可被修改。
注 在ES5当中把`variable object`改为了`Lexical Environments`。