likes
comments
collection
share

3天整理的3W3千字的JS年度毒打!

作者站长头像
站长
· 阅读数 10

3W3千字的年度JS毒打!

JS语言特性

  1. 弱类型:JavaScript是一种弱类型语言,声明变量时不用指定其数据类型,并且可以在运行时更改变量的类型。
  2. 动态语言: JavaScript是一种动态语言,可以在运行时添加、删除和修改对象的属性和方法。
  3. 事件驱动: JavaScript可以响应用户操作和事件,例如鼠标点击、键盘输入等,可以编写逻辑来处理这些事件。
  4. 客户端与服务器端都可运行: JavaScript最初被设计为在Web浏览器中运行,但现在也可以在服务器端(如Node.js)上运行。
  5. 垃圾回收: JavaScript具有自动垃圾回收机制,它会自动管理不再使用的内存,减轻了开发人员的负担。
  6. 支持函数式编程:JavaScript支持函数式编程范式,允许您将函数作为一等公民来传递、复制和储存,并使用高阶函数来处理数据。
  7. 支持面向对象:JavaScript支持面向对象编程范式,并且提供了类、对象、继承、封装等面向对象的概念和特性。
  8. 单线程:无论是浏览器还是nodejs
  9. 前端交互性:JavaScript可以与HTMLCSS进行交互,并通过DOM来操作和修改网页内容。

严格模式有什么特点?

  1. 全局变量必须先声明才能使用
  2. 禁止使用with
  3. 禁止this指向window
  4. 函数参数不能重名

ES6自带use strict, 是天生的严格模式

script标签加载js的3个时机

3天整理的3W3千字的JS年度毒打!

  1. 同步加载, js优先。html暂停解析,下载js并执行,完成后继续解析html

  2. asynchtml解析与js下载并行,js执行优先级高于html解析。js将在下载完成后立即执行,会暂停html页面的解析,待js执行完继续解析。例如:<script async src="script.js"></script>

  3. deferhtml解析与js下载并行,js执行优先级低于html解析。当设置为 defer 时,js将在html页面解析完毕后执行,但在 DOMContentLoaded 事件触发之前执行。例如:<script defer src="script.js"></script>

JS类型

原始类型:

  1. boolean
  2. undefine
  3. null
  4. number
  5. string
  6. symbol
  7. bigint

引用类型:Array、Object、function

JS类型如何转换?

显示类型转换

  1. 转为string: String().toString()
  2. 转为number:Number()parseInt()parseFloat()
  3. 转为boolean:Boolean()

隐式类型转换

什么时候会发生隐式类型转换?
  1. 字符串和数字之间的运算:
    • 当字符串参与加法运算时,其他操作数会隐式转换为字符串并进行拼接。
    • 当字符串参与减法、乘法、除法等运算时,字符串会先被转换为数字进行计算。
    console.log("10" + 5); // 输出 "105"
    console.log("10" - 5); // 输出 5
    
  2. 使用比较运算符时:
    • 在使用==相等运算符比较不同类型的操作数时,JavaScript会进行隐式类型转换以进行比较。
    • 在使用关系运算符(如<><=>=)比较不同类型的操作数时,JavaScript也会进行隐式类型转换进行比较。
    console.log("10" == 10); // 输出 true
    console.log("5" > 1); // 输出 true
    
  3. 逻辑运算符:
    • 在使用逻辑运算符(如&&||)进行逻辑运算时,JavaScript会对操作数进行隐式类型转换,并根据转换后的结果确定返回值。
    if ("" || 0) {
      console.log("This condition is true");
    } else {
      console.log("This condition is false"); // 输出
    }
    
    • 加法运算:
      • 当参与加法运算的两个操作数中至少一个为字符串时,会触发字符串拼接的操作,即将两个操作数转换为字符串并进行连接。
      • 当参与加法运算的两个操作数中有一个为对象时,会调用该对象的valueOf()toString()方法将其转换为原始值,然后进行加法运算。
      • 当参与加法运算的两个操作数中至少一个为浮点数时,会将整数操作数转换为浮点数来执行运算。
      • 当参与加法运算的两个操作数中至少一个为布尔值时,会将布尔值转换为数字(true转换为1,false转换为0)来执行运算。
  4. 条件语句: 在条件语句(如if语句、三元运算符)中,将非布尔类型的值作为条件时,JavaScript会将其隐式转换为布尔值进行判断。
    if ("" || 0) {
      console.log("This condition is true");
    } else {
      console.log("This condition is false"); // 输出
    }
    

NaN有什么特点?

// @ts-nocheck

