1.js基本数据类型有哪些及它们的区别
JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
- Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
- BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
2.数据类型检测的方式有哪些
typeof:其中数组、对象、null都会被判断为object,其他判断都正确。instanceof:只能正确判断引用数据类型,而不能判断基本数据类型。constructor:constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了。null和undefined是原始数据,没有构造函数,也没法判断。Object.prototype.toString.call()
typeof null 为什么是object?
这是一个历史遗留问题。JavaScript最初使用32位存储值,前3位是类型标签。null的机器码是全零,类型标签000对应对象类型,导致typeof误判。出于对Web兼容性的极端重视,这个行为一直被保留。我们使用 value === null 来正确检测。
3.判断数组的方式有哪些
-
通过ES6的Array.isArray()做判断
-
结合问题2
既然 instanceof 可以判断类型,为什么判断数组建议用 Array.isArray() 而不是 instanceof Array?instanceof 判断原理
instanceof 的原理是通过检查构造函数的 prototype 是否在对象的原型链上。
具体来说,当执行 obj instanceof Constructor 时:
- JavaScript 引擎会获取 obj 的原型对象
- 然后与 Constructor.prototype 比较
- 如果不相等,就继续获取原型对象的原型,直到找到匹配或到达 null
instanceof 有什么缺陷?
跨框架问题:不同 iframe 中的相同构造函数不相等原型可修改:修改 Constructor.prototype 会影响所有已有实例的判断性能问题:需要遍历原型链,深度嵌套时可能较慢不能检测基本类型:如 'hello' instanceof String 返回 false
能实现一个 instanceof 吗?
// 手写实现,注意边界情况 function myInstanceof(left, right) { // 基本类型直接返回 false if (typeof left !== 'object' || left === null) return false; let proto = Object.getPrototypeOf(left); while (true) { if (proto === null) return false; if (proto === right.prototype) return true; proto = Object.getPrototypeOf(proto); } }
4.请简述JavaScript中的this
- 以
函数形式调用时,非严格模式this永远都是window,严格模式是Undefined - 以
方法的形式调用,this是调用方法的对象 - 以
构造函数的形式调用时,this是新创建的那个对象 - 使用
call和apply调用时,this是指定的那个对象 箭头函数:箭头函数的this看外层是否有函数,如果有,外层函数的this就是内部箭头函数的this。如果没有,就是window- 特殊情况:通常意义上this指针指向为最后调用它的对象。这里需要注意的一点是如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。
5.AMD和CommonJS的区别
6.ES6模块与CommonJS模块有什么异同?
ES6 Module和CommonJS模块的区别:
- CommonJS输出的是一个值的
浅拷⻉,ES6输出的是值的引用;即ES6 Module只存只读,不能改变其值,也就是指针指向不能变,类似const; - CommonJS是
运行时加载,ES6是编译时输出接口; - CommonJS的require是同步加载模块,ES6的import是异步加载,有独立模块依赖的解析阶段。
ES6 Module和CommonJS模块的共同点:
- CommonJS和ES6 Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变。
7.let、const、var的区别
let、const 和 var 都用于声明变量,但它们有一些显著的区别。
var
- 作用域:
var声明的变量是函数作用域(或全局作用域)。即使它在代码块(如if或for循环)中声明,作用域也会扩展到整个函数或全局。 - 变量提升(Hoisting): 使用
var声明的变量会被提升到函数或全局作用域的顶部。变量会先被声明,初始化值为undefined,然后再执行赋值操作。 - 可以重新赋值和重新声明:
var声明的变量可以在同一作用域内重新声明和赋值。
问题:
- 变量提升: 由于
var变量会被提升,可能会导致意外的行为。 - 作用域问题: 它的作用域是函数级别的,而不是块级别的,这可能会引发一些问题,尤其是在循环中。
let
- 作用域:
let声明的变量是块级作用域,即它仅在代码块、语句或表达式内部有效。 - 变量提升:
let也会被提升,但在声明之前无法访问该变量,若访问会导致ReferenceError错误(这是因为let变量处于“暂时性死区”)。 - 可以重新赋值,但不能重新声明: 在相同作用域内,
let声明的变量不能被重新声明。
const
- 作用域:
const和let一样,具有块级作用域。 - 变量提升:
const也会被提升,但它同样处于“暂时性死区”。 - 不可重新赋值:
const声明的变量一旦赋值后,不能再重新赋值。它用于声明常量。 - 声明常量: 对象和数组等引用类型的变量可以被
const声明,但它们的内容(属性或元素)是可变的。
8.new操作符的实现原理
new操作符的执行过程
- 首先创建了一个新的空对象
- 将空对象的原型指向构造函数的原型对象。
- 将空对象的上下文(this)与构造函数绑定
具体实现:
function _new(fn, ...arg) [
var obj = Object.create(fn.prototype):
const result = fn.apply(obj, ...arg);
return Object.prototype.toString.call(result) == '[object Object]' ? result : obj;
}
9.数组的遍历方法有哪些?
10.哪些对象有...(扩展扩展运算符),为什么可以使用...
- 可迭代对象: 数组、类数组对象,字符串、Set、Map 以及 Generator 对象。
- 原因:扩展运算符默认调用的是
iterator接口,任何部署了iterator接口的数据结构都可以使用
11.for in和for of的区别
for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下
for…of遍历获取的是对象的键值,for…in 获取的是对象的键名;for… in会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;- 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;
总结: for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。
12.window.onload 和 DOMContentLoaded 的区别
window.onload 事件会等到页面 所有资源 (包括图像、样式表、脚本等)加载完毕后再执行。适用于在页面完全加载后执行的场景。
DOMContentLoaded 事件会等到 HTML 文档 被完全解析并且 DOM 完成构建后立即执行,忽略外部资源的加载。适用于需要尽早操作 DOM 的场景,能提高页面的响应速度。
13.script 标签中 defer 和 async 的区别?
无: HTML 暂停解析,下载 JS,执行 JS,再继续解析 HTMLdefer: HTML 继续解析,并行下载 JS,HTML 解析完再执行JSasync: HTML 继续解析,并行下载 JS,执行 JS,再解析 HTML
一般情况下,当执行到 script 标签时会进行下载 + 执行两步操作,这两步会阻塞 HTML 的解析;
async 和 defer 能将script的下载阶段变成异步执行(和 html解析同步进行);
async下载完成后会立即执行js,此时会阻塞HTML解析;
defer会等全部HTML解析完成且在DOMContentLoaded 事件之前执行。
14.Map和Object区别
Map 和 Object 都是 JavaScript 中用于存储键值对的数据结构,但它们有显著的区别,适合不同的使用场景。以下是两者的主要区别和特点:
- 键的类型
Map:键可以是任意类型,包括对象、数组、函数、基本类型(字符串、数字等)。
Object:键只能是字符串或符号(Symbol)。
- 键值对的有序性
Map:保留键值对的插入顺序。
Object:数字键会优先排序,而其他键按插入顺序排列。
- 原型链
Map:没有原型链问题,Map 上的键值对完全独立。
Object:默认会继承 Object.prototype,可能会产生原型链上的方法冲突。
- 默认属性
Map:没有默认属性,存储的所有键值对是纯粹的用户数据。
Object:有默认继承的属性(如 toString、hasOwnProperty 等)。
- 迭代
Map:可以直接用迭代器方法,如 map.keys()、map.values()、map.entries(),也支持 for...of 循环。
Object:需要使用 Object.keys()、Object.values() 或 Object.entries() 提取键、值或键值对,才能进行迭代。
15.深拷贝浅拷贝
浅拷贝
浅拷贝是创建一个新的对象,这个对象的属性是原始对象属性的引用。如果原始对象的属性是基本类型,拷贝的是其值;如果属性是引用类型(如数组、对象),拷贝的是引用地址。
实现方式
- 使用
Object.assign - 使用扩展运算符(
...)
深拷贝
深拷贝是创建一个完全独立的新对象,新对象中的属性和原对象无任何关联,即使属性是引用类型,也会被递归地复制一份。
实现方式
- JSON 方法(简单场景适用)
- 递归拷贝
- 使用库(如
lodash的cloneDeep)
16.undefined 和 null 区别?
undefined代表 变量未赋值 或 未定义的属性。null代表 “空”值,需要开发者手动赋值。undefined == null为true,但undefined === null为false。- JSON 序列化时
undefined会被忽略,null会保留。 null转换为0,undefined参与运算时会变成NaN。
17.for循环和forEach有什么区别?
for 特点
- 传统的循环结构,可用于遍历任何可迭代对象(如数组、字符串等)。
- 可以使用
break和continue控制循环流程。 - 支持
async/await,可以在循环体内使用await进行异步操作。
forEach 特点
- 只能用于数组(不能遍历对象或字符串)。
- 不能使用
break或continue,无法在中途终止或跳过某次循环。 - 不会返回新数组,但可以修改原数组的元素。
- 回调函数有三个参数:当前元素值、索引、整个数组。
性能
- for 循环:通常性能更高,尤其是在大数据量时。
- forEach:性能稍低,因为每次迭代都要调用回调函数。
20.事件委托
事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。
21.原型和原型链
-
Object.prototype.proto 的指向是什么?
null -
Function.proto === Function.prototype 为什么成立?
所有的函数,包括内置构造函数,都是
Function的实例。因此Function作为一个函数,也应该是它自己的实例,这就形成了Function.__proto__ === Function.prototype。
22.作用域(链)
什么是作用域?
作用域(Scope)是程序中变量的可访问范围,它决定了代码中哪些变量可以被访问和使用。
在 JavaScript 中,作用域分为以下几种类型:
- 全局作用域:在任何地方都可以访问的变量。
- 函数作用域:函数内部定义的变量,只能在该函数内部访问。
- 块级作用域:用
let或const声明的变量,其作用域仅限于包裹它的{}大括号内。
什么是作用域链?
作用域链(Scope Chain)是指当访问一个变量时,JavaScript 会沿着作用域逐层查找变量的过程。
- 作用域链的本质是一个由多个作用域对象组成的链条,链接着当前作用域和它的父级作用域,一直追溯到全局作用域。
- 如果在当前作用域找不到变量,会沿着作用域链向上查找,直到找到该变量或者到达全局作用域为止。
- 如果整个作用域链都没有找到该变量,则会报
ReferenceError。
23.闭包的理解
闭包:函数内部嵌套了一个新的函数,嵌套的函数对外部的函数造成了引用就是形成了闭包
闭包常见的两种情况:
一是函数作为返回值; 另一个是函数作为参数传递
闭包的作用:
可以让局部变量的值始终保持在内存中;对内部变量进行保护,使外部访问不到
最常见的案例:函数节流和防抖
防抖:在事件被触发后,等待一定的时间再执行事件处理函数,如果在等待时间内事件再次被触发,则重新计时。
适用场景
- 输入框输入搜索关键字,用户停止输入后再发起搜索请求。
- 调整浏览器窗口大小时,只在调整完成后执行一次操作。
- 表单验证,只在用户停止输入后进行验证。
节流:在规定的时间间隔内,只允许事件触发一次,无论期间事件被触发多少次都不响应,只有到达时间间隔后才能触发下一次事件
适用场景
- 页面滚动时,定期计算或渲染新的内容。
- DOM元素拖拽时定期更新位置。
- 防止短时间内连续点击按钮导致重复提交。
24.对执行上下文的理解
- 执行上下文 是 JavaScript 代码执行的环境,分为全局上下文、函数上下文和
eval上下文。 - 每个执行上下文包含 变量对象、作用域链 和
this值。 - 执行上下文栈用于管理上下文的创建和销毁。
- 理解执行上下文有助于掌握变量提升、作用域、闭包等核心概念。
25.实现call、apply 及 bind 函数
在 JavaScript 中,call、apply 和 bind 是 Function 对象的三个方法,它们允许你修改函数内部 this 的指向。
区别
- 三者第一个参数都是this要指向的对象,若该参数为undefined或null,this则默认指向全局window
call和apply的主要区别:都用于立即调用函数并改变this的指向,call是逐个传参,而apply是传入一个数组。bind的用途:bind方法不会立即执行函数,而是返回一个新的函数,你可以延迟执行,并且在返回的函数中this被固定。bind适用于需要预设参数并延迟执行的场景。
26.异步编程的实现方式?
JavaScript中的异步机制可以分为以下几种:
回调函数(Callbacks):回调函数是 JavaScript 中最基础的异步编程方式。通过将一个函数作为参数传递给异步操作,当操作完成时调用该函数。Promise:ES6 引入的一种异步编程解决方案,用于处理异步操作的结果。它提供了链式调用和更好的错误处理机制。async/await: 基于 Promise 的语法糖,使异步代码看起来像同步代码,提升了代码的可读性和可维护性。事件监听(Event Listeners):通过监听特定事件来处理异步操作的结果,常用于 DOM 操作或 Node.js 中的事件驱动编程。发布/订阅模式(Pub/Sub):发布/订阅模式是一种解耦的方式,通过发布消息和订阅消息来处理异步操作。Generator 函数:S6 引入的一种特殊函数,可以通过yield暂停函数执行,并通过next()恢复执行。它可以用于实现异步编程。
27.setTimeout、Promise、Async/Await 的区别
28.对async/await 的理解
作用:用同步方式,执行异步操作
总结:
async函数是generator(生成器函数)的语法糖- async函数返回的是一个Promise对象,有无值看有无return值
await关键字只能放在async函数内部,await关键字的作用 就是获取Promise中返回的resolve或者reject的值。async、await要结合try/catch使用,防止意外的错误
- 它是 Promise 的语法糖,让异步代码更易读。
- 但它本质还是异步,
await会把后续逻辑注册为.then回调,进入微任务队列。 - 使用时要注意并行优化、错误捕获,避免在数组方法中误用。
generator:
generator函数跟普通函数在写法上的区别就是,多了一个星号*- 只有在generator函数中才能使用
yield,相当于generator函数执行的中途暂停点 - generator函数是不会自动执行的,每一次调用它的
next方法,会停留在下一个yield的位置
29.浏览器的垃圾回收机制
30.哪些情况会导致内存泄漏
31.ES6有哪些新特性
回答思路可以是:
-
ES6 让JS 成为像JAVA一样大型企业级开发语言
- 块级作用域 let const
- class ES6 引入了基于原型的面向对象编程模型的类,提供了更接近传统面向对象语言的语法糖,使得定义和继承类变得更加直观。
- 模块(module) 通过
import和export关键字支持原生模块系统,这改善了代码组织结构,促进了代码重用,并有助于解决命名冲突问题。 vite 基于原生的模块化,更快. - Map和Set: 新的数据结构提供了更高效和灵活的方式来处理键值对集合和不重复值的集合,补充了原有的数组和对象。WeakMap,WeakSet 弱使用,更好地规避内存泄露.
- for...of循环: 提供了一种新的迭代数组、Set、Map、类数组对象或任何可迭代对象的方式,语法更简洁,使用更广泛。
-
ES6 让代码更简洁,优雅,提高可读性
- 箭头函数: 箭头函数提供了更简洁的函数表达式书写方式,自动绑定
this上下文,简化了函数内部作用域的处理,对于异步编程和回调函数特别有用。 - 模板字符串: 使用反引号(``)定义的字符串可以包含嵌入的表达式,通过
${expression}插入变量或表达式的值,使得字符串拼接更自然,更易于阅读。 - 解构赋值: 允许快速从数组或对象中提取值到变量中,这简化了数据处理和交换过程,使得代码更加简洁。
- 默认参数、展开运算符和rest参数: 提供了更灵活的函数参数处理方式,使得函数定义更加清晰,同时也方便了数组和对象的操作。
- 箭头函数: 箭头函数提供了更简洁的函数表达式书写方式,自动绑定
-
解决回调地调,更好地处理异步
- promise 和 generator/yield
- Promise和async/await: 这些异步编程特性使得处理异步操作变得更加流畅和同步化,提高了代码的可读性和可维护性,尤其是在处理复杂的异步逻辑时。
-
proxy
相比于
defineProperty, 拥有更多拦截器,性能更好.**箭头函数和普通函数的区别**箭头函数和普通函数有五个主要区别:
this指向:箭头函数继承外层,普通函数动态决定- 构造函数:箭头函数不能
new,普通函数可以 arguments:箭头函数没有,用rest参数代替prototype:箭头函数没有,普通函数有- 语法:箭头函数更简洁,特别适合回调"
32.你能举出一个柯里化函数(curry function)的例子吗?它有哪些好处?
柯里化是指将一个多参数的函数转化为多个单参数的函数,并且每个函数都返回一个新的函数,直到接收所有的参数为止。
33.什么是事件循环?调用堆栈和任务队列之间有什么区别?
Js事件循环
js的任务队列分为同步任务和异步任务,所有的同步任务都是在主线程里执行的。异步任务可能会在宏任务或者微任务里面,异步任务进入event table并注册函数。当指定的事情完成时,event table会将这个函数移入event queue。主线程内的任务执行完毕为空,会去event queue读取对应的函数,进入主线程执行。上述过程会不断重复,也就是常说的event loop
宏任务:
每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。浏览器为了能够使得js内部宏任务与dom任务能够有序执行,会在一个宏任务执行结束后,在下一个宏任务执行开始前,对页面进行重新渲染。宏任务包括:script(整体代码),setTimeout/setInterval,I/O
微任务:
可以理解是在当前task执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。所以它的响应速度比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在一个宏任务执行完后,就会将在它执行期间产生的所有微任务都执行完毕(在渲染前)。微任务有:promise,async/await
总结:
- 先执行主线程
- 遇见宏任务放到宏任务队列
- 遇见微任务放到微任务队列
- 主线程执行完毕
- 执行微队列,微队列执行完毕
- 执行一次宏任务队列中的一个任务,执行完毕
- 执行宏任务中产生的微队列,执行完毕
- 依次循环。。。
调用堆栈和任务队列之间有什么区别
调用堆栈(Call Stack):负责存储当前执行的函数,它是一个后进先出(LIFO)的栈结构,存储了当前执行的函数以及函数调用的上下文。任务队列(Task Queue):存储异步回调任务,是一个先进先出(FIFO)的队列,用于管理异步任务,等待调用堆栈清空后执行。事件循环(Event Loop):不断检查调用堆栈和任务队列,确保异步任务可以在调用堆栈为空时执行。