前端面经(Js部分)

目录:

(一)语法

1.1 数据类型

1.2 ==号

1.3 null undefined

1.4 Proxy

1.5 Promise

1.6 defer l async

1.7拷贝

1.8 生成|迭代器

1.9 定时器

1.10 严格模式

1.11 TS

1.12 Map和00

1.13 装饰器

1.14 typeof | instance

1.15 Weakmap

1.16 JSON 序列化

1.17 parselnt

(二)数组

2.1 判断数组或类

2.2 数组方法

2.3 for

(三)函数

3.1 箭头函数

3.2 this

3.3 方法

3.4 闭包

3.5 arguments

3.6 原型链

(四) ES5对象

4.1 创建类

4.2 属性|方法

4.2 继承

4.3 判空

(五)ES6类

5.1 属性|方法

(六)JS其他

6.1 单线程

6.2 垃圾回收(GC)

6.3 事件循环

6.4 CJS | ESM

6.5 AOP IOC

6.6 事件流

6.7 正则表达式

6.8 ES6模块

6.9 洋葱模型

6.10 ES6->ES5

6.11 js常见的库

6.12 ES新特性

6.13 作用域

(七)设计模式

(八) Dom

EventListener

InnerHTML

Attribute

MutationObserver

postMessage

EventEmitter

Event

获取Dom

DOMContentLoaded

(九) html组件化

(一) 语法

1.1 数据类型

分为基本数据类型和引用数据类型,其中基本数据类型包括Undefined、Null(typeof = Object)、Boolean、Number、String 5种,ES6中新增一种新的基本数据类型Symbol、bigint;引用数据类型含有Object、Function、Array、Date等类型,引用类型存在堆内存,引用数据类型会在栈中存储一个指针,这个指针指向堆内存空间中该实体的起始地址,这段内存如果没有强引用时会被GC垃圾回收。

1)var let const的区别

​ var特点:1.存在变量提升(声明式的class不存在); 2.一个变量可多次声明,后面的声明会覆盖前面的声明; 3.函数中使用var声明变量的时候,该变量是局部的; 4.函数内不使用var,该变量是全局的;5.win对象

​ let特点:1.不存在变量提升,声明前该变量不能使用(暂时性死区);2.let在块级作用域(词法作用域)内有效;3.let不允许在相同作用域中重复声明,不同作用域有重复声明不会报错;4. 在全局作用域声明不会被当作window对象的属性,但var会;5.由于是块级作用域,会更早的进入GC。

​ const特点:1.const声明一个只读的变量,声明后,值就不能改变; 2.const必须初始化;3.const并不是变量的值不能改动,而是变量指向的内存地址所保存的数据不得改动;4.let该有的特点const都有;

​ ES5里想实现const需要通过Obj.defineProperty

​ 原理:

JS 引擎在读取变量时,先找到变量绑定的内存地址,然后找到地址所指向的内存空间,最后读取其中的内容。当变量改变时,JS 引擎不会用新值覆盖之前旧值的内存空间(虽然从写代码的角度来看,确实像是被覆盖掉了),而是重新分配一个新的内存空间来存储新值,并将新的内存地址与变量进行绑定,JS 引擎会在合适的时机进行 GC,回收旧的内存空间。
const 定义变量(常量)后,变量名与内存地址之间建立了一种不可变的绑定关系,阻隔变量地址被改变,当 const 定义的变量进行重新赋值时,根据前面的论述,JS 引擎会尝试重新分配新的内存空间,所以会被拒绝,便会抛出异常。


2)symbol的应用场景

  • 使用Symbol来作为对象属性名(key)、类属性名定义私有变量。Symbol类型的key是不能通过Object.keys()或者for...in来枚举的,它未被包含在对象自身的属性名集合(property names)之中。所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用Symbol来定义。
  • 使用Symbol来替代常量const TYPE_AUDIO = 'AUDIO' ==> const TYPE_AUDIO = Symbol()
  • 利用Symbol.for()注册和获取全局Symbol

3)Number浮点计算

​ JS采用了IEEE 754数值一个浮点型数在计算机中的表示,它总共长度是64位,其中最高位为符号位,接下来的11位为指数位,最后的52位为小数位,即有效数字的部分。

​ 在进行浮点型运算时,首先将各个浮点数的小数位按照"乘2取整,顺序排列"的方法转换成二进制表示。

​ 可以通过parseInt或Number进行转化,使用parseInt会去掉字符串前后的空格。

4)new String() 和 string

​ 在JS中的变量在内存中的存储有两种形式,值类型存储和引用类型存储。String在JS中是基本类型,基本类型是存储在栈(stack)内存中的,数据大小确定,内存空间大小可以分配。而引用类型是存储在堆(heap)内存中的, 栈中存在的仅仅是一个堆的指针,是new String()指向一个地址,而正真的实例对象在堆中。所以我们可以为它添加一些属性和方法。

​ str.indexOf === String.prototype.indexOf //true

首先,它会从内存中读取str 的值。后台是这样进行的:创建String类型的一个实例;在实例上调用指定的方法;销毁这个实例,因此String可迭代;

1.2 ==号

这么理解: 当进行双等号比较时候: 先检查两个操作数数据类型,如果相同, 则进行===比较, 如果不同, 则愿意为你进行一次类型转换, 转换成相同类型后再进行比较, 而===比较时, 如果类型不同,直接就是false。

在转换不同的数据类型时,相等操作符遵循下列基本规则:

  1. 如果有一个操作数是布尔值,则在比较相等性之前,将其转换为数值;
  2. 如果一个操作数是字符串,另一个操作数是数值,在比较之前先将字符串转换为数值;
  3. 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf() 方法,用得到的基本类型值按照前面的规则进行比较;
  4. 如果有一个操作数是 NaN,无论另一个操作数是什么,相等操作符都返回 false;
  5. 如果两个操作数都是对象,则比较它们是不是同一个对象。如果指向同一个对象,则相等操作符返回 true;
  6. 在比较相等性之前,不能将 null 和 undefined 转成其他值。
  7. null 和 undefined 是相等的。

双等号==: 来进行一般比较检测两个操作数是否相等,可以允许进行类型转换

