3W3千字的年度JS毒打!
JS语言特性
- 弱类型:
JavaScript
是一种弱类型语言,声明变量时不用指定其数据类型,并且可以在运行时更改变量的类型。 - 动态语言:
JavaScript
是一种动态语言,可以在运行时添加、删除和修改对象的属性和方法。 - 事件驱动:
JavaScript
可以响应用户操作和事件,例如鼠标点击、键盘输入等,可以编写逻辑来处理这些事件。 - 客户端与服务器端都可运行:
JavaScript
最初被设计为在Web浏览器中运行,但现在也可以在服务器端(如Node.js
)上运行。 - 垃圾回收:
JavaScript
具有自动垃圾回收机制,它会自动管理不再使用的内存,减轻了开发人员的负担。 - 支持函数式编程:
JavaScript
支持函数式编程范式,允许您将函数作为一等公民来传递、复制和储存,并使用高阶函数来处理数据。 - 支持面向对象:
JavaScript
支持面向对象编程范式,并且提供了类、对象、继承、封装等面向对象的概念和特性。 - 单线程:无论是浏览器还是
nodejs
- 前端交互性:
JavaScript
可以与HTML
和CSS
进行交互,并通过DOM
来操作和修改网页内容。
严格模式有什么特点?
- 全局变量必须先声明才能使用
- 禁止使用
with
- 禁止
this
指向window
- 函数参数不能重名
ES6
自带use strict
, 是天生的严格模式
script标签加载js的3个时机
-
同步加载,
js
优先。html
暂停解析,下载js
并执行,完成后继续解析html
-
async
,html
解析与js
下载并行,js
执行优先级高于html
解析。js
将在下载完成后立即执行 ,会暂停html
页面的解析,待js
执行完继续解析。例如:<script async src="script.js"></script>
。 -
defer
,html
解析与js
下载并行,js
执行优先级低于html
解析。当设置为defer
时,js
将在html
页面解析完毕后执行 ,但在DOMContentLoaded
事件触发之前执行。例如:<script defer src="script.js"></script>
。
JS类型
原始类型:
- boolean
- undefine
- null
- number
- string
- symbol
- bigint
引用类型:Array、Object、function
JS类型如何转换?
显示类型转换
- 转为string:
String()
、.toString()
- 转为number:
Number()
、parseInt()
、parseFloat()
- 转为boolean:
Boolean()
隐式类型转换
什么时候会发生隐式类型转换?
-
字符串和数字之间的运算:
- 当字符串参与加法运算时,其他操作数会隐式转换为字符串并进行拼接。
- 当字符串参与减法、乘法、除法等运算时,字符串会先被转换为数字进行计算。
jsconsole.log("10" + 5); // 输出 "105" console.log("10" - 5); // 输出 5
-
使用比较运算符时:
- 在使用
==
相等运算符比较不同类型的操作数时,JavaScript会进行隐式类型转换以进行比较。 - 在使用关系运算符(如
<
、>
、<=
、>=
)比较不同类型的操作数时,JavaScript也会进行隐式类型转换进行比较。
jsconsole.log("10" == 10); // 输出 true console.log("5" > 1); // 输出 true
- 在使用
-
逻辑运算符:
- 在使用逻辑运算符(如
&&
、||
)进行逻辑运算时,JavaScript会对操作数进行隐式类型转换,并根据转换后的结果确定返回值。
jsif ("" || 0) { console.log("This condition is true"); } else { console.log("This condition is false"); // 输出 }
- 加法运算:
- 当参与加法运算的两个操作数中至少一个为字符串时,会触发字符串拼接的操作,即将两个操作数转换为字符串并进行连接。
- 当参与加法运算的两个操作数中有一个为对象时,会调用该对象的
valueOf()
或toString()
方法将其转换为原始值,然后进行加法运算。 - 当参与加法运算的两个操作数中至少一个为浮点数时,会将整数操作数转换为浮点数来执行运算。
- 当参与加法运算的两个操作数中至少一个为布尔值时,会将布尔值转换为数字(true转换为1,false转换为0)来执行运算。
- 在使用逻辑运算符(如
-
条件语句: 在条件语句(如if语句、三元运算符)中,将非布尔类型的值作为条件时,
JavaScript
会将其隐式转换为布尔值进行判断。jsif ("" || 0) { console.log("This condition is true"); } else { console.log("This condition is false"); // 输出 }
NaN有什么特点?
js
// @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
NaN
是number
类型NaN
是一个唯一值,NaN
不等于NaN
NaN
与任何其他值执行数学运算,结果都是NaN
(加法结果可能是string
)
如何判断是否是NaN?
js
// @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)。
==
比较时,会进行一些规则的判断和转换,如将字符串转换为数字,将布尔值转换为数字等。==
执行的是相等值的比较。
js
console.log(10 == "10"); // 输出 true,进行了隐式类型转换
console.log(true == 1); // 输出 true,进行了隐式类型转换
console.log(null == undefined); // 输出 true
console.log('0' == false); // 输出 true,字符串 '0' 转换为数字 0,再与 false 进行比
===
(严格相等运算符):
===
不会进行隐式类型转换,它要求比较的两个操作数不仅值相等,类型也必须相同。===
执行的是严格相等性的比较。
js
console.log(10 === "10"); // 输出 false,类型不同
console.log(true === 1); // 输出 false,类型和值都不同
console.log(null === undefined); // 输出 false
console.log('0' === false); // 输出 false,类型不同
特殊: 引用类型通过内存地址进行比较
js
// @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
js
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
无法精确的检测null
、Object
、Array
为什么typeof null的结果时'object'?
typeof null
的结果是object
, 但null
是原始类型。
造成这个结果的原因是null
的内存地址是以000开头,而js
会将000开头的内存地址视为object
如何准确检测一个值是null
类型?
js
/**
* @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()
来区分数组与对象
它的实现原理是什么?
js
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() 和 === 有什么区别?
js
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,获取详细的数据类型
js
/**
* @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比较两个引用类型的值是否一样
js
/**
* @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内存
- 哪些数据类型存储在
栈
中?
- string
- number
- boolean
- undefined
- null
- symbol
- bigint
- 哪些数据类型存储在
堆
中? 所有的引用类型
特殊:闭包中定义的所有变量不区分类型,都存储在
堆
中
JS内存垃圾回收用什么算法?
JavaScript
内存垃圾回收使用的是标记清除算法
。它的基本思路是通过标记来追踪哪些内存是仍然被程序使用的,然后清除那些未标记的内存块。
- 垃圾收集器会从根对象(通常是全局对象)开始,标记所有从根对象开始可达的对象。
- 对于标记过的对象,继续递归地标记其引用的对象,直到所有可达对象都被标记。
- 所有未被标记的对象将被视为垃圾,它们所占用的内存将被释放。
- 清除阶段会遍历所有的对象,释放未标记的对象所占用的内存,并将回收的内存块加入空闲列表中,以备后续分配使用。
标记清除算法
相对简单且高效,能够准确地找出并回收不再使用的内存。但它也存在一些缺点,如可能会造成停顿(暂停应用程序执行)和内存碎片化等问题。为了解决这些问题,现代的JavaScript
引擎还会采用其他的垃圾回收策略,如分代回收
和增量标记
等。
增量标记
会将一口气完成的标记任务采用类似于节流的方式进行稀释,拆解为很多小的标记任务,每完成一个小的标记任务,让就js
执行一会儿,再标记,再执行。。。直到标记阶段完成才进入内存碎片的整理上面来。
js内存泄漏场景有哪些?
- 被全局变量、函数引用,组件销毁时未清除
- 被全局事件、定时器引用,组件销毁时未清除
- 被自定义事件引用,组件销毁时未清除
WeakMap和WeakSet的价值是什么?
与Map
和Set
相比,WeakMap
和WeakSet
可以避免内存泄漏
在Map
或 Set
中,如果一个对象被添加到集合中,即使在程序中不再需要这个对象,它仍然会被保留,无法被垃圾回收。因为集合中的对象仍然被集合所引用着,垃圾回收器无法判断这些对象是否不再需要。
而对于 WeakMap
和 WeakSet
,当一个被引用的对象在其他地方没有被引用时,垃圾回收器会自动回收该对象。这种弱引用的特性使得我们可以更容易地避免内存泄漏。
通过 WeakMap
和 WeakSet
,我们可以利用对象的引用作为键,存储与这个对象相关的附加信息,而不会造成原始对象的内存泄漏。当原始对象被垃圾回收时,与之关联的附加信息也会被自动清理。
WeakMap
和 WeakSet
的弱引用特性意味着我们无法像常规的 Map
或 Set
那样遍历所有的键或值。此外,WeakMap
的key
必须是对象,WeakSet
的值也必须是对象。
综上所述,WeakMap
和 WeakSet
可以避免内存泄漏,因为它们不会阻止被引用对象被垃圾回收,并且可以用于关联对象的附加信息而不会造成原始对象的内存泄漏。
函数和箭头函数
箭头函数和寻常函数有什么区别?
-
语法不同
-
绑定
this
:箭头函数没有自己的this
绑定,它地的this
永远指向它定义时候的父作用域。而普通函数的this
值是在调用时动态确定的,根据函数的调用方式来决定this
的值。 -
不能作为构造函数:箭头函数没有原型
prototype
属性,因此不能通过new
关键字来创建对象实例。而普通函数可以作为构造函数,使用new
来创建对象。 -
不绑定
arguments
对象:箭头函数没有自己的arguments
对象,可以通过rest
参数语法 (...
) 来获取函数的参数。而普通函数可以使用arguments
对象获取传入的参数列表。 -
不能使用
yield
关键字:箭头函数不能用作生成器函数,即不能使用yield
关键字进行迭代操作。而普通函数可以通过使用函数*
关键字来定义一个生成器函数。 -
无法通过
apply
、call
、bind
改变this
函数的arguments是什么?
-
arguments
是一个类数组对象,包含了函数调用时传入的所有参数。它可以在函数体内部使用,用来访问传递给函数的所有参数。 -
arguments
对象的长度(即传入的参数个数)是动态的,会随着函数调用时传递的实际参数个数而改变。 -
可以通过修改
arguments
对象的元素来修改实际传递的参数值。 -
callee
属性:callee
属性,指向当前正在执行的函数本身,递归调用时非常有用,可以使用arguments.callee
引用当前函数,而不需要明确指定函数名。
什么时候不能用箭头函数?
- 对象方法
- 原型方法
- 用作构造函数
- 动态上下问中的回调函数
Vue
生命周期和methods
(Vue2
本质是配置对象)
小结:函数内部涉及到
this
,慎重考虑
如何实现一个分治狂魔curry函数?
柯里化
(Currying)是一种将接受多个参数的函数转换为一系列接受单个参数的函数的过程。通过柯里化
,我们可以重复应用函数并部分应用其参数。
js
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()
是什么?
Array.map()
是一个JS
数组原型得高级函数- 它用于对数组的每个元素应用一个
callback
,并返回一个新的数组,新数组中的元素是原始数组经过回调函数处理后的结果。 callback
会接收到两个参数分别是当前的item
和index
,执行完后会返回一个值- 它不会改变原始数组,因此它还是一个纯函数
js
// @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)
的结果是什么?
js
[
parseInt('1', 0),// 1
parseInt('2', 1),// NaN
parseInt('3', 2) // NaN
]
核心在于需要理解parseInt()
parseInt()
接受1个必传参数string
和一个默认参数radix
而后返回一个转换后得整数,重点在于默认参数radix
参数表示进制,会将string
按照什么进制进行转换,默认为10进制
parseInt('1', 0)
表示'1'按照十进制转换,因为0
和undefined
一样,转换为boolean
都是false
,所以内部会将radix
默认赋值为10,最终得到了1
parseInt('2', 1)
表示'2'按照一进制进行转换,可是并没有一进值,所以返回了NAN
parseInt('3', 2)
表示'2'按照二进制进行转换,可是二进制并没有3,所以返回了NAN
如何跳出forEach?
在forEach
中使用return
不会返回,函数还会继续执行 我们可以通过try
,手动抛出异常的方式跳出循环
推荐使用Array.every()
或者Array.some()
代替Array.forEach()
注意:仅仅抛出,不要捕获,捕获会继续执行接下来的循环
如何实现reduce?
首先,理解Array.reduce()
怎么用? 为数组中的所有元素调用指定的回调函数。回调函数的返回值是累积的结果,并在下次调用回调函数时作为参数提供。
@param callback -一个最多接受四个参数的函数。reduce方法对数组中的每个元素调用一次callback函数。
@param initialValue---如果指定了initialValue,将作为初始值开始累积。对callbackfn函数的第一次调用将该值作为参数而不是数组值提供。
js
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'}
使用场景:数组的数据清洗
自定义实现:
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
,用于可枚举 数据,如Object
、string
、Array
for-of
得到value
, 用于可迭代 数据,如Map
、Set
、Array
、String
针对数据类型不同:
- 遍历对象:
for-in
可以,for-of
不可以 - 遍历
Map
,Set
:for-of
可以,for-in
不可以 - 遍历
generator
:for-of
可以,for-in
不可以
js
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 of
和Promise.all
一样,用于并行执行promise
,区别在于Promise.all
需要所有promise
执行完才能获取到返回值,for await of
按顺序获取到返回值
js
// 模拟发送请求
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
VSfor
循环,不需要数据的长度,不需要知道元素的结构,不需要知道数据的结构,forEach
是一个简易的迭代器
迭代器解决了如何更加方便、简易地遍历一个有序的数据集合的问题
- 顺序访问有序结构(如:数组、
NodeList
) - 不知道数据的长度、内部结构
- 高内聚、低耦合
目标性:
for
循环和迭代器
都是为了解决有序数据的遍历问题
js中有序的数据结构:
- 数组
- 字符串
NodeList
等DOM
集合Map
Set
arguments
类数组
注意 :Object
是无序结构
应用场景
Symbol.iterator
。所有的有序数据结构,都内置了Symbol.iterator
这个key
,使用它可以获得该数据结构的迭代器- 自定义迭代器
- 用于
for of
,只要内置了Symbol.iterator
这个key,都可以使用for of
来进行遍历 - 用于数组的解构、扩展操作符、
Array.from
- 用于
Promise.all
和Promise.race
- 用于生成器
yield*
生成器
yield*
语法yield
遍历DOM
树
js
//#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
树
js
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]
,因此我们可以使用yield
或yield *
生成迭代器,借此迭代
遍历一个数组用for和forEach谁快?
在遍历一个数组时,使用for
循环通常比forEach
方法更快。这是因为for
循环是原生的JavaScript
语法,而forEach
是Array
对象的一个方法。
for
循环是一种比较底层的迭代机制,它通过索引直接访问数组元素,因此可以更快地遍历数组。而forEach
方法则是高级抽象的迭代器,它在每次迭代时都会执行一个回调函数,并且不能使用break
和continue
语句来控制迭代流程,因此它的执行速度相对较慢。
作用域和自由变量
作用域
代表着一个变量合法的使用范围
- 全局作用域
- 函数作用域
- 块级作用域
自由变量
一个变量在当前作用域没有被定义,但被使用了,那么会沿着作用域向上查找,直到找到为止,如果全局作用域都没找到,则报错
答案:600
闭包
闭包
的本质是作用域应用的特殊情况,有两种表现:
- 函数作为参数被传递
- 函数作为返回值被返回
答案:100
答案:100
答案:10
闭包:自由变量的查找,实在函数定义的地方,向上级作用域查找,不是从执行的地方向上查找
this
- 作为普通函数使用时,非严格模式下指向
window
- 使用
call
、apply
、bind
时this
指向传入对象, 传入undefined
、null
时指向全局对象 - 作为对象方法被调用时
this
指向当前对象 - 在
class
中this
指向当前实例 - 箭头函数中
this
指向上级作用域的this
答案:1 undefined
this
的取值是在执行时确定的,不是定义时确定的
如何改变this指向?
在JavaScript
中,有几种常见的方式可以改变 this
的指向:
- 使用
call()
方法:call()
方法调用一个函数,并且可以指定函数内部的this
指向。传递给call()
方法的第一个参数是要绑定给this
的对象,后续参数是函数的参数列表。例如:
js
function sayName() {
console.log(this.name);
}
const person = {
name: 'John'
};
sayName.call(person); // 输出: John
- 使用
apply()
方法:apply()
方法与call()
方法类似,也可以指定函数内部的this
指向。区别在于,apply()
方法接收一个数组作为参数列表。例如:
js
function sayName(...arg) {
console.log(this.name + arg[0]);
}
const person = {
name: 'John'
};
sayName.apply(person, [' say Hello']); // 输出: John say Hello
- 使用
bind()
方法:bind()
方法创建一个新的函数,并指定函数内部的this
指向。不同于call()
和apply()
直接调用函数,bind()
返回一个绑定了指定this
的新函数。例如:
js
function sayName() {
console.log(this.name);
}
const person = {
name: 'John'
};
const sayNameWithPerson = sayName.bind(person);
sayNameWithPerson(); // 输出: John
在使用 call()
、apply()
或 bind()
时,如果传递 null
或 undefined
,则 this
将指向全局对象(在浏览器环境中为 window
)。
此外,还可以通过闭包、使用ES6
的类和方法、使用观察者模式等方式来改变 this
的指向。
手写call
js
// @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
js
// @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
js
// @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小结
call
、apply
实现仅有传参不同
核心 :利用对象方法内的this
指向该对象,实现this
绑定
注意 :使用symbol
防止污染全局作用域,返回执行结果前,清除symbol
对应的方法
GlobalThis
GlobalThis
是一个全局对象,在不同的 JavaScript 环境中代表全局作用域的对象。它提供了一种标准化的方式来访问全局对象,不依赖于具体的 JavaScript 运行环境。
在浏览器环境中,全局对象是 window
对象,可以通过 window
访问全局变量和函数。
在 Node.js
环境中,全局对象是 global
对象,可以通过 global
访问全局变量和函数。
在各种环境和 JavaScript
引擎中,GlobalThis
可以用来替代直接使用特定的全局对象,以减少代码对特定环境的依赖性。它适用于在不同的 JavaScript
环境中编写可移植的代码。
例如,可以使用 GlobalThis
来访问全局对象中的 setTimeout
函数:
js
const { setTimeout } = GlobalThis;
setTimeout(() => {
console.log('Hello, world!');
}, 1000);
GlobalThis
是在ECMAScript 2020
标准中引入的,可能在部分老旧的JavaScript
运行环境中不被支持。但可以通过polyfill
或使用其他方法来模拟实现。
原型和原型链
前言:
js
本身是基于原型链
进行继承的语言
原型是对象的一个属性,用于共享方法和属性。
每个JavaScript
对象都有一个原型对象,它包含对象的共享方法和属性。当我们创建一个新对象时,它会自动从其原型对象中继承属性和方法。
原型是通过使用原型链来实现的。原型链是一个对象到其原型对象的链式连接。如果一个对象无法找到所需的属性或方法,它会顺着原型链向上查找,直到找到或者到达原型链的末端(null
)为止。
每个JavaScript
对象都有一个 __proto__
属性,它指向该对象的原型。通过这个属性,我们可以访问和修改对象的原型。
原型关系:
- 每个
class
都有显示原型prototype
- 每个实例都有隐式原型
__proto__
- 实例的
__proto__
指向它的构造函数的prototype
class和继承
class
的本质是一个函数
继承:extends
,super
,扩展,重写
js
// @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) // 生存
如何调用到继承的方法?
- 先在自身属性与方法中查找
- 如果找不到,就去
__proto__
(构造函数的prototype
)中查找
手写instanceof
js
// 手写 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()的区别
js
// @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
Object.create()
则是用来创建具有指定原型或具有空原型的对象。- 如果指定了原型,最终原型链的尽头仍然是
Object.protype
和null
,并且可以调用原型链上的方法 - 如果没有指定原型,则丧失了对象的原生方法
new 一个对象的过程中发生了什么?
- 创建一个空对象
obj
,继承构造函数的原型 - 执行构造函数,令
this
指向obj
- 返回
obj
js
// @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__
属性访问到。
异步
为什么需要异步?
- 因为
js
是单线程的,无论是在浏览器还是nodeJS
中 - 浏览器中
js
执行和DOM
渲染共用一个线程 - 异步分为宏任务和微任务
宏任务和微任务
- 宏任务:
setTimeout
、setInterval
、网络请求 - 微任务:
promise
、async await
、mutationObserver
- 微任务在下一轮
DOM
渲染之前执行,宏任务在DOM
渲染之后执行
Event Loop
浏览器的Event loop
- 当异步任务开始执行时,优先 执行
微任务
- 直到
微任务
队列为空,再执行宏任务 - 无论是执行宏任务还是微任务
- 每次执行完成后检查 先还有没有
微任务
,有就优先执行微任务
(继续回到第一步)
Promise
价值:解决回调地狱
三种状态:
Pending
:进行中Fulfilled
:已完成Rejected
: 已失败
进阶: 可通过
async await
语法糖使代码更简洁
DOM
DOM
的本质是将文档解析为树形结构,其中每个节点都是一个对象,代表文档中的一个元素、属性、文本或其它类型的信息。这些节点可以相互关联形成父子关系,即一个节点可以包含子节点,并且可以通过父节点访问到子节点。通过使用DOM API
,开发人员可以访问和操作这些节点,从而改变文档的结构和内容。
window对象和document对象
window
对象表示浏览器的窗口,它是 JavaScript 访问浏览器窗口的接口。window
对象具有很多属性和方法,用于处理窗口的尺寸、位置、打开或关闭窗口、发送和接收消息等操作。window
对象还提供了全局的 setTimeout
、setInterval
和 clearTimeout
等方法,用于定时执行代码和处理异步操作。
document
对象表示当前窗口的文档,它是 JavaScript 操作网页内容的接口。document
对象具有属性和方法,可用于访问和操作 HTML 文档的各个部分,如元素、属性、样式、事件等。通过 document
对象,可以选择元素、修改元素的内容、样式和属性,监听和响应事件等操作。
可以将 window
对象看作是整个浏览器窗口的全局对象,提供和窗口相关的功能。而 document
对象是窗口内部的文档对象,提供和文档内容相关的功能。因此document === window.document
返回true
总结来说,window
对象是整个浏览器窗口的接口,提供了访问和控制窗口的方法和属性。document
对象是当前窗口的文档对象,提供了操作和处理文档内容的方法和属性。两者是密切相关的,但具有不同的功能和用途。
HTMLCollection和NodeList有什么区别?
相同:
-
HTMLCollection
和NodeList
都是表示一组HTML
元素。 -
HTMLCollection
和NodeList
都是类数组对象,都可以通过Array.from()
、Array.prototype.slice.call()
将其转换为真正的数组进行处理。
不同:
-
集合类型不同:
HTMLCollection
表示由标签名或类名等条件筛选出的元素集合,而NodeList
表示由节点集合组成的列表,可以包含各种类型的节点。 -
获取方式不同:
HTMLCollection
可以通过元素的属性(如document.getElementsByName()
、element.getElementsByTagName()
等)获取,而NodeList
可以通过选择器(如document.querySelectorAll()
)或特定的属性(如element.childNodes
)获取。 -
遍历方式不同:
HTMLCollection
通常是一个实时集合,即获取时是动态的,会自动更新,可以使用for
循环或下标直接访问元素。而NodeList
通常是一个静态集合,获取的是一个快照,不会自动更新,可以使用forEach()
、for...of
循环或下标直接访问元素。 -
方法支持不同:
HTMLCollection
对象有额外的方法和属性,如namedItem()
方法可以根据元素的name
属性获取元素,refresh()
方法可以手动刷新集合。而NodeList
对象相对较简单,没有额外的方法和属性。
property与attribute区别
property
: 不会体现到html结构中attribute
: html标签上的属性,修改时会改变html结构- 二者都有可能引起
DOM
重新渲染
结构操作
结构操作:围绕DOM
的增删查改
新增/插入:
js
const div1 = document.getElementById('div1')
// 创建新节点
const p1 = document.createElement('p')
p1.innerHTML = 'p1'
div1.appendChild(p1)
移动节点:
js
const div1 = document.getElementById('div1')
const p2 = document.getElementById('p2')
div1.appendChild(p2)
获取父元素节点:
js
const div1 = document.getElementById('div1')
const parent = div1.parentNode
获取子元素节点列表:
js
const div1 = document.getElementById('div1')
const childList = div1.childNodes
删除节点:
js
const div1 = document.getElementById('div1')
const childList = div1.childNodes
div1.removeChild(childList[0])
优化: 频繁新增时思考是否可用
Fragment
事件
事件冒泡与事件捕获
事件冒泡:
事件先在发生元素上被触发,然后逐级向上冒泡到父元素,直至达到文档根节点(或根节点定义的捕获阶段的事件侦听器)。事件会从最具体的元素开始向父元素传播。
事件捕获:
事件从文档根节点(或根节点定义的捕获阶段的事件侦听器)开始传播,然后逐级向下捕获到发生事件的元素,最终在元素本身上触发事件。换句话说,事件会从最不具体的元素开始向下捕获。
事件代理
由于相同事件绑定数量过多,会产生大量监听器,统一把事件绑定放到父元素上去做,减少内存占用。
如:地图打点,每个点的事件由图层统一监听
h5端300ms点击延迟
为什么会有300ms点击延迟?
移动端浏览器的300ms点击延迟是由于浏览器为了判断用户是要进行双击缩放操作还是要进行单击操作,所以在用户点击后会等待一段时间来判断用户是否会进行双击操作。虽然这种机制对某些特定场景(如双击缩放)是有用的,但对于大部分的移动应用来说,这个延迟会给用户带来较差的交互体验。
如何解决300ms点击延迟
-
使用
FastClick
库,它的原理是通过在触发点击事件的时候,立即模拟触发一个click
事件,从而绕过浏览器的点击延迟 -
使用
CSS
属性touch-action: manipulation
,可以告诉浏览器不要进行双击缩放操作,从而减少点击延迟。例如:csshtml { touch-action: manipulation; }
这样设置后,点击事件会更加即时地触发。
-
使用
meta
标签禁用缩放, 从而减少浏览器的点击延迟。例如:html<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
这样设置后,用户将无法进行手势缩放,从而减少点击延迟。
BOM
navigator
screen
location
history
如何检测浏览器类型
js
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
js
console.log(location.href) // https://juejin.cn/flash-note/list?note_id=7273377193816162304&from=6&editing=1
console.log(location.host) // juejin.cn
console.log(location.protocol) // https:
console.log(location.pathname) // /flash-note/list
console.log(location.search) // ?note_id=7273377193816162304&from=6&editing=1
console.log(location.hash) // ''
console.log(location.origin) // https://juejin.cn
history
js
history.back()
history.forward()
JS中的observer们
-
MutationObserver
:用于监视DOM树中的变化,比如节点的添加、删除、属性修改等。 -
IntersectionObserver
:用于监视目标元素与其祖先元素或根元素交叉的情况,即目标元素在视口中是否可见。 -
ResizeObserver
:用于监视元素的大小变化,包括宽度、高度、内容区域大小等。
MutationObserver
html
<!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
js
// 目标元素
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
js
// 目标元素
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
防抖 限制执行次数,多次密集触发只执行一次
节流 限制频率,有节奏地执行
节流关注过程,防抖关注结果
html
<!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
空闲时才执行,低优
html
<!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>
requestAnimationFrame
与requestIdleCallback
都是宏任务
拷贝
深拷贝和浅拷贝的区别是什么?
深拷贝
与浅拷贝
的区别在于拷贝后的对象和原对象之间的关系以及对于引用类型的处理。
浅拷贝:
- 浅拷贝是指创建一个新对象,该对象只是原对象的一个副本,但是它们共享同一个内存地址。拷贝后的对象和原对象内部的引用类型数据(如对象、数组等)仍然指向同一片内存空间。
- 当修改拷贝后的对象内部的引用类型数据时,会影响到原对象。
- 浅拷贝通常使用一些简单的方法,如
Object.assign()
、数组的slice()
等。
深拷贝:
- 深拷贝是指创建一个新的对象,该对象和原对象完全独立,拷贝后的对象和原对象之间不共享内存地址。
- 修改拷贝后的对象内部的引用类型数据不会影响到原对象。
- 深拷贝会递归地复制原对象及其所有引用类型数据,直到所有的引用类型数据都被复制。
- 深拷贝通常需要使用递归或者序列化的方式实现,如
JSON.parse(JSON.stringify())
、递归赋值属性等。
如何实现深拷贝?包括Mao、Set,考虑循环引用
普通深拷贝只考虑了Object
、Array
,无法转换Map
、Set
和循环引用
为什么需要考虑循环引用?什么时候会发生循环引用? 如图,循环引用在普通深拷贝时会死循环,爆栈!
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记录下生成的正本、副本
如何实现数组扁平化?
- 定义一个空数组
arr = []
,遍历当前数组 - 如果
item
非数组,添加到arr
- 如果
item
是数组,则遍历之后累加到arr
js
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
的每个功能、单例
js
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
的基本思想是,通过记录每个缓存对象的访问顺序,最近被访问的对象被认为是最有可能再次访问的,而最久未被访问的对象则被认为是最有可能被淘汰的。
应用场景:
分析:
LRU
有长度限制- 人不够拼命招人
- 人够了,新人挤掉老人
- 老人挪位置,不管你人够不够
新增的时候:
header
是你部门的头头,习惯性欣赏'新人'tail
是把屠刀永远指向下次要被裁员的那个'老人'LRU
的长度则是你们部门规定的人数 现在又来了一个'新人',屠刀指向下一个'老人' 某天,某个同事立了大功,功劳大大的!领导说了一句吆西~
本质:双指针滑动窗口、双向链表队列
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?
本质: 考察对于异步的理解
核心:初始化时,使用异步执行下一个任务!
js
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('小猫咪')
constructor
内的异步任务在所有同步任务执行完成后开始执行!
难点:第一个任务怎么执行?