前端面试之道
2020-08-10 10:56:18 6 举报
AI智能生成
面试之道 掘金小册
作者其他创作
大纲/内容
小册食用指南
面试要治标更要治本
面向题目应对面试是没什么大的帮助的,即使有,也只是治标不治本。
因为每道面试题背后都会涉及到几个知识点,如果我们能够扎实地学习这些知识点的话,那么无论题目怎么变,只要涉及的知识点不变,那我们就能以不变应万变。
因为每道面试题背后都会涉及到几个知识点,如果我们能够扎实地学习这些知识点的话,那么无论题目怎么变,只要涉及的知识点不变,那我们就能以不变应万变。
改变一问一答的面试状态
在面试过程中,如果经常和面试官出现一问一答的情况的话,其实是不够理想的。虽然一道面试题看起来只涉及了一个知识点,但是如果你脑海中的知识点是串联起来的话,就可以引申出其他的知识点,这样能给到面试官一个好的印象。
每道思考题我都给出了一些个人的思路引导,帮助大家建立起知识点之间的串联关系,彻底理解这个模块中所涉及到的知识点。
每道思考题我都给出了一些个人的思路引导,帮助大家建立起知识点之间的串联关系,彻底理解这个模块中所涉及到的知识点。
养成记录与分享的习惯
写博客是一个很好的习惯,一方面能帮助自己理解知识,另一方面也能打造个人的影响力,所以我也很推荐大家能养成这个习惯。
JS 基础知识及常考面试题
原始Primitive类型
原始类型有哪几种?
boolean
null
undefined
number
string
symbol
null
undefined
number
string
symbol
null 是对象嘛?
null 不是 对象
另外对于 null 来说,很多人会认为他是个对象类型,其实这是错误的。虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
对象Object类型
对象类型和原始类型的不同之处?
在 JS 中,除了原始类型那么其他的都是对象类型了。
对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)
对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)
函数参数是对象会发生什么问题?
解析
首先,函数传参是传递对象指针的副本
到函数内部修改参数的属性这步,我相信大家都知道,当前 p1 的值也被修改了
但是当我们重新为 person 分配了一个对象时就出现了分歧,请看下图
到函数内部修改参数的属性这步,我相信大家都知道,当前 p1 的值也被修改了
但是当我们重新为 person 分配了一个对象时就出现了分歧,请看下图
所以最后 person 拥有了一个新的地址(指针),也就和 p1 没有任何关系了,导致了最终两个变量的值是不相同的。
typeof vs instanceof
typeof 是否能正确判断类型?
不能
typeof 对于原始类型来说,除了 null 都可以显示正确的类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof 对于对象来说,除了函数都会显示 object,
所以说 typeof 并不能准确判断变量到底是什么类型
所以说 typeof 并不能准确判断变量到底是什么类型
typeof [] // 'object'
typeof {} // 'object'
typeof {} // 'object'
typeof console.log // 'function'
instanceof 能正确判断对象的原理是什么?
内部机制是通过原型链来判断的
对于原始类型来说,你想直接通过 instanceof 来判断类型是不行的
var str = 'hello world'
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true
让 instanceof 判断原始类型
Symbol.hasInstance 其实就是一个能让我们自定义 instanceof 行为的东西,
以上代码等同于 typeof 'hello world' === 'string',所以结果自然是 true 了。
这其实也侧面反映了一个问题, instanceof 也不是百分之百可信的。
以上代码等同于 typeof 'hello world' === 'string',所以结果自然是 true 了。
这其实也侧面反映了一个问题, instanceof 也不是百分之百可信的。
类型转换
在 JS 中类型转换只有三种情况,分别是:
转换为布尔值
转换为数字
转换为字符串
转换为数字
转换为字符串
类型转换表格
注意图中有一个错误,Boolean 转字符串这行结果我指的是 true 转字符串的例子,不是说 Boolean、函数、Symblo 转字符串都是 `true`
转Boolean
在条件判断时,除了 undefined, null, false, NaN, '', 0, -0,
其他所有值都转为 true,包括所有对象。
其他所有值都转为 true,包括所有对象。
对象转原始类型
对象在转换类型的时候,会调用内置的 [[ToPrimitive]] 函数,
对于该函数来说,算法逻辑一般来说如下:
对于该函数来说,算法逻辑一般来说如下:
1、如果已经是原始类型了,那就不需要转换了
2、如果需要转字符串类型就调用 x.toString(),转换为基础类型的话就返回转换的值。
不是字符串类型的话就先调用 valueOf,结果不是基础类型的话再调用 toString
3、调用 x.valueOf(),如果转换为基础类型,就返回转换的值
4、如果都没有返回原始类型,就会报错
2、如果需要转字符串类型就调用 x.toString(),转换为基础类型的话就返回转换的值。
不是字符串类型的话就先调用 valueOf,结果不是基础类型的话再调用 toString
3、调用 x.valueOf(),如果转换为基础类型,就返回转换的值
4、如果都没有返回原始类型,就会报错
当然你也可以重写 Symbol.toPrimitive ,该方法在转原始类型时调用优先级最高。
四则运算符
加法运算符
加法运算符不同于其他几个运算符,
它有以下几个特点:
它有以下几个特点:
1、运算中其中一方为字符串,那么就会把另一方也转换为字符串
2、如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
2、如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
示例1
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
true + true // 2
4 + [1,2,3] // "41,2,3"
解析
1、对于第一行代码来说,触发特点一,所以将数字 1 转换为字符串,得到结果 '11'
2、对于第二行代码来说,触发特点二,所以将 true 转为数字 1
3、对于第三行代码来说,触发特点二,所以将数组通过 toString 转为字符串 1,2,3,得到结果 41,2,3
2、对于第二行代码来说,触发特点二,所以将 true 转为数字 1
3、对于第三行代码来说,触发特点二,所以将数组通过 toString 转为字符串 1,2,3,得到结果 41,2,3
示例2
'a' + + 'b' // -> "aNaN"
解析
因为 + 'b' 等于 NaN,所以结果为 "aNaN",
你可能也会在一些代码中看到过 + '1' 的形式来快速获取 number 类型。
你可能也会在一些代码中看到过 + '1' 的形式来快速获取 number 类型。
除了加法之外的运算符
对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
示例
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
4 * [] // 0
4 * [1, 2] // NaN
比较运算符
1、如果是对象,就通过 toPrimitive 转换对象
2、如果是字符串,就通过 unicode 字符索引来比较
2、如果是字符串,就通过 unicode 字符索引来比较
示例
解析
在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。
this
面试题
如何正确判断 this?
箭头函数的 this 是什么?
几个函数调用的场景中
this的指向
this的指向
1、对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
2、对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
3、对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this
2、对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
3、对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this
箭头函数this
首先箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this
示例
在这个例子中,因为包裹箭头函数的第一个普通函数是 a,所以此时的 this 是 window
对箭头函数使用 bind 这类函数是无效的
bind与this
this 取决于第一个参数,如果第一个参数为空,那么就是 window
一个函数进行多次 bind
可以从上述代码中发现,不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window
this规则同时出现时的优先级
1、new 的方式优先级最高,
2、接下来是 bind 这些函数,
3、然后是 obj.foo() 这种调用方式,
4、最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。
2、接下来是 bind 这些函数,
3、然后是 obj.foo() 这种调用方式,
4、最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。
单个规则时,this指向流程图
图中的流程只针对于单个规则
== vs ===
面试题:== 和 === 有什么区别?
对于 == 来说,如果对比双方的类型不一样的话,就会进行类型转换
=== 来说就简单多了,就是判断两者类型和值是否相同
对比 x 和 y 是否相同,判断流程
1、首先会判断两者类型是否相同。相同的话就是比大小了
2、类型不相同的话,那么就会进行类型转换
3、会先判断是否在对比 null 和 undefined,是的话就会返回 true
2、类型不相同的话,那么就会进行类型转换
3、会先判断是否在对比 null 和 undefined,是的话就会返回 true
4、判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number
5、判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
6、判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断
流程图
这个流程图并没有将所有的情况都列举出来,
我这里只将常用到的情况列举了,
如果你想了解更多的内容可以参考 标准文档
我这里只将常用到的情况列举了,
如果你想了解更多的内容可以参考 标准文档
思考题:看完了上面的步骤,对于 [] == ![] 你是否能正确写出答案呢?
[]==[]
两边都是相同数据类型时,==是直接比较两边数据,结果为false,因为[]属于引用类型,在两个[]分别指向不同的堆内存
[]==![]
过程:[]==![] ----> []==false -----> 0==0 --->//true
闭包
面试题
什么是闭包?
闭包的定义其实很简单:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。
循环中使用闭包解决 `var` 定义函数的问题
题目
首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6
注意输出的时间间隔,和事件循环结合起来理解
解决办法
使用闭包
在上述代码中,我们首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。
使用 setTimeout 的第三个参数
使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。
其中7是定时器的id ,
settimeout有返回值,便于清除的时候找到是哪个
settimeout有返回值,便于清除的时候找到是哪个
使用 let 定义 i
深浅拷贝
面试题:什么是浅拷贝?如何实现浅拷贝?什么是深拷贝?如何实现深拷贝?
浅拷贝
Object.assign
Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝
展开运算符...
深拷贝
JSON.parse(JSON.stringify(object))
该方法也是有局限性的
1、会忽略 undefined
2、会忽略 symbol
3、不能序列化函数
4、不能解决循环引用的对象
2、会忽略 symbol
3、不能序列化函数
4、不能解决循环引用的对象
不能解决循环引用的对象
实现深拷贝
MessageChannel
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel
手写深拷贝
其实实现一个深拷贝是很困难的,需要我们考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等等,所以这里我们实现的深拷贝只是简易版,并且我其实更推荐使用 lodash 的深拷贝函数。
Reflect.ownKeys(newObj)
可以替换成
Object.keys(newObj)
可以替换成
Object.keys(newObj)
原型
__proto__ 属性
每个 JS 对象都有 __proto__ 属性,这个属性指向了原型
原型也是一个对象,并且这个对象中包含了很多函数,所以我们可以得出一个结论:对于 obj 来说,可以通过 __proto__ 找到一个原型对象,在该对象中定义了很多函数让我们来使用。
constructor 属性,也就是构造函数
打开 constructor 属性我们又可以发现其中还有一个 prototype 属性,并且这个属性对应的值和先前我们在 __proto__ 中看到的一模一样。所以我们又可以得出一个结论:原型的 constructor 属性指向构造函数,构造函数又通过 prototype 属性指回原型,但是并不是所有函数都具有这个属性,Function.prototype.bind() 就没有这个属性
原型和原型链图示
原型链
原型链就是多个对象通过 __proto__ 的方式连接了起来。为什么 obj 可以访问到 valueOf 函数,就是因为 obj 通过原型链找到了
总结
1、Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
2、Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
3、函数的 prototype 是一个对象
4、对象的 __proto__ 属性指向原型, __proto__ 将对象和原型连接起来组成了原型链
2、Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
3、函数的 prototype 是一个对象
4、对象的 __proto__ 属性指向原型, __proto__ 将对象和原型连接起来组成了原型链
ES6 知识点及常考面试题
var、let 及 const 区别
面试题
什么是提升?
什么是暂时性死区?
var、let 及 const 区别?
什么是暂时性死区?
var、let 及 const 区别?
提升
var声明的变量提升
例子1
console.log(a) // undefined
var a = 1
var a = 1
虽然变量还没有被声明,但是我们却可以使用这个未被声明的变量,这种情况就叫做提升,并且提升的是声明。
对于这种情况,我们可以把代码这样来看:
var a
console.log(a) // undefined
a = 1
var a
console.log(a) // undefined
a = 1
例子2
var a = 10
var a
console.log(a) //10
var a
console.log(a) //10
上段代码可以这样来看:
var a
var a
a = 10
console.log(a)
var a
var a
a = 10
console.log(a)
子主题
子主题
使用 var 声明的变量会被提升到作用域的顶部
函数提升
console.log(a) // ƒ a() {}
function a() {}
var a = 1
function a() {}
var a = 1
打印结果会是 ƒ a() {},即使变量声明在函数之后,
这也说明了函数会被提升,并且优先于变量提升
这也说明了函数会被提升,并且优先于变量提升
提升存在的根本原因
为了解决函数间互相调用的情况
假如不存在提升这个情况,那么就实现不了上述的代码,因为不可能存在 test1 在 test2 前面然后 test2 又在 test1 前面。
let const
与var的区别
与var的区别
let b = 1
const c = 1
console.log(window.b) // undefined
console.log(window. c) // undefined
const c = 1
console.log(window.b) // undefined
console.log(window. c) // undefined
首先在全局作用域下使用 let 和 const 声明变量,
变量并不会被挂载到 window 上,这一点就和 var 声明有了区别
变量并不会被挂载到 window 上,这一点就和 var 声明有了区别
暂时性死区
在声明 a 之前如果使用了 a,就会出现报错的情况
首先报错的原因是因为存在暂时性死区,我们不能在声明前就使用变量,这也是 let 和 const 优于 var 的一点。
然后这里你认为的提升和 var 的提升是有区别的,虽然变量在编译的环节中被告知在这块作用域中可以访问,但是访问是受限制的。
然后这里你认为的提升和 var 的提升是有区别的,虽然变量在编译的环节中被告知在这块作用域中可以访问,但是访问是受限制的。
总结
1、函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
2、var 存在提升,我们能在声明之前使用。let、const 因为暂时性死区的原因,不能在声明前使用
3、var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会
4、let 和 const 作用基本一致,但是后者声明的变量不能再次赋值
2、var 存在提升,我们能在声明之前使用。let、const 因为暂时性死区的原因,不能在声明前使用
3、var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会
4、let 和 const 作用基本一致,但是后者声明的变量不能再次赋值
原型继承和 Class 继承
面试题
原型如何实现继承?
Class 如何实现继承?
Class 本质是什么?
Class 如何实现继承?
Class 本质是什么?
class
其实在 JS 中并不存在类,class 只是语法糖,本质还是函数。
class Person {}
Person instanceof Function // true
Person instanceof Function // true
组合继承
组合继承是最常用的继承方式
示例
以上继承的方式核心
1、在子类的构造函数中通过 Parent.call(this) 继承父类的属性
2、然后改变子类的原型为 new Parent() 来继承父类的函数
组合继承优缺点
优点
构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,
普通函数使用了new,他本体也算构造函数
构造函数指的是Child
构造函数指的是Child
缺点
但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,
导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
构造函数指的是new的时候调用的constructor
寄生组合继承
这种继承方式对组合继承进行了优化,
组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。
组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。
示例
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
组合寄生继承实现的核心
将父类的原型赋值给了子类,并且将构造函数设置为子类,
这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数
这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数
Class 继承
class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)
模块化
面试题
为什么要使用模块化?
都有哪几种方式可以实现模块化,各有什么特点?
都有哪几种方式可以实现模块化,各有什么特点?
模块化带来的好处
1、解决命名冲突
2、提供复用性
3、提高代码可维护性
2、提供复用性
3、提高代码可维护性
立即执行函数
在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题
AMD 和 CMD
鉴于目前这两种实现方式已经很少见到,所以不再对具体特性细聊,只需要了解这两者是如何使用的。
CommonJS
CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,当然目前在 Node 中的模块管理已经和 CommonJS 有一些区别了
ES Module
ES Module 是原生实现的模块化方案
ES Module与 CommonJS 有以下几个区别
1、CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
2、CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
3、CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
4、ES Module 会编译成 require/exports 来执行的
2、CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
3、CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
4、ES Module 会编译成 require/exports 来执行的
// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}
Proxy
面试题
Proxy 可以实现什么功能?
Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
let p = new Proxy(target, handler)
target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。
let p = new Proxy(target, handler)
target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。
Proxy 实现一个简易版数据响应式
我们通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
vue 3.0中的Proxy
Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式
果需要实现一个 Vue 中的响应式,需要我们在 get 中收集依赖,在 set 派发更新,
之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,
并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,
唯一缺陷可能就是浏览器的兼容性不好了。
之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,
并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,
唯一缺陷可能就是浏览器的兼容性不好了。
Proxy 无需一层层递归为每个属性添加代理
map, filter, reduce
面试题:
map, filter, reduce 各自有什么作用?
map
map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中。
[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]
[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]
另外 map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组
['1','2','3'].map(parseInt)
['1','2','3'].map(parseInt)
第一轮遍历 parseInt('1', 0) -> 1
第二轮遍历 parseInt('2', 1) -> NaN
第三轮遍历 parseInt('3',
第二轮遍历 parseInt('2', 1) -> NaN
第三轮遍历 parseInt('3',
filter
filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,
我们可以利用这个函数删除一些不需要的元素
我们可以利用这个函数删除一些不需要的元素
let array = [1, 2, 4, 6]
let newArray = array.filter(item => item !== 6)
console.log(newArray) // [1, 2, 4]
let newArray = array.filter(item => item !== 6)
console.log(newArray) // [1, 2, 4]
和 map 一样,filter 的回调函数也接受三个参数,用处也相同。
reduce
reduce 可以将数组中的元素通过回调函数最终转换为一个值。
将函数里的元素全部相加得到一个值
const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)
参数和累加过程
对于 reduce 来说,它接受两个参数,分别是回调函数和初始值,接下来我们来分解上述代码中 reduce 的过程
首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入
回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数
在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入
所以在第二次执行回调函数时,相加的值就分别是 1 和 2,以此类推,循环结束后得到结果 6
首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入
回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数
在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入
所以在第二次执行回调函数时,相加的值就分别是 1 和 2,以此类推,循环结束后得到结果 6
reduce 来实现 map 函数
JS 异步编程及常考面试题
并发(concurrency)和并行(parallelism)区别
面试题:
并发与并行的区别?
区别
并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。
并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。
回调函数(Callback)
面试题:
什么是回调函数?
回调函数有什么缺点?
如何解决回调地狱问题?
回调函数有什么缺点?
如何解决回调地狱问题?
一个回调函数的例子
ajax(url, () => {
// 处理逻辑
})
// 处理逻辑
})
回调地狱
回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)
请求存在依赖性,可能就会写出如下代码:
回调地狱的根本问题就是:
1、嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
2、嵌套函数一多,就很难处理错误
2、嵌套函数一多,就很难处理错误
回调函数中存在的其他问题
比如不能使用 try catch 捕获错误,
不能直接 return。
不能直接 return。
Generator
面试题:
你理解的 Generator 是什么?
1、首先 Generator 函数调用和普通函数不同,它会返回一个迭代器
2、当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
3、当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
4、当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42
2、当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
3、当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
4、当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42
通过 Generator 函数解决回调地狱的问题
Promise
面试题
Promise 的特点是什么,分别有什么优缺点?
什么是 Promise 链?
Promise 构造函数执行和 then 函数执行有什么区别?
什么是 Promise 链?
Promise 构造函数执行和 then 函数执行有什么区别?
是什么
Promise 翻译过来就是承诺的意思,这个承诺会在未来有一个确切的答复
三种状态
1、等待中(pending)
2、完成了 (resolved)
3、拒绝了(rejected)
2、完成了 (resolved)
3、拒绝了(rejected)
状态不可逆
这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 resolved 后,就不能再次改变
当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的
链式调用
Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。
如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装
如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装
解决回调地狱
缺点
如无法取消 Promise,
错误需要通过回调函数捕获
错误需要通过回调函数捕获
async 及 await
面试题
async 及 await 的特点,
它们的优点和缺点分别是什么?
await 原理是什么?
它们的优点和缺点分别是什么?
await 原理是什么?
一个函数如果加上 async ,那么该函数就会返回一个 Promise
async 就是将函数返回值使用 Promise.resolve() 包裹了下,和 then 中处理返回值一样,
并且 await 只能配套 async 使用
并且 await 只能配套 async 使用
async function test() {
let value = await sleep()
}
let value = await sleep()
}
async 和 await 是异步终极解决方案
async 和 await 可以说是异步终极解决方案了,
相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码,毕竟写一大堆 then 也很恶心,并且也能优雅地解决回调地狱问题。
相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码,毕竟写一大堆 then 也很恶心,并且也能优雅地解决回调地狱问题。
缺点
因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。
await例子
1、首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generator ,generator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
2、因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
3、同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10
2、因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
3、同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10
await本质
await 内部实现了 generator,其实 await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator
常用定时器函数
面试题:
setTimeout、setInterval、requestAnimationFrame 各有什么特点?
常见定时器函数
setTimeout
setInterval
requestAnimationFrame
setInterval
requestAnimationFrame
setTimeout
很多人认为 setTimeout 是延时多久,那就应该是多久后执行。
其实这个观点是错误的,因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。
其实这个观点是错误的,因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。
setInterval
通常来说不建议使用 setInterval
第一,它和 setTimeout 一样,不能保证在预期的时间执行任务
第二,它存在执行累积的问题,请看以下伪代码
以上代码在浏览器环境中,如果定时器执行过程中出现了耗时操作,多个回调函数会在耗时操作结束以后同时执行,这样可能就会带来性能上的问题。
requestAnimationFrame
首先 requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),
并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout。
并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout。
手写 Promise
实现一个简易版 Promise
在面试中,如果你能实现出一个简易版的 Promise 基本可以过关了
搭建构建函数的大体框架
关键点
Promise是一个构造函数
具有三种状态
参数是一个函数
解析
1、首先我们创建了三个常量用于表示状态,对于经常使用的一些值都应该通过常量来管理,便于开发及后期维护
2、在函数体内部首先创建了常量 that,因为代码可能会异步执行,用于获取正确的 this 对象
3、一开始 Promise 的状态应该是 pending
4、value 变量用于保存 resolve 或者 reject 中传入的值
5、resolvedCallbacks 和 rejectedCallbacks 用于保存 then 中的回调,因为当执行完 Promise 时状态可能还是等待中,这时候应该把 then 中的回调保存起来用于状态改变时使用
2、在函数体内部首先创建了常量 that,因为代码可能会异步执行,用于获取正确的 this 对象
3、一开始 Promise 的状态应该是 pending
4、value 变量用于保存 resolve 或者 reject 中传入的值
5、resolvedCallbacks 和 rejectedCallbacks 用于保存 then 中的回调,因为当执行完 Promise 时状态可能还是等待中,这时候应该把 then 中的回调保存起来用于状态改变时使用
完善 resolve 和 reject 函数,
添加在 MyPromise 函数体内部
添加在 MyPromise 函数体内部
解析
1、首先两个函数都得判断当前状态是否为等待中,因为规范规定只有等待态才可以改变状态
2、将当前状态更改为对应状态,并且将传入的值赋值给 value
3、遍历回调数组并执行
2、将当前状态更改为对应状态,并且将传入的值赋值给 value
3、遍历回调数组并执行
完成以上两个函数以后,
我们就该实现如何执行 Promise 中传入的函数了
我们就该实现如何执行 Promise 中传入的函数了
解析
1、实现很简单,执行传入的参数并且将之前两个函数当做参数传进去
2、要注意的是,可能执行函数过程中会遇到错误,需要捕获错误并且执行 reject 函数
2、要注意的是,可能执行函数过程中会遇到错误,需要捕获错误并且执行 reject 函数
最后我们来实现较为复杂的 then 函数
解析
1、首先判断两个参数是否为函数类型,因为这两个参数是可选参数
2、当参数不是函数类型时,需要创建一个函数赋值给对应的参数,同时也实现了透传,比如如下代码
// 该代码目前在简单版中会报错
// 只是作为一个透传的例子
Promise.resolve(4).then().then((value) => console.log(value))
// 该代码目前在简单版中会报错
// 只是作为一个透传的例子
Promise.resolve(4).then().then((value) => console.log(value))
3、接下来就是一系列判断状态的逻辑,当状态不是等待态时,就去执行相对应的函数。
如果状态是等待态的话,就往回调函数中 push 函数,比如如下代码就会进入等待态的逻辑
如果状态是等待态的话,就往回调函数中 push 函数,比如如下代码就会进入等待态的逻辑
一个Promise例子
实现一个符合 Promise/A+ 规范的 Promise
略过没看
EventLoop
进程与线程
面试题:
进程与线程区别?JS 单线程带来的好处?
线程和进程
两个名词都是 CPU 工作时间片的一个描述
进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。
线程是进程中的更小单位,描述了执行一段指令所需的时间。
线程是进程中的更小单位,描述了执行一段指令所需的时间。
把这些概念拿到浏览器中来说,
当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。
当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。
当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。
当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。
JS 是单线程
JS 引擎线程和渲染线程,大家应该都知道,在 JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。
这其中的原因是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。
这其中的原因是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。
执行栈
面试题
什么是执行栈?
执行栈
执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则
当开始执行 JS 代码时,首先会执行一个 main 函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈。
平时在开发中,大家也可以在报错中找到执行栈的痕迹
在上图清晰的看到报错在 foo 函数,
foo 函数又是在 bar 函数中调用的
foo 函数又是在 bar 函数中调用的
爆栈
当我们使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题
浏览器中的 Event Loop
面试题
异步代码执行顺序?
解释一下什么是 Event Loop ?
解释一下什么是 Event Loop ?
异步
当我们执行 JS 代码的时候其实就是往执行栈中放入函数,那么遇到异步代码的时候该怎么办?
其实当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。
一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。
其实当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。
一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。
任务源
不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。
在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。
下面来看以下代码的执行顺序:
在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。
下面来看以下代码的执行顺序:
例子
解释
首先先来解释下上述代码的 async 和 await 的执行顺序。当我们调用 async1 函数时,会马上输出 async2 end,并且函数返回一个 Promise,接下来在遇到 await的时候会就让出线程开始执行 async1 外的代码,所以我们完全可以把 await 看成是让出线程的标志。
然后当同步代码全部执行完毕以后,就会去执行所有的异步代码,那么又会回到 await 的位置执行返回的 Promise 的 resolve 函数,这又会把 resolve 丢到微任务队列中,接下来去执行 then 中的回调,当两个 then 中的回调全部执行完毕以后,又会回到 await 的位置处理返回值,这时候你可以看成是 Promise.resolve(返回值).then(),然后 await 后的代码全部被包裹进了 then 的回调中,所以 console.log('async1 end') 会优先执行于 setTimeout。
然后当同步代码全部执行完毕以后,就会去执行所有的异步代码,那么又会回到 await 的位置执行返回的 Promise 的 resolve 函数,这又会把 resolve 丢到微任务队列中,接下来去执行 then 中的回调,当两个 then 中的回调全部执行完毕以后,又会回到 await 的位置处理返回值,这时候你可以看成是 Promise.resolve(返回值).then(),然后 await 后的代码全部被包裹进了 then 的回调中,所以 console.log('async1 end') 会优先执行于 setTimeout。
把 async 的这两个函数改造成你一定能理解的代码
Event Loop 执行顺序如下所示:
1、首先执行同步代码,这属于宏任务
2、当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
3、执行所有微任务
4、当执行完所有微任务后,如有必要会渲染页面
5、然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数
2、当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
3、执行所有微任务
4、当执行完所有微任务后,如有必要会渲染页面
5、然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数
宏任务和微任务有哪些
微任务包括 process.nextTick ,promise ,MutationObserver,其中 process.nextTick 为 Node 独有。
宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering
宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering
一个错误认识
这里很多人会有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。
Node 中的 Event Loop
略过没看
面试题
Node 中的 Event Loop 和浏览器中的有什么区别?
process.nexttick 执行顺序?
process.nexttick 执行顺序?
Node 中的 Event Loop 和浏览器中的是完全不相同的东西。
Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
JS 进阶知识点及常考面试题
手写 call、apply 及 bind 函数
面试题
call apply 及 bind 函数内部实现是怎么样的?
apply 参数是一个数组,call bind参数是一个序列
call
分析
1、首先 context 为可选参数,如果不传的话默认上下文为 window
2、接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
3、因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
4、然后调用函数并将对象上的函数删除
2、接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
3、因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
4、然后调用函数并将对象上的函数删除
apply
bind
bind 的实现对比其他两个函数略微地复杂了一点,
因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现
因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现
分析
1、前几步和之前的实现差不多,就不赘述了
2、bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,我们先来说直接调用的方式
3、对于直接调用来说,这里选择了 apply 的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(...arguments)
4、最后来说通过 new 的方式,在之前的章节中我们学习过如何判断 this,对于 new 的情况来说,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this
2、bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,我们先来说直接调用的方式
3、对于直接调用来说,这里选择了 apply 的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(...arguments)
4、最后来说通过 new 的方式,在之前的章节中我们学习过如何判断 this,对于 new 的情况来说,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this
new
面试题
new 的原理是什么?
通过 new 的方式创建对象和通过字面量创建有什么区别?
通过 new 的方式创建对象和通过字面量创建有什么区别?
在调用 new 的过程中会发生以下四件事情:
1、新生成了一个对象
2、链接到原型
3、绑定 this
4、返回新对象
2、链接到原型
3、绑定 this
4、返回新对象
自己实现一个 new
手写new
分析
1、创建一个空对象
2、获取构造函数
3、设置空对象的原型
4、绑定 this 并执行构造函数
5、确保返回值为对象
2、获取构造函数
3、设置空对象的原型
4、绑定 this 并执行构造函数
5、确保返回值为对象
举个🌰
补充一个关于new的文章
JS 之父的关怀
JS 之父创建了 new 关键字,可以让我们少写几行代码:
JS 之父创建了 new 关键字,可以让我们少写几行代码:
只要你在士兵前面使用 new 关键字,那么可以少做四件事情:
不用创建临时对象,因为 new 会帮你做(你使用「this」就可以访问到临时对象);
不用绑定原型,因为 new 会帮你做(new 为了知道原型在哪,所以指定原型的名字为 prototype);
不用 return 临时对象,因为 new 会帮你做;
不要给原型想名字了,因为 new 指定名字为 prototype。
不用创建临时对象,因为 new 会帮你做(你使用「this」就可以访问到临时对象);
不用绑定原型,因为 new 会帮你做(new 为了知道原型在哪,所以指定原型的名字为 prototype);
不用 return 临时对象,因为 new 会帮你做;
不要给原型想名字了,因为 new 指定名字为 prototype。
这一次我们用 new 来写
new和字面量
对于对象来说,其实都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }
对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。
function Foo() {}
// function 就是个语法糖
// 内部等同于 new Function()
let a = { b: 1 }
// 这个字面量内部也是使用了 new Object()
// function 就是个语法糖
// 内部等同于 new Function()
let a = { b: 1 }
// 这个字面量内部也是使用了 new Object()
instanceof 的原理
面试题
instanceof 的原理是什么?
原理
instanceof 可以正确的判断对象的类型,
因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype
实现instanceof
分析
1、首先获取类型的原型
2、然后获得对象的原型
3、然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null
2、然后获得对象的原型
3、然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null
为什么 0.1 + 0.2 != 0.3
为什么 0.1 + 0.2 != 0.3?
如何解决这个问题?
如何解决这个问题?
0.1 在二进制中是无限循环的一些数字,其实不只是 0.1,其实很多十进制小数用二进制表示都是无限循环的。
这样其实没什么问题,但是 JS 采用的浮点数标准却会裁剪掉我们的数字。
这样其实没什么问题,但是 JS 采用的浮点数标准却会裁剪掉我们的数字。
怎么解决
用原生提供的方式来最简单的解决问题
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true
垃圾回收机制
面试题
V8 下的垃圾回收机制是怎么样的?
V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。
新生代算法
略
老生代算法
略
JS 思考题
要能够把JS的多个碎片知识点串联起来
一旦你拥有将多个碎片知识点串联起来的能力,在面试中就不会经常出现一问一答的情况。
如果面试官的每个问题你都能引申出一些相关联的知识点,那么面试官一定会提高对你的评价。
如果面试官的每个问题你都能引申出一些相关联的知识点,那么面试官一定会提高对你的评价。
思考题一:
JS 分为哪两大类型?
都有什么各自的特点?
你该如何判断正确的类型?
都有什么各自的特点?
你该如何判断正确的类型?
思路引导
1、对于原始类型来说,你可以指出 null 和 number 存在的一些问题。对于对象类型来说,你可以从垃圾回收的角度去切入,也可以说一下对象类型存在深浅拷贝的问题。
2、对于判断类型来说,你可以去对比一下 typeof 和 instanceof 之间的区别,也可以指出 instanceof 判断类型也不是完全准确的。
2、对于判断类型来说,你可以去对比一下 typeof 和 instanceof 之间的区别,也可以指出 instanceof 判断类型也不是完全准确的。
以上就是这道题目的回答思路,当然不是说让大家完全按照这个思路去答题,而是存在一个意识,当回答面试题的时候,尽量去引申出这个知识点的某些坑或者与这个知识点相关联的东西。
思考题二:
你理解的原型是什么?
思路引导
起码说出原型小节中的总结内容,
然后还可以指出一些小点,比如并不是所有函数都有 prototype 属性,
然后引申出原型链的概念,
提出如何使用原型实现继承,继而可以引申出 ES6 中的 class 实现继承。
然后还可以指出一些小点,比如并不是所有函数都有 prototype 属性,
然后引申出原型链的概念,
提出如何使用原型实现继承,继而可以引申出 ES6 中的 class 实现继承。
思考题三:
bind、call 和 apply 各自有什么区别?
思路引导
首先肯定是说出三者的不同,如果自己实现过其中的函数,可以尝试说出自己的思路。
然后可以聊一聊 this 的内容,有几种规则判断 this 到底是什么,this 规则会涉及到 new,那么最后可以说下自己对于 new 的理解。
然后可以聊一聊 this 的内容,有几种规则判断 this 到底是什么,this 规则会涉及到 new,那么最后可以说下自己对于 new 的理解。
思考题四:
ES6 中有使用过什么?
思路引导
这边可说的实在太多,你可以列举 1 - 2 个点。
比如说说 class,那么 class 又可以拉回到原型的问题;
可以说说 promise,那么线就被拉到了异步的内容;
可以说说 proxy,那么如果你使用过 Vue 这个框架,就可以谈谈响应式原理的内容;
同样也可以说说 let 这些声明变量的语法,那么就可以谈及与 var 的不同,说到提升这块的内容。
比如说说 class,那么 class 又可以拉回到原型的问题;
可以说说 promise,那么线就被拉到了异步的内容;
可以说说 proxy,那么如果你使用过 Vue 这个框架,就可以谈谈响应式原理的内容;
同样也可以说说 let 这些声明变量的语法,那么就可以谈及与 var 的不同,说到提升这块的内容。
思考题五:
JS 是如何运行的?
思路引导
这其实是很大的一块内容。
你可以先说 JS 是单线程运行的,这里就可以说说你理解的线程和进程的区别。
然后讲到执行栈,
接下来的内容就是涉及 Eventloop 了,微任务和宏任务的区别,哪些是微任务,哪些又是宏任务,还可以谈及浏览器和 Node 中的 Eventloop 的不同,
最后还可以聊一聊 JS 中的垃圾回收。
你可以先说 JS 是单线程运行的,这里就可以说说你理解的线程和进程的区别。
然后讲到执行栈,
接下来的内容就是涉及 Eventloop 了,微任务和宏任务的区别,哪些是微任务,哪些又是宏任务,还可以谈及浏览器和 Node 中的 Eventloop 的不同,
最后还可以聊一聊 JS 中的垃圾回收。
小结
虽然思考题不多,但是其实每一道思考题背后都可以引申出很多内容,
大家接下去在学习的过程中也应该始终有一个意识,你学习的这块内容到底和你现在脑海里的哪一个知识点有关联。
大家接下去在学习的过程中也应该始终有一个意识,你学习的这块内容到底和你现在脑海里的哪一个知识点有关联。
DevTools Tips
一些开发时可以提高效率的小技巧,很实用
用到时再查阅小册吧
用到时再查阅小册吧
和面试无关,笔记省略
浏览器基础知识点及常考面试题
浏览器的一些基础知识点,包括:事件机制、跨域、存储相关,这几个知识点也是面试经常会考到的内容
事件机制
事件触发三阶段
1、window 往事件触发处传播,遇到注册的捕获事件会触发
2、传播到事件触发处时触发注册的事件
3、从事件触发处往 window 传播,遇到注册的冒泡事件会触发
2、传播到事件触发处时触发注册的事件
3、从事件触发处往 window 传播,遇到注册的冒泡事件会触发
这话说的,(⊙o⊙)…迷迷糊糊
三个阶段
三个阶段:捕获,目标,冒泡;
(1)捕获阶段:当我们在 DOM 树的某个节点发生了一些操作(例如单击、鼠标移动上去),就会有一个事件发射过去。这个事件从 Window 发出,不断经过下级节点直到触发的目标节点。在到达目标节点之前的过程,就是捕获阶段(Capture Phase)。(所有经过的节点,都会触发这个事件。捕获阶段的任务就是建立这个事件传递路线,以便后面冒泡阶段顺着这条路线返回 Window。)在目标元素对象本身上注册的捕获事件处理程序不会被调用。
(2)目标阶段:当事件不断的传递直到目标节点的时候,最终在目标节点上触发这个事件,就是目标阶段。
(3)冒泡阶段:事件冒泡即事件开始时,由最具体的元素接收(也就是事件发生所在的节点),然后逐级传播到较为不具体的节点(我们平时用的事件绑定就是利用的事件冒泡的原理)
事件冒泡即事件开始时,由最具体的元素接收(也就是事件发生所在的节点),然后逐级传播到较为不具体的节点。
(1)捕获阶段:当我们在 DOM 树的某个节点发生了一些操作(例如单击、鼠标移动上去),就会有一个事件发射过去。这个事件从 Window 发出,不断经过下级节点直到触发的目标节点。在到达目标节点之前的过程,就是捕获阶段(Capture Phase)。(所有经过的节点,都会触发这个事件。捕获阶段的任务就是建立这个事件传递路线,以便后面冒泡阶段顺着这条路线返回 Window。)在目标元素对象本身上注册的捕获事件处理程序不会被调用。
(2)目标阶段:当事件不断的传递直到目标节点的时候,最终在目标节点上触发这个事件,就是目标阶段。
(3)冒泡阶段:事件冒泡即事件开始时,由最具体的元素接收(也就是事件发生所在的节点),然后逐级传播到较为不具体的节点(我们平时用的事件绑定就是利用的事件冒泡的原理)
事件冒泡即事件开始时,由最具体的元素接收(也就是事件发生所在的节点),然后逐级传播到较为不具体的节点。
注册事件
addEventListener
参数,省略。。
如果我们只希望事件只触发在目标上,这时候可以使用 stopPropagation 来阻止事件的进一步传播。
通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。
stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。
通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。
stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。
事件代理
如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上
事件代理的方式相较于直接给目标注册事件来说,有以下优点:
1、节省内存
2、不需要给子节点注销事件
2、不需要给子节点注销事件
跨域
面试题:
什么是跨域?
为什么浏览器要使用同源策略?
你有几种方式可以解决跨域问题?
了解预检请求嘛?
为什么浏览器要使用同源策略?
你有几种方式可以解决跨域问题?
了解预检请求嘛?
因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。
那么是出于什么安全考虑才会引入这种机制呢?
其实主要是用来防止 CSRF 攻击的。简单点说,CSRF 攻击是利用用户的登录态发起恶意请求。
也就是说,没有同源策略的情况下,A 网站可以被任意其他来源的 Ajax 访问到内容。
如果你当前 A 网站还存在登录态,那么对方就可以通过 Ajax 获得你的任何信息。当然跨域并不能完全阻止 CSRF。
如果你当前 A 网站还存在登录态,那么对方就可以通过 Ajax 获得你的任何信息。当然跨域并不能完全阻止 CSRF。
请求跨域了,那么请求到底发出去没有?
请求必然是发出去了,但是浏览器拦截了响应。
你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会。因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。
但是表单并不会获取新的内容,所以可以发起跨域请求。
同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。
你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会。因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。
但是表单并不会获取新的内容,所以可以发起跨域请求。
同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。
几种常见的方式来解决跨域的问题
JSONP
原理
JSONP 的原理很简单,就是利用 <script> 标签没有跨域限制的漏洞。
通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时。
通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时。
局限性
JSONP 使用简单且兼容性不错,但是只限于 get 请求
jsonp的简单实现
CORS
CORS 需要浏览器和后端同时支持
浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。
只要后端实现了 CORS,就实现了跨域。
只要后端实现了 CORS,就实现了跨域。
服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。
该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,
会在发送请求时出现两种情况,分别为简单请求和复杂请求。
会在发送请求时出现两种情况,分别为简单请求和复杂请求。
简单请求
以 Ajax 为例,当满足以下条件时,会触发简单请求
1、使用下列方法之一:
GET
HEAD
POST
HEAD
POST
2、Content-Type 的值仅限于下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
multipart/form-data
application/x-www-form-urlencoded
复杂请求
不符合简单请求条件的请求就肯定是复杂请求了
预检请求
对于复杂请求来说,首先会发起一个预检请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。
document.domain
该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域
只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域
postMessage
这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息
存储
面试题
有几种方式可以实现存储功能,分别有什么优缺点?
什么是 Service Worker?
什么是 Service Worker?
cookie,localStorage,sessionStorage,indexDB
从上表可以看到,cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStorage 和 sessionStorage 。
对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储
对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储
对于 cookie 来说,我们还需要注意安全性
Service Worker
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。
使用 Service Worker的话,传输协议必须为 HTTPS。
因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全
使用 Service Worker的话,传输协议必须为 HTTPS。
因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全
Service Worker 实现缓存功能一般分为三个步骤:
首先需要先注册 Service Worker,
然后监听到 install 事件以后就可以缓存需要的文件,
那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。
以下是这个步骤的实现:
然后监听到 install 事件以后就可以缓存需要的文件,
那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。
以下是这个步骤的实现:
浏览器缓存机制
注意:该知识点属于性能优化领域,并且整一章节都是一个面试题。
缓存用于性能优化
缓存可以说是性能优化中简单高效的一种优化方式了,它可以显著减少网络传输所带来的损耗。
对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。
对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。
缓存位置
Service Worker
Memory Cache
Disk Cache
Push Cache
网络请求
Memory Cache
Disk Cache
Push Cache
网络请求
从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络
Service Worker
Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。
当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。
Memory Cache
Memory Cache 也就是内存中的缓存,读取内存中的数据肯定比磁盘快。
但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。
一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。
一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?
先说结论,这是不可能的。
首先计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。
首先计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。
Disk Cache
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。
在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。
Push Cache
Push Cache 是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。并且缓存时间也很短暂,只在会话(Session)中存在,一旦会话结束就被释放。
Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及,但是 HTTP/2 将会是日后的一个趋势。
Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及,但是 HTTP/2 将会是日后的一个趋势。
网络请求
如果所有缓存都没有命中的话,那么只能发起请求来获取资源了。
缓存策略
通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。
强缓存
强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control 。
强缓存表示在缓存期间不需要请求,state code 为 200。
强缓存表示在缓存期间不需要请求,state code 为 200。
Expires
Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效
Cache-control
Cache-control: max-age=30
Cache-Control 出现于 HTTP/1.1,优先级高于 Expires 。该属性值表示资源会在 30 秒后过期,需要再次请求。
一些常见指令的作用
协商缓存
如果缓存过期了,就需要发起请求验证资源是否有更新。协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。
当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态码,并且更新浏览器缓存有效期。
当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态码,并且更新浏览器缓存有效期。
当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态码,并且更新浏览器缓存有效期。
Last-Modified 和 If-Modified-Since
Last-Modified 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回 304 状态码。
Last-Modified 存在一些弊端
1、如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
2、因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源
2、因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源
因为以上这些弊端,所以在 HTTP / 1.1 出现了 ETag
ETag 和 If-None-Match
ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。
并且 ETag 优先级比 Last-Modified 高。
并且 ETag 优先级比 Last-Modified 高。
如果什么缓存策略都没设置,那么浏览器会怎么处理?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。
实际场景应用缓存策略
频繁变动的资源
对于频繁变动的资源,
首先需要使用 Cache-Control: no-cache 使浏览器每次都请求服务器,
然后配合 ETag 或者 Last-Modified 来验证资源是否有效。
这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小
首先需要使用 Cache-Control: no-cache 使浏览器每次都请求服务器,
然后配合 ETag 或者 Last-Modified 来验证资源是否有效。
这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小
代码文件
这里特指除了 HTML 外的代码文件,因为 HTML 文件一般不缓存或者缓存时间很短。
一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。
基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存。
基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存。
浏览器渲染原理
为什么要来学习这个?
其实我们学习浏览器渲染原理更多的是为了解决性能的问题,如果你不了解这部分的知识,你就不知道什么情况下会对性能造成损伤。
并且渲染原理在面试中答得好,也是一个能与其他候选人拉开差距的一点。
浏览器接收到 HTML 文件并转换为 DOM 树
将 CSS 文件转换为 CSSOM 树
在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。
因为样式你可以自行设置给某个节点,也可以通过继承获得。
在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。
因为样式你可以自行设置给某个节点,也可以通过继承获得。
在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。
举例
对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的 span 标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的 span 标签,然后找到 span 标签上的 a 标签,最后再去找到 div 标签,然后给符合这种条件的 span 标签设置颜色,这样的递归过程就很复杂。
所以我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平。
所以我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平。
生成渲染树
当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树。
在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。
当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流),然后调用 GPU 绘制,合成图层,显示在屏幕上。对于这一部分的内容因为过于底层,还涉及到了硬件相关的知识,这里就不再继续展开内容了。
当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流),然后调用 GPU 绘制,合成图层,显示在屏幕上。对于这一部分的内容因为过于底层,还涉及到了硬件相关的知识,这里就不再继续展开内容了。
为什么操作 DOM 慢
操作DOM性能差的原因
因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。
面试题
插入几万个 DOM,如何实现页面不卡顿?
对于这道题目来说,首先我们肯定不能一次性把几万个 DOM 全部插入,这样肯定会造成卡顿,所以解决问题的重点应该是如何分批次部分渲染 DOM。
大部分人应该可以想到通过 requestAnimationFrame 的方式去循环的插入 DOM,
其实还有种方式去解决这个问题:虚拟滚动(virtualized scroller)
这种技术的原理就是只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容。
大部分人应该可以想到通过 requestAnimationFrame 的方式去循环的插入 DOM,
其实还有种方式去解决这个问题:虚拟滚动(virtualized scroller)
这种技术的原理就是只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容。
从上图中我们可以发现,即使列表很长,但是渲染的 DOM 元素永远只有那么几个,当我们滚动页面的时候就会实时去更新 DOM,这个技术就能顺利解决这道经典面试题。
什么情况阻塞渲染
首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。
然后当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。
当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。
当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。
对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染。
然后当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。
当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。
当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。
对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染。
重绘(Repaint)和回流(Reflow)
重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。
1、重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘
2、回流是布局或者几何属性需要改变就称为回流。
回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
1、重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘
2、回流是布局或者几何属性需要改变就称为回流。
回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
以下几个动作可能会导致性能问题:
改变 window 大小
改变字体
添加或删除样式
文字改变
定位或者浮动
盒模型
改变字体
添加或删除样式
文字改变
定位或者浮动
盒模型
重绘和回流其实也和 Eventloop 有关
1、当 Eventloop 执行完 Microtasks 后,会判断 document 是否需要更新,因为浏览器是 60Hz 的刷新率,每 16.6ms 才会更新一次。
2、然后判断是否有 resize 或者 scroll 事件,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。
3、判断是否触发了 media query
4、更新动画并且发送事件
5、判断是否有全屏操作事件
6、执行 requestAnimationFrame 回调
7、执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
8、更新界面
9、以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。
2、然后判断是否有 resize 或者 scroll 事件,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。
3、判断是否触发了 media query
4、更新动画并且发送事件
5、判断是否有全屏操作事件
6、执行 requestAnimationFrame 回调
7、执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
8、更新界面
9、以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。
减少重绘和回流
使用 transform 替代 top
使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
不要把节点的属性值放在一个循环里当成循环里的变量
不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
CSS 选择符从右往左匹配查找,避免节点层级过多
将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。
设置节点为图层的方式有很多,
我们可以通过以下几个常用属性可以生成新图层
我们可以通过以下几个常用属性可以生成新图层
will-change
video、
iframe 标签
video、
iframe 标签
思考题
在不考虑缓存和优化网络协议的前提下,考虑可以通过哪些方式来最快的渲染页面,也就是常说的关键渲染路径,这部分也是性能优化中的一块内容。
提示如何加速:
1、从文件大小考虑
2、从 script 标签使用上来考虑
3、从 CSS、HTML 的代码书写上来考虑
4、从需要下载的内容是否需要在首屏使用上来考虑
2、从 script 标签使用上来考虑
3、从 CSS、HTML 的代码书写上来考虑
4、从需要下载的内容是否需要在首屏使用上来考虑
安全防范知识点
XSS
面试题
什么是 XSS 攻击?
如何防范 XSS 攻击?
什么是 CSP?
如何防范 XSS 攻击?
什么是 CSP?
什么是 XSS 攻击?
XSS 简单点来说,就是攻击者想尽一切办法将可以执行的代码注入到网页中。
XSS 类型
总体上我认为分为两类:持久型和非持久型
持久型
持久型也就是攻击的代码被服务端写入进数据库中,这种攻击危害性很大,因为如果网站访问量很大的话,就会导致大量正常访问页面的用户都受到攻击。
举例
举个例子,对于评论功能来说,就得防范持久型 XSS 攻击,因为我可以在评论中输入以下内容
这种情况如果前后端没有做好防御的话,这段评论就会被存储到数据库中,这样每个打开该页面的用户都会被攻击到。
非持久型
非持久型相比于前者危害就小的多了,一般通过修改 URL 参数的方式加入攻击代码,诱导用户访问链接从而进行攻击。
举例
举个例子,如果页面需要从 URL 中获取某些参数作为内容的话,不经过过滤就会导致攻击代码被执行
<!-- http://www.domain.com?name=<script>alert(1)</script> -->
<div>{{name}}</div>
<div>{{name}}</div>
但是对于这种攻击方式来说,如果用户使用 Chrome 这类浏览器的话,浏览器就能自动帮助用户防御攻击。但是我们不能因此就不防御此类攻击了,因为我不能确保用户都使用了该类浏览器。
如何防御
转义字符
转义输入输出的内容
首先,对于用户的输入应该是永远不信任的。最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义
// -> <script>alert(1)</script>
escape('<script>alert(1)</script>')
// -> <script>alert(1)</script>
escape('<script>alert(1)</script>')
白名单黑名单
但是对于显示富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式。
const xss = require('xss')
let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')
// -> <h1>XSS Demo</h1><script>alert("xss");</script>
console.log(html)
以上示例使用了 js-xss 来实现,可以看到在输出中保留了 h1 标签且过滤了 script 标签。
const xss = require('xss')
let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')
// -> <h1>XSS Demo</h1><script>alert("xss");</script>
console.log(html)
以上示例使用了 js-xss 来实现,可以看到在输出中保留了 h1 标签且过滤了 script 标签。
CSP
CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。
CSP开启
略
对于这种方式来说,只要开发者配置了正确的规则,那么即使网站存在漏洞,攻击者也不能执行它的攻击代码,并且 CSP 的兼容性也不错。
CSRF
面试题
什么是 CSRF 攻击?如何防范 CSRF 攻击?
什么是 CSRF 攻击?
CSRF 中文名为跨站请求伪造。
原理就是攻击者构造出一个后端请求地址,诱导用户点击或者通过某些途径自动发起请求。
如果用户是在登录状态下的话,后端就以为是用户在操作,从而进行相应的逻辑。
原理就是攻击者构造出一个后端请求地址,诱导用户点击或者通过某些途径自动发起请求。
如果用户是在登录状态下的话,后端就以为是用户在操作,从而进行相应的逻辑。
举例
举个例子,假设网站中有一个通过 GET 请求提交用户评论的接口,那么攻击者就可以在钓鱼网站中加入一个图片,图片的地址就是评论接口
<img src="http://www.domain.com/xxx?comment='attack'"/>
<img src="http://www.domain.com/xxx?comment='attack'"/>
如何防御
防范 CSRF 攻击可以遵循以下几种规则:
1、Get 请求不对数据进行修改
2、不让第三方网站访问到用户 Cookie
3、阻止第三方网站请求接口
4、请求时附带验证信息,比如验证码或者 Token
2、不让第三方网站访问到用户 Cookie
3、阻止第三方网站请求接口
4、请求时附带验证信息,比如验证码或者 Token
SameSite
可以对 Cookie 设置 SameSite 属性。该属性表示 Cookie 不随着跨域请求发送,可以很大程度减少 CSRF 的攻击,但是该属性目前并不是所有浏览器都兼容。
验证 Referer
对于需要防范 CSRF 的请求,我们可以通过验证 Referer 来判断该请求是否为第三方网站发起的。
Token
服务器下发一个随机 Token,每次发起请求时将 Token 携带上,服务器验证 Token 是否有效。
点击劫持
面试题
什么是点击劫持?
如何防范点击劫持?
如何防范点击劫持?
什么是点击劫持
点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。
防御方法
X-FRAME-OPTIONS
X-FRAME-OPTIONS 是一个 HTTP 响应头,在现代浏览器有一个很好的支持。这个 HTTP 响应头 就是为了防御用 iframe 嵌套的点击劫持攻击。
该响应头有三个值可选,分别是
DENY,表示页面不允许通过 iframe 的方式展示
SAMEORIGIN,表示页面可以在相同域名下通过 iframe 的方式展示
ALLOW-FROM,表示页面可以在指定来源的 iframe 中展示
SAMEORIGIN,表示页面可以在相同域名下通过 iframe 的方式展示
ALLOW-FROM,表示页面可以在指定来源的 iframe 中展示
JS 防御
对于某些远古浏览器来说,并不能支持上面的这种方式,那我们只有通过 JS 的方式来防御点击劫持了。
以上代码的作用就是当通过 iframe 的方式加载页面时,攻击者的网页直接不显示所有内容了。
中间人攻击
面试题
什么是中间人攻击?
如何防范中间人攻击?
如何防范中间人攻击?
中间人攻击是攻击方同时与服务端和客户端建立起了连接,并让对方认为连接是安全的,但是实际上整个通信过程都被攻击者控制了。攻击者不仅能获得双方的通信信息,还能修改通信信息。
通常来说不建议使用公共的 Wi-Fi,因为很可能就会发生中间人攻击的情况。如果你在通信的过程中涉及到了某些敏感信息,就完全暴露给攻击方了。
当然防御中间人攻击其实并不难,只需要增加一个安全通道来传输信息。HTTPS 就可以用来防御中间人攻击,但是并不是说使用了 HTTPS 就可以高枕无忧了,因为如果你没有完全关闭 HTTP 访问的话,攻击方可以通过某些方式将 HTTPS 降级为 HTTP 从而实现中间人攻击。
通常来说不建议使用公共的 Wi-Fi,因为很可能就会发生中间人攻击的情况。如果你在通信的过程中涉及到了某些敏感信息,就完全暴露给攻击方了。
当然防御中间人攻击其实并不难,只需要增加一个安全通道来传输信息。HTTPS 就可以用来防御中间人攻击,但是并不是说使用了 HTTPS 就可以高枕无忧了,因为如果你没有完全关闭 HTTP 访问的话,攻击方可以通过某些方式将 HTTPS 降级为 HTTP 从而实现中间人攻击。
从 V8 中看 JS 性能优化
测试性能工具
Audits
在这个界面中,我们可以选择想测试的功能然后点击 Run audits ,工具就会自动运行帮助我们测试问题并且给出一个完整的报告
可以看到报告中分别为性能、体验、SEO 都给出了打分,并且每一个指标都有详细的评估
评估结束后,工具还提供了一些建议便于我们提高这个指标的分数
我们只需要一条条根据建议去优化性能即可。
我们只需要一条条根据建议去优化性能即可。
Performance
在这张图中,我们可以详细的看到每个时间段中浏览器在处理什么事情,哪个过程最消耗时间,便于我们更加详细的了解性能瓶颈。
JS 性能优化
JS 是编译型还是解释型语言其实并不固定。首先 JS 需要有引擎才能运行起来,无论是浏览器还是在 Node 中,这是解释型语言的特性。
但是在 V8 引擎下,又引入了 TurboFan 编译器,他会在特定的情况下进行优化,将代码编译成执行效率更高的 Machine Code,当然这个编译器并不是 JS 必须需要的,只是为了提高代码执行性能,所以总的来说 JS 更偏向于解释型语言。
但是在 V8 引擎下,又引入了 TurboFan 编译器,他会在特定的情况下进行优化,将代码编译成执行效率更高的 Machine Code,当然这个编译器并不是 JS 必须需要的,只是为了提高代码执行性能,所以总的来说 JS 更偏向于解释型语言。
V8 转换代码的过程
从上图中我们可以发现,JS 会首先被解析为 AST,解析的过程其实是略慢的。代码越多,解析的过程也就耗费越长,这也是我们需要压缩代码的原因之一。另外一种减少解析时间的方式是预解析,会作用于未执行的函数,这个我们下面再谈。
这里需要注意一点,对于函数来说,应该尽可能避免声明嵌套函数(类也是函数),因为这样会造成函数的重复解析。
function test1() {
// 会被重复解析
function test2() {}
}
function test1() {
// 会被重复解析
function test2() {}
}
小结
1、可以通过 Audit 工具获得网站的多个指标的性能报告
2、可以通过 Performance 工具了解网站的性能瓶颈
3.可以通过 Performance API 具体测量时间
4、为了减少编译时间,我们可以采用减少代码文件的大小或者减少书写嵌套函数的方式
5、为了让 V8 优化代码,我们应该尽可能保证传入参数的类型一致。这也给我们带来了一个思考,这是不是也是使用 TypeScript 能够带来的好处之一
2、可以通过 Performance 工具了解网站的性能瓶颈
3.可以通过 Performance API 具体测量时间
4、为了减少编译时间,我们可以采用减少代码文件的大小或者减少书写嵌套函数的方式
5、为了让 V8 优化代码,我们应该尽可能保证传入参数的类型一致。这也给我们带来了一个思考,这是不是也是使用 TypeScript 能够带来的好处之一
性能优化琐碎事
图片优化
计算图片大小
减少像素点
减少每个像素点能够显示的颜色
减少每个像素点能够显示的颜色
图片加载优化
1、不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
2、对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
3、小图使用 base64 格式
4、将多个图标文件整合到一张图片中(雪碧图)
5、选择正确的图片格式:
(1)对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
(2)小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
(3)照片使用 JPEG
2、对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
3、小图使用 base64 格式
4、将多个图标文件整合到一张图片中(雪碧图)
5、选择正确的图片格式:
(1)对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
(2)小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
(3)照片使用 JPEG
DNS 预解析
DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP。
<link rel="dns-prefetch" href="//yuchengkai.cn">
<link rel="dns-prefetch" href="//yuchengkai.cn">
节流
考虑一个场景,滚动事件中会发起网络请求,但是我们并不希望用户在滚动过程中一直发起请求,而是隔一段时间发起一次,对于这种情况我们就可以使用节流。
防抖
考虑一个场景,有一个按钮点击会触发网络请求,但是我们并不希望每次点击都发起网络请求,而是当用户点击按钮一段时间后没有再次点击的情况才去发起网络请求,对于这种情况我们就可以使用防抖。
预加载
在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载。
预加载其实是声明式的 fetch ,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载
<link rel="preload" href="http://example.com">
预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好。
预加载其实是声明式的 fetch ,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载
<link rel="preload" href="http://example.com">
预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好。
预渲染
可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染
<link rel="prerender" href="http://example.com">
预渲染虽然可以提高页面的加载速度,但是要确保该页面大概率会被用户在之后打开,否则就是白白浪费资源去渲染。
<link rel="prerender" href="http://example.com">
预渲染虽然可以提高页面的加载速度,但是要确保该页面大概率会被用户在之后打开,否则就是白白浪费资源去渲染。
懒执行
懒执行就是将某些逻辑延迟到使用时再计算。该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒。
懒加载
懒加载就是将不关键的资源延后加载。
懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载。
懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等等。
懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载。
懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等等。
CDN
CDN 的原理是尽可能的在各个地方分布机房缓存数据,这样即使我们的根服务器远在国外,在国内的用户也可以通过国内的机房迅速加载资源。
因此,我们可以将静态资源尽量使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名。并且对于 CDN 加载静态资源需要注意 CDN 域名要与主站不同,否则每次请求都会带上主站的 Cookie,平白消耗流量。
因此,我们可以将静态资源尽量使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名。并且对于 CDN 加载静态资源需要注意 CDN 域名要与主站不同,否则每次请求都会带上主站的 Cookie,平白消耗流量。
Webpack 性能优化
高频考点:
1、有哪些方式可以减少 Webpack 的打包时间
2、有哪些方式可以让 Webpack 打出来的包更小
2、有哪些方式可以让 Webpack 打出来的包更小
减少 Webpack 打包时间
优化 Loader
因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,
项目越大,转换代码越多,效率就越低
项目越大,转换代码越多,效率就越低
优化
优化 Loader 的文件搜索范围
对于 Babel 来说,我们肯定是希望只作用在 JS 代码上的,然后 node_modules 中使用的代码都是编译过的,所以我们也完全没有必要再去处理一遍。
将 Babel 编译过的文件缓存起来
可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间
loader: 'babel-loader?cacheDirectory=true'
HappyPack
受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。
HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了
HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了
思路引导
DllPlugin
DllPlugin 可以将特定的类库提前打包然后引入。
这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。
这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。
然后我们需要执行这个配置文件生成依赖文件,接下来我们需要使用 DllReferencePlugin 将依赖文件引入项目中
代码压缩
在 Webpack3 中,我们一般使用 UglifyJS 来压缩代码,但是这个是单线程运行的,为了加快效率,我们可以使用 webpack-parallel-uglify-plugin 来并行运行 UglifyJS,从而提高效率。
在 Webpack4 中,我们就不需要以上这些操作了,只需要将 mode 设置为 production 就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 console.log 这类代码的功能。
在 Webpack4 中,我们就不需要以上这些操作了,只需要将 mode 设置为 production 就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 console.log 这类代码的功能。
一些小的优化点
1、resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面
2、resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径
3、module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助
2、resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径
3、module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助
减少 Webpack 打包后的文件体积
按需加载
想必大家在开发 SPA 项目的时候,项目中都会存在十几甚至更多的路由页面。如果我们将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,我们肯定是希望首页能加载的文件体积越小越好,这时候我们就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 loadash 这种大型类库同样可以使用这个功能。
用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。
Scope Hoisting
Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。
举例
略
配置
module.exports = {
optimization: {
concatenateModules: true
}
}
optimization: {
concatenateModules: true
}
}
Tree Shaking
Tree Shaking 可以实现删除项目中未被引用的代码
举例
// test.js
export const a = 1
export const b = 2
// index.js
import { a } from './test.js'
export const a = 1
export const b = 2
// index.js
import { a } from './test.js'
对于以上情况,test 文件中的变量 b 如果没有在项目中使用到的话,就不会被打包到文件中。
如果你使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。
实现小型打包工具
实现一个几十行的迷你打包工具,
该工具可以实现以下两个功能
该工具可以实现以下两个功能
1、将 ES6 转换为 ES5
2、支持在 JS 文件中 import CSS 文件
2、支持在 JS 文件中 import CSS 文件
实现
略
React 和 vue两大框架之间的相爱相杀
MVVM
面试题
什么是 MVVM?
比之 MVC 有什么区别?
比之 MVC 有什么区别?
View 和 Model
1、View 很简单,就是用户看到的视图
2、Model 同样很简单,一般就是本地数据和数据库中的数据
2、Model 同样很简单,一般就是本地数据和数据库中的数据
基本上,我们写的产品就是通过接口从数据库中读取数据,然后将数据经过处理展现到用户看到的视图上。当然我们还可以从视图上读取用户的输入,然后又将用户的输入通过接口写入到数据库中。但是,如何将数据展示到视图上,然后又如何将用户的输入写入到数据中,不同的人就产生了不同的看法,从此出现了很多种架构设计。
MVC
传统的 MVC 架构通常是使用控制器更新模型,视图从模型中获取数据去渲染。当用户有输入时,会通过控制器去更新模型,并且通知视图进行更新。
但是 MVC 有一个巨大的缺陷就是控制器承担的责任太大了,随着项目愈加复杂,控制器中的代码会越来越臃肿,导致出现不利于维护的情况。
MVVM
在 MVVM 架构中,引入了 ViewModel 的概念。ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种情况下,View 和 Model 都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel。
以 Vue 框架来举例,ViewModel 就是组件的实例。View 就是模板,Model 的话在引入 Vuex 的情况下是完全可以和组件分离的。
思路引导
除了以上三个部分,其实在 MVVM 中还引入了一个隐式的 Binder 层,实现了 View 和 ViewModel 的绑定。
同样以 Vue 框架来举例,这个隐式的 Binder 层就是 Vue 通过解析模板中的插值和指令从而实现 View 与 ViewModel 的绑定。
对于 MVVM 来说,其实最重要的并不是通过双向绑定或者其他的方式将 View 与 ViewModel 绑定起来,而是通过 ViewModel 将视图中的状态和用户的行为分离出一个抽象,这才是 MVVM 的精髓。
Virtual DOM
面试题:
什么是 Virtual DOM?
为什么 Virtual DOM 比原生 DOM 快?
为什么 Virtual DOM 比原生 DOM 快?
那么相较于 DOM 来说,操作 JS 对象会快很多,并且我们也可以通过 JS 来模拟 DOM
JS 模拟 DOM
上述代码对应的 DOM 就是
<ul class='list'>
<li>1</li>
</ul>
<ul class='list'>
<li>1</li>
</ul>
么既然 DOM 可以通过 JS 对象来模拟,反之也可以通过 JS 对象来渲染出对应的 DOM。当然了,通过 JS 来模拟 DOM 并且渲染对应的 DOM 只是第一步,难点在于如何判断新旧两个 JS 对象的最小差异并且实现局部更新 DOM。
算法复杂度O(n)
首先 DOM 是一个多叉树的结构,如果需要完整的对比两颗树的差异,那么需要的时间复杂度会是 O(n ^ 3),这个复杂度肯定是不能接受的。于是 React 团队优化了算法,实现了 O(n) 的复杂度来对比差异。
实现 O(n) 复杂度的关键就是只对比同层的节点,而不是跨层对比,这也是考虑到在实际业务中很少会去跨层的移动 DOM 元素。 所以判断差异的算法就分为了两步
实现 O(n) 复杂度的关键就是只对比同层的节点,而不是跨层对比,这也是考虑到在实际业务中很少会去跨层的移动 DOM 元素。 所以判断差异的算法就分为了两步
判断差异的算法2步
1、首先从上至下,从左往右遍历对象,也就是树的深度遍历,
这一步中会给每个节点添加索引,便于最后渲染差异
这一步中会给每个节点添加索引,便于最后渲染差异
在第一步算法中我们需要判断
1)新旧节点的 tagName 是否相同,如果不相同的话就代表节点被替换了。
2)如果没有更改 tagName 的话,就需要判断是否有子元素,有的话就进行第二步算法。
1)新旧节点的 tagName 是否相同,如果不相同的话就代表节点被替换了。
2)如果没有更改 tagName 的话,就需要判断是否有子元素,有的话就进行第二步算法。
2、一旦节点有子元素,就去判断子元素是否有不同
在第二步算法中,我们需要判断:
1)原本的列表中是否有节点被移除,
2)在新的列表中需要判断是否有新的节点加入,
3)还需要判断节点是否有移动。
1)原本的列表中是否有节点被移除,
2)在新的列表中需要判断是否有新的节点加入,
3)还需要判断节点是否有移动。
举例
// 假设这里模拟一个 ul,其中包含了 5 个 li
[1, 2, 3, 4, 5]
// 这里替换上面的 li
[1, 2, 5, 4]
[1, 2, 3, 4, 5]
// 这里替换上面的 li
[1, 2, 5, 4]
从上述例子中,我们一眼就可以看出先前的 ul 中的第三个 li 被移除了,四五替换了位置。
如何识别改动了哪个节点?
key
这个属性是用来给每一个节点打标志的,用于判断是否是同一个节点。
当然在判断以上差异的过程中,我们还需要判断节点的属性是否有变化等等。
局部更新 DOM
当我们判断出以上的差异后,就可以把这些差异记录下来。当对比完两棵树以后,就可以通过差异去局部更新 DOM,实现性能的最优化。
当然了 Virtual DOM 提高性能是其中一个优势,其实最大的优势还是在于:
1、将 Virtual DOM 作为一个兼容层,让我们还能对接非 Web 端的系统,实现跨端开发。
2、同样的,通过 Virtual DOM 我们可以渲染到其他的平台,比如实现 SSR、同构渲染等等。
3、实现组件的高度抽象化
2、同样的,通过 Virtual DOM 我们可以渲染到其他的平台,比如实现 SSR、同构渲染等等。
3、实现组件的高度抽象化
路由原理
面试题
前端路由原理?
两种实现方式有什么区别?
两种实现方式有什么区别?
原理
前端路由实现起来其实很简单,本质就是监听 URL 的变化,
然后匹配路由规则,显示相应的页面,并且无须刷新页面。
目前前端使用的路由就只有两种实现方式
然后匹配路由规则,显示相应的页面,并且无须刷新页面。
目前前端使用的路由就只有两种实现方式
两种模式
Hash模式
www.test.com/#/ 就是 Hash URL,当 # 后面的哈希值发生变化时,可以通过 hashchange 事件来监听到 URL 的变化,从而进行跳转页面,并且无论哈希值如何变化,服务端接收到的 URL 请求永远是 www.test.com。
window.addEventListener('hashchange', () => {
// ... 具体逻辑
})
// ... 具体逻辑
})
History模式
History 模式是 HTML5 新推出的功能,主要使用 history.pushState 和 history.replaceState 改变 URL。
通过 History 模式改变 URL 同样不会引起页面的刷新,只会更新浏览器的历史记录。
// 新增历史记录
history.pushState(stateObject, title, URL)
// 替换当前历史记录
history.replaceState(stateObject, title, URL)
history.pushState(stateObject, title, URL)
// 替换当前历史记录
history.replaceState(stateObject, title, URL)
当用户做出浏览器动作时,比如点击后退按钮时会触发 popState 事件
window.addEventListener('popstate', e => {
// e.state 就是 pushState(stateObject) 中的 stateObject
console.log(e.state)
})
// e.state 就是 pushState(stateObject) 中的 stateObject
console.log(e.state)
})
两种模式对比
1、Hash 模式只可以更改 # 后面的内容,History 模式可以通过 API 设置任意的同源 URL
2、History 模式可以通过 API 添加任意类型的数据到历史记录中,Hash 模式只能更改哈希值,也就是字符串
3、Hash 模式无需后端配置,并且兼容性好。History 模式在用户手动输入地址或者刷新页面的时候会发起 URL 请求,后端需要配置 index.html 页面用于匹配不到静态资源的时候
2、History 模式可以通过 API 添加任意类型的数据到历史记录中,Hash 模式只能更改哈希值,也就是字符串
3、Hash 模式无需后端配置,并且兼容性好。History 模式在用户手动输入地址或者刷新页面的时候会发起 URL 请求,后端需要配置 index.html 页面用于匹配不到静态资源的时候
Vue 和 React 之间的区别
Vue使用v-model实现双向数据绑定更方便
Vue 的表单可以使用 v-model 支持双向绑定,相比于 React 来说开发上更加方便,
当然了 v-model 其实就是个语法糖,本质上和 React 写表单的方式没什么区别
当然了 v-model 其实就是个语法糖,本质上和 React 写表单的方式没什么区别
改变数据的方式
Vue 修改状态相比来说要简单许多,React 需要使用 setState 来改变状态,并且使用这个 API 也有一些坑点。
并且 Vue 的底层使用了依赖追踪,页面更新渲染已经是最优的了,但是 React 还是需要用户手动去优化这方面的问题。
并且 Vue 的底层使用了依赖追踪,页面更新渲染已经是最优的了,但是 React 还是需要用户手动去优化这方面的问题。
React使用JSX
Vue使用模板语法
Vue使用模板语法
React 需要使用 JSX,有一定的上手成本,并且需要一整套的工具链支持,但是完全可以通过 JS 来控制页面,更加的灵活。Vue 使用了模板语法,相比于 JSX 来说没有那么灵活,但是完全可以脱离工具链,通过直接编写 render 函数就能在浏览器中运行。
生态
两者其实没多大的差距,
当然 React 的用户是远远高于 Vue 的
当然 React 的用户是远远高于 Vue 的
使用成本
Vue 一开始的定位就是尽可能的降低前端开发的门槛,
然而 React 更多的是去改变用户去接受它的概念和思想,相较于 Vue 来说上手成本略高
然而 React 更多的是去改变用户去接受它的概念和思想,相较于 Vue 来说上手成本略高
Vue常考基础知识点
生命周期钩子函数
beforeCreate
获取不到 props 或者 data 中的数据的,因为这些数据的初始化都在 initState 中
created
以访问到之前不能访问到的数据,
但是这时候组件还没被挂载,所以是看不到的
但是这时候组件还没被挂载,所以是看不到的
beforeMount
开始创建 VDOM
mounted
将 VDOM 渲染为真实 DOM 并且渲染数据
件中如果有子组件的话,会递归挂载子组件,只有当所有子组件全部挂载完毕,才会执行根组件的挂载钩子。
件中如果有子组件的话,会递归挂载子组件,只有当所有子组件全部挂载完毕,才会执行根组件的挂载钩子。
beforeUpdate
updated
updated
数据更新时调用,分别在数据更新前和更新后会调用
activated
deactivated
deactivated
keep-alive 独有的生命周期
用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,
命中缓存渲染后会执行 actived 钩子函数
命中缓存渲染后会执行 actived 钩子函数
beforeDestroy
移除事件、定时器等等,否则可能会引起内存泄露的问题
然后进行一系列的销毁操作,如果有子组件的话,也会递归销毁子组件,
所有子组件都销毁完毕后才会执行根组件的 destroyed 钩子函数。
然后进行一系列的销毁操作,如果有子组件的话,也会递归销毁子组件,
所有子组件都销毁完毕后才会执行根组件的 destroyed 钩子函数。
destroyed
组件通信
父子通信
最典型的方法
父->子
props,父组件通过 props 传递数据给子组件
子->父
子组件通过 emit 发送事件传递数据给父组件
这种父子通信方式也就是典型的单向数据流
v-model
使用语法糖 v-model 来直接实现,因为 v-model 默认会解析成名为 value 的 prop 和名为 input 的事件。
这种语法糖的方式是典型的双向绑定,常用于 UI 控件上,但是究其根本,还是通过事件的方法让父组件修改数据。
这种语法糖的方式是典型的双向绑定,常用于 UI 控件上,但是究其根本,还是通过事件的方法让父组件修改数据。
$parent 或者 $children
可以通过访问 $parent 或者 $children 对象来访问组件实例中的方法和数据
$listeners 和 .sync
略过了,真的不想看。。。。
兄弟组件通信
对于这种情况可以通过查找父组件中的子组件实现,也就是 this.$parent.$children,
在 $children 中可以通过组件 name 查询到需要的组件实例,然后进行通信
在 $children 中可以通过组件 name 查询到需要的组件实例,然后进行通信
跨多层级组件通信
provide / inject
任意组件
Vuex
Event Bus
extend 能做什么
这个 API 很少用到,作用是扩展组件生成一个构造器,通常会与 $mount 一起使用。
mixin 和 mixins 区别
mixin
mixin 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。
虽然文档不建议我们在应用中直接使用 mixin,但是如果不滥用的话也是很有帮助的,
比如可以全局混入封装好的 ajax 或者一些工具函数等等。
比如可以全局混入封装好的 ajax 或者一些工具函数等等。
mixins
mixins 应该是我们最常使用的扩展组件的方式了。
如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。
如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。
computed 和 watch 区别
computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容。
watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。
所以一般来说需要依赖别的属性来动态获得值的时候可以使用 computed,
对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用 watch。
对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用 watch。
需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并
keep-alive 组件有什么作用
如果你需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。
对于 keep-alive 组件来说,它拥有两个独有的生命周期钩子函数,分别为 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。
对于 keep-alive 组件来说,它拥有两个独有的生命周期钩子函数,分别为 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。
v-show 与 v-if 区别
v-show 只是在 display: none 和 display: block 之间切换。无论初始条件是什么都会被渲染出来,后面只需要切换 CSS,DOM 还是一直保留着的。所以总的来说 v-show 在初始渲染时有更高的开销,但是切换开销很小,更适合于频繁切换的场景。
v-if 的话就得说到 Vue 底层的编译了。当属性初始为 false 时,组件就不会被渲染,直到条件为 true,并且切换条件时会触发销毁/挂载组件,所以总的来说在切换时开销更高,更适合不经常切换的场景
并且基于 v-if 的这种惰性渲染机制,可以在必要的时候才去渲染组件,减少整个页面的初始渲染开销。
并且基于 v-if 的这种惰性渲染机制,可以在必要的时候才去渲染组件,减少整个页面的初始渲染开销。
组件中 data 什么时候可以使用对象
为什么把data写成函数
组件复用时所有组件实例都会共享 data,
如果 data 是对象的话,就会造成一个组件修改 data 以后会影响到其他所有组件,
所以需要将 data 写成函数,每次用到就调用一次函数获得新的数据。
如果 data 是对象的话,就会造成一个组件修改 data 以后会影响到其他所有组件,
所以需要将 data 写成函数,每次用到就调用一次函数获得新的数据。
什么时候可以把data写成对象
当我们使用 new Vue() 的方式的时候,无论我们将 data 设置为对象还是函数都是可以的,
因为 new Vue() 的方式是生成一个根组件,该组件不会复用,也就不存在共享 data 的情况了。
因为 new Vue() 的方式是生成一个根组件,该组件不会复用,也就不存在共享 data 的情况了。
Vue 常考进阶知识点
响应式原理
监听数据的 set 和 get 的事件
Vue 内部使用了 Object.defineProperty() 来实现数据响应式,通过这个函数可以监听到 set 和 get 的事件
数据劫持
以上代码简单的实现了如何监听数据的 set 和 get 的事件,但是仅仅如此是不够的,因为自定义的函数一开始是不会执行的。只有先执行了依赖收集,才能在属性更新的时候派发更新,所以接下来我们需要先触发依赖收集。
依赖收集
<div>
{{name}}
</div>
{{name}}
</div>
在解析如上模板代码时,遇到 {{name}} 就会进行依赖收集。
实现一个 Dep 类,
用于解耦属性的依赖收集和派发更新操作
用于解耦属性的依赖收集和派发更新操作
以上的代码实现很简单,当需要依赖收集的时候调用 addSub,当需要派发更新的时候调用 notify。
接下来我们先来简单的了解下 Vue 组件挂载时添加响应式的过程。
在组件挂载时,会先对所有需要的属性调用 Object.defineProperty(),然后实例化 Watcher,传入组件更新的回调。
在实例化过程中,会对模板中的属性进行求值,触发依赖收集。
在组件挂载时,会先对所有需要的属性调用 Object.defineProperty(),然后实例化 Watcher,传入组件更新的回调。
在实例化过程中,会对模板中的属性进行求值,触发依赖收集。
触发依赖收集时的操作
因为这一小节主要目的是学习响应式原理的细节,所以接下来的代码会简略的表达触发依赖收集时的操作。
以上就是 Watcher 的简单实现,在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher 然后执行 update 函数。
接下来,需要对 defineReactive 函数进行改造,
在自定义函数中添加依赖收集和派发更新相关的代码。
在自定义函数中添加依赖收集和派发更新相关的代码。
以上所有代码实现了一个简易的数据响应式,核心思路就是手动触发一次属性的 getter 来实现依赖收集。
Object.defineProperty 的缺陷
缺陷
通过下标方式修改数组数据或者给对象新增属性并不会触发组件的重新渲染
产生这写缺陷的原因
因为 Object.defineProperty 不能拦截到这些操作,
更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题
更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题
怎么解决
Vue 提供了一个 API
解决添加对象属性不响应的问题
Vue.set( target, key, value )
解决添加对象属性不响应的问题
Vue.set( target, key, value )
好吧,我承认判断key的那段我没看懂。。
对于数组而言,
Vue 内部重写了以下函数实现派发更新
Vue 内部重写了以下函数实现派发更新
编译过程
三个阶段
1、将模板解析为 AST
2、优化 AST
3、将 AST 转换为 render 函数
2、优化 AST
3、将 AST 转换为 render 函数
NextTick 原理分析
nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM。
怎么实现
microtasks
macrotasks
会先判断是否能使用 setImmediate ,不能的话降级为 MessageChannel ,以上都不行的话就使用 setTimeout
React 常考基础知识点
生命周期
Reconciliation 阶段
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
因为 Reconciliation 阶段是可以被打断的,所以 Reconciliation 阶段会执行的生命周期函数就可能会出现调用多次的情况,从而引起 Bug。由此对于 Reconciliation 阶段调用的几个函数,除了 shouldComponentUpdate 以外,其他都应该避免去使用,并且 V16 中也引入了新的 API 来解决这个问题。
Commit 阶段
componentDidMount
componentDidUpdate
componentWillUnmount
componentDidUpdate
componentWillUnmount
setState
首先 setState 的调用并不会马上引起 state 的改变,并且如果你一次调用了多个 setState ,那么结果可能并不如你期待的一样。
第一,两次的打印都为 0,因为 setState 是个异步 API,只有同步代码运行完毕才会执行。setState 异步的原因我认为在于,setState 可能会导致 DOM 的重绘,如果调用一次就马上去进行重绘,那么调用多次就会造成不必要的性能损失。设计成异步的话,就可以将多次调用放入一个队列中,在恰当的时候统一进行更新过程。
第二,虽然调用了三次 setState ,但是 count 的值还是为 1。因为多次调用会合并为一次,只有当更新结束后 state 才会改变,三次调用等同于如下代码
第二,虽然调用了三次 setState ,但是 count 的值还是为 1。因为多次调用会合并为一次,只有当更新结束后 state 才会改变,三次调用等同于如下代码
当然你也可以通过以下方式来实现调用三次 setState 使得 count 为 3
如果你想在每次调用 setState 后获得正确的 state ,可以通过如下代码实现
性能优化
PS:下文中的 state 指代了 state 及 props
React不熟悉,略过略过。。。。
通信
其实 React 中的组件通信基本和 Vue 中的一致。同样也分为以下三种情况:
父子组件通信
兄弟组件通信
跨多层级组件通信
任意组件
兄弟组件通信
跨多层级组件通信
任意组件
父子通信
父组件通过 props 传递数据给子组件,子组件通过调用父组件传来的函数传递数据给父组件,这两种方式是最常用的父子通信实现办法。
这种父子通信方式也就是典型的单向数据流,父组件通过 props 传递数据,子组件不能直接修改 props, 而是必须通过调用父组件函数的方式告知父组件修改数据。
这种父子通信方式也就是典型的单向数据流,父组件通过 props 传递数据,子组件不能直接修改 props, 而是必须通过调用父组件函数的方式告知父组件修改数据。
兄弟组件通信
对于这种情况可以通过共同的父组件来管理状态和事件函数。比如说其中一个兄弟组件调用父组件传递过来的事件函数修改父组件中的状态,然后父组件将状态传递给另一个兄弟组件。
跨多层次组件通信
如果你使用 16.3 以上版本的话,对于这种情况可以使用 Context API。
任意组件
这种方式可以通过 Redux 或者 Event Bus 解决,另外如果你不怕麻烦的话,可以使用这种方式解决上述所有的通信情况
React 常考进阶知识点
HOC 是什么?
相比 mixins 有什么优点?
相比 mixins 有什么优点?
HOC高阶组件
其实这个做法在函数式编程里称之为高阶函数,大家都知道 React 的思想中是存在函数式编程的,高阶组件和高阶函数就是同一个东西。
我们实现一个函数,传入一个组件,然后在函数内部再实现一个函数去扩展传入的组件,最后返回一个新的组件,这就是高阶组件的概念,作用就是为了更好的复用代码。
我们实现一个函数,传入一个组件,然后在函数内部再实现一个函数去扩展传入的组件,最后返回一个新的组件,这就是高阶组件的概念,作用就是为了更好的复用代码。
mixins
其实这个做法在函数式编程里称之为高阶函数,大家都知道 React 的思想中是存在函数式编程的,高阶组件和高阶函数就是同一个东西。我们实现一个函数,传入一个组件,然后在函数内部再实现一个函数去扩展传入的组件,最后返回一个新的组件,这就是高阶组件的概念,作用就是为了更好的复用代码。
其实 HOC 和 Vue 中的 mixins 作用是一致的,并且在早期 React 也是使用 mixins 的方式。但是在使用 class 的方式创建组件以后,mixins 的方式就不能使用了,并且其实 mixins 也是存在一些问题的,比如:
其实 HOC 和 Vue 中的 mixins 作用是一致的,并且在早期 React 也是使用 mixins 的方式。但是在使用 class 的方式创建组件以后,mixins 的方式就不能使用了,并且其实 mixins 也是存在一些问题的,比如:
事件机制
React 其实自己实现了一套事件机制
以上类似代码想必大家经常会写到,但是你是否考虑过点击事件是否绑定在了每一个标签上?事实当然不是,JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document 上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。
另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault。
另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault。
那么实现合成事件的目的是什么呢?
总的来说在我看来好处有两点:
总的来说在我看来好处有两点:
1、合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力
2、对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。
2、对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。
更新内容
React 进阶系列:Hooks 该怎么用
监控
前端监控一般分为三种
页面埋点、
性能监控、
异常监控
性能监控、
异常监控
页面埋点
PV / UV
停留时长
流量来源
用户交互
停留时长
流量来源
用户交互
现思路大致可以分为两种
手写埋点
最常用的方式,可以自主选择需要监控的数据然后在相应的地方写入代码。
这种方式的灵活性很大,但是唯一的缺点就是工作量较大,每个需要监控的地方都得插入代码。
这种方式的灵活性很大,但是唯一的缺点就是工作量较大,每个需要监控的地方都得插入代码。
无埋点的方式
基本不需要开发者手写埋点了,而是统计所有的事件并且定时上报。
这种方式虽然没有前一种方式繁琐了,但是因为统计的是所有事件,所以还需要后期过滤出需要的数据。
这种方式虽然没有前一种方式繁琐了,但是因为统计的是所有事件,所以还需要后期过滤出需要的数据。
性能监控
使用浏览器自带的 Performance API
异常监控
对于异常监控来说,以下两种监控是必不可少的,分别是代码报错以及接口异常上报
代码报错
window.onerror
对于代码运行错误,通常的办法是使用 window.onerror 拦截报错。
该方法能拦截到大部分的详细报错信息,
该方法能拦截到大部分的详细报错信息,
但是也有例外
1、对于跨域的代码运行错误会显示 Script error. 对于这种情况我们需要给 script 标签添加 crossorigin 属性
2、对于某些浏览器可能不会显示调用栈信息,这种情况可以通过 arguments.callee.caller 来做栈递归
2、对于某些浏览器可能不会显示调用栈信息,这种情况可以通过 arguments.callee.caller 来做栈递归
catch
&
try catch
&
try catch
对于异步代码来说,可以使用 catch 的方式捕获错误。
比如 Promise 可以直接使用 catch 函数,async await 可以使用 try catch
比如 Promise 可以直接使用 catch 函数,async await 可以使用 try catch
但是要注意线上运行的代码都是压缩过的,需要在打包时生成 sourceMap 文件便于 debug。
将捕获的错误上传到服务器
对于捕获的错误需要上传给服务器,通常可以通过 img 标签的 src 发起一个请求
接口异常上报
接口异常就相对来说简单了,可以列举出出错的状态码。
一旦出现此类的状态码就可以立即上报出错。接口异常上报可以让开发人员迅速知道有哪些接口出现了大面积的报错,以便迅速修复问题。
一旦出现此类的状态码就可以立即上报出错。接口异常上报可以让开发人员迅速知道有哪些接口出现了大面积的报错,以便迅速修复问题。
UDP
面向无连接
不可靠性
高效
传输方式
适合使用的场景
UDP 相比 TCP 简单的多,不需要建立连接,不需要验证数据报文,不需要流量控制,只会把想发的数据报文一股脑的丢给对端
虽然 UDP 并没有 TCP 传输来的准确,但是也能在很多实时性要求高的地方有所作为
留言
虽然 UDP 并没有 TCP 传输来的准确,但是也能在很多实时性要求高的地方有所作为
留言
TCP
面试题
UDP 与 TCP 的区别是什么?
为什么 TCP 建立连接需要三次握手,明明两次就可以建立起连接
因为这是为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。
ARQ 协议
ARQ 协议也就是超时重传机制。通过确认和超时机制保证了数据的正确送达,ARQ 协议包含停止等待 ARQ 和连续 ARQ 两种协议。
滑动窗口
拥塞处理
拥塞处理和流量控制不同,后者是作用于接收方,保证接收方来得及接受数据。而前者是作用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的情况。
拥塞处理包括了四个算法,分别为:慢开始,拥塞避免,快速重传,快速恢复。
拥塞处理包括了四个算法,分别为:慢开始,拥塞避免,快速重传,快速恢复。
总结
建立连接需要三次握手,断开连接需要四次握手
滑动窗口解决了数据的丢包、顺序不对和流量控制问题
拥塞窗口实现了对流量的控制,保证在全天候环境下最优的传递数据
滑动窗口解决了数据的丢包、顺序不对和流量控制问题
拥塞窗口实现了对流量的控制,保证在全天候环境下最优的传递数据
HTTP 及 TLS
HTTP 请求中的内容
HTTP 请求由三部分构成
请求行
首部
实体
首部
实体
面试题:Post 和 Get 的区别?
1、Get 请求能缓存,Post 不能
2、Post 相对 Get 安全一点点,因为Get 请求都包含在 URL 里(当然你想写到 body 里也是可以的),且会被浏览器保存历史纪录。Post 不会,但是在抓包的情况下都是一样的。
3、URL有长度限制,会影响 Get 请求,但是这个长度限制是浏览器规定的,不是 RFC 规定的
4、Post 支持更多的编码类型且不对数据类型限制
2、Post 相对 Get 安全一点点,因为Get 请求都包含在 URL 里(当然你想写到 body 里也是可以的),且会被浏览器保存历史纪录。Post 不会,但是在抓包的情况下都是一样的。
3、URL有长度限制,会影响 Get 请求,但是这个长度限制是浏览器规定的,不是 RFC 规定的
4、Post 支持更多的编码类型且不对数据类型限制
首部
通用首部
请求首部
响应首部
实体首部
常见状态码
2XX 成功
200 OK,表示从客户端发来的请求在服务器端被正确处理
204 No content,表示请求成功,但响应报文不含实体的主体部分
205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
206 Partial Content,进行范围请求
204 No content,表示请求成功,但响应报文不含实体的主体部分
205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
206 Partial Content,进行范围请求
3XX 重定向
301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
302 found,临时性重定向,表示资源临时被分配了新的 URL
303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求
302 found,临时性重定向,表示资源临时被分配了新的 URL
303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求
4XX 客户端错误
400 bad request,请求报文存在语法错误
401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
403 forbidden,表示对请求资源的访问被服务器拒绝
404 not found,表示在服务器上没有找到请求的资源
401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
403 forbidden,表示对请求资源的访问被服务器拒绝
404 not found,表示在服务器上没有找到请求的资源
5XX 服务器错误
TLS
HTTPS 还是通过了 HTTP 来传输信息,但是信息通过 TLS 协议进行了加密。
在 TLS 中使用了两种加密技术,分别为:对称加密和非对称加密。
加密技术。。。。略过吧。。。真不想看
HTTP2 及 HTTP3
HTTP2
二进制传输
HTTP/2 中所有加强性能的核心点在于此。在之前的 HTTP 版本中,我们是通过文本的方式传输数据。在 HTTP/2 中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制格式编码。
多路复用
在 HTTP/2 中,有两个非常重要的概念,分别是帧(frame)和流(stream)。
帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。
多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能
帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。
多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能
Header 压缩
在 HTTP/1 中,我们使用文本的形式传输 header,在 header 携带 cookie 的情况下,可能每次都需要重复传输几百到几千的字节。
在 HTTP /2 中,使用了 HPACK 压缩格式对传输的 header 进行编码,减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header ,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。
在 HTTP /2 中,使用了 HPACK 压缩格式对传输的 header 进行编码,减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header ,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。
服务端 Push
在 HTTP/2 中,服务端可以在客户端某个请求后,主动推送其他资源。
可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用 prefetch 。
可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用 prefetch 。
HTTP3
略过了,不想看。。。
输入 URL 到页面渲染的整个流程
我觉得垃圾,不想做笔记。。。
设计模式
工厂模式
demo
当然工厂模式并不仅仅是用来 new 出实例。
可以想象一个场景。假设有一份很复杂的代码需要用户去调用,但是用户并不关心这些复杂的代码,只需要你提供给我一个接口去调用,用户只负责传递需要的参数,至于这些参数怎么使用,内部有什么逻辑是不关心的,只需要你最后返回我一个实例。这个构造过程就是工厂。
工厂起到的作用就是隐藏了创建实例的复杂度,只需要提供一个接口,简单清晰
可以想象一个场景。假设有一份很复杂的代码需要用户去调用,但是用户并不关心这些复杂的代码,只需要你提供给我一个接口去调用,用户只负责传递需要的参数,至于这些参数怎么使用,内部有什么逻辑是不关心的,只需要你最后返回我一个实例。这个构造过程就是工厂。
工厂起到的作用就是隐藏了创建实例的复杂度,只需要提供一个接口,简单清晰
单例模式
单例模式很常用,比如全局缓存、全局状态管理等等这些只需要一个对象,就可以使用单例模式。
单例模式的核心就是保证全局只有一个对象可以访问。因为 JS 是门无类的语言,所以别的语言实现单例的方式并不能套入 JS 中,我们只需要用一个变量确保实例只创建一次就行,以下是如何实现单例模式的例子
单例模式的核心就是保证全局只有一个对象可以访问。因为 JS 是门无类的语言,所以别的语言实现单例的方式并不能套入 JS 中,我们只需要用一个变量确保实例只创建一次就行,以下是如何实现单例模式的例子
适配器模式
适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。
demo
在 Vue 中,我们其实经常使用到适配器模式。比如父组件传递给子组件一个时间戳属性,组件内部需要将时间戳转为正常的日期显示,一般会使用 computed 来做转换这件事情,这个过程就使用到了适配器模式。
装饰器模式
装饰模式不需要改变已有的接口,作用是给对象添加功能。就像我们经常需要给手机戴个保护套防摔一样,不改变手机自身,给手机添加了保护套提供防摔功能。
demo
代理模式
代理是为了控制对对象的访问,不让外部直接访问到对象。在现实生活中,也有很多代理的场景。比如你需要买一件国外的产品,这时候你可以通过代购来购买产品。
在实际代码中其实代理的场景很多,也就不举框架中的例子了,比如事件代理就用到了代理模式。
在实际代码中其实代理的场景很多,也就不举框架中的例子了,比如事件代理就用到了代理模式。
因为存在太多的 li,不可能每个都去绑定事件。这时候可以通过给父节点绑定一个事件,让父节点作为代理去拿到真实点击的节点。
发布-订阅模式
写错了吧。。。。。。。。
观察者模式 vs 发布-订阅模式
区别
总结
1、在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
2、在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
3、观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
4、观察者 模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。
2、在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
3、观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
4、观察者 模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。
外观模式
外观模式提供了一个接口,隐藏了内部的逻辑,更加方便外部调用。
举个例子来说,我们现在需要实现一个兼容多种浏览器的添加事件方法
举个例子来说,我们现在需要实现一个兼容多种浏览器的添加事件方法
对于不同的浏览器,添加事件的方式可能会存在兼容问题。如果每次都需要去这样写一遍的话肯定是不能接受的,所以我们将这些判断逻辑统一封装在一个接口中,外部需要添加事件只需要调用 addEvent 即可。
常见数据结构
我选择看视频,文字太晦涩了。。。。。。。。。。。。
时间复杂度
栈
应用
队列
链表
树
AVL树
Trie
并查集
堆
常见算法题解析
评论里反映代码问题比较多。。。 so 看视频去
位运算
左移<<
算数右移>>
右移很好用,比如可以用在二分算法中取中间值
13 >> 1 // -> 6
13 >> 1 // -> 6
按位操作
按位与 &
按位或 |
按位异或 ^
8 ^ 7 // -> 15
8 ^ 8 // -> 0
// 1000 ^ 0111 -> 1111 -> 15
// 1000 ^ 1000 -> 0000 -> 0
8 ^ 8 // -> 0
// 1000 ^ 0111 -> 1111 -> 15
// 1000 ^ 1000 -> 0000 -> 0
面试题:两个数不使用四则运算得出和
这道题中可以按位异或,因为按位异或就是不进位加法,8 ^ 8 = 0 如果进位了,就是 16 了,所以我们只需要将两个数进行异或操作,然后进位。那么也就是说两个二进制都是 1 的位置,左边应该有一个进位 1,所以可以得出以下公式 a + b = (a ^ b) + ((a & b) << 1) ,然后通过迭代的方式模拟加法
排序
冒泡排序
原理
冒泡排序的原理如下,从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大,那么就交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。下一轮重复以上操作,但是此时最后一个元素已经是最大数了,所以不需要再比较最后一个元素,只需要比较到 length - 2 的位置。
时间复杂度
该算法的操作次数是一个等差数列 n + (n - 1) + (n - 2) + 1 ,去掉常数项以后得出时间复杂度是 O(n * n)
插入排序
选择排序
归并排序
快排
面试题
堆排序
系统自带排序实现
链表
反转单向链表
树
动态规划
CSS 常考面试题资料
如何写好一封简历
面试常用技巧
尽早准备简历
简历经常更新,隔一段时间就总结下自己的收获成长
分批投递简历
如何粗略判断公司是否靠谱
行业
融资
核心团队
司法经营风险
融资
核心团队
司法经营风险
如何回答问题
很推荐大家在准备面试的过程中,挖掘出自己擅长的技术内容,
然后在面试的过程中,寻找机会引导面试官提问你擅长的技术点。
然后在面试的过程中,寻找机会引导面试官提问你擅长的技术点。
如何应对可能答不好的题目
假如你遇到了一道不会的题目,但是稍微有一点想法,
你可以先坦白说这道题目不怎么会,但是愿意尝试回答一下,这样即使回答错了,也不会有什么问题。
但是千万不要不懂装懂,弄巧成拙。
你可以先坦白说这道题目不怎么会,但是愿意尝试回答一下,这样即使回答错了,也不会有什么问题。
但是千万不要不懂装懂,弄巧成拙。
多反思
子主题
谈钱
前方的路,让我们结伴同行
0 条评论
下一页