(1)如果两个值类型相同,再进行三个等号(===)的比较

(2)如果两个值类型不同,也有可能相等,需根据以下规则进行类型转换在比较:

1)如果一个是null,一个是undefined,那么相等

2)如果一个是字符串,一个是数值,把字符串转换成数值之后再进行比较

三等号===:

(1)如果类型不同,就一定不相等

(2)如果两个都是数值,并且是同一个值,那么相等;如果其中至少一个是NaN,那么不相等。(判断一个值是否是NaN,只能使用isNaN( ) 来判断)

(3)如果两个都是字符串,每个位置的字符都一样,那么相等,否则不相等。

(4)如果两个值都是true,或是false,那么相等

(5)如果两个值都引用同一个对象或是函数,那么相等,否则不相等

(6)如果两个值都是null,或是undefined,那么相等

注意:

undefined 与 null 比较特殊 要比较相等性之前,不能将 null 和 undefined 转换成其他任何值

undefined 和 null 互相比较返回 true,和自身比较也返回 true,其他情况返回 false

参考:

https://zhuanlan.zhihu.com/p/115298832

https://www.cnblogs.com/bryanfu/p/15063999.html

1.3 null | undefined

null:空对象指针,可用于判断真假

js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息。000:对象010:浮点数100:字符串110:布尔1:整数。null:所有机器码均为0因此判断为object。undefined:用 −2^30 整数来表示
typeof [] === 'array' // false obj
typeof null === object // true
typeof undefined = undefined
typeof class = function


① 是 JavaScript 基本类型之一,特指对象的值未设置,是表示缺少的标识,指示变量未指向任何对象,把 null 看为尚未创建的对象,也许更好理解;② 是一个字面量,不像 undefined,它不是全局对象的一个属性;③ 在布尔运算中被认为是 false;④ 与其他任何对象一样永远不会被 JavaScript 隐式赋值给变量。

判断null:!tmp && typeof(tmp)!="undefined" && tmp!=0 或者是 ===

​ undefined:

① 是 JavaScript 基本类型之一,表示 "缺少值",就是此处应该有一个值,但是还没有定义;② 是 JavaScript 在运行时创建的全局变量,是全局对象的一个属性;③ 在布尔运算中被认为是 false。

(1)变量被声明但没有赋值时,就等于 undefined。

(2)对象的某个属性没有赋值时,该属性的值为 undefined。

(3)调用函数过程中,应该提供的参数没有提供时,该参数就等于 undefined。

(4)函数没有返回值时,默认返回 undefined。

参考:

