浏览器中的JavaScript执行机制
浏览器中的JavaScript执行机制
执行流程核心
问 什么是变量提升?
所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。
JavaScript 代码的执行流程
JavaScript 代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为 JavaScript 代码在执行之前需要先编译。
在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为 undefined;在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。
如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的。
主要目的是让你清楚 JavaScript 的执行机制:先编译,再执行。
问 分析下面JavaScript 执行流程最终结果是?
showName()
var showName = function(){
console.log(2)
}
showName()
function showName() {
console.log(1)
}
一个JavaScript 执行需要有两个阶段
编译 ---> var showName = undefined ;
function showName() {
console.log(1)
}
执行 ---> showName() ..... 输出 1
var showName = function { console.log(2)} 赋值操作
showName() ..... 输出 2
栈溢出
问 哪些情况下代码才算是“一段”代码,才会在执行之前就进行编译并创建执行上下文?
- 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
- 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
- 当使用
eval
函数的时候,eval
的代码也会被编译,并创建执行上下文。
问 什么是栈?
栈就是类似于一端被堵住的单行线,车子类似于栈中的元素,栈中的元素满足后进先出的特点.
问 什么是 JavaScript 的调用栈?
JavaScript 引擎正是利用栈的这种结构来管理执行上下文的。在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。
栈溢出
调用栈是一种用来管理执行上下文的数据结构,符合后进先出的规则。不过还有一点你要注意,调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。
解释一下如下的代码
var a = 2
function add(b,c){
return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return a+result+d
}
addAll(3,6)
第一步,创建全局上下文,并将其压入栈底,全局执行上下文压入到调用栈后,JavaScript 引擎便开始执行全局代码了。首先会执行 a=2 的赋值操作,执行该语句会将全局上下文变量环境中 a 的值设置为 2
第二步是调用
addAll
函数, addAll 函数的执行上下文创建好之后,便进入了函数代码的执行阶段了,这里先执行的是 d=10 的赋值操作,执行语句会将 addAll 函数执行上下文中的 d 由 undefined 变成了 10。
- 第三步,当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈, 当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9。如下图所示:
- 紧接着
addAll
执行最后一个相加操作后并返回,addAll
的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。最终如下图所示:
好了,现在你应该知道了调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。
var缺陷以及为什么要引入let和const?
问 什么是作用域?
作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
//if块
if(1){}
//while块
while(1){}
//函数块
function foo(){}
//for循环块
for(let i = 0; i<100; i++){}
//单独一个块
{}
问 变量提升带来了什么问题?
- 变量容易在不被察觉的情况下被覆盖掉
var myname = "极客时间"
function showName(){
console.log(myname);
if(0){
var myname = "极客邦"
}
console.log(myname);
}
showName()
// 执行上面这段代码,打印出来的是 undefined.
- 本应销毁的变量没有被销毁
function foo(){
for (var i = 0; i < 7; i++) {
}
console.log(i);
}
foo()
// 在 JavaScript 代码中,i 的值并未被销毁,所以最后打印出来的是 7。这同样也是由变量提升而导致的,在创建执行上下文阶段,变量 i 就已经被提升了,所以当 for 循环结束之后,变量 i 并没有被销毁。这依旧和其他支持块级作用域的语言表现是不一致的,所以必然会给一些人造成误解
ES6
是如何解决变量提升带来的缺陷 ,ES6
引入了let
和const
关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域。
问 JavaScript 是如何支持块级作用域的?
站在执行上下文的角度来揭开答案
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
第一步是编译并创建执行上下文
函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。
通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。在函数的作用域块内部,
通过 let 声明的变量并没有被存放到词法环境中。
- 第二步继续执行代码,
当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量.
在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出
从上图你可以清晰地看出变量查找流程,不过要完整理解查找变量或者查找函数的流程,就涉及到作用域链了
问 通过分析词法环境,最终的结果是?
let myname= '极客时间'
{
console.log(myname)
let myname= '极客邦'
}
【最终打印结果】:VM6277:3 Uncaught ReferenceError: Cannot access 'myname' before initialization
【分析原因】:在块作用域内,let声明的变量被提升,但变量只是创建被提升,初始化并没有被提升,在初始化之前使用变量,就会形成一个暂时性死区。
【拓展】 var的创建和初始化被提升,赋值不会被提升。 let的创建被提升,初始化和赋值不会被提升。 function的创建、初始化和赋值均会被提升。
try /catch
分句结构中也具有块作用域.
补充 在ES3开始,作用域链和闭包
问 什么是作用域链?
JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链。
看看下面的代码
function bar() {
console.log(myName)
}
function foo() {
var myName = "极客邦"
bar()
}
var myName = "极客时间"
foo()
// 比如上面那段代码在查找 myName 变量时,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。
每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。
问 什么是词法作用域?
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
从图中可以看出,词法作用域就是根据代码的位置来决定的,其中
main
函数包含了bar
函数,bar
函数中包含了foo
函数,因为 JavaScript 作用域链是由词法作用域决定的,所以整个词法作用域链的顺序是:foo
函数作用域—>bar
函数作用域—>main
函数作用域—> 全局作用域。
词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。
比如
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()
需要打印出来变量 test,那么就需要查找到 test 变量的值,其查找过程我已经在上图中使用序号 1、2、3、4、5 标记出来了。
问 什么是闭包?
高级程序设计中的概念: 闭包是指有权访问另一个函数作用域中的变量的函数。
MDN
上的概念:闭包是函数和声明该函数的词法环境的组合。
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是
foo
,那么这些变量的集合就称为foo
函数的闭包
思考 分析下面代码
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()
// 解题思路
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()
// 全局执行上下文:
// 变量环境:
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的执行上下文
搞清楚this关键字
在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套 this 机制。
希望你能区分清楚作用域链和 this 是两套不同的系统,它们之间基本没太多联系。在前期明确这点,可以避免你在学习 this 的过程中,和作用域产生一些不必要的关联。
this
所在的位置
从图中可以看出,
this
是和执行上下文绑定的,也就是说每个执行上下文中都有一个this
。
全局执行上下文中的 this
作用域链的最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象。
函数执行上下文中的 this
bind , call 和 apply 方法来设置函数执行上下文中的 this
let bar = { myName : "极客邦", test1 : 1 } function foo(){ this.myName = "极客时间" } foo.call(bar) console.log(bar) console.log(myName)
通过对象调用方法设置改变执行上下问的this,使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的
var myObj = { name : "极客时间", showThis: function(){ console.log(this) } } myObj.showThis()
通过构造函数中设置
function CreateObj(){ this.name = "极客时间" } var myObj = new CreateObj() // 首先创建了一个空对象 tempObj; // 接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象; // 然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象; // 最后返回 tempObj 对象
思考 分析下面的代码
// 我想通过 updateInfo 来更新 userInfo 里面的数据信息,但是这段代码存在一些问题,你能修复这段代码吗? let userInfo = { name:"jack.ma", age:13, sex:male, updateInfo:function(){ //模拟xmlhttprequest请求延时 setTimeout(function(){ this.name = "pony.ma" this.age = 39 this.sex = female },100) } } userInfo.updateInfo()
// 修改方法一:箭头函数最方便 使用es6的箭头函数,此处省略(太简单) // 修改方法二:缓存外部的this 使用let that=this 此处省略(太简单) // 解法三,其实和方法二的思路是相同的 let userInfo = { name:"jack.ma", age:13, sex:'male', updateInfo:function(){ // 模拟 xmlhttprequest 请求延时 void function(me) { setTimeout(function() { me.name = "pony.ma" me.age = 39 me.sex = 'female' },100) }(this); } } userInfo.updateInfo() setTimeout(() => { console.log(userInfo) },200) let update = function() { this.name = "pony.ma" this.age = 39 this.sex = 'female' } // 解法四: 利用call或apply修改函数被调用时的this值(不知掉这么描述正不正确) let userInfo = { name:"jack.ma", age:13, sex:'male', updateInfo:function(){ // 模拟 xmlhttprequest 请求延时 setTimeout(function() { update.call(userInfo); // update.apply(userInfo) }, 100) } } userInfo.updateInfo() setTimeout(() => { console.log(userInfo) },200) // 解法五: 利用bind返回一个新函数,新函数被调用时的this指定为userInfo let userInfo = { name:"jack.ma", age:13, sex:'male', update: function() { this.name = "pony.ma" this.age = 39 this.sex = 'female' }, updateInfo:function(){ // 模拟 xmlhttprequest 请求延时 setTimeout(this.update.bind(this), 100) } }