JS基础类型和复杂类型
基础类型:string、number、boolean、undefined、null、symbol
复杂类型(引用类型):Object、Array、Function
箭头函数与普通函数的区别
- 代码更加简洁:无参数可以直接写空括号,只有一个参数可以省略括号,函数体只有一个return语句的话可以省略大括号。
- 箭头函数没有自己的this:函数内部不会创建自己的this,它只会访问自己作用域的上一层的this,本质是通过闭包来访问的。
- 箭头函数继承来的this指向永远不会改变
- call()、apply()、bind()等方法不能改变箭头函数中this的指向
- 箭头函数不能作为构造函数使用:由于箭头函数时没有自己的this,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。
- 箭头函数没有自己的arguments对象:在箭头函数中访问arguments实际上获得的是它外层函数的arguments值(也是闭包)。
- 箭头函数没有prototype。
JS中null和undefined的判断方法和区别
undefined表示缺少值,即此处应该有值,但是还没有定义。null表示一个值被定义了,但是这个值是空值。
typeof null; // 'object'
typeof undefined; // 'undefined'
Number(null); // 0
Number(undefined); // NaN 原型链和继承
基础定义:函数的原型对象prototype、原型对象的constructor、对象的原型__proto__。
原型链:访问对象属性时,如果这个对象上本身没有这个属性时,它就会去它的原型__proto__上去找,如果还找不到,就去原型的原型上去找…一直直到找到最顶层(Object.prototype)为止,Object.prototype对象也有__proto__属性,值为null。
原型链的作用:实现继承。
继承的几种方式:原型链继承、构造函数继承、组合继承、寄生组合继承。
this指向
函数中的this指向,是取决于如何调用的:
- 通过new调用,this指向新对象
- 直接调用,this指向全局对象,浏览器是window,node是global
- 通过对象调用,this指向前面的对象
- 通过call、apply、bind,this指向第一个参数。(特殊的,如果bind返回的函数通过new调用,则this指向新创建的对象)
理解箭头函数中的this:
箭头函数中的this的指向,取决于箭头函数定义的位置,而不是运行的位置。这是因为箭头函数中没有定义this,会基于闭包(词法作用域)从外层寻找this。
闭包
概念:多层函数作用域,在最外层执行栈结束之后,外部仍然持有对内部变量的引用。(考虑垃圾回收)
概念:有权访问另一个函数内部变量的函数。
闭包用处:1、读取另一个函数内部的变量。2、这些变量的值始终会保持在内存中,不会在外层函数调用后被自动清除。
优点:1、变量会一直在内存中,不会被清除;2、可以视为私有属性,不会被外部访问;3、避免全局变量的污染(三方库的封装)。
闭包缺点:变量长期储存在内存中,会增大内存的使用量,使用不当会造成内存泄露
内存泄漏怎么解决:首先避免闭包的使用,其次的话就是变量执行完以后,可以让它赋值为null,最后利用JS的一个垃圾回收机制进行回收。
- 解释一下作用域链是如何产生的。
- 解释一下js执行上下文的创建、执行过程。
- 解释一下闭包所产生的变量放在哪了。
简述:
- js代码运行需要一个运行环境,这个环境就是执行上下文。
- 执行上下文的周期,分为两个阶段,创建阶段和执行阶段。
- 创建阶段,创建词法环境,生成变量对象(VO),建立作用域链(重要),确认和绑定this指向。
- 执行阶段,进行变量赋值,函数引用及执行代码。
- 变量对象:是 js 代码在进入执行上下文时,js 引擎在内存中建立的一个对象,用来存放当前执行环境中的变量。
作用域和作用域链
垃圾回收
引用计数(老版本,无法解决循环引用的问题)、标记清除(现代浏览器)
V8引擎中新生代和老生代的垃圾回收机制。
标记清除的工作流程:
- 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记。
- 去掉环境中的变量以及被环境中的变量引用的变量的标记。
- 再被加上标记的会被视为准备删除的变量。
- 垃圾回收器完成内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间。
js列举和数组操作相关的方法
- 添加元素:push、unshift
- 删除元素:pop、shift、splice
- 查找元素:indexOf、lastIndexOf、find、findIndex
- 排序和翻转:sort、reverse
- 遍历和过滤:forEach、map、filter、reduce
- 归并数组:concat、slice
- 其它:join、split等等
js数组去重的方法
- Set
- Map
- array.filter + indexof
- array.reduce
typeof和instanceof的区别是什么
JS中 "=="和"==="的区别详解
"==":两个等号我们称为等值符,当等号两边的值为相同类型时比较值是否相同,类型不同时会发生类型的自动转换,转换为相同的类型后再作比较。也就是说两个等号只要值相等就可以。"===":三个等号我们称为等同符,当等号两边的值为相同类型的时候,直接比较等号两边 的值,值相同则返回 true,若等号两边的值类型不同时直接返回 false。也就是说三个等号既要判断值也要判断类型是否相等。
如何用原生 JS给一个按钮绑定两个 onclick 事件?
使用addEventListener
var、let和const的区别
- 全局污染
- 块级作用域
- 暂时性死区
- 重复声明
call、apply、bind的区别
相同点:都是改变this的指向,指向第一个参数。
不同点:
- call,接收的是参数列表,返回函数执行的结果。
- apply,接收的是数组,返回函数执行的结果。
- bind,不会立即执行函数,而是返回一个新的函数。作为构造函数时,绑定的this会失效,指向新对象。
Function.prototype.myCall = function (ctx, ...args) {
// 参数归一化
ctx = ctx === undefined || ctx === null ? globalThis : Object(ctx)
// 拿到要执行的函数
const fn = this
// 绑定函数到ctx,(优化:不能被遍历到)
const key = Symbol("function_call")
Object.defineProperty(ctx, key, {
value: fn,
enumerable: false,
})
// 执行函数并返回对应的结果
const result = ctx[key](...args)
delete ctx[key]
return result
}
function test(a, b) {
console.log(this, a, b, this.name)
return a + b
}
console.log(test.myCall({ name: "tttt" }, 1, 3))
Function.prototype.myBind = function (ctx, ...args) {
// 要执行的函数
const fn = this
return function A(...rest) {
// 如果是 new 调用的,保留new的行为
if (Object.getPrototypeOf(this) === A.prototype) {
// 或者 this instanceof A
return new fn(...args, ...rest)
}
// 执行函数并返回结果
return fn.apply(ctx, [...args, ...rest])
}
}
function test(a, b, c, d) {
console.log(this, a, b, c, d)
return a + b
}
const newFn = test.myBind({ name: "aa" }, 1, 2)
console.log(newFn(3, 4))
console.log(new newFn(3, 4))
栈溢出及解决方法
缓冲区溢出是由于C语言系列设有内置检查机制来确保复制到缓冲区的数据不得大于缓冲区的大小,因此当这个数据足够大的时候,将会溢出缓冲区的范围。栈溢出就是缓冲区溢出的一种。
栈溢出的解决方法:
- 减少栈空间的需求,不要定义占用内存较多的auto变量,应该将此类变量修改成指针,从堆空间分配内存。
- 函数参数中不要传递大型结构/联合/对象,应该使用引用或指针作为函数参数。
- 减少函数调用层次,慎用递归函数,例如A→B→C→A环式调用。
JS如何实现多线程
概念:JavaScript本身是单线程的,但是可以通过实现多线程来提高性能和用户体验。多线程允许JavaScript在等待用户交互或网络请求时,执行其他任务,从而提高页面加载速度和响应速度。
实现多线程的方式:Web Workers、SharedArrayBuffer、WebAssembly等。其中,Web Workers允许在后台线程中运行JavaScript代码,而SharedArrayBuffer和BufferSource API则允许在多个线程之间共享数据。
浅拷贝和深拷贝
浅拷贝:Object.assign()、array.slice()、es6扩展运算符
深拷贝:JSON.parse(JSON.stringify(obj))、三方库如lodash等
事件循环
简答:如何理解js的异步
js是一门单线程的语言,这是因为他运行在浏览器的渲染主线程中,而渲染主线程只有一个。 而渲染主线程承担着很多工作,渲染页面、执行js都在其中。 如果使用同步的方式,就会导致渲染主线程产生阻塞,从而导致消息队列中的其他任务无法得到执行。这样的话,一方面会导致繁忙的主线程白白消耗时间,另一方面页面无法及时更新,给用户造成卡死的现象。 所以浏览器采用异步的方式来避免。具体做法是当某任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即执行任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。 在这种异步模式下,浏览器可以实现永不阻塞,从而最大限度的保证了渲染主线程的流畅运行。 总结:单线程是异步产生的原因,事件循环是异步的实现方式。
简答:阐述浏览器的事件循环
事件循环又称消息循环,是浏览器渲染主线程的工作方式。 在chrome的实现中,会开启一个无限循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候,将任务加入到队列末尾即可。 过去把消息队列分为宏队列和微队列,目前已不适用,取而代之的是一种更灵活多变的处理方式。 根据w3c的官方解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务执行。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。
js操作数组、对象、Map的复杂度
数组:
- 访问元素:O(1),原因:数组是基于连续内存的,通过索引可以直接计算出内存地址。
- 末尾插入/删除元素,arr.push() / arr.pop(),O(1),原因:动态数组在末尾插入/删除元素不需要移动其他元素。
- 开头插入或删除元素,arr.unshift() / arr.shift(),O(n),原因:需要移动所有元素以腾出空间或填补空缺。
- 查找/遍历元素:arr.indexOf() / arr.includes() / arr.find() / arr.filter() / arr.forEach() / arr.map() / arr.reduce(),O(n)
- 排序:arr.sort(),O(n log n),现代 JavaScript 引擎通常使用高效的排序算法(如快速排序或归并排序)。
Map:JavaScript 的 Map 是基于哈希表实现的,类似于其他语言中的 HashMap。
- 插入/删除元素:map.set / map.delete,O(1)
- 查找元素:map.get、map.has,O(1)
- 遍历:map.forEach() / map.keys() / map.values() / map.entries(),O(n)
对象:
- 属性查找:O(1)
注意事项:
- 哈希冲突:Map 的复杂度在哈希冲突较多时可能退化为 O(n),可以采用拉链法解决冲突。
- 动态数组扩容:数组在容量不足时需要扩容,扩容操作的时间复杂度为 O(n),但均摊到每次插入操作后仍然是 O(1)。
- 引擎实现:不同 JavaScript 引擎(如 V8、SpiderMonkey)可能对数组和 Map 的实现有细微差异,但时间复杂度通常符合上述分析。
0.1 + 0.2
JavaScript 中的浮点数:都是以 双精度浮点数(double-precision floating-point format)来存储的。这是根据 IEEE 754 标准定义的,意味着它遵循与其他编程语言类似的浮点数表示规则。然而,双精度浮点数在表示某些十进制数时也存在精度问题。就像我们在前面提到的 0.1 和 0.2,这些数字在二进制浮点数中无法精确表示。
- 符号位,第1位用来存储符号位,0表示正数,1表示负数
- 指数位,第2位到第12位(共11位),用来存储指数
- 尾数位,第13位到第64位(共52位),用来存储小数
0.1 和 0.2 在二进制浮点数中无法被精确表示,它们是循环小数。当你执行 0.1 + 0.2 时,计算机在存储这些值时会做出一定的舍入,从而导致最终的计算结果不等于你期望的 0.3。
解决方式:建议使用专门的计算库Decimal.js