[Symbol() 的使用方法 - sjpqy - 博客园 (cnblogs.com)](

1.4 Proxy

​ ES6中新增了Proxy对象,从字面上看可以理解为代理器,主要用于改变对象的默认访问行为,实际表现是在访问对象之前增加一层拦截,任何对对象的访问行为都会通过这层拦截。在拦截中,我们可以增加自定义的行为。它实际是一个构造函数,接收两个参数,一个是目标对象target;另一个是配置对象handler,用来定义拦截的行为。

注意:

​ 如果在target中使用了this关键字,再通过Proxy处理后,this关键字指向的是Proxy的实例,而不是目标对象target。

const person = {
    getName: function () {
        console.log(this === proxy);
    }
};

const proxy = new Proxy(person, {});

proxy.getName();  // true
person.getName(); // false


使用场景:实现真正的私有、读取不存在时警告、读取负索引、属性拦截等。

​ Reflect 意思是反射,反射是在程序运行中获取和动态操作自身内容的一项技术。与Proxy对象不同的是,Reflect对象本身并不是一个构造函数,而是直接提供静态函数以供调用,Reflect对象的静态函数一共有13个,和Proxy呼应。

1.5 Promise

​ Promise 构造函数是 JavaScript 中用于创建 Promise 对象的内置构造函数。Promise 构造函数接受一个函数作为参数,该函数是同步的并且会被立即执行,所以我们称之为起始函数。起始函数包含两个参数 resolve 和 reject,分别表示 Promise 成功和失败的状态。一个 promise 对象初始化时的状态是 pending,调用了 resolve 后会将 promise 的状态扭转为 fulfilled,调用 reject 后会将 promise 的状态扭转为 rejected,这两种扭转一旦发生便不能再扭转该 promise 到其他状态。

​ 在Promise链中then()也可以返回promise对象,只有return pending状态可以中断promise链

​ 异步实现通过then()查看pending,并将callback传给Promise对象。

​ 起始函数执行成功时,它应该调用 resolve 函数并传递成功的结果。当起始函数执行失败时,它应该调用 reject 函数并传递失败的原因。Promise 构造函数返回一个 Promise 对象,该对象具有以下几个方法:

  • then:用于处理 Promise 成功状态的回调函数。
  • catch:用于处理 Promise 失败状态的回调函数。
  • finally:无论 Promise 是成功还是失败,都会执行的回调函数。
reject 是 Promise 的方法,而 catch 是 Promise 实例的方法
reject 是用来抛出异常,catch 是用来处理异常
reject 是 Promise 的方法,而 catch 是 Promise 实例的方法
reject后的东西,一定会进入then中的第二个回调,如果then中没有写第二个回调,则进入catch
网络异常(比如断网),会直接进入catch而不会进入then的第二个回调


  • Promise.all

​ 该方法接收一个Promise数组返回一个Promise,只有当该数组中的所有Promise完成后才会由pendding状态变为resolve执行then里面的回调函数,若数组中有任意一个promise被拒绝则会执行失败回调,catch方法会捕获到首个被执行的 reject函数。该方法获得的成功结果的数组里面的数据顺序和接收到的promise数组顺序是一致的。

  • Promise.any

​ 当传入的promise数组中有任意一个完成时就会终止,会忽略到所有被拒绝掉的promise,直到第一个promise完成。若传入所有的promise被拒绝则会执行拒绝回调。

  • Promise.race

​ 当promise数组中任意一个promise被拒绝或者成功,则会采用第一个promise作为他的返回值。若为成功的执行then,若失败则执行catch。

  • Promise.allSettled

​ 当给定的promise数组中的所有promise被拒绝后会返回一个拒绝的promise数组,与[]一一对应。

async/await

​ async/await 是ES2017(ES8)提出的基于Promise的解决异步的最终方案,使异步编程看起来像同步编程,是一个语法糖。

​ async是一个加在函数前的修饰符,被async定义的函数会默认返回一个Promise对象resolve的值。因此对async函数可以直接then,返回值就是then方法传入的函数。

​ await 也是一个修饰符,只能放在async定义的函数内。可以理解为等待。await 修饰的如果是Promise对象:可以获取Promise中返回的内容(resolve或reject的参数),且取到值后语句才会往下执行;如果不是Promise对象:把这个非promise的东西当做await表达式的结果。

​ 实现原理:

function request(url){
    return new Promise(resolve=>{
        setTimeout(()=>{
            resolve(url)
        },500)
    })
}


async function run(){
    const res1 = await request(1); // await接受一个promise对象
    const res2 = await request(res1);
    console.log(res2);
}
run() // 等待1s后得到结果1


//利用generator实现
function* generate(){
    const res1 = yield request(1); 
    const res2 = yield request(res1);
    console.log(res2)
}
const g = generate()
const {value:val1, done:done1} = g.next() 
val1.then(res=>{
    const {value:val2, done:done2} = g.next(res)
    val2.then((res2)=>{
        console.log(res2)
        g.next(res2)
    })
})
//使用递归实现
function run(){
	const g = generate()
    function exec(params){
        const {value, done} = g.next(params)
        if(!done){
            value.then(res=>exec(res))
        }
    }
}


​ 参考:

  • js常见面试题——详解Promise使用与原理及实现过程(附源码) https://blog.csdn.net/weixin_56134381/article/details/115868041

1.6 defer|async

异步加载:

​ 当浏览器碰到 script 脚本的时候:

<script src="script.js"></script>


​ 没有 defer 或 async,浏览器会立即加载并执行指定的脚本,"立即"指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。

<script async src="script.js"></script>


有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。脚本加载完成后,文档停止解析,脚本执行,执行结束后文档继续解析。

<script defer src="myscript.js"></script>


有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有文档解析完成之后,DOMContentLoaded 事件触发之前完成。他的优点不是增加JS的并发下载数量,而是做到下载时不block解析HTML。

然后从实用角度来说呢,首先把所有脚本都丢到 </body> 之前是最佳实践,因为对于旧浏览器来说这是唯一的优化选择,此法可保证非脚本的其他一切元素能够以最快的速度得到加载和解析。

插入位置:

将script放在<head>里,浏览器解析HTML,发现script标签时,会先下载完所有这些script,再往下解析其他的HTML。浏览器最多同时下载两个Js文件。

将script放在尾部的缺点,是浏览器只能先解析完整个HTML页面,再下载JS。

首先声明。这在</body>之后插入其他元素,从HTML 2.0起就是不合标准的。

带async的脚本一定会在load事件之前执行,可能会在DOMContentLoaded之前或之后执行。

情况1: HTML 还没有被解析完的时候,async脚本已经加载完了,那么 HTML 停止解析,去执行脚本,脚本执行完毕后触发DOMContentLoaded事件

情况2: HTML 解析完了之后,async脚本才加载完,然后再执行脚本,那么在HTML解析完毕、async脚本还没加载完的时候就触发DOMContentLoaded事件

defer 和 async 的区别

​ 带async的脚本一定会在load事件之前执行,可能会在DOMContentLoaded之前或之后执行。

​ 情况1: HTML 还没有被解析完的时候,async脚本已经加载完了,那么 HTML 停止解析,去执行脚本,脚本执行完毕后触发DOMContentLoaded事件

​ 情况2: HTML 解析完了之后,async脚本才加载完,然后再执行脚本,那么在HTML解析完毕、async脚本还没加载完的时候就触发DOMContentLoaded事件

​ 如果 script 标签中包含 defer,那么这一块脚本将不会影响 HTML 文档的解析,而是等到 HTML 解析完成后才会执行。而 DOMContentLoaded 只有在 defer 脚本执行结束后才会被触发。

​ 情况1:HTML还没解析完成时,defer脚本已经加载完毕,那么defer脚本将等待HTML解析完成后再执行。defer脚本执行完毕后触发DOMContentLoaded事件

​ 情况2:HTML解析完成时,defer脚本还没加载完毕,那么defer脚本继续加载,加载完成后直接执行,执行完毕后触发DOMContentLoaded事件

https://zhuanlan.zhihu.com/p/25876048

1.7拷贝

​ 浅克隆由于只克隆对象最外层的属性,如果对象存在更深层的属性,则不进行处理,这就会导致克隆对象和原始对象的深层属性仍然指向同一块内存。

1)浅克隆

  • 简单的引用复制,即遍历对象最外层的所有属性,直接将属性值复制到另一个变量中。
  • ES6的Object.assign()函数

当对象中只有一级属性,没有二级属性的时候,Object.assign()方法为深拷贝,但是对象中有对象的时候,此方法在二级属性以后就是浅拷贝。

​ 浅克隆实现方案都会存在一个相同的问题,即如果原始对象是引用数据类型(数组、对象、函数、Date、RegExp)的值,则对克隆对象的值的修改会影响到原始对象的值。

  • 利用解构{...obj}
  • arr.slice(0) 拷贝数组

2)深克隆

  • 拓展运算符
  • JSON序列化和反序列化

​ 如果一个对象中的全部属性都是可以序列化的,那么我们可以先使用JSON.stringify()函数将原始对象序列化为字符串,再使用JSON.parse()函数将字符串反序列化为一个对象,这样得到的对象就是深克隆后的对象。

var origin = {
   a: 1,
   b: [2, 3, 4],
   c: {
       d: 'name'
   }
};
// 先反序列化为字符串,再序列化为对象,得到深克隆后的对象
var result = JSON.parse(JSON.stringify(origin));

console.log(origin); // { a: 1, b: [ 2, 3, 4 ], c: { d: 'name' } }
console.log(result); // { a: 1, b: [ 2, 3, 4 ], c: { d: 'name' } }


