JS

coderljw 2024-10-13 JS 高级
  • JS
  • 判断方式
  • 类型检测
  • 闭包
  • 原型链
  • Event Loop
  • AST
  • Virtual DOM
大约 13 分钟

# 1. 判断方式

    1. 两个等号会进行类型转换。
    1. 对象 == 对象:比较地址。
    2. 对象 == 字符串:对象变为字符串。
    3. NaN == NaN:和自己及其他值都不相等。
    4. null == undefined:在 == 相等,=== 不相等,并和其他值都不相等。
    5. 剩下情况,只要两边类型不同,最后都转为数字。
    {} == {} // => 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
    1. 三个等号基本数据类型比较值,复杂数据类型比较内存地址(用的最多)。

    存在 -0 和 +0 比较为 true,NaN 和 NaN 比较为 false 的情况。

    -0 === +0 // => true
    NaN === NaN // => false
    
    1
    2
    1. Object.is() 最准确的比较方式,但效率不高,主要用来解决三个等号的缺陷(NaN 可用 Number.isNaN() 方法判断)。
    Object.is(-0, +0) // => false
    Object.is(NaN, NaN) // => true
    
    1
    2

# 2. 数据类型检测

    1. 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
    1. 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
    1. 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
    1. 通过 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
1
2
3
4
5
6
7
8
9
10

# 4. 闭包(Closure)

  • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure) — MDN (opens new window)

  • 当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包 — 《你不知道的 JavaScript(上)》。

  • 闭包的两大特性。

    1. 保护:保护私有变量不受外界干扰。
    2. 保存:形成不销毁栈内存,存储一些值。
function outer() {
  let a = 1
  return function () {
    console.log(++a)
  }
}

const inner = outer()
inner() // => 2
inner() // => 3
1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14

提示

# 6. new 运算符

  • new 执行步骤。

    1. 创建一个空对象({ })。
    2. 将新建对象的隐式原型(__proto__)指向构造函数的显示原型(prototype)。
    3. 将构造函数里的 this 指向新建对象(将新建对象作为 this 的上下文)。
    4. 如果构造函数没有返回对象,则返回新建对象(返回 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
    14
  • ES6

    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)

    1. Process.nextTick(仅 Node 中存在,在微任务队列执行之前执行)
    2. Promise(Async/Await 本质是 promise)
    3. MutationObserver(仅浏览器中存在,html5 新特性)
    4. queueMicrotask
  • 宏任务(macro-task)

    1. script(整体代码)
    2. MessageChannel
    3. setTimeout
    4. setInterval
    5. setImmediate(IE 10 和 Node 中存在)
    6. requestAnimationFrame(仅浏览器中存在)
    7. requestIdleCallback(仅浏览器中存在)
    8. I/O
    9. 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
    2
  • setInterval 本质上是每隔一定的时间向任务队列添加回调函数。在向队列中添加回调函数时,如果队列中存在之前由其添加的回调函数,就放弃本次添加(不会影响之后的计时)。

    间隔执行 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
    12
  • chrome 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
  • 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

# 11. CJS 与 EMS 区别

CommonJS ES6 Module
输出的是一个值的拷贝 输出的是值的引用
会缓存值 不会缓存值
运行时加载 编译时输出接口
不容易实现 Tree Shaking 容易实现 Tree Shaking

提示

CommonJS 引入:Webpack 5 以下版本无 Tree Shaking,5 版本部分可以 Tree Shaking。

以父之名
周杰伦.mp3