序言
本篇在拥有地基的情况下,开始进行搬砖工作,了解 js 相关的一些基础知识,包括原型、数据类型、以及执行相关问题,为之后的前端大厦打下基础。
一、原型
1.1 原型对象
在现代浏览器中,函数本身拥有 prototype 属性,当用函数作为构造函数构建实例后,该属性指向一个对象,这个对象包含创建的所有实例共享的属性和方法,同时,创建的实例中也会有 __proto__ 属性指向构造函数的该对象,这个对象就是原型对象。
1.2 原型链
当 js 对象无法寻找到对应的属性或方法时,都会通过 __proto__ 向其构造函数的原型对象上查找,直到找到源头 Object 对象为止,形成了一个原型指向的链条,就是原型链。
二、数据类型
2.1 堆与栈
数据类型与堆栈息息相关,首先了解一下堆栈概念。 - 数据结构中,栈是先进后出的结构,而堆是优先队列的结构 - 操作系统中,栈内存由编译器自动分配释放,堆内存由程序员分配释放
2.2 基本数据类型
直到现在的 ES10 版本,基本数据类型共七种:null, undefined, boolean, string, number, symbol, bigint,都存放在栈中。
2.2.1 symbol
symbol 是在 es6 标准中提出的,主要解决对象中键名重复的问题
2.2.2 number
typeof NaN
结果是 number,代表不是一个有效数字
isNaN 与 Number.isNaN
前者会存在一个问题,会将参数转换为数值,而不能转换的都会返回 true,比如 'a',这样会存在问题。解决方法就是使用 Number.isNaN,这种方式会先确认是不是数字,然后判断 isNaN,更加准确。
2.2.3 bigint
bigint 是在 es10 标准中提出,解决大数据(值超过 2^53 - 1)超出安全范围的问题
2.2.4 undefined
与 undeclared 区别
- undefined 代表变量未定义,不同于 undeclared 的未声明,后者引用会报错,而前者不会
- undefined 可以引用, undeclared 引用会报 引用错误
与 null 区别
- undefined 与 null 都是基本数据类型,null 代表空对象
- null 是保留字,不能作为变量名,而 undefined 可以
- typeof,undefined 是 'undefined',而 null 是 'object'
获取安全的 undefined 值
void(0),void __ 没有返回值,因此返回结果为 undefined
2.2.4 null是对象吗
不是。typeof null 的结果是 'object',这里是一个历史问题。以 000 开头的是对象,然而 null 本身就是全 0,导致 typeof 判断错误
2.2.5 boolean 与 假值对象
浏览器创建的外来值,就是‘假值对象’,比如 document.all,这种值与普通对象很像,但是强制转换为布尔值结果为 false
2.3 复杂数据类型
复杂数据存在着疑问,一般认为泛指 Object,但是在 typeof function 的情况下,返回值是 'function',所以我觉得 应该包括Object 和 Function,它们存在堆中。比较值得注意的是,闭包变量应该也存在堆中,我们后面讨论。
2.3.1 内置对象 [[class]]
对象在没有自定义 toString() 方法的情况下,执行 toString() 会输出内置对象 [[class]]的值,比如数组输出[Object Array]
2.4 闭包
闭包有很多版本的解释,我采用红宝书中的定义:闭包是指有权访问另一个函数作用域中的变量的函数。 当我们在一个函数A中常见另一个函数B,B能访问到A的局部变量,形成了闭包。这样做有什么好处呢: - 可以通过外部调用闭包函数,访问到函数内部的局部变量,这种局部变量就类似于私有变量供使用了 - 已经结束运行的函数上下文中的变量依旧留在内存中,闭包保留这个变量对象的引用,避免变量对象回收 通过第二条,就会发现一个很有意思的现象,闭包函数执行完,仍然保留变量对象,该对象的基本数据类型值依然留在内存中,不符合栈中数据在函数执行结束后就销毁的特性。之所以会这样,是因为闭包保留着对变量的引用。所以闭包变量存放在堆中。
2.5 数据转换
2.5.1 其他值转为字符串的转换规则
- null 和 undefined 类型,转换为 'null', 'undefined'
- boolean 类型,转为 'true' 和 'false'
- number 类型,直接转为字符串形式
- Symbol 类型 可以显式强制类型转换,隐式转换会报错
- 普通 Object,首先查看有无自定义 toString() 方法,有的话就调用并使用返回值,没有就调用 toString() 方法,返回内部属性 [[class]]
2.5.2 其他值转为数值的转换规则
- undefined 转为 NaN
- null 转为 0
- boolean,true 为 1,false 为 0
- string,空为 0,有非数字字符就返回 NaN,否则转为数字
- Symbol 不能转为数字
- Object,会先进行抽象操作
ToPrimitive,首先检查 valueOf() 方法,如果返回基本类型就再转化,否则查看 toString() 方法,如果返回基本类型就再转化,否则会报错
2.5.3 其他值到布尔类型转换
- undefined
- null
- false
- +0, -0,NaN
- ''
- 假值对象 以上为false,其他为真值
2.5.4 == 操作符的强制类型转换规则
- 字符串与数字比较,字符串转为数字
- 其他类型和布尔类型比较,现将布尔值转为数字,再引用规则
- null 和 undefined 相等,但与其他值不相等
- 对象与非对象比较,对象先调用 ToPrimitive 抽象操作后在比较
- NaN 不等于任何值,包括本身
- 两个对象比较,如果指向同一对象,则相等,否则不等
2.5.5 +操作符拼接
当其中一个操作数是字符串,或者能够通过(ToPrimitive)抽象操作为字符串的对象,则进行拼接,否则执行加法。
三、执行
3.1 V8 垃圾回收
3.1.1 V8 内存限制
虽然现在电脑的内存很大,但在64 位系统中,V8 也只是分配到1.4G内存,那么是什么原因使得 V8 内存限制,这么少的内存又是如何支撑起浏览器运行的呢?
- 首先,基于 js 单线程以及 js 垃圾回收机制的限制,不需要太大的内存;
- 其次,V8 采用限制堆内存,解决垃圾回收耗时的问题
下面,看看 V8 是如何操作的。
3.1.2 垃圾分类
现在深圳也开始进行垃圾分类了(鼓掌),那么v8是如何分类的呢:V8 将内存分为新生代内存和老生代内存。让我们看看这里面卖的什么药。
新生代内存回收
新生代内存是临时分配的内存,存放时间短。
64 位系统下,新生代内存仅占 32MB,它通过 From 与 To 两部分倒腾,实现小块内存处理新生代内存回收:
1.首先正在使用的内存放在 From 区,目前闲置的内存放在 To 区。
2.进行垃圾回收时,V8 检查 From 区的对象,将存活对象复制到 To 区,非存活对象回收。
3.倒腾完之后,From 与 To 互换。
To 区接收 From 区产生的内存碎片问题
To 区采用 Scavenge 算法,将零散的内存整合为连续的内存,这样能够方便后面内存的分配,解决碎片问题
老生代内存回收
老生代内存存活时间长,通过新生代内存晋升形成,形成方式为:
1.经历过一次 Scavenge 回收
2.To空间超过 25% 的内存
老生代采用增量标记法来完成,分为两步
1.进行标记-清除:遍历对重的所有对象,将其中一部分做上标记,然后对代码中使用的变量
以及强引用的变量取消标记,在随后的清除阶段对标记的变量进行空间回收
2.整理内存碎片,将存活对象全部往一端靠拢。接着暂停回收,执行js逻辑,然后再进行第一步
这样做的好处是比原本全部标记的方法减少到 1/6 的时间。
3.2 事件循环
浏览器中将任务分为宏任务和微任务。
3.2.1 宏任务
setTimeout、setInterval、setTmmediate(只兼容ie)
3.2.2 微任务
then 、messageChannel 、mutationObersve
3.2.3 执行过程
1. 所有代码作为一个宏任务执行
2. 执行过程中同步代码执行,宏任务进入宏任务队列,微任务进入微任务队列
3. 宏任务执行完出队,然后检查微任务队列,有则依次执行,直至为空
4. 执行浏览器 UI 线程的渲染工作
5. 检查是否有 web worker 任务,有则执行
6. 执行队首新的宏任务,接着第二步,直至所有宏任务与微任务队列为空