js相关
1.事件循环
js是单线程的,代码会从上到下依次执行,那么如果在遇到耗时比较大的任务时就会阻塞下面代码的执行,所以将js中的任务分为同步任务和异步任务。同步任务会立即执行,而异步任务会放到浏览器渲染进程中的其他线程进行执行,执行完毕后推入异步任务队列,等同步任务执行完毕后进行执行。如此循环往复。
而异步任务又被分为宏任务和微任务,宏任务主要包括js整体代码,setTimeout,setInterval等,微任务主要包括Promise,process.nextTick,点击事件等。所以js代码的执行顺序为先执行一个宏任务,等宏任务执行完毕后执行所有可执行的微任务,如此循环往复。
**node中的事件循环:**在浏览器中,事件循环是由浏览器的渲染进程开启一个新的线程去实现的,在node中,事件循环是由libuv去实现的,libuv是一个处理异步任务的C语言实现的一个库,它会开启一个线程池去运行每个异步任务。在node中,事件循环的宏任务主要包括:timer,pending callback,idle prepare,poll,check,close callback,其中比较重要的三个就是timer,pull以及check,其中timer主要是处理定时器相关,包括setTimeout,setInterval。pending callback主要是处理再上一次事件循环中延时处理的pull中的任务,idle prepare主要是node内部调用。pull主要处理文件操作,用户输入输出相关的任务。check主要处理setImmediate任务。而close callback主要处理跟close事件相关的任务。微任务主要包括nextTick以及Promise。在每次事件循环中,先运行一次宏任务,然后再运行所有的微任务。
2.defer和async的区别
两个都同于异步加载js的脚本,defer先下载JavaScript脚本,等待页面解析完毕后再运行脚本,且执行的顺序就是定义defer属性JavaScript标签的顺序,适用于依赖页面元素且脚本之间具有依赖关系的情况。而async会在脚本下载完毕后立即执行,所以执行顺序不确定,适用于一些不依赖于页面元素的一些JavaScript脚本文件。
3.0.1+0.2为什么不等于0.3?
JavaScript中使用Number来表示数字(整数和浮点数),遵循IEEE 754标准,使用64位表示一个数字。即:1 + 11 + 52。1个符号位,11个指数位以及52位小数部分(即有效数字)。
所以最大能表示的安全数字就是Math.pow(2, 53) - 1。为什么是53而不是52,因为在二进制表示的数中自动省略了最高位的1.
在进行小数运算时,首先进行进制的转换,0.1和0.2转换成二进制时会进行无限循环。然后进行对阶,在进制转换和对阶的过程中会进行尾数的截取,导致精度丢失。
解决办法:将小数转换成整数进行运算。或者采用第三方的库math.js,big.js等。
将数字转为整数:
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
4.js中的数据类型
基本数据类型:Number,String,Boolean,null,undefined,symbol(ES6新增),bigint(ES2020)。
引用类型:Object以及Object(对象)的子类型:Array,Function等。
基本数据类型的值存放在栈中,栈是由操作系统进行分配的。而引用数据类型存放在堆中,在栈中存放其在堆中起始位置的地址。
js中数据类型的判断方法:
- typeof:可以判断基本数据类型,但是当判断引用数据类型时无法正确判断。对函数类型结果为function,其它引用数据类型为object。但是对于null类型,结果为object。
- instanceof:可以对引用数据类型做出正确判断,但无法判断基本数据类型。其实现的原理就是判断该对象的原形链上是否有对应类型的prototype属性,即这个对象该构造函数的实例对象。
- constructor:使用prototype中的constructor属性判断对象的数据类型,但是会存在一个弊端,如果使构造函数的prototype的constructor属性指向了一个新的构造函数,那么它的判断会发生错误。
- 判断对象的数据类型还可以使用Object.prototype.toString.call()的方式。
js中判断是否为数组的方法:
- instanceof
- Object.prototype.toString.call
- ES6 isArray
- 根据原型链:arr.proto = Array.prototype
5.&&,||与!!运算符的作用
- &&(逻辑与):如果表达式左值为真则返回表达式右值,否则返回表达式左值。
- ||(逻辑或):如果表达式的左值为真则返回左值,否则返回表达式的右值。
- !!:可以将该表达式的右值转为一个boolean值,这也是将值转为boolean值的一种简单的方法。
6.js中数据类型的转换
js中的数据转换主要分为三个方面:
转为Number:Number(),parseInt(),parseFloat()。
转为String:toString(),String(),"" +隐式转换。
转为Boolean:Boolean(),!!。
7.原型和原型链
原型链简单的理解就是实例对象和原型对象之间形成的一种链式关系。当访问实例对象上的属性和方法时,如果实例对象上没有,就会去这个实例对象的原型对象上去查找,如果原型对象上也没有,就回去原型的原型身上去查找。因为在js中万物皆对象,所以顶层对象为Object,Object的原型对象为null。js通过这种方式实现了类似于类的继承的效果。
实例对象,原型对象以及与构造函数的关系:
实例对象是通过构造函数创建出来的,身上有一个____proto___的属性指向了原型对象。原型对象身上有一个constructor构造器指向了构造函数,而构造函数上有一个prototype属性指向了其原型对象。
js中获取原型对象的方法:
- p.proto
- 构造函数的prototype属性
- Object.getPrototypeOf()方法
8.==和===的区别
==不是严格的相等,在进行操作符比较时会进行数据类型的转换。而===为严格的相等,只要类型不同最终的结果都为false。
类型转换的规则:
- 如果有一个操作数为Boolean值,那么会将其转为Number类型,true转为1,false转为0。
- 如果一个操作数是字符串另一个操作数为数字,则先将字符串转为数字再进行比较。
- 如果一个操作数是对象另一个数是基本数据类型,则会调用对象的valueOf方法将其转为基本数据类型再进行比较。
9.谈谈this,call,apply和bind
js中使用this代表当前的执行环境,主要分为以下几种情况:
- 在函数中,this永远指向最后调用函数的那个对象。
- 在全局作用域下,this代表window对象。
- 在构造函数中,this指向new出来的对象。
- 在箭头函数中,因为箭头函数没有this,所以箭头函数中的this表示箭头函数父作用域中的this。
而call,apply,bind的作用就是改变某一个函数中this的指向。call,apply会立即执行函数,区别就是传入函数参数的方式不同,call是将参数作为单独的参数一个一个传入的而apply传入的是函数参数数组。bind会返回一个函数,只有调用这个函数的时候它才会执行。
10.闭包
闭包简单的来说就是一个函数,在这个函数中可以访问这个函数外部的变量。其实是利用了作用域链的原理,当在本函数的作用域下找不到变量时,会向他的父级作用域中进行查找。而闭包的作用主要有两个,一个是可以在外部访问函数内部所定义的变量,创建私有变量。第二个就是使变量一直保存在内存中,不会被垃圾回收机制回收。但是要注意内存泄露的问题。
11.Ajax
Ajax其实就是异步通信的一种方法,浏览器主动向服务器发送HTTP请求,在收到数据后去更新页面上相应的部分,而不必整体的刷新页面。
原生js实现:
const xhr = new XMLHttpRequest();
xhr.open("get", "index.html", true);
xhr.send(null);
xhr.onreadystatechange = function() {
if (xhr.readySate === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
}
12.js延迟加载的几种方式
js是单线程的,在页面渲染的过程中如果遇到js代码则优先执行js代码,所以js代码的执行会堵塞页面的渲染过程。我们期望js尽可能的最后再加载,提高页面的渲染效率。方法如下:
- 将js放在文档最底部,最后再执行。
- 使用script标签上的defer属性,即异步加载,js脚本的加载和页面的渲染同步进行,当页面渲染完毕后再执行js的脚本代码,最后多个defer标签定义的script执行顺序与定义顺序一致。
- 使用script标签上的async属性,也是异步加载,js脚本的加载和页面的渲染也是同步进行,与defer的区别是当js脚本加载完毕后立即执行,执行顺序与定义顺序可能不同。
- 使用页面的load加载完成事件,当页面加载完成后动态的创建script标签并插入到页面中。
13.js模块化开发及其模块化规范
模块其实就是一些特定功能的js文件的集合。在最初的时候,因为函数具有局部作用域,所以将不同的功能逻辑代码定义为不同的函数,但这样做的弊端就是会造成全局变量的污染。之后还有一种方法就是将模块的变量和功能函数都定义在一个对象中,但是这么做会暴露所有的对象成员。现在普遍采用立即执行函数的方式,即利用闭包的原理,将变量变为函数的私有变量,在函数外部可以访问这个私有变量。
主流的js模块化规范:CommenJs:使用export和require进行导入导出。Es Module:使用import和export进行导入导出。
14.arguments对象是什么
arguments是函数中代表参数的类数组对象,它拥有length属性以及可以通过索引访问每个元素,但是它没有数组的内置方法,如forEach,map,reduce,filter等。但是箭头函数中没有arguments对象,可以使用...args的方式,此时args是一个数组。
将类数组对象转为数组的方法:
function test() {
console.log(arguments);
console.log([...arguments]);
console.log(Array.from(arguments));
console.log(Array.prototype.slice.call(arguments));
}
test(1, 2, 3, 4, 5);
15.Promise的几种状态,可以重复改变吗?
Promise一共有三种状态,分别是:
- Pending:初始状态,即未发生改变之前的状态。
- Fulfilled:操作成功时的状态。
- Rejected:操作失败或发生错误时的状态。
Promise状态的改变是不可逆的,也就是说他只能从Pending状态变为Fullfilled状态或者Rejected状态,而不能再从Fullfilled/Rejected状态变为Pending状态,状态的改变只能发生一次。
Promise的其他用法:
- Promise.reject或Promise.resolve:可以快速的返回一个Promise失败或者成功的状态。
- Promise.all:传入一个Promise的数组,当所有的Promise都变为Fullfilled的状态时,返回的Promise才会变为Fullfilled,如果有一个Promise的状态变为Rejected,那么返回的Promise状态就会变为Rejected。
- Promise.race:传入一个Promise的数组,它总是返回第一个Promise成功/失败的结果,但是其他的Promise还会继续执行,只不过是不关心罢了。
- Promise.allSettled:传入一个Promise的数组,它会返回所有Promise的结果,无论成功还是失败。
Promise和async/await的区别:async/await其实就是Promise的语法糖,但是在进行错误捕获时,Promise可以使用.catch或者.then的第二个参数,而async/await只能使用try...catch。
16.var,let以及const的区别
var与let/const的区别:
-
let/const定义的变量存在暂时性死区。
-
在同一作用域下,var可以重复的定义变量,后面定义的变量值会覆盖前面的变量,但是let/const在同一作用域下不能重复的定义变量。
-
var关键字定义的变量会挂载到window下,而let/const定义的变量却不会(最主要的区别)。
let和const的区别:
let定义的是变量,const定义的是常量,const在定义时必须赋初值,且值不可以改变,对于对象而言,不能赋值给一个新的对象但可以改变对象的属性值(即不能改变对象的引用地址,但可以改变所引用对象的值)。
17.js的作用域及作用域链
在JS中主要存在三种作用域,分别是全局作用域,函数作用域以及块级作用域。
- 全局作用域:在全局下的作用域。
- 函数作用域:函数中的作用域。
- 块级作用域:块级作用域与let/const息息相关,当在一对{}中存在let/const时,其{}中的就是块级作用域。let/const与var不同,会产生暂时性死区。
作用域链就是当在一个作用域中访问某个变量时,如果这个作用域下不存在该变量,那么它会向上一个作用域中查找,如果查找不到,则返回undefined。作用域链的查找范围总是由里向外而不是反过来由外向里。
18.如果后端向前端返回100000条数据,那么前端该如何渲染
如果后端向前端返回大量数据,主要的优化方法有两种:
- 使用requestAnimationFrame和fragment分批渲染数据。使用setTimeout会+出现闪屏的问题,原因:第一个原因是比如写setTimeout 0,那么页面会立即创建Dom元素并进行渲染,但页面的刷新是在16.7ms以后。第二个原因是在执行两个setTimeout之间会进行一次页面渲染,页面渲染的时间比较长,这样就会产生一段空窗期,就会出现闪屏现象。
- 使用虚拟列表:虚拟列表原理很简单,就是使用一个数组去保存所有的需要展示的数据,然后根据所展示区域的变化去决定展示哪些数据。
19.为什么[] == ![]返回的结果为true?
==在进行比较时是值的比较,左右两边的操作数在比较时会进行数据类型的转换。因为!的优先级高于==所以![]的结果是false,再判断[] == false,在进行比较时会先把两边的操作数转换为数字类型,即0 == 0,所以结果为true。
20.ES6新特性
ES6是ECMAScript2015的那个版本,在这个版本进行了重大的更新,添加了好多新的特性。
- let和const关键字
- for...of...
- 模板字符串
- Set/WeakSet
- Map/WeakMap
- Promise
- Proxy代理
- Class
- 函数的扩展:参数的默认值以及箭头函数。
- 对象的扩展:Object.keys,Object.values,Object.assign等
- 模块导入导出import/export
- 解构赋值
21.Map和WeakMap的区别
- 键的类型方面:Map的键可以是任意的数据类型,包括基本数据类型和引用数据类型。WeakMap的键只能是对象类型。
- 引用关系:在Map中,所有键和值的关系都是强引用,也就意味着除非你手动去删除某个键值对,某则它会一直存在于内存中。而在WeakMap中,键和值的引用是弱引用,这和垃圾回收机制相关,如果这个键的引用次数为零,即没有被引用的地方了,则垃圾回收机制会对其进行自动回收。
因为WeakMap需要额外的内存处理机制,所以性能方面比Map要差一点儿。
22.js造成内存泄漏的原因有哪些?
- 闭包:因为闭包会导致所引用的变量一直存在于内存中,容易造成内存泄漏问题。
- 没有及时清除的定时器和事件监听器。
- 游离的Dom引用:Dom元素已经被删了,但还是存在其引用,也会造成内存泄漏问题。
- 如果变量没有定义就直接使用,就会把该变量添加到windows属性下,其引用一直存在就会造成内存泄漏问题。
- console.log:浏览器存储了每个console.log的数据的引用。
- 不合理的使用Map,Set:Map,Set都是强引用,其键和值如果没有手动删去是不会删掉的,可以使用WeakMap和WeakSet。
23.当使用new操作符时会发生什么?
当使用new操作符时,其实就是返回了一个新的对象,首先新建一个Object对象,其实使用apply方法调用传入的函数并使函数内this指向新创建的对象,然后使新创建的实例对象的____proto____属性指向函数的prototype的原型对象。最后返回这个新创建的对象。例子如下:
function Prople(name, age) {
this.name = name;
this.age = age;
}
function myNew(Fun, ...args) {
const obj = {};
Fun.apply(obj, args);
obj.__proto__ = Fun.prototype;
return obj;
}
const people = myNew(Prople, "111", "222");
console.log(people.name);
console.log(people.age);
24.Map和Object的区别,时间复杂度?
Map和Object的区别:
- Object的键必须是String或者Symbol类型的,对其其他数据类型会转为String类型。而Map的键和值都可以是任意数据类型。
- Map的键是根据内存地址来的,只要内存地址不同,键就不同,这意味着可以创建相同的名称的键。而Object的键是根据值知否相等来的。
- Map具有迭代器,可以使用for...of,forEach进行遍历,而Object没有迭代器,可以使用for...in进行遍历。
- Map中的键是有顺序的,顺序就是定义的顺序。而Object中的键是无序的。
时间复杂度:
在没有发生Hash冲突的时候时间复杂度就是O(1)。如果发生Hash冲突。对于比较少的数据采用链表处理,此时时间复杂度就是O(n),对于比较多的数据采用红黑树进行处理,此时的时间复杂度就是O(logn)。
注:红黑树是一种特殊的平衡二叉树(根节点的大小大于左子树小于右子树,且左右两颗子树的高度只差的绝对值小于等于1),每个结点都带有红或黑两种颜色属性。
25.深拷贝和浅拷贝
深拷贝和浅拷贝都是对于对象而言的。深拷贝和浅拷贝对于对象的基本数据类型而言是相同的,拷贝的是原始类型的值。他们的区别主要在引用数据类型上,对于引用数据类型,浅拷贝拷贝的是地址,也就是说拷贝对象和原对象指向的是同一块堆内存地址。而深拷贝则是拷贝的原对象的值,原对象和拷贝对象指的不是同一个对象,两者互不影响。
=号与深拷贝和浅拷贝的区别:等于号并没有开辟新的内存空间,相当于一个别名。而不管是深拷贝还是浅拷贝都会开辟一块新的内存空间。
浅拷贝的实现方式:
- Object.assign({}, oldObj)。
- ...解构运算符。
对于数组而言
- Array.concat():数组拼接函数(直接调用则为浅拷贝)。
- Array.slice():数组截取函数(直接调用则为浅拷贝)。
- Array.filter()
- Array.from()
手写浅拷贝函数:
const shallowClone = function(obj) {
if (obj === null || obj === undefined || typeof obj !== "object") {
return obj;
}
const _obj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
_obj[key] = obj[key];
}
}
return _obj;
}
深拷贝的实现方式:
-
Json.parse(Json.stringify(obj))。
在某些情境下会存在的问题:对undefined,函数会丢失,如果有Date等js对象也会丢失。如果对象中出现循环引用会报错。对NaN,Infinity会转化为null。
手写深拷贝函数:使用weakmap主要是解决循环引用的问题。
const deepClone = function(obj, hash = new WeakMap()) {
if (obj === null || obj === undefined || typeof obj !== "object") {
return obj;
}
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Date) return new Date(obj);
if (hash.has(obj)) return hash.get(obj);
const _obj = {};
hash.set(obj, _obj);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
_obj[key] = deepClone(obj[key]);
}
}
return _obj;
}
26.null和undefined的区别
两者都是基本数据类型,如果声明一个变量没有对其进行赋值就是undefined类型,undefined类型还是window对象下面的一个属性。null类型一般代指空对象,但是使用typeof进行类型判断时会返回object类型,这是个历史遗留问题,null所有位都为0,在js中,如果前几位为0的话就会判断为Object类型。
27.常见的git命令
git init(创建一个本地仓库)
git clone(克隆一个远程仓库)
git add(添加文件到暂存区)
git commit(将暂存区的文件提交到本地的仓库中)
git push(将分支提交到远程仓库中)
git checkout(创建/切换分支)
git branch(查看分支)
git tag(打标签)
28.js的事件冒泡
js中事件主要分为事件捕获,处于当前元素以及事件冒泡阶段。在事件捕获阶段,触发事件是由外向里触发的。在事件冒泡阶段,触发事件是由里向外触发的,可以通过e.stopPropagation()阻止事件冒泡。同时,可以使用事件委托(事件代理)的方式将多个子元素触发的事件绑定到父元素身上,减少创建多个事件所造成的内存开销。
29.Set和Map的区别
- Set中的值必须是不同的,Map中键是根据内存中的地址来的,只要内存中的地址不相同,那么它就可以是两个键,即使他们值都相同。
- Set初始化时使用的是一个一维数组,而Map初始化时使用的是一个二维数组。
- 在底层实现上,Set和Map应该都是使用Hash的一个方式,正常情况下,查找的复杂度是O(1),在遇到hash冲突时,如果采用链表的形式解决hash冲突,那么查找的复杂度就是O(n),如果冲突特别多采用红黑树的方式解决hash冲突时间复杂度就是O(logn)。