这篇博客是对前端面试所必须掌握的知识点的总结,并且这篇博客正在持续更新中…
1.JavaScript 基础
1.执行上下文/作用域/闭包
1.什么是执行上下文?
执行上下文是评估和执行JavaScript代码环境的抽象概念。每当JavaScript代码在运行时,他都是在执行上下文中运行。
执行上下文的类型
JavaScript共有三种执行上下文类型
- 全局执行上下文
- 这是基础的上下文,任何不在函数内部的代码都在全局上下文中.他会执行两件事:创建一个全局的window对象(浏览器环境的情况下),并且设置this的值等于这个全局对象。一个程序中只会有一个全局执行上下文
- 函数执行上下文
- 每当函数被调用时,都会为该函数创建一个新的执行上下文。每个函数都有他自己的执行上下文,只不过是在函数被调用时才被创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,他会按定义的瞬狙执行一系列步骤
- Eval函数执行上下文
- 执行在
eval
函数内部的代码也会有它属于自己的执行上下文
- 执行在
执行上下文栈
执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
当JavaScript引擎第一次遇到你的脚本时,他会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,他会为该函数创建一个新的执行上下文并压入栈的顶部.
引擎会执行那些执行上下文位于栈顶的函数.每当函数执行结束之后,最上层的执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文
一旦所有代码执行完毕,JavaScript引擎从当前栈中移除全局执行上下文
怎么创建执行上下文?
创建执行上下文有两个阶段:
- 创建阶段
- 执行阶段
创建阶段
在JavaScript代码执行前,执行上下文将经历创建阶段。在创建阶段将会发生三件事:
- this值的绑定
- 创建词法环境
- 创建变量环境
所以执行上下文在概念上表示如下:
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
This绑定:
在全局执行上下文中,this
的值指向全局对象(在浏览器中,全局对象为window
’)
在函数执行上下文中,this的值取决于该函数是如何被调用的.如果他被一个引用类型对象调用,那么this会被设置成那个对象,否则this
的值被设置成全局对象或者undefined
(严格模式)
let foo = {
baz: function() {
console.log(this);
}
}
foo.baz(); // 'this' 引用 'foo', 因为 'baz' 被
// 对象 'foo' 调用
let bar = foo.baz;
bar(); // 'this' 指向全局 window 对象,因为
// 没有指定引用对象
词法环境
官方的 ES6 文档把词法环境定义为
词法环境是一种持有变量符-变量映射的结构(标识符指的是变量/函数的名字,而变量是对实际对象或原始数据的引用)
在词法环境的内部有两个组件:1.环境记录器和2.一个外部环境的引用
1.环境记录器是存储变量和函数声明的实际位置
2.外部环境的引用意味着它可以访问其父级词法环境(作用域)
词法环境有两种类型: 全局环境和函数环境
-
全局环境(在全局执行上下文中)是没有外部环境引用的词法环境,全局环境的外部环境引用是null,
它拥有创建的Object/Array等,在环境记录器内的原型函数(关联全局对象,比如window对象)还有任何用户定义的全局变量,并且
this
的值指向全局对象 -
在函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。
环境记录器也有两种类型:
- 声明式环境记录器,用来存储变量、函数和参数
- 对象环境记录器,用来定义出现在全局上下文中的变量和函数关系
由上不难得知
- 在全局环境中,环境记录器是对象环境记录器
- 在函数环境中,环境记录器是声明式环境记录器
注意 : 对于函数环境,声明式环境记录器还包含了一个传递给函数的 arguments
对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。
抽象地讲,词法环境在伪代码中看起来像这样:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
}
outer: <null>
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
}
outer: <Global or outer function environment reference>
}
}
变量环境
变量环境其实也是一个词法环境,其环境记录器中持有变量声明语句在执行上下文中创建的绑定关系
变量环境有着词法环境的所有属性
在ES6中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let
和const
)绑定,而后者只用来存储var
变量绑定
我们看点样例代码来理解上面的概念:
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
执行上下文看起来像这样:
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
可能你已经注意到 let
和 const
定义的变量并没有关联任何值,但 var
定义的变量被设成了 undefined
。
这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 undefined
(var
情况下),或者未初始化(let
和 const
情况下)。
这就是为什么你可以在声明之前访问 var
定义的变量(虽然是 undefined
),但是在声明之前访问 let
和 const
的变量会得到一个引用错误。
这就是我们说的变量声明提升。
执行阶段
在此阶段完成对所有存储的变量的分配,最后执行代码.
注意: 在执行阶段,如果JavaScript引擎不能再源码中声明的实际位置找到let
变量的值,那么他就会被赋值为undefined
2.作用域(Scope)
什么是作用域?
作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
ES6之前,JavaScript只有全局作用域和函数作用域,ES6之后,新增了块级作用域,可以通过let
和const
来创建
全局作用域和函数作用域
**在代码中任何地方都能访问到的对象拥有全局作用域,**以下几种情况拥有全局作用域:
- 最外层函数和在最外层函数外定义的变量拥有全局作用域
- 所有未定义直接赋值的变量默认为全局变量,拥有全局作用域
- 所有window对象的属性拥有全局作用域
全局作用域的弊端:容易引发命名冲突,污染全局命名空间
函数作用域
在函数内部声明的变量拥有函数作用域,一般只能在固定的代码片段内可以访问到.
作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行
值得注意的是:块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。
if (true) {
// 'if' 条件语句块不会创建一个新的作用域
var name = 'Hammad'; // name 依然在全局作用域中
}
console.log(name); // logs 'Hammad'
块级作用域
块级作用域可以通过let
和const
声明,所声明的变量在指定块作用域·之外无法被访问。
块级作用域在如下情况被创建:
- 在一个函数内部
- 在一个代码块内部(‘{}’)
块级作用域有以下几个特点:
- 声明变量不会提升的代码块顶部
- 禁止重复声明
作用域链
在JavaScript中,函数、块、模块都可以形成作用域,他们之间可以相互嵌套、作用域之间会形成引用关系,这条链叫做作用域链
作用域链的创建和变化
函数创建时:
JavaScript中使用的是词法作用域,函数的作用域在函数定义的时候就已经决定了
函数有一个内部属性[[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]]);
至此,作用域链创建完毕
3.闭包
什么是闭包?
闭包就是同时含有对函数对象以及作用域对象引用的对象,实际上所有JavaScript对象都是闭包.
本质:在一个函数内部创建另一个函数
只要存在函数嵌套,并且内部函数调用了外部函数的属性,就产生了闭包.
闭包的特性:
- 函数嵌套函数
- 函数内部引用函数外部的参数和变量
- 参数和变量不会被垃圾回收机制回收
闭包是什么时候被创建的?
因为所有JavaScript对象都是闭包,所以当你定义一个函数时.就产生了闭包
闭包是什么时候被销毁的?
当他不被任何其他的对象引用的时候,闭包就被销毁
闭包的好处:
- 保护函数内的变量安全,实现封装,防止变量流入其他环境发生命名冲突
- 在内存中维持一个变量,延长变量的生命周期
- 匿名自执行函数可以减少内存消耗
闭包的缺点:
- 被引用的私有变量不能被销毁,增大了内存的消耗,造成内存泄露
- 闭包涉及跨域访问,会导致性能损失
闭包的作用
- 使得函数内部的变量在函数执行完之后,仍然存活在内存中(延长了局部变量的生命周期)
- 让函数外部可以操作到函数内部的数据
闭包的原理
当一个函数返回后,没有其他对象会保存对其的引用。所以,它就可能被垃圾回收器
回收。
函数对象中总是有一个[[scope]]
属性,保存着该函数被定义的时候所能够直接访问的作用域对象。所以,当我们在定义嵌套的函数的时候,这个嵌套的函数的[[scope]]
就会引用外围函数(Outer function)的当前作用域对象。
如果我们将这个嵌套函数返回,并被另一个标识符所引用的话,那么这个嵌套函数及其[[scope]]所引用的作用作用域对象就不会被垃圾回收器所销毁,这个对象就会一直存活在内存中,我们可以通过这个作用于对象获取到外部函数的属性和值。
这就是闭包的原理