§ 原生JS的数据类型都有哪些?
基本数据类型:Number/String/Boolean/Null/Undefined/Symbol/BigInt
引用数据类型:分两种:
1)Object(Object/Array/Date/RegExp/Match)
2)Function
§ 基本数据类型和引用数据类型的区别是什么?
1.基本数据类型存储在栈内存中
2.数据存储时,基本数据类型在变量中存的是值,引用数据类型在变量中存储的是空间地址
补充:对于BigInt的介绍:baijiahao.baidu.com/s?id=168140…
§ 什么是堆栈内存?
1.堆内存和栈内存是浏览器形成的两个虚拟内存
2.栈内存主要是用来存储基本数据的值,栈内存是一种简单的存储,但是存储的数据都是有范围上线的,一旦超过,就会造成栈溢出
3.堆内存主要是用来存储引用数据类型的
堆内存:存储引用类型,对象类型就是键值对,函数就是代码字符串
堆内存释放:将引用类型的空间地址变量赋值成null,或者没有变量占用堆内存了,浏览器就会释放掉这个地址
栈内存:提供代码执行的环境和存储基本类型值
栈内存释放:一般当函数执行完后,函数的私有作用域就会被释放掉
§ Null和Undefined的区别是什么?
1.Null表示为空,用来占位,但是以后可以重新赋值,Undefined表示为定义
2.Null表示空对象指针,可以给变量赋值为Null,来清空变量,可以用来释放堆内存
3.如果变量未定义,那么默认存储值为undefined
如果对象某个属性不存在,获取到的值也是undefined
如果函数的形参没有对应的实参,那么形参默认的存储值也是undefined
如果函数没有返回return的值,那么默认返回还是undefined
§ isNaN() 和 Number.isNaN()的区别是什么?
NaN(Not a Number)有一个非常特殊的特性,NaN不等于其本身,也不等于任何
isNaN:先尝试转换为数字,如果隐式转换为Number类型失败,就会返回NaN
Numnber.isNaN():直接判断是否为NaN
§ 如何判断当前是否为NaN
NaN == NaN // false
NaN === NaN // false
需要注意的是:NaN与任何值相加,结果都等于NaN
console.log(NaN + 1) // NaN
console.log(NaN + null) // NaN
console.log(NaN + undefined) // NaN
通过NaN的特性,我们封装一个方法:
const is_NaN = (x) => {
return x != x
}
// 测试
is_NaN(NaN) // true
is_NaN('a' - 100) // true
is_NaN('a100') // true
is_NaN('a' + 100) // true
is_NaN(100) // false
is_NaN('100') // false
还可以使用es6中的isNaN()来判断
isNaN(NaN) // true
isNaN('a' - 100) // true
isNaN('a100') // true
isNaN('a' + 100) // true
isNaN(100) // false
isNaN('100') // false
当一个表达式中有减号,乘号,除号运算符的时候,JS引擎在计算之前会试图将表达式的每一项都转化为Number类型,如果转换失败,就会返回NaN
100 - 'abc' // NaN
'abc' * 100 // NaN
'abc' / 100 // NaN
Number('abc') // NaN
+'100abc' // NaN
§ 为什么JS是单线程?
1.JS的主要用途是和用户互动以及操作DOM,如果不是单线程,那么就会造成很复杂的同步问题,所以JS只能被设计成单线程
2.为了利用多核CPU的计算能力,H5提出了web worker标准,允许JS脚本创建多线程,但是子线程完全受主线程控制,并且无法操作Dom,所以这个新标准并没有改变JS单线程的本质
§ 讲讲同步任务和异步任务(讲讲async和await)
单线程是指一次只能完成一个任务,如果在同时间执行多个任务,那么这些任务就得排队,只有前一个任务执行完成,才会执行下一个任务,但是如果有一个任务执行时间很长,就会导致后面的任务一直处于等待状态,这样就会造成用户体验问题,所以为了解决这个问题,JS将任务执行模式分为同步(Async)和异步(Await)
7.讲讲JS同步模式和异步模式
1.同步模式:同步模式就是前一个任务执行完成之后,再执行下一个任务,程序的执行顺序与任务的排列顺序是一致的,也是同步的
2.异步模式:异步模式就是每一个任务有一个或多个回调函数,前一个任务结束后,不是执行队列上的最后一个任务,而是执行回调函数,后一个任务不需要等前一个任务的回调函数执行完成之后再执行,所以程序执行顺序与任务的排列顺序是不一致的,也是异步的
§ 讲讲JS事件循环
JS事件循环,Event Loop
1.同步任务和异步任务分别进入不同的执行场所,同步任务进入主线程,异步任务进入Event Table并且注册回调函数
2.当执行的事情完成之后,EventTable会将这个函数植入任务队列,等待主线程的任务执行完毕
3.当栈中的代码执行完毕,执行栈中的任务为空时,就会读取任务的回调
4.如此循环,就形成了事件循环的机制
§ 讲讲什么是事件表格?
JS事件表格,Event Table
1.Event Table 可以理解为一张事件和回调函数的对应表
2.Event Table 用来存储JS中的异步事件以及对应的回调函数的列表
3.当执行的事件完成时,Event Table会将这个回调函数移入宏任务队列或微任务队列
§ 讲讲什么是宏任务,什么是微任务?
1.JS是单线程语言,简单的说就是只有一条通道,那么在任务多的情况下,会出现拥挤情况,这种情况下就产生了多线程,实际上这种多线程是通过单线程模仿的,也就是假的,那么就会用到同步任务和异步任务
2.宏任务是由node和浏览器发起的,微任务先执行,宏任务后执行
宏任务具体事件:setTimeout,setInterval,XMLHttpRequest,setImmedia,I/O,UI rendering等等
微任务是由JS引擎发起的,微任务具体事件:Promise,Process.nextTick,Object.observe,MutationObserver等等
§ 讲讲JS深拷贝和浅拷贝
1.基本数据类型的值放在栈区,可以直接访问和修改,并且相互之间不会影响
2.引用数据类型的地址放在栈区,值放在堆区,所以当你进行赋值操作的时候,实际上赋值的是地址
浅拷的方法:
Object.protype.toString.call
我们可以自己写一个浅拷贝,实现原理是通过Object.protype.toString.call获取数据类型,通过for循环判断,用数据私有化属性hasOwnProperty进行赋值
2.Object.assign()
3.Array.prototype.slice()
4.数据的浅拷贝用 ... 运算符和concat()
深拷贝的方法:
把一个对象中所有的属性或者方法一个一个的找到,并且在另一个对象中开辟对应的空间,然后一个一个的存储到另一个对象中
1.JSON.parse() / JSON.stringify()
具体的代码:
// 浅拷贝:
let data = [1,2,3,4,5,6]
let copy01 = data.slice()
console.log(data,copy01)
// [1,2,3,4,5,6]
// [1,2,3,4,5,6]
let copy02 = [].concat(data)
console.log(data,copy02)
// [1,2,3,4,5,6]
// [1,2,3,4,5,6]
let obj = {
name:"xiaoming",
age:22,
sex:"男"
}
let copy = {}
Object.assign(copy,obj)
console.log(obj, copy)
// {name: 'xiaoming', age: 22, sex: '男'}
// {name: 'xiaoming', age: 22, sex: '男'}
// 深拷贝:
let obj = {
name:"xiaoming",
age:22,
sex:"男"
}
let str = JSON.stringify(obj)
let copy = JSON.parse(str)
console.log(obj, copy)
// {name: 'xiaoming', age: 22, sex: '男'}
// {name: 'xiaoming', age: 22, sex: '男'}
window.structuredClone()
const obj = {
name: '123',
arr: [1, 10, '123', { aa: 'aaa' }],
data: {
name: 'abc',
age: 20,
},
field: null,
ceshi: undefined,
}
obj.other = obj;
const copy = window.structuredClone(obj)
console.log(copy)
完整的封装一个深拷贝方法:
// 测试
const obj = {
name: '123',
arr: [1, 10, '123', { aa: 'aaa' }],
data: {
name: 'abc',
age: 20
},
field: null,
ceshi: undefined
}
obj.other = obj
obj.arr.push(obj)
// 深拷贝
function deepClone(obj) {
// 当对象克隆的时候,对每个对象进行缓存,下次克隆的时候,直接读取缓存,来解决对象引用无限递归
// weakMap不影响垃圾回收,如果使用map,则会提升内存泄漏风险
const cache = new WeakMap()
// 声明子函数,避免全局变量污染
function _deepClone(obj) {
// 判断当前传参是否为非对象,如果非对象,则直接返回结果
if (obj === null || typeof obj !== 'object') {
return obj
}
// 如果当前传参是对象,则判断缓存中是否存在数据,如果存在,则直接读取缓存
if(cache.has(obj)) {
return cache.get(obj)
}
// 判断当前传过来的是数组还是对象
const result = Array.isArray(obj) ? [] : {}
// 如果缓存中不存在,则把对象加进去
cache.set(obj, result)
// 循环对象并且用递归的方式对对象的每个属性进行深度克隆
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = _deepClone(obj[key])
}
}
return result
}
return _deepClone(obj)
}
const cloneObj = deepClone(obj)
console.log(cloneObj)
§ 讲讲深拷贝和浅拷贝的区别是什么?
浅拷贝只赋值对象的第一层属性,深拷贝可以对对象的属性进行递归赋值
浅拷贝就是赋值,相当于把一个对象中所有的内容赋值一份给另一个对象,直接赋值,或者说就是把一个对象的地址给了另一个对象,他们指向相同,两个对象之间的共同属性或者方法都可以使用
§ 讲讲JS的Promise
Promise的使用场景:处理异步回调,多个异步函数同步处理,异步依赖异步回调,封装统一的入口办法或者错误处理
Promise是JS中进行异步操作的新解决方案,Promise是一个构造函数,Promise对象来封装一个异步操作并可以获取成功和失败的返回值,Promise支持链式调用
Promise异步操作有三个状态,分别是pending(进行中)reslove(成功)reject(失败)任何其他操作都不能改变这个状态
状态缺点:
无法取消Promise,一旦新建就会立即执行,无法中途取消。如果不设置回调函数,Promise内部抛出的作物,不会反应到外部,当处理pending状态时,无法得知目前进展到哪一个阶段
then方法接收俩哥哥函数当作参数,分别是成功和失败,两个函数指挥有一个被调用
then支持多次调用
Promise常见的API方法:
.then:得到Promise内部任务的执行结果
.catch:得到Prmise内部任务失败的结果
.finally:无论是成功还是失败,都会返回
.all:按顺序指定多个Promise并且都执行结束之后分别返回结果
.race:
Promise执行顺序:
console.log(1)
setTimeout(function(){
console.log(2)
}, 0)
new Promise(function(resolve){ // 这里的回调是同步的
console.log(3)
resolve()
}).then(function(){ // 异步微任务
console.log(4)
})
console.log(5) // 1,3,5,4,2
setTimeout(() => {
console.log(1)
})
new Promise((resolve) => {
console.log(2)
for(let i = 0;i < 10000;i++){
if(i == 10){
console.log(3)
}
i == 9999 && resolve(4)
}
}).then((val) => {
console.log(val)
})
console.log(5)
// 2,3,5,4,undefined,1
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
async1()
console.log('processing')
new Promise((resolve, reject) => {
for (let i = 0; i <= 2; i++) {
console.log(1)
resolve()
}
}).then(function () {
console.log('promise1')
}).then(function () {
console.log('promise2')
})
console.log('end')
/*
start
async1 start
async2
processing
1(三次)
end
async1 end
promise1
promise2
setTileout
*/
§ 讲讲箭头函数和function的区别,以及this指向的区别
1.function定义函数,this指向对着调用环境的变化而变化,箭头函数中的this指向是固定不变的
2.function可以定义构造函数,箭头函数不可以,箭头函数不能使用new,也不能使用argument对象,因为箭头函数不存在,如果使用,可以用rest代替
3.由于js内存机制,function级别最高,因为变量提升。因为var定义的变量不能得到变量提升,所以箭头函数一定要定义在调用之前
4.箭头函数不能使用yield命令,所以不能作为Generator函数
5.call()、apply()、bind()等方法不能改变箭头函数中this的指向
§ 讲讲JS闭包
闭包是什么:闭包是指有权访问另一个函数作用域中变量的函数。
形成闭包的原因:内部的函数存在外部作用域的引用就会导致闭包。
闭包的作用:
1.保护函数的私有变量不受外部的干扰,形成不销毁的栈内存。
2.把一些函数内的值保存下来,闭包可以实现方法和属性的私有化。
闭包的使用场景:
1.防抖节流函数
2.JS单例模式
3.实现柯里化函数
4.这样我们就可以在实现post请求方式了
闭包的缺点:
1.容易导致内存泄漏
2.闭包会携带包含其他的函数作用域,因此会比其他函数占用更多的内存
3.过度使用闭包会导致内存占用过多,所以要谨慎使用闭包
§ 讲讲原生JS,new一个对象的过程
1.创建空对象
2.新建对象执行prototype连接原型
3.绑定this到新对象上
4.执行构造函数
5.返回新对象
§ 讲讲JS中的原型,原型链的理解:
在JS中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个prototype属性,它的属性值就是一个对象,这个对象包括了可以由该构造函数的所有实例共享的属性和方法,当使用构造函数新建一个对象的时候,在这个对象的内部将包含一个指针,这个指针指向的构造函数的prototype属性对应的值,在ES5中这个指针,就被成为对象的原型。
ES5中新增了一个方法,叫 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象中去寻找这个属性,这个属性对象又会偶自己的原型,于是就这样一直找下去,这个就是原型链。
§ 讲讲JS中的原型链终点是什么?
原型链的终点是 null,因为Object是构造函数,原型链终点是Object.prototype.proto,因为Object.prototype.proto === null // true,所以原型链的终点是null
§ 讲讲如何获取对象非原型链上的属性?
使用 hasOwnProperty() 方法来判断属性是否属于原型链的属性
§ 讲讲原生JS对作用域,作用域链的理解
1)全局作用域:
最外层函数和最外层函数外面定义的变量拥有全局作用域
所有未定义直接赋值的变量自动声明为全局作用域
所有window对象的属性拥有全局作用域
全局作用域有很大的弊端,比如过多的全局作用域变量会污染全局命名空间,容易引起命名冲突
2)函数作用域
函数作用域声明在函数内部的变量,一般只有固定的代码片段可以访问到
作用域是分层的,内层作用域可以访问外层作用域,但是外层作用域却无法访问内层作用域
3)块级作用域
使用ES6中新增的let和const指令可以声明会计作用域,块级作用域可以在函数中创建,也可以在一个代码块中创建
let和const声明的变量不会有变量提升,也不可以重复声明
再循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制再循环内部
4)作用域链
在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象
§ 讲讲原生JS的let,const,var的区别:
1.块级作用域:let和const具有块级作用域,var不存在块级作用域,块级作用域解决了ES5的两个问题:分别是内层变量可以覆盖外层变量和用来计数的循环变量泄露为全局变量
2.变量提升:var存在变量提升,let和const不存在变量提升
3.给全局添加新属性:浏览器的全局对象是window,node的全局对象是global,var声明的变量为全局变量,并且会并且会将该变量添加为全局对象的属性,但是let和const不会
4.重复声明:var声明变量时,可以重复声明,后声明的同名变量名会覆盖之前生命的变量,const和let不允许重复声明变量
5.暂时性死区:再使用let和const命令声明变量之前,该变量都是不可用的,var声明变量就不会存在暂时性死区
6.初始值设置:再变量生命的时候,var和let可以不用设置初始值,但是const必须设置初始值
7.指针指向:let和const时ES6新增的,let可以更改指针指向,const不允许改变指针指向
§ 讲讲JS类数组对象的方法:
通过call调用数组的 slice 方法来实现转换:
Array.prototype.slice.call(arrayLike);
通过call调用数组的 splice 方法来实现转换:
Array.prototype.slice.call(arrayLike);
通过call调用数组的 concat 方法来实现转换:
Array.prototype.concat.apply([], arrayLike);
通过call调用数组的 Array.from 方法来实现转换:
Array.from(arrayLike);
§ 讲讲JS/ES6中的数组方法都有哪些?
数组和字符串方法:toString(),toLocalString(),join()
数组增删(前增后增,前删后删)的方法:pop() 和 push(),shift() 和 unshift()
数组排序方法:reverse() 和 sort()
数组连接方法:concat() 不影响原数组
数组截取方法:slice() 不影响原数组
数组插入方法:splice(),影响原数组
数组通过索引查找方法:indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach()
数组归并方法:reduce() 和 reduceRight() 方法
数组判断是否存在值:includes()
§ JS中哪些可以改变原数组,哪些不可以改变?
JS数组中,能改变原数组的方法是:shift,unshift,pop,push,reverse,sort,splice,copyWithin,fill
JS数组中,不能改变原数组的方法是:concat,join,slice,map,filter,forEach,some,every,reduce
§ 讲讲原生JS判断数据类型的方法
typeof:其中数组,对下个,null都会返回object类型
instanceof:只能正确判断引用数据类型,不能精准判断基本数据类型,其中运行机制是判断在原型链中是否能找到改类型的原型,所以instanceof可以用来测试一个对象在原型链中是否存在一个构造函数的prototype属性
constructor:有两个作用,一个是判断数据类型,第二个是对象实例通过constructor对象访问的构造函数
Object.prototype.toString.call():通过js原型链去判断数据类型
§ 讲讲原生JS判断数组类型的方法有哪些?
通过 Object.prototype.toString.call() 判断
通过ES6中的 Array.isArray() 判断
通过 instanceof 判断
call,apply,bind的作用是相同的,都是用来动态改变当前函数内部环境对象的this指向的
call,apply,bind的相同点是都不会改变原函数的this指向
不同点是执行方式不同,call,apply是改变后页面加载之后立即执行,是同步代码,bind是异步代码,改变后不会立即执行,而是返回一个新的函数
其次是传参不同,call和bind传参是一个一个的传入,不能使用剩余参数的方式传参,apply可以使用数组的方式传入,只要是数组方式,就可以使用剩余参数的方式传入
最后是修改this的性质不同,call,apply只是临时修改一次,当再次调用原函数的时候,他的指向还是原函数的指向,bind是永久修改函数this指向,但是他修改的不是原来的函数,而是返回一个修改之后的新函数通过 Array.prototype.isPrototypeOf(obj) 判断
§ 讲一下call,apply,bind的区别
call,apply,bind的作用是相同的,都是用来动态改变当前函数内部环境对象的this指向的
call,apply,bind的相同点是都不会改变原函数的this指向
不同点是执行方式不同,call,apply是改变后页面加载之后立即执行,是同步代码,bind是异步代码,改变后不会立即执行,而是返回一个新的函数
其次是传参不同,call和bind传参是一个一个的传入,不能使用剩余参数的方式传参,apply可以使用数组的方式传入,只要是数组方式,就可以使用剩余参数的方式传入
最后是修改this的性质不同,call,apply只是临时修改一次,当再次调用原函数的时候,他的指向还是原函数的指向,bind是永久修改函数this指向,但是他修改的不是原来的函数,而是返回一个修改之后的新函数
§ 讲讲js遍历对象的方法都有哪些?
1.for in,可以遍历对象所有的可枚举属性,包括对象本身的属性和对象继承来的属性
2.Object.keys(),可以遍历到所有对象本身可美剧的属性,但是返回值是数组类型,也就是对象的key
3.Object.values(),遍历对象的特性是相同的,但是返回的结果是以遍历的属性值构成的数组,也就是对象的value
4.Object.entries(),返回值为Object.keys()和Object.values()的结合,也就是会返回一个嵌套数组,数组中包含属性名和属性值
5.Object.getOwnPropertyNames(),返回结果与Object.keys()对应,但是他得特性与其相反,会返回对象得所有属性,包括了不可枚举属性
6.Object.getOwnPropertySymbols(),返回对象内的所有Symbol属性,返回形式依然是数组,需要注意的是,在对象初始化的时候,内部是不会包含任何Symbol属性
7.Reflect.ownKeys(),返回的是一个大杂烩数组,即包含了对象的所有属性,无论是否可枚举还是属性是symbol,还是继承,将所有的属性返回
§ for in可以遍历对象,for of可以遍历对象吗?
for of 不可以直接遍历对象,因为for of遍历数组的时候,会自动循环请求数组的迭代器,并且自动使用这个迭代器遍历数组的值,如果想要使用for of来循环数组,那么需要用Object.keys()或者Object.values()返回一个数组,然后根据获得的对象属性数组来遍历对象,这样做和for in的方法类似,但是不同的是Object.keys()不包含对象原型链上的属性,而for in循环包含原型链上的属性
var array = [1,4,3,4,5,2]
for(var value of a){
console.log(value);
}
// 1 4 3 4 5 2
var obj = {name:'xiaoming',age:'22'}
for(var value of obj){
console.log(value);
}
// Uncaught TypeError: obj is not iterable
var obj = {name:'xiaoming',age:'22'}
for(var value of Object.keys(obj)){
console.log(value);
}
// name age
var obj = {name:'xiaoming',age:'22'}
for(var value of Object.values(obj)){
console.log(value);
}
// xiaoming 22
§ 说说数组去重方法都有哪些
1.filters和indexOf()
let arrList = [1,2,2,3,3,4,4,5,6,7,8,9,9,0]
let newArr = arrList.filter((item,index,array) => {
return array.indexOf(item) === index
})
console.log(newArr)
原理:遍历数组并且检查当前检查项的索引是否与原数组中项的索引通过indexOf返回是否相同,如果不同,则索引值一定是相同的
2.reduce()和includes()
let arrList = [1,2,2,3,3,4,4,5,6,7,8,9,9,0]
let newArr = arrList.reduce((unique, item) => {
unique.includes(item) ? unique : [...unique, item]
}, [])
3.通过运算符new Set 或者 Array.from 实现去重
let originalArray = [1, 2, 3, 4, 1, 2, 3, 4]
let uniqueArray = array => [...new Set(array)]
let originalArray = [1, 2, 3, 4, 1, 2, 3, 4]
let uniqueArray = Array.from(new Set(originalArray))
4.双重for循环实现去重
let arr = [1, 2, 3, 4, 1, 2, 3, 4]
for(let i = 0;i <= arr.length-1-1;i++){
for(let j = i+1;j <= arr.length;j++){
if(arr[i] === arr[j]){
arr.splice(j,1)
j--
}
}
}
5.利用indexOf(),判断该数值第一次出现的索引下标是不是和本身的索引下标一样,如果不一样就说明有重复
let arr = [1, 2, 3, 4, 1, 2, 3, 4]
for(let i = 0;i <= arr.length-1;i++){
if(arr.indexOf(arr[i]) !== i){
arr.splice(i,1)
i--
}
}
6.声明一个空数组,利用indexOf()如果当前数据数值的索引下标与第一次出现的索引下标一样的话,就给空数组增加
let arr = [1, 2, 3, 4, 1, 2, 3, 4]
let newArr = []
for(let i = 0;i <= arr.length-1;i++){
if(!newArr.includes(arr[i])){
newArr.push(arr[i])
}
}