​ 这种方法能够解决大部分JSON类型对象的深克隆问题,但是对于以下几个问题不能很好地解决。

​ (1) 无法实现对函数、RegExp等特殊对象的克隆。

​ (2) 对象的constructor会被抛弃,所有的构造函数会指向Object,原型链关系断裂。

​ (3) 对象中如果存在循环引用,会抛出异常。

  • jQuery实现—— .clone()函数和.extend()函数

​ 在jQuery中提供了一个 .clone()函数,但是它是用于复制DOM对象的。真正用于实现克隆的函数是.extend(),使用$.extend()函数可以实现函数与正则表达式等类型的克隆,还能保持克隆对象的原型链关系,解决了深克隆中存在的3个问题中的前两个,但是却无法解决循环引用的问题。

  • 自定义实现深克隆
function deepCopy(obj) {
    let copy;

    //处理3种简单的类型,和null和undefined
    if (null == obj || "object" != typeof obj) return obj;

    // 处理Date|Array|Object
    structuredClone(obj)
    
    
    //处理Date
    if (obj instanceof Date) {
        copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }

    //处理Array
    if (obj instanceof Array) {
        copy = [];
        for (let i = 0, len = obj.length; i < len; i++) {
            copy[i] = deepCopy(obj[i]);
        }
        return copy;
    }

    //处理Object
    if (obj instanceof Object) {
        copy = {};
        for (let attr in obj) {
            if (obj.hasOwnProperty(attr)) copy[attr] = deepCopy(obj[attr]);
        }
        return copy;
    }
    
    //处理function
    // 方式1, 很多函数库都是用这个方法
	var closeFunc = new Function('return ' + func.toString())();

	// 方式2 // 利用bind 返回函数
	var closeFunc = func.prototype.bind({});

    throw new Error("Unable to copy obj! Its type isn't supported.");
}


1.8 生成|迭代器

迭代器原理1.可迭代对象:具有专用迭代器方法(Symbol.iterator),且该方法返回迭代器对象的对象

2.迭代器对象:具有next()方法,且返回值为迭代结果对象

3.迭代结果对象:具有属性value和done的对象

迭代流程1.调用其迭代器,获得迭代对象

2.重复调用迭代器对象的next()方法

3.直至返回done为true的迭代结果对象

生成器

迭代器这个概念固然是好的,但是他的创建和使用会让事情变得有些复杂,因此提出生成器概念,来简化自定义迭代器的创建。

generator 函数是协程在 ES6 的实现。协程简单来说就是多个线程互相协作,完成异步任务。

1.通过在函数名前加一个*的方式创建一个生成器函数,并进入suspended状态

2.调用生成器获取一个生成器对象(调用生成器函数并不会执行该函数,而是会返回一个生成器对象,该生成器对象本质是一个迭代器)

3.调用它的next()方法进入native状态,会使生成器函数的函数体从当前位置开始执行,直到遇到一个yield语句

4.yield语句的值会成为调用迭代器的next()方法的返回值

5.可以通过.return() |.throw() 提前关闭

注:不能使用箭头函数定义生成器函数async函数却可以

1.9 定时器

sleep函数实现:

function sleep(ms) {return new Promise(resolve => setTimeout(resolve, ms));}
async function delayedGreeting() {
  console.log('Hello');
  await sleep(2000);
  console.log('World!');
}


面试题:JS 中的计时器不能做到精确计时吗

  1. 计算机硬件没有原子钟,无法做到精确计时
  2. 操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的函数,也就携带了这些偏差
  3. 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层,则会带有 4 毫秒的最少时间,这样在计时时间少于 4 毫秒时又带来了偏差
  4. 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差

setInterval() 是window对象下内置的一个方法,接受两个参数,第一个参数允许是一个函数或者是一段可执行的 JS 代码,第二个参数则是执行前面函数或者代码的时间间隔;

  • setInterval 的最短间隔时间是10毫秒, 也就是说,小于10毫秒的时间间隔会被调整到10毫秒
var obj = {
    fun:function(){
        this ;
    }
}
setInterval(obj.fun,1000);      // this指向window对象,隐式丢失
setInterval('obj.fun()',1000);  // this指向obj对象


// 方式1 解决隐式丢失
setTimeout(obj.a.bind(obj), 1000);
// 方式2
setTimeout(function() {
  obj.a();
}, 1000);


  • setTimeout 的最短时间间隔是4毫秒

调用setTimeout()或setlnterval()时创建的计时器会被放入定时器观察者内部的红黑树中,每次Tick时,会从该红黑树中检查定时器是否超过定时时间,超过的话,就立即执行对应的回调函数。

setTimeout())和setlnterval()都是当定时器使用,他们的区别在于后者是重复触发,而且由于时间设的过短会造成前一次触发后的处理刚完成后一次就紧接着触发。

由于定时器是超时触发,这会导致触发精确度降低,比如用setTimeout设定的超时时间是5秒,当事件循环在第4秒循到了一个任务,它的执行时间3秒的话,那么setTimeout的回调函数就会过期2秒执行,这就是造成精度降低的原因。

并且由于采用红黑树和迭代的方式保存定时器和判断触发,较为浪费性能。

优化精度:

使用process.nextTick()所设置的所有回调函数都会放置在数组中,会在下一次Tick时所有的都立即被执行,该操作较为轻量,时间精度高。setlmmediate()设置的回调函数也是在下一次Tick时被调用,其和process.nextTick0)的区别在于两点:1.他们所属的观察者被执行的优先级不一样process.nextTick()属于idle观察者,setlmmediate()属于check观察者,idle的优先级>check。2.setlmmediate()设置的回调函数是放置在一个链表中,每次Tick只执行链表中的一个回调。这是为了保证每次Tick都能快速地被执行

3.使用webworkers

在node中,所有的异步任务都会有一个观察者,操作系统会询问这个观察者,你这个异步任务执行完了没有,如果执行完了我就执行你对应的回调函数了,如果没执行完,那没关系,我下个循环再来问你一下。

而操作系统询问观察者是有顺序的,观察者分为idle观察者、check观察者和I/O观察者,每一轮循环检查中,询问观察者的顺序是idel观察者 > I/O观察者 > check观察者,而process.nextTick属于idel观察者,setImmediate属于check观察者,所以process.nextTick会先执行。

