面试题合集
数据类型转换
==
和===
的区别
==
:会统一类型,如果是对比null和undefined会返回true,若比较string和number会转换为number,boolean会被转换为number,object和原始类型对比会将object转换为原始类型===
:就是判断两个变量类型和值是否相同
tip:判断[]==![]
结果为true,过程:[]==false->0==0->true
。判断{}==!{}
结果为false,过程{}==false->NaN==0->false
什么是闭包
闭包定义:函数A内部有一个函数B,函数B可以访问到函数A中的变量,那么函数B就是闭包
闭包的意义:
可以让我们间接访问函数内部的变量。
闭包的作用:
- 实现公有变量(累加器)
- 可以做缓存
- 可以实现封装,属性私有
- 模块开发,防止污染全局
解决var
定义函数的问题
for(var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
},i*1000)
}
上述代码显而易见的会输出一堆6
解决办法有三种:
方法一:使用闭包:
for(var i = 0; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j)
},j*1000)
})(i)
}
方法二:利用setTimeout方法的第三个参数,该参数会被当成timer函数的参数传入:
for(var i = 0; i <= 5; i++) {
setTimeout(function timer(j) {
console.log(j)
},i*1000,i)
}
方法三:使用let
深浅拷贝
在对象类型赋值的过程中实际是赋值了地址,因此在一方改变了值,其它地方也会被改变。可以使用浅拷贝解决该问题。
浅拷贝
一:可以使用Object.assign
:Object.assign
会拷贝所有属性值到新对象中,如果属性值是对象的话,拷贝的是地址。
二:可以使用{…}来实现浅拷贝
let a = {
age: 1
}
let b = {...a}
a.age = 2
console.log(b.age)
深拷贝
当对象里面嵌套对象时,就需要进行深拷贝
一:使用JSON进行对象深拷贝
JSON.parse(JSON.stringify(object))
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(a.jobs,first) //FE
存在的问题:
- 会忽略undefined
- 会忽略symbol
- 不能序列化function
- 不能解决循环引用的对象
let obj = {
a: 1,
b: {
c: 2,
d: 3
}
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
上述对象使用JSON会报错。
二:当需拷贝的对象有内置类型并不包括函数时可以使用MessageChannel:
function structuralClone(obj) {
return new Promise(resolve => {
const {port1,port2} = new MessageChannel()
port2.onmessage = ev => resolve(ev.data)
port1.postMessage(obj)
})
}
var obj = {
a: 1,
b: {
c: 2
}
}
obj.b.d = obj.b
//该方法是异步的
//可以处理undefined和循环引用对象
const test = async()=> {
const clone = await structuralClone(obj)
console.log(clone)
}
test()
三:递归,更推荐使用lodash的深拷贝函数:
function deepClone(obj) {
//判断是否为对象且对象是否为null
function isObject(o) {
return (typeof o === 'object' || typeof o === 'function') && typeof o !== null
}
//如果不是对象抛出错误
if(!isObject(obj)) {
throw new Error('非对象')
}
//判断是否是数组
let isArray = Array.isArray(obj)
//对应结构对象,此时可以浅拷贝,但是对象类型数据任然是复制内存地址
let newObj = isAarray ? [...obj] : {...obj}
//Reflect.ownKeys可以遍历所有可迭代和不可迭代的属性
Reflect.ownKeys(obj).forEach(key => {
//如果是基本类型数据可以直接复制,如果是对象就递归克隆
newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
})
return newObj
}
原型
每个对象都有一个_proto_
属性指向原型,但是不推荐直接使用_proto_
来访问原型。
原型的constructor
属性指向构造函数,构造函数又通过prototype
属性指回原型。
Object
是所有对象的爸爸,所有对象都可通过_proto_
找到它Function
是所有函数的爸爸,所有函数都可以通过_proto_
找到它- 函数的
prototype
是一个对象(只有函数有该属性) - 对象的
_proto_
属性指向原型,_proto_
将对象和原型连接起来组成了原型链
并非所有函数都有prototype,例如箭头函数和bind创建的函数
var,let,const
- var:声明的变量会被提升至当前作用域的顶部(优先级低于function),如果是在全局定义的变量会被挂载到window
- let:声明的变量会产生临时死区,在全局定义的变量不会被挂载到window
- const:声明的变量会产生临时死区,在全局定义的变量不会被噶在到window上,且上面的变量不会被改变值,但是可以改变引用类型数据的值例如一个对象中的属性
原型继承和Class继承
JS中不存在类,Class实际是一个语法糖。
组合继承
组合继承是最常见的继承方式
function Parent(value) {
this.value = value
}
function Child(value) {
Parent.call(this,value)
}
Child.prototype = new Parent()
组合继承的核心思想是通过Parent.call(this)继承父类的属性,然后改变子类的原型为new Parent()来继承父类的函数
- 优点:构造函数可以传参,不会与父类引用类型属性共享,可以复用父类函数
- 缺点:继承父类函数的时候调用了父类的构造参数,造成了内存浪费
寄生组合继承
优化了组合继承在继承父类函数时调用构造参数以至子类多了不需要的属性
function Parent(value) {
this.value = value
}
function Child(value) {
Parent.call(this,value)
}
Child.prototype = Object.create(Parent.prototype,{
constructor: {
value: 'Child',
enumerable: false,
writable: true,
configurable: true
}
})
Class继承
Class Parent {
constructor(value) {
this.value = value
}
getValue() {
return this.value
}
}
Class Child extends Parent{
constructor(value) {
super(value)
this.value = value
}
}
Class继承的核心在于通过extends来表示继承自哪个父类,然后通过super来继承父类的属性,super可以看成Parent.call(this,value)
模块化
Q:为什么要使用模块化
A:
- 解决命名冲突
- 提供复用性
- 提高代码可维护性
Q:实现模块化的几种方式:
A:
1.使用立即执行函数,早期最常见的手段:
(function(globalVariable) {
globalVariable.test = function(){}
})(globalVariable)
2.AMD和CMD,很少见了
3.CommanJS,目前被广泛使用:
//a.js中
module.exports = {
a: 1
}
//or
module.a = 1
//b.js中
var module = require('./a.js')
module.a // log 1
CommonJS模块的特点
- 所有代码都运行在模块作用域,不会污染全局作用域
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果,如果让模块再次运行,就必须清除缓存。
exports和module.exports用法相似,直接用module.exports就好了
ES Module
ES Module是原生实现的模块化方案,与CommonJS有以下几个区别:
- CommonJS支持动态导入,即
require(${path}/xx.js)
,后者不支持但是提案 - CommonJS是同步导入,因为用于服务端,文件都在本地,因此即使同步导入卡住主线程影响也不大。后者是异步导入
- CommonJS在导出时是值拷贝,就算导出值变了,导入的值也不会改变。ES是实时绑定。
- ES Module会编译成require/exports来执行
import xxx from './a.js'
//导出模块
export function a(){}
export default function (){}
Proxy代理
Q:Proxy可以实现什么功能?
A:在VUE3中替代Object.defineProperty来实现数据响应式
语法:
let p = new Proxy(target,handler)
target
代表需要添加代理的对象,handler
表示自定义对象中的操作。
map,filter,reduce
参考之前的文章
并发和并行的区别
- 并发:是宏观概念,在一段时间内通过任务时间的切换完成多个。
- 并行:微观概念,同时完成多个任务的情况就可以称为并行。
回调函数
Q:什么是回调函数?
A:回调函数是一个函数,将会在另一个函数完成后立即执行,回调函数是一个作为参数传给另一个JS函数的函数,这个回调函数会在传给的函数内部执行。
Q:回调函数的缺点?
A:容易出现回调地狱、不可使用try/catch
捕获错误,不可直接return
回调地狱的本质问题就是:
- 嵌套函数存在耦合性,牵一发而动全身
- 嵌套函数一多就容易出错
如何解决回调函数
1.Generator(生成器,一种返回迭代器的函数)
Generator可以控制函数的执行。
function *foo() {
let y = 2 * (yield(x + 1))
let z = yield(y / 3)
return (x+y+z)
}
let it = foo(5)
it.next() //{value: 6,done: false}
it.next(12) //{value: 8,done: false}
it.next(13) //{value: 42,done: true}
- Genterator函数和普通函数不同,其返回一个迭代器
- 第一次执行next时,传参会被忽略,并且函数暂停在
yield(x+1)
处,所以返回5+1=6 - 第二次执行next时,传入的参数等于上一个
yield
的返回值,如果不传参,yield
返回值为undefined
。此时let y = 2 * 12
,所以第二个yield
等于2*12/3=8
- 第三次执行next时,传入的参数会传递给z,所以最后的结果为42
一般Generator用的不多,可以用于解决回调地狱的问题
function *fetch() {
yield ajax(url,()=>{})
yield ajax(url,()=>{})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
Promise
Q:Promise的特点是什么,有什么优缺点?
A:特点:promise有三种状态,一旦从pending(等待)态转为其他状态,状态就不会在发生改变了;在构建promise时,构建函数内部的代码时立即执行的;promise实现了链式调用。优点:可以解决回调地狱;状态不可改变,让代码更清晰,更易维护。
缺点:
- 顺序处理错误,catch可以捕获链式中任何位置出现的错误,但是如果在catch之前进行了自身的错误处理就不会被catch捕获
- 单一值,当我们在复杂的场景下promise返回单一值是一种局限
- 单一决议,
无法取消promise,错误需要通过回调函数捕获。
aysnc和await
async和await的特点
- async表示当前函数为异步函数,不会阻塞线程,并且在执行后会自动返回一个promise
- await必须用在async函数内,不可单独使用,await后必须跟promise函数,否则没有意义
- async是异步的,await则表示同步,只有当await后面的promise执行完成才会执行本函数后面的内容,在await后面的promise完成前会跳出async函数向后执行。
async和await的优缺点
优点:可以用更清晰准确的代码处理then调用
缺点:滥用await可能会导致性能问题(比如多个异步代码没有依赖性却使用了await)
Q:await是如何实现的?
A:await其实就是Generator的语法糖,其内部实现了自动执行generator。
常用定时器
1.setTimeout:延时执行,但是js是单线程执行的,如果前面代码影响了性能,就会导致setTimeout不会按期执行。可以通过代码去修正setTimeout
2.setInterval:重复执行,同样不一定会在预期时间执行任务,同时还存在执行累积问题,如果有循环定时器的需求建议使用requestAnimationFrame来替代。requestAnimationFrame自带函数节流,基本可以保障在16.6毫秒内执行一次(不掉帧的情况下,浏览器默认60HZ)
手写Promise
首先要了解Promise/A+规范
实现简易的promise:
const PENDING = 'pending'
const REJECTED = 'rejected'
const RESOLVED = 'resolved'
function myPromise(fn) {
const that = this
//调用promise时的初始状态是pending
that.state = PENDING
//value用于保存resolve或reject传入的值
that.value = null
// resolvedCallbacks和rejectedCallbacks用于保存then中的回调,
// 因为当promise时状态可能还在等待中,这时应该将then中的回调保存起来用于状态改变时使用
that.resolvedCallbacks = []
that.rejectedCallbacks = []
//resolve函数
function resolve(value) {
if(that.state === PENDING) {
that.state = RESOLVED
that.value = value
that.resolvedCallbacks.map(cb => cb(that.value))
}
}
//reject函数
function reject(value) {
if(that.state === PENDING) {
that.state = REJECTED
that.value = value
that.rejectedCallbacks.map(cb => cb(that.value))
}
}
//执行promise中传入的函数
try {
//执行传入的参数并且将之前的两个函数当做参数传入
fn(resolve,reject)
} catch (error) {
//当发生错误时进行捕获并执行reject函数
reject(error)
}
}
//实现promise的then
myPromise.prototype.then = function (onFulfilled,onRejected) {
const that = this
//判断两个函数是否为函数类型,因为这两个函数时可选参数,当参数不是函数类型时,需要创建一个函数赋值给对应的参数,同时也实现了透传
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected = typeof onRejected === 'function' ? onRejected : r => {throw r}
// if(that.state === PENDING) {
// that.resolvedCallbacks.push(onFulfilled)
// that.rejectedCallbacks.push(onRejected)
// }
//获取当前状态,如果状态是等待态就往回调函数中push函数
switch(that.state) {
case PENDING: {
that.resolvedCallbacks.push(onFulfilled)
that.rejectedCallbacks.push(onRejected)
break
}
case RESOLVED: {
onFulfilled(that.value)
break
}
case REJECTED:
onRejected(that.value)
break
}
}
Event Loop
Event loop执行流程:
- 将整个脚本作为宏任务执行
- 执行过程中如果遇到同步代码就直接运行,遇到宏任务就添加进入宏任务队列,遇到微任务就添加进微任务队列
- 宏任务执行完成出队,检查微任务队列是否有任务,有就依次执行完成
- 进行UI渲染
- 检查是否有Web Worke,有就执行
- 本轮宏任务执行完成,重复步骤二
进程与线程
进程与线程的区别:
- 进程:描述了CPU在运行指令及加载和保存上下文所需要的的时间。是资源分配的基本单位
- 线程是进程中更小的单位,描述了执行一段指令所需的时间,是资源调度的基本单位
JS单线程的好处:
JS可以修改DOM,如果在JS执行时UI线程还在工作就可能导致不能安全的渲染UI,可以节省内存,节约上下文切换时间,没有锁的问题。
执行栈
可以把执行栈认为是一个存储函数调用的栈结构,当我们使用递归时,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放就会出现爆栈问题
手写call、apply和bind
手动实现call:
- 不传入第一个参数,那么山下文默认为window
- 改变了this指向,让新的对象可以执行该函数,并能接受参数
Function.prototype.myCall = function(context) {
if(typeof this !== 'function') {
throw new TypeError('xxxx')
}
context = context || window
context.fn = this
let args = [...arguments].slice(1)
let result = context.fn(...args)
delete context.fn
return result
}
apply与call实现方式类似,差别在于第二个参数
Function.prototype.myApply = function(context) {
if(typeof this !== 'function') {
throw new TypeError('xxxx')
}
context = context || window
context.fn = this
let result
if(arguments[1]) {
result = context.fn(...arguments[1])
}else {
result = context.fn()
}
delete context.fn
return result
}
bind相对要复杂,因为需要返回一个函数,需要判断边界问题
Function.prototype.myBind = function(context) {
if(typeof this !== 'function') {
throw new TypeError('xxxx')
}
const _this = this
const args = [...arguments].slice(1)
return F() {
if(this instanceof F) {
return new _this(...args,...arguments)
}else {
return _this.apply(context,args.concat(...arguments))
}
}
}
new
在调用new的过程中会发生的过程:
- 新生产了一个对象
- 链接到原型
- 绑定this
- 返回新对象
手动实现new:
function create(fn, ...args) {
//let obj = {}
//let Con = [].shift.call(arguments)
//使用构造函数的原型构建一个新对象
let obj = Object.create(fn.prototype)
//绑定this指向
let result = fn.apply(obj,args)
//如果fn返回的是null或undefined,我们就返回obj否则返回result
return result instanceof Object ? result : obj
}
实现分析:
- 创建一个空对象
- 获取构造函数
- 设置空对象的原型
- 绑定this并执行构造函数
- 确保返回值为对象
对于创建一个对象来说,更推荐使用字面量的方式创建对象,因为使用字面量不需要通过作用域链一层层找到Object
instanceof
原理:通过判断对象的原型链中是否可以找到类型的prototype
手动实现:
function myInstanceof(left,right) {
let prototype = right.prototype
left = left._proto_
while(true) {
if(left === null || left === undefined) {
return false
}
if(prototype === left) {
return true
}
left = left._proto_
}
}
实现分析:
- 获取类型的原型
- 获得对象的原型
- 然后一直循环判断对象的原型是否等于类型的原型,直到对象的原型为null,因为原型链最终为null
为什么0.1+0.2!=0.3
原因:JS采用IEEE754双精度版本(64位),只要是采用该版本的语言全都有该问题。有一些小数用二进制表示是无限循环的,而JS采用的浮点标准会进行裁剪,被裁剪的数字会导致精度丢失。
在进行下述运算时会出现精度丢失的问题,但是如果使用console.log打印小数是没有显示问题的,究其原因是在打印过程中二进制被转为了十进制,十进制又被转换为了字符串,该转换过程中发生了去近似值的过程
如何解决:原生函数toFiexed()即可解决
parseFloat((0.1+0.2).toFiexed(10))
垃圾回收机制
V8实现了准确式GC,GC算法采用了分代式垃圾回收机制。V8内场(堆)分为新生代和老生代两个部分:
新生代算法:新生代中的对象一般存活时间短,采用Scavenge算法。新生代空间中,内存空间分为两部分、分别是From空间和To空间,必定有一个空闲,一个使用。新分配的对象会被放入From空间,当From空间占满时,新生代GC启动,算法检测From中存活的对象并复制到To空间中,失活对象被销毁,复制完成后From空间和To空间互换。
老生代算法:老生代中的对象一般存活时间长且数目多,使用了标记清除算法和标记压缩算法。什么对象会出现在老生空间中:
- 新生代中的对象是否已经经历过一次Scavenge算法,如果经理过将会从新生代移到老生代
- To空间的对象占比大小超过25%,为了不影响内存分配,会将对象从新生代空间移到老生代空间
在老生代中,以下情况会启动标记清除算法:
- 某一个空间没有分块的时候
- 空间中被对象超过一定限制
- 空间不能保证新生代中的对象移动到老生代中
JS是如何运行的
V8引擎组成:
- 内存堆:执行内存分配的地方
- 调用堆栈:代码执行时堆栈帧的位置
中二进制被转为了十进制,十进制又被转换为了字符串,该转换过程中发生了去近似值的过程**
如何解决:原生函数toFiexed()即可解决
parseFloat((0.1+0.2).toFiexed(10))
垃圾回收机制
V8实现了准确式GC,GC算法采用了分代式垃圾回收机制。V8内场(堆)分为新生代和老生代两个部分:
新生代算法:新生代中的对象一般存活时间短,采用Scavenge算法。新生代空间中,内存空间分为两部分、分别是From空间和To空间,必定有一个空闲,一个使用。新分配的对象会被放入From空间,当From空间占满时,新生代GC启动,算法检测From中存活的对象并复制到To空间中,失活对象被销毁,复制完成后From空间和To空间互换。
老生代算法:老生代中的对象一般存活时间长且数目多,使用了标记清除算法和标记压缩算法。什么对象会出现在老生空间中:
- 新生代中的对象是否已经经历过一次Scavenge算法,如果经理过将会从新生代移到老生代
- To空间的对象占比大小超过25%,为了不影响内存分配,会将对象从新生代空间移到老生代空间
在老生代中,以下情况会启动标记清除算法:
- 某一个空间没有分块的时候
- 空间中被对象超过一定限制
- 空间不能保证新生代中的对象移动到老生代中
JS是如何运行的
V8引擎组成:
- 内存堆:执行内存分配的地方
- 调用堆栈:代码执行时堆栈帧的位置