大家好,我是练习时长一年半的前端练习生,喜欢唱、跳、rap、敲代码。本文是笔者一年多来对前端基础知识的总结和思考,这些题目对自己是总结,对大家也是一点微薄的资料,希望能给大家带来一些帮助和启发。成文过程中得到了许多大佬的帮助,在此感谢恺哥的小册、神三元同学的前端每日一问以及许多素未谋面的朋友们,让我等萌新也有机会在前人的财富中拾人牙慧,班门弄斧Thanks♪(・ω・)ノ
本文将从以下十一个维度为读者总结前端基础知识
这个问题实质上是在回答
let
和var
有什么区别,对于这个问题,我们可以直接查看babel
转换前后的结果,看一下在循环中通过let
定义的变量是如何解决变量提升的问题
babel在let定义的变量前加了道下划线,避免在块级作用域外访问到该变量,除了对变量名的转换,我们也可以通过自执行函数来模拟块级作用域
(function(){
for(var i = 0; i < 5; i ++){
console.log(i) // 0 1 2 3 4
}
})();
console.log(i) // Uncaught ReferenceError: i is not defined
复制代码
不过这个问题并没有结束,我们回到var
和let/const
的区别上:
var
声明的变量会挂到window上,而let
和const
不会var
声明的变量存在变量提升,而let
和const
不会let
和const
声明形成块作用域,只能在块作用域里访问,不能跨块访问,也不能跨函数访问let
和const
不能声明同名变量,而var
可以let
和const
声明的变量不能在声明前被使用babel的转化,其实只实现了第2、3、5点
实现const的关键在于Object.defineProperty()
这个API,这个API用于在一个对象上增加或修改属性。通过配置属性描述符,可以精确地控制属性行为。Object.defineProperty()
接收三个参数:
Object.defineProperty(obj, prop, desc)
参数 | 说明 |
---|---|
obj | 要在其上定义属性的对象 |
prop | 要定义或修改的属性的名称 |
descriptor | 将被定义或修改的属性描述符 |
属性描述符 | 说明 | 默认值 |
---|---|---|
value | 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined | undefined |
get | 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined | undefined |
set | 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法 | undefined |
writable | 当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false | false |
enumerable | enumerable定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举 | false |
Configurable | configurable特性表示对象的属性是否可以被删除,以及除value和writable特性外的其他特性是否可以被修改 | false |
对于const不可修改的特性,我们通过设置writable属性来实现
function _const(key, value) {
const desc = {
value,
writable: false
}
Object.defineProperty(window, key, desc)
}
_const('obj', {a: 1}) //定义obj
obj.b = 2 //可以正常给obj的属性赋值
obj = {} //无法赋值新对象
复制代码
call() 方法
使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数
语法:function.call(thisArg, arg1, arg2, ...)
call()
的原理比较简单,由于函数的this指向它的直接调用者,我们变更调用者即完成this指向的变更:
//变更函数调用者示例
function foo() {
console.log(this.name)
}
// 测试
const obj = {
name: '写代码像蔡徐抻'
}
obj.foo = foo // 变更foo的调用者
obj.foo() // '写代码像蔡徐抻'
复制代码
基于以上原理, 我们两句代码就能实现call()
Function.prototype.myCall = function(thisArg, ...args) {
thisArg.fn = this // this指向调用call的对象,即我们要改变this指向的函数
return thisArg.fn(...args) // 执行函数并return其执行结果
}
复制代码
但是我们有一些细节需要处理:
Function.prototype.myCall = function(thisArg, ...args) {
const fn = Symbol('fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
thisArg = thisArg || window // 若没有传入this, 默认绑定window对象
thisArg[fn] = this // this指向调用call的对象,即我们要改变this指向的函数
const result = thisArg[fn](...args) // 执行当前函数
delete thisArg[fn] // 删除我们声明的fn属性
return result // 返回函数执行结果
}
//测试
foo.myCall(obj) // 输出'写代码像蔡徐抻'
复制代码
apply() 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。
语法:func.apply(thisArg, [argsArray])
apply()
和call()
类似,区别在于call()接收参数列表,而apply()接收一个参数数组,所以我们在call()的实现上简单改一下入参形式即可
Function.prototype.myApply = function(thisArg, args) {
const fn = Symbol('fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
thisArg = thisArg || window // 若没有传入this, 默认绑定window对象
thisArg[fn] = this // this指向调用call的对象,即我们要改变this指向的函数
const result = thisArg[fn](...args) // 执行当前函数(此处说明一下:虽然apply()接收的是一个数组,但在调用原函数时,依然要展开参数数组。可以对照原生apply(),原函数接收到展开的参数数组)
delete thisArg[fn] // 删除我们声明的fn属性
return result // 返回函数执行结果
}
//测试
foo.myApply(obj, []) // 输出'写代码像蔡徐抻'
复制代码
bind()
方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
语法: function.bind(thisArg, arg1, arg2, ...)
从用法上看,似乎给call/apply包一层function就实现了bind():
Function.prototype.myBind = function(thisArg, ...args) {
return () => {
this.apply(thisArg, args)
}
}
复制代码
但我们忽略了三点:
Function.prototype.myBind = function (thisArg, ...args) {
var self = this
// new优先级
var fbound = function () {
self.apply(this instanceof self ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)))
}
// 继承原型上的属性和方法
fbound.prototype = Object.create(self.prototype);
return fbound;
}
//测试
const obj = { name: '写代码像蔡徐抻' }
function foo() {
console.log(this.name)
console.log(arguments)
}
foo.myBind(obj, 'a', 'b', 'c')() //输出写代码像蔡徐抻 ['a', 'b', 'c']
复制代码
防抖和节流的概念都比较简单,所以我们就不在“防抖节流是什么”这个问题上浪费过多篇幅了,简单点一下:
防抖,即
短时间内大量触发同一事件,只会执行一次函数
,实现原理为设置一个定时器,约定在xx毫秒后再触发事件处理,每次触发事件都会重新设置计时器,直到xx毫秒内无第二次操作
,防抖常用于搜索框/滚动条的监听事件处理,如果不做防抖,每输入一个字/滚动屏幕,都会触发事件处理,造成性能浪费。
function debounce(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
复制代码
防抖是
延迟执行
,而节流是间隔执行
,函数节流即每隔一段时间就执行一次
,实现原理为设置一个定时器,约定xx毫秒后执行事件,如果时间到了,那么执行函数并重置定时器
,和防抖的区别在于,防抖每次触发事件都重置定时器,而节流在定时器到时间后再清空定时器
function throttle(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
func.apply(context, args)
}, wait)
}
}
}
复制代码
实现方式2:使用两个时间戳
prev旧时间戳
和now新时间戳
,每次触发事件都判断二者的时间差,如果到达规定时间,执行函数并重置旧时间戳
function throttle(func, wait) {
var prev = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - prev > wait) {
func.apply(context, args);
prev = now;
}
}
}
复制代码
对于
[1, [1,2], [1,2,3]]
这样多层嵌套的数组,我们如何将其扁平化为[1, 1, 2, 1, 2, 3]
这样的一维数组呢:
1.ES6的flat()
const arr = [1, [1,2], [1,2,3]]
arr.flat(Infinity) // [1, 1, 2, 1, 2, 3]
复制代码
2.序列化后正则
const arr = [1, [1,2], [1,2,3]]
const str = `[${JSON.stringify(arr).replace(/(\[|\])/g, '')}]`
JSON.parse(str) // [1, 1, 2, 1, 2, 3]
复制代码
3.递归
对于树状结构的数据,最直接的处理方式就是递归
const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
let result = []
for (const item of arr) {
item instanceof Array ? result = result.concat(flat(item)) : result.push(item)
}
return result
}
flat(arr) // [1, 1, 2, 1, 2, 3]
复制代码
4.reduce()递归
const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
return arr.reduce((prev, cur) => {
return prev.concat(cur instanceof Array ? flat(cur) : cur)
}, [])
}
flat(arr) // [1, 1, 2, 1, 2, 3]
复制代码
5.迭代+展开运算符
// 每次while都会合并一层的元素,这里第一次合并结果为[1, 1, 2, 1, 2, 3, [4,4,4]]
// 然后arr.some判定数组中是否存在数组,因为存在[4,4,4],继续进入第二次循环进行合并
let arr = [1, [1,2], [1,2,3,[4,4,4]]]
while (arr.some(Array.isArray)) {
arr = [].concat(...arr);
}
console.log(arr) // [1, 1, 2, 1, 2, 3, 4, 4, 4]
复制代码
实现一个符合规范的Promise篇幅比较长,建议阅读笔者上一篇文章:异步编程二三事 | Promise/async/Generator实现原理解析 | 9k字
在JS中一切皆对象,但JS并不是一种真正的面向对象(OOP)的语言,因为它缺少类(class)
的概念。虽然ES6引入了class
和extends
,使我们能够轻易地实现类和继承。但JS并不存在真实的类,JS的类是通过函数以及原型链机制模拟的,本小节的就来探究如何在ES5环境下利用函数和原型链实现JS面向对象的特性
在开始之前,我们先回顾一下原型链的知识,后续new
和继承
等实现都是基于原型链机制。很多介绍原型链的资料都能写上洋洋洒洒几千字,但我觉得读者们不需要把原型链想太复杂,容易把自己绕进去,其实在我看来,原型链的核心只需要记住三点:
__proto__属性
,该属性指向其原型对象,在调用实例的方法和属性时,如果在实例对象上找不到,就会往原型对象上找prototype属性
也指向实例的原型对象constructor属性
指向构造函数首先我们要知道new
做了什么
prototype
,这一步是为了继承构造函数原型上的属性和方法this
被指定为该新实例,这一步是为了执行构造函数内的赋值操作// new是关键字,这里我们用函数来模拟,new Foo(args) <=> myNew(Foo, args)
function myNew(foo, ...args) {
// 创建新对象,并继承构造方法的prototype属性, 这一步是为了把obj挂原型链上, 相当于obj.__proto__ = Foo.prototype
let obj = Object.create(foo.prototype)
// 执行构造方法, 并为其绑定新this, 这一步是为了让构造方法能进行this.name = name之类的操作, args是构造方法的入参, 因为这里用myNew模拟, 所以入参从myNew传入
let result = foo.apply(obj, args)
// 如果构造方法已经return了一个对象,那么就返回该对象,否则返回myNew创建的新对象(一般情况下,构造方法不会返回新实例,但使用者可以选择返回新实例来覆盖new创建的对象)
return Object.prototype.toString.call(result) === '[object Object]' ? result : obj
}
// 测试:
function Foo(name) {
this.name = name
}
const newObj = myNew(Foo, 'zhangsan')
console.log(newObj) // Foo {name: "zhangsan"}
console.log(newObj instanceof Foo) // true
复制代码
说到继承,最容易想到的是ES6的extends
,当然如果只回答这个肯定不合格,我们要从函数和原型链的角度上实现继承,下面我们一步步地、递进地实现一个合格的继承
原型链继承的原理很简单,直接让子类的原型对象指向父类实例,当子类实例找不到对应的属性和方法时,就会往它的原型对象,也就是父类实例上找,从而实现对父类的属性和方法的继承
// 父类
function Parent() {
this.name = '写代码像蔡徐抻'
}
// 父类的原型方法
Parent.prototype.getName = function() {
return this.name
}
// 子类
function Child() {}
// 让子类的原型对象指向父类实例, 这样一来在Child实例中找不到的属性和方法就会到原型对象(父类实例)上寻找
Child.prototype = new Parent()
Child.prototype.constructor = Child // 根据原型链的规则,顺便绑定一下constructor, 这一步不影响继承, 只是在用到constructor时会需要
// 然后Child实例就能访问到父类及其原型上的name属性和getName()方法
const child = new Child()
child.name // '写代码像蔡徐抻'
child.getName() // '写代码像蔡徐抻'
复制代码
原型继承的缺点:
super()
的功能// 示例:
function Parent() {
this.name = ['写代码像蔡徐抻']
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {}
Child.prototype = new Parent()
Child.prototype.constructor = Child
// 测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['foo'] (预期是['写代码像蔡徐抻'], 对child1.name的修改引起了所有child实例的变化)
复制代码
构造函数继承,即在子类的构造函数中执行父类的构造函数,并为其绑定子类的this
,让父类的构造函数把成员属性和方法都挂到子类的this
上去,这样既能避免实例之间共享一个原型实例,又能向父类构造方法传参
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
Parent.call(this, 'zhangsan') // 执行父类构造方法并绑定子类的this, 使得父类中的属性能够赋到子类的this上
}
//测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
child2.getName() // 报错,找不到getName(), 构造函数继承的方式继承不到父类原型上的属性和方法
复制代码
构造函数继承的缺点:
既然原型链继承和构造函数继承各有互补的优缺点, 那么我们为什么不组合起来使用呢, 所以就有了综合二者的组合式继承
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 构造函数继承
Parent.call(this, 'zhangsan')
}
//原型链继承
Child.prototype = new Parent()
Child.prototype.constructor = Child
//测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
child2.getName() // ['zhangsan']
复制代码
组合式继承的缺点:
Parent.call()
和new Parent()
),虽然这并不影响对父类的继承,但子类创建实例时,原型中会存在两份相同的属性和方法,这并不优雅为了解决构造函数被执行两次的问题, 我们将指向父类实例
改为指向父类原型
, 减去一次构造函数的执行
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 构造函数继承
Parent.call(this, 'zhangsan')
}
//原型链继承
// Child.prototype = new Parent()
Child.prototype = Parent.prototype //将`指向父类实例`改为`指向父类原型`
Child.prototype.constructor = Child
//测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
child2.getName() // ['zhangsan']
复制代码
但这种方式存在一个问题,由于子类原型和父类原型指向同一个对象,我们对子类原型的操作会影响到父类原型,例如给Child.prototype
增加一个getName()方法,那么会导致Parent.prototype
也增加或被覆盖一个getName()方法,为了解决这个问题,我们给Parent.prototype
做一个浅拷贝
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 构造函数继承
Parent.call(this, 'zhangsan')
}
//原型链继承
// Child.prototype = new Parent()
Child.prototype = Object.create(Parent.prototype) //将`指向父类实例`改为`指向父类原型`
Child.prototype.constructor = Child
//测试
const child = new Child()
const parent = new Parent()
child.getName() // ['zhangsan']
parent.getName() // 报错, 找不到getName()
复制代码
到这里我们就完成了ES5环境下的继承的实现,这种继承方式称为寄生组合式继承
,是目前最成熟的继承方式,babel对ES6继承的转化也是使用了寄生组合式继承
我们回顾一下实现过程:
一开始最容易想到的是原型链继承
,通过把子类实例的原型指向父类实例来继承父类的属性和方法,但原型链继承的缺陷在于对子类实例继承的引用类型的修改会影响到所有的实例对象
以及无法向父类的构造方法传参
。
因此我们引入了构造函数继承
, 通过在子类构造函数中调用父类构造函数并传入子类this来获取父类的属性和方法,但构造函数继承也存在缺陷,构造函数继承不能继承到父类原型链上的属性和方法
。
所以我们综合了两种继承的优点,提出了组合式继承
,但组合式继承也引入了新的问题,它每次创建子类实例都执行了两次父类构造方法
,我们通过将子类原型指向父类实例
改为子类原型指向父类原型的浅拷贝
来解决这一问题,也就是最终实现 —— 寄生组合式继承
上面几点只是V8执行机制的极简总结,建议阅读参考资料:
1.V8 是怎么跑起来的 —— V8 的 JavaScript 执行管道
2.JavaScript 引擎 V8 执行流程概述
JS引擎中对变量的存储主要有两种位置,栈内存和堆内存,栈内存存储基本类型数据以及引用类型数据的内存地址,堆内存储存引用类型的数据
栈内存的回收:
栈内存调用栈上下文切换后就被回收,比较简单
堆内存的回收:
V8的堆内存分为新生代内存和老生代内存,新生代内存是临时分配的内存,存在时间短,老生代内存存在时间长
参考资料:聊聊V8引擎的垃圾回收
参考资料:为什么V8引擎这么快?
大体流程如下:
defer、async
来进行异步下载width/height/padding/margin/border
)发生变化时会触发回流offset/scroll/client
等属性时会触发回流window.getComputedStyle
会触发回流class
替代style
,减少style的使用resize、scroll
时进行防抖和节流处理,这两者会直接导致回流visibility
替换display: none
,因为前者只会引起重绘,后者会引发回流offsetWidth
这类属性的值时,可以使用变量将查询结果存起来,避免多次查询,每次对offset/scroll/client
等属性进行查询时都会触发回流参考资料:必须明白的浏览器渲染机制
- Service Worker
和Web Worker类似,是独立的线程,我们可以在这个线程中缓存文件,在主线程需要的时候读取这里的文件,Service Worker使我们可以自由选择缓存哪些文件以及文件的匹配、读取规则,并且缓存是持续性的
- Memory Cache
即内存缓存,内存缓存不是持续性的,缓存会随着进程释放而释放
- Disk Cache
即硬盘缓存,相较于内存缓存,硬盘缓存的持续性和容量更优,它会根据HTTP header的字段判断哪些资源需要缓存
- Push Cache
即推送缓存,是HTTP/2的内容,目前应用较少
强缓存(不要向服务器询问的缓存)
设置Expires
「Expires: Thu, 26 Dec 2019 10:30:42 GMT」
表示缓存会在这个时间后失效,这个过期日期是绝对日期,如果修改了本地日期,或者本地日期与服务器日期不一致,那么将导致缓存过期时间错误。设置Cache-Control
max-age
字段来设置过期时间,例如「Cache-Control:max-age=3600」
除此之外Cache-Control还能设置private/no-cache
等多种字段协商缓存(需要向服务器询问缓存是否已经过期)
Last-Modified
Last-Modified
,当浏览器再次请求该资源时,浏览器会在请求头中带上If-Modified-Since
字段,字段的值就是之前服务器返回的最后修改时间,服务器对比这两个时间,若相同则返回304,否则返回新资源,并更新Last-ModifiedETag
两者对比
参考资料:浏览器缓存机制剖析
模型 | 概述 | 单位 |
---|---|---|
物理层 | 网络连接介质,如网线、光缆,数据在其中以比特为单位传输 | bit |
数据链路层 | 数据链路层将比特封装成数据帧并传递 | 帧 |
网络层 | 定义IP地址,定义路由功能,建立主机到主机的通信 | 数据包 |
传输层 | 负责将数据进行可靠或者不可靠传递,建立端口到端口的通信 | 数据段 |
会话层 | 控制应用程序之间会话能力,区分不同的进程 | |
表示层 | 数据格式标识,基本压缩加密功能 | |
应用层 | 各种应用软件 |
2xx 开头(请求成功)
200 OK
:客户端发送给服务器的请求被正常处理并返回
3xx 开头(重定向)
301 Moved Permanently
:永久重定向,请求的网页已永久移动到新位置。 服务器返回此响应时,会自动将请求者转到新位置
302 Moved Permanently
:临时重定向,请求的网页已临时移动到新位置。服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
304 Not Modified
:未修改,自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容
4xx 开头(客户端错误)
400 Bad Request
:错误请求,服务器不理解请求的语法,常见于客户端传参错误
401 Unauthorized
:未授权,表示发送的请求需要有通过 HTTP 认证的认证信息,常见于客户端未登录
403 Forbidden
:禁止,服务器拒绝请求,常见于客户端权限不足
404 Not Found
:未找到,服务器找不到对应资源
5xx 开头(服务端错误)
500 Inter Server Error
:服务器内部错误,服务器遇到错误,无法完成请求
501 Not Implemented
:尚未实施,服务器不具备完成请求的功能
502 Bad Gateway
:作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。
503 service unavailable
:服务不可用,服务器目前无法使用(处于超载或停机维护状态)。通常是暂时状态。
标准答案:
更进一步:
其实HTTP协议并没有要求GET/POST请求参数必须放在URL上或请求体里,也没有规定GET请求的长度,目前对URL的长度限制,是各家浏览器设置的限制。GET和POST的根本区别在于:GET请求是幂等性的,而POST请求不是
幂等性,指的是对某一资源进行一次或多次请求都具有相同的副作用。例如搜索就是一个幂等的操作,而删除、新增则不是一个幂等操作。
由于GET请求是幂等的,在网络不好的环境中,GET请求可能会重复尝试,造成重复操作数据的风险,因此,GET请求用于无副作用的操作(如搜索),新增/删除等操作适合用POST
一个HTTP请求报文由请求行(request line)、请求头(header)、空行和请求数据4个部分组成
响应报文和请求报文结构类似,不再赘述
通用头(请求头和响应头都有的首部)
字段 | 作用 | 值 |
---|---|---|
Cache-Control | 控制缓存 | public:表示响应可以被任何对象缓存(包括客户端/代理服务器) private(默认值):响应只能被单个客户缓存,不能被代理服务器缓存 no-cache:缓存要经过服务器验证,在浏览器使用缓存前,会对比ETag,若没变则返回304,使用缓存 no-store:禁止任何缓存 |
Connection | 是否需要持久连接(HTTP 1.1默认持久连接) | keep-alive / close |
Transfer-Encoding | 报文主体的传输编码格式 | chunked(分块) / identity(未压缩和修改) / gzip(LZ77压缩) / compress(LZW压缩,弃用) / deflate(zlib结构压缩) |
请求头
字段 | 作用 | 语法 |
---|---|---|
Accept | 告知(服务器)客户端可以处理的内容类型 | text/html、image/*、*/* |
If-Modified-Since | 将Last-Modified 的值发送给服务器,询问资源是否已经过期(被修改),过期则返回新资源,否则返回304 | 示例:If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT |
If-Unmodified-Since | 将Last-Modified 的值发送给服务器,询问文件是否被修改,若没有则返回200,否则返回412预处理错误,可用于断点续传。通俗点说If-Unmodified-Since 是文件没有修改时下载,If-Modified-Since 是文件修改时下载 | 示例:If-Unmodified-Since: Wed, 21 Oct 2015 07:28:00 GMT |
If-None-Match | 将ETag 的值发送给服务器,询问资源是否已经过期(被修改),过期则返回新资源,否则返回304 | 示例:If-None-Match: "bfc13a6472992d82d" |
If-Match | 将ETag 的值发送给服务器,询问文件是否被修改,若没有则返回200,否则返回412预处理错误,可用于断点续传 | 示例:If-Match: "bfc129c88ca92d82d" |
Range | 告知服务器返回文件的哪一部分, 用于断点续传 | 示例:Range: bytes=200-1000, 2000-6576, 19000- |
Host | 指明了服务器的域名(对于虚拟主机来说),以及(可选的)服务器监听的TCP端口号 | 示例:Host:www.baidu.com |
User-Agent | 告诉HTTP服务器, 客户端使用的操作系统和浏览器的名称和版本 | User-Agent: Mozilla/<version> (<system-information>) <platform> (<platform-details>) <extensions> |
响应头
字段 | 作用 | 语法 |
---|---|---|
Location | 需要将页面重新定向至的地址。一般在响应码为3xx的响应中才会有意义 | Location: <url> |
ETag | 资源的特定版本的标识符,如果内容没有改变,Web服务器不需要发送完整的响应 | ETag: "<etag_value>" |
Server | 处理请求的源头服务器所用到的软件相关信息 | Server: <product> |
实体头(针对请求报文和响应报文的实体部分使用首部)
字段 | 作用 | 语法 |
---|---|---|
Allow | 资源可支持http请求的方法 | Allow: <http-methods>,示例:Allow: GET, POST, HEAD |
Last-Modified | 资源最后的修改时间,用作一个验证器来判断接收到的或者存储的资源是否彼此一致,精度不如ETag | 示例:Last-Modified: Wed, 21 Oct 2020 07:28:00 GMT |
Expires | 响应过期时间 | Expires: <http-date>,示例:Expires: Wed, 21 Oct 2020 07:28:00 GMT |
HTTP首部当然不止这么几个,但为了避免写太多大家记不住(主要是别的我也没去看),这里只介绍了一些常用的,详细的可以看MDN的文档
Entity tag,If-Unmodified-Since, If-Match, If-None-Match
等新的请求头来控制缓存,详见浏览器缓存小节HTTP/2解决的问题,就是HTTP/1.1存在的问题:
为了解决以上几个问题,HTTP/2一个域名只使用一个TCP⻓连接来传输数据,而且请求直接是并行的、非阻塞的,这就是多路复用
实现原理: HTTP/2引入了一个二进制分帧层,客户端和服务端进行传输时,数据会先经过二进制分帧层处理,转化为一个个带有请求ID的帧,这些帧在传输完成后根据ID组合成对应的数据。
尽管HTTP/2解决了很多1.1的问题,但HTTP/2仍然存在一些缺陷,这些缺陷并不是来自于HTTP/2协议本身,而是来源于底层的TCP协议,我们知道TCP链接是可靠的连接,如果出现了丢包,那么整个连接都要等待重传,HTTP/1.1可以同时使用6个TCP连接,一个阻塞另外五个还能工作,但HTTP/2只有一个TCP连接,阻塞的问题便被放大了。
由于TCP协议已经被广泛使用,我们很难直接修改TCP协议,基于此,HTTP/3选择了一个折衷的方法——UDP协议,HTTP/2在UDP的基础上实现多路复用、0-RTT、TLS加密、流量控制、丢包重传等功能。
参考资料:http发展史(http0.9、http1.0、http1.1、http2、http3)梳理笔记 (推荐阅读)
我们通过分析几种加密方式,层层递进,理解HTTPS的加密方式以及为什么使用这种加密方式:
对称加密
客户端和服务器公用一个密匙用来对消息加解密,这种方式称为对称加密。客户端和服务器约定好一个加密的密匙。客户端在发消息前用该密匙对消息加密,发送给服务器后,服务器再用该密匙进行解密拿到消息。
这种方式一定程度上保证了数据的安全性,但密钥一旦泄露(密钥在传输过程中被截获),传输内容就会暴露,因此我们要寻找一种安全传递密钥的方法。
非对称加密
采用非对称加密时,客户端和服务端均拥有一个公钥和私钥,公钥加密的内容只有对应的私钥能解密。私钥自己留着,公钥发给对方。这样在发送消息前,先用对方的公钥对消息进行加密,收到后再用自己的私钥进行解密。这样攻击者只拿到传输过程中的公钥也无法破解传输的内容
尽管非对称加密解决了由于密钥被获取而导致传输内容泄露的问题,但中间人仍然可以用篡改公钥
的方式来获取或篡改传输内容,而且非对称加密的性能比对称加密的性能差了不少
第三方认证
上面这种方法的弱点在于,客户端不知道公钥是由服务端返回,还是中间人返回的,因此我们再引入一个第三方认证的环节:即第三方使用私钥加密我们自己的公钥
,浏览器已经内置一些权威第三方认证机构的公钥,浏览器会使用第三方的公钥
来解开第三方私钥加密过的我们自己的公钥
,从而获取公钥,如果能成功解密,就说明获取到的自己的公钥
是正确的
但第三方认证也未能完全解决问题,第三方认证是面向所有人的,中间人也能申请证书,如果中间人使用自己的证书掉包原证书,客户端还是无法确认公钥的真伪
数字签名
为了让客户端能够验证公钥的来源,我们给公钥加上一个数字签名,这个数字签名是由企业、网站等各种信息和公钥经过单向hash而来,一旦构成数字签名的信息发生变化,hash值就会改变,这就构成了公钥来源的唯一标识。
具体来说,服务端本地生成一对密钥,然后拿着公钥以及企业、网站等各种信息到CA(第三方认证中心)去申请数字证书,CA会通过一种单向hash算法(比如MD5),生成一串摘要,这串摘要就是这堆信息的唯一标识,然后CA还会使用自己的私钥对摘要进行加密,连同我们自己服务器的公钥一同发送给我我们。
浏览器拿到数字签名后,会使用浏览器本地内置的CA公钥解开数字证书并验证,从而拿到正确的公钥。由于非对称加密性能低下,拿到公钥以后,客户端会随机生成一个对称密钥,使用这个公钥加密并发送给服务端,服务端用自己的私钥解开对称密钥,此后的加密连接就通过这个对称密钥进行对称加密。
综上所述,HTTPS在验证阶段使用非对称加密+第三方认证+数字签名获取正确的公钥,获取到正确的公钥后以对称加密的方式通信
参考资料:看图学HTTPS
CSRF即Cross-site request forgery(跨站请求伪造),是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。
假如黑客在自己的站点上放置了其他网站的外链,例如"www.weibo.com/api
,默认情况下,浏览器会带着weibo.com
的cookie访问这个网址,如果用户已登录过该网站且网站没有对CSRF攻击进行防御,那么服务器就会认为是用户本人在调用此接口并执行相关操作,致使账号被劫持。
Token
:浏览器请求服务器时,服务器返回一个token,每个请求都需要同时带上token和cookie才会被认为是合法请求Referer
:通过验证请求头的Referer来验证来源站点,但请求头很容易伪造SameSite
:设置cookie的SameSite,可以让cookie不随跨域请求发出,但浏览器兼容不一XSS即Cross Site Scripting(跨站脚本),指的是通过利用网页开发时留下的漏洞,注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。常见的例如在评论区植入JS代码,用户进入评论页时代码被执行,造成页面被植入广告、账号信息被窃取
<script><iframe>
等标签进行转义或者过滤冒泡排序应该是很多人第一个接触的排序,比较简单,不展开讲解了
function bubbleSort(arr){
for(let i = 0; i < arr.length; i++) {
for(let j = 0; j < arr.length - i - 1; j++) {
if(arr[j] > arr[j+1]) {
let temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
}
return arr
}
复制代码
冒泡排序总会执行(N-1)+(N-2)+(N-3)+..+2+1趟,但如果运行到当中某一趟时排序已经完成,或者输入的是一个有序数组,那么后边的比较就都是多余的,为了避免这种情况,我们增加一个flag,判断排序是否在中途就已经完成(也就是判断有无发生元素交换)
function bubbleSort(arr){
for(let i = 0; i < arr.length; i++) {
let flag = true
for(let j = 0; j < arr.length - i - 1; j++) {
if(arr[j] > arr[j+1]) {
flag = false
let temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
// 这个flag的含义是:如果`某次循环`中没有交换过元素,那么意味着排序已经完成
if(flag)break;
}
return arr
}
复制代码
快排基本步骤:
function quickSort(arr) {
if(arr.length <= 1) return arr //递归终止条件
const pivot = arr.length / 2 | 0 //基准点
const pivotValue = arr.splice(pivot, 1)[0]
const leftArr = []
const rightArr = []
arr.forEach(val => {
val > pivotValue ? rightArr.push(val) : leftArr.push(val)
})
return [ ...quickSort(leftArr), pivotValue, ...quickSort(rightArr)]
}
复制代码
原地排序
上边这个快排只是让读者找找感觉,我们不能这样写快排,如果每次都开两个数组,会消耗很多内存空间,数据量大时可能造成内存溢出,我们要避免开新的内存空间,即原地完成排序
我们可以用元素交换来取代开新数组,在每一次分区的时候直接在原数组上交换元素,将小于基准数的元素挪到数组开头,以[5,1,4,2,3]
为例:
我们定义一个pos指针, 标识等待置换的元素的位置, 然后逐一遍历数组元素, 遇到比基准数小的就和arr[pos]交换位置, 然后pos++
代码实现:
function quickSort(arr, left, right) { //这个left和right代表分区后“新数组”的区间下标,因为这里没有新开数组,所以需要left/right来确认新数组的位置
if (left < right) {
let pos = left - 1 //pos即“被置换的位置”,第一趟为-1
for(let i = left; i <= right; i++) { //循环遍历数组,置换元素
let pivot = arr[right] //选取数组最后一位作为基准数,
if(arr[i] <= pivot) { //若小于等于基准数,pos++,并置换元素, 这里使用小于等于而不是小于, 其实是为了避免因为重复数据而进入死循环
pos++
let temp = arr[pos]
arr[pos] = arr[i]
arr[i] = temp
}
}
//一趟排序完成后,pos位置即基准数的位置,以pos的位置分割数组
quickSort(arr, left, pos - 1)
quickSort(arr, pos + 1, right)
}
return arr //数组只包含1或0个元素时(即left>=right),递归终止
}
//使用
var arr = [5,1,4,2,3]
var start = 0;
var end = arr.length - 1;
quickSort(arr, start, end)
复制代码
这个交换的过程还是需要一些时间理解消化的,详细分析可以看这篇:js算法-快速排序(Quicksort)
三路快排
上边这个快排还谈不上优化,应当说是快排的纠正写法,其实有两个问题我们还能优化一下:
[1,2,2,2,2,3]
, 无论基准点取1、2还是3, 都会导致基准点两侧数组大小不平衡, 影响快排效率对于第一个问题, 我们可以通过在取基准点的时候随机化来解决,对于第二个问题,我们可以使用三路快排
的方式来优化,比方说对于上面的[1,2,2,2,2,3]
,我们基准点取2,在分区的时候,将数组元素分为小于2|等于2|大于2
三个区域,其中等于基准点的部分不再进入下一次排序, 这样就大大提高了快排效率
归并排序和快排的思路类似,都是递归分治,区别在于快排边分区边排序,而归并在分区完成后才会排序
function mergeSort(arr) {
if(arr.length <= 1) return arr //数组元素被划分到剩1个时,递归终止
const midIndex = arr.length/2 | 0
const leftArr = arr.slice(0, midIndex)
const rightArr = arr.slice(midIndex, arr.length)
return merge(mergeSort(leftArr), mergeSort(rightArr)) //先划分,后合并
}
//合并
function merge(leftArr, rightArr) {
const result = []
while(leftArr.length && rightArr.length) {
leftArr[0] <= rightArr[0] ? result.push(leftArr.shift()) : result.push(rightArr.shift())
}
while(leftArr.length) result.push(leftArr.shift())
while(rightArr.length) result.push(rightArr.shift())
return result
}
复制代码
堆是一棵特殊的树, 只要满足
这棵树是完全二叉树
和堆中每一个节点的值都大于或小于其左右孩子节点
这两个条件, 那么就是一个堆, 根据堆中每一个节点的值都大于或小于其左右孩子节点
, 又分为大根堆和小根堆
堆排序的流程:
以[1,5,4,2,3]
为例构筑大根堆:
代码实现:
// 堆排序
const heapSort = array => {
// 我们用数组来储存这个大根堆,数组就是堆本身
// 初始化大顶堆,从第一个非叶子结点开始
for (let i = Math.floor(array.length / 2 - 1); i >= 0; i--) {
heapify(array, i, array.length);
}
// 排序,每一次 for 循环找出一个当前最大值,数组长度减一
for (let i = Math.floor(array.length - 1); i > 0; i--) {
// 根节点与最后一个节点交换
swap(array, 0, i);
// 从根节点开始调整,并且最后一个结点已经为当前最大值,不需要再参与比较,所以第三个参数为 i,即比较到最后一个结点前一个即可
heapify(array, 0, i);
}
return array;
};
// 交换两个节点
const swap = (array, i, j) => {
let temp = array[i];
array[i] = array[j];
array[j] = temp;
};
// 将 i 结点以下的堆整理为大顶堆,注意这一步实现的基础实际上是:
// 假设结点 i 以下的子堆已经是一个大顶堆,heapify 函数实现的
// 功能是实际上是:找到 结点 i 在包括结点 i 的堆中的正确位置。
// 后面将写一个 for 循环,从第一个非叶子结点开始,对每一个非叶子结点
// 都执行 heapify 操作,所以就满足了结点 i 以下的子堆已经是一大顶堆
const heapify = (array, i, length) => {
let temp = array[i]; // 当前父节点
// j < length 的目的是对结点 i 以下的结点全部做顺序调整
for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
temp = array[i]; // 将 array[i] 取出,整个过程相当于找到 array[i] 应处于的位置
if (j + 1 < length && array[j] < array[j + 1]) {
j++; // 找到两个孩子中较大的一个,再与父节点比较
}
if (temp < array[j]) {
swap(array, i, j); // 如果父节点小于子节点:交换;否则跳出
i = j; // 交换后,temp 的下标变为 j
} else {
break;
}
}
}
复制代码
参考资料: JS实现堆排序
排序 | 时间复杂度(最好情况) | 时间复杂度(最坏情况) | 空间复杂度 | 稳定性 |
---|---|---|---|---|
快速排序 | O(nlogn) | O(n^2) | O(logn)~O(n) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(n) | 稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
其实从表格中我们可以看到,就时间复杂度而言,快排并没有很大优势,然而为什么快排会成为最常用的排序手段,这是因为时间复杂度只能说明随着数据量的增加,算法时间代价增长的趋势
,并不直接代表实际执行时间,实际运行时间还包括了很多常数参数的差别,此外在面对不同类型数据(比如有序数据、大量重复数据)时,表现也不同,综合来说,快排的时间效率是最高的
在实际运用中, 并不只使用一种排序手段, 例如V8的Array.sort()
就采取了当 n<=10 时, 采用插入排序, 当 n>10 时,采用三路快排的排序策略
设计模式有许多种,这里挑出几个常用的:
设计模式 | 描述 | 例子 |
---|---|---|
单例模式 | 一个类只能构造出唯一实例 | Redux/Vuex的store |
工厂模式 | 对创建对象逻辑的封装 | jQuery的$(selector) |
观察者模式 | 当一个对象被修改时,会自动通知它的依赖对象 | Redux的subscribe、Vue的双向绑定 |
装饰器模式 | 对类的包装,动态地拓展类的功能 | React高阶组件、ES7 装饰器 |
适配器模式 | 兼容新旧接口,对类的包装 | 封装旧API |
代理模式 | 控制对象的访问 | 事件代理、ES6的Proxy |
单一职责原则:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
开放封闭原则:核心的思想是软件实体(类、模块、函数等)是可扩展的、但不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。
单例模式即一个类只能构造出唯一实例,单例模式的意义在于共享、唯一,Redux/Vuex
中的store、JQ
的$或者业务场景中的购物车、登录框都是单例模式的应用
class SingletonLogin {
constructor(name,password){
this.name = name
this.password = password
}
static getInstance(name,password){
//判断对象是否已经被创建,若创建则返回旧对象
if(!this.instance)this.instance = new SingletonLogin(name,password)
return this.instance
}
}
let obj1 = SingletonLogin.getInstance('CXK','123')
let obj2 = SingletonLogin.getInstance('CXK','321')
console.log(obj1===obj2) // true
console.log(obj1) // {name:CXK,password:123}
console.log(obj2) // 输出的依然是{name:CXK,password:123}
复制代码
工厂模式即对创建对象逻辑的封装,或者可以简单理解为对new
的封装,这种封装就像创建对象的工厂,故名工厂模式。工厂模式常见于大型项目,比如JQ的$对象,我们创建选择器对象时之所以没有new selector就是因为$()已经是一个工厂方法,其他例子例如React.createElement()
、Vue.component()
都是工厂模式的实现。工厂模式有多种:简单工厂模式
、工厂方法模式
、抽象工厂模式
,这里只以简单工厂模式为例:
class User {
constructor(name, auth) {
this.name = name
this.auth = auth
}
}
class UserFactory {
static createUser(name, auth) {
//工厂内部封装了创建对象的逻辑:
//权限为admin时,auth=1, 权限为user时, auth为2
//使用者在外部创建对象时,不需要知道各个权限对应哪个字段, 不需要知道赋权的逻辑,只需要知道创建了一个管理员和用户
if(auth === 'admin') new User(name, 1)
if(auth === 'user') new User(name, 2)
}
}
const admin = UserFactory.createUser('cxk', 'admin');
const user = UserFactory.createUser('cxk', 'user');
复制代码
观察者模式算是前端最常用的设计模式了,观察者模式概念很简单:观察者监听被观察者的变化,被观察者发生改变时,通知所有的观察者。观察者模式被广泛用于监听事件的实现,有关观察者模式的详细应用,可以看我另一篇讲解Redux实现的文章
//观察者
class Observer {
constructor (fn) {
this.update = fn
}
}
//被观察者
class Subject {
constructor() {
this.observers = [] //观察者队列
}
addObserver(observer) {
this.observers.push(observer)//往观察者队列添加观察者
}
notify() { //通知所有观察者,实际上是把观察者的update()都执行了一遍
this.observers.forEach(observer => {
observer.update() //依次取出观察者,并执行观察者的update方法
})
}
}
var subject = new Subject() //被观察者
const update = () => {console.log('被观察者发出通知')} //收到广播时要执行的方法
var ob1 = new Observer(update) //观察者1
var ob2 = new Observer(update) //观察者2
subject.addObserver(ob1) //观察者1订阅subject的通知
subject.addObserver(ob2) //观察者2订阅subject的通知
subject.notify() //发出广播,执行所有观察者的update方法
复制代码
有些文章也把观察者模式称为发布订阅模式,其实二者是有所区别的,发布订阅相较于观察者模式多一个调度中心。
装饰器模式,可以理解为对类的一个包装,动态地拓展类的功能,ES7的装饰器
语法以及React中的高阶组件
(HoC)都是这一模式的实现。react-redux的connect()也运用了装饰器模式,这里以ES7的装饰器为例:
function info(target) {
target.prototype.name = '张三'
target.prototype.age = 10
}
@info
class Man {}
let man = new Man()
man.name // 张三
复制代码
适配器模式,将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作。我们在生活中就常常有使用适配器的场景,例如出境旅游插头插座不匹配,这时我们就需要使用转换插头,也就是适配器来帮我们解决问题。
class Adaptee {
test() {
return '旧接口'
}
}
class Target {
constructor() {
this.adaptee = new Adaptee()
}
test() {
let info = this.adaptee.test()
return `适配${info}`
}
}
let target = new Target()
console.log(target.test())
复制代码
代理模式,为一个对象找一个替代对象,以便对原对象进行访问。即在访问者与目标对象之间加一层代理,通过代理做授权和控制。最常见的例子是经纪人代理明星业务,假设你作为一个投资者,想联系明星打广告,那么你就需要先经过代理经纪人,经纪人对你的资质进行考察,并通知你明星排期,替明星本人过滤不必要的信息。事件代理、JQuery的$.proxy
、ES6的proxy
都是这一模式的实现,下面以ES6的proxy为例:
const idol = {
name: '蔡x抻',
phone: 10086,
price: 1000000 //报价
}
const agent = new Proxy(idol, {
get: function(target) {
//拦截明星电话的请求,只提供经纪人电话
return '经纪人电话:10010'
},
set: function(target, key, value) {
if(key === 'price' ) {
//经纪人过滤资质
if(value < target.price) throw new Error('报价过低')
target.price = value
}
}
})
agent.phone //经纪人电话:10010
agent.price = 100 //Uncaught Error: 报价过低
复制代码
aside / figure / section / header / footer / nav
等),增加多媒体标签video
和audio
,使得样式和结构更加分离input
的type属性;meta
增加charset以设置字符集;script
增加async以异步加载脚本localStorage
、sessionStorage
和indexedDB
,引入了application cache
对web和应用进行缓存拖放API
、地理定位
、SVG绘图
、canvas绘图
、Web Worker
、WebSocket
声明文档类型,告知浏览器用什么文档标准解析这个文档:
href(hyperReference)
即超文本引用:当浏览器遇到href时,会并行的地下载资源,不会阻塞页面解析,例如我们使用<link>
引入CSS,浏览器会并行地下载CSS而不阻塞页面解析. 因此我们在引入CSS时建议使用<link>
而不是@import
<link target="_blank" href="https://url.apipost.cn/url?style.css" rel="external nofollow" rel="stylesheet" />
复制代码
src(resource)
即资源,当浏览器遇到src时,会暂停页面解析,直到该资源下载或执行完毕,这也是script标签之所以放底部的原因
<script target="_blank" src="script.js" rel="external nofollow" ></script>
复制代码
meta标签用于描述网页的元信息
,如网站作者、描述、关键词,meta通过name=xxx
和content=xxx
的形式来定义信息,常用设置如下:
<meta charset="UTF-8" >
复制代码
<meta http-equiv="expires" content="Wed, 20 Jun 2019 22:33:00 GMT">
复制代码
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
>
复制代码
为什么要清除浮动:清除浮动是为了解决子元素浮动而导致父元素高度塌陷的问题
1.添加新元素
<div class="parent">
<div class="child"></div>
<!-- 添加一个空元素,利用css提供的clear:both清除浮动 -->
<div style="clear: both"></div>
</div>
复制代码
2.使用伪元素
/* 对父元素添加伪元素 */
.parent::after{
content: "";
display: block;
height: 0;
clear:both;
}
复制代码
3.触发父元素BFC
/* 触发父元素BFC */
.parent {
overflow: hidden;
/* float: left; */
/* position: absolute; */
/* display: inline-block */
/* 以上属性均可触发BFC */
}
复制代码
其实我本来还写了一节水平/垂直居中相关的,不过感觉内容过于基础还占长篇幅,所以删去了,作为一篇总结性的文章,这一小节也不应该从“flex是什么”开始讲,主轴、侧轴这些概念相信用过flex布局都知道,所以我们直接flex的几个属性讲起:
容器属性(使用在flex布局容器上的属性)
.container {
justify-content: center | flex-start | flex-end | space-between | space-around;
/* 主轴对齐方式:居中 | 左对齐(默认值) | 右对齐 | 两端对齐(子元素间边距相等) | 周围对齐(每个子元素两侧margin相等) */
}
复制代码
.container {
align-items: center | flex-start | flex-end | baseline | stretch;
/* 侧轴对齐方式:居中 | 上对齐 | 下对齐 | 项目的第一行文字的基线对齐 | 如果子元素未设置高度,将占满整个容器的高度(默认值) */
}
复制代码
.container {
flex-direction: row | row-reverse | column | column-reverse;
/* 主轴方向:水平由左至右排列(默认值) | 水平由右向左 | 垂直由上至下 | 垂直由下至上 */
}
复制代码
.container {
flex-wrap: nowrap | wrap | wrap-reverse;
/* 换行方式:不换行(默认值) | 换行 | 反向换行 */
}
复制代码
.container {
flex-flow: <flex-direction> || <flex-wrap>;
/* 默认值:row nowrap */
}
复制代码
.container {
align-content: center | flex-start | flex-end | space-between | space-around | stretch;
/* 默认值:与交叉轴的中点对齐 | 与交叉轴的起点对齐 | 与交叉轴的终点对齐 | 与交叉轴两端对齐 | 每根轴线两侧的间隔都相等 | (默认值):轴线占满整个交叉轴 */
}
复制代码
项目属性(使用在容器内子元素上的属性)
.item {
flex-grow: <number>; /* default 0 */
}
复制代码
flex-shrink
都为1,某个子元素flex-shrink
为0,那么该子元素将不缩小.item {
flex-shrink: <number>; /* default 1 */
}
复制代码
.item {
flex-basis: <length> | auto; /* default auto */
}
复制代码
flex-grow
, flex-shrink
和 flex-basis
的简写,默认值为0 1 auto,即有剩余空间不放大,剩余空间不够将缩小,子元素占据自身大小.item {
flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}
复制代码
flex有两个快捷值:auto
和none
,分别代表1 1 auto
(有剩余空间则平均分配,空间不够将等比缩小,子元素占据空间等于自身大小)和0 0 auto
(有剩余空间也不分配,空间不够也不缩小,子元素占据空间等于自身大小)
.item {
order: <integer>;
}
复制代码
.item {
align-self: auto | flex-start | flex-end | center | baseline | stretch;
}
复制代码
参考资料:阮一峰Flex布局
编辑中,请稍等-_-||
BFC全称 Block Formatting Context 即块级格式上下文
,简单的说,BFC是页面上的一个隔离的独立容器,不受外界干扰或干扰外界
float
不为 noneoverflow
的值不为 visibleposition
为 absolute 或 fixeddisplay
的值为 inline-block 或 table-cell 或 table-caption 或 grid对于前端基础知识的讲解,到这里就告一小段落。前端的世界纷繁复杂,远非笔者寥寥几笔所能勾画,笔者就像在沙滩上拾取贝壳的孩童,有时侥幸拾取收集一二,就为之欢欣鼓舞,迫不及待与伙伴们分享。
最后还想可耻地抒(自)发(夸)一下(•‾̑⌣‾̑•)✧˖°:
不知不觉,在掘金已经水了半年有余,这半年来我写下了近6万字,不过其实一共只有5篇文章,这是因为我并不想写水文,不想把基础的东西水上几千字几十篇来混赞升级。写下的文章,首先要能说服自己。要对自己写下的东西负责任,即使是一张图、一个标点。例如第一张图,我调整了不下十次,第一次我直接截取babel的转化结果,觉得不好看,换成了代码块,还是不好看,又换成了carbon的代码图,第一次下载,发现两张图宽度不一样,填充宽度重新下载,又发现自己的代码少了一个空格,重新下载,为了实现两张图并排效果,写了一个HTML来调整两张图的样式,为了保证每张图的内容和边距一致,我一边截图,一边记录下每次截图的尺寸和边距,每次截图都根据上一次的数据调整边距。
其实我并非提倡把时间花在这些细枝末节上,只是单纯觉得,文章没写好,就不能发出来,就像小野二郎先生说的那样:“菜做的不好,就不能拿给客人吃”,世间的大道理,往往都这样通俗简单。
作者:写代码像蔡徐抻
链接:https://juejin.cn/post/6844904116552990727
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。