2024JS前端面试题

1.JS变量提升

变量提升是指JS的变量和函数声明会在代码编译期,提升到代码的最前面。

变量提升成立的前提是使用Var关键字进行声明的变量,并且变量提升的时候只有声明被提升,赋值并不会被提升,同时函数的声明提升会比变量的提升优先。

变量提升的结果,可以在变量初始化之前访问该变量,返回的是undefined。在函数声明前可以调用该函数。

使用let和const声明的变量是创建提升,形成暂时性死区,在初始化之前访问let和const创建的变量会报错。

2.JS数据类型,区别

数据类型分为两类:一类是基本数据类型,也叫简单数据类型,包含7种类型,分别是Number 、String、Boolean、BigInt、Symbol、Null、Undefined。另一类是引用数据类型也叫复杂数据类型,通常用Object代表,普通对象,数组,正则,日期,Math数学函数都属于Object。

数据分成两大类的本质区别:基本数据类型和引用数据类型它们在内存中的存储方式不同。

基本数据类型是直接存储在栈中的简单数据段,占据空间小,属于被频繁使用的数据。

引用数据类型是存储在堆内存中,占据空间大。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。

Symbol是ES6新出的一种数据类型,这种数据类型的特点就是没有重复的数据,可以作为object的key。

数据的创建方法Symbol(),因为它的构造函数不够完整,所以不能使用new Symbol()创建数据。由于Symbol()创建数据具有唯一性,所以 Symbol() !== Symbol(), 同时使用Symbol数据作为key不能使用for获取到这个key,需要使用Object.getOwnPropertySymbols(obj)获得这个obj对象中key类型是Symbol的key值。

let key = Symbol('key');

let obj = { [key]: 'symbol'};

let keyArray = Object.getOwnPropertySymbols(obj); // 返回一个数组[Symbol('key')]

obj[keyArray[0]] // 'symbol'

BigInt也是ES6新出的一种数据类型,这种数据类型的特点就是数据涵盖的范围大,能够解决超出普通数据类型范围报错的问题。

使用方法:

-整数末尾直接+n:647326483767797n

-调用BigInt()构造函数:BigInt("647326483767797")

注意:BigInt和Number之间不能进行混合操作

多维输入,如何判断数组的最深层有多少层?例如:[1,3,3[4,6,7,[5,6,7,43,[23,4]]]],返回4!

递归:

function deepLevel(arr) { if (!Array.isArray(arr)) { return 0; } let result = 1; for (let i = 0; i < arr.length; i++) { if (Array.isArray(arr[i])) { result += deepLevel(arr[i]) } } return result; }

JSON.stringify(arr):

function deepLevel(arr) { if (!Array.isArray(arr)) { return 0; } const str = JSON.stringify(arr); let result = 0; let counter = 0; for (let val of str) { if (val === '[') { counter++; result = Math.max(result, counter); } else if (val === ']') { counter--; } } return result; }

while循环,判断tempArr.length>0:

function deepLevel(arr) { if (!Array.isArray(arr)) { return 0; } let result = 1; const tempArr = [{ level: 1, data: arr }]; while (tempArr.length > 0) { const counter = tempArr.shift(); result = Math.max(result, counter.level); counter.data.forEach(a => { if (Array.isArray(a)) { tempArr.push({ level: counter.level + 1, data: a }); } }) } return result; }

js 如何判断空对象

  • 将对象转为字符串比较;主要使用JSON.stringify()这个方法对对象进行强转:

var a={}; var b=new Object(); console.log(JSON.stringify(a)=="{}") //true console.log(JSON.stringify(b)=="{}") //true

  • for…in循环;使用for in循环可以遍历所有属性以次判断对象是否为空对象:

var a={}; function isEmptyObject(obj){ for(var key in obj){ return false }; return true }; console.log(isEmptyObject(a));

  • Object.getOwnPropertyNames(),方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。用此方法判断空对象只需要判断返回的数组长度是否为零,为零的话就是空对象。

var obj = { }; console.log(Object.getOwnPropertyNames(obj).length == 0); // true

  • Object.keys()该方法属于 ES5 标准,IE9 以上和其它现代浏览器均支持。Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用 for…in 循环遍历该对象时返回的顺序一致。用此方法判断空对象只需要判断返回的数组长度是否为零,为零的话就是空对象。

var data = {}; var arr = Object.keys(data); console.log(arr.length == 0);//true

0.1+0.2不等于0.3

在计算机中,浮点数的加法并不总是精确的,这主要是由于计算机的存储方式和二进制表示法的限制。

计算机使用二进制系统来表示数字,但是十进制中的某些数字在二进制中无法精确表示。例如,0.1 在二进制中是一个无限循环的小数,因此计算机无法精确地表示它。同样,0.2 和 0.3 在二进制中也是无限循环小数,所以它们也无法被精确表示。

当计算机执行加法操作时,由于存储的限制,它可能会产生精度误差。因此,虽然 0.1 加 0.2 在数学上应该等于 0.3,但由于计算机浮点数的表示精度限制,这个等式在计算机中可能不成立。

另外,不同的编程语言和平台可能对浮点数的处理方式不同,这也可能导致不同的结果。

为了避免这种精度问题,程序员可以使用一些技巧,如四舍五入、使用更大的数据类型或使用特定的库来进行高精度的浮点数运算。

0.1和0.2在二进制中是无限循环小数,但由于计算机的存储限制,它们可能会被近似表示。但是,当0.1加上0.1时,由于它们的有效精度位数可以互相抵消,结果可能会被精确地表示为0.2。

3.对闭包的理解

闭包 一个函数和词法环境的引用捆绑在一起,这样的组合就是闭包(closure)。

一般就是一个函数A,return其内部的函数B,被return出去的B函数能够在外部访问A函数内部的变量,这时候就形成了一个B函数的变量背包,A函数执行结束后这个变量背包也不会被销毁,并且这个变量背包在A函数外部只能通过B函数访问。

闭包形成的原理:作用域链,当前作用域可以访问上级作用域中的变量

闭包解决的问题:能够让函数作用域中的变量在函数执行结束之后不被销毁,同时也能在函数外部可以访问函数内部的局部变量。

闭包带来的问题:由于垃圾回收器不会将闭包中变量销毁,于是就造成了内存泄露,内存泄露积累多了就容易导致内存溢出。由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,过度使用闭包可能会导致内存占用过多的问题。所以不能滥用闭包,否则会造成网页的性能问题。

闭包的应用,能够模仿块级作用域,能够实现柯里化,在构造函数中定义特权方法、Vue中数据响应式Observer中使用闭包等。

如果销毁一个闭包,可以把被引用的变量设置为 null。即手动清除变量,这样下次 js 垃圾回收机制回收时,就会把设为 null 的变量给回收了。

引起内存泄漏的原因:

  • 意外的全局变量,由于 js 对未声明变量的处理方式是在全局对象上创建该变量的引用。如果在浏览器中,全局对象就是 window 对象。变量在窗口关闭或重新刷新页面之前都不会被释放,如果未声明的变量缓存大量的数据,就会导致内存泄露。解决方法,避免创建全局变量,使用严格模式, 在 JavaScript 文件头部或者函数的顶部加上 use strict。
  • 闭包引起的内存泄漏
  • 没有清理的 DOM 元素引用,虽然别的地方删除了,但是对象中还存在对 dom 的引用。解决方法:手动删除
  • 被遗忘的定时器或者回调,定时器中有 dom 的引用,即使 dom 删除了,但是定时器还在,所以内存中还是有这个 dom。解决方法:手动删除定时器和 dom。removeEventListener 移除事件监听
  • console使用问题,生产环境最好不要有console.log,如果有,最好是文字或者格式化后的JSON字符串,不要引用当前实例的数据或是dom对象,否则相关引用将无法回收。

