JavaScript 高频面试题

JavaScript

手写防抖和节流

目的:控制事件处理函数的执行频率

  • 函数节流(throttle):控制事件执行的时间间隔,在函数需要频繁触发时: 函数执行一次后,只有大于设定的执行周期后才会执行第二次
    • 适合多次事件按时间做平均分配触发:窗口调整(resize)+ 页面滚动(scroll)等
  • 函数防抖(debounce):触发事件后不会立即执行,需要等待wait时间,如果在等待的过程中再一次触发了事件,计时器wait重新开始计时,直到达到wait时间后执行最后一次的回调。在函数需要频繁触发时: 在规定时间内,只让最后一次生效,前面的不生效。
    • 适合多次事件一次响应的情况:输入框实时搜索联想(keyup/input)

函数节流throttle(callback, time)
返回值:返回值为callback函数,该函数参数为event对象

1.需要一个变量记录上一次执行的时间,才能判断出是否满足执行的时间间隔
2.如果满足执行的时间间隔,则执行函数

//使用形式
c.addEventListener('scroll',throttle(()=>{},500))
//实现
//定义时立刻执行,this执行window
function throttle(callback,time){
	let pre = 0;
	//返回函数使用了闭包,闭包会永远在内存中保存所以这个pre都是记录的上一次的结果
	return function(event){  //返回函数的this指向DOM
		const current = Date.now();
		 if (current - pre > wait) {
		 	//callback()是window调用的,修改this的目的是让函数的指向指向绑定事件的DOM
			callback.call(this,event);
			pre = current;		
		 }
	}
}

注意点
1.返回函数使用了闭包,闭包会永远在内存中保存所以这个pre都是记录的上一次的结果
2.修改this的目的是让函数的指向指向绑定事件的DOM

函数防抖debounce(callback,wait)
1.函数执行后返回一个函数
2.利用定时器实现time秒后执行callback函数
3.将timeId缓存起来。执行函数时,如果timeId存在,说明之前已经有定时器,那么清除之前的定时器,开启新的定时器。

//使用形式
window.addEventListener('scroll',debounce(()=>{},500))

function debounce(callback,time){
    let timeId = null;//缓存timeId
	return function(event){//防抖一般用于事件绑定,所以需要接受event
        if(timeId!== null){//说明已经开启了一个定时器,所以需要清空上一次的
            clearTimeout(timeId);
        }
		/*
        启动定时器,定时器执行后里面的回调是异步执行的
        setTimeout返回值是立即返回的
		*/
        timeId = setTimeout(()=>{
			callback.call(this,event);//回调函数调用之后清除timeId
            timeId = null;  //执行成功之后,重置timeId
		},time)   
	}
}

CommonJS和ES6模块的区别

浏览器-ES6模块
node端-CommonJS

CommonJS的 require 语法是同步的,当我们使用require加载一个模块的时候,会执行被加载模块中的代码。

区别

  • CommonJS模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
    值拷贝: 输出了某个值,如果模块内部后续的变化,影响不了外部对这个值的使用。
  • CommonJS对于模块的依赖,CommonJS是动态的,ES6 Module 是静态
    动态是指对于模块的依赖关系建立在代码执行阶段; 静态是指对于模块的依赖关系建立在代码编译阶段

this 指向问题,如何修改this指向

  • 一般函数:在调用时向函数传递执行上下文对象
    • 以函数形式调用,指向window
    • 以方法形式调用,this指向调用的方法
    • 以构造函数的形式调用,this是新创建的对象
  • 箭头函数:本身没有this,它的this可以沿作用域链(定义时就确定了的)查找

如何修改this指向?

区别callapplybind
调用函数×
参数从第二个参数开始依次传递封装成数组传递从第二个参数开始依次传递

注意
bind函数特殊点:多次绑定,只指向第一次绑定的obj对象

引申1:模拟bind

使用
1.bind返回值是一个函数
2.f函数参数可以从两个位置传递,第一个是bind函数,第二个是bind返回的函数

let obj ={name:"ranan"}
function f(data,data2) {
    console.log(this);
    console.log(data + data2);
    return data+data2
}
let bar=f.bind(obj,21) //bind不调用函数
bar(33)//{name:"ranan"} 54