console.log(typeof NaN) // number
console.log(NaN == NaN) // false
console.log(NaN + {}) // 'NaN[object Object]'
console.log(NaN + []) // 'NaN'
console.log(NaN + '123') // 'NaN123'
console.log(NaN - '123') // NaN
console.log(NaN * '123') // NaN
console.log(NaN / '123') // NaN
console.log(NaN % '123') // NaN
  • NaNnumber类型
  • NaN是一个唯一值,NaN不等于NaN
  • NaN与任何其他值执行数学运算,结果都是NaN(加法结果可能是string

如何判断是否是NaN?

// @ts-nocheck

console.log(isNaN(NaN)); // 输出 true
console.log(isNaN("hello")); // 输出 true(隐式转换为NaN)
console.log(Number.isNaN(NaN)); // 输出 true
console.log(Number.isNaN("hello")); // 输出 false(不进行类型转换)

注意: isNaN()Number.isNaN()的区别

== 和 === 的区别是什么?

==(相等运算符):

  • ==会进行隐式类型转换,然后比较两个操作数。
  • 如果两个操作数类型不同,JavaScript 会尝试将它们转换为相同类型。这个过程称为类型强制转换(Type coercion)。
  • ==比较时,会进行一些规则的判断和转换,如将字符串转换为数字,将布尔值转换为数字等。
  • ==执行的是相等值的比较。
console.log(10 == "10"); // 输出 true,进行了隐式类型转换
console.log(true == 1); // 输出 true,进行了隐式类型转换
console.log(null == undefined); // 输出 true

console.log('0' == false); // 输出 true,字符串 '0' 转换为数字 0,再与 false 进行比

===(严格相等运算符):

  • ===不会进行隐式类型转换,它要求比较的两个操作数不仅值相等,类型也必须相同。
  • ===执行的是严格相等性的比较。
console.log(10 === "10"); // 输出 false,类型不同
console.log(true === 1); // 输出 false,类型和值都不同
console.log(null === undefined); // 输出 false

console.log('0' === false); // 输出 false,类型不同

特殊: 引用类型通过内存地址进行比较

// @ts-nocheck
console.log({} == {}) // false
console.log({} === {}) // false
console.log([] == []) // false
console.log([] === []) // false

const a = {}
const b = a

const c = []
const d = c
console.log(a == b) // true
console.log(a === b) // true
console.log(c == d) // true
console.log(c === d) // true

为什么 0.1 + 0.2 !== 0.3

因为JavaScript中的数字采用的是双精度浮点数表示法,而双精度浮点数无法精确地表示所有的十进制小数。

typeof

console.log(typeof 42) // number
console.log(typeof "JavaScript") // string
console.log(typeof true) // boolean
console.log(typeof undefined) // undefined
console.log(typeof null) // object
console.log(typeof [1, 2, 3]) // object
console.log(typeof function() {}) // function
const a = () => { console.log(1)}
console.log(typeof a) // function
console.log(typeof Symbol('123')) // symbol

typeof 无法精确的检测nullObjectArray

为什么typeof null的结果时'object'?

typeof null的结果是object, 但null是原始类型。

造成这个结果的原因是null的内存地址是以000开头,而js会将000开头的内存地址视为object

如何准确检测一个值是null类型?

/**
 * @description: 检测是否为null
 * @param {any} value
 */
const isNull = (value: any) => value == null && typeof value === 'object'

console.log(isNull(0)) // false
console.log(isNull(1)) // false
console.log(isNull('')) // false
console.log(isNull('1')) // false
console.log(isNull(undefined)) // false
console.log(isNull({})) // false
console.log(isNull([])) // false
console.log(isNull(false)) // false
console.log(isNull(true)) // false
console.log(isNull(Symbol(undefined))) // false
console.log(isNull(Symbol('123'))) // false
console.log(isNull(Symbol(123))) // false
console.log(isNull(null)) // true

如何区别一个引用类型是数组还是对象?

我们可以通过Array.isArray()来区分数组与对象

它的实现原理是什么?

const isArray = (value: any) => Object.prototype.toString.call(value) === '[object Array]'

console.log(isArray([])) // true
console.log(isArray({})) // false
console.log(isArray(function() {})) // false

Object.is() 和 === 有什么区别?

console.log(NaN === NaN) // false
console.log(Object.is(NaN, NaN)) // true

console.log(0 === 0) // true
console.log(Object.is(0, 0)) // true

console.log(+0 === -0) // true
console.log(Object.is(+0, -0)) // false
  • ===无法正确判断NaN+0-0
  • Object.is()===的强化版,修复了一些特殊情况下===的错误

手写getType,获取详细的数据类型

/**
 * @description: 获取详细类型
 * @param {any} value
 */
const getType = (value: any) => {
  const str: string = Object.prototype.toString.call(value)
  const typeStrArray = str.substring(1, str.length - 1).split(' ')
  return typeStrArray[1].toLowerCase()
}

console.log(getType(0)) // number
console.log(getType('')) // string
console.log(getType(undefined)) // undefined
console.log(getType(null)) // null
console.log(getType(() => {})) // function
console.log(getType(true)) // false
console.log(getType({})) // object
console.log(getType([])) // array
console.log(getType(new Map())) // map
console.log(getType(new Set())) // set
console.log(getType(Symbol('123'))) // symbol
console.log(getType(new WeakMap())) // weakmap
console.log(getType(new WeakSet())) // weakset
console.log(getType(BigInt(998))) // bigint

核心:通过toString可以获取到带有具体类型的字符串

手写isEqual比较两个引用类型的值是否一样

/**
 * @description: 判断两个引用类型值是否相等
 * @param {any} a
 * @param {any} b
 */
const isEqual = (a: any, b: any): boolean => {

  if(a === b) return true
  
  // 判断类型是否一致
  if (typeof a !== typeof b) return false

  // 判断是否是基础类型或者symbol
  if (typeof a !== 'object') return a === b

  // 判断是否都是null
  if (!a && !b) return true 

  // 判断引用类型size是否相同
  if(Object.keys(a).length !== Object.keys(b).length) return false

  // 判断对象
  const keys: Array<string> = Object.keys(a)

  return keys.every((key) => {
    return isEqual(a[key], b[key])
  })
}

console.log(isEqual(1, 1)) // true
console.log(isEqual(0, 1)) // false
console.log(isEqual(NaN, NaN)) // false
console.log(isEqual(0, -0)) // true
console.log(isEqual('', '')) // true
console.log(isEqual('', '1')) // false
console.log(isEqual(undefined, undefined)) // true
console.log(isEqual(null, null)) // true 
const sym = Symbol('1')
console.log(isEqual(sym, sym)) // true
console.log(isEqual(Symbol('1'), Symbol('1'))) // false
console.log(isEqual(Symbol('1'), Symbol('2'))) // false
console.log(isEqual([], [])) // true
console.log(isEqual([1,2,], [1,2,3])) // false
console.log(isEqual([1,2,3], [1,2,3])) // true
console.log(isEqual([1, 3, 2], [1, 2, 3])) // false
console.log(isEqual([{}, {a: { b: '123'}}, 3], [{}, {a: { b: '123'}}, 3])) // true
console.log(isEqual({a: 1}, {a: 1})) // true
console.log(isEqual({a: 1, b: ''}, {a:1})) // false
console.log(isEqual({a: 1, b: '' }, {a: 1, b: '', c: null })) // false
console.log(isEqual({ a: 1, b: [{ c: [1, 2, 3] }] }, { a: 1, b: [{ c: [1, 2, 3] }] })) // true
console.log(isEqual({ a: 1, b: [{ c: [1, 2, 4] }] }, { a: 1, b: [{ c: [1, 2, 3] }] })) // false

注意:typeof null 返回object, null需要特殊处理

JS内存

  • 哪些数据类型存储在中?
  1. string
  2. number
  3. boolean
  4. undefined
  5. null
  6. symbol
  7. bigint
  • 哪些数据类型存储在中? 所有的引用类型

特殊:闭包中定义的所有变量不区分类型,都存储在

JS内存垃圾回收用什么算法?

JavaScript内存垃圾回收使用的是标记清除算法。它的基本思路是通过标记来追踪哪些内存是仍然被程序使用的,然后清除那些未标记的内存块。

  1. 垃圾收集器会从根对象(通常是全局对象)开始,标记所有从根对象开始可达的对象。
  2. 对于标记过的对象,继续递归地标记其引用的对象,直到所有可达对象都被标记。
  3. 所有未被标记的对象将被视为垃圾,它们所占用的内存将被释放。
  4. 清除阶段会遍历所有的对象,释放未标记的对象所占用的内存,并将回收的内存块加入空闲列表中,以备后续分配使用。

标记清除算法相对简单且高效,能够准确地找出并回收不再使用的内存。但它也存在一些缺点,如可能会造成停顿(暂停应用程序执行)和内存碎片化等问题。为了解决这些问题,现代的JavaScript引擎还会采用其他的垃圾回收策略,如分代回收增量标记等。

增量标记会将一口气完成的标记任务采用类似于节流的方式进行稀释,拆解为很多小的标记任务,每完成一个小的标记任务,让就js执行一会儿,再标记,再执行。。。直到标记阶段完成才进入内存碎片的整理上面来。

js内存泄漏场景有哪些?

  1. 被全局变量、函数引用,组件销毁时未清除
  2. 被全局事件、定时器引用,组件销毁时未清除
  3. 被自定义事件引用,组件销毁时未清除

WeakMap和WeakSet的价值是什么?

MapSet相比,WeakMapWeakSet可以避免内存泄漏

MapSet 中,如果一个对象被添加到集合中,即使在程序中不再需要这个对象,它仍然会被保留,无法被垃圾回收。因为集合中的对象仍然被集合所引用着,垃圾回收器无法判断这些对象是否不再需要。

而对于 WeakMapWeakSet,当一个被引用的对象在其他地方没有被引用时,垃圾回收器会自动回收该对象。这种弱引用的特性使得我们可以更容易地避免内存泄漏。

通过 WeakMapWeakSet,我们可以利用对象的引用作为键,存储与这个对象相关的附加信息,而不会造成原始对象的内存泄漏。当原始对象被垃圾回收时,与之关联的附加信息也会被自动清理。

WeakMapWeakSet 的弱引用特性意味着我们无法像常规的 MapSet 那样遍历所有的键或值。此外,WeakMapkey必须是对象,WeakSet 的值也必须是对象。

综上所述,WeakMapWeakSet 可以避免内存泄漏,因为它们不会阻止被引用对象被垃圾回收,并且可以用于关联对象的附加信息而不会造成原始对象的内存泄漏。

函数和箭头函数

箭头函数和寻常函数有什么区别?

  1. 语法不同

  2. 绑定this:箭头函数没有自己的this绑定,它地的this永远指向它定义时候的父作用域。而普通函数的this值是在调用时动态确定的,根据函数的调用方式来决定this的值。

  3. 不能作为构造函数:箭头函数没有原型prototype属性,因此不能通过new关键字来创建对象实例。而普通函数可以作为构造函数,使用new来创建对象。

  4. 不绑定arguments对象:箭头函数没有自己的 arguments对象,可以通过rest参数语法 (...) 来获取函数的参数。而普通函数可以使用arguments对象获取传入的参数列表。

  5. 不能使用yield关键字:箭头函数不能用作生成器函数,即不能使用yield关键字进行迭代操作。而普通函数可以通过使用函数*关键字来定义一个生成器函数。

  6. 无法通过applycallbind改变this

函数的arguments是什么?

  1. arguments是一个类数组对象,包含了函数调用时传入的所有参数。它可以在函数体内部使用,用来访问传递给函数的所有参数。

  2. arguments对象的长度(即传入的参数个数)是动态的,会随着函数调用时传递的实际参数个数而改变。

  3. 可以通过修改arguments对象的元素来修改实际传递的参数值。

  4. callee属性:callee属性,指向当前正在执行的函数本身,递归调用时非常有用,可以使用 arguments.callee引用当前函数,而不需要明确指定函数名。

什么时候不能用箭头函数?

  1. 对象方法
  2. 原型方法
  3. 用作构造函数
  4. 动态上下问中的回调函数
  5. Vue生命周期和methods(Vue2本质是配置对象)

小结:函数内部涉及到this,慎重考虑

如何实现一个分治狂魔curry函数?

柯里化(Currying)是一种将接受多个参数的函数转换为一系列接受单个参数的函数的过程。通过柯里化,我们可以重复应用函数并部分应用其参数。

function curry(fn) {
  return function curried(...args) {
    // 核心:参数够不够
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    } else {
      return function(...moreArgs) {
        return curried.apply(this, args.concat(moreArgs))
      };
    }
  };
}

