js数据类型
简单数据类型
null undefined string number
boolean
bigint 任意精度的大整数
symbol 创建唯一且不变的值,常用来表示对象属性的唯一标识
复杂数据类型
object,数组,函数,正则,日期等
区别
-
存储区别
简单数据类型因为其大小固定且经常使用,存储在栈中
复杂数据类型因为占据空间太大,大小不固定,存储在堆中,栈中存放指向他的指针
栈 内存分配效率高,自动管理
堆 内存分配灵活,需要手动管理 -
赋值方式的区别
复制的是值本身,两个变量互不影响
复制的是引用,一个变量修改影响其他变量
数据类型检测的方式
- typeof
检查原始类型 返回类型字符串
null 比较特殊检查返回object
检查引用类型时,返回object,除了function返回’function’ - instance of
(检查当前类型是否在当前实例的原型链上)
检查引用类型 返回布尔值 - object prototype tostring call
适用于所有类型的判断检测, 返回的是该数据类型的字符串。
判断数组
- instance of
- Array.isArray
- Object.prototype.toString.call()
== 与 === 的区别
1)当两边类型不一致,类型转换后比较相等
2)不进行类型转化,不止比较值相等,还比较类型是否相等
null 与 undefinded的区别
- 类型检查区别
type of检查null 返回object - 比较操作区别
== 比较两个会认为相等,都表示没有值
=== 比较不相等,因为是不同类型 - 变量赋值区别
undefined是js引擎自动赋予未赋值变量,当定义未初始化是变量的值为undefined
null是开发者手动显示赋值表示变量没有值
变量提升
what:
变量提升是指JS的变量和函数声明会在代码编译期
,提升到用域顶部
如此可以在实际声明代码位置之前使用变量
how
- 变量提升成立的前提:
使用Var
关键字进行声明的变量,声明被提升
,赋值不会被提升 - 函数的声明提升:
使用function声明,会比变量的提升优先。
函数表达式提升,不可在声明之前调用
//函数声明提升
console.log(myFunction()); // 输出: "Hello, World!"
function myFunction() {return "Hello, World!";
}//函数表达式console.log(myFunction2()); // 报错: myFunction2 is not functionvar myFunction2 = function() {return "Hello, World2!";}//变量提升console.log(myVariable); // 输出: undefinedvar myVariable = 10;
why
- 代码可读性与可维护性下降
- 潜在bug,使用未初始化的变量可能导致不可预测的问题
解决
ES6使用let和const声明的变量是不会创建提升,
在初始化之前访问let和const创建的变量会报错。
作用域与作用域链
变量的作用范围
-
全局作用域
最外层作用域,
-
函数作用域
在函数内部声明的变量只在函数内部可见
-
块级作用域
使用let/const 声明的变量,作用域只在定义的代码块内
作用域链是当变量在当前作用域无法找到的时候,js会一层一层向外寻找,直到找到变量或到达全局作用域,这种层级关系就是作用域链
词法作用域
作用域在定义的时候确定
不在运行时确定
js采取的是静态作用域,函数的作用域在函数定义的时候就确定了,在取自由变量的值的时候,要到创建当前函数的作用域去取,而不是调用函数时的作用域
立即执行函数表达式
立即调用匿名函数来创建一个新的作用域,避免污染全局作用域
用来封装独立的代码模块
说一说你对执行上下文的理解
代码在执行时所处的上下文环境
包括了代码在运行时能够访问的变量对象,作用域链,和this值
-
全局上下文
默认执行上下文,当js代码开始执行,全局上下文被创建,this指向全局对象
-
函数执行上下文
每调用一个函数都会创建一个函数上下文,可以被嵌套
-
Eval执行上下文
上下文内容
-
变量对象
包含函数所有的形参,内部变量,函数声明对于函数上下文,它被称为活动对象
-
作用域链
包含当前上下文的变量对象+所有父级上下文的变量对象
-
this值
全局上下文,指向全局对象函数上下文,this的值取决于函数调用方式
上下文生命周期
-
创建阶段
-
创建变量对象:包括函数参数,函数声明,变量声明(不会立刻赋值)
-
创建作用域:形成作用域,与当前上下文相连
-
确定this:根据调用位置,确定上下文的this
-
-
执行阶段
-
变量分配:变量赋值,函数引用开始执行
-
执行代码:根据代码逻辑逐行执行
-
简单说js的闭包
一个函数,引用了另一个函数作用域中变量
也就是说能够从外部访问函数内部的变量
用于隐藏与封装
问题
垃圾回收器不会将闭包中变量销毁,于是就造成了内存泄露,内存泄露积累多了就容易导致内存溢出。
解释一下什么是原型与原型链
1)每个js对象都有一个内部属性_proto_,
指向一个对象—-原型对象,可以从原型对象上继承属性与方法
2)原型对象也是对象也有_proto_,指向自己的原型对象,
这样相互关联的原型对象形成的链式结构构成原型链
构造函数
构造函数有一个prototype属性指向原型对象
原型对象有constructor属性 指向构造函数
遍历对象属性的方法有哪些
1)获取对象自身属性(不包含原型链)
返回值是属性数组
-
Object.keys() //返回对象自身可枚举属性
-
Object.getOwnPropertyNames() //返回对象自身所有属性(不包含Symbol属性)
- Object.getOwnPropertySymbols()//获取对象自身Symbol属性
- Reflect.ownKeys()//获取对象自身所有属性,包括字符串与Symbol属性
2)获取对象自身属性以及原型链继承的属性
- for……in
- Object.hasOwnProperty() 判断是否是对象自身属性
new过程发生了什么
-
创建一个空对象
{}
-
将这个新对象的
_proto_
属性指向构造函数的原型对象 -
将构造函数的
this
绑定到新对象,并执行构造函数中的代码 -
如果一个构造函数返回了一个对象,那么new 表达式最后返回这个对象,否则返回新创建的对象
call/apply/bind有什么区别
改变函数运行时的this指向
-
执行时机不同
call/apply 立即执行
bind 返回新函数,手动调用新函数执行
-
传参方式不同
call 第一个参数 this的值,参数2,参数3,………
apply 第一个参数 this的值,[参数1,参数2,……]
bind 第一个参数 this的值,参数作为新函数的参数
-
修改this性质
call/apply 临时修改this指向
bind 永久修改this指向
this绑定的规则
1)普通函数
this 指向函数的调用者,是动态的
2)箭头函数
所处上下文的this,保持不变
箭头函数与普通函数的区别
箭头函数
没有自己的this
不适用事件绑定,定义对象上的方法
//this指向问题var dog = { lives: 20, jumps: () => { this.lives--; } }dog.jumps();console.log(dog.lives); // 20// this指向的是window对象,无法切换var button = document.querySelector('button');button.addEventListener('click', () => { this.classList.toggle('on'); });
继承的实现方式
- 原型链继承
所有实例共享父类的属性。修改子类可能影响所有的实例 - 构造函数继承
子类构造函数中调用父类构造函数,子类便可继承父类的属性
不能继承父类的原型方法 - 组合继承
调用父类构造函数继承属性
通过原型链继承方法
调用两次父类构造函数开销较大 - 寄生组合继承
在组合继承的基础上
通过原型链继承方法时使用
Object.create(Coder.prototype) - class语法
extends/super关键字
深拷贝与浅拷贝实现方式
浅拷贝
- 拷贝的是普通类型,拷贝值
- 拷贝的是对象类型,拷贝地址(修改其中一个,会影响另外一个,两个对象指向同一份地址空间)
深拷贝
- 拷贝的是普通类型,拷贝值
- 拷贝的是对象类型,新建地址,复制原始地址中的值,放入新的地址中(修改不会影响原始对象)
什么是暂时性死区
代码块中使用 let
或 const
声明变量,
如果尝试在声明之前使用它们,就会抛出 ReferenceError
错误。就形成了暂时性死区
好处
有利于防止在变量尚未初始化之前访问,避免潜在错误
什么是同步与异步,js异步解决方案
- 同步:
指令按顺序指向,上一条指令未完成,下一条指令只能一直等待 - 异步:
类似系统中断,在上一条指令执行期间,可以执行下一条指令 - js异步方案:
回调函数/promise/async,await
对事件循环的理解
事件循环是js的一个执行机制,用于实现异步、非阻塞编程的操作。
在一个事件循环中,程序会不断地检查事件队列,如果有新事件到达,就会触发相应处理程序的回调函数来执行。允许程序在等待 I/O 操作完成的同时继续执行其他任务,而不会阻塞整个进程。
举个生活中的例子,老师要收整个班级的作业,可以找一位课代表,课代表 每小时 循环检查所有同学交的作业情况,如果发现有新的提交,就通知老师批改。这样老师就不用一直等着学生完成作业(I / O 操作),不去做其他的事情。
宏任务与微任务
在任务队列中,分为宏任务队列(Task Queue)和微任务队列(Microtask Queue),对应的里面存放的就是宏任务和微任务。
异步任务优先级
如果异步任务的优先级不区分,按照队列先进先出的机制,如果前一个任务耗时长,会影响后续任务
耗时长的异步任务放入宏任务队列
耗时短的异步任务放入微任务队列
- 宏任务:DOM 渲染后触发,如
setTimeout
、setInterval
、DOM 事件
、script
。 - 微任务:DOM 渲染前触发,如
Promise.then
、MutationObserver
、Node 环境下的process.nextTick
。
Promise,以及常见方法
Promise 是 JavaScript 中用于处理异步操作的一种对象。它代表了一个尚未完成但承诺会在未来某个时候完成的操作,并允许你在操作完成前注册回调函数。
- 三个状态 pending fuilied rejected
- 原型上的方法 then catch finnaly
- Promise类上的静态方法
Promise.all/allSettled/race/any/resovle/reject
async 与await关键字作用
async/await 是基于Promises的语法糖
async函数返回一个Promise,
await表达式会暂停async函数的执行并等待Promise解决,然后恢复async函数的执行
这样一来就可以控制异步任务的执行顺序,
当后一个任务需要前一个任务的结果时就非常方便
proxy 与 Object.defineProperty
js中用于实现数据拦截和响应式处理
- 灵活性:
Proxy
更加灵活,因为它可以拦截对象的几乎所有操作,而Object.defineProperty
只能控制属性的特性。 - 兼容性:
Object.defineProperty
在 ES5 中就已经存在,因此在兼容性方面可能更胜一筹。 - 性能:在某些情况下,
Proxy
的性能可能不如Object.defineProperty
,因为Proxy
需要在每次操作时都进行拦截和处理。 - 适用场景:
Proxy
更适合需要动态改变对象行为或进行复杂操作拦截的场景,而Object.defineProperty
则更适合对属性进行精确控制的场景。
map与普通对象,WeakMap的区别
Map 和 WeakMap 都是键值对的集合
- 键的类型:
- Map的键可以是任何数据类型。
- WeakMap的键只能是对象(原始数据类型如字符串、数字不能作为WeakMap的键)。
- 引用类型:
- Map对键是强引用。这意味着只要Map存在,其中的键就不会被垃圾回收器回收(除非显式地删除它们)。
- WeakMap对键是弱引用。如果键不再被其他对象引用(即它是不可达的),那么垃圾回收器可以随时回收它,同时WeakMap中对应的键值对也会自动消失。这个特性使得WeakMap非常适合用于缓存等场景,因为它不会阻止垃圾回收器回收不再使用的对象。
- 遍历性:
- Map是可遍历的。它提供了
keys()
、values()
、entries()
和forEach()
等方法来遍历Map中的键值对。 - WeakMap是不可遍历的。WeakMap 没有内置的迭代器,因此不能直接遍历键值对。
- Map是可遍历的。它提供了
Map与普通对象的区别
- 键的类型:
- Map对象的键可以是任何数据类型,包括原始数据类型(如字符串、数字)和对象。
- 普通对象的键只能是字符串或Symbol类型。如果你尝试使用其他类型的值作为键,它们会被自动转换为字符串(例如,数字会被转换为字符串形式的数字)。
- 键的顺序:
- Map对象保持键值对的插入顺序。
- 普通对象不保证键值对的顺序。在不同的JavaScript引擎中,对象的属性顺序可能会有所不同,甚至在同一引擎的不同版本中也可能有所不同。
- 方法:
- Map对象提供了一些特殊的方法,如
size
属性(返回Map中键值对的数量)、forEach()
方法(遍历Map中的每个键值对)等。 - 普通对象使用点语法或方括号语法来访问和操作键值对,没有提供像
size
这样的内置属性来获取键值对的数量(需要手动计算)。
- Map对象提供了一些特殊的方法,如
- 性能:
- 在大多数情况下,Map和对象的性能是相似的。然而,在频繁添加和删除键值对的情况下,Map可能会表现出更好的性能,因为它的内部实现是基于哈希表的。
for,foreach,map的区别/for in,for of
forEach
是数组的一个方法,用于遍历数组中的每个元素,并对每个元素执行一个提供的函数。
map
也是数组的一个方法,它创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
for...in
循环用于遍历对象的可枚举属性(包括原型链上的属性)。
for...of
循环用于遍历可迭代对象(如数组、字符串、Set、Map等)的值。
array.forEach((item, index) => { console.log(item); });
const newArray = array.map(item => item * 2); console.log(newArray);
const obj = {a: 1, b: 2}; for (let key in obj) { console.log(key, obj[key]); }
const array = [1, 2, 3]; for (let value of array) { console.log(value); }
对ComonnJS与ES6模块化理解
1)相同点:
两者都是 JavaScript 的模块化规范,都可以用来导入导出模块。
2)不同点:
ES Module (ESM) 是 ES6 引入的标准化模块系统,支持动态导入,在编译时加载,可以异步加载模块。
export import
CommonJS (CJS) 出现在 ES6 之前,不支持动态导入,在运行时加载,采取的是同步加载模块的方式。
module.exports require
事件流的过程
事件流可以分为三个阶段:事件捕获、目标阶段和事件冒泡。
- 事件捕获阶段:事件从最外层的文档节点一直往下传递,直到事件到达事件的目标元素。在这个过程中,事件会经过父节点和祖先节点,直到到达目标节点。如果在这个过程中有事件处理程序,则事件将被调用。
- 目标阶段:事件到达了目标元素后,将在目标元素上调用事件处理程序。如果有多个事件处理程序绑定到目标元素上,将按照它们的顺序执行。
- 事件冒泡阶段:事件在目标元素上处理后,会从目标元素开始,向上传递回文档节点。在这个过程中,事件也会遇到任何绑定的事件处理程序。
目前有两种事件流模型:
- W3C 标准事件模型(DOM2级事件模型):事件流由三个阶段组成:捕获阶段、目标阶段和冒泡阶段。这种事件模型中,事件处理程序的调用顺序与它们注册的顺序相同。
- Microsoft 事件模型(IE 事件模型):事件流由两个阶段组成:目标阶段和冒泡阶段。在这个事件模型中,事件处理程序的调用顺序与它们注册的相反。
事件委托
其核心思想是利用事件冒泡机制,将子元素的事件委托到父元素,从而通过一个事件处理程序来管理多个子元素的事件。
具体来说,如果我们有多个子元素都需要响应某个事件(比如点击),与其在每个子元素上绑定事件处理器,不如在它们的父元素上绑定一个事件处理器。当子元素触发事件时,事件会冒泡传递到父元素,由父元素的事件处理程序来处理这个事件。
e.target / e.currentTarget 区别
- e.target:
触发事件的元素 - e.currentTarget
绑定事件的元素
<div class="parent"><button>click Me</button></div><script>let parent = document.querySelector('.parent')let button = document.querySelector('button')parent.addEventListener('click', function(e) {console.log("e.target: ", e.target);//e.target: <button>click Me</button>console.log("e.currentTarget: ", e.currentTarget);//e.currentTarget: <div class="parent">…</div>})
讲一讲js垃圾回收机制
基于V8引擎,使用标记-清除算法来管理内存
哪些操作会导致内存泄漏
定时器没有被清除
闭包
dom元素引用
循环引用