思路
1.返回值是函数bar,返回的函数也可以接受参数
2.获取调用的函数f,让obj也有该函数,这样obj才可以调用
3.当返回值函数bar被调用时,传入两个参数
4.删除obj上的f函数
5.返回bar函数的调用值

//1.参数个数不确定先都封装成数组
Function.prototype.bind = function (obj,...args){
	//1.返回的函数也可以接受参数
	return (...agrs2) =>{
	 	//处理obj是undefined与null的情况
    	if (obj === undefined || obj === null) {
        	obj = window
    	}
		//2.获取调用的函数,让obj也有该函数
		obj.temFun = this;
		//3.当返回值函数被调用时,传入两个参数
		let res = obj.temFun(...args,...agrs2);
		delete obj.temFun;
		return res;
		// return this.call(obj,...args,...args2) 精简写法
	} 
}

引申2:为什么多次绑定,只指向第一次绑定的obj对象?有没有解决办法?

问题描述

function testBind() {
    console.log(this.value);
}
obj = {
     value: 10
 };
 obj1 = {
     value: 100
 };
 obj2 = {
     value: 1000
 };
 p = testBind.bind(obj).bind(obj1);
 p();

1.testBind.bind(obj) 返回了函数先命名函数为fn,()=>{return testBind.call(obj)}
2.fn.bind(obj1) 返回了函数fn2,()=>{return fn.call(obj1)}
3.先执行函数fn2 => 返回 fn.call(obj1) 所以需要执行函数fn => 返回 testBind.call(obj),所以需要执行testBind,此时this指向obj。所以控制台最终输出10

bind多次绑定是无效的,只会输出第一次绑定的结果

解决办法
思路:如何将后面绑定的对象穿透给第一次的bind?
将要绑定的运行上下文对象直接绑定在一个全局属性,不管bind几次,最后调用的都是最后一次调用的函数

Function.prototype.bind = function (obj){
        var that = this
        arg = obj //保存在全局属性,每次调用bind更新obj,最后调用就是最后绑定的obj
        return function(){
             return that.call(arg);
        }
}

引申3:一般函数和箭头函数的区别

区别一般函数箭头函数
this指向调用时确定定义时确定,没有自己的this,沿着作用域链找父级的this
改变this指向call,apply,bind不能改变,静态
arguments没有,可以用rest参数代替
作为构造函数× 没有prototype属性
匿名函数可以匿名可以不匿名匿名函数

数据类型有哪些

基本数据类型
String:任意字符串
Numberr:任意数字
Boolean:true/false
Undefined:undefined
Null:null

引用数据类型
Object:任意对象
Function: 特别对象,可以执行
Array:特别对象,内部数据有序

undefined与null的区别
undefined:定义了没有赋值
null:定义了赋值了值为null

引申1:如何判断数据类型

方法描述注意点
typeof主要是针对基本数据类型引用数据类型->Object
null不能识别 null->Object
Function可以识别
===判断值和数据类型是否相等,可用来判断null和undefinedxxx===undefined
xxx===null
A instanceof B判单A是否是B的实例根据A的隐式原型链找是否有B的原型
Object.prototype.toString可以判断出内置属性这是Object原型上的方法,其他数据类型的toString可能被重写,所以需要使用Object.prototype.toString.call调用

new操作的原理

1.立刻创建一个新的对象
2.将该对象的__proto__指向构造器的prototype
3.将函数中的this设置成新建对象,在构造函数中可以使用this来引用新建的对象
3.逐行执行函数中的代码
4.将新建的对象作为返回值返回,如果构造函数本身的返回值是对象,则返回对象

function myNew(Fn,...args){
  let obj = {}; //1.定义空对象
  //1.获取构造函数,将该对象的__proto__指向构造器的prototype属性
  obj.__proto__ = Fn.prototype;
 //2.将新建的对象设置为函数中this,在构造函数中可以使用this来引用新建的对象
 //3.逐行执行函数中的代码,如果有返回值记录返回值
  let res = Fn.apply(obj,args);
  return res instanceof Object ? res : obj;//4.如果有返回值直接返回且为对象直接返回,如果没有则返回新建的对象
}