4.promise、使用方法

Promise的作用:

Promise是异步微任务,解决了异步多层嵌套回调的问题,让代码的可读性更高,更容易维护;

promise的then方法的参数的意义是什么:

在Promise的then方法中,可以传入两个参数,第一个参数是成功时执行的函数,第二个参数是失败时执行的函数。这两个函数都是可选的,如果不传入,则会将结果传递给下一个then方法。如果第一个函数抛出异常,则会执行第二个函数。

Promise使用:

Promise是ES6提供的一个构造函数,可以使用Promise构造函数new一个实例

Promise构造函数接收一个函数作为参数,这个函数有两个参数,分别是两个函数 `resolve`和`reject`:

  • `resolve`将Promise的状态由等待变为成功,将异步操作的结果作为参数传递过去;
  • `reject`则将状态由等待转变为失败,在异步操作失败时调用,将异步操作报出的错误作为参数传递过去;

实例创建完成后,可以使用`then`方法分别指定成功或失败的回调函数,也可以使用catch捕获失败,then和catch最终返回的也是一个Promise,所以可以链式调用。

promise的实现原理:

是基于回调函数的,它通过回调函数的方式处理异步操作的结果,promise的实现原理可以分为三个部分,状态、值和回调函数;

Promise的特点:

1. 对象的状态不受外界影响(Promise对象代表一个异步操作,有三种状态)。 - pending(执行中) - Resolved(成功,又称Fulfilled) - rejected(拒绝) 其中pending为初始状态,fulfilled和rejected为结束状态(结束状态表示promise的生命周期已结束)。

2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。 Promise对象的状态改变,只有两种可能(状态凝固了,就不会再变了,会一直保持这个结果): - 从Pending变为Resolved - 从Pending变为Rejected

3. resolve 方法的参数是then中回调函数的参数,reject 方法中的参数是catch中的参数

4. then 方法和 catch方法 只要不报错,返回的都是一个resolved状态的promise

Promise的其他方法:

Promise.resolve() :返回的Promise对象状态为resolved,并且将该value传递给对应的then方法。

Promise.reject():返回一个状态为失败的Promise对象,并将给定的失败信息传递给对应的处理方法。

Promise.all():返回一个新的promise对象,该promise对象在参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。

Promise.any():接收一个Promise对象的集合,当其中的一个 promise 成功,就返回那个成功的promise的值;

Promise.race():当参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。

Promise.finally():finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。

Promise.allSettled():该 promise 在所有给定的 promise 已被解析或被拒绝后解析,并且每个对象都描述每个 promise 的结果。

Promise中的then第二个参数和catch有什么区别

第一,reject是用来抛出异常的,catch是用来处理异常的;第二:reject是Promise的方法,而then和catch是Promise实例的方法

主要区别就是,如果在then的第一个函数里抛出了异常,后面的catch能捕获到,而then的第二个函数捕获不到。

then的第二个参数和catch捕获错误信息的时候会就近原则,如果是promise内部报错,reject抛出错误后,then的第二个参数和catch方法都存在的情况下,只有then的第二个参数能捕获到,如果then的第二个参数不存在,则catch方法会捕获到。

5.JavaScript有几种方法判断变量的类型

JavaScript有4种方法判断变量的类型,分别是typeof、instanceof、Object.prototype.toString.call()(对象原型链判断方法)、 constructor (用于引用数据类型)

  • typeof:常用于判断基本数据类型,对于引用数据类型除了function返回’function‘,其余全部返回’object'
  • instanceof:主要用于区分引用数据类型,检测方法是检测的类型在当前实例的原型链上,用其检测出来的结果都是true,不太适合用于简单数据类型的检测,检测过程繁琐且对于简单数据类型中的undefined, null, symbol检测不出来。
  • constructor:用于检测引用数据类型,检测方法是获取实例的构造函数判断和某个类是否相同,如果相同就说明该数据是符合那个数据类型的,这种方法不会把原型链上的其他类也加入进来,避免了原型链的干扰。
  • Object.prototype.toString.call():适用于所有类型的判断检测,检测方法是Object.prototype.toString.call(数据) 返回的是该数据类型的字符串。

这四种判断数据类型的方法中,各种数据类型都能检测且检测精准的就是Object.prototype.toString.call()这种方法。

instanceof的实现原理:验证当前类的原型prototype是否会出现在实例的原型链__proto__上,只要在它的原型链上,则结果都为true。因此,`instanceof` 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 `prototype`,找到返回true,未找到返回false。

实现 instanceof

function myInstanceOf(a,b){ let valA = a.__proto__; let valB = b.prototype; while(1){ if(valA == null) return false; if(valA == valB) return true; valA = valA.__proto__; } } function Person(name){ this.name = name; } let per = new Person("Asia"); let obj = {} console.log(myInstanceOf(per,Person));//true console.log(per instanceof Person);//true console.log(myInstanceOf(obj,Person));//false console.log(obj instanceof Person);//fasle

6.JS实现异步的方法

所有异步任务都是在同步任务执行结束之后,从任务队列中依次取出执行。

回调函数是异步操作最基本的方法,比如AJAX回调,回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。此外它不能使用 try catch 捕获错误,不能直接 return;

Promise包装了一个异步调用并生成一个Promise实例,当异步调用返回的时候根据调用的结果分别调用实例化时传入的resolve 和 reject方法,then接收到对应的数据,做出相应的处理。

Promise不仅能够捕获错误,而且也很好地解决了回调地狱的问题,缺点是无法取消 Promise,错误需要通过回调函数捕获。

Generator 函数是 ES6 提供的一种异步编程解决方案,Generator 函数是一个状态机,封装了多个内部状态,可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果。优点是异步语义清晰,缺点是手动迭代`Generator` 函数很麻烦,实现逻辑有点绕;

async/awit是基于Promise实现的,async/awit使得异步代码看起来像同步代码,所以优点是,使用方法清晰明了,缺点是awit 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 awit 会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all 的方式。

JS 异步编程进化史:callback -> promise -> generator/yield -> async/awit。

async/awit函数对 Generator 函数的改进,体现在以下三点:

  1. 内置执行器。 Generator 函数的执行必须靠执行器,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
  2. 更广的适用性。 yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 awit 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
  3. 更好的语义。 async 和 awit,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,awit 表示紧跟在后面的表达式需要等待结果。 目前使用很广泛的就是promise和async/awit。

async/awit的原理

用来简化 Promise 异步操作。

async 用来声明一个 function 是异步的,await 用来等待一个异步方法执行完成。

async 声明的函数,返回的结果是一个 Promise 对象

await 等待的是右侧表达式的返回值

如果 await 等到的不是一个 Promise 对象,那么 await 表达式的运算结果就是它等到的东西,比如返回的是字符串,那么运算结果就是字符串

