你们好,我是练习时长一年半的前端练习生,喜欢唱、跳、rap、敲代码。本文是笔者一年多来对前端基础知识的总结和思考,这些题目对本身是总结,对你们也是一点微薄的资料,但愿能给你们带来一些帮助和启发。成文过程当中获得了许多大佬的帮助,在此感谢恺哥的小册、神三元同窗的前端每日一问以及许多素未谋面的朋友们,让我等萌新也有机会在前人的财富中拾人牙慧,班门弄斧Thanks♪(・ω・)ノ
css
本文将从如下十一个维度为读者总结前端基础知识html
这个问题实质上是在回答
let
和var
有什么区别,对于这个问题,咱们能够直接查看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
的区别上:react
var
声明的变量会挂到window上,而let
和const
不会var
声明的变量存在变量提高,而let
和const
不会let
和const
声明造成块做用域,只能在块做用域里访问,不能跨块访问,也不能跨函数访问let
和const
不能声明同名变量,而var
能够let
和const
声明的变量不能在声明前被使用babel的转化,其实只实现了第二、三、5点git
实现const的关键在于Object.defineProperty()
这个API,这个API用于在一个对象上增长或修改属性。经过配置属性描述符,能够精确地控制属性行为。Object.defineProperty()
接收三个参数:github
Object.defineProperty(obj, prop, desc)web
参数 | 说明 |
---|---|
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 = {} //没法赋值新对象 复制代码
参考资料:如何在 ES5 环境下实现一个const ?数据库
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.九、http1.0、http1.一、http二、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]
为例:
代码实现:
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]
, 不管基准点取一、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 href="style.css" rel="stylesheet" /> 复制代码
src(resource)
即资源,当浏览器遇到src时,会暂停页面解析,直到该资源下载或执行完毕,这也是script标签之因此放底部的缘由
<script src="script.js"></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" > 复制代码
为何要清除浮动:清除浮动是为了解决子元素浮动而致使父元素高度塌陷的问题
<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来调整两张图的样式,为了保证每张图的内容和边距一致,我一边截图,一边记录下每次截图的尺寸和边距,每次截图都根据上一次的数据调整边距。
其实我并不是提倡把时间花在这些细枝末节上,只是单纯以为,文章没写好,就不能发出来,就像小野二郎先生说的那样:“菜作的很差,就不能拿给客人吃”,世间的大道理,每每都这样通俗简单。
往期文章:
1. 异步编程二三事 | Promise/async/Generator实现原理解析 | 9k字
2. 10行代码看尽redux实现 —— redux & react-redux & redux中间件设计实现 | 8k字
3.红黑树上红黑果,红黑树下你和我 —— 红黑树入门 | 6k字
4. 深刻React服务端渲染原理 | 1W字