- JavaScript 相关
- 数据类型
- 类型转换
- 在 JS 中为什么 0.2+0.1>0.3?⭐⭐⭐⭐
- 那为什么 0.2+0.3=0.5 呢?⭐⭐⭐⭐
- 那既然 0.1 不是 0.1 了,为什么在 console.log(0.1)的时候还是 0.1 呢?⭐⭐⭐
- 判断数据类型有几种方法⭐⭐⭐⭐⭐
- instanceof 原理⭐⭐⭐⭐⭐
- 为什么 typeof null 是 Object⭐⭐⭐⭐
- ==和===有什么区别⭐⭐⭐⭐⭐
- 手写 call、apply、bind⭐⭐⭐⭐⭐
- 字面量创建对象和 new 创建对象有什么区别,new 内部都实现了什么,手写一个 new ⭐⭐⭐⭐⭐
- 字面量 new 出来的对象和 Object.create(null)创建出来的对象有什么区别⭐⭐⭐
- 执行栈和执行上下文
- 闭包
- 原型和原型链
- 继承
- 内存泄露、垃圾回收机制
- 深拷贝和浅拷贝
- 单线程,同步异步
- 为什么 JS 是单线程的?⭐⭐⭐⭐⭐
- 如何实现异步编程?
- Generator 是怎么样使用的以及各个阶段的变化如何?⭐⭐⭐
- Promise 的原理?你是如何理解 Promise 的?⭐⭐⭐⭐⭐
- 以下代码的执行顺序是什么?⭐⭐⭐⭐⭐
- 宏任务和微任务都有哪些?⭐⭐⭐⭐⭐
- 宏任务和微任务都是怎样执行的?⭐⭐⭐⭐⭐
- 变量提升
- 变量和函数怎么进行提升的?优先级是怎么样的?⭐⭐⭐⭐
- 箭头函数和普通函数的区别?箭头函数可以当做构造函数 new 吗?⭐⭐⭐⭐⭐
- 说说你对代理的理解⭐⭐⭐
- 为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?⭐⭐⭐
- exports 和 module.exports 有什么区别?⭐⭐⭐
- JS 模块包装格式有哪些?⭐⭐⭐
- ES6 和 commonjs 的区别⭐⭐⭐
- 跨域
- 网络原理
- 讲一讲三次握手四次挥手,为什么是三次握手四而不是两次握手?⭐⭐⭐⭐⭐
- HTTP 的结构⭐⭐⭐⭐
- HTTP 头都有哪些字段⭐⭐⭐⭐
- 说说你知道的状态码⭐⭐⭐⭐⭐
- 网络 OSI 七层模型都有哪些?TCP 是哪一层的⭐⭐⭐⭐
- http1.0 和 http1.1,还有 http2 有什么区别?⭐⭐⭐⭐
- https 和 http 有什么区别,https 的实现原理?⭐⭐⭐⭐⭐
- localStorage、SessionStorage、cookie、session 之间有什么区别⭐⭐⭐⭐⭐
- localstorage 存满了怎么办?⭐⭐⭐
- 怎么使用 cookie 保存用户信息⭐⭐⭐
- 怎么删除 cookie⭐⭐⭐
- Get 和 Post 的区别⭐⭐⭐⭐⭐
- 讲讲 http 缓存⭐⭐⭐⭐⭐
- tcp 和 udp 有什么区别⭐⭐⭐⭐⭐
- 从浏览器输入 url 后都经历了什么?⭐⭐⭐⭐⭐⭐具重要!
- 滑动窗口和拥塞窗口有什么区别⭐⭐⭐
- 什么是 CDN?⭐⭐⭐⭐
- 什么是 xss?什么是 csrf?⭐⭐⭐⭐⭐
- OWASP top10 (10 项最严重的 Web 应用程序安全风险列表)都有哪些?⭐⭐⭐
- 什么是回流 什么是重绘?⭐⭐⭐⭐⭐
- 杂项
- Vue
- Webpack 相关
- CSS/HTML 相关
- flex 布局⭐⭐⭐⭐⭐
- grid 布局⭐⭐⭐⭐
- 常见的行内元素和块级元素都有哪些?⭐⭐⭐⭐⭐
- 请说明 px,em,rem,vw,vh,rpx 等单位的特性⭐⭐⭐⭐⭐
- 常见的替换元素和非替换元素?⭐⭐
- first-of-type 和 first-child 有什么区别⭐⭐⭐⭐
- doctype 标签和 meta 标签⭐⭐⭐⭐⭐
- script 标签中 defer 和 async 都表示了什么⭐⭐⭐⭐⭐
- 什么是 BFC?⭐⭐⭐⭐⭐
- 如何清除浮动⭐⭐⭐⭐⭐
- 什么是 DOM 事件流?什么是事件委托⭐⭐⭐⭐⭐
- link 标签和 import 标签的区别⭐⭐⭐⭐
- 算法
- git 相关
本文整理自 2021 年开始,技术面是常考的面试题,虽然已经 2202 年了,相信这些面试题依然是常考点。接下来你要面对上百道面试题,如果你是小白菜,推荐使用 2~3 周的时间来消化接下来的面试题,遇到不会的没听说过名词请立刻去搜,文章中只是简答,如果想要详细了解的话还需要你自觉去搜索。当然,大佬就绕道了。如果有错误的地方烦请各位一定要指出,免得误导更多人。接下来的题我会根据重点程度使用⭐来标记,⭐越多标明越重点,满星是 5 颗星。好了,废话不多说,让我们一起觉醒吧!
JavaScript 相关
数据类型
JavaScript 中什么是基本数据类型什么是引用数据类型?以及各个数据类型是如何存储的?⭐⭐⭐⭐⭐
基本数据类型有:
- Number
- String
- Boolean
- Null
- Undefined
- Symbol(ES6 新增数据类型)
- bigInt
引用数据类型统称为 Object 类型,细分的话有:
- Object
- Array
- Date
- Function
- RegExp
基本数据类型的数据直接存储在栈中;而引用数据类型的数据存储在堆中,在栈中保存数据的引用地址,这个引用地址指向的是对应的数据,以便快速查找到堆内存中的对象。
顺便提一句,栈内存是自动分配内存的。而堆内存是动态分配内存的,不会自动释放。所以每次使用完对象的时候都要把它设置为 null,从而减少无用内存的消耗。
类型转换
在 JS 中为什么 0.2+0.1>0.3?⭐⭐⭐⭐
因为在 JS 中,浮点数是使用 64 位固定长度来表示的,其中的 1 位表示符号位,11 位用来表示指数位,剩下的 52 位尾数位,由于只有 52 位表示尾数位。
而0.1
转为二进制是一个无限循环数0.0001100110011001100......
(1100 循环)
// 0.1 和 0.2 都转化成二进制后再进行运算 0.00011001100110011001100110011001100110011001100110011010 + 0.0011001100110011001100110011001100110011001100110011010 = 0.0100110011001100110011001100110011001100110011001100111 // 转成十进制正好是 0.30000000000000004
那为什么 0.2+0.3=0.5 呢?⭐⭐⭐⭐
// 0.2 和 0.3 都转化为二进制后再进行计算 0.001100110011001100110011001100110011001100110011001101 + 0.0100110011001100110011001100110011001100110011001101 = 0.10000000000000000000000000000000000000000000000000001 //尾数为大于 52 位 // 而实际取值只取 52 位尾数位,就变成了 0.1000000000000000000000000000000000000000000000000000 //0.5
0.2
和0.3
分别转换为二进制进行计算:在内存中,它们的尾数位都是等于 52 位的,而他们相加必定大于 52 位,而他们相加又恰巧前 52 位尾数都是0
,截取后恰好是0.1000000000000000000000000000000000000000000000000000
也就是 0.5
那既然 0.1 不是 0.1 了,为什么在 console.log(0.1)的时候还是 0.1 呢?⭐⭐⭐
在console.log
的时候会二进制转换为十进制,十进制再会转为字符串的形式,在转换的过程中发生了取近似值,所以打印出来的是一个近似值的字符串。
判断数据类型有几种方法⭐⭐⭐⭐⭐
typeof
缺点:typeof null
的值为Object
,无法分辨是null
还是Object
。instanceof
缺点:只能判断对象是否存在于目标对象的原型链上。constructor
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]"
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 转化为原始类型
手写 call、apply、bind⭐⭐⭐⭐⭐
- call 和 apply 实现思路主要是:
- 判断是否是函数调用,若非函数调用抛异常
- 通过新对象(context)来调用函数
- 给 context 创建一个
fn
设置为需要调用的函数 - 结束调用完之后删除
fn
- 给 context 创建一个
- bind 实现思路
- 判断是否是函数调用,若非函数调用抛异常
- 返回函数
- 判断函数的调用方式,是否是被 new 出来的
- new 出来的话返回空对象,但是实例的
__proto__
指向_this
的prototype
- new 出来的话返回空对象,但是实例的
- 判断函数的调用方式,是否是被 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 }
字面量 new 出来的对象和 Object.create(null)创建出来的对象有什么区别⭐⭐⭐
字面量和 new 创建出来的对象会继承 Object 的方法和属性,他们的隐式原型会指向 Object 的显式原型,
而 Object.create(null)
创建出来的对象原型为 null,作为原型链的顶端,自然也没有继承 Object 的方法和属性
执行栈和执行上下文
什么是作用域,什么是作用域链?⭐⭐⭐⭐
规定变量和函数的可使用范围称作作用域;
每个函数都有一个作用域链,查找变量或者函数时,需要从局部作用域到全局作用域依次查找,这些作用域的集合称作作用域链。
什么是执行栈,什么是执行上下文?⭐⭐⭐⭐
执行上下文分为:
- 全局执行上下文
- 创建一个全局的
window
对象,并规定this
指向window
,执行 js 的时候就压入栈底,关闭浏览器的时候才弹出。
- 创建一个全局的
- 函数执行上下文
- 每次函数调用时,都会新创建一个函数执行上下文;
- 执行上下文分为创建阶段和执行阶段:
- 创建阶段:函数环境会创建变量对象:
arguments
对象(并赋值)、函数声明(并赋值)、变量声明(不赋值),函数表达式声明(不赋值);会确定this
指向;会确定作用域; - 执行阶段:变量赋值、函数表达式赋值,使变量对象编程活跃对象。
- 创建阶段:函数环境会创建变量对象:
eval
执行上下文。
执行栈:
- 首先栈特点:先进后出;
- 当进入一个执行环境,就会创建出它的执行上下文,然后进行压栈,当程序执行完成时,它的执行上下文就会被销毁,进行弹栈;
- 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文;
- 只有浏览器关闭的时候全局执行上下文才会弹出。
闭包
很多人都吃不透 js 闭包,这里推荐一篇文章:JavaScript 闭包实例讲解
什么是闭包?闭包的作用?闭包的应用?⭐⭐⭐⭐⭐
函数执行,形成私有的执行上下文,使内部私有变量不受外界干扰,起到保护和保存的作用。
作用:
- 保护
- 避免命名冲突。
- 保存
- 解决循环绑定引发的索引问题。
- 变量不会销毁
- 可以使用函数内部的变量,使变量不会被垃圾回收机制回收。
应用:
- 设计模式中的单例模式;
- for 循环中的保留 i 的操作;
- 防抖和节流;
- 函数柯里化。
缺点:
- 会出现内存泄漏的问题。
原型和原型链
什么是原型?什么是原型链?如何理解⭐⭐⭐⭐⭐
原型:原型分为隐式原型和显式原型,每个对象都有一个隐式原型,它指向自己的构造函数的显式原型。
原型链:多个__proto__
组成的集合成为原型链。
- 所有实例的
__proto__
都指向他们构造函数的prototype
- 所有的
prototype
都是对象,自然它的__proto__
指向的是Object()
的prototype
- 所有的构造函数的隐式原型指向的都是
Function()
的显示原型 Object
的隐式原型是null
关于原型与原型链,推荐阅读:我对 js 中原型原型链的理解(有图有真相)
继承
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);
ES6 的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 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)) } // let obj = { a: { c: 1 }, b: {} }; // obj.b = obj; // console.log(JSON.parse(JSON.stringify(obj))) // 报错 // Converting circular structure to JSON
深拷贝能使用 hash 递归的方式写出来就可以了,不过技多不压身,推荐还是看一看使用 while 实现深拷贝方法。
单线程,同步异步
为什么 JS 是单线程的?⭐⭐⭐⭐⭐
因为 JS 里面有可视的 Dom,如果是多线程的话,这个线程正在删除 DOM 节点,另一个线程正在编辑 Dom 节点,导致浏览器不知道该听谁的。
如何实现异步编程?
回调函数
Generator 是怎么样使用的以及各个阶段的变化如何?⭐⭐⭐
- 首先生成器是一个函数,用来返回迭代器的;
- 调用生成器后不会立即执行,而是通过返回的迭代器来控制这个生成器的一步一步执行的;
- 通过调用迭代器的
next
方法来请求一个一个的值,返回的对象有两个属性,一个是value
,也就是值;另一个是done
,是个布尔类型,done
为true
说明生成器函数执行完毕,没有可返回的值了; done
为true
后继续调用迭代器的next
方法,返回值的value
为undefined
状态变化:
- 每当执行到
yield
属性的时候,都会返回一个对象 - 这时候生成器处于一个非阻塞的挂起状态
- 调用迭代器的
next
方法的时候,生成器又从挂起状态改为执行状态,继续上一次的执行位置执行 - 直到遇到下一次
yield
依次循环 - 直到代码没有
yield
了,就会返回一个结果对象done
为 true,value
为undefined
Promise 的原理?你是如何理解 Promise 的?⭐⭐⭐⭐⭐
做到会写简易版的 promise 和 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)) } }) }
以下代码的执行顺序是什么?⭐⭐⭐⭐⭐
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
、setTimeOut
、setInterval
、setImmediate
微任务:promise.then
,process.nextTick
、Object.observe
、MutationObserver
注意:Promise 是同步任务。
宏任务和微任务都是怎样执行的?⭐⭐⭐⭐⭐
- 执行宏任务 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,4,5,10,8,9,3,6,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
变量提升
变量和函数怎么进行提升的?优先级是怎么样的?⭐⭐⭐⭐
var
var
声明的变量可进行变量提升,let
和const
不会var
可以重复声明var
在非函数作用域中定义是挂在到window
上的
let
let
声明的变量只在局部起作用let
防止变量污染- 不可在声明
const
- 具有
let
的所有特征 - 不可被改变
- 如果使用
const
声明的是对象的话,是可以修改对象里面的值的
- 如果使用
- 具有
箭头函数和普通函数的区别?箭头函数可以当做构造函数 new 吗?⭐⭐⭐⭐⭐
- 箭头函数是普通函数的简写,但是它不具备很多普通函数的特性;
- 第一点,
this
指向问题,箭头函数的this
指向它定义时所在的对象,而不是调用时所在的对象 - 不会进行函数提升;
- 没有
arguments
对象,不能使用arguments
,如果要获取参数的话可以使用rest
运算符; - 没有
yield
属性,不能作为生成器Generator
使用; - 不能
new
- 没有自己的
this
,不能调用call
和apply
; - 没有
prototype
,new
关键字内部需要把新对象的_proto_
指向函数的prototype
。
- 没有自己的
说说你对代理的理解⭐⭐⭐
- 代理有几种定义方式:
- 字面量定义,对象里面的
get
和set
; - 类定义, class 中的
get
和set
; - Proxy 对象,里面传两个对象,第一个对象是目标对象
target
,第二个对象是专门放get
和set
的handler
对象。Proxy
和上面两个的区别在于Proxy
专门对对象的属性进行get
和set
。
- 字面量定义,对象里面的
- 代理的实际应用有:
- Vue 的双向绑定 vue2 用的是
Object.defineProperty
,vue3 用的是proxy
; - 校验值;
- 计算属性值(get 的时候加以修饰)。
- Vue 的双向绑定 vue2 用的是
为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?⭐⭐⭐
为什么要使用模块化:
- 可以解决命名冲突
- 管理依赖
- 提高代码的可读性
- 代码解耦,提高代码的复用性
立即执行函数
在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题。
(function(globalVariable){ globalVariable.test = function() {} // ... 声明各种变量、函数都不会污染全局作用域 })(globalVariable)
AMD 和 CMD
鉴于目前这两种实现方式已经很少见到,所以不再对具体特性细聊,只需要了解这两者是如何使用的。
// AMD define(['./a', './b'], function(a, b) { // 加载模块完毕可以使用 a.do() b.do() }) // CMD define(function(require, exports, module) { // 加载模块 // 可以把 require 写在函数体的任意地方实现延迟加载 var a = require('./a') a.doSomething() })
CommonJS
CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,当然目前在 Node 中的模块管理已经和 CommonJS 有一些区别了。
// a.js module.exports = { a: 1 } // or exports.a = 1 // b.js var module = require('./a.js') module.a // -> log 1
因为 CommonJS 还是会使用到的,所以这里会对一些疑难点进行解析
先说 require 吧
var module = require('./a.js') module.a // 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了, // 重要的是 module 这里,module 是 Node 独有的一个变量 module.exports = { a: 1 } // module 基本实现 var module = { id: 'xxxx', // 我总得知道怎么去找到他吧 exports: {} // exports 就是个空对象 } // 这个是为什么 exports 和 module.exports 用法相似的原因 var exports = module.exports var load = function (module) { // 导出的东西 var a = 1 module.exports = a return module.exports }; // 然后当我 require 的时候去找到独特的 // id,然后将要使用的东西用立即执行函数包装下,over
另外虽然 exports
和 module.exports
用法相似,但是不能对 exports
直接赋值。因为 var exports = module.exports
这句代码表明了 exports
和 module.exports
享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对 exports
赋值就会导致两者不再指向同一个内存地址,修改并不会对 module.exports
起效。
ES Module
ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别
- CommonJS 支持动态导入,也就是
require(${path}/xx.js)
,后者目前不支持,但是已有提案 - CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
- CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
- ES Module 会编译成
require/exports
来执行的
// 引入模块 API import XXX from './a.js' import { XXX } from './a.js' // 导出模块 API export function a() {} export default function() {}
exports 和 module.exports 有什么区别?⭐⭐⭐
- 导出方式不一样
exports.xxx='xxx'
module.export = {}
exports
是module.exports
的引用,两个指向的是用一个地址,而 require 能看到的只有module.exports
JS 模块包装格式有哪些?⭐⭐⭐
- commonjs
- 同步运行,不适合前端
AMD
- 异步运行
- 异步模块定义,主要采用异步的方式加载模块,模块的加载不影响后面代码的执行。所有依赖这个模块的语句都写在一个回调函数中,模块加载完毕,再执行回调函数
CMD
- 异步运行
- seajs 规范
ES6 和 commonjs 的区别⭐⭐⭐
commonjs
模块输出的是值的拷贝,而 ES6 输出的值是值的引用
commonjs
是在运行时加载,是一个对象,ES6 是在编译时加载,是一个代码块
commonjs
的 this 指向当前模块,ES6 的 this 指向 undefined
跨域
跨域的方式都有哪些?他们的特点是什么 ⭐⭐⭐⭐⭐
JSONP⭐⭐⭐⭐⭐
JSONP
通过同源策略涉及不到的”漏洞”,也就是像img
中的src
,link
标签的href
,script
的src
都没有被同源策略限制到JSONP
只能 get 请求- 源码:
function addScriptTag(src) { var script = document.createElement("script") script.setAttribute('type', 'text/javascript') script.src = src document.appendChild(script) } // 回调函数 function endFn(res) { console.log(res.message); } // 前后端商量好,后端如果传数据的话,返回`endFn({message:'hello'})`
document.domain⭐
- 只能跨一级域名相同的域(www.xxx.om 和 www.id.xxx.com , 二者都有 xxx.com)
- 使用方法
>
表示输入,<
表示输出 ,以下是在www.id.aa.com
网站下执行的操作 > var w = window.open("https://www.xxx.com") < undefined > w.document ✖ VM3061:1 Uncaught DOMException: Blocked a frame with origin "https://id.xxx.com" from accessing a cross-origin frame. at :1:3 > document.domain < "id.xxx.com" > document.domain = 'xxx.com' < "xxx.com" > w.document < #document
location.hash + iframe⭐⭐
- 因为 hash 传值只能单向传输,所有可以通过一个中间网页,a 若想与 b 进行通信,可以通过一个与 a 同源的 c 作为中间网页,a 传给 b,b 传给 c,c 再传回 a
- 具体做法:在 a 中放一个回调函数,方便 c 回调。放一个 iframe 标签,随后传值
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe> <script> var iframe = document.getElementById('iframe'); // 向 b.html 传 hash 值 setTimeout(function() { iframe.src = iframe.src + '#user=admin'; }, 1000); // 开放给同域 c.html 的回调方法 function onCallback(res) { alert('data from c.html ---> ' + res); } </script>
- 在 b 中监听哈希值改变,一旦改变,把 a 要接收的值传给 c
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe> <script> var iframe = document.getElementById('iframe'); // 监听 a.html 传来的 hash 值,再传给 c.html window.onhashchange = function () { iframe.src = iframe.src + location.hash; }; </script>
- 在 c 中监听哈希值改变,一旦改变,调用 a 中的回调函数
<script> // 监听 b.html 传来的 hash 值 window.onhashchange = function () { // 再通过操作同域 a.html 的 js 回调,将结果传回 window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', '')); }; </script>
- 具体做法:在 a 中放一个回调函数,方便 c 回调。放一个 iframe 标签,随后传值
window.name + iframe⭐⭐
- 利 Access 用 window.name 不会改变(而且很大)来获取数据,
- a 要获取 b 的数据,b 中把数据转为 json 格式放到 window.name 中
postMessage⭐
- a 窗口向 b 窗口发送数据,先把 data 转为 json 格式,在发送。提前设置好 messge 监听
- b 窗口进行 message 监听,监听到了以同样的方式返回数据,
- a 窗口监听到 message,在进行一系列操作
CORS⭐⭐⭐⭐⭐
-
- 通过自定义请求头来让服务器和浏览器进行沟通
- 有简单请求和非简单请求
- 满足以下条件,就是简单请求
- 请求方法是
HEAD
、POST
、GET
- 请求头只有
Accept
、AcceptLanguage
、ContentType
、ContentLanguage
、Last-Event-Id
- 请求方法是
- 简单请求,浏览器自动添加一个 Origin 字段
- 同时后端需要设置的请求头
Access-Control-Allow-Origin
–必须- Access-Control-Expose-Headers
XMLHttpRequest
只能拿到六个字段,要想拿到其他的需要在这里指定
Access-Control-Allow-Credentials
–是否可传 cookie- 要是想传
cookie
,前端需要设置xhr.withCredentials = true
,后端设置Access-Control-Allow-Credentials
- 同时后端需要设置的请求头
- 非简单请求,浏览器判断是否为简单请求,如果是非简单请求,则 浏览器先发送一个 header 头为 option 的请求进行预检
- 预检请求格式(请求行 的请求方法为 OPTIONS(专门用来询问的))
Origin
Access-Control-Request-Method
Access-Control-Request-Header
- 浏览器检查了
Origin
、Access-Control-Allow-Method
和Access-Control-Request-Header
之后确认允许就可以做出回应了; - 通过预检后,浏览器接下来的每次请求就类似于简单请求了。
- 预检请求格式(请求行 的请求方法为 OPTIONS(专门用来询问的))
nginx 代理跨域⭐⭐⭐⭐
- nginx 模拟一个虚拟服务器,因为服务器与服务器之间是不存在跨域的,
- 发送数据时 ,客户端->nginx->服务端
- 返回数据时,服务端->nginx->客户端
网络原理
讲一讲三次握手四次挥手,为什么是三次握手四而不是两次握手?⭐⭐⭐⭐⭐
- 客户端和服务端之间通过三次握手建立连接,四次挥手释放连接
- 三次握手,客户端先向服务端发起一个 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
网络 OSI 七层模型都有哪些?TCP 是哪一层的⭐⭐⭐⭐
七层模型:
- 应用层
- 表示层
- 会话层
- 传输层
- 网络层
- 数据链路层
- 物理层
TCP 属于传输层。
http1.0 和 http1.1,还有 http2 有什么区别?⭐⭐⭐⭐
- http0.9 只能进行 get 请求;
- http1.0 添加了 POST,HEAD,OPTION,PUT,DELETE 等;
- http1.1 增加了长连接 keep-alive,增加了 host 域,而且节约带宽;
- http2 多路复用,头部压缩,服务器推送。
https 和 http 有什么区别,https 的实现原理?⭐⭐⭐⭐⭐
- http 无状态无连接,而且是明文传输,不安全
- https 传输内容加密,身份验证,保证数据完整性
- https 实现原理⭐⭐⭐⭐⭐
- 首先客户端向服务端发起一个随机值,以及一个加密算法
- 服务端收到后返回一个协商好的加密算法,以及另一个随机值
- 服务端在发送一个公钥 CA
- 客户端收到以后先验证 CA 是否有效,如果无效则报错弹窗,有过有效则进行下一步操作
- 客户端使用之前的两个随机值和一个预主密钥组成一个会话密钥,在通过服务端传来的公钥加密把会话密钥发送给服务端
- 服务端收到后使用私钥解密,得到两个随机值和预主密钥,然后组装成会话密钥
- 客户端在向服务端发起一条信息,这条信息使用会话秘钥加密,用来验证服务端时候能收到加密的信息
- 服务端收到信息后返回一个会话秘钥加密的信息
- 都收到以后 SSL 层连接建立成功
localStorage、SessionStorage、cookie、session 之间有什么区别⭐⭐⭐⭐⭐
- localStorage
- 生命周期:关闭浏览器后数据依然保留,除非手动清除,否则一直在;
- 作用域:相同浏览器的不同标签在同源情况下可以共享 localStorage。
- sessionStorage
- 生命周期:关闭浏览器或者标签后即失效;
- 作用域:只在当前标签可用,当前标签的 iframe 中且同源可以共享。
- cookie
- 是保存在客户端的,一般由后端设置值,可以设置过期时间;
- 储存大小只有 4K;
- 一般用来保存用户的信息的;
- 在 http 下 cookie 是明文传输的,较不安全;
- cookie 属性有
- http-only:不能被客户端更改访问,防止 XSS 攻击(保证 cookie 安全性的操作);
- Secure:只允许在 https 下传输;
- Max-age: cookie 生成后失效的秒数;
- expire: cookie 的最长有效时间,若不设置则 cookie 生命期与会话期相同。
- session
- session 是保存在服务端的;
- session 的运行依赖 sessionId,而 sessionId 又保存在 cookie 中,所以如果禁用的 cookie,session 也是不能用的,不过硬要用也可以,可以把 sessionId 保存在 URL 中;
- session 一般用来跟踪用户的状态;
- session 的安全性更高,保存在服务端,不过一般为使服务端性能更加,会考虑部分信息保存在 cookie 中。
localstorage 存满了怎么办?⭐⭐⭐
- 划分域名,各域名下的存储空间由各业务组统一规划使用;
- 跨页面传数据:考虑单页应用、采用 url 传输数据;
- 最后兜底方案:情调别人的存储。
怎么使用 cookie 保存用户信息⭐⭐⭐
document.cookie(“名字 = 数据;expire=时间”)
怎么删除 cookie⭐⭐⭐
目前没有提供删除的操作,但是可以把它的 Max-age 设置为 0,也就是立马失效,也就是删除了
Get 和 Post 的区别⭐⭐⭐⭐⭐
- 冪等/不冪等(可缓存/不可缓存)
- get 请求是冪等的,所以 get 请求的数据是可以缓存的
- 而 post 请求是不冪等的,查询查询对数据是有副作用的,是不可缓存的
- 传参
- get 传参,参数是在 url 中的
- 准确的说 get 传参也可以放到 body 中,只不过不推荐使用
- post 传参,参数是在请求体中
- 准确的说 post 传参也可以放到 url 中,只不过不推荐使用
- get 传参,参数是在 url 中的
- 安全性
- get 较不安全
- post 较为安全
- 准确的说两者都不安全,都是明文传输的,在路过公网的时候都会被访问到,不管是 url 还是 header 还是 body,都会被访问到,要想做到安全,就需要使用 https
- 参数长度
- get 参数长度有限,是较小的
- 准确来说,get 在 url 传参的时候是很小的
- post 传参长度不受限制
- get 参数长度有限,是较小的
- 发送数据
- post 传参发送两个请求包,一个是请求头,一个是请求体,请求头发送后服务器进行验证,要是验证通过的话就会给客户端发送一个 100-continue 的状态码,然后就会发送请求体
- 字符编码
- get 在 url 上传输的时候只允许 ASCII 编码
更详细的推荐阅读这篇文章:传送门
讲讲 http 缓存⭐⭐⭐⭐⭐
- 缓存分为强缓存和协商缓存
- 强缓存
- 在浏览器加载资源时,先看看
cache-control
里的max-age
,判断数据有没有过期,如果没有直接使用该缓存 ,有些用户可能会在没有过期的时候就点了刷新按钮,这个时候浏览器就回去请求服务端,要想避免这样做,可以在cache-control
里面加一个immutable
. - public
- 允许客户端和虚拟服务器缓存该资源,cache-control 中的一个属性
- private
- 只允许客户端缓存该资源
- no-cache
- 不允许强缓存,可以协商缓存
- no-store
- 不允许缓存
- 在浏览器加载资源时,先看看
- 协商缓存
- 浏览器加载资源时,没有命中强缓存,这时候就去请求服务器,去请求服务器的时候,会带着两个参数,一个是
If-None-Match
,也就是响应头中的etag
属性,每个文件对应一个etag
;另一个参数是If-Modified-Since
,也就是响应头中的Last-Modified
属性,带着这两个参数去检验缓存是否真的过期,如果没有过期,则服务器会给浏览器返回一个 304 状态码,表示缓存没有过期,可以使用旧缓存。 etag
的作用- 有时候编辑了文件,但是没有修改,但是
last-modified
属性的时间就会改变,导致服务器会重新发送资源,但是etag
的出现就完美的避免了这个问题,他是文件的唯一标识
- 有时候编辑了文件,但是没有修改,但是
- 浏览器加载资源时,没有命中强缓存,这时候就去请求服务器,去请求服务器的时候,会带着两个参数,一个是
缓存位置:
- 内存缓存 Memory-Cache
- 离线缓存 Service-Worker
- 磁盘缓存 Disk-Cache
- 推送缓存 Push-Cache
相关文章推荐:浏览器缓存知识
tcp 和 udp 有什么区别⭐⭐⭐⭐⭐
- 连接方面
- tcp 面向连接,udp 不需要连接
- tcp 需要三次握手四次挥手请求连接
- tcp 面向连接,udp 不需要连接
- 可靠性
- tcp 是可靠传输;一旦传输过程中丢包的话会进行重传
- udp 是不可靠传输,但会最大努力交付
- 工作效率
- UDP 实时性高,比 TCP 工作效率高
- 因为不需要建立连接,更不需要复杂的握手挥手以及复杂的算法,也没有重传机制
- UDP 实时性高,比 TCP 工作效率高
- 是否支持多对多
- TCP 是点对点的
- UDP 支持一对一,一对多,多对多
- 首部大小
- tcp 首部占 20 字节
- udp 首部占 8 字节
从浏览器输入 url 后都经历了什么?⭐⭐⭐⭐⭐⭐具重要!
推荐阅读:前端浏览器输入 URL 后发生什么?
- 先进行 DNS 域名解析,先查看本地 hosts 文件,查看有没有当前域名对应的 ip 地址,若有直接发起请求,没有的话会在本地域名服务器去查找,该查找属于递归查找,如果本地域名服务器没查找到,会从根域名服务器查找,该过程属于迭代查找,根域名会告诉你从哪个与服务器查找,最后查找到对应的 ip 地址后把对应规则保存到本地的 hosts 文件中。
- 如果想加速以上及之后的 http 请求过程的话可以使用缓存服务器 CDN,CDN 过程如下:
- 用户输入 url 地址后,本地 DNS 会解析 url 地址,不过会把最终解析权交给 CNAME 指向的 CDN 的 DNS 服务器
- CDN 的 DNS 服务器会返回给浏览器一个全局负载均衡 IP
- 用户会根据全局负载均衡 IP 去请求全局负载均衡服务器
- 全局负载均衡服务器会根据用户的 IP 地址,url 地址,会告诉用户一个区域负载均衡设备,让用户去请求它。
- 区域负载均衡服务器会为用户选择一个离用户较近的最优的缓存服务器,并把 ip 地址给到用户
- 用户想缓存服务器发送请求,如果请求不到想要的资源的话,会一层层向上一级查找,知道查找到为止。
- 进行 http 请求,三次握手四次挥手建立断开连接
- 服务器处理,可能返回 304 也可能返回 200
- 返回 304 说明客户端缓存可用,直接使用客户端缓存即可,该过程属于协商缓存
- 返回 200 的话会同时返回对应的数据
- 客户端自上而下执行代码
- 其中遇到 CSS 加载的时候,CSS 不会阻塞 DOM 树的解析,但是会阻塞 DOM 树的渲染,并且 CSS 会阻塞下面的 JS 的执行
- 然后是 JS 加载,JS 加载会影响 DOM 的解析,之所以会影响,是因为 JS 可能会删除添加节点,如果先解析后加载的话,DOM 树还得重新解析,性能比较差。如果不想阻塞 DOM 树的解析的话,可以给 script 添加一个
defer
或者async
的标签。- defer:不会阻塞 DOM 解析,等 DOM 解析完之后在运行,在
DOMContentloaed
之前 - async: 不会阻塞 DOM 解析,等该资源下载完成之后立刻运行
- defer:不会阻塞 DOM 解析,等 DOM 解析完之后在运行,在
- 进行 DOM 渲染和 Render 树渲染
- 获取 html 并解析为 Dom 树
- 解析 css 并形成一个 cssom(css 树)
- 将 cssom 和 dom 合并成渲染树(render 树)
- 进行布局(layout)
- 进行绘制(painting)
- 回流重绘
- 回流必将引起重绘,重绘不一定引起回流
滑动窗口和拥塞窗口有什么区别⭐⭐⭐
- 滑动窗口
- 发送窗口永远小于或等于接收窗口,发送窗口的大小取决于接收窗口的大小
- 控制流量来保证 TCP 的可靠传输(不控制流量的话可能会溢出)
- 发送方的数据分为
- 1 已发送,接收到 ACK 的
- 2 已发送,未接收到 ACK 的
- 3 未发送,但允许发送的
- 4 未发送,但不允许发送的
- 2 和 3 表示发送窗口
- 接收方
- 1.已接收
- 2.未接受但准备接受
- 3.未接受不准备接受
- 拥塞窗口
- 防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。
- 是一个全局性的过程
- 方法
- 慢开始、拥塞避免、快重传、快恢复
什么是 CDN?⭐⭐⭐⭐
- 首先访问本地的 DNS ,如果没有命中,继续递归或者迭代查找,直到命中拿到对应的 IP 地址;
- 拿到对应的 IP 地址之后服务器端发送请求到目的地址。注意这里返回的不直接是 cdn 服务器的 IP 地址,而是全局负载均衡系统的 IP 地址;
- 全局负载均衡系统会根据客户端的 IP 地址和请求的 url 和相应的区域负载均衡系统通信;
- 区域负载均衡系统拿着这两个东西获取距离客户端最近且有相应资源的 cdn 缓存服务器的地址,返回给全局负载均衡系统;
- 全局负载均衡系统返回确定的 cdn 缓存服务器的地址给客户端;
- 客户端请求缓存服务器上的文件
什么是 xss?什么是 csrf?⭐⭐⭐⭐⭐
- xss 脚本注入
- 不需要你做任何的登录认证,它会通过合法的操作(比如在 url 中输入、在评论框中输入),向你的页面注入脚本(可能是 js、hmtl 代码块等)。
- 防御
- 编码:对用户输入的数据进行 HTML Entity 编码。把字符转换成 转义字符。Encode 的作用是将$var 等一些字符进行转化,使得浏览器在最终输出结果上是一样的。
- 过滤:移除用户输入的和事件相关的属性。
- csrf 跨域请求伪造
- 在未退出 A 网站的前提下访问 B,B 使用 A 的 cookie 去访问服务器
- 防御:token,每次用户提交表单时需要带上 token(伪造者访问不到),如果 token 不合法,则服务器拒绝请求
OWASP top10 (10 项最严重的 Web 应用程序安全风险列表)都有哪些?⭐⭐⭐
- SQL 注入
- 在输入框里输入 sql 命令
- 失效的身份验证
- 拿到别人的 cookie 来向服务端发起请求,就可以做到登陆的目的
- 敏感数据泄露
- 明文传输状态下可能被抓包拦截,这时候就造成数据泄露
- 想做到抓包,比如在网吧,共享一个猫上网,这时候抓包就可行,方法网上一搜一大把
- 不过此风险大部分网站都能得到很好的解决,https 或者 md5 加密都可以
- 明文传输状态下可能被抓包拦截,这时候就造成数据泄露
- XML 外部实体
- 失效的访问控制
- 安全配置错误
- XSS
- 不安全的反序列化
- 使用含有已知漏洞的组件
- 不足的日志记录和监控
什么是回流 什么是重绘?⭐⭐⭐⭐⭐
- 回流
- render 树中一部分或全部元素需要改变尺寸、布局、或着需要隐藏而需要重新构建,这个过程叫做回流
- 回流必将引起重绘
- 重绘
- render 树中一部分元素改变,而不影响布局的,只影响外观的,比如颜色。该过程叫做重绘
- 页面至少经历一次回流和重绘(第一次加载的时候)
杂项
事件冒泡和事件捕捉有什么区别⭐⭐⭐⭐⭐
- 事件冒泡
- 在 addEventListener 中的第三属性设置为 false(默认)
- 从下至上(儿子至祖宗)执行
- 事件捕捉
- 在 addEventListener 中的第三属性设置为 true
- 从上至下(祖宗到儿子)执行
什么是防抖?什么是节流?手写一个⭐⭐⭐⭐⭐
- 防抖
- 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)
什么是 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);
- 代替 new 创建一个对象,且这个对象想工厂制作一样,批量制作属性相同的实例对象(指向不同)
- 构造函数模式
- 发布订阅者模式
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()
- 代理模式
- 迭代器模式
JS 性能优化的方式⭐⭐⭐⭐⭐
- 垃圾回收;
- 闭包中的对象清楚;
- 防抖节流;
- 分批加载(
setInterval
,加载 10000 个节点); - 事件委托;
- 少用
with
; requestAnimationFrame
的使用;- script 标签中的
defer
和async
; - CDN
Vue
Vue 双向绑定
数据劫持: vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调
阐述一下你所理解的 MVVM 响应式原理⭐⭐⭐⭐⭐
vue 是采用数据劫持配合发布者-订阅者的模式的方式,通过Object.defineProperty()
来劫持各个属性的 getter 和 setter,在数据变动时,发布消息给依赖收集器(dep 中的 subs),去通知(notify)观察者,做出对应的回调函数,去更新视图。
MVVM 作为绑定的入口,整合 Observer,Compile 和 Watcher 三者,通过 Observer 来监听 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer,Compile 之间的通信桥路,达到数据变化=>视图更新;视图交互变化=>数据 model 变更的双向绑定效果。
杂乱笔记
- data 中每一个数据都绑定一个 Dep,这个 Dep 中都存有所有用到该数据的观察者
- 当数据改变时,发布消息给 dep(依赖收集器),去通知每一个观察者。做出对应的回调函数:
const dep = new Dep() // 劫持并监听所有属性 Object.defineProperty(obj, key, { enumerable: true, configurable: false, get() { // 订阅数据变化时,在 Dep 中添加观察者 Dep.target && dep.addSub(Dep.target) return value }, set: (newVal) => { if (newVal !== value) { this.observe(newVal) value = newVal } // 告诉 Dep 通知变化 dep.notify() }, })
说说 vue 的生命周期⭐⭐⭐⭐⭐
beforeCreate
- 创建之前,此时还没有 data 和 Method
Created
- 创建完成,此时 data 和 Method 可以使用了
- 在 Created 之后 beforeMount 之前如果没有 el 选项的话那么此时生命周期结束,停止编译,如果有则继续
beforeMount
- 在渲染之前
mounted
- 页面已经渲染完成,并且
vm
实例中已经添加完$el
了,已经替换掉那些 DOM 元素了(双括号中的变量),这个时候可以操作 DOM 了(但是是获取不了元素的高度等属性的,如果想要获取,需要使用nextTick()
)
- 页面已经渲染完成,并且
beforeUpdate
data
改变后,对应的组件重新渲染之前
updated
data
改变后,对应的组件重新渲染完成
beforeDestory
- 在实例销毁之前,此时实例仍然可以使用
destoryed
- 实例销毁后
vue 中父子组件的生命周期⭐⭐⭐⭐⭐
- 父子组件的生命周期是一个嵌套的过程
- 渲染的过程
- 父
beforeCreate
->父created
->父beforeMount
->子beforeCreate
->子created
->子beforeMount
->子mounted
->父mounted
- 父
- 子组件更新过程
- 父
beforeUpdate
->子beforeUpdate
->子updated
->父updated
- 父
- 父组件更新过程
- 父
beforeUpdate
->父updated
- 父
- 销毁过程
- 父
beforeDestroy
->子beforeDestroy
->子destroyed
->父destroyed
- 父
Vue 中的 nextTick⭐⭐⭐⭐⭐
nextTick
解释:
nextTick
:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
应用:
- 想要在 Vue 生命周期函数中的
created()
操作 DOM 可以使用Vue.nextTick()
回调函数 - 在数据改变后要执行的操作,而这个操作需要等数据改变后而改变 DOM 结构的时候才进行操作,需要用到
nextTick
computed 和 watch 的区别⭐⭐⭐⭐⭐
- computed:计算属性,依赖其他属性,当其他属性改变的时候下一次获取 computed 值时也会改变,
computed
的值会有缓存 - watch:
- 类似于数据改变后的回调
- 如果想深度监听的话,后面加一个 deep:true
- 如果想监听完立马运行的话,后面加一个 immediate:true
Vue 优化方式⭐⭐⭐⭐⭐
v-if
和v-show
;- 使用
Object.freeze()
方式冻结 data 中的属性,从而阻止数据劫持; - 组件销毁的时候会断开所有与实例联系,但是除了
addEventListener
,所以当一个组件销毁的时候需要手动去removeEventListener
; - 图片懒加载;
- 路由懒加载;
- 为减少重新渲染和创建 dom 节点的时间,采用虚拟 dom。
Vue-router 的模式⭐⭐⭐⭐⭐
- hash 模式
- 监听 hashchange 事件实现前端路由,利用 url 中的 hash 来模拟一个 hash,以保证 url 改变时,页面不会重新加载。
- history 模式
- 利用 pushstate 和 replacestate 来将 url 替换但不刷新,但是有一个致命点就是,一旦刷新的话,就会可能 404,因为没有当前的真正路径,要想解决这一问题需要后端配合,将不存在的路径重定向到入口文件。
MVC 与 MVVM 有什么区别?它和其它框架(jquery)的区别是什么?哪些场景适合?⭐⭐⭐⭐⭐
- mvc 和 mvvm 其实区别并不大。都是一种设计思想。主要就是 mvc 中 Controller 演变成 mvvm 中的
viewModel。mvvm 主要解决了 mvc 中大量的 DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。 - 区别:vue 数据驱动,通过数据来显示视图层而不是节点操作。
- 场景:数据操作比较多的场景,更加便捷
diff 算法⭐⭐⭐⭐⭐
diff 算法是指对新旧虚拟节点进行对比,并返回一个 patch 对象,用来存储两个节点不同的地方,最后利用 patch 记录的消息局部更新 DOM。
虚拟 DOM 的优缺点⭐⭐⭐⭐⭐
缺点:
- 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。
优点:
- 减少了 dom 操作,减少了回流与重绘;
- 保证性能的下限,虽说性能不是最佳,但是它具备局部更新的能力,所以大部分时候还是比正常的 DOM 性能高很多的。
Vue 的 Key 的作用 ⭐⭐⭐⭐
key 主要用在虚拟 Dom 算法中,每个虚拟节点 VNode 有一个唯一标识 Key,通过对比新旧节点的 key 来判断节点是否改变,用 key 就可以大大提高渲染效率,这个 key 类似于缓存中的 etag。
Vue 组件之间的通信方式⭐⭐⭐⭐⭐
- 子组件设置 props + 父组件设置
v-bind:
/:
- 父传子
- 子组件的$emit + 父组件设置
v-on
/@
- 子传父
- 任意组件通信,新建一个空的全局 Vue 对象,利用 emit 发送,on 接收
- 传说中的$bus
- 任意组件
-
Vue.prototype.Event = new Vue(); Event.$emit(事件名, 数据); Event.$on(事件名, data => { });
- vuex
- 里面的属性有:
- state
- 存储数据的
- 获取数据最好推荐使用 getters
- 硬要使用的话可以用 MapState, 先引用,放在 compute 中
...mapState(['方法名','方法名'])
- getters
- 获取数据的
- this.$store.getters.xxx
- 也可使用 mapGetters 先引用,放在 compute 中,
...mapGetters(['方法名','方法名'])
- mutations
- 同步操作数据的
- this.$store.commit(“方法名”,数据)
- 也可使用 mapMutations ,使用方法和以上一样
- actions
- 异步操作数据的
- this.$store.dispatch(“方法名”,数据)
- 也可使用 mapActions ,使用方法和以上一样
- modules
- 板块,里面可以放多个 vuex
- state
- 里面的属性有:
- 父组件通过
v-bind:
/:
传值,子组件通过this.$attrs
获取- 父传子
- 当子组件没有设置 props 的时候可以使用
this.$attrs
获取到的是一个对象(所有父组件传过来的集合)
- 祖先组件使用 provide 提供数据,子孙组件通过 inject 注入数据
- parent/children
- refs—$ref
- 还有一个,这个网上没有,我自己认为的,我觉得挺对的,slot-scope,本身父组件使用 slot 插槽是无法获取子组件的数据的,但是使用了 slot-scope 就可以获取到子组件的数据(拥有了子组件的作用域)
Vue-router 有哪几种钩子函数⭐⭐⭐⭐⭐
- beforeEach
- 参数有
- to(Route 路由对象)
- from(Route 路由对象)
- next(function 函数) 一定要调用才能进行下一步
- 参数有
- afterEach
- beforeRouterLeave
Webpack 相关
webpack 常用的几个对象及解释⭐⭐⭐⭐
- entry 入口文件
- output 输出文件
- 一般配合 node 的 path 模块使用
-
// 入口文件 entry: "./src/index.js", output: { // 输出文件名称 filename: "bundle.js", // 输出的路径(绝对路径) path: path.resolve(__dirname, "dist") //利用 node 模块的 path 绝对路径 }, // 设置模式 mode: "development"
-
- 一般配合 node 的 path 模块使用
- mode 设计模式
- module(loader)
- 里面有一个 rules 数组对某种格式的文件进行转换处理(转换规则)
- use 数组解析顺序是从下到上逆序执行的
-
module:{ // 对某种格式的文件进行转换处理(转换规则) rules:[ { // 用到正则表达式 test:/.css$/, //后缀名为 css 格式的文件 use:[ // use 数组解析顺序是从下到上逆序执行的 // 先用 css-loader 再用 style-loader // 将 js 的样式内容插入到 style 标签里 "style-loader", // 将 css 文件转换为 js "css-loader" ] } ] } // -----vue 的 module.exports={ module:{ rules:[ { test: /.vue$/, use:["vue-loader"] } ] } }
- plugin
- 插件配置
-
const uglifyJsPlugin = reqiure('uglifyjs-webpack-plugin') module.exports={ plugin:[ new uglifyJsPlugin() //丑化 ] }
- devServer
- 热更新
-
devServer:{ // 项目构建路径 contentBase:path.resolve(__dirname,"dist"), // 启动 gzip 亚索 compress:true, // 设置端口号 port:2020, // 自动打开浏览器:否 open:false, //页面实时刷新(实时监听) inline:true }
- resolve
- 配置路径规则
- alias 别名
-
module.exports= { resolve:{ //如果导入的时候不想写后缀名可以在 resolve 中定义 extensions extensions:['.js','.css','.vue'] //alias:别名 alias:{ //导入以 vue 结尾的文件时,会去寻找 vue.esm.js 文件 'vue$':"vue/dist/vue.esm.js" } } }
- babel(ES6 转 ES5)
- 下载插件
babel-loader
,在 module(loader)中配置
- 下载插件
loader 和 plugin 的区别是什么?⭐⭐⭐
- loader 是用来解析非 js 文件的,因为 Webpack 原生只能解析 js 文件,如果想把那些文件一并打包的话,就需要用到 loader,loader 使 webpack 具有了解析非 js 文件的能力;
- plugin 用来给 webpack 扩展功能的,可以加载许多插件。
CSS/HTML 相关
flex 布局⭐⭐⭐⭐⭐
grid 布局⭐⭐⭐⭐
常见的行内元素和块级元素都有哪些?⭐⭐⭐⭐⭐
- 行内元素 inline
- 不能设置宽高,多个元素共享一行,占满的时候会换行
- span、input、img、textarea、label、select
- 块级元素 block
- 可以设置宽高,一个元素占满一整行
- p、h1/h2/h3/h4/h5、div、ul、li、table
- inline-block
- 可以设置宽高,多个元素共享一行,占满的时候会换行
请说明 px,em,rem,vw,vh,rpx 等单位的特性⭐⭐⭐⭐⭐
- px
- 像素
- em
- 当前元素的字体大小
- rem
- 根元素字体大小
- vw
- 100vw 是总宽度
- vh
- 100vh 是总高度
- rpx
- 750rpx 是总宽度
相关阅读:
常见的替换元素和非替换元素?⭐⭐
- 替换元素
- 是指若标签的属性可以改变标签的显示方式就是替换元素,比如 input 的 type 属性不同会有不同的展现,img 的 src 等
- img、input、iframe
- 非替换元素
- div、span、p
first-of-type 和 first-child 有什么区别⭐⭐⭐⭐
- first-of-type
- 匹配的是从第一个子元素开始数,匹配到的那个的第一个元素。
- first-child
- 必须是第一个子元素
doctype 标签和 meta 标签⭐⭐⭐⭐⭐
doctype
- 告诉浏览器以什么样的文档规范解析文档
- 标准模式和兼容模式
- 标准模式 ->正常,排版和 js 运作模式都是以最高标准运行
- 兼容模式->非正常
script 标签中 defer 和 async 都表示了什么⭐⭐⭐⭐⭐
- 众所周知 script 会阻塞页面的加载,如果我们要是引用外部 js,假如这个外部 js 请求很久的话就难免出现空白页问题,好在官方为我们提供了 defer 和 async
- defer
<script src="d.js" defer></script> <script src="e.js" defer></script>
- 不会阻止页面解析,并行下载对应的 js 文件
- 下载完之后不会执行
- 等所有其他脚本加载完之后,在
DOMContentLoaded
事件之前执行对应d.js
、e.js
- async
<script src="b.js" async></script> <script src="c.js" async></script>
- 不会阻止 DOM 解析,并行下载对应的 js 文件
- 下载完之后立即执行
- 补充,
DOMContentLoaded
事件- 是等 HTML 文档完全加载完和解析完之后运行的事件
- 在
load
事件之前。 - 不用等样式表、图像等完成加载
什么是 BFC?⭐⭐⭐⭐⭐
- BFC 是一个独立渲染区域,它丝毫不会影响到外部元素
- BFC 特性
- 同一个 BFC 下 margin 会重叠
- 计算 BFC 高度时会算上浮动元素
- BFC 不会影响到外部元素
- BFC 内部元素是垂直排列的
- BFC 区域不会与 float 元素重叠
- 如何创建 BFC
- position 设为 absolute 或者 fixed
- float 不为 none
- overflow 设置为 hidden
- display 设置为 inline-block 或者 inline-table 或 flex
如何清除浮动⭐⭐⭐⭐⭐
额外标签clear:both
:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> .fahter{ width: 400px; border: 1px solid deeppink; } .big{ width: 200px; height: 200px; background: darkorange; float: left; } .small{ width: 120px; height: 120px; background: darkmagenta; float: left; } .clear{ clear:both; } </style> </head> <body> <div > <div >big</div> <div >small</div> <div >额外标签法</div> </div> </body>
利用 BFC:overflow:hidden
.fahter{ width: 400px; border: 1px solid deeppink; overflow: hidden; }
使用 after(推荐)
<style> .clearfix:after{/*伪元素是行内元素 正常浏览器清除浮动方法*/ content: ""; display: block; height: 0; clear:both; visibility: hidden; } .clearfix{ *zoom: 1;/*ie6 清除浮动的方式 *号只有 IE6-IE7 执行,其他浏览器不执行*/ } </style> <body> <div > <div >big</div> <div >small</div> </div> </body>
什么是 DOM 事件流?什么是事件委托⭐⭐⭐⭐⭐
- DOM 事件流
- 分为三个阶段
- 捕获阶段
- 目标阶段
- 冒泡阶段
- 在 addeventListener()的第三个参数(useCapture)设为 true,就会在捕获阶段运行,默认是 false 冒泡
- 分为三个阶段
- 事件委托
- 利用冒泡原理(子向父一层层穿透),把事件绑定到父元素中,以实现事件委托
link 标签和 import 标签的区别⭐⭐⭐⭐
- link 属于 html,而@import 属于 css
- 页面被加载时,link 会同时被加载,而@import 引用的 css 会等到页面加载结束后加载。
- link 是 html 标签,因此没有兼容性,而@import 只有 IE5 以上才能识别。
- link 方式样式的权重高于@import 的。
算法
冒泡算法排序⭐⭐⭐⭐⭐
// 冒泡排序 /* 1.比较相邻的两个元素,如果前一个比后一个大,则交换位置。 2.第一轮的时候最后一个元素应该是最大的一个。 3.按照步骤一的方法进行相邻两个元素的比较,这个时候由于最后一个元素已经是最大的了,所以最后一个元素不用比较。 */ function bubbleSort(arr) { for (var i = 0; i < arr.length; i++) { for (var j = 0; j < arr.length; j++) { if (arr[j] > arr[j + 1]) { var temp = arr[j] arr[j] = arr[j + 1] arr[j + 1] = temp } } } } var Arr = [3, 5, 74, 64, 64, 3, 1, 8, 3, 49, 16, 161, 9, 4] console.log(Arr, "before"); bubbleSort(Arr) console.log(Arr, "after");
快速排序⭐⭐⭐⭐⭐
/* 快速排序是对冒泡排序的一种改进,第一趟排序时将数据分成两部分,一部分比另一部分的所有数据都要小。 然后递归调用,在两边都实行快速排序。 */ function quickSort(arr) { if (arr.length <= 1) { return arr } var middle = Math.floor(arr.length / 2) var middleData = arr.splice(middle, 1)[0] var left = [] var right = [] for (var i = 0; i < arr.length; i++) { if (arr[i] < middleData) { left.push(arr[i]) } else { right.push(arr[i]) } } return quickSort(left).concat([middleData], quickSort(right)) } var Arr = [3, 5, 74, 64, 64, 3, 1, 8, 3, 49, 16, 161, 9, 4] console.log(Arr, "before"); var newArr = quickSort(Arr) console.log(newArr, "after");
插入排序⭐⭐⭐⭐
function insertSort(arr) { // 默认第一个排好序了 for (var i = 1; i < arr.length; i++) { // 如果后面的小于前面的直接把后面的插到前边正确的位置 if (arr[i] < arr[i - 1]) { var el = arr[i] arr[i] = arr[i - 1] var j = i - 1 while (j >= 0 && arr[j] > el) { arr[j + 1] = arr[j] j-- } arr[j + 1] = el } } } var Arr = [3, 5, 74, 64, 64, 3, 1, 8, 3, 49, 16, 161, 9, 4] console.log(Arr, "before"); insertSort(Arr) console.log(Arr, "after");
是否回文⭐⭐⭐⭐⭐
function isHuiWen(str) { return str == str.split("").reverse().join("") } console.log(isHuiWen("mnm"));
正则表达式,千分位分隔符⭐⭐⭐⭐
function thousand(num) { return (num + "").replace(/d(?=(d{3})+$)/g, "$&,") } console.log(thousand(123456789));
斐波那契数列⭐⭐⭐⭐⭐
// num1 前一项 // num2 当前项 function fb(n, num1 = 1, num2 = 1) { if (n == 0) return 0 if (n <= 2) { return num2 } else { return fb(n - 1, num2, num1 + num2) } }
数组去重的方式⭐⭐⭐⭐⭐
var arr = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2] // 最 low1 let newArr2 = [] for (let i = 0; i < arr.length; i++) { if (!newArr2.includes(arr[i])) { newArr2.push(arr[i]) } } console.log(newArr2); // 最 low2 let arr2 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2] for (let i = 0; i < arr2.length; i++) { var item = arr2[i] for (let j = i + 1; j < arr2.length; j++) { var compare = arr2[j]; if (compare === item) { arr2.splice(j, 1) j-- } } } console.log(arr2); // 基于对象去重 let arr3 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2] let obj = {} for (let i = 0; i < arr3.length; i++) { let item = arr3[i] if (obj[item]) { arr3[i] = arr3[arr3.length - 1] arr3.length-- i-- continue; } obj[item] = item } console.log(arr3); console.log(obj); // 利用 Set let newArr1 = new Set(arr) console.log([...newArr1]); let arr4 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2] //利用 reduce newArr4 = arr4.reduce((prev, curr) => prev.includes(curr)? prev : [...prev,curr],[]) console.log(newArr4); console.log(document);
git 相关
git 的常用命令⭐⭐⭐⭐⭐
- commit 之后撤回
- git reset soft HEAD^
- 分支
- git branch xx 创建分支
- git checkout xx 切换分支
- 添加
- git add .
- git push
- git commit -m