chrome 版本 114.0.5735.198(正式版本) (arm64) ,控制台特性一直变化,所以需要记录版本
字节码是 介于 AST 和机器码之间的一种代码,不能直接执行,需要通过解释器 Ignition 动态解释为机器码后才能执行
JavaScript 属于解释型语言,是逐行解释,逐行执行
但是 V8 不仅仅有解释器,他包含编译器 TurboFan 与解释器 Ignition
因为 V8 采用的是即时编译技术(JIT):字节码配合解释器和编译器的技术
在解释器 Ignition 解释并执行字节码过程中,如果有热点代码(重复执行多次的代码),会通过编译器 TurboFan 把字节码编译为机器码,提高效率
V8 一开始并没有字节码,而是直接将 AST 转换为机器码,因为执行机器码的效率非常高效。但是 需要消耗大量的内存来存放机器码。为了解决内存占用问题,V8 团队才大幅重构了引擎架构,引入字节码
JavaScript 执行流程分为两个阶段:编译阶段和执行阶段。
编译阶段:
执行阶段:
执行上下文就是 JavaScript 执行一段代码时的运行环境
执行上下文种类分为:
注意,全局执行上下文是在编译阶段就生成并存储在执行上下文栈中的
每一次执行到**(函数/代码块)时,就会创建(函数/块级)执行上下文,然后放入执行上下文栈**中,执行完后弹出
所以全局执行上下文直到程序终止前都会存在 当递归调用函数时,没有终止条件就一直入栈,会产生著名的栈溢出(Stack Overflow)
执行上下文创建时,会:
为了新增块级作用域并兼容变量提升等旧特性,出现了新的定义比如词法环境和变量环境,本质并没有太大变化,但是尽量以 es3 为基础理解,以 es2018 为基础应用
es3:
es6:
es2018:
执行上下文栈是按执行顺序压入的,但是代码作用域往往不同于调用的顺序,所以作用域是通过作用域链来控制代码可以访问的变量等内容
如:
function fuc1() {
console.log(myName)
}
function fuc2() {
var myName = '函数2'
fuc1()
}
var myName = '全局'
func2()
调用栈顺序为全局->fuc2->fuc1 但是作用域链顺序为 fuc1->全局 和 fuc2->全局
作用域是 es3 的知识了,我们拓展到 2018 进行理解
JavaScript 采用词法(静态)作用域,就是指作用域是由代码中函数(代码块)声明的位置来决定的
作用域种类分为:
es3 执行阶段先看自身作用域再看父级作用域[[scope]],注意[[scope]]是我们无法看到的
es2018 执行阶段先查找词法环境再查找变量环境再查找 outer,即先看块级作用域再看函数作用域再看父级作用域,通过这种方式解决了新增 let/const 的块级作用域的问题
现在看变量提升就很好理解了
在编译阶段创建执行上下文栈的时候,创建的变量环境中会提取出来 var 和函数声明,优先提取函数
在执行阶段执行代码时才进行赋值操作
优先提取函数,重复声明的函数使用最后的声明,函数和变量同时声明则使用函数声明
闭包就是函数执行后还保存在内存的变量
环境指的是内部函数引用外部函数的变量的集合
根据词法作用域的规则,内部函数总是可以访问它们的外部函数中的变量
当外部函数执行完时,内部函数引用的外包函数的变量依然保存在内存中
也有人称保存的这些变量的集合叫闭包
在 chrome 控制台就可以看到闭包的特性
[[Scopes]] 不是内部 JavaScript 属性,它是 Chromium 调试器创建的功能
[[Scopes]]在 chrome 版本 114.0.5735.198(正式版本) (arm64) 浏览器内可以观察闭包
在控制台可以通过console.dir
看到属性[[Scopes]]
[[Scopes]]
是一个类数组的栈结构,[Closure,Script,Global]
function getFunc() {
const a = 1
return function func() {
console.log(a)
}
}
const func = getFunc()
func()
可以看到 getFunc 执行完后
返回的内部函数 func 仍然可以打印出 getFunc 中的变量
这就是闭包
在[[Scopes]]
中则可以看到 Closure (getFunc)中保存了 a 变量
先看一个例子
var obj = {
myName: '对象',
logName: function () {
console.log(myName)
},
}
function getLogFunc() {
let myName = '函数'
return obj.logName
}
let myName = '全局'
let logFunc = getLogFunc()
logFunc()
obj.logName()
因为 JavaScript 是静态作用域,obj 的父作用域是全局作用域,所以这两个打印出来的值都是"全局"
但是我们使用面向对象时,经常需要 logName 访问 obj 对象的 name 属性,并且可以继承复用
所以就有了 this 机制,
上面创建执行上下文的时候也创建了 this,所以 this 也是分为:
而他们都是一种指向:
也有方法使 this 指向不同值:
setTimeout 方法中的 this ,即使使用对象方法调用也是指向 window/global
有了 this 后就不一样了,如:
var obj = {
myName: '对象',
logName: function () {
console.log(this.myName)
},
}
function getLogFunc() {
let myName = '函数'
return obj.logName
}
let myName = '全局'
let logFunc = getLogFunc()
logFunc()
obj.logName()
当 logName 里改为使用 this.myName 后
logFunc 是直接调用,this 指向 window
obj.logName()是对象方法调用,this 指向 obj
所以打印应该是 undefined 和'对象'
注意,嵌套函数中的直接调用的函数也是直接调用,并不会继承上一个函数的 this
如:
var obj = {
myName: '对象',
logName: function () {
console.log(this.myName)
function logThis() {
console.log(this)
}
logThis()
},
}
obj.logName()
而箭头函数没有 this ,访问 this 是继承上一层函数的 this,这就是为什么推出箭头函数功能
如:
var obj = {
myName: '对象',
logName: function () {
console.log(this.myName)
const logThis = () => {
console.log(this)
}
logThis()
},
}
obj.logName()
function createObj() {
this.myName = '对象'
}
var myObj = new createObj()
在构造函数中,JavaScript 引擎做了四件事:
原型对象可以通过 看得见的原型链 了解
相当于代码:
var tempObj = {}
CreateObj.call(tempObj)
CreateObj()
return tempObj
23.12.06