当浏览器设为不可见状态时,浏览器自身会对定时器进行优化导致定时器不准。谷歌浏览器中,当页面处于不可见状态时,setInterval的最小间隔时间会被限制为1s。火狐浏览器的setInterval和谷歌特性一致。ie浏览器没有对不可见状态时的setInterval进行性能优化,不可见前后间隔时间不变。

可以通过webWorkers创建一个独立的线程进行解决。还可以解决一个页面存在多个定时器时候间隔时间误差较大的问题。

//专用线程由 Worker()方法创建,可以接收两个参数,第一个参数是必填的脚本的位置,第二个参数是可选的配置对象,可以指定 type、credentials、name 三个属性。
var worker = new Worker('worker.js', { name: 'dedicatedWorker'})
worker.onmessage = function(event) {
    document.getElementById("result").innerHTML = event.data;};
//由于 web worker 位于外部文件中,它们无法访问下列 JavaScript 对象:window,document,parent


setTimeout函数在JavaScript中用于设定一个特定的延迟后执行某个函数或代码。但有时候,实际的延迟时间可能会大于你设定的时间。这种情况通常是由于以下几个原因导致的:

  1. JavaScript单线程模型: JavaScript是单线程的,这意味着在一个时间内只能执行一个任务。如果在调用setTimeout之前的任务需要的时间超过了你设定的延迟时间,那么setTimeout中的函数会等待这些任务执行完毕才会被执行。
  2. 浏览器或者环境的因素: 在某些情况下,例如标签页处于后台,或电脑进入睡眠状态,浏览器可能会降低或暂停JavaScript的执行,这会影响setTimeout的精确性。

这是几个可能的解决方案:

  1. 使用requestAnimationFrame: 如果你的代码与渲染或动画有关,那么使用requestAnimationFrame可能是更好的选择。它会在浏览器准备进行下一次渲染时运行你的代码,从而达到更精确控制的效果。
  2. 使用Web Workers: Web Workers提供了在浏览器后台运行代码的能力,这个后台线程是完全独立的,可以让你的setTimeout不受主线程阻塞的影响。
  3. 优化你的代码: 减少执行的任务或者优化代码,使得在setTimeout之前的代码能够更快地运行。

记住,setTimeout不能保证精确的时间延迟,所以在需要精确计时的情况下,可能需要寻找其他的解决方案,如使用performance.now来获取更精确的时间戳。

function requestAnimationFrameTimeout(fn, delay) {
    let start = performance.now();
    let handle = null;
    function loop(now) {
        if (now - start >= delay) {
            cancelAnimationFrame(handle);
            fn();
        } else {
            handle = requestAnimationFrame(loop);
        }
    }
    handle = requestAnimationFrame(loop);
    return {
        cancel: function() {
            cancelAnimationFrame(handle);
        }
    };
}
// 使用示例
let timeout = requestAnimationFrameTimeout(() => {
    console.log("Hello, world!");
}, 2000);

// 取消定时器
timeout.cancel();


https://juejin.cn/post/6899796711401586695

https://juejin.cn/post/6844903736238669837#heading-4

1.10 严格模式

'use strict' 是用于对整个脚本或单个函数启用严格模式的语句。严格模式是可选择的一个限制 JavaScript 的变体一种方式 。优点:

  • 无法再意外创建全局变量。
  • 会使引起静默失败(silently fail,即:不报错也没有任何效果)的赋值操抛出异常。
  • 试图删除不可删除的属性时会抛出异常(之前这种操作不会产生任何效果)。
  • 要求函数的参数名唯一。
  • 全局作用域下,this的值为undefined。
  • 捕获了一些常见的编码错误,并抛出异常。
  • 禁用令人困惑或欠佳的功能。

缺点:

  • 缺失许多开发人员已经习惯的功能。
  • 无法访问function.caller和function.arguments。
  • 以不同严格模式编写的脚本合并后可能导致问题。

1.11 TS

​ TypeScript 是 JavaScript 的类型的超集,支持ES6语法,支持面向对象编程的概念,如类、接口、继承、泛型等。主要是为了在编译阶段 catch 掉所有类型错误。

TypeScript 的特性主要有如下:

  • 类型批注和编译时类型检查 :在编译时批注变量类型
  • 类型推断:ts 中没有批注变量类型会自动推断变量的类型
  • 类型擦除:在编译过程中批注的内容和接口会在运行时利用工具擦除
  • 接口:ts 中用接口来定义对象类型
  • 枚举:用于取值被限定在一定范围内的场景
  • Mixin:可以接受任意类型的值
  • 泛型编程:写代码时使用一些以后才指定的类型
  • 名字空间:名字只在该区域内有效,其他区域可重复使用该名字而不冲突
  • 元组:元组合并了不同类型的对象,相当于一个可以装不同类型数据的数组

TS 高级类型:

Partial:转为可选型|undefined

Required:转化为必选

Exclude<T, U>:排除 U 中的某个 T,针对联合类型

Omit<T, K extends keyof any>:删除某个接口的字段

NonNullable< T >:过滤掉 联合类型 中的 null 和 undefined 类型

Record:以 typeof 格式快速创建一个类型,此类型包含一组指定的属性且都是必填。

Pick:从类型定义的属性中,选取指定一组属性,返回一个新的类型定义。

Readonly:只读,不可以被修改

as !区别:

1、as!用于属性的读取,都可以缩小类型检查范围,都做判空用途时是等价的。只是!具体用于告知编译器此值不可能为空值(nullundefined),而as不限于此。

2、?可用于属性的定义和读取,读取时告诉编译器此值可能为空值(nullundefined),需要做判断。

JSDoc:

​ 很多开发人员之所以选择 TypeScript,是因为强类型可以减少错误,并通过代码完成和弹出帮助等功能改善代码编辑器中的开发体验。但是使用 TypeScript 会带来额外的工具。例如,如果在 TypeScript 中构建一个库,并在另一个项目中使用该库,就不能只修改代码库还需要重新构建代码。

