这几道 JavaScript 的简答题你都会了吗?

268 阅读8分钟

一、请说出下列最终执行结果,并解释为什么

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.

整个表格看下

特性varletconst
是否绑定全局作用域
是否块级作用域
是否可以重复声明

五、请说除下列代码的最终输出结果,并解释为什么

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;obj2obj 其实都指向了 { a: 1, b: 2 },并且 引用类型 相比较时,比较的是 内存地址是否相同,所以此处打印 true

当执行 obj2.a = 999 后 因为 obj2obj 都指向了同一个对象。所以 obja 属性也是 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 时,可以立即回收空间
  • 无法回收循环引用的对象

十一、描述标记整理算法的工作流程

说到 标记整理算法 不得不 说下 标记清除算法

标记清除算法

  1. 标记阶段: 首先递归去寻找 可达对象,再对可达对象进行标记,
  2. 清除阶段: 然后再去把没有标记的对象进行回收。并且会清除上一轮的标记。

但是这些垃圾对象再内存中的位置不是连续的。回收后不便于程序再次申请使用。所以有 标记清除算法 的 Pro 版本

标记整理算法

  1. 标记阶段:与 标记清除算法一致 首先递归去寻找 可达对象,再对可达对象进行标记,
  2. 清除阶段:先执行整理,移动对象的位置。让对象在地址上尽可能是连续的,然后再去把没有标记的对象进行回收

十二、描述 V8 新生代存储区垃圾回收的流程

在 V8 中 GC 回收策略采用 分代回收的策略,称为 新生代老生代,并且对此采用了不同回收算法。

在新生代中又被分成了 2 块等大小的空间 我们分别对其称为 fromto。 当程序运行时,空间使用的是 from 空间,而to 空间是空闲的,当 from 空间使用到一定程度时将会触发 GC 机制,即对 from 空间使用标记整理算法对 活动对象 进行标记和整理,再将from 空间整理出来的 活动对象 完整的拷贝到 to 空间。因为 to 空间 已经有了 from 空间中的活动对象,所以只需要把 from 空直接进行释放即可。再然后fromto 进行交换。就完成了 新生代 空间 的回收操作。

  1. from 空间的 活动对象 进行标记整理
  2. 再将整理过的 活动对象 完整拷贝到 to 空间
  3. 直接释放 from 空间
  4. 交换 from 空间 和 to 空间
  5. 完成新生代的垃圾回收

是不是有种 空间换时间 的骚操作


十三、描述增量标记算法在何时使用及工作原理

首先我们要知道在执行 GC 操作时,我们的 JavaScript 是会暂停执行的。如果 GC 的时间太长。那我们的 JavaScript 阻塞的时间就会更长。太长时间的 阻塞 肯定是不行的。这时就需要采用 增量标记算法 来对 GC 操作进行优化了

增量标记算法 到底是个啥呢?

它就是将一整段的 GC 操作,拆分成多个小步骤,组合着去完成当前的整个回收操作,用这种拆分的方式去替代直接进行一整段的垃圾回收操作。从而达到 JavaScript 运行 和 GC 操作的交替执行。

好处:使用这种方式。可以让之前一整段的垃圾回收操作分成了更小段,程序暂停的时间段也变小了,这样对与用户来说体验也会更好!


纸上得来终觉浅 绝知此事要躬行