Javascript的数据类型有哪些,如何检测变量的类型
基本类型:Number、String、Boolean、Undefined、Null、Symbol、BigInt引用类型:Object
typeof操作符检测数据类型,只能精确的检测基本数据类型,并且检测Null类型,返回的是字符串object
instanceof:可以正确判断对象到底属于什么类型通过检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上来判断
constructor:通过返回目标的构造函数,来判断数据的类型因为Number、String、Boolean具有包装对象,用constructor的方式也可以检测。Undefined、Null不可以被检测
Object.prototype.toString.call():通过调用toString方法,返回一个表示目标对象的字符串,通过这个字符串可以准确判断目标对象的类型。
2.什么是javascript变量提升
JavaScript代码在执行过程中,会把变量声明提升到当前作用域的最顶部,只提升声明部分,不会提升初始化,提升后,默认值是undefined
JavaScript代码在编译阶段,会收集所有声明的标识符组成的一系列查询,也就是编译阶段就要收集声明的变量,剩下的赋值语句等在执行到的时候才生效
var、let都会发生变量提升,但是let是块作用域,在用它声明的手就形成了封闭的作用域,在声明前使用就会报错,形成暂时性死区
3.符号(===)和符号(==),这两者有什么区别
这两个符号都是相等性操作符,用来比较两个变量值是否相等
===号:是全等性操作,既要判断操作数的类型是否相同,还要判断值是否相同。只有在类型和值都相同到时候才返回布尔值 true
==号:非全等性操作,不需要考虑类型,只要符号两遍的操作数值相等就返回布尔值true。两边类型不同时会先进行类型转换
4.讲一讲new操作符在实例化的过程中做了什么事
第一步,先创建空对象,这个空对象有可能会作为结果返回
第二步,把对象隐式原型指向构造函数prototype属性
第三步,改变构造函数this的指向,为新创建的空对象
第四步,判断构造函数中有没有返回一个对象,如果没有,则return出去新创建的对象,如果有则返回构造函数返回的对象
5.简述下你对作用域和作用域链的理解
作用域是javascript存储变量、查找变量时遵循的规则,它负责收集所有声明的标识符组成的一些列查询,并确定当前执行的代码对这些标识符的访问权限
作用域分为全局作用域、函数作用域、和块级作用域
全局作用域:在任何地方都能访问到的对象遵循的是全局作用于规则
函数作用域:存在于函数内部,声明在函数内部的变量,在不考虑闭包的情况下,只能在该函数内访问
块级作用域:通过新增的关键词 let 和 const可以声明块级作用域,块级声明的变量只能在该作用域下访问,不可以变量提升
作用域链:函数调用的时,就代表创建执行上下文。创建执行上下文会创建作用域链,作用域链可以看做是一个集合,这个集合里面包含着当前上下文的变量对象和所有父级上下文的变量对象。当访问一个变量时,会在当前上下文中找,如果没有则继续在作用域链上的父级上下文的变量对象里找。
6.说一说对闭包的理解
作用域是由书写代码时函数声明的位置来决定的,函数可以记住并访问所在的词法作用域,这时就产生了闭包
访问变量会现在当前执行上下文中查找,如果找不到会通过作用域链查找父级上下文的变量。按照这种查找方式,内层作用域可以访问外层作用域的变量,而外层的却无法访问内层的变量。而闭包就是要打破这个规则
通俗理解就是内层函数作用域链中包含外层函数的作用域对象,且内层函数被引用,导致内层函数不会被释放,同时它又保持着对父级作用域的引用,这个时候就形成了闭包
7.你对JS中的原型和原型链的理解
原型:英文名是 prototype,声明的每个函数都有一个预定义的prototype属性,这个prototype属性的值是一个对象,这就是原型对象
通过prototype,可以在prototype上挂在自定义的属性、方法,这样实例对象上就能使用这些自定义的属性和方法
原型链:就是属性查找规则,形成的一条链条,这条链条的连接点是由 __proto__这个实例对象的属性链接,它又叫做隐式原型
查找对象属性的顺序规则:先在对象自身上查找,再到对象的原型上查找,再沿着隐式原型查找父级,直到最顶级的Object,Object的原型上的__proto__为null,因此到这就终止了
8.JS实现继承有哪几种方式
原型链继承:就是利用原型链的查找属性的特性,将子类的构造函数的prototype属性指向父类的实例对象,这样子类的实例对象就能沿着隐式原型访问到父类对象的属性及方法。
缺点:当用new构造多个子类实例对象,更改其中一个的引用类型属性值,其它的实例对象也跟着发生改变。这是因为多个实例对象的隐式原型指向的是同一个实例对象,他们共用相同的内存空间。
function Parent() {
this.name = '张大虎'
this.cars = ['凯迪拉克', '宝马', '奔驰']
}
function Child() { }
Child.prototype = new Parent()
const c1 = new Child()
const c2 = new Child()
// 这里是修改了父类的cars属性
c1.cars.push('悍马')
//输出c2的cars,实际访问的是父类的cars
console.Log(c2.cars)
构造函数继承:就是在子类构造函数中调用父类构造函数,并且要使用call或apply方法将this指向子类。
优点:解决了原型链继承的缺点,每个子类实例都会继承父类构造函数中的属性、方法,并且不会互相影响
缺点:子类不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式,不可以复用
function Parent() {
this.name = '张大虎'
this.cars = ['凯迪拉克', '宝马', '奔驰']
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child() {
Parent.call(this)
}
const c1 = new Child()
const c2 = new Child()
c1.cars.push('大G') // 不影响c2
console.Log(c2.getName()) // 报错,不能继承父类原型上的方法
组合式继承:组合式继承是既使用原型链继承原型上的属性和方法,又通过构造函数继承实例属性。
优点:集合了原型链继承和构造函数继承的有点,可传参可复用,且每个新实例都是独立的
缺点:调用了两次父类的构造函数,更耗内存,且子类的构造函数会代替原型上的父类构造函数
function Parent() {
this.name = '张大虎'
this.cars = ['凯迪拉克', '宝马', '奔驰']
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child() {
Parent.call(this)
}
Child.prototype = new Parent()
const c1 = new Child()
const c2 = new Child()
c1.cars.push('大G') // 不影响c2
console.log(c2.cars)
c2.getName() //输出父类name
寄生式继承:创建一个函数,在该函数内部创建一个新对象,使用父类对象来作为新创建对象的原型,最后返回这个对象。
优点:在函数内不可以增强对象,给对象添加一些额外的属性
缺点:多个子类实例对象,更改其中一个的引用类型属性值,其它的实例对象也跟着发生改变。
function Parent() {
this.name = '张大虎'
this.cars = ['凯迪拉克', '宝马', '奔驰']
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function clone(o) {
let clone = Object.create(o)
return clone
}
const p = new Parent()
const c1 = new clone(p)
const c2 = new clone(p)
c1.cars.push('特斯拉')
console.log(c1.cars, c2.cars) //引用类型会被影响
c2.getName()
组合寄生式继承:通过借用构造函数来继承属性,通过原型链形式来继承方法
优点:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费

9.什么是防抖和节流
节流和防抖是一种编程技巧、性能优化方案。它可以防止高频事件处理程序内的代码频繁执行,从而有效的降低性能损耗。
1.防抖函数:在指定时间范围内,没有触发事件则执行一次事件处理程序内的代码,防抖函数就是让事件触发后的第n秒执行一次函数,在不断触发的过程中阻止执行
function debounce(fn, delay) {
let timer = null
return function () {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arguments)
clearTimeout(timer)
timer = null
}, delay)
}
}
//立即执行防抖
function debounce(fn, delay, immediate) {
let timer = null
return function () {
if (timer) clearTimeout(timer)
if (immediate) {
fn.apply(this, arguments)
immediate = false
}
timer = setTimeout(() => {
fn.apply(this, arguments)
clearTimeout(timer)
timer = null
}, delay)
}
}
节流函数:限制在指定时间范围内只能触发事件处理程序内代码只执行一次,如果一个事件被频繁触发,节流函数可以规定事件处理程序内代码按照固定的频率执行。可以理解成,防抖函数是回城时被打断,再次回城需要再次读秒;节流是技能释放,需要冷却时间,冷却时间内无法释放。
// setTimeout节流
function throttle(fn, delay) {
let timer = null
return function () {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments)
clearTimeout(timer)
timer = null
}, delay)
}
}
}
// 时间差节流
function throttle2(fn, delay) {
let initTimes = new Date().getTime()
return function () {
const nowTimes = new Date().getTime()
if (nowTimes - initTimes > delay) {
fn.apply(this, arguments)
initTimes = nowTimes
}
}
}
10.说一说事件冒泡和事件捕获两者的区别
事件发生时会在目标节点和根节点之间按照特定的顺序传播,传播所经过的路径上的所有节点都会接收到这个事件,这个传播过程就是事件流。
事件流分为两类,一类是冒泡事件流、一类是捕获事件流。冒泡流和捕获流两者接收到的事件的顺序是完全相反的
冒泡流的接收顺序是,事件从目标节点逐级外层所有的父节点传播,知道最外层的document文档节点
捕获流的接收顺序是,事件从最外层document文档节点逐级向内层传递,直到目标节点
基于冒泡的特性,可以实现事件委托,从而提高页面的性能。当一个父元素有多个子节点都要绑定一个事件时,可以将这个事件绑定在父元素上,点击某个子元素,由于事件冒泡的特性会触发该事件。事件委托可以减少监听数,减少内存消耗 从而提高性能。
11.for...in和for...of有什么区别
通过for...in可以对普通对象遍历其可枚举属性获取键名 通过for...of可以对可迭代对象进行循环迭代获取其键值
for...in 作用于普通对象,并且遍历的是可枚举属性,包括原型上的属性 for...of作用于可迭代对象,object不是可迭代对象
Array、 Map、 Set、 String,这些都是可迭代对象
通常用for...in遍历对象,for...of遍历数组
12.什么是类数组,请列举出你在开发中遇见过哪些类数组
类数组是索引不为负数,且具有length属性的对象,类数组不能使用数组的方法,如 map()、push()等方法
普通函数中的arguments是类数组,获取到的元素集合也是类数组,通过querySelectorAll()等方法获取
通过一些手段可以将类数组转换成数组
13.如何将一个类数组转换成数组
通过 call 调用数组的 slice 方法来实现转换Array.prototype.slice.call(arrayLike)
通过 call 调用数组的 splice 方法来实现转换Array.prototype.splice.call(arrayLike, 0)
通过 apply 调用数组的 concat 方法来实现转换Array.prototype.concat.apply([], arrayLike)
通过 Array.from 方法来实现转换Array.from(arrayLike)
14.列举数组去重的几种方案
利用indexOf,定义一个空的新数组,遍历源数组,每次都判断新数组中是否存在当前项,没有就把当前项push进这个新数组中
利用includes方法实现,原理同indexOf一样,也是判断新数组中是否包含当前元素项,没有就push进去
利用Set的成员值的唯一性实现数组的去重,把要去重的数组作为new Set()的参数,然后再利用结构或者Array.from()转为数组
双重循环实现数组去重
相邻元素去重,先将数组进行排序,然后for循环数组,判断相邻元素是否相等,不相等则push进空数组,相等则忽略