代码的执行过程
01_全局代码执行过程
var name = "ytl"
// 遍历声明被提升了,但是还没赋值,所以打印出来的是undefined
console.log(num1) // undefined
var num1 = 20
var num2 = 30
var result = num1 + num2
console.log(num1) // 20
console.log(result) // 50
/*
1. 代码被解析(编译),v8引擎内部会帮助我们在堆内存中创建一个对象(GlobalObject --> GO)
1.1 该对象所有的作用域(scope)都可以访问
1.2 里面包含Date、Array、String、Number、setTimeout等等
1.3 其中还有一个window属性指向自己
变量都先声明加入GlobalObject中,但是没有值
2. 运行代码
2.1 v8为了执行代码,v8引擎内部会有一个执行上下文栈(Execution Context Stack)(函数调用栈)
2.1 因为我们执行的是全局代码,为了全局代码能够正常的执行,需要创建全局执行上下文(Global Execution Context)(全局代码需要被执行时才会创建)
*/
var globalObject = {
String: "类",
Date: "类",
setTimeout: "函数",
window: globalObject,
name: undefined,
num1: undefined,
num2: undefined,
result: undefined
}
02_全局代码执行过程(函数)
var name = "coder"
foo(123) // 控制台也会打印foo
function foo(num) { // AO对象中num被赋值123
console.log(m) // 执行该函代码,m能被找到,但是值为undefined
var m = 10 // 执行该行代码,AO中 m被赋值为 10
var n = 20 // 执行该行代码,AO中 n被赋值为 20
// console.log('foo') // 控制台输出foo
// 当我们查找一个变量时,真实的查找路径是沿着作用域链来查找的
console.log(name) // 控制台输出coder
}
foo(321) // 再执行一次,又会重新创建FEC
// 代码被编译过程,先在Global Object中声明,当遇到声明的是一个函数时,会在内存中开个的单独的空间来存储该函数(foo),所以会有一个内存地址比如:0xa00
/* 该存储空间主要存有:
1. 该函数的父级作用域 -> [[scope]]: parent scope
2. 函数的执行体(代码块)
*/
var globalObject = {
name: undefined,
foo: 0xa00,
window: globalObject
}
//foo() // 控制台会打印foo
/*
在代买运行过程,执行函数时,会把函数放入调用栈里的 Functional Execution Context(FEC)(函数执行上下文),
在FEC里也有一个VO(variable object),还包含有 scope chain(作用域链): VO + ParentScope (AO + GO) ,
foo函数的父级作用域就是全局作用域
这个对象会在之后被销毁,它对应的是AO(Activation object)
在函数真正被执行之前,就会创建该AO对象,将该函数的参数、遍历等都提升到AO当中去
(函数注释为函数执行过程)
当该函数被执行完成后,FEC就会被移出栈,FEC被销毁销毁掉,而AO没有东西指向它,AO也会自动销毁
*/
var activationObject = {
num: 123,
m: undefined,
n: undefined
}
03_全局代码执行过程(函数嵌套)
var name = "ytl"
foo(123)
function foo(num) {
console.log(m)
var m = 10
var n = 20
/*
编译bar函数时,也会有一块存储空间0xb00来存储该函数相关内容
并且在调用栈里会再添加一个函数执行上下文,同时也有属于自己(bar)的AO对象
*/
function bar() {
// 当在自己的作用域里找不到,会一层一层往上级作用域查找
console.log(name) // why 有个注意点:window当中自带就有name属性,当没有声明name变量的时候,打印也不会报错
}
bar()
}
04_函数调用函数执行过程
var message = "Hello Global"
// foo 的父级作用域是全局作用域,和它在哪被调用没关系
function foo() {
console.log(message) // Hello Global
}
function bar() {
var message = "Hello bar"
foo()
}
bar()
变量环境和记录
-
其实上面的讲解都是基于早期ECMA的版本规范:
Every execution context has associated with it a variable. Variables and funtions declared in the source text are added as properties of the variable object. For funtion code, parameters are added as properties of the variable object.
每一个执行上下文会被关联到一个变量对象(variable object,VO),在源代码中的变量和函数声明会被作为属性添加到VO中。对于函数来说,参数也会被添加到VO中。
-
在最新的ECMA的版本规范中,对于一些词汇进行了修改:
Every execution context has an associated VariableEnvironment. Variable and funtions declared in ECMAScript code evaluated in an execution context are added as bindings in that VariableEnvironment’s Environment Record. For function code, parameters are also added as bindings to that Environment Record.
每一个执行上下文会关联到一个变量环境(VariableEnvironment)中,在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。
对于函数来说,参数也会被作为环境记录添加到变量环境中。
-
面试时可以聊:ES5之前的在对代码进行编译的时候会将我们定义的变量和函数添加到VO对象里面。但是在最新的ECMA规范里,现在不称为VO/GO/AO了,而是叫做变量环境(VE),而变量环境中每添加一个东西叫做环境记录。
作用域提升面试题
作用域面试题一
var n = 100;
function foo() {
n = 200;
}
console.log(n); // 输出为200
作用域面试题二
function foo() {
console.log(n); // undefined
var n = 200;
console.log(n); // 200
}
var n = 100;
foo();
因为在函数的作用域里存在 n,所以函数内使用的 n 都是函数内部定义的 n
作用域面试题三
var n = 100; // 1. n被赋值为100
function foo1() {
console.log(n); // 6. 因为AO(foo1)里没有n,向父级作用域查找,即GO里的n:100
}
function foo2() {
var n = 200; // 3. AO(foo2)里的n被赋值为200
console.log(n); // 4. 打印AO(foo2)里的n:200
foo1(); // 5. 执行foo1函数
}
foo2(); // 2. 执行foo2函数
console.log(n); // 7. 打印GO里的n:100
作用域面试题四
var a = 100;
function foo() {
// 这里打印的还是函数自己的AO对象里的a,而执行过程,a还没赋值就打印,所以是undefined
console.log(a); // undefined
return;
var a = 200;
}
foo();
作用域面试题五
function foo() {
var a = (b = 10);
// => 转成下面的两行代买
// var a = 10
// b = 10
}
foo();
// 这种情况下面有补充说明
console.log(a); // 报错: a is not defined
console.log(b); // 10
作用域面试题补充
function foo() {
var m = 100;
}
foo();
console.log(m); // 报错:m is no defined
/*
因为m是函数内部定义的,不是全局变量,在函数执行完后,函数执行上下文就会被销毁,所以m是找不到的
*/
function foo() {
m = 100;
}
foo();
console.log(m); // 100
/*
正常情况下,m没被定义就赋值,语法上来说就是错误的
而在js引擎编译的时候,这种情况就把 m 当作全局变量来定义
*/
认识内存空间
- 不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的,不同的是某些编程语言需要我们自己手动的管理内存,某些编程语言会自动帮助我们管理内存:
- 不管以什么样的方式来管理内存,内存的管理都会有如下的生命周期:
- 第一步:分配申请你需要的内存(申请);
- 第二步:使用分配的内存(存放一些东西,比如对象等);
- 不需要使用时,对其进行释放;
- 不同的编程语言对于第一步和第三步会有不同的实现:
- 手动管理内存:比如 C、C++,包括早期的 OC,都是需要手动来管理内存的申请和释放的(malloc 和 free 函数);
- 自动管理内存:比如 Java、JavaScript、Python、Swift、Dart 等,它们有自动帮助我们管理内存;
- 我们可以知道 JavaScript 通常情况下是不需要手动管理的。
JS的内存管理
- JavaScript会在定义变量时为我们分配内存。
- 但是内存分配方式是不一样的:
- JS对于基本数据类型内存的分配会在执行时直接在栈空间进行分配;
- JS对于复杂数据类型内存的分配会在堆内存中开辟一块空间,并且将这块空间的指针返回值给变量引用;
JS的垃圾回收
- 因为内存的大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间。
- 在手动管理内存的语言中,我们需要通过一些方式自己来释放不再需要的内存,比如free函数:
- 但是这种管理的方式其实非常的低效,影响我们编写逻辑的代码的效率;
- 并且这种方式对开发者的要求也很高,并且一不小心就会产生内存泄漏;
- 所以大部分现代的编程语言都是有自己的垃圾回收机制:
- 垃圾回收**(Garbage Collection),简称GC**;
- 对于那些不再使用的对象,我们都称之为是垃圾,它需要被回收,以释放更多的内存空间;
- 而我们的语言运行环境,比如Java的运行环境JVM,JavaScript的运行环境js引擎都会内存垃圾回收器;
- 垃圾回收器也简称GC,所以在很多地方看到GC时其实指的是垃圾回收器;
- 但是这里就出现另外一个很关键的问题:GC怎么知道哪些对象不再使用的呢?
- 这里就要用到GC的算法了
常见的GC算法
引用计数
- 当一个对象有一个引用指向它时,那么这个对象的引用就+1,当一个对象的引用为0时,这个对象就可以被销毁;
- 这个算法有一个很大的弊端就是会产生循环引用,无法被销毁,造成内存泄漏;
标记清除
-
这个算法是设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于那些没有引用到的对象,就认为是不可用的对象;
-
这个算法可以很好的解决循环引用的问题;
-
JS引擎比较广泛的采用的就是标记清除发,当然类似于V8引擎为了进行更好的优化,它在算法的实现细节上也会结合一些其他的算法。
©coderwhy
coderwhy大神的js高级课程