function add(x, y, z) {
  return x + y + z
}

const curriedAdd = curry(add)

console.log(curriedAdd(1)(2)(3)) // 6
console.log(curriedAdd(1, 2)(3)) // 6
console.log(curriedAdd(1)(2, 3)) // 6

核心:看参数够不够,不够就继续等待下次执行,够就执行

循环

Array.map()是如何实现的?

首先我们得知道Array.map()是什么?

  1. Array.map()是一个JS数组原型得高级函数
  2. 它用于对数组的每个元素应用一个callback,并返回一个新的数组,新数组中的元素是原始数组经过回调函数处理后的结果。
  3. callback会接收到两个参数分别是当前的itemindex,执行完后会返回一个值
  4. 它不会改变原始数组,因此它还是一个纯函数
// @ts-nocheck
Array.prototype.myMap = function (callback) {
  const newArr = []
  for (let i = 0; i < this.length; i++) {
    newArr[i] = callback(this[i], i)
  }
  return newArr
}

const arr = [1, 2, 3]

const newArr = arr.myMap((item, index) => {
  return item * index
})

console.log(arr, newArr) // [1, 2, 3] [0, 2, 6]
console.log([,,].myMap((item, index) => {
  return item * index
})) // [NaN NaN]

核心:map需要让每个数组实例都能运用,注意this问题

[1,2,3].map(parseInt)的结果是什么?

[
  parseInt('1', 0),// 1
  parseInt('2', 1),// NaN
  parseInt('3', 2) // NaN
]

核心在于需要理解parseInt() 3天整理的3W3千字的JS年度毒打!

  1. parseInt()接受1个必传参数string和一个默认参数radix而后返回一个转换后得整数,重点在于默认参数
  2. radix参数表示进制,会将string按照什么进制进行转换,默认为10进制

parseInt('1', 0)表示‘1’按照十进制转换,因为0undefined一样,转换为boolean都是false,所以内部会将radix默认赋值为10,最终得到了1

parseInt('2', 1)表示‘2’按照一进制进行转换,可是并没有一进值,所以返回了NAN

parseInt('3', 2)表示‘2’按照二进制进行转换,可是二进制并没有3,所以返回了NAN

如何跳出forEach?

forEach中使用return不会返回,函数还会继续执行 我们可以通过try,手动抛出异常的方式跳出循环

3天整理的3W3千字的JS年度毒打!

推荐使用Array.every()或者Array.some()代替Array.forEach()

注意:仅仅抛出,不要捕获,捕获会继续执行接下来的循环

如何实现reduce?

首先,理解Array.reduce()怎么用? 3天整理的3W3千字的JS年度毒打! 为数组中的所有元素调用指定的回调函数。回调函数的返回值是累积的结果,并在下次调用回调函数时作为参数提供。

@param callback -一个最多接受四个参数的函数。reduce方法对数组中的每个元素调用一次callback函数。

@param initialValue—如果指定了initialValue,将作为初始值开始累积。对callbackfn函数的第一次调用将该值作为参数而不是数组值提供。

const arr = ['ljx', 'dys', 'hzc']

// 数组处理
const newArr = arr.reduce((previousValue, curValue, curIndex, rawArray) => {
  console.log(curIndex, rawArray)
  if (curIndex !== 2) {
     previousValue.push(curValue)
  }
  return previousValue
}, [] as Array<string>)

console.log(newArr) // ['ljx', 'dys']

// 数组转对象
const obj = arr.reduce((previousValue, curValue, curIndex, rawArray) => {
  console.log(curIndex, rawArray)
  previousValue[curValue] = curValue
  return previousValue
}, {} as Record<string, string>)

console.log(obj) // {ljx: 'ljx', dys: 'dys', hzc: 'hzc'}

3天整理的3W3千字的JS年度毒打!

使用场景:数组的数据清洗

自定义实现:

// @ts-nocheck
const arr = ['ljx', 'dys', 'hzc']

Array.prototype.myReduce = function (callback, initialValue) {
  // 不传initialValue时,使用数组第一个元素作为初始值,从第二个元素开始迭代
  let arr = [...this]
  if (!initialValue) {
    [initialValue, ...arr] = [...arr]
  }

  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    initialValue = callback(initialValue, item, i, arr)
  }

  return initialValue
}

// 数组处理
const newArr2 = arr.myReduce((previousValue, curValue, curIndex, rawArray) => {
  console.log(curIndex, rawArray)
  if (curIndex !== 2) {
     previousValue.push(curValue)
  }
  return previousValue
}, [] as Array<string>)

console.log(newArr2) // (2) ['ljx', 'dys']

// 数组转对象
const obj2 = arr.myReduce((previousValue, curValue, curIndex, rawArray) => {
  console.log(curIndex, rawArray)
  previousValue[curValue] = curValue
  return previousValue
}, {} as Record<string, string>)

console.log(obj2) // {ljx: 'ljx', dys: 'dys', hzc: 'hzc'}


// 检测不传初始值
const str = arr.myReduce((previousValue, curValue, curIndex, rawArray) => {
  console.log(curIndex, rawArray)
  return previousValue + ' ' + curValue 
})

console.log(str) // 'ljx dys hzc'

注意: 如果没传初始值需要特殊处理

for-in 和for-of有什么区别?

  • for-in得到key,用于可枚举数据,如ObjectstringArray
  • for-of得到value, 用于可迭代数据,如MapSetArrayString

针对数据类型不同:

  • 遍历对象:for-in可以,for-of不可以
  • 遍历Map,Setfor-of可以,for-in不可以
  • 遍历generator: for-of可以,for-in不可以
const arr = []
const obj = {}
const map = new Map()
const set = new Set()
const str = ''

// 查看哪些类型可迭代
if (arr[Symbol.iterator]) {
  console.log('Array可迭代')
} else {
  console.log('Array不可迭代')
}
if (obj[Symbol.iterator]){
  console.log('Object可迭代')
} else {
  console.log('Object不可迭代')
}
if (map[Symbol.iterator]){
  console.log('Map可迭代')
}  else {
  console.log('Map不可迭代')
}
if (set[Symbol.iterator]){
  console.log('Set可迭代')
}  else {
  console.log('Set不可迭代')
}
if (str[Symbol.iterator]){
  console.log('String可迭代')
}  else {
  console.log('String不可迭代')
}

for await of 有什么作用?

for await ofPromise.all一样,用于并行执行promise,区别在于Promise.all需要所有promise执行完才能获取到返回值,for await of按顺序获取到返回值

// 模拟发送请求
function createPromise(name: string, delay: number) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(name)
    }, delay)
  })
}

const p1 = createPromise('p1', 1000)

const p2 = createPromise('p2', 1000)

const p3 = createPromise('p3', 1000)

Promise.all([p1, p2, p3]).then((res) => {
  console.log(res) // ['p1', 'p2', 'p3']
})

async function test() {
  for await (const res of [p1, p2, p3]) {
    console.log(res)
  }
}

test()

迭代器和生成器

  • for循环不是迭代器
  • 迭代器是用来解决for循环的问题的

