1、bind函数的实现过程
// 简化实现,完整版实现中的第 2 步
Function.prototype.bind = function (context) {var self = this;// 第 1 个参数是指定的 this,截取保存第 1 个之后的参数// arr.slice(begin); 即 [begin, end]var args = Array.prototype.slice.call(arguments, 1); return function () {// 此时的 arguments 是指 bind 返回的函数调用时接收的参数// 即 return function 的参数,和上面那个不同// 类数组转成数组var bindArgs = Array.prototype.slice.call(arguments);// 执行函数return self.apply( context, args.concat(bindArgs) );}
}
2、高阶函数、函数柯里化
简单来说,高阶函数是一个接受函数作为参数传递或者将函数作为返回值输出的函数。
一个经常用到的柯里化函数:
var curry = function (fn) {var args = [].slice.call(arguments, 1);return function() {var newArgs = args.concat([].slice.call(arguments));return fn.apply(this, newArgs);};
};
3、节流
函数节流,指的是在一定的时间间隔内(比如说3秒),只执行一次,在这三秒内无视后来产生的函数调用请求,也不会延长时间间隔。3秒间隔结束后,第一遇到新的函数调用会触发执行,然后在这新的3秒内依旧无视后来产生的函数调用请求,以此类推。
- 应用场景:非常适用于函数被频繁调用的场景,例如:window.onresize事件,mousemove事件,上传进度等等。
- 实现方案一:用时间戳来实现
// fn 是需要执行的函数
// wait 是时间间隔
const throttle = (fn, wait = 50) => {// 上一次执行 fn 的时间let previous = 0// 将 throttle 处理结果当作函数返回return function(...args) {// 获取当前时间,转换成时间戳,单位毫秒let now = +new Date()// 将当前时间和上一次执行函数的时间进行对比// 大于等待时间就把 previous 设置为当前时间并执行函数 fnif (now - previous > wait) {previous = nowfn.apply(this, args)}}
}// DEMO
// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log('fn 函数执行了'), 1000)
// 每 10 毫秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(betterFn, 10)
- 实现方案二:用定时器来实现
//原始函数function add(a:any, b: any) {console.log(a + b, '****本身调用', +new Date());return a + b;};//节流函数const throttle = (fn: any, wait = 50) => {let timer: any = null;return (...args: any) => {if (!timer) {timer = setTimeout(() => {fn.apply(this, [...args])timer = null;}, wait)}}}// betterFn就是节流之后返回的新函数const betterFn = throttle(add, 2000);setInterval(() => {// 如果在此处直接调用add,会出新add频繁触发的情况,加上throttle之后,明显情况好多了betterFn(1, 1)}, 200)
4、防抖
防抖函数debounce指的是某个 函数在某段时间内,无论触发了多少次回调,都只执行最后一次。假如我们设置了一个等待时间3秒的函数,在这3秒内如果遇到函数调用请求就重新计时3秒,直至新的3秒内没有函数请求,此时执行函数,不然就以此类推重新计时。
//防抖const debounce = (fn: any, wait = 50) => {let timer: any = null;return (...args: any) => {if (timer) {clearTimeout(timer)}timer = setTimeout(() => {fn.apply(this, args);}, wait);};};// betterFn就是防抖之后返回的新函数const betterFn = debounce(() => console.log('****执行了', +new Date()), 2000);document.addEventListener('resize', betterFn)
5、null和undefined的本质区别是什么?
(1)从类型来说:
- null的数据类型是object,typeof null == object,会被隐式转换成0,不容易发现错误;
- undefined是一个基本数据类型,转换成数值为NaN;
(2)从变量赋值来说
- 给一个全局变量赋值为null,相当于将这个变量的指针对象以及值清空,如果给对象的属性赋值为null,或者局部变量赋值为null,相当于给这个属性分配了一块空的内存,然后值为null,JS会回收全局变量为null的对象;
- 给一个全局变量赋值为undefined,相当于将这个对象的值清空,但是这个对象依旧存在,如果给对象的属性赋值为undefined,说明这个值为空;
总结:
null,已经声明赋值为空;引用数据类型;数值转换成0。
undefined,已经声明未赋值;基本数据类型;数值转换成NaN。
6、ES6语法中的const声明一个只读的常量,那为什么可以修改const的值?
const foo = {};// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123// 将 foo 指向另一个对象,就会报错
foo = { name: '123' }; // TypeError: "foo" is read-only
解答:
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
对于简单类型的数据(number,string,boolean),值就是保存在变量指向的那个内存地址,因此等同于常量。
但是对于复合数据类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。
因此将一个对象声明为常量必须非常小心。
7、JS数据类型
(1)分为两大类:基本数据类型和引用数据类型
- 基本数据类型:Number,String,Boolean,Null,Undefined,Symbol,BigInt
- 引用数据类型:Object(json,Array,function)
(2)两种数据类型存放机制
- 基本数据类型,体积小,放在栈内存里面
- 引用数据类型,体积大,放在堆内存里面
- 引用数据类型会有一个指针放在栈内存里面,用来定位堆里面存放的引用类型的数据
(3)如何判断这两种数据类型
- typeof 用来查找基本数据类型,引用类型除了function外,其他的都为object
- instanceof 用来查找引用数据类型;原理:instanceof在查找的时候会遍历左边变量的原型链,直到找到右边变量的prototype,找得到就返回true,找不到就返回false
- Object.prototype.toString().call() 所有数据类型,原型链和原型有关,首先toString()这个方法本来应该返回string的字符串形式,但是大多数情况会返回[object,****]这种形式,因为js重写了某些数据类型的toString方法;
const var1 = typeof 123;const var2 = typeof '哈哈哈';const var3 = typeof true;console.log(var1); // numberconsole.log(var2); // stringconsole.log(var3); // booleanconst date1 = new Date()const var4 = date1 instanceof Date;const var5 = date1 instanceof Array;console.log(var4); // trueconsole.log(var5); // falseconst arr1 = [1, 2];const obj1 = { name: '张三' };const fn1 = () => {};const str1 = 'I am a string';const var6 = Object.prototype.toString.call(arr1);const var7 = Object.prototype.toString.call(obj1);const var8 = Object.prototype.toString.call(fn1);const var9 = Object.prototype.toString.call(str1);console.log(var6); // [object Array]console.log(var7); // [object Object]console.log(var8); // [object Function]console.log(var9); // [object String]
(4)数据类型如何转换
- 转换为字符串:toString()/String()
- 转换为数字类型:Number()/ParseInt()/ParseFloat()
- 转换为布尔值:Boolean()/在实际的开发中,也会调用双感叹号强制转换成布尔类型
- 隐式转换:js是一门弱类型语言,运行期间,会根据运行环境自动类型转换
const var1 = String(123);const var2 = !!'哈哈哈';const var3 = Number(true);const var4 = '人民' + 899;console.log(var1); // '123'console.log(var2); // trueconsole.log(var3); // 1console.log(var4); // '人民899'
8、JS的事件循环(Event Loop)
(1)首先得解释一下单线程
js是单线程语言,用途决定了它必须是单线程语言;单线程指同一时间只能做一件事。
(2)然后解释一下代码执行流程
同步任务执行完毕,再去执行异步任务,异步任务分宏任务和微任务;如此循环往复,就构成了事件循环
(3)解释一下事件循环
常见的宏任务:setTimeout,setInterval,I/O操作,UI渲染等等
常见的微任务:Promise.then(回调),MutationObserver等等
执行顺序:
先执行同步任务、再执行微任务、最后执行宏任务(宏任务里面还有微任务,先执行微任务);同步任务、微任务、宏任务;同步任务、微任务、宏任务......如此循环往复.....
记住:要执行宏任务的前提是清空了所有微任务;
function test () {console.log('start')setTimeout(() => {console.log('children2')Promise.resolve().then(() => {console.log('children2-1')})}, 0)setTimeout(() => {console.log('children3')Promise.resolve().then(() => {console.log('children3-1')})}, 0)Promise.resolve().then(() => {console.log('children1')})console.log('end')}test()/*
以上代码在浏览器的执行顺序是:
* start
* end
* children1
* children2
* children2-1
* children3
* children3-1
* */
9、讲一下前端的深拷贝和浅拷贝
因为js的数据类型分两大类,基本数据类型和引用数据类型,在进行变量赋值的时候,分为下面两部分:
- 基本数据类型:赋值,赋值之后两个变量互不影响
- 引用数据类型:赋址,两个变量具有相同的引用,指向同一个对象,相互之间有影响
其实,简单来说,浅拷贝就是把变量A的指针赋值给了变量B,变量A和变量B本质上指向的是同一块内存;而深拷贝,是在内存中重新开辟出来一块新的空间给变量B。
看代码比较直观,如下所示:
let a = 'Jesus Loves You';let b = a;console.log(b);// Jesus Loves Youa = 'God Loves You'console.log(a);// God Loves Youconsole.log(b);// Jesus Loves You/*从上面的变动中可以看出,a的改动并没有影响到b,因为a为基本数据类型*/
在来看另外一组代码,如下所示:
let a = {name: 'Holy Bible',book: {title: 'God Loves Everyone',price: 45,},}let b = a;console.log(JSON.stringify(b));// {"name":"Holy Bible","book":{"title":"God Loves Everyone","price":45}}a.name = 'Other Book';a.book.price = 55;console.log(JSON.stringify(a));// {"name":"Other Book","book":{"title":"God Loves Everyone","price":55}}console.log(JSON.stringify(b));// {"name":"Other Book","book":{"title":"God Loves Everyone","price":55}}/*从上面的变动中可以看出,a的改动影响到了b,因为a为引用数据类型*/
(1)常用的浅拷贝方式:
- Object.assign()
- 展开语法Spread
- Array.prototype.slice()
(2)常用的深拷贝方式:
JSON.parse(JSON.stringify(object))
这个深拷贝的方式有bug,不建议在日常的开发中使用,会有以下几个问题:
会忽略undefined,会忽略symbol,不能序列化函数,不能解决循环引用的对象,不能正确处理new Date(),不能处理正则