如果 await 等到的是一个 Promise 对象,await 就开始忙起来,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

7.数组去重

第一种方法:

利用对象属性key排除重复项:遍历数组,每次判断对象中是否存在该属性,不存在就存储在新数组中,并且把数组元素作为key,设置一个值,存储在对象中,最后返回新数组。这个方法的优点是效率较高,缺点是占用了较多空间,使用的额外空间有一个查询对象和一个新的数组

第二种方法:

利用Set类型数据无重复项:new 一个 Set,参数为需要去重的数组,Set 会自动删除重复的元素,再将 Set 转为数组返回。这个方法的优点是效率更高,代码简单,思路清晰,缺点是可能会有兼容性问题

const result = Array.from(new Set(arr))

第三种方法:

filter+indexof 去重:这个方法和第一种方法类似,利用 Array 自带的 filter 方法,返回 arr.indexOf(num) 等于 index 的num。原理就是 indexOf 会返回最先找到的数字的索引,假设数组是 [1, 1],在对第二个1使用 indexOf 方法时,返回的是第一个1的索引0。这个方法的优点是可以在去重的时候插入对元素的操作,可拓展性强。

function removeDuplicate(arr) { return arr.filter((item, index) => { return arr.indexOf(item) === index }) } function removeDuplicate(arr) { let list = []; return arr.filter((item) => !list.includes(item.name) && list.push(item.name)) }

第四种方法:

这个方法比较巧妙,从头遍历数组,如果元素在前面出现过,则将当前元素挪到最后面,继续遍历,直到遍历完所有元素,之后将那些被挪到后面的元素抛弃。这个方法因为是直接操作数组,占用内存较少。

第五种方法:

reduce +includes去重:这个方法就是利用reduce遍历和传入一个空数组作为去重后的新数组,然后内部判断新数组中是否存在当前遍历的元素,不存在就插入到新数组中。这种方法时间消耗多,内存空间也有额外占用。

以上五个方法中,在数据低于10000条的时候没有明显的差别,高于10000条,第一种和第二种的时间消耗最少,后面三种时间消耗依次增加,由于第一种内存空间消耗比较多,且现在很多项目不再考虑低版本浏览器的兼容性问题,所以建议使用第二种去重方法,简洁方便。

第六种方法:单层for循环

let newlist5 = []; for (let i = 0; i < list.sort().length; i++) { if (list[i] == list[i + 1]) { list.splice(i, 1) i-- } }

第七种:利用map去重