迭代器模式解决了什么问题?

  • for循环的触发,需要知道数组长度,需要知道如何获取元素(index
  • forEach VS for循环,不需要数据的长度,不需要知道元素的结构,不需要知道数据的结构,forEach是一个简易的迭代器

迭代器解决了如何更加方便、简易地遍历一个有序的数据集合的问题

  • 顺序访问有序结构(如:数组、NodeList
  • 不知道数据的长度、内部结构
  • 高内聚、低耦合

目标性:for循环和迭代器都是为了解决有序数据的遍历问题

js中有序的数据结构:

  • 数组
  • 字符串
  • NodeListDOM集合
  • Map
  • Set
  • arguments 类数组

注意Object是无序结构

应用场景

  • Symbol.iterator。所有的有序数据结构,都内置了Symbol.iterator这个key,使用它可以获得该数据结构的迭代器
  • 自定义迭代器
  • 用于for of,只要内置了Symbol.iterator这个key,都可以使用for of来进行遍历
  • 用于数组的解构、扩展操作符、Array.from
  • 用于Promise.allPromise.race
  • 用于生成器yield*

生成器

  • yield*语法
  • yield遍历DOM

//#region 使用 yield 生成迭代器

function* genNums() {
  yield 10
  yield 20
  yield 30
}

// 生成器的本质就是返回一个迭代器
const numsIterator = genNums()

// 所以我们可以通过迭代器的方式去应用
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator)

// 也可以通过for of去使用
for (const num of numsIterator) {
  console.log(num)
}

// 也可以使用扩展操作符
console.log([...numsIterator])

//#endregion

//#region 使用 yield* 生成迭代器

function* genNums2() {
  // yield* 后面跟的需要是一个有序结构,这个有序结构本身已经实现了[Symbol.Iterator]
  yield* [11, 21, 31]
}

const numsIterator2 = genNums2()

// 也可以通过for of去使用
for (const num of numsIterator2) {
  console.log(num)
}

// 也可以使用扩展操作符
console.log([...numsIterator2])

console.log(numsIterator2.next())
console.log(numsIterator2.next())
console.log(numsIterator2.next())
console.log(numsIterator2.next())
console.log(numsIterator2)
//#endregion

使用generator + yield 遍历DOM

function* traverse(elemList: Array<Element>): any {
  for (const elem of elemList) {
    yield elem
    
    const children = Array.from(elem.children)
    if (children.length) {
      yield* traverse(children)
    }
  }
}

节点列表本来就是一个类数组,它具有[Symbol.iterator],因此我们可以使用yieldyield *生成迭代器,借此迭代

遍历一个数组用for和forEach谁快?

在遍历一个数组时,使用for循环通常比forEach方法更快。这是因为for循环是原生的JavaScript语法,而forEachArray对象的一个方法。

for循环是一种比较底层的迭代机制,它通过索引直接访问数组元素,因此可以更快地遍历数组。而forEach方法则是高级抽象的迭代器,它在每次迭代时都会执行一个回调函数,并且不能使用breakcontinue语句来控制迭代流程,因此它的执行速度相对较慢。

作用域和自由变量

作用域代表着一个变量合法的使用范围

  1. 全局作用域
  2. 函数作用域
  3. 块级作用域

自由变量

一个变量在当前作用域没有被定义,但被使用了,那么会沿着作用域向上查找,直到找到为止,如果全局作用域都没找到,则报错

3天整理的3W3千字的JS年度毒打!

答案:600

闭包

闭包的本质是作用域应用的特殊情况,有两种表现:

  1. 函数作为参数被传递
  2. 函数作为返回值被返回

3天整理的3W3千字的JS年度毒打!

答案:100

3天整理的3W3千字的JS年度毒打!

答案:100

3天整理的3W3千字的JS年度毒打!

答案:10

闭包:自由变量的查找,实在函数定义的地方,向上级作用域查找,不是从执行的地方向上查找

this

  • 作为普通函数使用时,非严格模式下指向window
  • 使用callapplybindthis指向传入对象, 传入undefinednull时指向全局对象
  • 作为对象方法被调用时this指向当前对象
  • classthis指向当前实例
  • 箭头函数中this指向上级作用域的this

3天整理的3W3千字的JS年度毒打!

答案:1 undefined

3天整理的3W3千字的JS年度毒打!

this的取值是在执行时确定的,不是定义时确定的

如何改变this指向?

JavaScript中,有几种常见的方式可以改变 this 的指向:

  1. 使用 call() 方法:call() 方法调用一个函数,并且可以指定函数内部的 this 指向。传递给 call() 方法的第一个参数是要绑定给 this 的对象,后续参数是函数的参数列表。例如:
function sayName() {
  console.log(this.name);
}

const person = {
  name: 'John'
};

sayName.call(person); // 输出: John
  1. 使用 apply() 方法:apply() 方法与 call() 方法类似,也可以指定函数内部的 this 指向。区别在于,apply() 方法接收一个数组作为参数列表。例如:
function sayName(...arg) {
  console.log(this.name + arg[0]);
}

const person = {
  name: 'John'
};

sayName.apply(person, [' say Hello']); // 输出: John say Hello
  1. 使用 bind() 方法:bind() 方法创建一个新的函数,并指定函数内部的 this 指向。不同于 call()apply() 直接调用函数,bind() 返回一个绑定了指定 this 的新函数。例如:
function sayName() {
  console.log(this.name);
}

const person = {
  name: 'John'
};

const sayNameWithPerson = sayName.bind(person);
sayNameWithPerson(); // 输出: John

在使用 call()apply()bind() 时,如果传递 nullundefined,则 this 将指向全局对象(在浏览器环境中为 window)。

此外,还可以通过闭包、使用ES6的类和方法、使用观察者模式等方式来改变 this 的指向。

手写call

// @ts-nocheck

Function.prototype.myCall= function (instance, ...args) {
  // 如果传入的实例是null、undefined就指向全局作用域
  if (instance == null) instance = global
  // 如果传入的是基础类型
  if (typeof instance !== 'object') instance = new Object(instance)

  // 注意防止污染全局作用域
  const symbol = Symbol('fn')
  instance[symbol] = this
  
  const res = instance[symbol](...args)
  
  delete instance[symbol]
  
  return res
}

const obj = {
  name: 'ljx'
}

function print(a,b,c) {
  console.log(this.name, a,b,c)
}

print.myCall(obj) // ljx undefined undefined undefined
print.myCall(obj, '唱', '跳', 'rap') // ljx 唱 跳 rap
print.myCall(undefined, '唱', '跳', 'rap') // undefined  唱 跳 rap
print.myCall(null, '唱', '跳', 'rap') // undefined  唱 跳 rap
print.myCall(123, '唱', '跳', 'rap') // undefined  唱 跳 rap
print.myCall('123', '唱', '跳', 'rap') // undefined  唱 跳 rap
print.myCall(0, '唱', '跳', 'rap') // undefined  唱 跳 rap
print.myCall('', '唱', '跳', 'rap') // undefined  唱 跳 rap
print.myCall([], '唱', '跳', 'rap') // undefined  唱 跳 rap
print.myCall(['123'], '唱', '跳', 'rap') // undefined  唱 跳 rap
print.myCall(true, '跳', 'rap') // undefined 跳 rap undefined
print.myCall(false,  'rap') // undefined rap undefined undefined

手写apply

// @ts-nocheck

Function.prototype.myApply= function (instance, args = []) {
  // 如果传入的实例是null、undefined就指向全局作用域
  if (instance == null) instance = global
  // 如果传入的是值类型
  if (typeof instance !== 'object') instance = new Object(instance)

  // 注意防止污染全局作用域
  const symbol = Symbol('fn')
  instance[symbol] = this
  
  const res = instance[symbol](...args)
  
  delete instance[symbol] 
  
  return res
}

const obj = {
  name: 'ljx'
}

function print(a,b,c) {
  console.log(this.name, a,b,c)
}

print.myApply(obj) // ljx undefined undefined undefined
print.myApply(obj, ['唱', '跳', 'rap']) // ljx 唱 跳 rap
print.myApply(undefined, ['唱', '跳', 'rap']) // undefined  唱 跳 rap
print.myApply(null, ['唱', '跳', 'rap']) // undefined  唱 跳 rap
print.myApply(123, ['唱', '跳', 'rap']) // undefined  唱 跳 rap
print.myApply('123', ['唱', '跳', 'rap']) // undefined  唱 跳 rap
print.myApply(0, ['唱', '跳', 'rap']) // undefined  唱 跳 rap
print.myApply('', ['唱', '跳', 'rap']) // undefined  唱 跳 rap
print.myApply([], ['唱', '跳', 'rap']) // undefined  唱 跳 rap
print.myApply(['123'], ['唱', '跳', 'rap']) // undefined  唱 跳 rap
print.myApply(true, ['跳', 'rap']) // undefined 跳 rap undefined
print.myApply(false, ['rap']) // undefined rap undefined undefined

手写bind

// @ts-nocheck

Function.prototype.myBind = function (instance, ...args) {
  // 如果传入的是null、undefined
  if (instance == null) instance = global
  // 如果是基础类型
  if (typeof instance !== 'object') instance = new Object(instance)

  return (...postArgs) => {
    // 使用Symbol防止污染全局作用域
    const symbol = Symbol('fn')
    instance[symbol] = this
    const res = instance[symbol](...args, ...postArgs)

    delete instance[symbol]

    return res
  }
}

function print(...args) {
  console.log(this)
  console.log(args)
}

const obj = {
  name: 'ljx'
}

// 测试
print.myBind(obj)() // 'ljx'  参数:[] 
print.myBind(undefined)() // 'undefined' 参数:[] 
print.myBind(undefined)('hzc') // 'undefined' 参数:['hzc']
print.myBind(undefined, 'dys')() // 'undefined' 参数:['dys']
print.myBind(undefined, 'dys')('hzc') // 'undefined' 参数:['dys', 'hzc']
print.myBind(null, 'dys')('hzc') // 'undefined' 参数:['dys', 'hzc']
print.myBind(obj, 'dys')('hzc') // 'ljx' 参数:['dys', 'hzc']
print.myBind(true, 'dys')('hzc') // 'undefined' 参数:['dys', 'hzc']
print.myBind(false, 'dys')('hzc') // 'undefined' 参数:['dys', 'hzc']
print.myBind(false, 'dys')('hzc') // 'undefined' 参数:['dys', 'hzc']
print.myBind('', 'dys')('hzc') // 'undefined' 参数:['dys', 'hzc']
print.myBind(0, 'dys')('hzc') // 'undefined' 参数:['dys', 'hzc']
print.myBind('123', 'dys')('hzc') // 'undefined' 参数:['dys', 'hzc']
print.myBind(123, 'dys')('hzc') // 'undefined' 参数:['dys', 'hzc']

手写bind、call、apply小结

callapply实现仅有传参不同

核心:利用对象方法内的this指向该对象,实现this绑定

注意:使用symbol防止污染全局作用域,返回执行结果前,清除symbol对应的方法

GlobalThis

GlobalThis 是一个全局对象,在不同的 JavaScript 环境中代表全局作用域的对象。它提供了一种标准化的方式来访问全局对象,不依赖于具体的 JavaScript 运行环境。

在浏览器环境中,全局对象是 window 对象,可以通过 window 访问全局变量和函数。

Node.js 环境中,全局对象是 global 对象,可以通过 global 访问全局变量和函数。

在各种环境和 JavaScript 引擎中,GlobalThis 可以用来替代直接使用特定的全局对象,以减少代码对特定环境的依赖性。它适用于在不同的 JavaScript 环境中编写可移植的代码。

例如,可以使用 GlobalThis 来访问全局对象中的 setTimeout 函数:

const { setTimeout } = GlobalThis;
setTimeout(() => {
  console.log('Hello, world!');
}, 1000);

GlobalThis 是在 ECMAScript 2020 标准中引入的,可能在部分老旧的 JavaScript 运行环境中不被支持。但可以通过 polyfill 或使用其他方法来模拟实现。

原型和原型链

前言: js本身是基于原型链进行继承的语言

原型是对象的一个属性,用于共享方法和属性。

每个JavaScript对象都有一个原型对象,它包含对象的共享方法和属性。当我们创建一个新对象时,它会自动从其原型对象中继承属性和方法。

原型是通过使用原型链来实现的。原型链是一个对象到其原型对象的链式连接。如果一个对象无法找到所需的属性或方法,它会顺着原型链向上查找,直到找到或者到达原型链的末端(null)为止。

每个JavaScript对象都有一个 __proto__ 属性,它指向该对象的原型。通过这个属性,我们可以访问和修改对象的原型。

3天整理的3W3千字的JS年度毒打!

原型关系:

  • 每个class都有显示原型prototype
  • 每个实例都有隐式原型__proto__
  • 实例的__proto__指向它的构造函数的prototype

class和继承

class的本质是一个函数

继承:extends,super,扩展,重写

// @ts-nocheck
type Gender = 'male' | 'female'

class People {
  name: string
  age: number
  height: number
  gender: Gender
  talent: string
  constructor(name: string, age: number, height: number, gender: Gender) {
    this.name = name
    this.age = age
    this.height = height
    this.gender = gender
    this.talent = '生存'
  }

  eat(food: string) {
    console.log(`吃 ${food}`)
  }

  doSports() {
    console.log(`做运动`)
  }
}

class Student extends People {
  constructor(name: string, age: number, height: number, gender: Gender) {
    super(name, age, height, gender)
    this.talent = '学习'
  }

  learn(course: string) {
    console.log(`学习 ${course}`)
  }

  // 重写父类的eat方法
  eat() {
    console.log('吃学生餐')
  }
}

const xiaoMing = new Student('小明', 15, 180, 'male')

const strange = new People('陌生人', 20, 172, 'male')

// 继承关系
console.log(xiaoMing instanceof Student) // true
console.log(xiaoMing instanceof People) // true
console.log(strange instanceof Student) // false
console.log(strange instanceof People) // true

// 验证 实例的隐式原型指向它的构造函数的显示原型
console.log(xiaoMing.__proto__ === Student.prototype) // true

// 原型链验证
console.log(Student.prototype.__proto__ === People.prototype) // true
console.log(People.prototype.__proto__.__proto__) // null
console.log(People.prototype.__proto__ === Object.prototype) // true


xiaoMing.learn('数学')
// 继承父类的方法
xiaoMing.doSports()
xiaoMing.eat() // 吃学生餐
//@ts-ignore
// xiaoMing.attack() // 报错
console.log(xiaoMing.talent) // 生存

如何调用到继承的方法?

3天整理的3W3千字的JS年度毒打!

3天整理的3W3千字的JS年度毒打!

  1. 先在自身属性与方法中查找
  2. 如果找不到,就去__proto__(构造函数的prototype)中查找

手写instanceof

// 手写 instanceof
const myInstanceof = (instance: any, classInstance: any) => {
  let p = instance.__proto__
  while (p) {
    if(p === classInstance.prototype) return true
    p = p.__proto__
  }
  return false
}

console.log(myInstanceof(xiaoMing, Student)) // true
console.log(myInstanceof(xiaoMing, People)) // true

console.log(myInstanceof(strange, Student)) // false
console.log(myInstanceof(strange, People)) // true

核心:通过指针不停指向一个个构造函数的显示原型,看能否找到匹配的

new Object() 与 Object.create()的区别

// @ts-nocheck

const a = {}
const b = new Object()

// 字面量的方式与new Object()一致
console.log(a.__proto__ === b.__proto__) // true
console.log(a.__proto__ === Object.prototype) // true

const c = Object.create(null)
console.log(c.__proto__ === Object.prototype) // false
console.log(c.__proto__) // undefined
// c.toString() // 报错

// 如果指定了原型,最终原型链的尽头仍然是Object.protype和null
const d = Object.create({})
console.log(d.__proto__ === Object.prototype) // false
console.log(d.__proto__) // {}
console.log(d.__proto__.__proto__ === Object.prototype) // true
console.log(d.toString()) // [object Object]

字面量{ }等同于new Object(), 它们的隐式原型指向Object.prototype

3天整理的3W3千字的JS年度毒打!

  • Object.create()则是用来创建具有指定原型或具有空原型的对象。
  • 如果指定了原型,最终原型链的尽头仍然是Object.protypenull,并且可以调用原型链上的方法
  • 如果没有指定原型,则丧失了对象的原生方法

new 一个对象的过程中发生了什么?

  1. 创建一个空对象obj,继承构造函数的原型
  2. 执行构造函数,令this指向obj
  3. 返回obj
// @ts-nocheck
function myNew<T>(constructor: Function, ...args: Array<any>): T {
  //  以构造函数的原型为原型,创建对象
  const obj = Object.create(constructor.prototype)
  constructor.apply(obj, args)
  console.log(obj)
  return obj
}

// TypeError: Class constructor Obj cannot be invoked without 'new' 

// class Obj {
//   name: string
//   age: number
//   constructor(name: string, age: number) {
//     this.age = age
//     this.name = name
//   }
//   eat() {
//     console.log('干饭')
//   }
// }

function Obj(name: string, age: number) {
  this.name = name
  this.age = age
}

Obj.prototype.eat = function () {
  console.log(`${this.name} 开始干饭`)
}

const obj = myNew<Obj>(Obj, 'ljx', 18)
obj.eat() // ljx 开始干饭

当前版本,无论是在编辑器还是浏览器中都已经不允许对class使用new以外的执行方式,所以需要使用ES5的构造函数

注意: this问题

原型对象和构造函数有什么区别?

构造函数是用于创建对象的函数。它使用 new 关键字和函数名称来创建一个对象。构造函数可以理解为一个class的模板,它定义了对象的初始状态和行为。

原型是一个对象,它包含可以由该类型的所有实例共享的属性和方法。每个JavaScript对象都有一个原型,它可以通过 __proto__ 属性访问到。

异步

为什么需要异步?

  1. 因为js是单线程的,无论是在浏览器还是nodeJS
  2. 浏览器中js执行和DOM渲染共用一个线程
  3. 异步分为宏任务和微任务

宏任务和微任务

  • 宏任务:setTimeoutsetInterval、网络请求
  • 微任务:promiseasync awaitmutationObserver
  • 微任务在下一轮DOM渲染之前执行,宏任务在DOM渲染之后执行

Event Loop

浏览器的Event loop

3天整理的3W3千字的JS年度毒打!

  1. 当异步任务开始执行时,优先执行微任务
  2. 直到微任务队列为,再执行宏任务
  3. 无论是执行宏任务还是微任务
  4. 每次执行完成后检查先还有没有微任务,有就优先执行微任务(继续回到第一步)

Promise

价值:解决回调地狱

三种状态:

  1. Pending:进行中
  2. Fulfilled:已完成
  3. Rejected: 已失败

进阶: 可通过async await语法糖使代码更简洁

DOM

DOM的本质是将文档解析为树形结构,其中每个节点都是一个对象,代表文档中的一个元素、属性、文本或其它类型的信息。这些节点可以相互关联形成父子关系,即一个节点可以包含子节点,并且可以通过父节点访问到子节点。通过使用DOM API,开发人员可以访问和操作这些节点,从而改变文档的结构和内容。

window对象和document对象

window 对象表示浏览器的窗口,它是 JavaScript 访问浏览器窗口的接口。window 对象具有很多属性和方法,用于处理窗口的尺寸、位置、打开或关闭窗口、发送和接收消息等操作。window 对象还提供了全局的 setTimeoutsetInterval 和 clearTimeout 等方法,用于定时执行代码和处理异步操作。

document 对象表示当前窗口的文档,它是 JavaScript 操作网页内容的接口。document 对象具有属性和方法,可用于访问和操作 HTML 文档的各个部分,如元素、属性、样式、事件等。通过 document 对象,可以选择元素、修改元素的内容、样式和属性,监听和响应事件等操作。

可以将 window 对象看作是整个浏览器窗口的全局对象,提供和窗口相关的功能。而 document 对象是窗口内部的文档对象,提供和文档内容相关的功能。因此document === window.document返回true

总结来说,window 对象是整个浏览器窗口的接口,提供了访问和控制窗口的方法和属性。document 对象是当前窗口的文档对象,提供了操作和处理文档内容的方法和属性。两者是密切相关的,但具有不同的功能和用途。

HTMLCollection和NodeList有什么区别?

相同:

  1. HTMLCollectionNodeList都是表示一组HTML元素。

  2. HTMLCollectionNodeList都是类数组对象,都可以通过 Array.from() 、 Array.prototype.slice.call() 将其转换为真正的数组进行处理。

不同:

  1. 集合类型不同:HTMLCollection 表示由标签名或类名等条件筛选出的元素集合,而 NodeList 表示由节点集合组成的列表,可以包含各种类型的节点。

  2. 获取方式不同:HTMLCollection 可以通过元素的属性(如 document.getElementsByName()element.getElementsByTagName() 等)获取,而 NodeList 可以通过选择器(如 document.querySelectorAll())或特定的属性(如 element.childNodes)获取。

  3. 遍历方式不同:HTMLCollection 通常是一个实时集合,即获取时是动态的,会自动更新,可以使用 for 循环或下标直接访问元素。而 NodeList 通常是一个静态集合,获取的是一个快照,不会自动更新,可以使用 forEach()for...of循环或下标直接访问元素。

  4. 方法支持不同:HTMLCollection 对象有额外的方法和属性,如 namedItem() 方法可以根据元素的 name 属性获取元素,refresh() 方法可以手动刷新集合。而 NodeList 对象相对较简单,没有额外的方法和属性。

property与attribute区别

  • property: 不会体现到html结构中
  • attribute: html标签上的属性,修改时会改变html结构
  • 二者都有可能引起DOM重新渲染

结构操作

结构操作:围绕DOM的增删查改

新增/插入:

const div1 = document.getElementById('div1')

// 创建新节点
const p1 = document.createElement('p')
p1.innerHTML = 'p1'
div1.appendChild(p1)

移动节点:

const div1 = document.getElementById('div1')

const p2 = document.getElementById('p2')

div1.appendChild(p2)

获取父元素节点:

const div1 = document.getElementById('div1')
const parent = div1.parentNode

获取子元素节点列表:

const div1 = document.getElementById('div1')
const childList = div1.childNodes

删除节点:

const div1 = document.getElementById('div1')
const childList = div1.childNodes
div1.removeChild(childList[0])

优化: 频繁新增时思考是否可用Fragment

事件

事件冒泡与事件捕获

3天整理的3W3千字的JS年度毒打!

事件冒泡:

事件先在发生元素上被触发,然后逐级向上冒泡到父元素,直至达到文档根节点(或根节点定义的捕获阶段的事件侦听器)。事件会从最具体的元素开始向父元素传播。

事件捕获:

事件从文档根节点(或根节点定义的捕获阶段的事件侦听器)开始传播,然后逐级向下捕获到发生事件的元素,最终在元素本身上触发事件。换句话说,事件会从最不具体的元素开始向下捕获。

事件代理

由于相同事件绑定数量过多,会产生大量监听器,统一把事件绑定放到父元素上去做,减少内存占用。

如:地图打点,每个点的事件由图层统一监听

h5端300ms点击延迟

为什么会有300ms点击延迟?

移动端浏览器的300ms点击延迟是由于浏览器为了判断用户是要进行双击缩放操作还是要进行单击操作,所以在用户点击后会等待一段时间来判断用户是否会进行双击操作。虽然这种机制对某些特定场景(如双击缩放)是有用的,但对于大部分的移动应用来说,这个延迟会给用户带来较差的交互体验。

如何解决300ms点击延迟
  1. 使用FastClick库,它的原理是通过在触发点击事件的时候,立即模拟触发一个 click 事件,从而绕过浏览器的点击延迟

  2. 使用CSS属性 touch-action: manipulation,可以告诉浏览器不要进行双击缩放操作,从而减少点击延迟。例如:

    html {
      touch-action: manipulation;
    }
    

    这样设置后,点击事件会更加即时地触发。

  3. 使用 meta 标签禁用缩放, 从而减少浏览器的点击延迟。例如:

    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    

    这样设置后,用户将无法进行手势缩放,从而减少点击延迟。

BOM

  • navigator
  • screen
  • location
  • history

如何检测浏览器类型

navigator.userAgent // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36

location

history

history.back()
history.forward()

JS中的observer们

  1. MutationObserver:用于监视DOM树中的变化,比如节点的添加、删除、属性修改等。

  2. IntersectionObserver:用于监视目标元素与其祖先元素或根元素交叉的情况,即目标元素在视口中是否可见。

  3. ResizeObserver:用于监视元素的大小变化,包括宽度、高度、内容区域大小等。

MutationObserver

3天整理的3W3千字的JS年度毒打!

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
      height: 100vh;
      margin: 0;
    }

    .center {
      margin: auto;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
    }

    .container {
      height: 50vh;
      width: 50vh;
      background-color: aqua;

      display: flex;
      flex-wrap: wrap;
    }
  </style>
