JS
# 1. 判断方式
- 两个等号会进行类型转换。
- 对象 == 对象:比较地址。
- 对象 == 字符串:对象变为字符串。
- NaN == NaN:和自己及其他值都不相等。
- null == undefined:在 == 相等,=== 不相等,并和其他值都不相等。
- 剩下情况,只要两边类型不同,最后都转为数字。
{} == {} // => false ({}) == '[object Object]' // => true NaN == NaN // => false null == undefined // => true null == '' || null == 0 || null == false // => false 0 == '\n' || 0 == ' ' // true // 拆封调用 toString() 方法 'abc' == Object('abc') // => true // null 与 undefined 不能够被封装,Object(null) 和 Object(undefined) 返回空对象({}) null == Object(null) || undefined == Object(undefined) // => false // 转换过程:('' -> 0 ) == ([null] -> '' -> 0) '' == [null] // true // 转换过程:([] -> '' -> 0 ) == (![] -> false -> 0) [] == ![] // true // 转换过程:({} -> '[object Object]' -> NaN ) == (!{} -> false -> 0) {} == !{} // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18- 三个等号基本数据类型比较值,复杂数据类型比较内存地址(用的最多)。
存在 -0 和 +0 比较为 true,NaN 和 NaN 比较为 false 的情况。
-0 === +0 // => true NaN === NaN // => false
1
2- Object.is() 最准确的比较方式,但效率不高,主要用来解决三个等号的缺陷(NaN 可用 Number.isNaN() 方法判断)。
Object.is(-0, +0) // => false Object.is(NaN, NaN) // => true
1
2
# 2. 数据类型检测
- typeof 基于二进制值的前三位进行检测,检测对象、数组、正则、日期、null 等均为 'object',可用于检测除 null 以外的基本数据类型。
JS 设计时,二进制值前三位都是 0 就会被判断为对象,而 null 的二进制值都是 0,所以 typeof null 为 'object'。
typeof 7 // => 'number' typeof '7' // => 'string' typeof true // => 'boolean' typeof null // => 'object' typeof undefined // => 'undefined' typeof NaN // => 'number' typeof Symbol(7) // => 'symbol' typeof 7n // => 'bigint' typeof function foo() {} // => 'function' typeof [] // => 'object' typeof {} // => 'object' typeof /7/ // => 'object' typeof new Date() // => 'object' typeof new Set() // => 'object' typeof new Map() // => 'object'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15- instanceof 检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上(foo instanceof Bar 为检测 foo.__proto__ === Bar.prototype),不能检测基本数据类型,检测复杂数据类型是否属于 Object 均为 true。
因开发人员可以自行修改原型链,因此此方式也是较为不安全的。
7 instanceof Number // => false '7' instanceof String // => false true instanceof Boolean // => false NaN instanceof Number // => false Symbol(7) instanceof Symbol // => false 7n instanceof BigInt // => false (function foo () {}) instanceof Function // => true [] instanceof Array // => true ({}) instanceof Object // => true /7/ instanceof RegExp // => true new Date() instanceof Date // => true new Set() instanceof Set // => true new Map() instanceof Map // => true [function () {}, {}, [], /7/, new Date(), new Set(), new Map()].every(i => i instanceof Object) // => true // 更改原型链指向 function foo() {} foo.prototype = Object.create(Array.prototype) new foo() instanceof Array // => true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20- constructor 为原型链上构造函数的属性(obj.__proto__.constructor),指向构造函数本身。不能检测 undefined 和 null。
因开发人员可以自行修改原型链中的属性,因此此方式也是较为不安全的。
7..constructor === Number // => true '7'.constructor === String // => true true.constructor === Boolean // => true NaN.constructor === Number // => true Symbol(7).constructor === Symbol // true 7n.constructor === BigInt // true (function foo () {}).constructor === Function // => true [].constructor === Array // => true ({}).constructor === Object // => true /7/.constructor === RegExp // => true new Date().constructor === Date // => true new Set().constructor === Set // => true new Map().constructor === Map // => true // 更改构造函数中的 constructor 属性 function foo() {} foo.prototype.constructor = Array new foo().constructor === Array // => true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18- 通过 Object.prototype.toString.call() 获取对象的类型(最全面的检测方式)。
const toString = {}.toString toString.call(7) // => '[object Number]' toString.call('7') // => '[object String]' toString.call(true) // => '[object Boolean]' toString.call(null) // => '[object Null]' toString.call(undefined) // => '[object Undefined]' toString.call(NaN) // => '[object Number]' toString.call(Symbol(7)) // => '[object Symbol]' toString.call(7n) // => '[object BigInt]' toString.call(function foo() {}) // => '[object Function]' toString.call([]) // => '[object Array]' toString.call({}) // => '[object Object]' toString.call(/7/) // => '[object RegExp]' toString.call(new Date()) // => '[object Date]' toString.call(new Set()) // => '[object Set]' toString.call(new Map()) // => '[object Map]' function Foo() {} toString.call(new Foo()) // => '[object Object]'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19封装类型检测方法。
const detectionType = value => typeof value === 'object' ? {}.toString.call(value).slice(8, -1).toLowerCase() : typeof value
1
2
3
4
# 3. 栈和堆
栈(Stack)是内存中一块用于存储局部变量和函数参数的线性结构,遵循着后入先出的原则(储存基本类型数据)。
堆(Heap)数据结构是一种树状结构,在栈中保留对象在堆中的地址,按引用访问(储存引用类型数据)。
一段代码的执行会经过压栈(执行栈 ECStack) -> 执行上下文 -> 执行代码 -> 出栈的操作。
function fn1(num) {
if (num < 3) {
console.log(num) // 打印顺序 1 -> 2
num++
fn1(num)
console.log(num) // 打印顺序 3 -> 2
}
}
fn1(1) // 打印顺序 1 -> 2 -> 3 -> 2
2
3
4
5
6
7
8
9
10
# 4. 闭包(Closure)
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure) — MDN (opens new window)。
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包 — 《你不知道的 JavaScript(上)》。
闭包的两大特性。
- 保护:保护私有变量不受外界干扰。
- 保存:形成不销毁栈内存,存储一些值。
function outer() {
let a = 1
return function () {
console.log(++a)
}
}
const inner = outer()
inner() // => 2
inner() // => 3
2
3
4
5
6
7
8
9
10
# 5. 原型链
原型链是实例对象和原型之间构成的有限链,原型链分为显示原型(prototype)和隐式原型(__proto__)。
显示原型是构造函数才拥有的属性,隐式原型属性在构造函数和实例对象中均有,实例对象的隐式原型指向构造函数的显示原型。
当访问对象中的某个属性时,会通过原型链依次查找,直到 Object 的隐式原型 null。
在开发中会通过给构造函数的显示原型添加公用属性或方法,方便实例引用。例如在 Vue 构造函数显示原型中添加 $log 方法(Vue.prototype.$log = console.log),在 template 模板上就可以通过 $log() 打印数据了。
function Foo() {}
const f = new Foo()
f.prototype // => undefined
f.__proto__ === Foo.prototype // => true
Foo.prototype.__proto__ === Object.prototype // => true
Object.prototype.__proto__ === null // => true
Object.__proto__ === Function.prototype // => true
Foo.__proto__ === Function.prototype // => true
Function.__proto__ === Function.prototype // => true
Function.prototype.__proto__ === Object.prototype // => true
2
3
4
5
6
7
8
9
10
11
12
13
14
提示
# 6. new 运算符
new 执行步骤。
- 创建一个空对象({ })。
- 将新建对象的隐式原型(__proto__)指向构造函数的显示原型(prototype)。
- 将构造函数里的 this 指向新建对象(将新建对象作为 this 的上下文)。
- 如果构造函数没有返回对象,则返回新建对象(返回 this)。
手写 new。
function _new(Fn, ...arg) { const obj = Object.create(Fn.prototype) Fn.apply(obj, arg) return obj }
1
2
3
4
5
# 7. ES5 与 ES6 类的区别
ES5 | ES6 |
---|---|
使用构造函数实现 | 使用 class 实现 |
内部为非严格模式 | 类和内部均为严格模式 |
声明会提升 | 声明不会提升 |
原型链上的方法默认可枚举 | 原型链上的方法默认不可枚举 |
可自调用,this 指向全局(window) | 必须使用 new 调用 |
定义的方法可以被 new | 定义的方法不能被 new(没有 prototype) |
内部可以重写类 | 内部不能重写类 |
ES5
new Foo() // 不报错 function Foo() { name = 'Neo' // 不报错 console.log(this) // => window Foo = 'Matrix' // 可重写 } Foo.prototype.rank = function () {} const foo = new Foo() for (const key in foo) console.log(key) // => 'rank' new foo.rank() // 不报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14ES6
new Foo() // Uncaught ReferenceError: Cannot access 'Foo' before initialization class Foo { constructor() { name = 'Neo' // Uncaught ReferenceError: b is not defined console.log(this) Foo = 'Matrix' // Uncaught TypeError: Assignment to constant variable } rank() {} } const foo = new Foo() for (const key in foo) console.log(key) // 无输出 new foo.rank() // => Uncaught TypeError: foo.rank is not a constructor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 8. 事件捕获、事件冒泡、事件委托(事件代理)
事件捕获和事件冒泡基于 addEventListener 监听事件,事件委托(事件代理)基于事件冒泡。
事件捕获是指当要触发子级事件前,如果父级有相同事件,就会先触发父级的事件,再触发子级事件(事件捕获默认不开启)。
事件冒泡是指触发子级事件后,如果父级有相同事件,接着触发父级的事件(事件冒泡默认开启)。
当要给多个子级触发相同事件时,可以把事件定义到父级上,通过事件冒泡触发父级上的事件,从而提升性能。此方式称为事件委托(事件代理)。
点击 box3,依次打印:'box1' -> 'box2' -> 'box3' -> 'box3' -> 'box2' -> 'box1'
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <style> body { height: 100vh; display: grid; } #box1 { width: 500px; height: 500px; background: gold; display: grid; place-self: center; } #box2 { width: 350px; height: 350px; background: aqua; display: grid; place-self: center; } #box3 { width: 150px; height: 150px; background: pink; place-self: center; } </style> </head> <body> <div id="box1"> <div id="box2"> <div id="box3"></div> </div> </div> <script> // 事件冒泡 box1.addEventListener('click', () => console.log('box1')) box2.addEventListener('click', () => console.log('box2')) box3.addEventListener('click', () => console.log('box3')) // 事件捕获 box1.addEventListener('click', () => console.log('box1'), true) box2.addEventListener('click', () => console.log('box2'), true) box3.addEventListener('click', () => console.log('box3'), true) </script> </body> </html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 9. 事件循环(Event Loop)
由于 JS 是单线程的,需要异步方式来防止代码运行阻塞,从而产生事件循环机制。
事件循环中涉及到同步任务和异步任务,异步任务又可以分为宏任务(macro-task)和微任务(micro-task)。微任务队列,每次会执行队列里的全部任务。如果在微任务的执行中又加入了新的微任务,也会在这一步一起执行。同一循环微任务过多会导致长时间不能将主线程交给浏览器,造成页面卡顿。宏任务队列,每次只会执行队列内的一个任务。
事件循环中可能会有一个或多个任务队列,这些队列分别为了处理
鼠标和键盘事件
、其他的一些 Task
。浏览器会在保持任务顺序的前提下,可能分配四分之三的优先权给鼠标和键盘事件,保证用户的输入得到最高优先级的响应,而剩下的优先级交给其他 Task,并且保证不会 “饿死” 它们。微任务(micro-task)
- Process.nextTick(仅 Node 中存在,在微任务队列执行之前执行)
- Promise(Async/Await 本质是 promise)
- MutationObserver(仅浏览器中存在,html5 新特性)
- queueMicrotask
宏任务(macro-task)
- script(整体代码)
- MessageChannel
- setTimeout
- setInterval
- setImmediate(IE 10 和 Node 中存在)
- requestAnimationFrame(仅浏览器中存在)
- requestIdleCallback(仅浏览器中存在)
- I/O
- UI render(仅浏览器中存在)
在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度),或者是由于已经执行的 setInterval 的回调函数阻塞导致的。在 Chrome 和 Firefox 中,定时器的第 5 次调用被阻塞了(chromium (opens new window));在 Safari 是在第 6 次;Edge 是在第 3 次。
在浏览器中,setTimeout()/setInterval() 非间隔执行,Chrome 和 Node 中执行最小时长均为 1ms(chromium (opens new window)),Firefox 中为 0ms。
Chrome 和 Node 打印顺序:1 -> 0。 Firefox 打印顺序:0 -> 1。
setTimeout(_ => console.log(1), 1) setTimeout(_ => console.log(0), 0)
1
2包括 IE、Chrome、Safari、Firefox 在内的浏览器其内部以 32 位带符号整数存储延时。这就会导致如果一个延时(delay)大于 2147483647 毫秒(大约 24.8 天)时就会溢出,导致定时器将会被立即执行。
Chrome 和 Node 大概输出 1。 Firefox 大概输出 0。
const startTime = Date.now() setTimeout(_ => console.log(Date.now() - startTime), 2147483648)
1
2setInterval 本质上是每隔一定的时间向任务队列添加回调函数。在向队列中添加回调函数时,如果队列中存在之前由其添加的回调函数,就放弃本次添加(不会影响之后的计时)。
间隔执行 fn 时间(intervalTime)约为 300ms。
let count = 0 let execEnd = 0 const fn = delay => { const execStart = Date.now() console.log('intervalTime', execEnd && execStart - execEnd) while (Date.now() < execStart + delay) continue execEnd = Date.now() } const timer = setInterval(() => { fn(700) count++ if (count > 3) clearInterval(timer) }, 1000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15异步轮询勿使用 setInterval。
const request = delay => new Promise(resolve => setTimeout(resolve, delay)) const timer = setInterval(async () => { await request(3000) // 执行三次 console.log('request') }, 1000) setTimeout(() => { clearTimeout(timer) }, 1000 * 3.5)
1
2
3
4
5
6
7
8
9
10可能打印顺序:6 -> 7 -> 3 -> 2 -> 4 -> 1 -> 5。
- requestAnimationFrame(重绘前)和 requestIdleCallback(空闲时)执行顺序不确定,取决于浏览器。requestAnimationFrame 在 requestIdleCallback 之前执行。
- Promise(then)和 queueMicrotask 一定最先执行,执行顺序与注册顺序一致。
- MessageChannel 在 setTimeout 和 setInterval 之前执行。setTimeout 和 setInterval 执行顺序与注册顺序一致(浏览器间有差异,Firefox 中 MessageChannel、setTimeout、setInterval 按注册顺序执行)。
setTimeout(_ => console.log(1)) const { port1, port2 } = new MessageChannel() port1.onmessage = ({ data }) => console.log(data) port2.postMessage(2) requestAnimationFrame(_ => console.log(3)) requestIdleCallback(_ => console.log(4)) const timer = setInterval(_ => { console.log(5) clearInterval(timer) }) Promise.resolve(6).then(console.log) queueMicrotask(_ => console.log(7))
1
2
3
4
5
6
7
8
9
10
11
12并不是每一个宏任务都会紧跟着一次渲染。
打印顺序:mic -> mic -> sto -> sto -> rAF -> rAF
// 间隔时间较短,浏览器会合并这两个定时器任务 setTimeout(() => { console.log('sto') requestAnimationFrame(() => console.log('rAF')) }) setTimeout(() => { console.log('sto') requestAnimationFrame(() => console.log('rAF')) }) queueMicrotask(() => console.log('mic')) queueMicrotask(() => console.log('mic'))
1
2
3
4
5
6
7
8
9
10
11
12chrome 73 版本前的 await 后续代码执行时机会低于 promise.then()。
- 73 版本前打印顺序:
promise1 -> promise2 -> async1
- 73 版本及以上打印顺序:
async1 -> promise1 -> promise2
async function async1() { await 777 console.log('async1') } async1() new Promise(resolve => { resolve() }) .then(() => { console.log('promise1') }) .then(() => { console.log('promise2') })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16- 73 版本前打印顺序:
Node 中的 Event Loop 基于 libuv 实现。Node v11 及以上版本的 Event Loop 与浏览器一致,v10 及以下版本有些许差异。
在 Node v10 及以下版本,若 time2 定时器已经在执行队列中,打印顺序为:
timer1 -> timer2 -> promise1 -> promise2
setTimeout(() => { console.log('timer1') Promise.resolve().then(() => { console.log('promise1') }) }, 0) setTimeout(() => { console.log('timer2') Promise.resolve().then(() => { console.log('promise2') }) }, 0)
1
2
3
4
5
6
7
8
9
10
11
12
13
Node v10 及以下,当 time2 定时器已经在执行队列中图示
Node v11 及以上与浏览器图示
Event Loop 图示
# 10. AST 和 Virtual DOM
AST(抽象语法树)是用对象描述 JS 语法,Virtual DOM(虚拟 DOM)是用对象描述真实 DOM 元素。
AST 作用。
- IDE 的错误提示、代码格式化、代码高亮、代码自动补全等。
- JSLint、JSHint 对代码错误或风格的检查等。
- webpack、rollup 进行代码打包等。
- CoffeeScript、TypeScript、JSX 等转化为原生 Javascript。
- Vue 模板编译、React 模板编译。
Virtual DOM 优点。
- 容易实现跨平台开发(React Native、React Art)。
- 利用 DOM-DIFF 实现增量更新,减少重排重绘。
- 处理浏览器兼容性问题,避免直接操作真实 DOM(合成事件)。
- 内容经过了 XSS 处理,防范 XSS 攻击。
- 相较于 DOM 有体积上的优势,能够减少属性比较时的内存开销。
虚拟 DOM 真正的价值从来都不是性能(尤大语录 (opens new window))。
- 虚拟 DOM 为函数式的 UI 编程方式打开了大门。
- 虚拟 DOM 可以渲染到 DOM 以外的 backend,比如 ReactNative。
# 11. CJS 与 EMS 区别
CommonJS | ES6 Module |
---|---|
输出的是一个值的拷贝 | 输出的是值的引用 |
会缓存值 | 不会缓存值 |
运行时加载 | 编译时输出接口 |
不容易实现 Tree Shaking | 容易实现 Tree Shaking |
提示
CommonJS 引入:Webpack 5 以下版本无 Tree Shaking,5 版本部分可以 Tree Shaking。