let newList3 = []; let map = new Map() list.forEach((item) => { // 如果map.has指定的item不存在,那么就设置key和value 这个item就是当前map里面不存在的key,把这个item添加到新数组 // 如果下次出现重复的item,那么map.has(item等于ture 取反 !map.has(item) 不执行 if (!map.has(item)) { map.set(item,ture) newList3.push(item) } })

8.null 和 undefined 的区别

undefind 是全局对象的一个属性,当一个变量没有被赋值或者一个函数没有返回值或者某个对象不存在某个属性却去访问或者函数定义了形参但没有传递实参,这时候都是undefined。

undefined通过typeof判断类型是'undefined'。

undefined == undefined undefined === undefined 。

null代表对象的值未设置,相当于一个对象没有设置指针地址就是null。null通过typeof判断类型是'object'。

null === null null == null null == undefined null !== undefined

undefined 表示一个变量初始状态值,而 null 则表示一个变量被人为的设置为空对象,而不是原始状态。在实际使用过程中,不需要对一个变量显式的赋值 undefined,当需要释放一个对象时,直接赋值为 null 即可。

让一个变量为null,直接给该变量赋值为null即可。

null 其实属于自己的类型 Null,而不属于Object类型,typeof 之所以会判定为 Object 类型,是因为JavaScript 数据类型在底层都是以二进制的形式表示的,二进制的前三位为 0 会被 typeof 判断为对象类型,而 null 的二进制位恰好都是 0 ,因此,null 被误判断为 Object 类型。 对象被赋值了null 以后,对象对应的堆内存中的值就是游离状态了,GC 会择机回收该值并释放内存。因此,需要释放某个对象,就将变量设置为 null,即表示该对象已经被清空,目前无效状态。

9.es6中箭头函数

箭头函数相当于匿名函数,简化了函数定义。

箭头函数有两种写法,当函数体是单条语句的时候可以省略{}和return。

另一种是包含多条语句,不可以省略{}和return。

箭头函数最大的特点就是没有this,所以this是从外部获取,就是继承外部的执行上下文中的this,由于没有this关键字所以箭头函数也不能作为构造函数, 同时通过 `call()` 或 `apply()` 方法调用一个函数时,只能传递参数(不能绑定this),第一个参数会被忽略。箭头函数也没有原型和super。

不能使用yield关键字,因此箭头函数不能用作 Generator 函数。不能返回直接对象字面量。

箭头函数继承来的this指向永远不会改变

call()、apply()、bind()等方法不能改变箭头函数中this的指向

箭头函数不能作为构造函数使用

由于箭头函数时没有自己的this,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。

箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。

箭头函数没有prototype

箭头函数的不适用场景:

  • 定义对象上的方法 当调用` dog.jumps` 时,`lives` 并没有递减。因为 `this` 没有绑定值,而继承父级作用域。 var dog = { lives: 20, jumps: () => { this.lives--; } }
  • 不适合做事件处理程序 此时触发点击事件,this不是button,无法进行class切换 var button = document.querySelector('button'); button.addEventListener('click', () => { this.classList.toggle('on'); });

箭头函数函数适用场景:

  • 简单的函数表达式,内部没有this引用,没有递归、事件绑定、解绑定,适用于map、filter等方法中,写法简洁 var arr = [1,2,3]; var newArr = arr.map((num)=>num*num)
  • 内层函数表达式,需要调用this,且this应与外层函数一致时 let group = { title: "Our Group", students: ["John", "Pete", "Alice"], showList() { this.students.forEach( student => alert(this.title + ': ' + student) ); } }; group.showList();

10.call apply bind的作用和区别

call、apply、bind的作用都是改变函数运行时的this指向。

bind和call、apply在使用上有所不同:

  • bind在改变this指向的时候,返回一个改变执行上下文的函数,不会立即执行函数,而是需要调用该函数的时候再调用即可,但是call和apply在改变this指向的同时执行了该函数。
  • bind接受两个参数,第一个就是this指向的执行上文, 第二个和call类似, 传入参数列表。 call、apply接收多个参数,第一个参数都是this指向的执行上文,后面的参数都是作为改变this指向的函数的参数。但是call和apply参数的格式不同,call是一个参数对应一个原函数的参数,但是apply第二个参数是数组,数组中每个元素代表函数接收的参数,数组有几个元素函数就接收几个元素。
  • 当 bind 返回的函数 使用new作为构造函数时,绑定的 this 值会失效,this指向实例对象,但传入的参数依然生效 (new调用的优先级 > bind调用)

call的应用场景:

对象的继承,在子构造函数这种调用父构造函数,但是改变this指向,就可以继承父的属性 function superClass () { this.a = 1; this.print = function () { console.log(this.a); } } function subClass () { superClass.call(this); // 执行superClass,并将superClass方法中的this指向subClass this.print(); } subClass(); 借用Array原型链上的slice方法,把伪数组转换成真数组 let domNodes = Array.prototype.slice.call(document.getElementsByTagName("div"));

apply的应用场景:

Math.max,获取数组中最大、最小的一项 let max = Math.max.apply(null, array); let min = Math.min.apply(null, array); 实现两个数组合并 let arr1 = [1, 2, 3]; let arr2 = [4, 5, 6]; Array.prototype.push.apply(arr1, arr2); console.log(arr1); // [1, 2, 3, 4, 5, 6]

bind的应用场景 在vue或者react框架中,使用bind将定义的方法中的this指向当前类

Function.prototype.bind = function(context){ // 取出bind方法中传入的除第一个参数(第一个参数是需要绑定的this)外的其余参数存在args数组中 var args = Array.prototype.slice.call(arguments, 1), // 这里的this是指调用bind方法的函数 self = this; return function(){ // 获取执行bind函数传入的参数 var innerArgs = Array.prototype.slice.call(arguments); // 将第二个括号中的参数concat进args得到除第一个参数外所有传入的参数(这里有两个知识点:1、因为闭包args参数的值一直存在在内存中;2、偏函数(和函数柯里化相似但有点不同)) var finalArgs = args.concat(innerArgs); // 调用apply方法,return函数结果 return self.apply(context,finalArgs); }; }; Function.prototype.call = function(context, ...args) { if (typeof this !== 'function') console.error('type Error'); context = (context!==null && context!==undefined) ? Object(context) : window context.fn = this // 2 const result = context.fn(...args) // 3 delete context.fn; return result } Function.prototype.apply = function (context, arr) { var context = Object(context) || window; context.fn = this; var result; //在call()的基础上增加了一次对arr该第二参数的判断 if (!arr) { result = context.fn(); } else { var args = []; for (var i = 0, len = arr.length; i < len; i++) { args.push('arr[' + i + ']'); } result = eval('context.fn(' + args + ')') } delete context.fn return result; }

11.this指向

this关键字由来:

在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。

但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套 this 机制。

this存在的场景有三种:全局执行上下文、函数执行上下文和eval执行上下文,eval这种不讨论。

  • 在全局执行环境中无论是否在严格模式下,(在任何函数体外部)`this` 都指向全局对象。
  • 在函数执行上下文中访问this,函数的调用方式决定了 `this` 的值。在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window,通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
  • 普通函数this指向:
  1. 当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;
  2. 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
  3. new 关键字构建好了一个新对象,并且构造函数中的 this 其实就是新对象本身。
  4. 嵌套函数中的 this 不会继承外层函数的 this 值。
  5. 箭头函数this指向:箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数。

箭头函数因为没有this,所以也不能作为构造函数,但是需要继承函数外部this的时候,使用箭头函数比较方便;

12.map 和 forEach 的区别

map 和 forEach 的区别:

map有返回值,可以开辟新空间,return出来一个length和原数组一致的数组,即便数组元素是undefined或者是null。forEach默认无返回值,返回结果为undefined,可以通过在函数体内部使用索引修改数组元素。

map的处理速度比forEach快,而且返回一个新的数组,方便链式调用其他数组新方法,比如filter、reduce;

13.js继承的方法和优缺点

1.原型链继承:让一个构造函数的原型是另一个类型的实例,那么这个构造函数new出来的实例就具有该实例的属性,原型链继承的。

优点:写法方便简洁,容易理解。

缺点:在父类型构造函数中定义的引用类型值的实例属性,会在子类型原型上变成原型属性被所有子类型实例所共享。同时在创建子类型的实例时,不能向超类型的构造函数中传递参数。

2.借用构造函数继承:在子类型构造函数的内部调用父类型构造函数;使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上。

优点:解决了原型链实现继承的不能传参的问题和父类的原型共享的问题。

缺点:借用构造函数的缺点是方法都在构造函数中定义,因此无法实现函数复用。在父类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

3.组合继承:将原型链和借用构造函数的组合到一块。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性。

优点就是解决了原型链继承和借用构造函数继承造成的影响。

缺点是无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部

4.原型式继承:在一个函数A内部创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。本质上,函数A是对传入的对象执行了一次浅复制。ECMAScript 5通过增加Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时,Object.create()与这里的函数A方法效果相同。

优点是:不需要单独创建构造函数。

缺点是:属性中包含的引用值始终会在相关对象间共享

5.寄生式继承:寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

优点:写法简单,不需要单独创建构造函数。

缺点:通过寄生式继承给对象添加函数会导致函数难以重用。

6.寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

优点是:高效率只调用一次父构造函数,并且因此避免了在子原型上面创建不必要,多余的属性。与此同时,原型链还能保持不变;

缺点是:代码复杂

7.ES6 Class实现继承。原理:原理ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。 ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。

优点:语法简单易懂,操作更方便。

缺点:并不是所有的浏览器都支持class关键字

14.new

`new` 关键字会进行如下的操作:

1. 创建一个空的简单JavaScript对象(即`{}`);

2. 为步骤1新创建的对象添加属性`__proto__`,将该属性链接至构造函数的原型对象 ;

3. 将步骤1新创建的对象作为`this`的上下文 ;

4. 如果该函数没有返回对象,则返回`this`。

`new`关键字后面的构造函数不能是箭头函数。

15.伪数组和数组的区别

伪数组它的类型不是Array,而是Object,而数组类型是Array。可以使用的length属性查看长度,也可以使用[index]获取某个元素,但是不能使用数组的其他方法,也不能改变长度,遍历使用for in方法。

伪数组的常见场景:

-函数的参数arguments

-原生js获取DOM:document.querySelector('div') 等

-jquery获取DOM:$(“div”)等

伪数组转换成真数组方法

-Array.prototype.slice.call(伪数组)

-[].slice.call(伪数组)

-Array.from(伪数组) 转换后的数组长度由 `length` 属性决定。索引不连续时转换结果是连续的,会自动补位。

16.JavaScript是单线程

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

JS如何实现多线程

JavaScript本身是单线程的,但是可以通过实现多线程来提高性能和用户体验。多线程允许JavaScript在等待用户交互或网络请求时,执行其他任务,从而提高页面加载速度和响应速度。

JavaScript有多种实现多线程的方式,包括Web Workers、SharedArrayBuffer、WebAssembly等。其中,Web Workers允许在后台线程中运行JavaScript代码,而SharedArrayBuffer和BufferSource API则允许在多个线程之间共享数据。

使用Web Workers实现多线程需要创建一个新的worker线程,并将需要执行的代码作为字符串传递给worker。worker线程可以访问全局对象messageChannel的postMessage方法来发送消息,主线程可以使用onmessage方法来接收消息并执行相应的操作。

多线程环境下的安全问题主要包括数据竞争和死锁等。为了解决这些问题,需要使用同步机制,如使用Promise、async/await等异步编程方式,或者使用事件循环、共享内存等机制来保证数据的一致性和安全性。

在实际应用中,多线程可以用于提高页面加载速度和响应速度,例如在电商网站中,可以使用Web Workers在后台线程中加载和处理商品图片,从而提高页面加载速度和用户体验。同时,多个并发请求也可以使用Web Workers并行处理,提高系统性能和响应速度。

17.深拷贝、浅拷贝

由于基本数据类型是存储在栈内存中,在从一个变量向另一个变量复制基本类型值时,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上,新变量和原变量可以参与任何操作而互不影响。而对于引用类型这种复杂的数据类型,他们的实体内容则会被存储到堆内存中,栈内存只会存储他们在堆内存的一串地址。所以深拷贝和浅拷贝的概念只适用于对象或者数组这种复杂数据类型(引用数据类型)。

浅拷贝的意思就是,复制了对象(数组)存储在栈内存中的地址,而不是在内存中重新开辟一个新的存储空间用于存储新的对象。也就是两个对象共用一个堆内存内容。

深拷贝几种实现方式:

1.利用JSON的序列化和反序列化方法,可以实现简易对象深拷贝,但此种方法有较大的限制:

  • 会忽略属性值为undefined的属性
  • 会忽略属性为Symbol的属性
  • 不会序列化函数
  • 不能解决循环引用的问题,直接报错

2.自己写一个函数,主要使用for in 循环原来的对象

3.使用lodash(opens new window)第三方函数库实现(需要先引入lodash.js)

let newObj = _.cloneDeep(obj);

递归:

function deepClone1(obj) { //判断拷贝的要进行深拷贝的是数组还是对象,是数组的话进行数组拷贝,对象的话进行对象拷贝 var objClone = Array.isArray(obj) ? [] : {}; //进行深拷贝的不能为空,并且是对象或者是 if (obj && typeof obj === "object") { for (key in obj) { if (obj.hasOwnProperty(key)) { if (obj[key] && typeof obj[key] === "object") { objClone[key] = deepClone1(obj[key]); } else { objClone[key] = obj[key]; } } } } return objClone; }

JSON.stringify:(只能拷贝没有方法的对象)

function deepClone2(obj) { var _obj = JSON.stringify(obj), objClone = JSON.parse(_obj); return objClone; }

js中的扩展运算符(...)、 slice() 、concat() 和 assign() 方法 是深拷贝还是浅拷贝?

concat、slice、assign() 以及 es6扩展运算符(...)仅拷贝对象的第一层目录,因此,如果对象属性是基本数据类型则是深拷贝,如果对象属性是引用数据类型则是浅拷贝。

18.防抖、节流

1.防抖 (多次触发 只执行最后一次)

防抖策略(debounce)是当事件被触发后,延迟n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时。

作用: 高频率触发的事件,在指定的单位时间内,只响应最后一次,如果在指定的时间内再次触发,则重新计算时间。

1.1 防抖的应用场景

登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖;

调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖;

文本编辑器实时保存,当无任何更改操作一秒后进行保存。

2.节流 (规定时间内 只触发一次)

节流策略(throttle),控制事件发生的频率,如控制为1s发生一次,甚至1分钟发生一次。与服务端(server)及网关(gateway)控制的限流 (Rate Limit) 类似。

作用: 高频率触发的事件,在指定的单位时间内,只响应第一次。

2.1 节流的应用场景

鼠标连续不断地触发某事件(如点击),单位时间内只触发一次;

监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断。例如:懒加载;

浏览器播放事件,每个一秒计算一次进度信息等。

19.reduce

reduce()方法接受一个函数作为累加器,数组中的每个值(从左向右)开始缩减,最终计算为一个值。对空数组是不会执行回调函数的。

20.作用域

JS中作用域有:全局作用域、函数作用域。没有块作用域的概念。ECMAScript 6(简称ES6)中新增了块级作用域,使用let声明的变量只能在块级作用域里访问,有“暂时性死区”的特性(也就是说声明前不可用)。

21.原型

JavaScript 是基于原型继承(Prototypal inheritance)的语言。原型(prototype)是给其他对象提供共享属性的对象,每个函数都有一个 prototype 属性,它指向的是一个 prototype 对象。每个对象都有一个隐式引用([[Prototype]]),并且 [[Prototype]] 指向它的原型对象,并从中继承数据、结构和行为。同时原型对象同样拥有原型(函数也是对象,它也有[[Prototype]]),这样一层一层,最终指向 null,这种关系被称为原型链。

(1) prototype:所有的函数都有原型prototype属性,这个属性指向函数的原型对象。

(2) proto,这是每个对象(除null外)都会有的属性,叫做__proto__,这个属性会指向该对象的原型。

(3) constructor: 每个原型都有一个constructor属性,指向该关联的构造函数。

原型链:获取对象时,如果这个对象上本身没有这个属性时,它就会去它的原型__proto__上去找,如果还找不到,就去原型的原型上去找...一直直到找到最顶层(Object.prototype)为止,Object.prototype对象也有__proto__属性,值为null

此外,每一个prototype原型上都会有一个constructor属性,指向它关联的构造函数。

22.事件捕获、事件冒泡

事件捕获是事件传播的第一个阶段。在事件捕获阶段,事件从文档的根节点(通常是

元素)向下传播到目标元素。这意味着最外层的元素首先接收事件,然后是内部元素,依此类推,直到达到事件的目标元素。

事件冒泡是事件传播的第二个阶段。在事件冒泡阶段,事件从目标元素开始向外传播,逐级冒泡至文档的根节点。

事件捕获和事件冒泡可以用于不同的场景,使事件处理更加灵活。一些常见的应用场景包括:

事件代理(Event Delegation):通过在父元素上捕获事件并根据目标元素来执行不同的操作,可以减少事件处理程序的数量,提高性能。

事件委托(Event Delegation):可以在事件捕获或事件冒泡阶段注册事件处理程序,根据需要选择使用哪个阶段。

23.函数柯里化

一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

24.事件循环Event loop,宏任务与微任务

浏览器的事件循环:

执行js代码的时候,遇见同步任务,直接推入调用栈中执行,遇到异步任务,将该任务挂起,等到异步任务有返回之后推入到任务队列中,当调用栈中的所有同步任务全部执行完成,将任务队列中的任务按顺序一个一个的推入并执行,重复执行这一系列的行为。

异步任务又分为宏任务和微任务。

宏任务:任务队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列。

微任务:等宏任务中的主要功能都完成后,渲染引擎不急着去执行下一个宏任务,而是执行当前宏任务中的微任务

宏任务包含:执行script标签内部代码、setTimeout/setInterval、ajax请求、postMessageMessageChannel、setImmediate,I/O(Node.js)

微任务包含:Promise、MutonObserver、Object.observe、process.nextTick(Node.js)

宏任务和微任务的执行顺序是:同步任务先执行,然后先执行微任务队列中的所有微任务,再执行一个宏任务,然后再检查微任务队列,有就执行全部微任务,然后再执行一个宏任务,直到微任务队列中没有任务,再跳到下一个宏任务队列执行。

微任务比宏任务优先执行的原因在于JavaScript的事件循环机制。事件循环中的每一轮循环都会先处理宏任务,然后再处理微任务。由于微任务的执行时间短,且不会阻塞主线程,因此它们通常会在宏任务之前完成。

当一个宏任务执行完后,事件循环会检查微任务队列,如果有微任务存在,就会立即执行所有微任务。由于微任务通常会在宏任务之前完成,因此它们可以更快地响应用户的交互和其他的异步操作。

通过区分宏任务和微任务,JavaScript可以更好地协调任务的执行顺序,提高应用程序的性能和响应速度。

25.事件扩展符

展开语法(Spread syntax), 可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;

还可以在构造字面量对象时, 将对象表达式按key-value的方式展开。

常见的场景:等价于apply的方式、将数组展开为构造函数的参数、字面量数组或字符串连接不需要使用concat等方法了、构造字面量对象时,进行浅克隆或者属性拷贝

只能用于可迭代对象 在数组或函数参数中使用展开语法时, 该语法只能用于 可迭代对象: var obj = {'key1': 'value1'}; var array = [...obj]; // TypeError: obj is not iterable 剩余语法(剩余参数) 剩余语法(Rest syntax) 看起来和展开语法完全相同,不同点在于, 剩余参数用于解构数组和对象。从某种意义上说,剩余语法与展开语法是相反的:展开语法将数组展开为其中的各个元素,而剩余语法则是将多个元素收集起来并“凝聚”为单个元素。 function f(...[a, b, c]) { return a + b + c; } f(1) // NaN (b and c are undefined) f(1, 2, 3) // 6 f(1, 2, 3, 4) // 6 (the fourth parameter is not destructured)

26.js的几种设计模式

1.工厂模式

简单工厂模式:为解决多个相似的问题

抽象工厂模式:将其成员对象的实列化推迟到子类中,子类可以重写父类接口方法以便创建的时候指定自己的对象类型

2.单例模式

一个类只有一个实例,并且提供可全局访问点 全局对象是最简单的单例模式

3.模块模式

模块模式的思路是为单体模式添加私有变量和私有方法能够减少全局变量的使用

4.代理模式

代理对象可以代替本体被实例化,并使其可以被远程访问

27.什么是JSON,如何解析和序列化JSON数据

JSON的解析和序列化涉及到将JSON数据转换为程序能处理的格式,或者将程序数据转换为JSON格式以便于传输。

解析JSON数据,即将JSON格式的字符串转换为程序能处理的格式(如JavaScript对象),通常需要使用JSON解析库或函数。例如,在JavaScript中,可以使用

JSON.parse()方法来解析JSON字符串。该方法接受一个JSON字符串作为参数,并返回一个JavaScript对象。

序列化JSON数据,即将程序数据转换为JSON格式以便于传输,通常需要使用JSON序列化库或函数。例如,在JavaScript中,可以使用

JSON.stringify()方法来序列化JavaScript对象。该方法接受一个JavaScript对象作为参数,并返回一个JSON格式的字符串。

28.如何使用ES6的模块系统,以及它与CommonJS的区别是什么

ES6(ECMAScript 2015)引入了模块化的概念,这是一种将代码拆分成独立文件的方式,每个文件都可以作为一个模块,有自己的作用域和生命周期。使用ES6模块系统,可以通过import和export关键字来导入和导出模块的特定部分。

ES6模块与CommonJS模块存在以下主要差异:

  1. 语法:ES6使用export和import关键字,而CommonJS使用module.exports和require关键字。
  2. 静态还是动态:ES6模块在编译时加载,并且输出的是值的引用,而CommonJS模块在运行时加载,输出的是值的拷贝。这意味着ES6模块更加高效,因为它们可以在编译阶段进行优化。
  3. 异步加载:ES6支持异步加载模块,而CommonJS不支持。这意味着ES6更加适合构建大型应用程序,因为它可以更好地利用现代浏览器的缓存机制和并行处理能力。

29.如何进行有效的错误处理和异常捕获

  1. 抛出异常:当检测到错误或异常情况时,应当抛出异常。这可以通过使用throw关键字来实现。
  2. 捕获异常:使用try/catch语句来捕获和处理异常。try块包含可能会抛出异常的代码,而catch块包含处理异常的代码。
  3. 错误处理:在捕获到异常后,通常需要对其进行处理。这可能包括记录错误、通知用户、回滚事务等。
  4. 记录错误:通过将错误信息写入日志文件或数据库,可以帮助后续分析问题。可以使用专门的日志库,如Python的logging模块或JavaScript的console.log函数。
  5. 显示友好的错误消息:当捕获到异常时,向用户显示友好的错误消息而不是技术性错误信息,可以提高用户体验。
  6. 回滚事务:在数据库操作中,如果遇到异常,应当回滚事务以保持数据的一致性。
  7. 避免过度捕获:避免使用过于宽泛的捕获范围,这可能会导致一些意外的问题被隐藏起来。
  8. 自定义异常:根据需要,可以定义自己的异常类,以便于区分不同的错误类型。
  9. 检查输入参数:在函数或方法的开始处,检查输入参数的有效性,避免无效或恶意的输入导致异常。
  10. 使用断言:在某些情况下,断言可以帮助验证程序的某些假设是否成立。如果断言失败(即假设不成立),则抛出异常。
  11. 提供恢复机制:在某些情况下,尽管发生了错误,但程序仍可以继续运行。提供这样的机制可以提高程序的容错能力。
  12. 使用第三方库:有些语言有专门的错误处理和异常捕获库或框架,可以使用这些库来简化工作。
  13. 进行单元测试和集成测试:通过单元测试和集成测试来确保代码的健壮性,并捕获任何潜在的异常或错误。
  14. 持续学习与实践:随着技术的不断进步,新的错误处理和异常捕获技术也会出现。持续学习和实践可以帮助你保持与时俱进。

30.WebAssembly,它有什么优势和限制

WebAssembly(Wasm)是一种可以在现代Web浏览器中运行的二进制指令集。它被设计为一种高效的、安全的、可移植的编译目标,能够使得多种编程语言(如C、C++、Rust等)编写的代码以接近原生性能的速度在Web上运行。WebAssembly的主要优势和限制如下:

优势:

  1. 高效性:WebAssembly是一种二进制格式,相比JavaScript,它的解析和执行速度更快,因此能够提供更高的性能。此外,WebAssembly还可以利用现代硬件的特性进行优化,进一步提高执行效率。
  2. 安全性:WebAssembly被设计为一种安全的编译目标,它在执行过程中受到浏览器的严格沙箱限制。这意味着WebAssembly代码无法直接访问或修改主机系统的资源,从而有效防止了恶意代码的攻击。
  3. 可移植性:WebAssembly是一种跨平台的编译目标,它可以在不同的操作系统和硬件架构上运行,而无需对源代码进行修改。这使得开发人员能够更容易地将应用程序部署到不同的平台上。
  4. 多语言支持:WebAssembly支持多种编程语言,这使得开发人员可以使用自己熟悉的编程语言来编写Web应用程序,而无需学习新的编程语言或框架。

限制:

  1. 浏览器兼容性:虽然现代Web浏览器都支持WebAssembly,但一些较旧的浏览器版本可能不支持。此外,不同浏览器之间的实现可能存在差异,这可能会导致跨浏览器兼容性问题。
  2. 可读性差:WebAssembly是一种二进制格式,相比文本格式的JavaScript,它的可读性更差。这使得开发人员在调试和排查问题时可能会面临更大的挑战。
  3. 代码大小:由于WebAssembly是一种二进制格式,相比JavaScript,它的代码体积可能更大。这可能会导致加载时间较长,特别是在网络条件较差的情况下。
  4. 生态系统限制:尽管WebAssembly得到了广泛的关注和支持,但它的生态系统仍然相对较小。这意味着可用的库、框架和工具可能相对较少,这可能会限制开发人员的选择和灵活性。

31.JavaScript中的模块化编程

在JavaScript中,模块化编程是一种将代码分解为独立、可重用的模块的方法。这些模块可以包含函数、变量、类等,并允许你以结构化和组织化的方式编写和管理代码。

模块化编程的好处包括:

  1. 代码重用:通过将代码分解为独立的模块,你可以在不同的项目或应用程序中重用这些模块,从而提高代码的利用率。
  2. 代码组织:模块化编程可以帮助你更好地组织代码,使其更易于阅读、理解和维护。
  3. 降低耦合:通过将代码分解为独立的模块,你可以降低代码之间的耦合度,使得各个模块更加独立,易于测试和修改。
  4. 提高可扩展性:通过模块化编程,你可以更容易地添加新的功能或修改现有功能,而不会影响到其他部分的代码。

在JavaScript中,有几种常见的模块化编程的方法:

  1. ES6模块:ES6(ECMAScript 2015)引入了内置的模块系统,允许你使用import和export关键字来导入和导出模块的特定部分。
  2. CommonJS模块:CommonJS是Node.js中使用的模块系统,使用require和module.exports关键字。虽然它在浏览器端不常用,但在Node.js环境中非常流行。
  3. AMD(Asynchronous Module Definition):AMD是一种异步加载模块的方法,它使用define和require关键字来定义和加载模块。AMD在浏览器端比较常见。
  4. UMD(Universal Module Definition):UMD是一种兼容AMD和CommonJS的模块定义方法,它允许你的模块在浏览器和Node.js环境中都能运行。
  5. 立即执行函数表达式(IIFE, Immediately Invoked Function Expression):IIFE是一种将函数封装为模块的方法,函数在定义后立即执行,从而创建局部作用域,减少变量冲突。

32.如何处理和调试JavaScript代码中的错误和异常

  1. 使用console.log():这是最基本的方法,可以在代码的关键部分添加console.log()语句,以查看特定变量的值或跟踪代码的执行流程。
  2. 使用浏览器的开发者工具:几乎所有的现代浏览器(如Chrome、Firefox、Edge等)都提供了强大的开发者工具,这些工具可以帮助你查看和调试代码。例如,你可以使用"Sources"或"Debugger"面板来设置断点,然后逐行执行代码,查看变量的值,并检查任何可能的问题。
  3. 使用try/catch块:你可以使用try/catch块来捕获和处理异常。如果try块中的代码抛出异常,控制将立即传递给catch块,你可以在catch块中处理异常。
  4. 使用Promise和async/await:对于异步代码,你可以使用Promise和async/await来处理错误。Promise有一个.catch()方法,可以捕获任何未处理的错误。你也可以在async函数中使用try/catch块。
  5. 使用Error对象:JavaScript的Error对象提供了一个有用的方法来抛出错误,并获取有关错误的更多信息。你可以使用throw new Error('message')来抛出一个错误,并使用Error对象的.stack属性来获取错误的堆栈跟踪。
  6. 使用防错和预防性编程:一种有效的处理错误的方法是进行防错和预防性编程。这意味着你应该尽可能预测和防止可能出现的问题,而不是等待问题出现后再处理。例如,你可以使用参数校验、边界检查和错误处理机制来防止错误。
  7. 代码审查:代码审查是一种非常有效的错误检测方法。你可以邀请其他开发人员或你自己在完成代码后进行代码审查,以查找可能的错误或改进的地方。
  8. 单元测试和集成测试:编写单元测试和集成测试可以帮助你确保代码的正确性和稳定性。这些测试可以检查代码的每个部分是否按预期工作,并帮助你发现任何问题。

33.JavaScript中的递归函数,以及如何避免栈溢出错误

递归函数是函数在自身的定义中调用了自身的一种函数。递归在处理某些问题时非常有用,比如遍历树形结构或处理分治问题。然而,如果不正确地使用递归,可能会导致栈溢出错误。

栈溢出错误通常发生在递归函数中,当递归深度过深,超过了JavaScript引擎的调用堆栈限制时,就会发生栈溢出错误。这种情况通常是因为没有正确的递归终止条件,或者递归深度过深。

为了避免栈溢出错误,可以采取以下几种策略:

  1. 设置合适的终止条件:确保递归函数有一个明确的终止条件。这个终止条件应该是可以快速满足的,以便在递归深度过大时能够及时停止。
  2. 使用循环代替递归:如果可能的话,尝试使用循环代替递归。循环通常比递归更高效,也更容易避免栈溢出错误。
  3. 限制递归深度:可以通过限制递归深度来避免栈溢出错误。例如,可以使用一个计数器来跟踪递归深度,当达到一定深度时,停止递归。
  4. 使用尾递归优化:在一些JavaScript引擎中,尾递归会被优化为循环,从而避免栈溢出错误。但是,不是所有的JavaScript引擎都支持尾递归优化,因此这并不是一个通用的解决方案。
  5. 使用Web Workers或WebAssembly:如果你的递归函数需要处理大量数据或进行复杂的计算,可以考虑使用Web Workers或WebAssembly来避免栈溢出错误。Web Workers和WebAssembly可以在后台线程中运行代码,不会受到调用堆栈的限制。

34.如何测试JavaScript代码,包括单元测试、集成测试

  1. 单元测试:单元测试是针对代码中的最小可测试单元进行测试的过程。在JavaScript中,单元测试通常使用Mocha、Jasmine等测试框架进行。这些框架提供了一套丰富的断言库,可以用来验证代码的行为是否符合预期。编写单元测试时,应该尽可能地隔离每个模块或函数,并编写针对每个功能或方法的测试用例。
  2. 集成测试:集成测试是测试代码中多个模块或组件之间的交互和集成。集成测试通常涉及到模拟外部系统或服务,以确保整个系统或应用程序在集成后能够正常工作。在JavaScript中,可以使用Mock.js等库来模拟外部系统或服务,并编写针对集成功能的测试用例。
  3. 端到端(E2E)测试:端到端测试是测试整个应用程序或系统的流程,包括前端和后端的交互。E2E测试通常用于验证应用程序是否符合业务需求和用户期望。在JavaScript中,可以使用Cypress、Puppeteer等工具进行端到端测试。这些工具可以模拟用户与应用程序的交互,并验证应用程序的响应和行为是否正确。
  4. 性能测试:性能测试是评估应用程序的性能指标,如响应时间、吞吐量、资源利用率等。在JavaScript中,可以使用Benchmark.js等库进行性能测试。这些库可以帮助你度量和比较不同代码实现之间的性能差异。
  5. 代码覆盖率:代码覆盖率是衡量测试用例覆盖代码的程度。使用代码覆盖率工具可以帮助你了解哪些代码行被执行过,哪些代码行没有执行过。在JavaScript中,可以使用Istanbul、JSCoverage等工具进行代码覆盖率分析。

35.如何编写单元测试

  1. 选择测试框架:选择一个适合的测试框架,如Mocha、Jasmine等。这些框架提供了丰富的断言库和测试工具,可以帮助编写和执行测试。
  2. 安装测试框架:使用npm或yarn等包管理器安装所选的测试框架。例如,要使用Mocha,可以运行npm install --save-dev mocha命令。
  3. 创建测试文件:创建一个单独的JavaScript文件来存放测试代码。通常,测试文件的命名与要测试的代码文件相同,但以.test.js作为后缀名。
  4. 编写测试用例:编写测试用例是单元测试的核心部分。每个测试用例应该测试代码中的一个特定功能或方法。使用测试框架提供的断言库来验证代码的行为是否符合预期。例如,使用Mocha和Chai的组合,可以使用assert.equal()函数来比较预期值和实际值。
  5. 设置和清理测试环境:在每个测试用例之前,可以设置测试环境,如初始化变量、模拟外部系统等。在每个测试用例之后,清理测试环境,如清除模拟对象或重置变量。
  6. 运行测试:使用测试框架提供的命令或脚本来运行测试。例如,在Mocha中,可以使用mocha命令来运行测试。
  7. 查看测试结果:运行测试后,查看测试结果以确定代码是否按预期工作。如果某个测试用例失败,则可能需要调试代码并重新运行测试。

36.null和undefined的判断方法和区别

1.undefined 的判断 

(1) undefined表示缺少值,即此处应该有值,但是还没有定义

(2) 变量被声明了还没有赋值,就为undefined

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

(4) 对象没有赋值的属性,该属性的值就等于undefined

(5) 函数没有返回值,默认返回undefined

2.null 的判断 

(1) null表示一个值被定义了,但是这个值是空值

(2) 作为函数的参数,表示函数的参数不是对象

(3) 作为对象原型链的终点 (Object.getPrototypeOf(Object.prototype))

(4) 定义一个值为null是合理的,但定义为undefined不合理(var name = null)

3.typeof 类型不同

typeof null; // 'object'

typeof undefined; // 'undefined'

4.Number() 转数字也不同

Number(null); // 0

Number(undefined); // NaN

37.列举和数组操作相关的方法

push; 从后面添加

pop; 从后面删除

shift; 从前面删除

unshift; 从前面添加

reverse; 反转

sort; 排序

indexOf; 返回指定元素的索引

join; 把数组转换成字符串

split; 把字符串转换成数组

concat; 连接数组

splice; 删除数组中的指定元素

38.const定义的对象属性是否可以改变

情况一:const定义的变量存在块级作用域,且不存在变量提升,一般用于定义常量,定义的时候必须初始化。

答:不可以,const定义的如果是基本数据类型(string,number,boolean,null,undifined,symbol),定义后就不可再修改,如果修改,会报错。

情况二:那么如果是const定义的对象呢?是否可以修改对象中的属性?

答案:可以

原因:对象是引用类型的,const定义的对象t中保存的是指向对象t的指针,这里的“不变”指的是指向对象的指针不变,而修改对象中的属性并不会让指向对象的指针发生变化,所以用const定义对象,对象的属性是可以改变的。

39.字符串的常用方法

  • 连接或拼接字符串:
    • str.concat(other_str):将两个或多个字符串连接在一起。
    • + 运算符:也可以用来拼接字符串。
    • f-string 或 format() 方法:用于格式化字符串。
  • 截取或切片字符串:
    • str.substring(start, end):返回从start到end(不包括end)的子字符串。
    • str.slice(start, end):与substring类似,但end参数是可选的,并且可以是负数,表示从字符串末尾开始计数。
    • str[start:end]:使用切片语法来截取子字符串。
  • 查找子字符串:
    • str.index(sub_str):返回子字符串第一次出现的索引,如果没有找到则抛出异常。
    • str.find(sub_str):返回子字符串第一次出现的索引,如果没有找到则返回-1。
    • str.in():检查子字符串是否存在于字符串中,返回布尔值。
  • 替换字符串:
    • str.replace(old_str, new_str):替换字符串中所有old_str为new_str。
    • str.replacefirst(old_str, new_str):只替换第一次出现的old_str。
  • 检查字符串:
    • str.isalpha():检查字符串是否只包含字母。
    • str.isdigit():检查字符串是否只包含数字。
    • str.isalnum():检查字符串是否只包含字母和数字。
    • str.startswith(prefix):检查字符串是否以指定前缀开始。
    • str.endswith(suffix):检查字符串是否以指定后缀结束。
  • 转换字符串:
    • str.lower():将字符串转换为小写。
    • str.upper():将字符串转换为大写。
    • str.capitalize():将字符串的第一个字符转换为大写。
    • str.title():将字符串中每个单词的首字母转换为大写。
    • str.strip():去除字符串两端的空白字符。
    • str.lstrip() 和 str.rstrip():分别去除字符串左端和右端的空白字符。

40.大文件上传如何做断点续传

大文件上传的断点续传主要涉及到两个概念:分片上传及断点续传。

  • 分片上传:就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(Part)来进行分片上传。上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。具体流程如下:
  • 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块。
  • 初始化一个分片上传任务,返回本次分片上传唯一标识。
  • 按照一定的策略(串行或并行)发送各个分片数据块。
  • 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。
  • 断点续传:当文件上传过程中发生中断时,可以从已经上传的部分开始继续上传未完成的部分,而不是从头开始上传。实现方式主要有两种:
  • 服务器端返回,告知从哪里开始。浏览器端自行处理。
  • 上传过程中将文件在服务器写为临时文件,等全部写完了(文件上传完),将此临时文件重命名为正式文件即可。如果中途上传中断过,下次上传的时候根据当前临时文件大小,作为在客户端读取文件的偏移量,从此位置继续读取文件数据块,上传到服务器从此偏移量继续写入文件即可。

使用断点续传,用户可以节省时间,提高上传速度。特别是在文件大小超过预期、网络环境较差或者出现上传失败的情况下,断点续传可以大大提高用户体验。

本文是集成网络上大部分的面试题, 并刨除了一些基本问题, 如果博主找到了更多的有关CSS的面试题也会进行更新; 因为有一部分是在网上查到的, 所以可能会有一部分和其他博主相同, 由于来源太杂无法一一标明出处, 如有介意麻烦联系博主增加来源标注; 

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
2024年前端面试题可能会涉及以下几个方面的内容: 1. HTML/CSS基础知识:包括HTML标签的使用、CSS选择器、盒模型、浮动、定位等基本概念和常见问题。 2. JavaScript基础知识:包括数据类型、变量、运算符、流程控制语句、函数、作用域、闭包等基本概念和常见问题。 3. 前端框架和库:例如React、Vue等,可能会涉及到它们的基本原理、生命周期、组件通信等方面的问题。 4. 前端性能优化:包括减少HTTP请求、压缩和合并文件、使用CDN加速、懒加载、缓存等方面的知识。 5. 前端工程化:包括模块化开发、构建工具(如Webpack)、版本控制(如Git)、自动化测试等方面的知识。 6. 前端安全:包括XSS攻击、CSRF攻击、点击劫持等常见安全问题及其防范措施。 7. 前端跨域问题:包括同源策略、跨域请求的方法(如JSONP、CORS等)以及解决跨域问题的方案。 8. 移动端开发:包括响应式设计、移动端适配、触摸事件、移动端性能优化等方面的知识。 9. Web标准和浏览器兼容性:包括HTML5、CSS3的新特性以及不同浏览器之间的差异和兼容性问题。 10. 数据可视化:包括使用图表库(如Echarts、D3.js)进行数据可视化的基本原理和常见问题。 以上只是一些可能涉及到的内容,具体的面试题目还会根据面试官的要求和公司的需求而有所不同。在准备面试时,建议多做一些实际项目练习,加深对前端知识的理解和应用能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值