</head>

<body>
  <div id='box' class="center container">
  </div>

  <script>
    // 创建一个DOM,模拟插入
    const targetDiv = document.createElement('targetDiv')
    targetDiv.style.width = '100px'
    targetDiv.style.height = '100px'
    targetDiv.style.backgroundColor = 'red'

    setTimeout(() => {
      box.appendChild(targetDiv)
    }, 9000)

    // 被观察的DOM
    const box = document.getElementById('box');

    // 创建一个新的MutationObserver实例
    const observer = new MutationObserver((mutationsList, observer) => {
      for (let mutation of mutationsList) {
        if (mutation.type === 'childList') {
          const addedNodes = Array.from(mutation.addedNodes);
          // 检查是否有新的元素被添加到DOM中
          if (addedNodes.includes(targetDiv)) {
            console.log('目标元素已进入页面')
          }
        }
      }
    });

    // 配置观察选项
    const config = {
      childList: true,
      subtree: true
    };

    // 开始观察目标元素及其子孙元素的变化
    observer.observe(box, config);
  </script>
</body>

</html>

IntersectionObserver

// 目标元素
const targetElement = document.getElementById('target');

// 创建IntersectionObserver
const observer = new IntersectionObserver((entries, observer) => {
   for(let entry of entries) {
      if(entry.isIntersecting) {
         console.log('目标元素进入视口');
      }
   }
});

