基本概念
数据类型
基础类型
- number
- string
- null
- undefined
- boolean
- symbol
- bigInt
引用类型
- object
- array
- function
- date
- regExp
作用域
什么是作用域
作用域决定了代码区块中变量和其他资源的可访问性和可见性。
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
js采用的是静态作用域,在函数定义时就决定了作用域。
分类
- 全局作用域
- 函数作用域
- 块级作用域(可用
let和const声明,变量不提升,变量禁止重复声明)
变量提升:指定义时提升到顶部,针对var和函数,但如果是函数赋值给变量,则无法提前调用。
JS如何执行
JavaScript的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样:
解释阶段
- 词法分析
- 语法分析
- 作用域规则确定
执行阶段
- 创建执行上下文
- 执行函数代码
- 垃圾回收
JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。
作用域和执行上下文之间最大的区别是: 执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
执行上下文
执行上下文三个重要属性
- 变量对象(Variable object,VO)
- 作用域链
- this
执行上下文栈
JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。
初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 全局上下文。
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。
变量对象
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
- 全局上下文的变量对象初始化是全局对象
- 函数上下文的变量对象初始化只包括 Arguments 对象
- 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
- 在代码执行阶段,会再次修改变量对象的属性值
作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
函数的作用域在函数定义的时候就决定了。
这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
举个例子:
function foo() {
function bar() {
...
}
}
复制代码
函数创建时,各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
复制代码
当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。
这时候执行上下文的作用域链,我们命名为 Scope:
Scope = [AO].concat([[Scope]]);
复制代码
至此,作用域链创建完毕。
this
- 普通函数指向window
- 对象调用时指向对象
new 一个对象时发生了什么?
- 创建了临时对象
- 将this指向临时对象
- 执行构造函数
- 返回临时对象
JS进阶
闭包
在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的
[[scope]]中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。
垃圾回收
两种策略
- 标记清除
- 引用计数
标记清除
标记阶段把所有的活动对象做上标记,清除阶段把所有没有标记的活动对象销毁。
缺点: 存在内存碎片,分配速度慢。
改进: 标记整理。会将活着的对象向内存一端整理,清理掉内存的边界。
引用计数
跟踪记录每个变量被引用的次数,当变量的引用次数为0时进行回收。
缺点:计数器占内存,不知道被引用次数的上限;循环引用无法回收。
事件循环
先执行同步任务,再执行异步任务,同步任务结束后,会先执行微任务,再执行宏任务。