​ 而 JSDoc 也可以用于类型检查。JSDoc 是一种用于在 JavaScript 代码中编写文档和类型注释的标记语言,它使用类似于JavaDoc 的注释语法。通过在代码中添加特定的注释标记,可以生成文档,提高代码的可读性和可维护性。JSDoc不仅可以描述函数的参数和返回值类型,还可以用来描述类、对象、模块和命名空间等各种 JavaScript 实体的属性和方法。

  • 渐进式采用:可以逐渐向代码库添加JSDoc,这样可以逐步将类型检查和文档引入项目,而无需完全采用TypeScript。
  • 无需构建步骤:JSDoc不像TypeScript那样需要构建步骤进行类型检查和转译。

1.12 Map和{}

1 对于 Object 而言,它键(key)的类型只能是字符串,数字或者 Symbol(如果给对象传入其他类型会通过String转换成字符串);而对于 Map 而言,它可以是任何类型。(包括 Date,Map,或者自定义对象,因为输入会通过hash运算转变为数字)

2 Map` 中的元素会保持其插入时的顺序;而 Object则不会完全保持插入时的顺序,而是根据如下规则进行排序:

  • 非负整数会最先被列出,排序是从小到大的数字顺序
  • 然后所有字符串,负整数,浮点数会被列出,顺序是根据插入的顺序
  • 最后才会列出 SymbolSymbol 也是根据插入的顺序进行排序的

3 读取 Map 的长度很简单,只需要调用其 .size() 方法即可;而读取 Object 的长度则需要额外的计算: Object.keys(obj).length

4 Map 是可迭代对象,所以其中的键值对是可以通过 for of 循环或 .foreach() 方法来迭代的;而普通的对象键值对则默认是不可迭代的,只能通过 for in 循环来访问(或者使用 Object.keys(o)、Object.values(o)、Object.entries(o) 来取得表示键或值的数字)迭代时的顺序就是上面提到的顺序。

5 在 Map 中新增键时,不会覆盖其原型上的键;而在 Object 中新增键时,则有可能覆盖其原型上的键:

Object.prototype.x = 1;
const o = {x:2};
const m = new Map([[x,2]]);
o.x; *// 2,x = 1 被覆盖了*
m.x; *// 1,x = 1 不会被覆盖*


6 JSON 默认支持 Object 而不支持 Map。若想要通过 JSON 传输 Map 则需要使用到 .toJSON() 方法,然后在 JSON.parse() 中传入复原函数来将其复原。

7 Map在频繁增删键值对的场景下表现更好。

8 内存占用: Map可以比Object多存储50%的键值对。

9 查找:Object更好

Weakmap对key是弱引用,其键只能是Object,不影响垃圾回收器的工作。一旦key被回收,对应的键和值就访问不到了。如果使用map,即便用户对目标对象没有任何引用,这个目标对象也不会被回收,会导致内存溢出。

1.13 装饰器

装饰模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。 这种模式属于结构型模式,它是作为现有的类的一个包装。 这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

​ 装饰器其实就是一个函数,通常放在类和类方法的前面,不能用于函数,因为存在函数提升。

优点:

  1. 不需要通过创建子类的方式去拓展功能(不需要子类化),这样可以避免代码臃肿的问题
  2. 装饰类的方法复用性很高
  3. 不会影响到原对象的代码结构

1.14 typeOf|inst...

typeof运算符返回一个字符串,表示操作数的类型,typeof可以精准的判断基本数据类型(null)除外

typeof 666 // 'number'
typeof '666' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object


instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

内部实现机制是:通过判断对象的原型链上是否能找到对象的 prototype,来确定 instanceof 返回值

console.log(Object instanceof Object) //true 
console.log(Function instanceof Function) //true 
console.log(Number instanceof Number) //false 
console.log(String instanceof String) //false 
console.log(Array instanceof Array) // false
 
console.log(Function instanceof Object) //true 
 
console.log(Foo instanceof Function) //true 
console.log(Foo instanceof Foo) //false


1.15 Weak...

weakMap

​ 与WeakSet不同的是WeakMap中存储的是键值对,WeaMap对键是弱引用的,值是正常引用,如果键在其他地方不被引用时,垃圾回收机制就会自动回收这个对象所占用的内存空间,同时移除WeakMap中的键值对,但键名对应的值如果是一个对象,则保存的是对象的强引用,不会触发垃圾回收机制被回收。

  • WeakMap中存储的是许多键值对的无序列表,列表的键名必须是非null的对象,对应的值可以是任意类型
  • WeaMap对键名是弱引用的,键值是正常引用
  • 因为垃圾回收机制可能随时清除其中的对象,所以不可以进行forEach( )遍历等操作
  • 因为弱引用,WeaMap 结构没有keys( ),values( ),entries( )等方法和 size 属性

​ ES5中我们经常利用立即执行函数的方式来设置私有变量,但问题是私有变量不会随着实例对象的销毁被回收,WeakMap正好可以解决这个问题。

weakSet

  • WeakSet结构同样不会存储重复的值,它的成员只能是对象类型。
  • 因为垃圾回收机制可能随时清除其中的对象,所以不可以进行forEach()遍历、for-of循环等迭代操作
  • 因为弱引用, WeakSet 结构没有keys(),alues(),entries()等方法和size属性

由于弱引用带来的限制,WeakSet只支持三种基本操作。

  • add(value):向 WeakSet 实例添加一个新成员。
  • delete(value):清除 WeakSet 实例的指定成员。
  • has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。

​ WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。具体一个场景就是存储DOM对象,当我们存储的DOM对象元素被另外一段脚本移除,我们也不想保留这些元素的引用而造成内存泄漏,就可以使用WeakSet来存储。

weakRef

​ 在Python和JavaScript中,weakref库和 WeakRef对象的主要用途是创建弱引用。在许多场景中,程序设计者需要引用一个对象,但又不希望该引用阻止垃圾收集器回收这个对象。这就是弱引用发挥作用的地方。

​ Python中的weakref库:这个库提供了工具来创建,使用和测试弱引用。例如,可以用它来实现多线程缓存。弱引用不会增加对象的引用计数,因此不会阻止被引用对象被垃圾收集。

​ JavaScript中的WeakRefWeakRef对象让你能够持有一个对象的弱引用,也就是说,引用不会阻止被引用的对象被垃圾收集器回收。这在处理循环引用或管理内存占用大的资源时很有用。比如,可以通过弱引用来实现一个大对象的缓存,当内存需要回收时,这个大对象就可以被释放。请注意,弱引用不能用于所有类型的对象。具体来说,只有那些可以被垃圾收集的对象才能被弱引用(全局对象、闭包中的对象、循环引用、正在使用的数据结构不可以被弱引用)。

​ JavaScript 中的 WeakRef 主要用于管理那些占用大量内存但又可以在不影响程序运行的情况下被垃圾收集的对象。它们对于防止内存泄露和优化性能非常有用。以下是一些具体的应用场景:

  1. 缓存机制:你可以使用 WeakRef 创建一个缓存系统,其中对象只在需要时才被保存。当内存压力增大时,这些对象可以被垃圾收集器自动回收,从而避免了内存泄漏。
  2. 映射和集合:WeakRef 与 FinalizationRegistry 一起可以用来创建弱映射和弱集合,它们存储的元素不会阻止垃圾收集。这在处理大型数据集时特别有用,因为你可以安全地引用对象而不必担心内存泄漏。
  3. 监听或观察模式:当你需要监听或观察一个对象,但又不希望这个监听或观察过程阻止该对象被垃圾收集时,WeakRef 可以派上用场。
  4. 管理大型对象:如果你的应用使用了很多大型对象,比如图像或视频,那么 WeakRef 可以帮你管理它们的生命周期。当这些对象不再需要时,它们可以被自动回收。

虽然 WeakRef 很有用,但它也需要谨慎使用,因为过度依赖弱引用可能会导致代码难以理解和维护。此外,WeakRef 的行为可能会受到 JavaScript 引擎的垃圾收集策略的影响,这可能会导致一些不可预见的行为。

1.16 JSON 序列化

​ 序列化(Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。因为对象本身存储的只是一个地址映射,如果程序停止了,映射关系就没了,所以对象obj的内容需要保存或传输,就要将对象序列化。JSON.stringify()和JSON.parse()用来序列化和反序列化js对象。

​ JSON数据有一定的格式要求,在JSON.stringify()序列化后,js对象会被规范为正确的JSON格式的字符串,属性名必须使用双引号,不能使用十六进制值,对象不能以,结尾。

  • undefined、任意的函数以及 symbol 作为对象属性值时 JSON.stringify() 对跳过(忽略)它们进行序列化
  • undefined、任意的函数以及 symbol 作为数组元素值时,JSON.stringify() 将会将它们序列化为 null
  • undefined、任意的函数以及 symbol 被 JSON.stringify() 作为单独的值进行序列化时,都会返回 undefined
  • 转换值如果有 toJSON() 函数,该函数返回什么值,序列化结果就是什么值,并且忽略其他属性的值。
  • NaN 和 Infinity 格式的数值及 null 都会被当做 null。
  • 序列化RegExp、Error、map等对象时会得到{}空对象。
  • JSON.stringify()只能序列化对象的可枚举的自有属性

1.17 parseInt

parseInt() 函数可解析一个字符串,并返回一个整数。它会去掉开头的空格,检查正负号,如果是英文开头则返回NAN,数字开头则会取整。

parseInt(string, radix)
parseInt('123', 5) // 将'123'看作 5 进制数,返回十进制数 38 => 1*5^2 + 2*5^1 + 3*5^0 = 38


filterInt = function (value) { // 一种更好的判断方式
  if (/^(-|+)?([0-9]+|Infinity)$/.test(value)) return Number(value);
  return NaN;
};


(二) 数组

2.1 判断数组或类

​ 1 instanceof 运算符:用来确定一个对象实例的原型链上是否有原型。内部采用Symbol.hasInstance()实现。

​ instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的prototype。

var a =  [1, 2, 3];
console.log(a instanceof Array);  // true
console.log(a instanceof Object); // true

var b = {name: 'kingx'};
console.log(b instanceof Array);  // false
console.log(b instanceof Object); // true

//通过原型链
console.log(a.constructor === Array);  // true
console.log(a.constructor === Object); // false
console.log(b.constructor === Array);  // false
console.log(b.constructor === Object); // true


​ 2 toString()函数

​ 每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回[Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用 call 或者 apply 方法来改变 toString 方法的执行上下文。

var a = [1, 2, 3];
var b = {name: 'kingx'};
//Object输出函数体"function Object() { [native code] }"通过call将this指向obj)
console.log(Object.prototype.toString.call(a)); // [object Array]
console.log(Object.prototype.toString.call(b)); // [object Object]


​ 3 Array.isArray() 只能判断是否是数组

当检测 Array 实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes

// 下面的函数调用都返回"true"
Array.isArray([]);
Array.isArray([1]);
Array.isArray(new Array());
// 鲜为人知的事实:其实 Array.prototype 也是一个数组。
Array.isArray(Array.prototype);
 
// 下面的函数调用都返回"false"
Array.isArray();
Array.isArray({});
Array.isArray(null);
Array.isArray(undefined);
Array.isArray(17);
Array.isArray('Array');
Array.isArray(true);

Array.prototype.isPrototypeOf([]); // true
Array.prototype.isPrototypeOf({}); // false

Object.prototype.isPrototypeOf([]); // true
Object.prototype.isPrototypeOf({}); // true


​ 4 typeof关键字只能判断函数和对象,不能判断数组和对象

2.2 数组方法

改变原数组内容:push...、reverse、splice、sort

不改变数组内容:concat、join(数组合并成字符串)、toString、slice、map...、findIndex

  • ​ 如果你需要一个数据,请使用—> map()方法
  • ​ 如果你需要一个结果,请使用—> reduce()方法
  • ​ 如果你需要过滤一个结果,请使用—> filter()方法

map() 对数组的每个元素都遍历一次,同时返回一个新的值(不改变原数组),注:返回的数据长度和原始数据长度是一致的。

// map() 方法的使用
let nums =[10,20,30,40,50]
let newnums = nums.map(function(n){
    return n * 3
})
console.log(newnums);  // 30,60,90,120,150


filter() filter() 中的回调函数有一个要求,必须返回一个boolean值!true() 当返回true时,函数内部会自动将这次回调的n加入到新的数组当中, false() 当返回的false时,函数内部会过滤掉这次的n

// filter() 方法的使用
currentValue	必须。当前元素的值
index	可选。当前元素的索引值
arr	可选。当前元素属于的数组对象
let nums =[10,20,30,40,50]
let newnums = nums.filter(function(n){
   return n < 30
})
console.log(newnums);   //10 20


reduce() reduce() 作用对数组中所有的内容进行汇总

//reduce() 方法的使用
arr: 表示将要原数组
prev:表示上一次调用回调时的返回值,或者初始值init
cur:表示当前正在处理的数组元素
index:表示正在处理的数组元素的索引,若提供init值,则索引为0,否则索引为1
init: 表示初始值
let nums = [10, 20, 30];
let add = nums.reduce(function(pre,cur){
    console.log(pre,cur);
    return pre + cur
},0)
console.log(add);

//这里的 0 ,是pre 的初始值
//第一次 pre:0   cur:10
//第二次 pre:0+10  cur:20
//第三次 pre:0+10+20  cur:30
//60


some() 方法用于检测数组中的元素是否满足指定条件(函数提供)。

​ 参数 fn是用来测试每个元素的函数,接受三个参数:

​ item:数组中正在处理的元素。

​ index:数组中正在处理的元素的索引值。

​ array:some()被调用的数组。

  • some() 方法会依次执行数组的每个元素:
  • 如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。如果没有满足条件的元素,则返回false。Array.from()

​ 伪数组:如果一个对象的所有键名都是正整数或零,并且有length属性,那么这个对象就很像数组,语法上称为"类似数组的对象"(array-like object),即为伪数组。典型的"类似数组的对象"是函数的arguments对象,以及大多数 DOM 元素集,还有字符串。

​ 将伪数组对象或可遍历对象转换为真数组, Array.from接受三个参数,但只有input是必须的:

  • input: 你想要转换的类似数组对象和可遍历对象
  • map: 类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组
  • context: 绑定map中用到的this
Array.from(obj, mapFn, thisArg) 相当于 Array.from(obj).map(mapFn, thisArg),

那么 Array.from({length: 5},(v,i)=>i) 就相当于 Array.from({length: 5}).map((v,i)=>i)。


array.splice(index,howmany,item1,.....,itemX)

如果从 arrayObject 中删除了元素,则返回的是含有被删除的元素的数组。

2.3 for

forEach()

arr.forEach((self,index,arr) =>{},this)
self: 数组当前遍历的元素,默认从左往右依次获取数组元素。
index: 数组当前元素的索引,第一个元素索引为0,依次类推。
arr: 当前遍历的数组。
this: 回调函数中this指向。


forEach是ES5提出的,挂载在可迭代对象原型上的方法,例如Array Set Map。

forEach() 本身是不支持的 continue 与 break 语句的,我们可以通过 someevery 来实现。使用 return 语句实现 continue 关键字的效果。

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

前端校招面经分享 文章被收录于专栏

前端校招面经分享,包含CSS、JS、Vue、React、计算机网络、难点项目、手撕题等。这份面经总结了几乎大厂所有的面试题与牛客近几年公开的面经,可以说面试不会超出范围。 因为我只负责总结加一些个人见解,所以一开始免费开源。但互联网戾气真的重,很多人拿着面经还一副理所应当的样子质问我要语雀,还说网上同类的有很多??唉,分享不易,那我只好收费了233。当然也欢迎直接来找我要语雀,语雀会多一些内容。

全部评论
爱了爱了
点赞 回复
分享
发布于 03-11 17:03 陕西
不错不错
点赞 回复
分享
发布于 03-11 17:14 陕西
联易融
校招火热招聘中
官网直投
大佬能收费教教我嘛😭😭
点赞 回复
分享
发布于 03-11 18:50 陕西
太牛了
点赞 回复
分享
发布于 03-11 19:05 陕西
大佬求带
点赞 回复
分享
发布于 03-11 19:30 云南
厉害 厉害
点赞 回复
分享
发布于 03-12 14:57 河北

相关推荐

#腾讯音乐工作体验#&nbsp;投递应该有一个月了,终于发面了,前面的笔试做的不好,都以为寄了。今天上午发邮件约面,直接约了下午。面试以八股为主,两个代码输出题,两个手写题。有几个问的还是挺难的,之前从来没见过。1.JS如何判断对象类型2.Object.prototype.toString.call()如果放进去一个Date数据类型会返回什么(&#39;[object&nbsp;Date]&#39;)3.基本数据类型和引用数据类型存储区别4.箭头函数普通函数5.两个代码题①输出function&nbsp;fn(a)&nbsp;{&nbsp;&nbsp;console.log(a);&nbsp;&nbsp;var&nbsp;a&nbsp;=&nbsp;2;&nbsp;&nbsp;function&nbsp;a()&nbsp;{&nbsp;&nbsp;}&nbsp;&nbsp;console.log(a);}fn(1);②页面显示和控制台(见图4)6.跨域方法7.Access-Control-Allow-Origin一般设置什么值?设置这些值有什么区别?对cookie有没有影响?(对cookie的影响这个不太清楚)8.浏览器缓存,强缓存两个关键字的区别,协商缓存的两对关键字9.etag的值是什么,怎么得到这个值10.协商缓存一般用哪个值11.什么情况下会出现文件更新了Last-Modify不更新的情况?(蒙了个更新频率太快的时候)12.CJS、ES6、AMD、CMD、UMD的理解和区别13.CSP?有什么规则和作用(这个不会)14.CSRF?如何防御15.webpack发布的原理16.有没有自己写过Plugin(一问到webpack就不会)17.Vue生命周期18.父子组件生命周期执行顺序19.双向绑定原理手写:发布订阅模式、单例模式#腾讯音乐##前端##暑期实习##软件开发2024笔面经##我的实习求职记录#
点赞 评论 收藏
转发
12 22 评论
分享
牛客网
牛客企业服务