// 开始观察目标元素
observer.observe(targetElement);

ResizeObserver

// 目标元素
const targetElement = document.getElementById('target');

// 创建一个ResizeObserver
const observer = new ResizeObserver((entries, observer) => {
   for(let entry of entries) {
      const { width, height } = entry.contentRect;
      console.log('目标元素的大小发生变化:', width, height);
   }
});

// 开始观察目标元素的大小变化
observer.observe(targetElement);

三种观察者用法一致,根据场景使用

性能

防抖debounce 和 节流throttle

防抖 3天整理的3W3千字的JS年度毒打! 限制执行次数,多次密集触发只执行一次

节流 3天整理的3W3千字的JS年度毒打! 限制频率,有节奏地执行

节流关注过程,防抖关注结果

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

</head>

<body>
  <div id="outer">
    <div id="inner">
      Click me!
    </div>
  </div>

</body>

<script lang="js">
  // 节流
  function throttle(fn, delay = 500) {
    let timeout
    return function (...args) {
      if (timeout) return
      timeout = setTimeout(() => {
        console.log(this)
        fn.apply(this, args)
        clearTimeout(timeout)
        timeout = null
      }, delay)
    }
  }

  // 防抖
  function debounce(fn, delay = 500) {
    let timeout
    return function (...args) {
      if (timeout) {
        clearTimeout(timeout)
        timeout = null
      }
      timeout = setTimeout(() => {
        fn.apply(this, args)
        clearTimeout(timeout)
        timeout = null
      }, delay)
    }
  }

  const obj = {
    name: 'jjlk'
  }

  const handleClick = debounce(function () {
    console.log(this.name) // jjlk
  })

  document.getElementById('inner').addEventListener('click', handleClick.bind(obj))
