一、请说出下列最终执行结果,并解释为什么
var a = [];
for (var i = 0; i < 10; i++)
a[i] = function () {
console.log(i);
};
a[6](); // log -> 10
咦。 为什么是 10 。不应该是 6 呢。其实很简单。我们把代码重新改造一下就看能出来了
var a = []; // ..., ..., ...,..., ..., function() {console.log(i)}, ..., ..., ...,
var i;
for (i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
const fn = a[6]; // function() {console.log(i)}
fn(); // (1)
我们都知道 var
定义变量是会绑定到全局的,就如改造后的代码一样。 当循环执行完之后,在 a
数组中存放了 10 个打印 i
的 方法(函数) 当代码执行到 标注(1) 位置 fn
时。这时打印 i
已经是 代码最外层的 提升过的 var i
了。通过 循环体的 践踏后 var i
已经变成 10 。所以打印了 10
var 关键值 是有是会绑定到全局的,在日常开发中应减少甚至不用 var 关键字
用 var 声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,或者对于声明在任何函数外的变量来说是全局 MDN
二、请说出下列最终执行结果,并解释为什么
var tmp = 123;
if (true) {
console.log(tmp); // ReferenceError: Cannot access 'tmp' before initialization
let tmp;
}
当变量存在于 块级作用域 时 优先使用 块级作用域 中的变量 此处的 console.log(tmp)
找到块级中有一个 tmp
,但是此 tmp
定义在使用后,所以 报错 初始化前无法访问 错误。
其实这个地方还看出一个东西,那就是这里的
let
也是有 "变量提升" ! 不信你品。let tmp
处于打印之后,按代码执行流程的话 那console.log(tmp);
的tmp
应该 去取var tmp = 123;
才对, 但是却报出错误ReferenceError: Cannot access 'tmp' before initialization
, 这就说明let tmp;
在这里是可以"访问"到。只不过是无法访问到 /笑哭。这 T* 是不是有点矛盾啊 ?? 我的想法可能不是完全正确,但我觉得 只有 var 才有 "变量提升" 的说法是可以再 推敲推敲 的。感谢兴趣的朋友去百度百度
三、结合 ES6 新语法,用最简单的方式找出数组中的最小值
var arr = [12, 34, 32, 89, 4];
var arr2 = [12, 34, 32, 89, 4];
const result1 = arr.sort((a, b) => a > b).pop();
const result2 = Math.min(...arr2);
console.log(result1, result2); // 4 4
四、请详细说明 var let const
三种声明方式之间的具体差别
var:JavaScript 最古老的定义变量方式。可在定义前使用。在任何地方定义都将绑定到全局
// 可在定义前使用
console.log(val); // undefined
var val;
// 示例2
console.log(val); // 100
var val = 100;
// 在任何地方定义都将绑定到全局
console.log(val2); undefined
if (true) {
var val2 = 99;
}
console.log(val2); 99
}
let:ES6 (ECMAScript 2015) 新定义变量方式,块级作用域。不会绑定到全局
// 不可在定义前使用
console.log(val); // Cannot access 'val' before initialization
let val;
// 示例2
console.log(val); // Cannot access 'val' before initialization
let val1 = 100;
// 不会绑定到全局 块级作用域
console.log(val2); //val2 is not defined
if (true) {
let val2 = 99;
}
console.log(val2); // val2 is not defined
const:ES6 (ECMAScript 2015) 定义变量时必须初始化值。此变量之后不可再次被赋值,其他特性与 let
相同
// 定义变量时必须初始化值
const name = 1;
console.log(name); // Missing initializer in const declaration
const name2 = "tom";
console.log(name2); // tom
// 不可再次被赋值
const name3 = "tom";
console.log(name3); // tom
name3 = "Baboon"; // Assignment to constant variable.
注意
const
如果初始化的是一个对象(引用类型),该对象的 成员(属性) 可以更改
const obj = { a: 1, b: 2 }; obj.a = 99; // 可以 obj = { c: 3, d: 4 }; // 报错 Assignment to constant variable.
整个表格看下
特性 | var | let | const |
---|---|---|---|
是否绑定全局作用域 | 是 | 否 | 否 |
是否块级作用域 | 否 | 是 | 是 |
是否可以重复声明 | 是 | 否 | 否 |
五、请说除下列代码的最终输出结果,并解释为什么
var a = 10;
var obj = {
a: 20,
fn() {
setTimeout(() => {
console.log(this.a);
});
},
};
obj.fn(); // 20
头发不多同学应该都知道 谁是调用者 this 就指向谁 但是这里有个 箭头函数 混淆视听。不过只要记住 箭头函数 跟 this
一点关系也没有。箭头函数 也没有 this
。问题也就迎刃而解了。
愿天下再无 this 问题
六、简述 Symbol
类型的用途
Symbol 是 ES6 中引入的新数据类型,它表示一个唯一的常量,通过 Symbol 函数来创建对应的数据类型,创建时可以添加变量描述,该变量描述在传入时会被强行转换成字符串进行存储。
根据 Symbol
的特性 最好的用途就是 私有对象属性 和 常量值
- 私有对象属性
const privateMethod = Symbol();
this[privateMethod] = (v) => v;
当需要屏蔽外部访问某个属性时可以使用这种方式
- 常量值
const KEY = {
mayun: Symbol(),
mahuateng: Symbol(),
leijun: Symbol(),
};
function getValue(key) {
switch (key) {
case KEY.mayun:
return "111";
case KEY.mahuateng:
return "222";
case KEY.leijun:
return "333";
}
}
getValue(KEY.baidu);
在有些 不关心值本身只关心值的唯一性时场景 可以使用这种方式
七、说说什么是浅拷贝,什么是深拷贝
首先这两个 “说法” ,说的是 引用类型 的拷贝
浅拷贝:引用拷贝
const obj = { a: 1, b: 2 };
const obj2 = obj;
console.log(obj === obj2); // true
obj2.a = 999;
console.log(obj.a); // 999
从上面代码可以看出。这是很显然的浅拷贝。
当执行 const obj2 = obj;
后 obj2
,obj
其实都指向了 { a: 1, b: 2 },并且 引用类型 相比较时,比较的是 内存地址是否相同,所以此处打印 true
。
当执行 obj2.a = 999
后 因为 obj2
,obj
都指向了同一个对象。所以 obj
的 a
属性也是 999
深拷贝:对象拷贝
JSON.parse(), JSON.stringify() 方式
缺点:无法拷贝 undefined , function, RegExp 等类型
const obj = { a: 1, b: 2 };
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj === obj2); // false
obj2.a = 999;
console.log(obj.a); // 1
console.log(obj2.a); // 999
Object.assign({}, obj)方式
缺点:无法拷贝多层对象
const obj = { a: 1, b: 2, c: { d: 3 } };
const obj2 = Object.assign({}, obj);
console.log(obj === obj2); // false
obj2.a = 999;
console.log(obj.a); // 1
console.log(obj2.a); // 999
obj2.c.d = 88;
// obj 和 obj2 的 c 属性 指向同一个对象
console.log(obj.c.d); // 88
console.log(obj2.c.d); // 88
八、请简述 TypeScript 与 JavaScript 之间的关系
TypeScript 的 JavaScript 超集,TypeScript 扩展了 JavaScript
简单说 TypeScript 包含了 JavaScript
九、请谈谈你所认为的 TypeScript 优缺点
优点
- 微软大法真香,与自家的 vs code 强强联合
- 提高了代码可读性和可维护性
- 提供强大并丰富类型系统。在编译时即可检查出常见错误
- 容易上手
- 语法糖,一些还未被标准化的特性也可使用
缺点
- 缺点,有啥缺点!快给我学。都 2020 年了 再不学 TypeScript Plus 都出来了
十、描述引用计数的工作原理和优缺点
工作原理
通过引用计数器 去保存 代码在运行时 对象的 引用数
只要一个对象被引用 如 const obj = { a: 1, b: 2 };
引用计数器 就会增加
当一个对象移除引用时 引用计数器 就会减少
当某个对象的引用一个都没有时,也就是 引用计数器 为 0 时,GC 将会工作回收这个对象
优缺点
- 因为采用引用计数方式,所以当发现 引用数为 0 时,可以立即回收空间
- 无法回收循环引用的对象
十一、描述标记整理算法的工作流程
说到 标记整理算法 不得不 说下 标记清除算法
标记清除算法
- 标记阶段: 首先递归去寻找 可达对象,再对可达对象进行标记,
- 清除阶段: 然后再去把没有标记的对象进行回收。并且会清除上一轮的标记。
但是这些垃圾对象再内存中的位置不是连续的。回收后不便于程序再次申请使用。所以有 标记清除算法 的 Pro 版本
标记整理算法
- 标记阶段:与 标记清除算法一致 首先递归去寻找 可达对象,再对可达对象进行标记,
- 清除阶段:先执行整理,移动对象的位置。让对象在地址上尽可能是连续的,然后再去把没有标记的对象进行回收
十二、描述 V8 新生代存储区垃圾回收的流程
在 V8 中 GC 回收策略采用 分代回收的策略,称为 新生代, 老生代,并且对此采用了不同回收算法。
在新生代中又被分成了 2 块等大小的空间 我们分别对其称为 from
和 to
。 当程序运行时,空间使用的是 from
空间,而to
空间是空闲的,当 from
空间使用到一定程度时将会触发 GC 机制,即对 from
空间使用标记整理算法对 活动对象 进行标记和整理,再将from
空间整理出来的 活动对象 完整的拷贝到 to
空间。因为 to
空间 已经有了 from
空间中的活动对象,所以只需要把 from
空直接进行释放即可。再然后from
和 to
进行交换。就完成了 新生代 空间 的回收操作。
- 对
from
空间的 活动对象 进行标记整理 - 再将整理过的 活动对象 完整拷贝到
to
空间 - 直接释放
from
空间 - 交换
from
空间 和to
空间 - 完成新生代的垃圾回收
是不是有种 空间换时间 的骚操作
十三、描述增量标记算法在何时使用及工作原理
首先我们要知道在执行 GC 操作时,我们的 JavaScript 是会暂停执行的。如果 GC 的时间太长。那我们的 JavaScript 阻塞的时间就会更长。太长时间的 阻塞 肯定是不行的。这时就需要采用 增量标记算法 来对 GC 操作进行优化了
那 增量标记算法 到底是个啥呢?
它就是将一整段的 GC 操作,拆分成多个小步骤,组合着去完成当前的整个回收操作,用这种拆分的方式去替代直接进行一整段的垃圾回收操作。从而达到 JavaScript 运行 和 GC 操作的交替执行。
好处:使用这种方式。可以让之前一整段的垃圾回收操作分成了更小段,程序暂停的时间段也变小了,这样对与用户来说体验也会更好!
纸上得来终觉浅 绝知此事要躬行