//测试
function fo(name,age) {
   this.name=name;
   this.age=age;
   console.log("name:"+this.name);
   console.log("age:"+this.age);
}
//第一个参数传入构造函数,第二个传入参数
let obj = myNew (fo,'ranan',18);

引申1:什么是闭包?

什么是闭包?
闭包就是能够读取其他函数内部变量的函数,由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的作用

  1. 可以读取函数内部的变量
  2. 使函数的内部变量执行完后,仍然存活在内存中,延长了局部变量的生命周期。
    JavaScript闭包就是在另一个作用域中保存了一份它从上一级函数或者作用域得到的变量,而这些变量是不会随上一级函数的执行完成而销毁

闭包的缺点
1.函数执行完后,函数内的局部变量没有释放,占用内存时间会变长
2.容易造成内存泄露
解决办法
及时释放:让内部函数成为垃圾对象–> 回收闭包

引申2:内存泄露和内存溢出

内存溢出
想象成水杯,满了就会溢出
是一种程序运行出现的错误

内存泄露
占用的内存没有及时释放
内存泄露积累多了就容易导致内存溢出

1.没有及时清理的计时器或回调函数
2.意外的全局变量
3.闭包

引申3:作用域和作用域链?

作用域
理解:一个代码段所在的区域,是静态的,在编写代码时就确定了。
作用:隔离变量,不同作用域下同名变量不会有冲突

分类
1.全局作用域
2.函数作用域
3.块作用域(ES6开始有)

作用域链
多个作用域嵌套,就近选择,先在自己作用域找,然后去就近的作用域找。

作用域链主要用于变量的查找的,当前作用域没有找到,可以去他嵌套的作用域上找。

JS 实现数组扁平化

深拷贝与浅拷贝的区别

深浅拷贝只是针对引用数据类型
复制之后的副本进行修改会不会影响到原来的

浅拷贝:修改拷贝以后的数据会影响原数据,拷贝的引用。使得原数据不安全。(只拷贝一层)
深拷贝:修改拷贝以后的数据不会影响原数据,拷贝的时候生成新数据。

引申1:如何实现深拷贝 递归+Map

实现思路
1.判断是否是引用类型,如果是引用类型循环遍历所有元素进行复制
2.保证对象只克隆了一次,使用Map存储已经克隆之后的对象,目的是:防止循环引用时死循环,A引用B,B中又引用了A,防止套娃

//参数1:本层需要复制的对象target,参数2:全局存储已经克隆的对象
function deepClone(target,map=new Map()){
	//1.判断是否是引用类型,如果是引用类型循环遍历所有元素
	if(typeof target==='object' && target!== null){
	//2.先判断是否已经克隆过了?如果克隆过了直接返回之前的克隆结果
	let cache= map.get(target);
	if(cache)return cache;
	//2.说明没有克隆过,进行克隆后放入克隆结果,key为原对象,result为克隆的对象
	let isAtrray = Array.isArray(target);
	const result =isAtrray ?[]:{};
	map.set(target,result);
	if(isAtrray){//如果是数组
		target.forEach((item.index)=>{
			result[index] = deepClone(item,map);//让数组中的每个元素都是深拷贝的
		})
	else{//遍历对象
		Object.keys(target).forEach((key,index)=>{
                result[key] = deepClone(target[key],map);//让对象中的每个结果都是深拷贝的
       })
	}
	return result;//将拷贝结果返回
	}else{//1.说明是基本数据类型,返回深克隆的结果,这里是数据本身
	 	return target;
	}
}

原型与原型链

原型
1.每个函数都有prototype显式原型属性,在定义函数时自动添加的,默认是一个空Object对象,这个对象叫做原型对象
2.实例对象都有__proto__隐式原型属性,对象的隐式原型的值=对应构造函数的显式原型的值
原型的作用
给原型对象添加方法,函数的所有实例对象自动拥有原型中的方法,通过__proto__属性

原型链 隐式原型链
访问一个对象的属性时
1.现在自身属性中查找,找到返回
2.没有找到,再沿__proto__隐式原型这条链上找,找到返回
3.最终没找到,返回undefined

原型链的尽头
Object的prototype原型的隐式原型__proto__ 为null,也就是原型链的尽头。

特殊的
Function 可以看成是构造函数,也可以看成Function的实例
Object 可以看成构造函数,也可以看成Function的实例

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值