执行流程
先编译,再执行。
- 编译阶段:进行变量提升,变量与函数会被存放到变量环境中,变量的默认值被设为 undefined.若存在两个相同的函数,最终存放在变量环境中的是后面那个。如果函数带有参数,编译过程中,参数会通过参数列表保存在变量环境中。
- 执行阶段:JS 引擎会从变量环境中去查找自定义的变量和函数。
哪些情况下代码在执行之前会编译并创建执行上下文?
- 当 JS 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面生存周期内,全局执行上下文只有一份。
- 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
- 当使用 eval 的时候,eval 的代码也会被编译,并创建执行上下文。
调用栈
- 栈溢出是如何产生的?
当调用一个函数时,会给他创建一个执行上下文 push 到栈中,执行完毕从栈中 pop。若函数内部又调用了其他函数,内部又调用其他函数…,不断将执行上下文往栈中 push 却没有 pop,超过一定数量就会导致栈溢出报错。
没有终止条件的递归会一直创建新函数的执行上下文压入栈中,超过栈容量的最大先之后就会报错;
可以通过把递归改造成其他形式、加入定时器拆分任务等方法来解决。
调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能追踪到哪个函数正在被执行和各个函数间的调用关系。
如何用好调用栈?
- 利用浏览器查看调用栈信息
- 加入 console.trace() 输出当前函数调用关系
JS 中 let,const,{} 如何实现块级作用域?
ES6 之前的作用域
- 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
- 函数作用域就是在函数内部定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量被销毁。
变量提升带来的问题
- 变量容易被覆盖
chat* myname = "geek time";
void showName() {
printf("%s \n", myname); // 'geek time'
if(0){
chat* myname = "Hei ha";
}
}
int main(){
showName();
return 0;
}
最终打印为 ‘geek time’
var myname = "geek time";
function showName() {
console.log(myname);
if (0) {
var myname = "Hei ha";
}
}
showName();
最终打印为 undefined
- 本应销毁的变量没有销毁
function foo() {
for (var i = 0; i < 7; i++) {}
console.log(i);
}
foo(); //7
输出为 7,变量 i 在 foo 循环结束后并没有被销毁,说明在创建执行上下文阶段,变量 i 就已经被提升了。
在其他语言中,for,if,while,{},函数块等内部变量执行完后就会被销毁。
ES6 如何解决变量提升带来的问题?
通过 var 声明的变量,在编译阶段被放进变量环境
,而通过 let,const 声明的被放进词法环境(Lexical Environment)
;
每个块级作用域内的 let,const 声明又被放进词法环境的一个单独区域中。
当作用域块执行结束后,内部定义的变量就会从词法环境的栈顶弹出,从而实现和其他语言一样的变量销毁。
作用域链与闭包
作用域链
- 下面代码输出什么?
function bar() {
console.log(myName);
}
function foo() {
var myName = "极客邦";
bar();
}
var myName = "极客时间";
foo();
按上面调用栈顺序来分析,那么结果应该是极客邦
;
实际答案是极客时间
每个执行上下文的环境中都包含了一个外部引用,用来指向外部执行的上下文,上图中的 outer。
当一段代码使用一个变量时,JS 引擎首先在“当前执行上下文(bar)”中查找该变量,若没有,则在 outer 所指向的执行上下文中查找,这个查找链条就是作用域链
。
- 问题:那么 foo 中调用的 bar,为什么 bar 的外部引用是全局执行上下文而不是 foo 函数的执行上下文?
因为在 JS 执行过程中,作用域链
是由词法作用域
决定的。
词法作用域
词法作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态作用域,通过它能预测代码在执行过程中如何查找标识符。
上图中,整个词法作用域链的顺序是:
foo 函数作用域 -> bar 函数作用域 -> main 函数作用域 -> 全局作用域。
词法作用域是代码阶段就决定好的,和函数怎么调用没有关系。
再看上面的问题,就知道打印的结果为什么是“极客时间”了。
如果换成下面的:
function foo() {
var myName = "极客邦";
function bar() {
console.log(myName);
}
return bar();
}
var myName = "极客时间";
foo();
此时打印的就是“极客邦”了。
块级作用域中的变量查找
function bar() {
var myName = "浏览器";
let test1 = 100;
if (1) {
let myName = "Chrome 浏览器";
console.log(test);
}
}
function foo() {
var myName = "极客邦";
let test = 2;
{
let test = 3;
bar();
}
}
var myName = "极客时间";
let myAge = 10;
let test = 1;
foo();
结合上面的作用域链与词法作用域,易得最终输出结果为 1。
查找顺序如下(图中标记的 1,2,3,4,5)
闭包
function foo() {
var myName = "极客时间";
let test1 = 1;
const test2 = 2;
var innerBar = {
getName() {
console.log(test1);
return myName;
},
setName(newName) {
myName = newName;
}
};
return innerBar;
}
var bar = foo();
bar.setName("极客邦");
bar.getName();
console.log(bar.getName());
根据词法作用域的规则易得,内部函数getName
与setName
可以访问 foo 中的 myName 和 test1。所以,当 foo 执行完后,这两个变量成为 foo 闭包的专属变量,除了 setName 和 getName 其他任何地方都无法访问 foo 闭包中的变量。调用栈的状态如下:
通过上图可以看出,当执行到 foo 时,闭包就产生了,foo 结束后,getName 与 setName 都引用了clourse(foo)
对象,所以即使 foo 函数结束了,clourse(foo)
依然被其内部的 getName 和 setName 引用,调用这两个方法时,创建的执行上下文就包含了 clourse(foo)
- 站在内存模型角度分析代码的执行流程
- 执行 foo 函数,编译、创建执行上下文。
- 编译过程中,遇到 setName,发现其中使用了外部函数的变量
myName
,于是生成一个闭包环境来存放 myName 变量。 - 接着扫描,又遇到 getName 发现函数内部有使用了外部变量,JS 引擎又将 test1 存放到闭包中。
- test2 没有被函数内部引用,所以依然保存在执行栈中。
- 产生闭包的核心两步:
- 预扫描内部函数
- 把内部函数引用的外部变量保存到堆中
闭包如何使用?
当执行 bar.setName()
方法中的 myName = ‘xxx’ 时,JS 引擎会沿着“当前执行上下文 -> foo 函数闭包 -> 全局执行上下文”的属性来查找,如下:
Chrome 开发者工具中在 innerBar 的函数中打断点,刷新页面也可查看闭包状态。
通过Scope
即可查看作用域链的情况。
闭包如何回收?
如果引用闭包的函数是全局变量,那么闭包会一直存在到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。
如果引用闭包的函数是局部变量,等函数销毁后,下次 JS 引擎执行垃圾回收时,判断闭包如果已不再被使用,就会回收这块内存。
综上所述,若闭包一直使用,则作为全局变量,否则为局部变量。
this
this 是和执行上下文绑定的,执行上下文有全局、函数、eval 执行上下文,故对应的 this 也有这三种。
全局执行上下文中的 this
window
函数执行上下文中的 this
- 通过 call,bind,apply 设置
let bar = {
myName: 'x'
}
function foo() {
this.myName = 'xxx'
}
foo.call(bar)
- 通过对象调用方法设置
var myObj = {
name: 'x',
showThis() {
console.log(this)
}
}
myObj.showThis() // 等同于 myObj.showThis.call(myObj)
var foo = myObj.shiwThis
foo() // window
在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
3. 通过构造函数中设置
new 运算符
this 的设计缺陷
- 嵌套函数中的 this 不会从外层继承
this 没有作用域限制,所以嵌套函数不会从调用它的函数中继承。
var myObj = {
name: 'jk',
showThis() {
console.log(this) // myObj
function bar() {
console.log(this)
}
bar() // window
}
}
解决办法:1. 外层绑定 this 2. 箭头函数
2. 普通函数中的 this 默认指向全局对象 window
严格模式下,默认执行一个函数,这个函数执行上下文中的 this 是 undefined
思考题
- 第一题
showName();
var showName = function() {
console.log(2);
};
function showName() {
console.log(1);
}
输出 1,第一个 showName 带 var 经过变量提升后被赋值为 undefined,变量 showName 会被下面同名函数覆盖,再次执行 showName 就为 2,具体过程如下
// 编译
var showName = undefined;
function showName() {
console.log(1);
}
// 执行
showName(); // 1
showName = function() {
console.log(2);
};
showName(); // 2
- 第二题
let myname = "geek time";
{
console.log(myname);
let myname = "Hei ha";
}
最终的打印结果不是 undefined.
而是:Cannot access ‘myname’ before initialization
原因:在块级作用域内,let 变量只是创建被提升,初始化并没有被提升,在初始化之前使用变量,会形成一个暂时性死区。
- var 的创建和初始化被提升,赋值不会被提升。
- let 的创建被提升,初始化和赋值不会被提升。
- function 的创建、初始化和赋值均会被提升。
- 暂时性死区:
执行函数时才有进行编译,抽象语法树(AST)在进入函数阶段就生成了,并且函数内部作用域已经明确了,所以进入块级作用域不会有编译过程,只不过通过 let 或者 const 声明的变量会在进入块级作用域时才被创建,但是在该变量没有赋值之前,引用该变量 JavaScript 引擎会抛出错误—-这就是“暂时性死区”
- 第三题
var bar = {
myName: "time.geekbang.com",
printName: function() {
console.log(myName);
}
};
function foo() {
let myName = " 极客时间 ";
return bar.printName;
}
let myName = " 极客邦 ";
let _printName = foo();
_printName();
bar.printName();
foo 函数返回的 printName 是全局声明的函数,因此和 foo 当中定义的变量也没有任何联系,这个时候 foo 函数返回 printName 并不会产生闭包
全局执行上下文:
变量环境:
Bar=undefined
Foo= function
词法环境:
myname = undefined
_printName = undefined
开始执行:
bar ={myname: “time.geekbang.com”, printName: function(){…}}
myName = “ 极客邦 “
_printName = foo()
调用 foo 函数,压执行上下文入调用栈
foo 函数执行上下文:
变量环境: 空
词法环境: myName=undefined
开始执行:
myName = “ 极客时间 “
return bar.printName
开始查询变量 bar, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找 outer 词法环境(没有)-> 查找 outer 语法环境(找到了)并且返回找到的值
pop foo 的执行上下文
_printName = bar.printName
printName()压 bar.printName 方法的执行上下文入调用栈
bar.printName 函数执行上下文:
变量环境: 空
词法环境: 空
开始执行:
console.log(myName)
开始查询变量 myName, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找 outer 词法环境(找到了)
打印” 极客邦 “
pop bar.printName 的执行上下文
bar.printName() 压 bar.printName 方法的执行上下文入调用栈
bar.printName 函数执行上下文:
变量环境: 空
词法环境: 空
开始执行:
console.log(myName)
开始查询变量 myName, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找 outer 词法环境(找到了)
打印” 极客邦 “
pop bar.printName 的执行上下文
精选问答
- 关于同名变量和函数的两点处理原则:
- 如果是同名的函数,JavaScript 编译阶段会选择最后声明的那个。
- 如果变量和函数同名,那么在编译阶段,变量的声明会被忽略
- 下面 log 为啥是 undefined?
console.log(a);
{
function a() {}
}
ES 规定函数只不能在块级作用域中声明,
function foo(){
if(true){
console.log('hello world');
function g(){ return true; }
}
}
也就是说,上面这行代码执行会报错,但是个大浏览器都没有遵守这个标准。
接下来到了 ES6 了,ES6 明确支持块级作用域,ES6 规定块级作用域内部声明的函数,和通过 let 声明变量的行为类似。
规定的是理想的,但是还要照顾实现,要是完全按照 let 的方式来修订,会影响到以前老的代码,所以为了向下兼容,个大浏览器基本是按照下面的方式来实现的:
function foo(){
if(true){
console.log('hello world');
var g = function(){return true;}
}
}