关于前端面试的题,最近整理了一些干货,经常被问到的一些问题,出现频率比较高的问题,如有不足之处,请高调指出,(⭐代表难度,星星越多越难,以次类推)
1、typeof
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof undefined); // undefined
console.log(typeof []); // object
console.log(typeof{}); // object
console.log(typeof function(){}); //function
console.log(typeof null); // object
// 优点:能够快速区分基本数据类型 + function
// 缺点:不能将Object、Array和Null区分,都返回object
2、instanceof
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
//优点:能够区分Array、Object和Function,适合用于判断自定义的类实例对象
//缺点:Number,Boolean,String基本数据类型不能判断
3、Object.prototype.toString.call()
console.log(toString.call(2)); //[object Number]
console.log(toString.call(true)); //[object Boolean]
console.log(toString.call('str')); //[object String]
console.log(toString.call([])); //[object Array]
console.log(toString.call(function(){})); //[object Function]
console.log(toString.call({})); //[object Object]
console.log(toString.call(undefined)); //[object Undefined]
console.log(toString.call(null)); //[object Null]
//优点:精准判断数据类型
//缺点:写法繁琐不容易记,推荐进行封装后使用
叫做相等运算符,= 叫做严格运算符,
前者会自动转换类型,再判断是否相等,后者不会自动类型转换,直接去比较
undefined | null |
---|---|
用于变量、属性和方法 | 用于对象 |
空值 | 空值 |
变量未没有定义 | 定义未指向 |
隐藏式 | 声明式 |
typeof null // ‘object’ | typeof undefined // ‘undefined’ |
null == undefined // true
null === undefined // false
!!null === !!undefined // true
可参考文献:https://zhuanlan.zhihu.com/p/463090509
var | let | const | |
---|---|---|---|
声明变量 | 声明变量 | 声明变量\常量 | |
变量提升 | 存在 | 不存在 | 不存在 |
暂时性死区 | 不存在 | 存在 | 存在 |
重复声明 | 不允许 | 不允许 | 不允许 |
块级作用域 | 不存在 | 存在 | 存在 |
tip:其实在JS中只要是声明一个变量,就会存在变量的提升,但是为什么大家都会说,let和const不存在提升呢,其实是因为在ES6 中明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错
也就是暂时性死区(temporal dead zone,简称 TDZ),其实是TDZ组织了变量的提升,不能说他没有提升,只是用报错的方式阻止了。
可参考文献:https://es6.ruanyifeng.com/#docs/let
因为 对象和数组是引用数据类型 ,我们使用const定义的对象保存的仅是对象在栈中的指针,这就意味着,const仅保证指针不发生改变,修改对象的值不会改变对象的指针,所以是被允许的。
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
浅拷贝只复制指向某个对象的地址,而不复制对象本身,新旧对象还是共享在堆中的内存,相互影响。
1、序列化和反序列
JSON.parse( JSON.stringify() )
JSON在执行字符串化的这个过程时,会先进行一个JSON格式化
,获得安全的JSON值。因此如果是非安全的JSON值,就会被丢弃掉。其中undefined、function、symbol
这三种类型的值就是非安全的(包括该对象的属性循环赋值该对象),所以格式化后,就被过滤掉了,而set、map
这种数据格式的对象,也并没有被正确处理,而是处理成了一个空对象。
2、 Object.assign(target, source1, source2)
es6新增的方法,可用于对象合并,将源对象的所有可枚举属性,复制到目标对象上。
3、迭代递归方法
扩展运算符只是部分深拷贝,只是对第一层进行了深拷贝,其他都是浅拷贝
栈(stack):
堆(heap):
基本数据类型:Undefined,String,Boolean,Null,Number,都是直接按值存放在栈内存中,占用的内存空间的大小是确定的,并由系统自动分配和自动释放。这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间且采用值传递。
引用数据类型:指那些可能由多个值构成的对象,如对象(Object)、数组(Array)、函数(Function) ,它们是通过拷贝和new出来的,这样的数据存储于堆中且采用地址传递。
JS 的 事件执行机制 先同步 —> 所有异步 微任务 —> 异步宏任务 异步 微任务:promise 回调(then , .catch)
异步 宏任务: setTimeOut SetInterval 注意: 有哪些是 一创建就立即执行的
事件循环
Event Loop(事件循环)中,每一次循环称为 tick, 每一次tick的任务如下:
执行栈选择最先进入队列的宏任务(通常是script整体代码),如果有则执行 检查是否存在 Microtask,如果存在则不停的执行,直至清空
microtask 队列 更新render(每一次事件循环,浏览器都可能会去更新渲染) 重复以上步骤
console.log(1)
setTimeout(() => {
console.log("2")
}, 0)
console.log(3)
let p = new Promise((resolve, reject) => {
console.log(4)
resolve("此处为成功的 信息")
})
p.then(
(res) => {
console.log(5)
//此处为接收成功的信息
},
(res) => {
console.log('6 :>> ', 6)
//此处为接收失败的信息
}
)
console.log(7)
//1,3,4,7,5,2
概念:闭包是在另一个函数(称为父函数)中定义的函数,并且可以访问在父函数作用域中声明和定义的变量。
优点:
1.可以让我们函数外部能够访问到函数内部的变量。
2.闭包函数保留了使用的变量对象的引用,保证变量对象不会被回收
缺点:不当的时用会造成内存泄漏
这里要补充一下,目前大部分浏览器真实开发场景下,闭包并不会引起内存泄漏,只是由于IE9之前的版本对JavaScript对象和COM对象(IE9之前BOM和DOM中的对象是C++实现的组件对象模型对象,简称COM对象)使用不同的垃圾收集,从而导致内存无法进行回收,这是IE的问题,所以闭包和内存泄漏没半毛钱关系。
防抖是控制次数,节流是控制频率
1.防抖 - 防抖是控制次数
function debounce(func, delay) {
var timeout;
return function(e) {
console.log("清除",timeout,e.target.value)
clearTimeout(timeout);
var context = this, args = arguments
console.log("新的",timeout, e.target.value)
timeout = setTimeout(function(){
console.log("----")
func.apply(context, args);
},delay)
};
};
var validate = debounce(function(e) {
console.log("change", e.target.value, new Date-0)
}, 380);
// 绑定监听
document.querySelector("input").addEventListener('input', validate);
2.节流 - 节流是控制频率
function throttle(fn, threshhold) {
var timeout
var start = new Date;
var threshhold = threshhold || 160
return function () {
var context = this, args = arguments, curr = new Date() - 0
clearTimeout(timeout)//总是干掉事件回调
if(curr - start >= threshhold){
console.log("now", curr, curr - start)//注意这里相减的结果,都差不多是160左右
fn.apply(context, args) //只执行一部分方法,这些方法是在某个时间段内执行一次
start = curr
}else{
//让方法在脱离事件后也能执行一次
timeout = setTimeout(function(){
fn.apply(context, args)
}, threshhold);
}
}
}
var mousemove = throttle(function(e) {
console.log(e.pageX, e.pageY)
});
// 绑定监听
document.querySelector("#panel").addEventListener('mousemove', mousemove);
内存泄漏可以定义为程序不再使用或不需要的一块内存,但是由于某种原因没有被释放仍然被不必要的占有。在代码中创建对象和变量会占用内存,但是javaScript是有自己的内存回收机制,可以确定那些变量不再需要,并将其清除。但是当你的代码存在逻辑缺陷的时候,你以为你已经不需要,但是程序中还存在着引用,导致程序运行完后并没有合适的回收所占用的空间,导致内存不断的占用,运行的时间越长占用的就越多,随之出现的是,性能不佳,高延迟,频繁崩溃。‘
常见的四种内存泄漏(当然,不止这四种咯)
JS的垃圾回收机制是为了以防内存泄漏,内存泄漏的含义就是当已经不需要某块内存时这块内存还存在着,垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,并释放掉它们所指向的内存。
每一个对象都与另一个对象相关联,那个关联的对象就称为原型。
每一个对象,都有一个原型对象与之关联,这个原型对象它也是一个普通对象,这个普通对象也有自己的原型对象,这样层层递进,就形成了一个链条,这个链条就是原型链。通过原型链可以实现JS的继承,把父类的原型对象赋值给子类的原型,这样子类实例就可以访问父类原型上的方法了。
1、原型链继承
function Parent() {
this.name = 'parent'
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child'
}
Child.prototype = new Parent()
console.log(new Child())
缺陷:声明多个去继承同一个,他们会共享同一个原型,内存空间也是共享的,当一个发生变化的时候,另外一个也随之进行了变化。
2、构造函数继承(借助 call)
function Parent(){
this.name = 'parent'
}
Parent.prototype.getName = function () {
return this.name
}
function Child(){
Parent.call(this)
this.type = 'child'
}
let child = new Child()
console.log(child) // 没问题
console.log(child.getName()) // 会报错
缺陷:从上面的结果就可以看到构造函数实现继承的优缺点,它使父类的引用属性不会被共享,优化了第一种继承方式的弊端;但是随之而来的缺点也比较明显——只能继承父类的实例属性和方法,不能继承原型属性或者方法
。
3、组合继承(前两种组合)
function Parent () {
this.name = 'parent'
this.play = [1, 2, 3]
}
Parent.prototype.getName = function () {
return this.name
}
function Child() {
Parent.call(this)
this.type = 'child'
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
var s1 = new Child()
var s2 = new Child()
s1.play.push(4)
console.log(s1.play, s2.play) // 不互相影响
console.log(s1.getName()) // 正常输出'parent'
console.log(s2.getName()) // 正常输出'parent'
4、原型式继承
5、寄生式继承
6、寄生组合式继承
7、ES6 的 extends 关键字实现逻辑
(1) 创建一个新对象
(2) 将构造函数中的this指向该对象
(3) 执行构造函数中的代码(为这个新对象添加属性)
(4) 返回新对象
扩展代码块(手写new)
function _new(obj, ...rest){
// 基于obj的原型创建一个新的对象
const newObj = Object.create(obj.prototype)
// 添加属性到新创建的newObj上, 并获取obj函数执行的结果.
const result = obj.apply(newObj, rest)
// 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象
return typeof result === 'object' ? result : newObj
}
1、写法不同
2、箭头函数都是匿名函数,普通函数可以是别的
3、箭头函数不能被new,因为他没有自己的this
4、箭头函数本身没有this,他会捕捉上下文的this供自己使用且任何方法都改变不了其指向,如 call() , bind() , apply()
自身没有this
this的指向是根据调用的上下文来决定的,默认指向window对象,指向window对象时可以省略不写
全局环境下
局部环境下
Function.prototype.call = function () {
const [ctx, ...args] = arguments;
ctx.fn = this || window;
const result = ctx.fn(...args);
delete ctx.fn;
return result;
}
Function.prototype.apply = function () {
const [ctx, args] = arguments; // args区别
ctx.fn = this || window;
const result = ctx.fn(...args);
delete ctx.fn;
return result;
}
push、pop、shift、unshift、sort等等
参考文献:https://www.w3school.com.cn/jsref/jsref_obj_array.asp
foreach参考文献:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
map参考文献:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/map
nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复
出现。例:输入nums [2,7,9,11] target 11 ,输出[0,2]⭐⭐⭐