JS
数据类型
面试官:JS的数据类型都有哪些⭐⭐⭐⭐⭐
答:
数据类型分为基本数据类型和引用数据类型;
基本数据类型有:
- Number
- String
- Boolean
- null
- Undefined
- Symbol
- BigInt
引用数据类型统称为Object类型,
- 细分的话有:
- Object
- Array
- Function
- Date
- RegExp
基本数据类型的数据直接存储在栈中;而引用数据类型的数据存储在堆中,在栈中保存数据的引用地址,这个引用地址指向的是对应的数据,以便快速查找到堆内存中的对象。
顺便提一句,栈内存是自动分配内存的。而堆内存是动态分配内存的,不会自动释放。所以每次使用完对象的时候都要把它设置为null,从而减少无用内存的消耗
为什么0.1+0.2>0.3 ⭐⭐⭐⭐
答:
因为在JS底层中,每个变量是以二进制表示,固定长度为64位,其中第1位是符号位,再往后11位是指数为,最后52表示的是尾数位,而0.1和0.2转为二进制的时候是无限循环小数,所以JS就会进行截取,截取以后0.1和0.2就不是他们本身了,要比原来大那么一丢丢,所以0.1+0.2就>0.3了
如何解决这个问题,使0.1+0.2等于0.3? ⭐⭐⭐⭐⭐
答:
先给他们放大倍数,随后在除以相应倍数
const a = 0.1;
const b = 0.2;
console.log(a + b === 0.3) // false
console.log((a * 1000 + b * 1000) / 1000 === 0.3) // true
数据类型的判断方式⭐⭐⭐⭐⭐
答:
1.typeof
- 缺点:typeof null的值为Object,无法分辨是null还是Object
2.instanceof
- 缺点:只能判断某对象是否存在于目标对象得的原型链上
3.constructor.name
4.Object.prototype.toString.call()
- 一种最好的基本类型检测方式 Object.prototype.toString.call() ;它可以区分 null 、 string 、
boolean 、 number 、 undefined 、 array 、 function 、 object 、 date 、 math 数据类型。
- 缺点:不能细分为谁谁的实例
示例:
// -----------------------------------------typeof
typeof undefined // 'undefined'
typeof '10' // 'String'
typeof 10 // 'Number'
typeof false // 'Boolean'
typeof Symbol() // 'Symbol'
typeof Function // ‘function'
typeof null // ‘Object’
typeof [] // 'Object'
typeof {} // 'Object'
// -----------------------------------------instanceof
function Foo() { }
var f1 = new Foo();
var d = new Number(1)
console.log(f1 instanceof Foo);// true
console.log(d instanceof Number); //true
console.log(123 instanceof Number); //false -->不能判断字面量的基本数据类型
// -----------------------------------------constructor
var d = new Number(1)
var e = 1
function fn() {
console.log("ming");
}
var date = new Date();
var arr = [1, 2, 3];
var reg = /[hbc]at/gi;
console.log(e.constructor);//ƒ Number() { [native code] }
console.log(e.constructor.name);//Number
console.log(fn.constructor.name) // Function
console.log(date.constructor.name)// Date
console.log(arr.constructor.name) // Array
console.log(reg.constructor.name) // RegExp
//-----------------------------------------Object.prototype.toString.call()
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call(123)); // "[object Number]"
console.log(Object.prototype.toString.call("abc")); // "[object String]"
console.log(Object.prototype.toString.call(true)); // "[object Boolean]"
function fn() {
console.log("ming");
}
var date = new Date();
var arr = [1, 2, 3];
var reg = /[hbc]at/gi;
console.log(Object.prototype.toString.call(fn));// "[object Function]"
console.log(Object.prototype.toString.call(date));// "[object Date]"
console.log(Object.prototype.toString.call(arr)); // "[object Array]"
console.log(Object.prototype.toString.call(reg));// "[object RegExp]"
为什么要用Object.prototype.toString.call(),为什么不用 Array.prototype.toString.call()?⭐⭐
答:
因为只有Object.prototype.toString.call()返回的是统一格式,而且 Array.prototype.toString.call()的部分类型无法检验。
function fn() {
console.log("ming");
}
var date = new Date();
var arr = [1, 2, 3];
var reg = /[hbc]at/gi;
console.log(Array.prototype.toString.call(undefined)); // 报错
console.log(Array.prototype.toString.call(null)); // 报错
console.log(Array.prototype.toString.call(123)); // "[object Number]"
console.log(Array.prototype.toString.call("abc")); // "[object String]"
console.log(Array.prototype.toString.call(true)); // "[object Boolean]"
console.log(Array.prototype.toString.call(fn)); // "[object Function]"
console.log(Array.prototype.toString.call(date)); // "[object Date]"
console.log(Array.prototype.toString.call(arr)); // "1,2,3"
console.log(Array.prototype.toString.call(reg));// "[object RegExp]"
instanceof原理⭐⭐⭐⭐⭐
答:
instanceof原理实际上就是查找目标对象的原型链
function myInstance(L, R) {//L代表instanceof左边,R代表右边
var RP = R.prototype
var LP = L.__proto__
while (true) {
if(LP == null) {
return false
}
if(LP == RP) {
return true
}
LP = LP.__proto__
}
}
console.log(myInstance({},Object));
面试官:为什么typeof null 是Object?⭐⭐⭐⭐⭐
答:
因为在JavaScript中,不同的对象都是使用二进制存储的,如果二进制前三位都是0的话,系统会判断为是Object类型,而null的二进制全是0,自然也就判断为Object
这个bug是初版本的JavaScript中留下的,扩展一下其他五种标识位:
- 000 对象
- 1 整型
- 010 双精度类型
- 100字符串
- 110布尔类型
面试官:和=有什么区别 ⭐⭐⭐⭐⭐
答:
===是严格意义上的相等,会比较两边的数据类型和值大小
- 数据类型不同返回false
- 数据类型相同,但值大小不同,返回false
==是非严格意义上的相等,
-
两边类型相同,比较大小
-
两边类型不同,根据下方表格,再进一步进行比较。
- Null == Undefined ->true
- String == Number ->先将String转为Number,在比较大小
- Boolean == Number ->现将Boolean转为Number,在进行比较
- Object == String,Number,Symbol -> Object 转化为原始类型
面试官:NaN === NaN返回什么?⭐⭐⭐⭐⭐
答:
返回 false,NaN<永远不等于NaN,判断是否为NaN用一个函数 isNaN来判断;
isNaN传入的如果是其他数据类型,那么现将它使用Number()转为数字类型在进行判断
call、apply、bind三者的用法和区别
面试官:手写call、apply、bind⭐⭐⭐⭐⭐
答:
-
call和apply实现思路主要是:
- 判断是否是函数调用,若非函数调用抛异常
- 通过新对象(context)来调用函数
- 给context创建一个fn设置为需要调用的函数
- 结束调用完之后删除fn
-
bind实现思路
- 判断是否是函数调用,若非函数调用抛异常
- 返回函数
- 判断函数的调用方式,是否是被new出来的
- new出来的话返回空对象,但是实例的proto指向_this的prototype
- 判断函数的调用方式,是否是被new出来的
- 完成函数柯里化
- Array.prototype.slice.call()
call:
Function.prototype.myCall = function (context) {
// 先判断调用myCall是不是一个函数
// 这里的this就是调用myCall的
if (typeof this !== 'function') {
throw new TypeError("Not a Function")
}
// 不传参数默认为window
context = context || window
// 保存this
context.fn = this
// 保存参数
let args = Array.from(arguments).slice(1) //Array.from 把伪数组对象转为数组
// 调用函数
let result = context.fn(...args)
delete context.fn
return result
}
apply:
Function.prototype.myApply = function (context) {
// 判断this是不是函数
if (typeof this !== "function") {
throw new TypeError("Not a Function")
}
let result
// 默认是window
context = context || window
// 保存this
context.fn = this
// 是否传参
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
bind:
Function.prototype.myBind = function(context){
// 判断是否是一个函数
if(typeof this !== "function") {
throw new TypeError("Not a Function")
}
// 保存调用bind的函数
const _this = this
// 保存参数
const args = Array.prototype.slice.call(arguments,1)
// 返回一个函数
return function F () {
// 判断是不是new出来的
if(this instanceof F) {
// 如果是new出来的
// 返回一个空对象,且使创建出来的实例的__proto__指向_this的prototype,
// 且完成函数柯里化
return new _this(...args,...arguments)
}else{
// 如果不是new出来的改变this指向,且完成函数柯里化
return _this.apply(context,args.concat(...arguments))
}
}
}
函数柯里化
面试官:字面量创建对象和new创建对象有什么区别,new内部都实现了什么,手写一个new⭐⭐⭐⭐⭐
答:
字面量:
- 字面量创建对象更简单,方便阅读
- 不需要作用域解析,速度更快
- new内部:
创建一个新对象:
- 使新对象的__proto__指向原函数的prototype
- 改变this指向(指向新的obj)并执行该函数,执行结果保存起来作为result
- 判断执行函数的结果是不是null或Undefined,如果是则返回之前的新对象,如果不是则返回result
手写new:
// 手写一个new
function myNew(fn, ...args) {
// 创建一个空对象
let obj = {}
// 使空对象的隐式原型指向原函数的显式原型
obj.__proto__ = fn.prototype
// this指向obj
let result = fn.apply(obj, args)
// 返回
return result instanceof Object ? result : obj
}
执行栈和执行上下文
面试官:什么是作用域,什么是作用域链?⭐⭐⭐⭐
答:
- 规定变量和函数的可使用范围称作作用域
- 每个函数都有一个作用域链,查找变量或者函数时,需要从局部作用域到全局作用域依次查找,这些作用域的集合称作作用域链
面试官:什么是执行栈,什么是执行上下文?⭐⭐⭐⭐
答:
执行栈:
- 首先栈特点:先进后出
- 当进入一个执行环境,就会创建出它的执行上下文,然后进行压栈,当程序执行完成时,它的执行上下文就会被销毁,进行弹栈。
- 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文
- 只有浏览器关闭的时候全局执行上下文才会弹出
执行上下文分为:
- 全局执行上下文
- 创建一个全局的window对象,并规定this指向window,执行js的时候就压入栈底,关闭浏览器的时候才弹出
- 函数执行上下文
- 每次函数调用时,都会新创建一个函数执行上下文
- 执行上下文分为创建阶段和执行阶段
- 创建阶段:函数环境会创建变量对象:arguments对象(并赋值)、函数声明(并赋值)、变量声明 (不赋值),函数表达式声明(不赋值);会确定this指向;会确定作用域
- 执行阶段:变量赋值、函数表达式赋值,使变量对象编程活跃对象
- eval执行上下文
闭包
很多人都吃不透js闭包,这里推荐一篇文章:彻底理解js中的闭包
面试官:什么是闭包?闭包的作用?闭包的应用?⭐⭐⭐⭐⭐
答:
函数执行,形成私有的执行上下文,使内部私有变量不受外界干扰,起到保护和保存的作用
作用:
- 保护
- 避免命名冲突
- 保存
- 解决循环绑定引发的索引问题
- 变量不会销毁
- 可以使用函数内部的变量,使变量不会被垃圾回收机制回收
应用:
- 设计模式中的单例模式
- for循环中的保留i的操作
- 防抖和节流
- 函数柯里化
缺点
- 会出现内存泄漏的问题
原型和原型链
面试官:什么是原型?什么是原型链?如何理解⭐⭐⭐⭐⭐
答:
原型: 原型分为隐式原型和显式原型,每个对象都有一个隐式原型,它指向自己的构造函数的显式原型。每个构造方法都有一个显式原型。
- __proto__是隐式原型;prototype是显式原型
- 所有实例的__proto__都指向他们构造函数的prototype
- 所有的prototype都是对象,自然它的__proto__指向的是Object()的prototype
- 所有的构造函数的隐式原型指向的都是Function()的显示原型
- Object的隐式原型是null
原型链: 多个__proto__组成的集合成为原型链(概念类似于作用域链)
- instanceof 就是判断某对象是否位于某构造方法的原型链上。
继承
面试官:说一说 JS 中的常用的继承方式有哪些?以及各个继承方式的优缺点。⭐⭐⭐⭐⭐
答:
原型继承、组合继承、寄生组合继承、ES6的extend
原型继承
// ----------------------方法一:原型继承
// 原型继承
// 把父类的实例作为子类的原型
// 缺点:子类的实例共享了父类构造函数的引用属性 不能传参
var person = {
friends: ["a", "b", "c", "d"]
}
var p1 = Object.create(person)
p1.friends.push("aaa")//缺点:子类的实例共享了父类构造函数的引用属性
console.log(p1);
console.log(person);//缺点:子类的实例共享了父类构造函数的引用属性
组合继承
// ----------------------方法二:组合继承
// 在子函数中运行父函数,但是要利用call把this改变一下,
// 再在子函数的prototype里面new Father() ,使Father的原型中的方法也得到继承,最后改变Son的原型中的constructor
// 缺点:调用了两次父类的构造函数,造成了不必要的消耗,父类方法可以复用
// 优点可传参,不共享父类引用属性
function Father(name) {
this.name = name
this.hobby = ["篮球", "足球", "乒乓球"]
}
Father.prototype.getName = function () {
console.log(this.name);
}
function Son(name, age) {
Father.call(this, name)
this.age = age
}
Son.prototype = new Father()
Son.prototype.constructor = Son
var s = new Son("ming", 20)
console.log(s);
寄生组合继承
// ----------------------方法三:寄生组合继承
function Father(name) {
this.name = name
this.hobby = ["篮球", "足球", "乒乓球"]
}
Father.prototype.getName = function () {
console.log(this.name);
}
function Son(name, age) {
Father.call(this, name)
this.age = age
}
Son.prototype = Object.create(Father.prototype)
Son.prototype.constructor = Son
var s2 = new Son("ming", 18)
console.log(s2);
extend
**// ----------------------方法四:ES6的extend(寄生组合继承的语法糖)
// 子类只要继承父类,可以不写 constructor ,一旦写了,则在 constructor 中的第一句话
// 必须是 super 。
class Son3 extends Father { // Son.prototype.__proto__ = Father.prototype
constructor(y) {
super(200) // super(200) => Father.call(this,200)
this.y = y
}
}**
内存泄露、垃圾回收机制
面试官:什么是内存泄漏⭐⭐⭐⭐⭐
答:
内存泄露是指不再用的内存没有被及时释放出来,导致该段内存无法被使用就是内存泄漏
面试官:为什么会导致的内存泄漏⭐⭐⭐⭐⭐
答:
内存泄漏指我们无法在通过js访问某个对象,而垃圾回收机制却认为该对象还在被引用,因此垃圾回收机制不会释放该对象,导致该块内存永远无法释放,积少成多,系统会越来越卡以至于崩溃
面试官:垃圾回收机制都有哪些策略?⭐⭐⭐⭐⭐
答:
- 标记清除法
- 垃圾回收机制获取根并标记他们,然后访问并标记所有来自它们的引用,然后在访问这些对象并标记它们的引用…如此递进结束后若发现有没有标记的(不可达的)进行删除,进入执行环境的不能进行删除
- 引用计数法
- 当声明一个变量并给该变量赋值一个引用类型的值时候,该值的计数+1,当该值赋值给另一个变量的时候,该计数+1,当该值被其他值取代的时候,该计数-1,当计数变为0的时候,说明无法访问该值了,垃圾回收机制清除该对象
- 缺点: 当两个对象循环引用的时候,引用计数无计可施。如果循环引用多次执行的话,会造成崩溃等问题。所以后来被标记清除法取代。
深拷贝和浅拷贝
手写浅拷贝深拷贝⭐⭐⭐⭐⭐
// 只是把对象的属性和属性值拷贝到另一个对象中
// ----------------------------------------------浅拷贝
var obj1 = {
a: {
a1: { a2: 1 },
a10: { a11: 123, a111: { a1111: 123123 } }
},
b: 123,
c: "123"
}
方式1:
// 方式1
function shallowClone1(o) {
let obj = {}
for (let i in o) {
obj[i] = o[i]
}
return obj
}
方式2:
var shallowObj2 = { ...obj1 }
// 方式3
var shallowObj3 = Object.assign({}, obj1)
let shallowObj = shallowClone1(obj1);
shallowObj.a.a1 = 999
shallowObj.b = true
console.log(obj1); //第一层的没有被改变,一层以下就被改变了
// ----------------------------------------------深拷贝
简易版
function deepClone(o) {
let obj = {}
for (var i in o) {
// if(o.hasOwnProperty(i)){
if (typeof o[i] === "object") {
obj[i] = deepClone(o[i])
} else {
obj[i] = o[i]
}
// }
}
return obj
}
var myObj = {
a: {
a1: { a2: 1 },
a10: { a11: 123, a111: { a1111: 123123 } }
},
b: 123,
c: "123"
}
var deepObj1 = deepClone(myObj)
deepObj1.a.a1 = 999
deepObj1.b = false
console.log(myObj);
简易版存在的问题:参数没有做检验,传入的可能是 Array、null、regExp、Date
function deepClone2(o) {
if (Object.prototype.toString.call(o) === "[object Object]") { //检测是否为对象
let obj = {}
for (var i in o) {
if (o.hasOwnProperty(i)) {
if (typeof o[i] === "object") {
obj[i] = deepClone(o[i])
} else {
obj[i] = o[i]
}
}
}
return obj
} else {
return o
}
}
function isObject(o) {
return Object.prototype.toString.call(o) === "[object Object]" || Object.prototype.toString.call(o) === "[object Array]"
}
// 继续升级,没有考虑到数组,以及ES6中的map、set、weakset、weakmap
function deepClone3(o) {
if (isObject(o)) {//检测是否为对象或者数组
let obj = Array.isArray(o) ? [] : {}
for (let i in o) {
if (isObject(o[i])) {
obj[i] = deepClone(o[i])
} else {
obj[i] = o[i]
}
}
return obj
} else {
return o
}
}
//有可能碰到循环引用问题 var a = {}; a.a = a; clone(a);//会造成一个死循环
// 循环检测
// 继续升级
function deepClone4(o, hash = new map()) {
if (!isObject(o)) return o//检测是否为对象或者数组
if (hash.has(o)) return hash.get(o)
let obj = Array.isArray(o) ? [] : {}
hash.set(o, obj)
for (let i in o) {
if (isObject(o[i])) {
obj[i] = deepClone4(o[i], hash)
} else {
obj[i] = o[i]
}
}
return obj
}
递归易出现爆栈问题
将递归改为循环,就不会出现爆栈问题了
var a1 = { a: 1, b: 2, c: { c1: 3, c2: { c21: 4, c22: 5 } }, d: 'asd' };
var b1 = { b: { c: { d: 1 } } }
function cloneLoop(x) {
const root = {};
// 栈
const loopList = [ //->[]->[{parent:{a:1,b:2},key:c,data:{ c1: 3, c2: { c21: 4, c22: 5 } }}]
{
parent: root,
key: undefined,
data: x,
}
];
while (loopList.length) {
// 深度优先
const node = loopList.pop();
const parent = node.parent; //{} //{a:1,b:2}
const key = node.key; //undefined //c
const data = node.data; //{ a: 1, b: 2, c: { c1: 3, c2: { c21: 4, c22: 5 } }, d: 'asd' } //{ c1: 3, c2: { c21: 4, c22: 5 } }}
// 初始化赋值目标,key 为 undefined 则拷贝到父元素,否则拷贝到子元素
let res = parent; //{}->{a:1,b:2,d:'asd'} //{a:1,b:2}->{}
if (typeof key !== 'undefined') {
res = parent[key] = {};
}
for (let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
})
} else {
res[k] = data[k];
}
}
}
}
return root
}
function deepClone5(o) {
let result = {}
let loopList = [
{
parent: result,
key: undefined,
data: o
}
]
while (loopList.length) {
let node = loopList.pop()
let { parent, key, data } = node
let anoPar = parent
if (typeof key !== 'undefined') {
anoPar = parent[key] = {}
}
for (let i in data) {
if (typeof data[i] === 'object') {
loopList.push({
parent: anoPar,
key: i,
data: data[i]
})
} else {
anoPar[i] = data[i]
}
}
}
return result
}
let cloneA1 = deepClone5(a1)
cloneA1.c.c2.c22 = 5555555
console.log(a1);
console.log(cloneA1);
// ------------------------------------------JSON.stringify()实现深拷贝
function cloneJson(o) {
return JSON.parse(JSON.stringify(o))
}
深拷贝能使用hash递归的方式写出来就可以了 不过技多不压身,
推荐还是看一看使用while实现深拷贝方法
单线程,同步异步
面试官:为什么JS是单线程的?⭐⭐⭐⭐⭐
答: 因为JS里面有可视的Dom,如果是多线程的话,这个线程正在删除DOM节点,另一个线程正在编辑Dom节点,导致浏览器不知道该听谁的
面试官:说说 Promise 的原理?你是如何理解 Promise 的?⭐⭐⭐⭐⭐
做到简易版的promise理解,以及会写race和all函数就可以
答:
class MyPromise2 {
constructor(executor) {
// 规定状态
this.state = "pending"
// 保存 `resolve(res)` 的res值
this.value = undefined
// 保存 `reject(err)` 的err值
this.reason = undefined
// 成功存放的数组
this.successCB = []
// 失败存放的数组
this.failCB = []
let resolve = (value) => {
if (this.state === "pending") {
this.state = "fulfilled"
this.value = value
this.successCB.forEach(f => f())
}
}
let reject = (reason) => {
if (this.state === "pending") {
this.state = "rejected"
this.value = value
this.failCB.forEach(f => f())
}
}
try {
// 执行
executor(resolve, reject)
} catch (error) {
// 若出错,直接调用reject
reject(error)
}
}
then(onFulfilled, onRejected) {
if (this.state === "fulfilled") {
onFulfilled(this.value)
}
if (this.state === "rejected") {
onRejected(this.value)
}
if (this.state === "pending") {
this.successCB.push(() => { onFulfilled(this.value) })
this.failCB.push(() => { onRejected(this.reason) })
}
}
}
Promise.all = function (promises) {
let list = []
let count = 0
function handle(i, data) {
list[i] = data
count++
if (count == promises.length) {
resolve(list)
}
}
return Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
promises[i].then(res => {
handle(i, res)
}, err => reject(err))
}
})
}
Promise.race = function (promises) {
return Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
promises[i].then(res => {
resolve(res)
}, err => {
reject(err)
})
}
})
}
}
面试官:以下代码的执行顺序是什么⭐⭐⭐⭐⭐
答:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
async1()
console.log('script start')
//执行到await时,如果返回的不是一个promise对象,await会阻塞下面代码(当前async代码块的代码),会先执行async外的同步代码(在这之前先看看await中函数的同步代码,先把同步代码执行完),等待同步代码执行完之后,再回到async内部继续执行
//执行到await时,如果返回的是一个promise对象,await会阻塞下面代码(当前async代码块的代码),会先执行async外的同步代码(在这之前先看看await中函数的同步代码,先把同步代码执行完),等待同步代码执行完之后,再回到async内部等promise状态达到fulfill的时候再继续执行下面的代码
//所以结果为
//async1 start
//async2
//script start
//async1 end
面试官:宏任务和微任务都是怎样执行的⭐⭐⭐⭐⭐
答:
- 执行宏任务script,
- 进入script后,所有的同步任务主线程执行
- 所有宏任务放入宏任务执行队列
- 所有微任务放入微任务执行队列
- 先清空微任务队列,
- 再取一个宏任务,执行,再清空微任务队列
- 依次循环
例题1
setTimeout(function(){
console.log('1')
});
new Promise(function(resolve){
console.log('2');
resolve();
}).then(function(){
console.log('3')
});
console.log('4');
new Promise(function(resolve){
console.log('5');
resolve();
}).then(function(){
console.log('6')
});
setTimeout(function(){
console.log('7')
});
function bar(){
console.log('8')
foo()
}
function foo(){
console.log('9')
}
console.log('10')
bar()
解析
- 首先浏览器执行Js代码由上至下顺序,遇到setTimeout,把setTimeout分发到宏任务Event Queue中
- new Promise属于主线程任务直接执行打印2
- Promis下的then方法属于微任务,把then分到微任务 Event Queue中
- console.log(‘4’)属于主线程任务,直接执行打印4
- 又遇到new Promise也是直接执行打印5,Promise 下到then分发到微任务Event Queue中
- 又遇到setTimouse也是直接分发到宏任务Event Queue中,等待执行
- console.log(‘10’)属于主线程任务直接执行
- 遇到bar()函数调用,执行构造函数内到代码,打印8,在bar函数中调用foo函数,执行foo函数到中代码,打印9
- 主线程中任务执行完后,就要执行分发到微任务Event Queue中代码,实行先进先出,所以依次打印3,6
- 微任务Event Queue中代码执行完,就执行宏任务Event Queue中代码,也是先进先出,依次打印1,7。
例题2
setTimeout(() => {
console.log('1');
new Promise(function (resolve, reject) {
console.log('2');
setTimeout(() => {
console.log('3');
}, 0);
resolve();
}).then(function () {
console.log('4')
})
}, 0);
console.log('5'); //5 7 10 8 1 2 4 6 3
setTimeout(() => {
console.log('6');
}, 0);
new Promise(function (resolve, reject) {
console.log('7');
// reject();
resolve();
}).then(function () {
console.log('8')
}).catch(function () {
console.log('9')
})
console.log('10');
运行结果: 5 7 10 8 1 2 4 6 3
变量提升
面试官:变量和函数怎么进行提升的?优先级是怎么样的?⭐⭐⭐
答:
- 对所有函数声明进行提升(除了函数表达式和箭头函数),引用类型的赋值
- 开辟堆空间
- 存储内容
- 将地址赋给变量
- 对变量进行提升,只声明,不赋值,值为undefined
面试官:var let const 有什么区别⭐⭐⭐⭐⭐
答:
- var
- var声明的变量可进行变量提升,let和const不会
- var可以重复声明
- var在非函数作用域中定义是挂在到window上的
- let
- let声明的变量只在局部起作用
- let防止变量污染
- 不可在声明
- const
- 具有let的所有特征
- 不可被改变
- 不可改变只适用于直接地址。如果使用const声明的是对象的话,是可以修改对象内部的值的。
模块化
面试官:为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?⭐⭐⭐
- 为什么要使用模块化
- 防止命名冲突
- 更好的分离,按需加载
- 更好的复用性
- 更高的维护性
面试官:exports和module.exports有什么区别?⭐⭐⭐
- 导出方式不一样
- exports.xxx=‘xxx’
- module.export = {}
- exports是module.exports的引用,两个指向的是用一个地址,而require能看到的只有module.exports
面试官:JS模块包装格式有哪些?⭐⭐⭐
- commonjs
- 同步运行,不适合前端
- AMD
- 异步运行
- 异步模块定义,主要采用异步的方式加载模块,模块的加载不影响后面代码的执行。所有依赖这个模块的语句都写在一个回调函数中,模块加载完毕,再执行回调函数
- CMD
- 异步运行
- seajs 规范
面试官:ES6和commonjs的区别⭐⭐⭐
Commonjs、AMD、CMD、UMD、ESM 都有什么区别
-
Commonjs
-
是同步执行的,不适合前端,后端 nodejs 可以使用 commonjs。
-
使用方式
-
module.exports = xxx
require('xxx')
AMD/CMD/UMD 适用前端 异步执行
- AMD
define(["a","b","c","d","e"],function(a,b,c,d,e){
// 相当于在前面声明并初始化了要用到的所有模块
a.dosomething()
if(false) {
// 即使没有用到模块 b,也会提前执行
b.dosomething()
}
})
- CMD
define(function(require, exports, module){
var a = require("./a") //需要的时候声明
a.dosomething()
if(false) {
var b = require("./b")
b.dosomething()
}
})
-
AMD 和 CMD 的差别是
- AMD 是依赖前置(把依赖放在前面)、提前执行(即使没有用到某个模块,也会提前执行)
- CMD依赖就近、延时执行(用到的时候在声明依赖)
-
ESM
- 使用 export 、 export default 来导出模块,使用 import 来引入模块
- 使用 export 、 export default 来导出模块,使用 import 来引入模块
-
ESM 和 commonjs 的区别主要在于
- commonjs 是运行时加载 ;ESM 是编译时加载
- commonjs 是同步加载模块;ESM 是异步加载模块
- commonjs 是对值的浅拷贝;ESM 是对值的引用,而且不可修改(直接地址不可修改,类似于 const)。
面试官问:require 和 import的区别?⭐⭐⭐
-
调用时机
- require 是运行时调用,所以其实是可以放在任何地方的
- Import 是编译时调用,所以必须放在文件的开头
-
使用时,
- require 需要使用 module.exports = fs 或者exports.fs = xxx
- import 用 export default 或 export const xx
-
解构赋值
- require 是赋值的过程
- import 是解构的过程
JS其他
面试官:箭头函数和普通函数的区别?箭头函数可以当做构造函数 new 吗?⭐⭐⭐⭐⭐
- 箭头函数是普通函数的简写,但是它不具备很多普通函数的特性
- 第一点,this指向问题,箭头函数的this指向它定义时所在的对象,而不是调用它的对象
- 不会进行函数提升
- 没有arguments对象,不能使用arguments,如果要获取参数的话可以使用rest运算符
- 没有yield属性,不能作为生成器Generator使用
- 不能new
- 没有自己的this,不能调用call和apply
- 没有prototype,new关键字内部需要把新对象的_proto_指向函数的prototype
ajax
手写 ajax⭐⭐⭐⭐
var Ajax = {
get: function (url, callback) {
let xhr = XMLHttpRequest();
xhr.open("get", url, false)
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status == 304) {
console.log(xhr.responseText);
callback(xhr.responseText)
}
}
}
xhr.send()
},
post: function (url, data, callback) {
let xhr = new XMLHttpRequest()
// 第三个参数为是否异步执行
xhr.open('post', url, true)
// 添加http头,设置编码类型
xhr.setRequestHeader("Content-type","x-www-form-urlencoded")
xhr.onreadystatechange = function () {
if(xhr.readyState == 4) {
if(xhr.status == 200 || xhr.status == 304) {
console.log(xhr.responseText);
callback(xhr.responseText)
}
}
}
xhr.setRequestHeader('Content-type', "application/x-www-urlencoded")
xhr.send(data)
}
}
防抖节流
什么是防抖?什么是节流?手写一个⭐⭐⭐⭐⭐
- 防抖
- n秒后在执行该事件,若在n秒内被重复触发,则重新计时
- 节流
- n秒内只运行一次,若在n秒内重复触发,只有一次生效
防抖:
// ---------------------------------------------------------防抖函数
function debounce(func, delay) {
let timeout
return function () {
let arg = arguments
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func(arg)
}, delay);
}
}
// ---------------------------------------------------------立即执行防抖函数
function debounce2(fn, delay) {
let timer
return function () {
let args = arguments
if (timer) clearTimeout(timer)
let callNow = !timer
timer = setTimeout(() => {
timer = null
}, delay);
if (callNow) { fn(args) }
}
}
// ---------------------------------------------------------立即执行防抖函数+普通防抖
function debounce3(fn, delay, immediate) {
let timer
return function () {
let args = arguments
let _this = this
if (timer) clearTimeout(timer)
if (immediate) {
let callNow = !timer
timer = setTimeout(() => {
timer = null
}, delay);
if (callNow) { fn.apply(_this, args) }
} else {
timeout = setTimeout(() => {
func.apply(_this, arguments)
}, delay);
}
}
}
节流
// ---------------------------------------------------------节流 ,时间戳版
function throttle(fn, wait) {
let previous = 0
return function () {
let now = Date.now()
let _this = this
let args = arguments
if (now - previous > wait) {
fn.apply(_this, arguments)
previous = now
}
}
}
// ---------------------------------------------------------节流 ,定时器版
function throttle2(fn, wait) {
let timer
return function () {
let _this = this
let args = arguments
if (!timer) {
timer = setTimeout(() => {
timer = null
fn.apply(_this, arguments)
}, wait);
}
}
}
函数柯里化原理⭐⭐⭐⭐⭐
function add() {
var args = Array.prototype.slice.call(arguments)
var adder = function () {
args.push(...arguments)
return adder
}
adder.toString = function () {
return args.reduce((prev, curr) => {
return prev + curr
}, 0)
}
return adder
}
let a = add(1, 2, 3)
let b = add(1)(2)(3)
console.log(a)
console.log(b)
console.log(add(1, 2)(3));
console.log(Function.toString)
// --------普通函数转为柯里化函数------
function createCurry(fn, args = []) {
return function () {
let _args = args.concat(...arguments)
if (_args.length < fn.length) {
return createCurry.call(this, fn, _args)
}
return fn.apply(this, _args)
}
}
function add(a, b, c) {
return a + b + c;
}
var _add = createCurry(add);
console.log(_add(1, 2, 3));
console.log(_add(1)(2, 3));
console.log(_add(1)(2)(3));
什么是requestAnimationFrame?⭐⭐⭐⭐
-
requestAnimationFrame请求数据帧可以用做动画执行
-
可以自己决定什么时机调用该回调函数
-
能保证每次频幕刷新的时候只被执行一次
-
页面被隐藏或者最小化的时候暂停执行,返回窗口继续执行,有效节省CPU
var s = 0
function f() {
s++
console.log(s);
if (s < 999) {
window.requestAnimationFrame(f)
}
}
window.requestAnimationFrame(f)
设计模式
js常见的设计模式⭐⭐⭐⭐
单例模式、工厂模式、构造函数模式、发布订阅者模式、迭代器模式、代理模式
-
单例模式
-
不管创建多少个对象都只有一个实例
var Single = (function () {
var instance = null
function Single(name) {
this.name = name
}
return function (name) {
if (!instance) {
instance = new Single(name)
}
return instance
}
})()
var oA = new Single('hi')
var oB = new Single('hello')
console.log(oA);
console.log(oB);
console.log(oB === oA);
-
工厂模式
-
代替new创建一个对象,且这个对象想工厂制作一样,批量制作属性相同的实例对象(指向不同)
function Animal(o) {
var instance = new Object()
instance.name = o.name
instance.age = o.age
instance.getAnimal = function () {
return "name:" + instance.name + " age:" + instance.age
}
return instance
}
var cat = Animal({name:"cat", age:3})
console.log(cat);
-
构造函数模式
-
发布订阅者模式
```javascript
class Watcher {
// name模拟使用属性的地方
constructor(name, cb) {
this.name = name
this.cb = cb
}
update() {//更新
console.log(this.name + "更新了");
this.cb() //做出更新回调
}
}
class Dep {//依赖收集器
constructor() {
this.subs = []
}
addSubs(watcher) {
this.subs.push(watcher)
}
notify() {//通知每一个观察者做出更新
this.subs.forEach(w => {
w.update()
});
}
}
// 假如现在用到age的有三个地方
var w1 = new Watcher("我{{age}}了", () => { console.log("更新age"); })
var w2 = new Watcher("v-model:age", () => { console.log("更新age"); })
var w3 = new Watcher("I am {{age}} years old", () => { console.log("更新age"); })
var dep = new Dep()
dep.addSubs(w1)
dep.addSubs(w2)
dep.addSubs(w3)
// 在Object.defineProperty 中的 set中运行
dep.notify()
-
代理模式
-
迭代器模式
点击300ms 延迟问题⭐⭐⭐⭐⭐
- 设置一个 meta 标签
- fastClick 插件
- 自己实现
- 实现大概思路:封装一个函数,接受两个参数,一个是目标对象,一个是点击后的回调函数; 在 ontouchstart 开始计时,在 ontouchend 中计时结束,如果超出150ms 就
- //封装函数,处理延迟300ms问题
function tap(obj, callback) {
var isMove = false;
var startTime = 0;
obj.addEventListener('touchstart', function () {
startTime += Date.now();
})
obj.addEventListener('touchmove', function () {
isMove = true;
})
obj.addEventListener('touchend', function () {
if (!isMove && (Date.now() - startTime) < 150) {
callback && callback();
}
isMove = false;
startTime = 0;
})
}
如何实现上传视频?⭐⭐⭐⭐
-
input type = 'file’去接收
-
用 window.URL.createObjectURL(file)把 file 文件转换为 URL(现场的/前端转为 URL)
或者用 FormData 去接收, 把 file 文件append 进去,然后传给后端,使用返回的 URL
setTimeOut第三个参数是什么?⭐⭐⭐⭐⭐
可以作为参数传给函数,一般用于 for 循环赋值
什么是暂时性死区?⭐⭐⭐⭐⭐
暂时性死区是指,当进入一个作用域,我去使用一个变量名,而这个变量名已经存在了,但是是不可获取的,就会报错,造成暂时性死区问题;比如一个作用域下面使用了 let 定义了 x,但是在定义之前就使用了 x,就会报错;暂时性死区意味着 typeof 也不是绝对安全的操作
x = '123'; // 报错
let x = 1
typeof y; // 报错
let y = 123
js 遍历数组的方法⭐⭐⭐⭐⭐
reduce、map、filter、every、some、foreach.
数组可以改变原数组的方法⭐⭐⭐⭐⭐
Push、pop、shift、unshift、splice、sort、reverse
不改变的
join 变成字符
Slice,截取
concat 合并数组
forEach 和 map 有什么区别⭐⭐⭐⭐
- foreach 没有返回值,一般如果用来遍历修改原数组的话可以用 foreach 方法
如何捕获浏览器关闭事件?⭐⭐
- 创建一个canvas对象,把图片保存在 canvas 中,然后 canvas 对象 toDataUrl,在把 dataurl 数据存储在 localstorage 中。
或者使用 blob 二进制流存储,canvas 对象toBlob
如何实现 localstorage 定时清除⭐⭐⭐⭐
- 自己重写一个 set 方法,内部逻辑就是添加一个现在的时间以及有效时长
- 再重写一个 get 方法,每当 get 的时候先进行判断是否过期,如果过期就删除,并返回 null,没过期的话正常返回
web worker是干什么的?⭐⭐⭐
js是单线程的,而web worker可以多创建一个子线程,多出来的这个子线程执行代码时不会阻塞主线程。它有几个限制,
同源限制,子线程资源必须和主线程资源是同源
dom限制,子线程不能操作dom
文件限制,不能打开本机(file://)文件,只能来源于网络
通信限制,只能使用postmessage来传输信息
脚本限制,不能使用alert、confirm方法
jquery 如何实现链式调用⭐⭐⭐
let fun = {
fun1: function() {
console.log("fun1");
return this;
},
fun2: function() {
console.log("fun2");
return this;
},
fun3: function() {
console.log("fun3");
return this;
}
}
fun.fun1().fun2().fun3();
node 事件循环机制和浏览器事件循环机制有什么区别⭐⭐
-
浏览器和 Node 环境下,microtask 任务队列的执行时机不同
- Node 端,microtask 在事件循环的各个阶段之间执行
- 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
https://zhuanlan.zhihu.com/p/54882306
讲一讲Reflect⭐⭐
顾名思义,reflect反射的意思。可以反射对象
Reflect可以提供一些方法去拦截js的操作,Reflect不是一个函数对象,所以它不可构造,Reflect内部的方法和属性都是静态的。
比如创建一个没有原型的对象,也就是说他自己不能调用任何基于Object原型链上的方法
var myObject = Object.create(null)
// 如果想列举它的key值,只需使用Reflect的静态方法,拦截该对象,然后做出处理
Reflect.ownKeys(myObject)
Object.keys和Object.getOwnPropertyNames有什么区别?⭐⭐⭐
Object.keys只列出非原型上可枚举的key值,而Object.getOwnPropertyNames列出非原型上的所有key值(Symbol除外)
如何配置rem⭐⭐⭐
//rem适配
(function () {
const styleEle = document.createElement('style');
const docWidth = document.documentElement.clientWidth;
const rootFontSize = docWidth / 16;
styleEle.innerHTML = 'html{font-size:' + rootFontSize + 'px!important}';
document.head.appendChild(styleEle);
})()
clientHeight、offsetHeight、scrollHeight有什么区别⭐⭐⭐
-
clientHeight
- 用户可见内部高度+padding
-
offsetHeight
- 总高度,算上滚动条
-
scrollHeight
- 可滚动高度的+padding
-
scrollTop
- 当前元素距离顶部的距
-
触底加载
- scrollTop + clientHeight >= scrollHeight - 50px
- scrollTop + clientHeight >= scrollHeight - 50px
bom和dom的区别⭐⭐⭐
bom就是window,包含windows(窗口)、navigator(浏览器)、screen(浏览器屏幕)、history(访问历史)、location(地址)等,浏览器相关的东西。bom是包含dom的。
dom是document, html相关的都在里面
倒计时用setimeout来实现还是setInterval⭐⭐⭐⭐
- setTimeout
- 因为假如用setInterval的话,该程序执行需要105ms,而设置的间隔为100ms,则还没运行完最后的那5毫秒就会运行下一次的函数
promise相对于async…await的优缺点⭐⭐⭐
-
promise
- 无法取消
- 错误无法被try…catch捕获,但是可以被catch方法捕获
-
async传染力比较强
fetch优缺点 ⭐⭐⭐⭐
-
fetch脱离了XHR,基于promise实现
-
对某些错误不会reject,比如状态码400、500
-
fetch不支持超时timeout处理
-
fetch默认不携带cookie,需要手动配置
-
fetch没有办法监测请求进度,而xhr可以
秒传、分片传输、断点传输⭐⭐⭐⭐
- 秒传
- 文件上传前,服务器先对文件做MD5校验,如果服务器上有同样的文件,则返回一个新地址,如果不想秒传也可以,修改文件中的内容就可以了(改名字不行)
- 分片传输
- 利用Blob提供的slice方法把大文件分割为一个个小文件分别传输。全部上传完成时候由服务端进行归总整合
- 断点传输
- 在分片上传的基础上,分成一个个小文件之后,每个小文件上传完毕之后对其进行状态的存储(localStorage),如果中间发生网络断线或者刷新,下次可以接着上次的进度上传
e.target和e.currentTarget的区别⭐⭐⭐
e.target是点击的那个对象,e.currentTarget是绑定该事件的对象
JS性能优化的方式⭐⭐⭐⭐⭐
- 垃圾回收
- 闭包中的对象清楚
- 防抖节流
- 分批加载(setInterval,加载10000个节点)
- 事件委托
- 少用with
- requestAnimationFrame的使用
- script标签中的defer和async
- CDN
JS章节的结束了,下面迎接下一个boss——计算机网络
计算机网络
面试官:讲一讲三次握手四次挥手,为什么是三次握手四而不是两次握手?⭐⭐⭐⭐⭐
-
客户端和服务端之间通过三次握手建立连接,四次挥手释放连接
-
三次握手,客户端先向服务端发起一个SYN包,进入SYN_SENT状态,服务端收到SYN后,给客户端返回一个ACK+SYN包,表示已收到SYN,并进入SYN_RECEIVE状态,最后客户端再向服务端发送一个ACK包表示确认,双方进入establish状态。
- 之所以是三次握手而不是两次,是因为如果只有两次,在服务端收到SYN后,向客户端返回一个ACK确认就进入establish状态,万一这个请求中间遇到网络情况而没有传给客户端,客户端一直是等待状态,后面服务端发送的信息客户端也接受不到了。
-
四次挥手,首先客户端向服务端发送一个FIN包,进入FIN_WAIT1状态,服务端收到后,向客户端发送ACK确认包,进入CLOSE_WAIT状态,然后客户端收到ACK包后进入FIN_WAIT2状态,然后服务端再把自己剩余没传完的数据发送给客户端,发送完毕后在发送一个FIN+ACK包,进入LAST_ACK(最后确认)状态,客户端收到FIN+ACK包后,再向服务端发送ACK包,在等待两个周期后在关闭连接
- 之所以等待两个周期是因为最后客户端发送的ACK包可能会丢失,如果不等待2个周期的话,服务端在没收收到ACK包之前,会不停的重复发送FIN包而不关闭,所以得等待两个周期
面试官:HTTP的结构⭐⭐⭐⭐
- 请求行 请求头 空行 请求体
- 请求行包括 http版本号,url,请求方式
- 响应行包括版本号,状态码,原因
HTTP头都有哪些字段⭐⭐⭐⭐
- 请求头
- cache-control 是否使用缓存
- Connection:keep-alive 与服务器的连接状态
- Host 主机域
- 返回头
- cache-control
- etag 唯一标识,缓存用的
- last-modified最后修改时间
面试官:说说你知道的状态码⭐⭐⭐⭐⭐
- 2开头的表示成功
- 一般见到的就是200
- 3开头的表示重定向
- 301永久重定向
- 302临时重定向
- 304表示可以在缓存中取数据(协商缓存)
- 4开头表示客户端错误
- 403跨域
- 404请求资源不存在
- 5开头表示服务端错误
- 500
- 504网关错误
网络OSI七层模型都有哪些?TCP是哪一层的⭐⭐⭐⭐
- 七层模型
- 应用层
- 表示层
- 会话层
- 传输层
- 网络层
- 数据链路层
- 物理层
- TCP属于传输层