# fed-e-task-01-02 **Repository Path**: drx2020/fed-e-task-01-02 ## Basic Information - **Project Name**: fed-e-task-01-02 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-07-12 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 简答题 ### 一、分析代码(作用域) 输出结果:10 原因分析:for 循环的索引 `i` 使用了 var 声明方式,由于声明提前的特性,题中代码等价于如下: ```js var a; var i; a = []; for (i = 0; i < 10; i++) { a[i] = function() { console.log(i); } } a[6](); ``` 执行以上代码时,JavaScript 解释器会创建一个执行上下文 Execution Context,如下形式: ``` GlobalExecutionContext: LexicalEnvironment: [[record]] -> {} [[outer]] -> null VariableEnvironment: [[record]] -> { a: undefined; i: undefined; } [[outer]] -> global ``` 在代码继续执行完 for 循环之后,上述的上下文会变成如下形式: ``` GlobalExecutionContext: LexicalEnvironment: [[record]] -> {} [[outer]] -> null VariableEnvironment: [[record]] -> { a: [Function, ..., Function]; i: 10; } [[outer]] -> global ``` 当我们调用 `a` 元素所存储的方法时,JavaScript 会在创建一个新的执行上下文,如下形式: ``` // 调用 a[6] 方法时所创建的新的 ExecutionContext ExecutionContext: LexicalEnvironment: [[record]] -> {} [[outer]] -> // 引用外部的词法上下文 LexicalEnvironment: [[record]] -> {} [[outer]] -> null VariableEnvironment: [[record]] -> { a: [Function, ..., Function]; i: 10; } [[outer]] -> global ``` 所以,当我们调用 `a[6]` 时,虽然 LexicalEnvironment 的 [[record]] 不存在 `i` 但我们可以通过 `[[outer]]` 对 VariableEnviroment 的引用,找到 VariableEnvironment 中 [[record]] 的 `i`,由于此时该值为 10,函数的执行结果即输出 10。 ### 二、分析代码(TDZ 问题) 输出结果:ReferenceError 原因分析:区别于 var 在刚创建出来时会被自动赋值 undefined,let 在初始阶段时不会被赋予任何值且处于 uninitialized 状态,如果此时访问该状态的变量就会出现 Temporal Dead Zone 问题,即处在变量创建之后与赋值之前的临时区域。题目中可以看到 let 发生在 if 语句的块级作用域内,而 let 是具有块级作用域声明的能力,JavaScript 会为此创建一个新的词法上下文,形式如下: ``` // if 所在的词法上下文 LexcicalEnvironment: [[record]] -> { tmp: } [[outer]] -> // 引用 if 外部的词法上下文 LexicalEnvironment: [[record]] -> {} [[outer]] -> null VariableEnvironment: [[record]] -> { tmp: 123 } [[outer]] -> global ``` 从上可以看到,在执行 `console.log` 时 `tmp` 已经存在于 if 语句中的 LexicalEnvironment 所以不会沿着 [[outer]] 去寻找外部的 VariableEnvironment,所以此时访问 `tmp` 会抛出异常。 ### 三、ES6 最小数 ```js var arr = [12, ..., 4] Math.min(...arr); ``` ### 四 、var / let / const 对比 | 声明方式 | 作用域 | 可重复声明 | 可重新赋值 | 声明提升 | TDZ 问题 | | -------- | ------------------------------------------ | ---------- | ---------- | -------- | ---------------------------------- | | var | 函数级作用域(函数外声明会污染全局作用域) | 可以 | 可以 | 会 | 无(创建时会被默认赋值 undefined) | | let | 块级作用域 | 不可以 | 可以 | 会 | 有 | | const | 块级作用域 | 不可以 | 不可以 | 会 | 有 | ### 五、分析代码(this 问题) 输出结果:20 原因分析:setTimout 的回调函数使用了 ES6 语法的箭头函数声明方式,所以该回调函数自动绑定当前声明作用域的 this 变量,而这个 this 变量的值是由 fn 函数在运行时的调用者确定。因此,`obj.fn()` 使得 fn 函数的 this 指向了 `obj`,而 `this.a` 就是 `obj.a` 的值,所以输出了 20。 ### 六、Symbol 用途 Symbol 是 ES6 新加入的基础数据结构,具有唯一性,用它来命名对象属性可以避免重名。我们还可以通过内置的 Symbol 来实现诸如自定义迭代器 Symbol.iterator 或者异步迭代器 Symbol.asyncIterator,甚至覆写 instanceof 操作符 Symbol.hasInstance、数据类型转换操作 Symbol.toPrimitive 等。 ### 七、浅拷贝 vs 深拷贝 - 浅拷贝:仅复制基础数据类型的值,对于复杂数据类型,只复制引用,不复制引用所对应的值 - 深拷贝:除了复制基础类型的值,对于复杂数据类型,会进行递归式复制,直到完成所有基础数据类型的复制 ### 八、TypeScript 与 JavaScript 关系 TypeScript 是 JavaScript 的超集,是对 JavaScript 的语法增强并加入可选的类型系统,但编译产物依然是 JavaScript。 ### 九、TypeScript 优缺点 TypeScript 作为 JavaScript 的超集,可以很好地兼容当前及未来的 JavaScript 特性,这样还可以帮助我们摆脱对具体的浏览器版本的依赖而使用到最新的 JavaScript 特性;其次,其类型系统能够提高代码的质量,体现在(1)类型利于代码重构,编译器可以在编译时而非运行时捕获异常;(2)类型声明提高代码可读性,通过接口、参数签名等形式构成代码文档的一部分;另外,这个类型系统具备类型推断的能力,为代码提供轻便的类型安全保证。最后,随着社区的发展,越来越多的第三方 JavaScript 库都提供了 TypeScript 类型声明文件,从而帮助我们开发者获得更好的代码智能提示和类型安全。 但也因为其类型安全的特性,存在着以下缺点:(1)它不具备运行时的类型检查能力,所以诸如 API 响应的数据结构等运行时发生类型变化这些问题是无法检测出来的,这也使得我们不得不额外在 React 引入 PropTypes 等运行时的类型检测机制,而这样的工作明显是重复的;(2)有时我们需要利用动态类型的特点在运行时改变数据结构,此时引入类型系统,可能导致类型声明过于复杂而降低了代码的可读性,甚至会遇到类型系统所提供的声明能力无法满足特殊数据结构的类型声明。 ### 十、引用计数的工作原理与优缺点 引用计数法是通过给每个对象添加引用计数器来回收那些计数器归零的对象。具体来说,当对象被引用一次时,计数器增加 1,当该引用解除时,计数器减少 1;如果计数器归零,则说明对象不再被使用,那么垃圾回收就可对其进行处理。用程序表示如下: ```js // 假设每个对象在内存中用链表的节点表示如下 interface ObjectNode { // 对象的值 value: any; // 对象的计数器 refCount: number; // 对象所引用的另一个对象 nextObj?: ObjectNode; } // 假设有一个根对象,存储内存中的可达对象 const Root = { objects: [] } // 假设有一个空闲对象,存储了可用的空闲内存块 const Free = { objects: new Array(SIZE) } // 创建一个对象时 // 1. 从空闲对象中获取一个对象空间 // 2. 为对象添加计数器并置为 1 function create(data) { const obj = Free.objects.shift(); obj.value = data; obj.refCount = 1; return obj; } // 释放一个对象时 // 1. 从根对象移除该对象 // 2. 把对象添加到空闲区域 function reclaim(obj) { remove(Root.objects, obj); push(Free.objects, obj); } // 将一个对象 src 的引用指向另一个对象 dest 时 // 1. dest 计数器递增 1 // 2. 当前所引用的对象 src 计数器递减 1 // 3. 更新指针,使 src 的引用指向 dest function updateRef(src, dest) { incRefCount(dest); decRefCount(src.nextObj); src.nextObj = dest; } // 对象的计数器增加 1 function incRefCount(obj) { obj.refCount++; } // 对象的计数器减少 1 // 当计数器归零时,与其关联的对象都应递减 1 function decRefCount(obj) { obj.refCount--; if (obj.refCount > 0) { return; } let ptr = obj.nextObj; while (ptr !== null) { decRefCount(ptr); ptr = ptr.nextObj; } reclaim(obj); } ``` 引用计数法的优点如下: - 可立即回收垃圾,因为当对象计数器归零时会立即回收 - 最小的程序暂停时间,因为其立即回收的特性,使得回收所需的开销被分散到整个程序的运行期间,程序也就无需为了回收而挂起,从而最大程度地减少了程序的暂停时间 - 无需沿指针查找需要回收的对象,因为需要释放的对象会立即把添加到空闲区域,而不需要沿着指针寻找需要回收的对象 - 实现成本低,因为其核心算法是对计数器进行操作与判断 其缺点如下: - 无法回收循环引用的对象,这种情况下容易造成内存泄漏 - 回收所需的开销大,特别是引用较多的对象,相关联的计数器的更新频次较多 - 空间利用率较低,因为计数器本身会占用固定的内存空间 ### 十一、标记整理算法的工作流程 标记整理算法分为两个阶段:标记和整理。 在标记阶段,递归式地从根(如全局对象和当前活动函数)出发标记出内存中所有的存活对象;在整理阶段,把所有存活对象依次按顺序向一端移动,然后把移动之后的末端内存地址的后续空间全部回收。 ### 十二、V8 新生代存储区垃圾回收流程 V8 新生代存储区在 32 位系统下占用 16MB 内存空间,在 64 位系统下占用 32MB 内存空间。新生代存储区内的对象都是生存周期较短的,所以该区域的垃圾回收频次也较高,其回收算法则选用了“空间换时间”的 Scavenge 算法: 1. 把新生代存储区划分为两个空间相等的区域,分别定义为 From 空间和 To 空间 2. 在 From 空间中为对象分配空间,此时 To 空间未存储任何对象 3. 垃圾回收器开始第一轮回收,找出没有引用的对象,同时把其他对象复制到 To 空间去,此时 To 空间只存在活跃对象 4. 垃圾回收器清空 From 空间的全部内存,此时新生代存储区中只有 To 空间存在着活跃对象 5. 交换 To 空间和 From 空间,此时新生代存储区只有 From 空间存储了对象,To 空间则为空 6. 垃圾回收器进入下一轮回收,继续从 From 空间找出没有引用的对象,同时把其他对象复制到 To 空间去,接着清空 From 空间,再交换 From 空间与 To 空间,然后进入下一轮回收 垃圾回收器如果发现经过多轮复制却依然活跃的对象时,会视其为生命周期较长的对象,于是将其移动到老生代存储区中。此外,如果从 From 空间复制一个对象到 To 空间时,To 空间的使用率超过 25%,则直接把该对象移动到老生代存储区中。我们把对象从新生代移动到老生代的过程,称为对象的晋升。 ### 十三、增量标记算法在何时使用及工作原理 增量标记算法是在对老生代区域进行垃圾回收时使用。相比于需要暂停程序来执行垃圾回收的算法,增量式标记算法实现了并发式的回收垃圾过程,即允许垃圾回收与应用程序相互交叉执行,从而降低垃圾回收所需的暂停时间。 V8 采用了三色标记算法来实现增量标记算法。所谓的三色分别为白色、灰色和黑色,它们分别对应了对象的不同状态:白色是最初所有对象的颜色,即垃圾收集器未发现的对象;当垃圾收集器发现一个对象时就会将其标记为灰色;垃圾收集器如果访问并标记了灰色节点的全部子节点,则把该灰色节点设置为黑色。通过这样的标色流程之后,只要内存中的对象没有灰色节点,就意味着标记完成,那么垃圾收集器就可以开始工作,然后把全部的白色节点清除即可。 然而,标记的过程是并发执行的,应用程序可能在经过一次标色之后把白色节点添加到已完成扫描的黑色节点去,而后这个白色节点又被其他已扫描过的节点所引用,那么这个白色节点就无法被重新标色了。为此 V8 实现了 Dijkstra 所定义的三色不变性,即强制要求不能让黑色节点指向白色节点。每当引用发生变化时,立即对被引用的节点进行着色,即白的立即染灰,灰色和黑色保持不变。通过这种写屏障,保证垃圾回收器不会回收掉活跃对象。