</script>

</html>

注意:this指向!

requestIdleCallback 和 requestFrameAnimation

区别: requestAnimationFrame每一帧都会执行,高优 requestIdleCallback空闲时才执行,低优

3天整理的3W3千字的JS年度毒打!

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
      height: 100vh;
      margin: 0;
    }

    .center {
      margin: auto;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
    }

    .container {
      height: 50vh;
      width: 50vh;
      background-color: aqua;

      display: flex;
      flex-wrap: wrap;
    }
  </style>
</head>

<body>
  <div id='box' class="center container">
  </div>


  <script>
    const box = document.getElementById('box')

    document.addEventListener('DOMContentLoaded', () => {
      let curAngle = 0
      const maxAngle = 360

      function rotate() {
        curAngle = curAngle + 1
        box.style.transform = `rotate(${curAngle}deg)`
        if (curAngle < maxAngle) {
          window.requestAnimationFrame(rotate)
          // window.requestIdleCallback(rotate)
        } else {
          curAngle = 0
          window.requestAnimationFrame(rotate)
          // window.requestIdleCallback(rotate)
        }
      }

      rotate()
    })
  </script>
</body>

</html>

requestAnimationFramerequestIdleCallback都是宏任务

拷贝

深拷贝和浅拷贝的区别是什么?

深拷贝浅拷贝的区别在于拷贝后的对象和原对象之间的关系以及对于引用类型的处理。

浅拷贝:

  • 浅拷贝是指创建一个新对象,该对象只是原对象的一个副本,但是它们共享同一个内存地址。拷贝后的对象和原对象内部的引用类型数据(如对象、数组等)仍然指向同一片内存空间。
  • 当修改拷贝后的对象内部的引用类型数据时,会影响到原对象。
  • 浅拷贝通常使用一些简单的方法,如Object.assign()、数组的slice()等。

深拷贝:

  • 深拷贝是指创建一个新的对象,该对象和原对象完全独立,拷贝后的对象和原对象之间不共享内存地址。
  • 修改拷贝后的对象内部的引用类型数据不会影响到原对象。
  • 深拷贝会递归地复制原对象及其所有引用类型数据,直到所有的引用类型数据都被复制。
  • 深拷贝通常需要使用递归或者序列化的方式实现,如JSON.parse(JSON.stringify())、递归赋值属性等。

如何实现深拷贝?包括Mao、Set,考虑循环引用

普通深拷贝只考虑了ObjectArray,无法转换MapSet和循环引用

为什么需要考虑循环引用?什么时候会发生循环引用? 3天整理的3W3千字的JS年度毒打! 如图,循环引用在普通深拷贝时会死循环,爆栈

const cloneDeep = (element: any, weakMap = new WeakMap()) => {
  // null undefined 处理
  if (!element) return
  // 基础类型处理
  if (typeof element !=='object') return element

  // 防止循环引用
  const elementInMap = weakMap.get(element)
  // 直接返回之前的对象,保证与克隆对象一致的引用关系
  if(elementInMap) return elementInMap

  // 数组处理
  if (Array.isArray(element)) {
    const arr: Array<any> = []
    weakMap.set(element, arr)

    element.forEach((item) => {
      const newItem = cloneDeep(item, weakMap)
      arr.push(newItem)
    })

    return arr  
  }

  // Map处理
  if (element instanceof Map) {
    const temp = new Map()
    weakMap.set(element, temp)

    element.forEach((v, k) => {
      const key = cloneDeep(k, weakMap)
      const value = cloneDeep(v, weakMap)
      temp.set(key, value)
    })

    return temp
  }

  // Set处理
  if (element instanceof Set) {
    const temp = new Set()
    weakMap.set(element, temp)

    element.forEach((v) => temp.add(cloneDeep(v, weakMap)))

    return temp
  }

  // 对象处理
  const obj: Record<string, any> = {}
  weakMap.set(element, obj)

  const keys = Object.keys(element)
  keys.forEach((k: any) => {
    obj[k] = cloneDeep(element[k], weakMap)
  })

  return obj
}

const set = new Set([{test: 'dys', o: {ok: true}},1,2,3,''])

const map = new Map<any, any>([
  ['dys', {test: 'dys', o: {ok: true}}],
  ['123', '123']
])

const obj: any = {
  name: 'ljx',
  obj: {
    name: 'dys',
  },
  arr: [1, 2, 3, [4, 5, 6]],
  is: true,
  no: undefined,
  se: set,
  map,
  fn: () => {console.log('123 ')}
}

obj.b = obj
obj.se.add(set)
obj.map.set('33', map)

const ectype = cloneDeep(obj)
ectype.fn()

我们使用weakMap作为cloneDeep函数顶层作用域的变量,进行透传,每次clone前,查看克隆对象是否是被拷贝过的,防止陷入死循环并复刻其引用关系

核心:每次拷贝前,使用weakMap记录下生成的正本、副本

如何实现数组扁平化?

  1. 定义一个空数组arr = [],遍历当前数组
  2. 如果item非数组,添加到arr
  3. 如果item是数组,则遍历之后累加到arr
const flatten = (rawArr: Array<any>) => {
  let res: Array<any> = []

  rawArr.forEach((item) => {
    if (!Array.isArray(item)) {
      res.push(item)
    } else {
      res = res.concat(flatten(item))
    } 
  })


  return res
}

// 测试
console.log(flatten([1, 2, 3, 4, [5, 6, 7, 8]])) //[1,2,3,4,5,6,7,8]
console.log(flatten([1, [2, 3, 4, [5, 6, 7, 8]]])) //[1,2,3,4,5,6,7,8]
console.log(flatten([1, 2, 3, 4, [5, [6, 7], 8]])) //[1,2,3,4,5,6,7,8]
console.log(flatten([[],[1, 2], 3, 4, [5, 6, 7, 8]])) //[1,2,3,4,5,6,7,8]
console.log(flatten([1,2,3,4,[5,6,7,8],[]])) //[1,2,3,4,5,6,7,8]

如何实现一个EventBus?

核心: 理解EventBus的每个功能、单例

class FnRecord {
  isOnce: boolean
  fn: any
  constructor(fn: any, isOnce: boolean ) {
    this.fn = fn
    this.isOnce = isOnce
  }
}
  
class EventBus {
  private static instance: EventBus
  protected map: Map<string, Array<FnRecord>>
  private constructor() {
    this.map = new Map()
  }

  static getInstance() {
    if (!EventBus.instance) {
      EventBus.instance = new EventBus()  
    }
    return EventBus.instance
  }

  private addEvent(eventType: string, fn: any, isOnce: boolean) {
    const record = new FnRecord(fn, isOnce)
    let recordList = this.map.get(eventType) || []
    recordList = recordList.concat(record)
    this.map.set(eventType, recordList)
  }

  on(eventType: string, fn: any, isOnce = false) {
    this.addEvent(eventType, fn, isOnce)
  }

  once(eventType: string, fn: any) {
    this.addEvent(eventType, fn, true)
  }

  emit(eventType: string, ...args: Array<any>) {
    let recordList = this.map.get(eventType)
    if(!recordList) throw new Error('暂无该事件')
    recordList = recordList.filter(({ fn, isOnce }) => {
      fn(...args)
      return !isOnce
    })
    this.map.set(eventType, recordList)
  }

