前端面试
准备出去面试找找自己的不足,为了在面试时掌握主动权,所以写这篇博客回顾前端知识点。
JavaScript 有几种数据类型,分别是怎么存储的,如何判断一个数据是什么类型的?
在 JavaScript 中有两种数据类型-基本数据类型和引用数据类型,基本数据类型现阶段有 boolean,number,string,null,undefined,symbol 6 种,基本数据类型存储的是值。除了基本数据类型,其它的全部都是引用数据类型,引用数据类型存储的是地址(指针),当我们创建一个引用数据类型时,JS 引擎会帮我们在堆中开辟一块空闲区域,引用数据类型此时存储的就是这块区域对应的地址值。
判断数据的类型可以使用 typeOf
1 | typeof 1 // 'number' |
typeof 和 instanceof 有什么区别? instanceof 是怎么用的,其原理是什么?
根据上面的代码我们可以看出,typeof 能够判断绝大部分数据类型,但是 typeof null 时返回的却是 object。这算是 JavaScript 一个悠久的 bug 了,在最初的 JavaScript 中采用的是 32 位系统,为了性能的考虑,采用了低位存储数据的类型,000 表示的数据类型是 object, 但是 null 表示的却是全 0,所以 typeof 错误的将 null 判断为 object。
然而用 instanceof 来判断的话
1 | null instanceof null |
可以看出因为 null 不是一个 object,所以判断的时候报错,其它基本数据类型判断时同样会报错。
但是如果这么写
1 | let s = new String('123') |
所以 instanceof 主要是判断数据是哪一种具体的 object, 其实 instanceof 主要的作用就是判断一个实例是否属于某种类型,当然 instanceof 也可以判断一个实例是否是其父类型或者祖先类型的实例。
1 | let Parent = function () { |
这是 instanceof 的用法,根据 instanceof 的用法,我们可以大致推断出 instanceof 的代码实现:
1 | function new_instanceof (leftValue, rightValue) { |
其实 instanceof 的实现主要是通过原型链来判断的,掌握了原型链,instanceof 就很简单了。
那么现在问题来了,instanceof 是否能够准确判断呢?
现在来看这么一段代码:
1 | function Parent1 () {} |
what? 发生了什么,两次输出的结果不一样,因为原型链是可以改变的,所以 instanceof 的判断也是不准确的。
其实判断数据类型还有一种方法,那就是 Object.prototype.toString 方法,我们可以利用这个方法对数据类型做一个比较精确的判断
1 | Object.prototype.toString.call('1') |
JS 类型转换
类型转换一直都是 JavaScript 中比较复杂的一部分,但是熟悉了规则就不需要惧怕了。
- 转 boolean 类型
- 转 number 类型
- 转 string 类型
转换规则参考下表:
| 原始值 | 转换目标 | 结果 |
|---|---|---|
| number | boolean | 除了 0,-0,NaN 全为 true |
| string | boolean | 除了空字符串都为 true |
| null, undefined | boolean | false |
| Symbol, 引用类型 | boolean | true |
| number | string | 1 -> ‘1’ |
| null,undefined | string | ‘null’,’undefined’ |
| boolean | string | true -> ‘true’ |
| symbol | string | Cannot convert a Symbol value to a string |
| 引用类型 | string | 如果对象有 toString() 方法,就调用 toString() 方法。如果该方法返回原始值,就讲这个值转化为字符串。如果没有,就会调用该对象的 valueOf() 方法。存在就调用这个方法,如果返回值是原始值,就转化为字符串。否则就报错。 |
| string | number | ‘1’ -> 1,’100’ -> 100, ‘a’ -> NaN |
| boolean | number | false -> 0,true -> 1 |
| Symbol | number | TypeError: Cannot convert a Symbol value to a number |
| null | number | 0 |
| undefined | number | NaN |
| 数组 | number | 空数组转为 0,存在一个元素并且是数字转数字,其它情况 NaN |
| 除了数组以外的引用数据类型 | number | NaN |
对象转原始类型
对象转原始类型的时候,会调用内置的[[ToPrimitive]]函数,该函数判断逻辑简化来说就是:
- 如果已经是原始类型了,就不在进行转换
- 如果需要转成字符串类型就调用 toString 方法,如果能转换为基本类型的话就返回转换后的值。不是字符串类型就先调用 valueOf()方法,如果结果是基础类型的话就直接返回,不是基础类型再去调用 toString()方法。
- 如果都没有返回原始类型的值就会报错。
当然也可以重写 ToPrimitive 方法,该方法在调用时优先级最高。
四则运算符
四则运算在面试题中经常出现,我们分别来解析。
首先加法运算符和其它的都不一样。它主要有下面几个特点:
- 运算符中如果有一方为字符串,那么就把另外一方也转换为字符串。
- 运算符的一方不是字符串或者数字,就把它转换成字符串或者数字。
1 | 1 + [1,2,3] // 11,2,3 |
另外加法中还有这样的一个运算
1 | 'a' + + 'b' // aNaN |
这是因为+ ‘b’会被转换成数字, 结果为 NaN, 所以最终输出为’aNaN’
除了加法运算符,其它的运算符都是只要一方为数字,就把另一方转换为数字。
V8 下的垃圾回收机制
V8 实现了准确式 GC, GC 算法采用了分代式垃圾回收算法,将堆内存分成了新生代和老生代。
新生代算法
新生代中存活的时间比较短,采用 Scavenge GC 算法。
在新生代中,内存空间分成两部分,分为 From 空间和 To 空间,一般大小是 8:2, 在这两个空间中,From 空间是被使用的,而 To 空间是空闲的,一个对象创建后就会被放入 From 空间,当 From 空间被占满后,此时就启用了新生代 GC 算法,算法会检测 From 空间中存活的对象并复制到 To 空间,如果对象失活了就释放空间,复制完成之后将 From 空间和 To 空间互换,这样 GC 就结束了。
老生代算法
老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。
在讲算法前,先来说下什么情况下对象会出现在老生代空间中:
- 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
- To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。
老生代中的空间很复杂,有如下几个空间
1 | enum AllocationSpace { |
在老生代中,以下情况会先启动标记清除算法:
- 某一个空间没有分块的时候
- 空间中被对象超过一定限制
- 空间不能保证新生代中的对象移动到老生代中
在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行。