  off(eventType?: string, fn?: any): boolean {
    // 移除所有类型所有事件
    if (!eventType) {
      this.map.clear()
      return true
    }

    // 全部对应类型的全部事件
    if (!fn) {
      return this.map.delete(eventType)
    }

    // 单个移除处理
    let recordList = this.map.get(eventType)
    if (!recordList) return false
    recordList = recordList.filter(({ fn: f }) => f !== fn)
    if (recordList.length) {
      this.map.set(eventType, recordList)
      return true
    } else {
      return this.map.delete(eventType)
    }
  }
}

// 单例测试
// new EventBus() // 类“EventBus”的构造函数是私有的,仅可在类声明中访问

const bus = EventBus.getInstance()
const bus2 = EventBus.getInstance()

console.log(bus === bus2) // true

// on、once、emit测试
bus.on('add', () => {
  console.log('add')
})

bus.on('add', () => {
  console.log('add2')
}, true)

bus.on('sub', () => {
  console.log('sub')
})

bus.once('sub', () => {
  console.log('sub2')
})

bus.emit('add') // add add2
bus.emit('add') // add
bus.emit('sub') // sub sub2
bus.emit('sub') // sub

// 移除事件类型测试
console.log(bus.off('add')) // true
// bus.emit('add') // Error: 暂无该事件

// 移除指定执行函数测试
const add3 = () => {
  console.log('add3')
}
const add4 = (a: any,b: any,c: any) => {
  console.log('add4')
  console.log(a,b,c)
}
bus.on('add', add3)
bus.emit('add') // add3
bus.on('add', add4)
bus.emit('add') // add3 add4 undefined undefined undefined

// 传参测试
console.log(bus.off('add', add3)) // true
bus.emit('add', 123, {a: 1}, false) // add4 123 {a: 1} false
// bus.emit('add') // Error: 暂无该事件

// 清空所有类型测试
bus.off()
console.log(bus) // {map: {}}

如何实现一个LRU?

LRU(Least Recently Used)是一种缓存淘汰策略,用于选择最近最少使用的缓存对象进行淘汰。

LRU的基本思想是,通过记录每个缓存对象的访问顺序,最近被访问的对象被认为是最有可能再次访问的,而最久未被访问的对象则被认为是最有可能被淘汰的。

应用场景: 3天整理的3W3千字的JS年度毒打!

分析: 3天整理的3W3千字的JS年度毒打!

  1. LRU有长度限制
  2. 人不够拼命招人
  3. 人够了,新人挤掉老人
  4. 老人挪位置,不管你人够不够

新增的时候: 3天整理的3W3千字的JS年度毒打!

  1. header是你部门的头头,习惯性欣赏'新人'
  2. tail是把屠刀永远指向下次要被裁员的那个'老人'
  3. LRU的长度则是你们部门规定的人数 3天整理的3W3千字的JS年度毒打! 现在又来了一个'新人',屠刀指向下一个'老人' 3天整理的3W3千字的JS年度毒打! 某天,某个同事立了大功,功劳大大的!领导说了一句吆西~ 3天整理的3W3千字的JS年度毒打!

本质:双指针滑动窗口、双向链表队列

class DoubleLinkedNode<T=any> {
  value: T
  prev?: DoubleLinkedNode
  next?: DoubleLinkedNode
  constructor(value: T) {
    this.value = value
  }
}

class LRU<T = any> {
  curSize: number
  maxSize: number
  header: DoubleLinkedNode | undefined
  tail: DoubleLinkedNode | undefined
  constructor(maxSize: number) {
    if(maxSize < 1) throw new Error('长度必须大于0')
    this.maxSize = maxSize
    this.curSize = 0
  }

  put(value: T) {
    // 初始化
    if (!this.header) {
      const node = new DoubleLinkedNode<T>(value)
      this.header = node
      this.tail = node
      this.curSize++
      return true
    }
    const repeatNode = this.getRepeatNode(value)
    // 老人挪位置(只针对原来的链表操刀即可)
    if (repeatNode) {
      this.moveNode(repeatNode)
    } else {   
      // 新增
      this.addNewNode(value)
    }
    return true
  }

  /**
   * @description: 获取重复节点
   * @param {T} value
   */  
  private getRepeatNode(value: T) {
    if(!this.header) return
    let node: DoubleLinkedNode | undefined = this.header
    // 遍历到链表尽头 ,首位指针相碰即可
    while (node) {
      if (node.value === value) return node
      node = node.next
    }
  }

  /**
   * @description: 新增节点
   * @param {T} value
   */  
  private addNewNode(value: T) {
    if(!this.header) return
    // 新增
    const newNode = new DoubleLinkedNode(value)
    this.header.prev = newNode
    newNode.next = this.header
    this.header = newNode 
    // 如果满载移动尾部指针
    if (this.curSize === this.maxSize && this.tail) {
      const prevNode = this.tail.prev as DoubleLinkedNode
      prevNode.next = undefined
      this.tail.prev = undefined
      this.tail = prevNode
    } else {
      this.curSize++
    }
  }
  
  /**
   * @description: 移动节点
   * @param {DoubleLinkedNode} repeatNode
   */  
  private moveNode(repeatNode: DoubleLinkedNode) {
    if (this.curSize === 1 || !this.header) return
    if(repeatNode === this.header) return
    const prevNode = repeatNode.prev
    const nextNode = repeatNode.next
    if (prevNode) prevNode.next = nextNode
    if (nextNode) nextNode.prev = prevNode
    repeatNode.prev = undefined
    this.header.prev = repeatNode
    repeatNode.next = this.header
    this.header = repeatNode
  }

  get list() {
    let p: DoubleLinkedNode | undefined = this.header
    const arr: Array<T> = []
    while (p) {
      arr.push(p.value)
      p = p.next
    }
    return arr
  }
}

const lru = new LRU(5)

// 非满载情况下添加数据测试
lru.put(0)
console.log(lru.list) // [0]
lru.put(1)
console.log(lru.list) // [1,0]
lru.put(2)
console.log(lru.list) // [2,1,0]
lru.put(3)
console.log(lru.list) // [3,2,1,0]
lru.put(4)
console.log(lru.list) // [4,3,2,1,0]

// 添加重复的最新数据测试
lru.put(4)
console.log(lru.list) // [4,3,2,1,0]
lru.put(0)
console.log(lru.list) // [0,4,3,2,1]
lru.put(0)
console.log(lru.list) // [0,4,3,2,1]

// 添加重复的中段数据测试
lru.put(3)
console.log(lru.list) // [3,0,4,2,1]
lru.put(2)
console.log(lru.list) // [2,3,0,4,1]

// 添加重复的尾端数据测试
lru.put(1)
console.log(lru.list) // [1,2,3,0,4]
lru.put(4)
console.log(lru.list) // [4,1,2,3,0]

// 添加全新引用类型数据测试
const obj = {name: 'ljx'}
lru.put(obj)
console.log(lru.list) // [{name: 'ljx'},4,1,2,3]
lru.put(obj)
console.log(lru.list) // [{name: 'ljx'},4,1,2,3]
lru.put(4)
console.log(lru.list) // [4, {name: 'ljx'},1,2,3]
lru.put(0)
console.log(lru.list) // [0,4, {name: 'ljx'},1,2]

思路2:使用Map代替链表,利用Map新增元素会放在最前方,效率低于链表

如何实现一个LazyMan?

本质: 考察对于异步的理解

核心:初始化时,使用异步执行下一个任务!

class LazyMan{
  name: string
  private fnQueue: Array<any>

  constructor(name: string) {
    console.log('初始化')
    this.name = name
    this.fnQueue = []
    
    // 核心!!!让任务开始执行!!!
    setTimeout(() => {
      this.nextFn()
      console.log('开始执行任务')
    }) 
  }
  eat(food: string) {
    console.log('添加eat任务')
    const fn = () => {
      console.log(`${this.name} eat ${food}`)
      this.nextFn()
    }
    this.fnQueue.push(fn)
    return this
  }
  sleep(time: number) {
    console.log('添加sleep任务')
    const fn = () => {
      const timer = setTimeout(() => {
        console.log(`${this.name} sleep ${time} s`)
        this.nextFn()
        clearTimeout(timer)
      }, time * 1000)
    }
    this.fnQueue.push(fn)
    return this
  }
  private nextFn() {
    const [curFn = undefined, ...newFnQueue] = this.fnQueue
    this.fnQueue = newFnQueue
    if (curFn) curFn()
  }
}

const ljx = new LazyMan('ljx')

ljx.sleep(5).eat('香蕉').sleep(2).eat('苹果').sleep(3).eat('香肠').sleep(4).eat('小猫咪')

3天整理的3W3千字的JS年度毒打! constructor内的异步任务在所有同步任务执行完成后开始执行!

难点:第一个任务怎么执行?