2万字 | 前端基础拾遗90问

大家好,我是练习时长一年半的前端练习生,喜欢唱、跳、rap、敲代码。转眼又到了金三银四的时节,相信很多小伙伴已经在为自己心仪的公司做准备,本文是笔者一年多来对前端基础知识的总结和思考,这些题目对自己是总结,对大家也是一点微薄的资料D V ? k N & 9,希望` ~ A ]能给大家带来一些帮助和启发。3 F 4 k $ c K 9成文过程中得到了许多大佬的帮助,在此感谢恺哥的小册、神三元同学的前端每日一问以及许多素未谋面的朋友们,让我等萌新也有机会在前人的财富中拾人牙慧,班门弄斧Thanks♪(・・)ノ

本文将从以下十一个维度为读者总结前端基础知识

JS基础

1. 如何在ES5环境下实现lee n & !t

对于这A Z N L个问题,我们可以直接查看babel转换前后的结果,看一下在循环中通过let定义的变量是如何解决变量提升的问题

babel在let定义的变量前加了道下划线,避免在块级作用域外访问到该A – U U & a变量,除了对变量名的转换,我们也可以通过自执3 ( U行函数来模拟块级作用域

(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

2. 如何在ES5环境下实现const

实现const的关键在于Objecm p @ Vt.defineProperty()5 O o ;个API,这个API用于C # L h F ) y Q在一个对象上L W ^ – A J w y增加或修改属性。* b P f Q : W E ~通过配置属性描述符,x s M s f U |可以精确地控6 o D Y S制属性行为。Object.definePropeE c x g n u 1 X urty() 接收三个参数:

Object.defineProperty(obj, prop, desc)

参数 说明
obj 要在其上定义属性的对象
prop 要定义或修改的属性的名称
descriptor 将被定义或修改的属性描述符

属性描述符 说明 默认值
value 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,N E Z J q x a函数等)。默认为 undefined undefined
get 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined undefined
set 一个给属性提供 sG ; $etter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法 undefined
writablg y ^ Ie 当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false false
enumerable enumerable定义了对象的属性是否0 x d f可以在 for…in 循环和 Obje~ n l uct.keys() 中被枚举 false
Configurable configI f .urable特性表示对象的属性是否可以被删除,以及除value和writable特性外的其他特性是否可以被修改 false

对于const不可修改的特性,我们通; A N } V U . $ q过设置wd K | j A J V ] pritable属性来实现

function _const( 4 x a . G v ? (key, value) {
const desc = {
value,
writan M C J / 8 h ;ble: false
}
Object.h = udefine* C L T J uProperty(window, key, desc)
}
_const('obj', {a: 1})   //定义obj
obj.b = 2               //可以正常给obj的属性赋值
obj = {}                //抛出错误,提示对象read-only

参考资料:如何在 ES5 环境下实现一个const ?

3. 手写call()

call() 方z ) ) % [ h G B W使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数
语法:function.call(thisArg, arg1, arg2, ...)

call()的原理比较简单,由于函数的6 @ ?this指向它的直接调用者,我们变x S p c更调用者即完成this指h * T 3向的变更:

//变更函数调用者示例
function foy x io() {
console.log(this.name)
}
// 测试
const obj = {
name: '写代码像蔡徐抻'
}
obj.foo = foo   // 变更foo的调用者
obj.foo()       // '写代码像蔡徐抻'

基于以上原理, 我们两句代码就能实现call()

Function.prototype.myCall = function(thisArg, ...args) {
thisArg.fn = tc Y ~his              // this指向调用call~ B s ,的对象,即我们要改变this指向的函数
return thisArg.fn(...args)     /P r + N ?/ 执行函数并return其执行结果
}

但是我们有一些细节需要处理:

Function.prototype.myCall = function(thisArg, ...args) {
const fn = Symbol('fn')        // 声明一个独有的Symbol属性, 防止fn8 , A 7 ` ; ] l l覆盖已有属性
thisArg = thisArg ||l A p m 6 u ^ _ ww 1 K c R j 3indow    // 若没有传入this, 默认绑定window对象
thisArg[fn] = this              // this指向调用call的对象,g _ @ Z ` x [ L即我们要改变this指向的函数
const resuq D & Xlt = thisArg[fn](...args)| u N 5 k 5 m  // 执行当前函数
delete thisArg[fn]              // 删除我们声明的fn属性
return result                  // 返回函数执行结果
}
//测试
foo.myCall(obj)     // 输出'写代码像蔡徐r @ r P抻'

4. 手写apply()

apply() 方法调用一个具有给定+ b L 2 3 Nthis值的函数,以及作为一个数组(或类似数z P F % ? R V组对象)提[ ) ` ^ ? ^ &供的参数。
语法:func.app_ G w K | Fly(thisArg, [argsArray])

apply()call()类似,区别在于call()接收参数列表,而apply()接收一个参数数组,所以我们在call()的实现上简单改一下入参形式即可

Function.prototype.myApply = function(thisArg, args) {
const fn = SymbN ? g B E , rol('fn')        // 声明一个: { ( x / N J ]独有的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.myApp& ,  J a %ly(obj, [])     // 输出'写代码像蔡徐抻'

5. 手写bind()

bind() 方法创建一个新的| h ` f N函数,在 bind() 被调用时,这个新函数@ p v C t f – B &的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使W 8 d e l用。
语法: function.bind~ 8 d g W E (thisArg, ar| } D –g1, arg2, …)

从用法上看,似乎给call/apply包一层function就实现了bind():

Function.prototype.? 4 ; 3 - w O 3myBind = function(thisArg, ...args) {
r? 8 # Heturn () => {
this.apply(thisArg, args)
}
}

但我们忽略F + z 8 ~ 8 f l h了三点:

  1. bind()除了this还接收其他参数,bind()返回的函F ) T U数也接收参数,这两部分的参数都要传给返V y D回的函数
  2. nP S eew的优先级:如果bind绑定后的函数被new了,那么此时this指向就发生改变。此时的this就是当前函数的实例
  3. 没有保留j V : ^ ] # l 0原函数在原型链上的属性和方法
Function.prototype.myBind = function (thisArg, ...args) {
var self = thisu 0 Q h F s 4 W
// new优先级
var fb, m ] a , - 8 g Tound = function () {
self.apply(thisu z w y c K 0 X instanceof self ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)))
}
// 继承原型上的属性和方法
fbound.prototype = Object.create(self.prototype);
return fbound;
}
//测试
const obj = { name: ; V 7 A { S 8 7 '写代码像蔡徐抻' }
f@ 9 2unction foo() {
console.log(this.na4 + S + q ~ L Dme)
console.log(arguments)
}
foo.j S ] p h I .myBP f U h 3 z $ ind(obj, 'a', 'b', 'c')()    //输出写代码像蔡徐抻 ['a', 'b', 'c']

6. 手写一个防抖函数

防抖和节流的概念都比较简单,所以我们就不在“防抖节流是什么”这个问题上浪费过多篇幅了,简单点一下:

防抖,即短时间内大量触发同一事件9 0 U m (,只会执行一0 N ;次函数p ) d a R |,实现原理为设置一个定时器,约定在xx毫秒后再触发事件处理,每次触发事件都会重新设置计时器,直到xx毫秒内无第二次操作,防抖常用于搜索框/滚动条的监听事] @ H d s L件处理b ! O D E 0,如果不做防抖,每输入一个字/滚动屏幕,都会触发事件处理,造成性能浪费。

function debounce(func, wait) {
leF ~ { Ct timeout = null
rl N w @ N B Xeturn function() {
let context = this
let args = arguments
if (timeout) cg 4 slearTimeout(timeout)
timeout =, ^  $ l m B 0 6 setTimeout(() => {
func.apply(context, args)
}, wait)
}
}

7. 手C 4 d写一个节流函数

防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间就执行一次,实现原理为设置一个定时器,约定xx毫秒后执行事件,如果时间到了,那么执行函数并重置定时器,和防抖的区别在于,防抖每次触发事件都重置定时器,而节6 0 3 e流在定时器到时间后再清空定时器

function throttle(func, wait) {
let timeout = nulC s l hl
return funcS 1 M O 0 z q etion() {
let contexP  = ( X y :t = this
let args = argumenZ ; . 1 R /ts
if (!timeoD v 0 n f _ # I Iut) {
timeout = setTimeout(() => {
timeout = nu; * 6 v  xll
func.apply(context, args)
}, wy [ Y T ~ } uait)
}
}
}

实现方式2:使用两个时间戳prev旧时间戳now新时间戳,每次触发事件都判断二者的时间差,如果到达规定时间,执行函数并重置旧时间戳

function throttle(func, wait) {
var prev = 0;
return function() {
let noo U y . z p c ?w = Date.now()h I ( `;
let con0 T V 2 $ +text = this;
let args = arguments;
if (now - prev > wait) {
func.apply(context, args);
prev = now;
}
}
}

8./ g & t 2 M 数组扁平化

对于[1, [1,2], [1,2,3]]这样多层嵌套的数组,我们如何将其扁平化为[1, 1, 2, 1, 2,U v T _ [ # R # 3]这样的一维数组呢:

1.ES6的flat()

const arr = [1, [1,2], [1,2,3]]
arr.flat(Infinity)  // [1, 1, 2, 1, 2, 3]

2.序列化后正则

conY i c Sst arr = [1, [1,2], [1,2,3]]C 6 ? w B X
const str = `[${JSON.stringify(arr).replp W ,ace(/([|])/g, '')}]`
JSON.parse(s: E @ # ftr)   // [1, 1, 2, 1, 2, 3]e $ $ E P L

3.递归
对于树状结L L ; e / o e构的数据,最直接的处理方式就是递归

const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
let result = []
for (const iw I A stem of arr) {
item instanceof Array ? result = result.concat(flat} ! } 6 _ I Q(item)) : result.push(item)
}
rel , ] % 7 y Q E $turn result
}
flat(arr) // [1, 1, 2, 1, 2, 3]

4.reduce()递归

const C $ + o arr = [1, [1,2], [1,2,3]]
function flat(arr) {
return arr. O 9 Q ?reduce((prev, cur) => {
reg % w cturn 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.soz X & h m /me判定数组中是否存在数组,因为存在[4,4,4],继续进入第二次循环进行合并
let arr = [1, [1,2], [1,2,3,[4,4,4]]]
while (arr.some(Array.isArray)) {
arr = [- | _ V t y].concat(...arr);
}
console.log(arr)  // [1, 1, 2, 1, 2, 3, 4, 4, 4]

9. 手写一个u ` 1 ] _Promise

实现一个符合规范的Promise篇幅比较长,建议阅读笔者上一篇文章:异步编程二三事 | Promise/asynW = lc/Generator实现原理解析 | 9k字

JS面向对象

在JS中一切皆对象,但JS并不是一种真正的面向对象(OOP)的语言,因为它缺少类(class)的概念。虽然ES6引入了classextends,使我们能够轻易地实现类和继承。但JS并不存在真实的类,JS的类是通过函数以及原型链机制8 L h ^ + h Q模拟的,本小节的就来探究如何在ES5环境下利用函数和原型链实现JS面向对象的特性

在开始之前,我们先回顾一下原型链的知识,后续new继承等实现都是基于原型链机制。很多介绍原型链的资料都能写上洋洋洒洒几千字,但我觉得读者f # k 4 @ 5 / e们不需要把原型链想太复杂,容易把` % w _ * : u #自己绕进去,其实在我看来,原型链的核心只需要记住三` K z ] . A # M点:

  1. 每个对象都有__proto__属性,该属性指向其原型对象,在调用实例的方法和属性时,如果在实例对象上找不到,就会往原型对象上找
  2. 构造函数的prototype属性也指向实例的原型对象
  3. 原型对象的constructor属性指向构造函数

1. 模拟实现new

首先我们要知道new做了什么

  1. 创建一个新对象,并继承其构造函数的prototype,这一步是为了继承构造函数原型上的属性和方法
  2. 执行构造函数,方法内的this被指定为该新实例,这一步是为了执行构造k c T ; Z P } Y函数内的赋值操作
  3. 返回新实例(规范规定,如果构造方法返回了一个对象,那么返回该对象,否则返回第一步创建的新对象)
// new是关键字,这里我们用函数来模拟,new Foo(args) <=> myNew(Foo, args)
functioi B 1n myNew(foo, ...args)n  b 9 ) ( 1 5 {
// 创建新对象,并继承构造方法的proY m T H # ; =tg | 1 s G ~otype属性, 这一步是为了0 h e - L } g b [把obj挂原型链上, 相当于} g ] Q Oobj.__prU ) K t :oto__G - $ =3 F ) r = | h Foo.prototype
let obj = Object.create(foo.prototype)
// 执行构造方法, 并为n ! _其绑定新this, 这一步是为了让构造方法能进行this.name = name之类的6 = ] F ) X + 6 L操作, args是构造方法的入参, 因为这里用myNew模拟, 所以入参从myNew传入
let result = foo.apply(obj, args)
// 如果构造方法已经return了一个对象, 那么就返回该对象, 一般情况下,构造方法不会返回新实例,但使用者可以选择返回新实例来覆盖neT g u | C e Qw创建的对象 否L P t k则返回myNew创建的新对象
return typV o =eof result === 'object' && result !== null ? result : ob] | 8j
}
functioL N _n FooN Q 4 O * u n K(name) {
this.name = name
}
const newObj = myNew(Foo, 'zhaE z 9 L m _ . _ngsan')
console.log(newObj)                 // Foo {name: "zhX ; i @ u 2angsan"}
console.log(newObj instanceof Foo)  // true

2. ES5如何实现继承

说到继承,最容易想到的是ES6的extends,当然如果只回答这个肯定不合格,我们要从函数和原型链的角度上实现继 3 9 承,下面我们一步步e P = z [地、递进地O N w 2实现一个合格的继承

一. 原型链继承

原型链继承的原理很简单,直接让子类的原型对象指向父类实例,当子类实例找不到对应的属性和方法时,就会往它的原型对象,也就是父类实例上找,从而实现对父类的属性和方法的继承

// 父类
function Parent() {
this.name = '写代码像蔡徐抻'
}
// 父类的原型方法
Parent.prototype.getName = function() {
return this., G | w S 4 U &name
}
// 子类
function Child() {}
// 让子类的原型对象指向父类实例, 这样一来在Child实例中找不到的属性和方法就会到原型对象(父类实例)上寻找
Child.protR } F r ~ 7 l Jotype = new Parent()
Child.pS $ y m ) Q s Zrototype.constructor = Child // 根据原型链的规则,顺便绑定一下constructor, 这一步不影响继承, 只是在用到constructor时会需要
//{ w 6 i 4 { { 然后Child实例就能访问到父类及其原型上的name属性和getName()方法
const child = new Child()
child.name          // '写代码像蔡徐抻'
child.getNd / B h W ; , ] game()     // '写代码像蔡徐抻'

原型继承的缺点:

  1. 由于所有Child实例原型都指向同一个Parent实例, 因此对某个Child实例的父类引用类型变量修改会影响所有的Child实例
  2. 在创建子类实例时无法向父类构造传参, 即没有实现super()的功能
// 示例:
function Parent() {
this.name = ['写代码像蔡徐抻']
}
Parent.prototym q ~ j q , 4 lpe.g0 1 ketName = functi] c ?on() {
return this.name
}
function Child() {}
Child.prototype =S  & N + o X new Parent()
Child.proto a { Ttype.constructor = Child
// 测试
const child1 = new Child()
const child2 = nl A B E 4 * :ew Child()
child1.name[0] = 'foo'
cu y + h ?onsole.log(child1., . 8 ] 9name)          // ['foo']
console.log(child2.name)          // ['foo'] (预期是['写代码像蔡徐抻'], 对child1.name的修改引起了所有child实例的变化)

二. 构造函数继承

构造函数继承,即在子类的构造函数中执行父类的构造函数,并为其绑定子类的this,让父类的构造函数把成员属性和方法都挂到子类的this上去,这样既能避免实例之间共享一个原型实例,又能向父类构造方法传参

function Parent(name) {
this.name = [name]
}
Pc h 2 { W uarent.prototype.getName = function()q 6 | {
return this.name
}
function Child() {
Parent.call(this, 'zhangsan')   // 执G X = i行父类构造方法并绑定子类的this, 使J & i [ ! B R C &得父类中的属性能够赋到子类的this上
}
//测试o a X
const child1 = new Child()
const child2 = new Child()
child1.name[? w ` J0] = 'foo'
console.log(child1.name)          // ['foo']
console.log(child2L f q Y V.name)a  @          // ['zhN 1 | z ,angsan']
child2.getNK ] Pame()                  // 报错,找不到getName(), 构造函数继承的方式继承不到父类原型上的属性和方D p  + R + =

构造函数继承的缺点:

  1. 继承不到父类原} s D M $ | 1 ` 0型上的属性和方法

三. 组合式继承

既然原型链继承和构造函数继承各有互补的优缺点, 那么我们为什么不组合) D w 4 b Y g ? %起来使用呢, 所以就有了综合二者的组合式继承

function Parent(nx _ w 0 p Lame) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 构造函数继承
Parent.call(this, 'n B [ zhangsan')
}
//原型链继承
Child.prototy? ; I J Bpe = new Pare9 ~ nI & Y 8 @ @ 2 vt()
Child.prS $ 5 N } = ?ototype.constructor = Child
//测试
const child1 = new Child()
const child2 =M B A F 3 h ^ c new Child()
child1.name[0] = 'foo'
consol( @ &e.log(child1.name)          // ['foo']
console.log(child2.name)          // ['zhangs( R f  A ? oan']
child2.getName()                  // ['zhad J Kngsan']

组合式继承的缺点:

  1. 每次创建子类实例都执T X 4 m Y C行了两次构造函数(Parent.call()new Parent()),虽然这并不影响对父类的继承,但子类创建实例时,原型中会存在两份相同的属性和方法,这并不优雅

四. 寄生式组合继承

为了解决构造函数被执行? W 0两次的问题, 我们将指向父类实例改为指向父类原型, 减去一次构造函数的执行

fun& s zction Parent(name) {
this.a H % 5 d (name = [name]
}
PaY  L b e ;rent.prototype.getName = function() {
return this.name
}
function Child()0 * 3 j P v {
// 构造函数继承
PU S barent.call(this, 'zhangsan')
}
//原型n m G Z _ ? T链继承
// Child.prototype = new Parent()
Child.prototype = Parent.prototype  //将`指向父类实例`改为`指向父类原型`
Child.prototyq  pe.constructor = Child
//测试; w $
const child1 = new Child(){ ` [ K 
const child2 = new Child()
child1~ q + h , ? A.name[0] = 'foo'
console.log(child1.name)          // ['foo']
console.log(child21 P v N 8 G l _.name)          // ['zhangsan']
ch. P `ild2.gZ b 8 $ ; 9etName()                  // ['zhangsan']

但这种方式存在一个问题,由于子类原型和父O 2 s @ 4 e u @ ,类原型指向同一个对象,我们对子类原型的操作会影响到父类原q F 1 i D 4 M型,例如给Child.prototype增加一个getName()方法,那么会导致Parent.prototypI # ~ I .e也增加或被覆盖一个getName()方法,为了解决这个问题,我们给Parent.protota ` ^ X i ,ype做一个浅拷贝

func? / w h I { L &tion Parent(name) {
this.name = [name* 8 $ T e D u *]
}
Parent.prototype.getName = function() {
return thisz E S { { V.name
}
function Child() {
/8 | { Z j/ 构造函数继承
Parent.call(thii ? os, 'zhangsan')
}
//原型链继承
// Child.prototype = new Parq D p L g b g c *ent(m z s 8)
Child.prototype = Object.create(Parent.prototype)  //将`指向父类实例`改为`指向父M ^ )类原型`
Child.prototype.constructor = Child
//测试
const child = new Child()
cO  6 i K z ~ ! |onst parent = new Parent(5 w K)
child.getName()                  // ['zhangsan']
parent.getName()                 // 报错, 找不到gH ] 7 x f K ietName()p H / l

到这里我们就完成了ES5环境下的继承的实现,这种继承方式称为寄生组合式继承,是目前最成熟的继承方式,babel对ES6继承的转化也是使用了寄生组合式继承

我们回顾一下实现过程:
一开始最容易想到的是原型链继承,通过把子类实例的原型指向父. u T 类实例来继承父类的属性和方法,但原型链继承的缺陷在于对子类实例继承的引用类型的修改会影响到所有的实例对象以及无法向父类的u / M 8 2构造方法传参
因此我们引入了5 m *造函数继承, 通过在子类构造函数中调用父类构造函数并传入子类this来获取父类的属性和方法,但构造函数继承也存在缺陷,构造函数继承不能继承到父类原型链上的属性和方法
所以我们综合了两种继承的优点,提出了组合式继承,但组合式继承也引入了新的问题,它每次创建子类实例都执行了两次父类构造方法,我们通过将子类原型指向父类实例改为子类原型指向父类原型的浅拷贝来解决这一问题,也就是最终实现 ——H _ 2 9 寄生组合式继承

V8引擎机制

1. V8如何执行一段JS代码

  1. 预解析:检查语法错误但不生成AST
  2. 生成AST:经过词法G G /语法分析,生成抽象语法树
  3. 生成字节码:基线编译器(Ignition)将– R m .AST转换成字节码
  4. 生成机器码:优化编译器(Turbofan1 3 a)将字节码A q N # g转换成优化过的机器码,此外在逐行执行字节码的过程中,如果一段代码经常被执行,那么V8会将这段代码直接转换成机器码保存起来,下一次执行就不必经过字节码,优化了执行速j s . c y

上面几点只是V8执行机制的极简总结,建议阅读参考资料:

1.V8 是怎么跑起来的 —— V8 的 JavaScript 执行管道
2.JavaScript 引擎 V8 执行流程概述

2. 介绍一下引R e ~ s C A用计数和标记清除

  • 引用计数:给一个变量赋值引用类型,则该5 O x . Y对象的引用次数+1,如果7 } ] F r z这个变量变成了其他值,那么该对象的引用次数-1,垃圾回收器会回收引用次数为0的对象。但是当对象循环引用时,会导致引用次数永远无法归零,造成内存无法释放。
  • 标记清除:垃圾收集器先给内存中所有对Q C r [ $ i 4 U K象加上标记,然后从根节点开始遍历,去掉被引用的对象和运行环境中对象的7 U a标记,剩下的被标记的对象就是无法访问的等待回收的对象。

3. V8如何进行垃圾回收

JS引擎中对变量的存储主要有两种位置,栈内存和堆内存,栈内存存储基本类型数据以及引用类型数据b x 4的内存地址,堆内存储存引用类型的数据

栈内存的回收:

栈内存调用栈上下文切换后就被回收,比较简单I m % K u _

堆内存的回收:

V8的堆内存分为新生代内存和老生代内存,新生代内存是临时分配的内存,存在时间短,老生代内存存在时间长

  • 新生代内存回收机制:
    • 新生代D , $内存容量= w { A : # N # !小,64位系统下( 0 a # Y B !仅有32M。新生代内存分为From、To两部分,进行垃圾回收时,先扫描From,将非存活对象回收,将存活对象顺序复制到To中,之后调换From/To,等待下一次回收
  • 老生代内存回收机制
    • 晋升:如果新生代的变量经过多次回收依然存在,那么就会被放入老生代内存中
    • 标记清除:老生代内存会先遍历所有对象并打上标记,然后对正在使用或被强引用的对象取消标记,回收被标记的对象
    • 整理内存碎片:把对象挪到内存的一端

参考资料:聊聊V . d L A d8引擎的垃圾回收

4. JS相较于C++等语言为什么慢,V8做了哪些优化

  1. JS的问题:
    • 动态类型:导致每次存取属性/寻求方法时候,都需要先检查类型;此外动态类型也很难在编译阶段进行优化
    • 属性存取:C++/Java等语言中方法、属性@ Q @ r y } 是存储在数组中的,仅t 3 H m N R i J需数组位移就可以获取,而JS存储在对象中,每次获取都要进行哈希查询
  2. V8的优化:
    • 优化JIT(即时编译):相较于C++/Java这类编译型语言,JS一边解释一边执行,效率低。V8对这个过程进行了优化:如果一段代码被执行多次,那么V8会把G O Y a C = i这段代码转化为机器码缓存下来,下次运行时直接使用机器码。
    • 隐藏类:对于C+5 } ; k ) ) b+这类语言来说,仅需几个指令就能~ a , S z ]通过偏移量获取变量信息,而JS需要进行字符串匹配,效率低,V8借用了类和偏移位置的思想,将对象划分成不同的组,即y / * v y O *隐藏类
    • 内嵌缓存:即缓存对象查询的结果。常规查询过程是:获取隐藏类地址 -> 根据属性名查找偏移值 -> 计算该属性地址,内嵌缓存就是对这一过程结果的缓存
    • 垃圾回9 $ S v O T i收管理:上文已介绍

参考资料:为什么V8引擎这么快?

浏览器渲染机0 o w # T l

1. 浏览器的渲染过程是怎样的

大体流程如下:

  1. HTML和CSS经过各自解析,生成DOM树和CSSOM树
  2. 合并成为渲染树
  3. 根据渲染树进行布局
  4. 最后调用GPC n G T 7 3 eU进行绘制,显示在屏幕上

2. 如何根据浏览器渲染机制加快首屏速度

  1. 优化文件大小:HTML和CSt G , C 3 k D l }S的加载和解析都会阻塞渲染树的生成,从而影响首屏展示速度,因此我们可以通过优化文件大小、减少CSS文件层级的方法来加快首屏速度
  2. 避免资源下载阻塞文档解析:浏览器解析到J i y h A<script>标签时,会阻塞文档解析,直到脚本执行完成,因此我们通常把<script>标签放在底部,或者加上defer、async来进行异步下载

3. 什么是回流() k 7 F重排),什么情况下会触发回流

  • 当元素的尺寸或者位置发生了变化,就需要重新计算渲染树,这就是回u _ ! Q ` c j = .
  • DOM元素的几何属性E { 1 4 q(width/height/padding/margin/border)发生变化时会触发回流
  • DOM元素移动或增加会触发回流
  • 读写offset/scroll/client等属性时J T x ; [ / L 3会触发回流
  • 调用wind& ! &ow.getComputedStyle会触发回流

4. 什么是重绘,什么情况下会触发重绘

  • DOM样式发生了变化,但没有影响DOM的几何属性时g . f ] :,会触发重绘,而不会触发回流。重绘由于DOM位置信息不需要更新,省去了布局过程,因而性能上优t M t于回流

5. 什么是GPU加速,t V ! 8 [如何使用GPU加速,GPU加速的缺点

  • 优点:使用transform、w l g : ! K % T ?opacity、filters等属性时,会直接在GPU中完成处理,这些属性的变化不会引起回流重绘
  • 缺点:GPU渲染字体会导致字体模糊,过多的GPU处理会导致内存问题

6. 如何减少回流

  • 使用class替代style,减少style的使用
  • 使用resize、scroll时进行防抖和节流处理,这两者会直接导致回流
  • 使用visibility替换display: none,因为前者只会引起重绘,后者会引发回流
  • 批量修改元素时,可以先让元素脱离文档流,等修改完毕后,再放入文档流
  • 避免触发同步布局事件,我们在获取offsetWidth这类属性的值时,可以使用变量将查询结果存起来,避免多次查询,每次对offset/scr| ^ 5 * / # Z 0oll/client等属性进行查询时都会触发回流
  • 对于复杂动画效果,使用绝对定位让其脱离文档流,复z F | ; C ? S杂的动画效果会频繁地触发回流重绘,我们可以将动画元素设置绝对定位从而脱离文档流避免反复回流重绘。

参考资料:必须明白的浏览器渲染机制

浏览器缓存策略

1. 介绍一下浏览器缓存位置和优先级

  1. Service Worker
  2. Memory Cache(内存缓存)
  3. Disk Cache(硬盘缓存)
  4. Push Cache(推送缓存)
  5. 以上缓存都没命中就会进行网络请求

2. 说说不同缓存间的差别

  1. Servi { } r C . @ 5 !ice Worker

和WeC ( g [ ; Ub Worker类似,是独立的线程,我们可以在这个线程中缓存文f Q a e g [ M a件,在主线程需要的时候读取这里的文件,Service Worker使我们可以自由选择缓存哪些文件以及文件的匹配、读取规则,并且缓存是持续性的

  1. Memory Cache

即内存缓存,内存缓存不是持续性的,缓存会随着进程释放而释放

  1. Disk Cache

即硬盘缓存,相较于内存缓存,硬盘缓存的持续性和容量更优,它会根据HTTP header的字段判断哪些资源需要缓存

  1. Push Cache

即推送缓存,是G Z { rHTT4 Y u 4 h X 1 _ uP/2的内容,目前应用较少

3. 介绍一下浏览器缓存策略

强缓存(不要向服务器询问的缓存)

设置Expires

  • 即过期时间,例如「Expires: Thu, 26 Dec 2019 10:30:42 GMT」表示缓存会在这个时间后失效,这个过期日期是绝对日期,如果修改了本地日期,或者本地日期与服务器日期不一致,那么将导致缓存过期l * X ^ C @时间错误。

设置Cache-Control

  • HTTP/1.1新增字段,Cache-Control可以通过max-age字段来设置过期时间,例如「Cache-Control:max-age=3600」除此之外Cache-Control还能设置private/no-cache等多种字段

协商缓存(需要向服务器询问缓存是否已经过期)

Last-Modified

  • 即最后修改时间,浏览S | k t v H I器第一次请求资源时,服务器会在响应头上加上Last-Modified ,当浏览器再次请求该资源时,浏览器会在请求头中带上If-Mos E s n U P t Rdified-Since 字段,字段的值就是之前A q 6 @ = , h ^服务器返回的最后修改时间,服务器对比这两个时间,若相同则返回304,否则返回新资源+ 2 r d,并更新Last-Modified

ETag

  • HTTs [ h 7 M h m B |P/1.1新增字段,表示文件唯一标识,只要文件内容改动,ETag就会重新计算。缓存流程和 Last-Modim | u { ^fied 一样:服务器发送 ETag 字段 -> 浏览器再次请求时发送 If-None-Match -> 如果ETag值不匹配,说明文件已经改变,返回新资源u G Q n a J并更新ETag,若匹配则返回304

两者对比

  • ETag 比 Last-Modified 更准确:如果我们打开g | b J s 4 #文件但并没有修改,Last-Modi* | v i e l !fied 也会改变,并且 Last-Modified 的单位时间为一秒,如果一秒内修改完了文件,那么还是会命中缓存
  • 如果什么缓存策略都没有设置,那么浏览器会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间

参考资料:浏览器缓存机制剖析

网络相关

1. 讲讲网络OSI七层模型,TCP/IP和HTTP分别位于哪一层

alt
alt
模型 概述 单位
物理层 网络连接介质,如网线、光缆,数. # H s ` J V #据在其中以比特为单位传输 bit
数据链路层 数据链路层将比特封装成数据帧并传递
网络层 定义IP地址,定义路由功能,建立主机到主1 1 s机的通信 数据包
传输层 负责将数据进行可靠或者不可靠3 . a传递,建立端口到端口的通信 数据段
会话层 控制应用程序之间会话能力,区分不同的进程
表示层 数据格式标识,基本压缩加密功能
应用层 各种应用软件

2. 常见HTTP0 m o w K ^ 0 d状态码有哪些

2xx 开头(请求成功)

200 OK:客户f v R E } r c (端发送给服务器的请求被正常处理并返回

3xx 开头(重定向)

301 Moved Permanently:永久重定向,请求的网页已永久移* * V Z | ? G J 5动到新位置。 服务器返回此响应时,会自动将请求者转到新位置

302 Moved Permanently:临时重定向,请求的网页已3 ( t临时移动到新v r 3 q ^ /位置。服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进5 q h行以后的请求

304 Not Modified:未修改,自从上次请求后,2 v S f Q / 4 y请求的网页未修改过。服务器返回此[ w b @ l ( D响应时,不会返回网页内容

4xx 开头(客户端错误)

400 Bad Request:错误请求,服务器不理解请求的语法,常见于客户端传参错误

401 Unauthorized:未授权,表示发送的请求需要有通过 HTTP 认证的认证信息,常见于客户端未登录

403 Forbidden:禁止,服务器拒绝请求,常见于客户端权限不足

404 Not Found:未找到,服务器找不到对应资源

5xx 开头(服务端错误)

500 Inter Server Error:服务器内部K K | – N s z q错误,服务器遇到错误,无法完成请求

501 Not Implemented:尚未实施,服务器不具备完成请求的功能

502 Bad Gateway:作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。

503 service unavailable:服务不可用,服务器目前无法使用/ m # : : c(处于超载或停机维护状态)。通常是暂时状态。

3. GET请求和POST请求有何区别

标准答案:

  • GET请求参数放在URL上,POST请求参数放在请求体里
  • GET请求参数长度有限制,POST请求参数长度可以非常大
  • PO7 3 X J c f hST请求相较于GET请求安全一点点,因为GET请求的参数在URL上,且有历史记录
  • GET请求能缓存,Px – e 4 aOST不能

更进一步:

其实HTTP协议并没有要求GET/POST请求参数必须放在URL上或请求体里,也没有规定GETj E R ~ Z w g +请求H K + c = &的长度,目前对URL的长度限制,是各家浏览器设置的限制。GET和POST的根本区别在于:GET请求是幂等性的,而POST请求不, U A Y 4 S % *

幂等性,指的是对某3 e V z一资源进行一次或多次请求都具有相同的副作用。例如搜索就是一个幂等的n W [ u W : }操作,而删除、新增则不是一个幂等操作。

由于GET请求是幂等的,在网络不好的环境中,GET请求可能会重复尝试4 o ! e ] C d J,造成重复操作数据的风险,因此,GET请求用于无副作用的操作(如搜索),新增/– E j , 0删除等操作适% 4 &合用POST

参考资料:HTTP|GET 和 POST 区别?网上多数答案都是错的

4. HTTP的请求报文由哪几部分组成

一个HTTP请求报文由请求行(request line)、请求头(header)、空行和请求数据4个部分组成

响应报文和请求报文结构类似,不再赘述

5. HTTP常见请求/响应头及其含义

通用头(请求a B V Q b S 0 _ #头和响应头都有的首部)

字段 作用
Cache-Control 控制缓存 public:表示响应可以被任何对象缓存(包a * ^括客户端/代理服务器)
privaL / * Ete^ M } o ; {(默认值):响应只能被单个客户缓存,不能被代理服务器缓存$ Q p = z F V o
no-cache:缓存要经过服务器验证,在浏览器使用缓存前,会对比ETag,, I , – { y若没变则返回304,使用缓存9 N 7 j .
no-store:禁止任何缓存
Connection 是否需要持久连接(H| ; + 9 [ q 6 q KTTP 1.1默认持久连接) keep-alive / close
Transfer-Encodi@ ; R [ l { i O eng 报文主体的传输编码格式 chunked(分块) / identity(未压缩和修改) / gzip(LZ77压缩) / compress(LZW压缩,弃用) / deflate(zlib结构压缩)

请求头

字段 作用 语法
Accept 告知(服务器)客户端可以处理的内容类型 text/html、image/*、*/*
If-MoL U w .dified-Since Last-Modified的值发送给服务器,询问资源是否已经过期(3 ^ k Z p . i k被修改),过期则返回新资源,否则返回304/ e T k 7 $ / , Q 示例:If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
If-Unmp J 2 ~ Y Q c & Qodified-Since Last-Modified的值发送给服务器,询问文件是否被修改,若没有则返回200,否2 ( N则返回412预处理错误,可用于断点续传。通俗点说If-Unmodified-Sinct P X Ee是文件没t H t e h v Y B有修改时下载,If-Modified-Since是文件修改时下载 示例:If-Unmodified-c } X L 6 C z –Since: Wed, 21 Oct 2015 07:28:00 G7 g qMT
If-None-Match ETag的值发送给服务器,询问资源是否已经过期(被修改),过期b u t $ ` g }则返回新资源,否则返回304 示例:If-None-Match: “bfc13a6472992d82d”
If-Match ETag的值发送给服务器,询问文件是否被修改,若没有则返回200,否则返回412预处理错误,可用于断点续传 示例:If-Ma@ r v [tch: “bfc129c88ca92d82d”
Range 告知服务器返回文件的哪一部分, 用于断点G { 9 `续传 示例:Range: bytes=200-1000, 2000-6576, 19000-
Host 指明了服务器的域名(对于虚拟主机来说),以及(可选的)服务器监听的TCP端口号 示例:Host:www.baidu.com
UsF – X 1 0er-Agent 告诉HTTP服务器, 客户端使用的操作系统和浏览器的名称和版本 User-Agent: Mozilla/<version> (<system-informatio; O x `n&Y y r d # 6gt;) <platform> (<platform-details>) <extensions>

响应头

字段 作用 语法
Location 需要将页面重新定向至的地址。一般在响应码为3xx的响应中才会有意义 Location: <url>
ETag 资源的特定版本的标识符1 – ( C & z,如果内容没有改变,Web服务器不需要发送完整的响应 ETag: “<etag_value>”
Server 处理{ g z O 4请求的源头服务器所用到的软件相关信息 Server: <product>

实体头(针对请求报文和响应报文的实体部分使用首部)

字段 作用 语法
Allow 资源可支持http请求的方法 AlT o K I c 2 / & ;low: <http-methods>,示例:Allow: GET, POz j X I / DST, HEAD
Last-Modified 资源最后的修改时间,用作一个验证器来判断接收到 v , O ?的或者存储的资源是否彼此一致,精度不如ETag 示例:Last-Modified: Wed, 21 Oct 2020 07:28:00 GMT
Expires 响应过期时间 Expires: <http-date>,示例:Expires: WQ W ( K @ed, 21 Oct 2020 07:28:00 GMT

HTTP首部当然不止这么几个,但为了避免写太多大家记不住(主要是别的我也没去看),这里只介绍了一些常用的,详细的可以看MDN的文档

6. HTTP/1.0和HTTP/1.1有什么区别

  • 长连接: HTTP/1.1支持长连接和请求的流水线,在一个TCP连接上可以传送多个HTTP请求,避免了因为多次建立TCP连接的时间消耗和延时
  • 缓存处理: HTTP/1.1引入Entity tag,If- D 4 f % o w 5-Unmodified-Since, If-Match, If-None-Match等新的请求头来控制缓存,详见浏览器缓存小节
  • 带宽优化及网络连接的使用: HTTP1.1则在请求头引入了range头域,支持断点续传功能
  • Host头处理: 在HTTP/1.0中认为每台服务器都. % ! } i 有唯一的IP地址,但随着虚拟主机技术的发展,多个主机共享一个IP地址愈发普遍,HTTP1.1的请求消息和响应消息都应J T k t % )支持Host头域,且G l L : V P 6 2R ) Z求消息中如果没有Host头域会400错误

7.T R g v x C % / 介绍一下HTTP/2.0新特性

  • 多路复用: 即多个R n j L请求W L 5 , o F S都通过一个TCP连接并发地完成
  • 服务端推送: 服务端能够主动把资源推送给客户端
  • 新的二进制格式: HTTP/2采用二进制格式传输数据,相比于HTTP/1.1的文本格式,二进制格式具有更好的解析性和拓展性
  • header压缩: HTTP/2压缩消息头,减少了传输数据的大小

8. 说说HTTP/2.0多路复用基本原理以及解决的问题

HTTP/2解决的问题,就是HTTP/1.1存在的问题:

  • TCP慢启动: TCP连接建立后,会经历一个先慢后快的发y P j } ]送过程,就像汽车启动一般,如果我们的网页文件(HTM/ _ i y s ? 8 v JL/JS/CSS/icon)都经过一次慢启动,对性能是不小的损耗。另外慢启动是TCP为了减少网络拥塞的一种策略,我们是没有办法改变的。
  • 多条TCP连接竞争带宽: 如果同时建立多条TCP连接,当带宽不足时就会竞争带宽,影响关键资源的下载。
  • HTTP/1.1队头阻塞: 尽管HTTP/1.1长链接可以通过一个TCP连接传输多个请求,但同一时刻只能处理一个请求,当前请求未结束前,其他请求只能处于阻塞状态。

为了解决以上几个问题U E 7HTTP/2一个域名只使用一个TCP⻓连接来传输数据,而且请求直接是并行的、非阻塞的,这就是多路h ? m T $ v # Y w复用

实现原理: HTTPl P l ] M 9 @ C m/2引入了一个二进制分帧层,客户端和服务端B 5 s ^ C [ D进行Z T 传输时,数据会先经过二进制分帧层处理,转化为一个个带有[ Z f 5 *请求ID的帧,这些帧在传输完成后根据ID组合成对应的数据。

9. 说说HTTP/3.0

尽管HTTP/2解决了很多1.1的问题,但HTTP/2仍然存在一些缺陷,这些缺陷并不是来自M c + ! {于HTTP/2协议本身,而是来源于底层的TCP协议,我们知道TCP链接是可靠的连接,如果出现了丢包,那么整个连接都要等待重传,HTTl _ F a + R =P/1.1可以同时使用6个TCP连接,一个阻W /塞另外五N O G [个还能工作,但HTTP/2只有一个TCP连接,阻塞的问题便被放大了。

由于TCP协议已经被广泛使用,我们很难直接修改TCP协议,基于此,HTTP/3选择了一个折衷j ? d ` F & v v的方法——UDP协议,HTTP/2在UDP的基础上实现多路v O F复用、0-RTT、TLS加密t e b +、流量控制、丢包重传等功能。

参考资料:http发展史(http0.9、; Z | % g L :http1.0、http1.1、http2R a . ( ^ Z n ) 6、htt@ 1 y D , + x * (p3)梳理笔记 (推荐阅读)

10. HTTP和HTTPS有何区别

  • HTTPS使用447 9 Y n3端口,而HTTq = J b tP使用80
  • HTTPS需要申请证书
  • HTTP是超文本传输协议Q r L p @,是明文传输;HTTPS是经过SSL加密的协议,传输更安全
  • HTTPS比HTTP慢,因C : h s为HTTPS除了TCP握手的三个包,还要加上SSL握手的九个包

11. HTTPS是如何进行加密的

我们通过分析几种加密方式,层层递j ` Q S进,理解HTTPS的加密方式以及为什么使用这种加密方式:

对称加密

客户端和服务器公用一个密匙用来对消息加解密,这种方式称为对称加密。客户端和服务器约定好一个加密的密匙。客户端在发消息前用该密匙对消息加密,发送 p ! F 7 /给服务器后,服务器再用该密匙进行解密拿到消息。

这种方式一定程度上保证了数据的安全性,但密钥一旦泄露(密钥在传输过程中被截获),传输内容就会暴露,因此我们要寻找一种安全传递密钥的方法。

非对称加密

采用非对称加密时,客户端和服务端均拥有一个1 T ( . r C * K J公钥和私钥,公钥] H b w q r V加密的内容只有对应的私钥能解密。私钥自己留N a @ K着,公钥发给对方。这样在发送消息前,先用对方的公钥对消息进行加密,收到后再用自己的私钥进行解密。这样攻击者只拿到传输过程中的公钥也无法破解传输的内容

尽管非对称加密解C o a g决了由于密钥被获取而导致传输内容泄露的问题,但中间% I , # 6人仍然可以用篡改公钥的方式来获取或篡改传输内容,而且非对称加密的性能比对称加密的性能差了不少

第三方认证

上面这种[ I F ] x u [ D方法的弱点在于,客户端不知道公钥是由服务端返回,还是中间人返8 ! E Z 3回的,因此我们再引入一个第三方认证的环节:即第三方使用私钥加密我们自己的公钥,浏览器已经内置一些权威第三方认证机构的公钥,浏览器会使用第三方的公钥来解开K w |第三方私钥加密过的我们自己的公钥,从而获取公钥,如果能成功解密,就说明获取到的r u U 1 F G D己的公钥是正确的

但第三方认证也未能完全解决问题,第三方认证是面向所有人的,中间人也能申请证书,如果中间人使用自己的证书掉包原证书,客户端还是无法确认公钥的真伪

数字签名

为了让客户端能够验证公钥的来[ } H N L O : b源,我们给公B n f F Q n Y j钥加上一个数字签名,这个数字签名是由企业、网站等各种信息和公钥经过单向hash而来,一旦构成数字t q 4 H ? C C签名的信息发生变化,hae – ` Y h c bsh值就会改变,这就构成了公钥来源的唯一标识。

具体来说,服务端本地生成一对密钥,然后拿着公钥以及企业、网, + d T t . 8 )站等各种信息B ? 0 ; I a b @到CA(第三方认证中心)去申请数字证书,CA会通过一种单向hash算法(比如MD5),生成一串摘要,这串摘要就是这堆信息的唯一标识,然后CA还会使用自己的C p ! 2 i i私钥对摘要进行加密,连同我们自己/ x p G服务器的公钥一同发送给我我们。

浏览器拿到数字签名后,会使用浏览器本地内置的CA公钥解开数字证书并验证,从而拿到正确的公钥。由于非对称加密性能低下,拿到公钥以后,客户端会S ^ ^随机生成一个对称密钥,使用这O J V M ~个公钥加密并发送给服务端,服务端用自己的私钥解开对称密s . H 2钥,此后的加密连接就通过这个对称密钥进行对称加密s { d e ( f ^ b

综上o 6 ? f G w所述,HTTPS在验证阶段使用非对称加密+第三方认证+数字签名获取正确的公钥,获取到正确的公钥后以对称加密的方式通信

参考资p 8 ?料:看图学HTTPS

前端安全

什么是CSRF攻击

CSRF即Cross-site request forgery(跨站请求伪造),是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。

假如黑客在自己的站点上放置了其他网站的外链,例如– I T t K"www.weibo.com/api,默认情况下,浏览3 y o 7 G ? ^ z器会带着weibo.com的cookie访问这个网址,如果用户已登录过该网站且网站没有对CSRC ) A o I v B I (F攻击进行防御,那么服务器就会认为是用户本人在调用此接口并执行相关操作,致使账号被劫持。

如何防御CSRF攻击

  • 验证Token:浏览器请求服务器时,服务器返回一个token,每个请求都需要同时带上token和cookie才会被认为是合法请求
  • 验证Referer6 3通过验证请求头的Referer来验证来源站点,但请求头很容易伪造
  • 设置SameSite:设置cookie的SameSite,可以让cookie不随跨域请求发出,但浏览器兼容不一

什么是XSS攻击

XSS即Cross Site Scriptik X = { C 3 bng(跨站脚本),指的是通过利用网页开发F W X T 6 2 $ S时留下的漏洞,注, d c h ] # 入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。常见的例如在评论区植入JS代码,用户进入评论页时代码被执行,造成页面被植入广告、账号信息被窃取

XSS攻击有哪些类型

  • 存储型:即攻击被存储在服务端,常见的是在评论区插入攻击脚U j [ o } _ E本,如果脚本= x ] o l c u b被储存到服务端,那么所有看见对应评论的用户都会受到攻击。
  • 反射型:攻击者将脚本混在URL里,服务端接收到URL将恶意代码当做参数取出并拼接9 a B在HTML里返回,浏览器解析此HTML后即执行恶意代码
  • DOM型:将攻击脚本写在URL中,诱导用户点击该URL,如果URL被解析,那么攻击脚本就会被运行。和前两者的差别主要在于DOM型攻击不经过服务端

如何防御XSS攻击

  • 输入检X t = a A ) W:对输入内容中的<script><iframe>等标签进行转义或者过滤
  • | | A c } l置httpOnly:很多XSS攻击目标都是窃取用户cookie伪造身份认证,设置此属性可防止JS获取cookie
  • 开启CSP,即开启白名单,可阻f w i U止白名单以外的资源加载和运行

排序算法

1. 手写冒泡排序

冒泡排序应该是很多K . L i人第一个接触的排序,比较简单,不展开讲解了

function bubbleSort(arr){
for(let i = 0; i < arr.lengG O 3 V kth; i++z S 1 g e = m K) {
for(let j = 0; j < arr.lk Z k ~ength - i - 1; j++) {
if(arr[j] > arr[j+1]) {
let temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
}
return arr
}

2. 如何优化一个冒泡排序

冒泡排序总会@ x p H F ^执行(N-1)+(N-2)+(N-3)+..+2+1趟,但如果运行到当中某一趟时排序已经完成,或者输v J s w ~ R 1 q入的是一个有序数组,那么后边的比较就都是多余的,为了避免这种情况T s = o } s i,我们增加一个flag,判断排序是否在中途就R s :已经完成(也就是判断有无发生元素交换)

functio. u W Tn bubbleSort(arr){
for(let i = 0; i < arr.length; i++) {
l$ U r /  ( oet flag = true
for(let j = 0; j < arr.lenV 0 & @gth - i - 1; j++)n E 5 K X @ { {
if(arr[j] > arr[j+1]) {
flag = fan x , 6 jlse
let temp = a% 6 $ Z $ Err[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
// 这个flag的含义是O n O -:如果`某次循环`中没有交换过元素,那么意味着排序已经完成
if(flag)break;
}
return arr
}

3. 手写快速排序

快排基本步骤:

  1. 选取基准元素V d n
  2. 比基准元素小的元素放到左边,大的放右边
  3. 在左右子数组中G + U !重复步骤一二,直到数组只剩下x J S一个元素
  4. 向上逐级合并数组
functi( p & D | F ?on quickSort(arr) {
if(arr.length <= 1) return arr          //递归终止条件
const pN p m K a - 9ivot = arr.length / 2 | 0        //基准点
cob c $nst pivotValue = arr.splice(p9 m Givot, 1)[0]
const leftArr = []
con3 S D )st riR 1 ) q @ R I FghtArr = []
arr.forEach(val => {
val > pivotValue ? rightArr.i ` $ ]push(val)S A y : leftArr.push(val)
})
return [ ...quickSort(lefs @ T 6tArr), pivotValue, ...quickSort(rightArr)]
}= B |  A

4. 如何优化一个快速排序

原地排序

上边这0 p ; D A E b ?个快排只是让读者找找感觉,我们不能这样写9 w d /快排,如果每次都开两个数组,会消耗很多内存空间,数据量大时可能造成内存h # Z . x溢出,我们要避免开新的内存空间,即原地完成排序

我们可以用元素交换来取代开新数组,在每一次分区的时候直接在原数组上交换元素,将小于基准数的元素挪到数组开头,以[5,1,4,2,3]为例:

我们定义一个pos指针, 标识等待置换的元素的位* b @ 3 t l &置, 然后逐一遍历数组元素, 遇到比基准数小的就和arr[pos]交换位置, 然后pos++C 1 e @ y P j L I

代码实现:

functd N ; 1 C ! & /ion quickSort(arr, left, right) {          //这个left和right代表分区后“新数组”的区间下标,因# X p H S R 为这里没有新开数组,所以需要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) {               //若小于等于O 5 T % ] =基准数,pos++,并置换元素, 这里使用小于等于而不是小于, 其实是为了避免因为重复数据而进入死循环
pov 3 - D y ~ 5 ?s++
let temp = arr[pos]
arr[pos] = arr[i]
arr[i] = temp
}
}
//一趟排序完成后,pos位置即基准数的位置,以pos的位置分割数组
quickSort(arr, left, pos - 1@ d ! d @ H)
quickSoM u , S B yrt(arr, pos + 1, right)
}
return arr      //5 F ? L F n /数组只包含1或0个元素时(即left>=right),递归终止
}
/| 5 ,/使用
var arr = [5,1,4,2,3]
var start = 0;
var end = arr.length - 1;
quickSort(arr, start, end)

这个交换的过程还是需要一些时间理解消化的,详细分析可以看这篇:js算法h C M ?-快速排序(Quicksort)

三路快排

上边这个快排还谈不上优化,应当说是快排的纠正写法,其实有两个问题我们还能优化一R + 8 5 E下:

  1. 有序数组的情况:如果输入的数组是有, W ~ { P x Q序的,而取基准点时也顺序取,就可能导致基准点一侧的子数组一直为空, 使时间复杂度退化到O(n2)
  2. 大量重复数据的情况:例如输入的数据是[1,2,2,2,2,3B d % A { + o], 无论基准点取1、2还是3, 都会导致基准点两侧数组大小不平衡, 影响快排效率

对于第一个问题, 我们可以通过在取基. – S准点的时候随机化来解决,对于第二个问题,我们可以使用三路快排的方式来优化,比方说对于上面的[1,2,2,2,2,3],我们基准点取2,在分区j ; i 的时候,将数组元素分为小于2|等于2|大于2三个区域,其中等于基准点的部分不再进入下一次排序, 这样就大大提高了快排效率

5. 手写归并排序

归并排序和快排的思路类似,都是递归分治,区别在于快排边分区边排序,而归并在分区完成后才会排序

function mergeSort(arr) {
if(arr.length <= 1) return arr		//数组元素被划分到剩1个时,递归终止
const midIndex = arr.lengthT  ;/2 | 0
const leftArr = arr.slice(0, midIndex)
const rightArr = arr.slice(mi, # VdIndexa J +, arr.length)
return merge(mergeSort(leftArr), mergeSort(rP r `ightArr))	//先划分,后合并
}
//合并
function mergei D ; J u 2(leftArr, right) K i  QArr$ ; p M F) {
const result = []
while(leftArr.length && rightArr.length) {
leftArr[0] <= rightArr[0] ? res7 j g s / 9 A tult.push(leftArr.shift()) : result) I 0 F 8 r ) y E.push(ri. } 4 H 6 ) c 7ghtArr.shift())
}
while(leftArr.length) result.push(leftArr.shift())
while(rightArr.leng5 E c ; cth) result.push(rightArr.shift())
return result
}

6. 手写堆排序

堆是一棵特殊的树, 只要满足这棵树是完全二叉树堆中每一个节点的值都大于g ` t m J N或小于其左右孩子节点这两个条件, 那么就是一个堆, 根据堆中每一个节点的值都大于或小于其左右孩子节点, 又分为大根Y u T U f ; N堆和小根堆

堆排序2 W K 7 Y K j m `的流程:

  1. 初始化大(小)根堆,此时根节点为最大(小)值,将根. ? Y节点与最后一个节点(数组最后一个元素)交换
  2. 除开最后一个节点,重新调整大(小)根堆,使根节点为最大(小)值
  3. 重复步骤二,直到堆中元素剩一个,排序完成

[1,5,4,2,3]为例构筑大根堆:

i ^ E J 6 –码实现:

// 堆排序
const heapSort =T c x $ ) array => {
// 我们用数( # q b U V 7 d w组来储存这个大根堆,数组就: : !是堆本身
// 初始化大顶堆,从第( N % f一个非叶子结点开始
for (let i = Math.floor(array.length / 2 - 1); i >= 0; i--)A : G C 9 2 {
heapify(array, i, array.length);
}
// 排序,每一次 for 循环找出一个当前最大值,数组S h R长度减一
for (letJ { I w 4 h m E H i = Matt G bh.floor(array.length - 1); i > 0; i--) {
// 根节点与最后一个节点交换
swap(array, 0, i);
// 从根节点开始调r P $ c 0 & ` c整,并且最后一个结点已经为当前最大值,不需要E f G 7 u再参与比较,所以第三个参数为 i,即比较到最后一个结点前一个即可
heapify(array, 0,E @ A a u i);
}
return array;
};
// 交换两个节点
consk I 3 y Yt swap = (arr; G C q ( : 0 | )ay, i,6 2 [ g I T j) => {
l/ & s } # _ Eet temp = array[i];
array[i] = array[j];
arr` B H i r P &ay[j], 2 P = temp;
}F . x # G j / s ,;
// 将 i 结点以下的堆整理为大顶堆,注意这一步实现的基础实际上是:
// 假设结点 i 以下的子堆已经是一个大顶堆,heapify 函数实现的
// 功能是i 6 g Z R ? a 2 5实际上是:找到 结点 i 在包括结点 i 的堆中的正确位置。
// 后面将写一个 for 循环,从第一个非叶子结点开始,对每一个非叶子结点
// 都执行 heapify 操作,所以就满足了结点 i 以下的子堆已经是一大顶堆
const heapify = (array, i, length)j 3 4 i ; U 5 q => {
let temp = array[i( 4 Q O Y e Z ]]; // 当前父节点
// j < length 的目的L - D是对结点 i 以下的结点全部做顺序调整
for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
temp = aY . B C x Hrray[i]; // 将 array[i] 取出,整个过程相当于找到 array[i] 应处于的位置
if (j + 1 < length && array[j] < array[j + 1d 7 q])V [ D % . L Y q T {
j++; // 找到两个孩子中较大的N # ] 2 ? 4 a m [一个,再与父节点比较~ 8 x . d ^ v ~ `
}
if (temp < array[j]) {
swap(array, i, j); // 如果父节点小于子节点:交换;否则跳出
i = j; // 交换后,temp 的下标变为 j
} else {
breav ` }k;
}
}
}

参考资料: JS实现堆排序

7. 归并、快排、q A { 5 D 2 v c堆排有何区别

排序 时间复杂度(最好情况) 时间复杂度(最坏情况) 空间复杂度 稳定性
快速排序 O(nlogn) O(n^2) O(logn)~O(n) 不稳定
归并排序 O(nlogn) O(nl[ t 5 8 R B @ogn) O(n) 稳定
堆排序 O(nlogn) O(nlogn) O(1) 不稳定H | b F + L % 4

其实从表格中我们可以看到,就时间复( A m u杂度而言,快排并没有很大优势,然而为什么快排会成为最常用的排序手段,这是因为时间复杂度只能说明随着数据量的增加,算法时间代价增长的趋势,并不直接代表实际执行时间,实际运行时间还包y ] ) } ]括了很多常数参数的差别,此外在面对不同类型数据(比如有序数据、大量重} 0 A 8 x , s _ $复数据)时,表现也不同,综合来说,快排的时间效率是最高的

在实际运用中, 并不只使用一种排序手z 2 i Q ; ` #段, 例如V8的Array.sortw ~ v V [ l P #()就采取了当 n<=10 时, 采用插入排序, 当 n>10 时,采用三路快排的排序策略

设计模式

设计模式有许多种,这里挑出几个常用的:

设计模式 描述 例子
单例模式 一个类只能构造出唯一实$ 6 H 8 L Redux/Vuex的store
工厂模式 对创建对象逻辑的封装 jQuery的$(selector)
观察者模式 当一个对象被修改时,会自动通知它的依赖对象 ReduX % y h Hx的subscribe、Vue的双向绑定
装饰器模8 C 0 G l @ , ~ 对类的包装,动态地拓展类的功能 Reac} i 4 3 y F D 2 Wt高阶组件、ES7 装饰器
适配器模式 兼容新旧接口,对类的包装 封装旧API
代理模式 控制对象的访问 事件代理、ES6的Proxy

1. 介绍一下单一职责原则和开放封闭原则

  • 单一职责原则:一个类只负责一个功G M _ : 6 W F [能领域中/ 5 N G的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它M } 1 R变化的原因。

  • 开放封闭原则m ~ B 9 N N d . k:核心的思想是软件实体(类、模块、函数等)是可扩展的、但不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。

2. 单例F E D }模式

单例模式即一个类只能构造出唯一实例,单例模式的意义在于共享、唯一Redux/Vuex中的store、JQ的$或者业务场景中的W { H购物车、登录框都是m P ! 2 0 Y @ 2z r L N ` M例模式的应用

class Singlv P _ C g & - g YetonLogin {
constructor(name,password){
this.name = name
this.password} % 9 m S o = pJ s fassword
}
static getInstance(name,password){
//判断对象是否已经被创建,若创建则返回旧对象
if(!this.instanp L #ce)this.ink  V N C 9 bstab w V g ? v d Vnce =& w  z g f new Singletone * c ] 8 C WLoi I Q f & &gin(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)           // 输出的依然是$ x $ m = 2 I l{name:CXK,passwoJ ~ 3 t P @ c )rd:123}

3. 工厂模6 g , ) p

工厂模式即对创建对象逻辑的封0 b c [ = I r J |装,或者可以简单理解为对new的封装,这种封装就像创建# D J 6 j v对象的工厂,故名工厂模式。工厂模式常见于大型项目,比如JQ的$对象,我们创建选择器对象时之所以没有new selector就是因为$()已经是一个工厂方法,其他例子例如React.createElL | b x V . -ement()Vue.component()都是工厂模式的实现。工厂模式有多种:简单工厂模式工厂方法模式E e p Y $ G - y ]象工厂模式,这里只以简单工厂模式为例:

class User {
constructor(name, auth) {
this.name = name
thisI . | n s h c {.auth = auth
}
}
cl! = I R & m [ `ass UserFactory {
s@ 8 j ) @ Ftatic createUser(s % Aname, auth) {
//工厂内部封装了创建对象的逻辑:
//权限为ads = _ w @mi` # J Xn时,auth=1, 权限[ I V 3 0 Q J为user时, auth为2
//使用者在外部创建对象时,不需要知道各个权限对应哪个字段, 不需要知道赋权的逻辑,只需( 9 ( { ? i 要知道创建了一个管理w 6 X员和用户
if(as k H q y q r S buth === 'admin')  new User(name, 1)
if(auth === 'user')  new User(name, 2)
}
}
const admin = Us* R Q D j  ^ ~ eerFactory.createUser('cxk', 'admin');
const user = UserFe # g 5 q Z Bactory.createUs{ J [ 3 r D [er('cxk', 'user');

4. 观察者模式

观察者模式算是前端最常用的设计模式了,8 F n % n观察者模式概念很简单:观察者监听被观察者的变化,被观察者发生改变时,通知所有的观察者。观察者模式被广泛用于监听事件的实现,有关观察者模式的详细应用,可以看我另一篇讲解Redux实现的文章

//观察者
class Observer {0 , T ` 5 l K
constructor (fn) {
this.update = fn
}
}
//被观察者
class8 f V a 9 2 Subject {
const( ` Yructor() {
this.observers = []          //观察者队列    
}
addObserver(observer) {
this.observers.push(observer)//往观察者队列添加观察者H $ ; [ P |    
}
notify() {                       //通R & ) [ s % . _知所有观察者,实际上是把观察者的update()都执行了一遍       
thi4 Q + p # L ps.observers.forEach(observer =&gs z L @t; {
observers K 9 m 5 I 3 P.update()            //依次取出观察者,并执行观察者的update方x ? z M } Z
})
}
}
var subject = new Subject()       //被观察者
const ug ] P m u h | J spdatw / l ke = () => {consol} H v 6 = -e.log('被观察者发出通知')}  //收到广播时要执行的方法
varh O q ob1 = new Observer(update)! ^ v 7 g Y    //观察者1
var ob2 = new Observer(update)    //观察者2
subject.addObserver(obV + 7 4 3 j1)          //观察者1订阅subject的通知
subject.addObserver(y % [ . ] %ob2)          //观察者2订阅subject的通知
subject.notify()                  //发出广播,执行所有观察者的update方法

有些文章也把观察者模式称为发布订阅模式,其实二者是有所区别的,发布订阅相较于观察者模式多一个调度中心。

5. 装饰器模式

装饰器模式,可以理解为对类的一个包装,动态地拓展类的功能,ES7的装饰j ] L语法以及React中的高阶组n 6 L ~ 5 8 l(HoC)都是这一模式的实现。react-redux的connect()也运用了装饰器模式,这里以ES7的装饰器为例:

function info(target[ C _ . j s ` O M) {
target.prototyb } ^ 9 @ y S ? [pe.name = '张三'
target.prototy) ~ ] F a R y $ !pe.agA F T  $ 7e = 10
}
@info
class Man {}
let ma@ o &n = new Man(t b P g #)
man.name // 张三

6. 适配器模式

适配器模式,将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作。我们在生活中就常常有使用适配0 } w H | F F器的场景,例如出境旅游插头插座不匹配,这时我们就需要使用转换插头,也就是适Z r s r P . ; y 1配器来帮E 4 – Y C B我们解决问题% m & H 1 n N 9 m

class Adaptee {
test() {
ret2 0 n l W v 6urn '旧接口'
}
}
class Target {
constructor() {
this.adaptee = new Adaptee()
}
test() {Z ~ [
let info = this.adaptee.test()
return `适配${info}`
}
}
let tarV M ; N [ Yget = new Target()
console.log(target.test())

7. 代理模式

代理模式,为一个对象找一个替代对象,以便对原对象进行访问。即在访问者与目标对象之间加一层代理,通过代理做授权和控制。最常见的例子是经纪人代理明星业务,假设你作为一个投4 & # ( p Y资者,想联系明星打广告,那么你就需要先经过代理经纪人,经纪人对你的资质进行考察,并通知你明星排期,替明星本人过D $ V y滤不必要的信息。5 – A 2事件代理、JQuery的$.proxy、ES6的pro; 2 5xy都是这一模式的实现,下面以ES6的proxy为例:

coM V p j m b xnst idol = {
name: '蔡x抻',
phone: 10086,
price: 1000000  //报价
}
const agent = new Proxy(idol, {
get: function(target) {
//拦截明星电话的请求,只提供经纪人电话
return '经纪人4 W Q * 6 k H电话:? + 410010'
},
set: function(target, key, value) {
ifc s G d 5 + p * ;(key === 'price' ) {
//经纪人过滤资质
if(value &lb h Nt; target.price) throw new Error('报价过低')
target.price = value
}
}
})
agent.pho{ 3 ^ne        //经纪人电话:10010
agent.price = 100  //Un( + I } a n 7 wcaught Error: 报价过低

HTML相关

1. 说说HTML5在标签、属性、存储、API上的新特性

  • 标签:新增语义化标签(aside / figure / section / header / footer / nav等),增加多媒体标签videoaudio,使得样式和结构更加分离
  • 属性:增强表单,主要是增强了input的type属性;meta增加charset以设置字符集;script增加async以异步加载脚本
  • 存储:增加localStoragesessionStorageindexedDB,引入了application cache对web和应用进行缓存
  • API:增= L o S拖放API地理定位SVG绘图canm 9 $ M n Avas绘图Web WorkerWe9 . sbSocket

2. doc_ X |type的作用是什么?

声明文档类型,告知浏览器用什么文档标准解析这个文档:

  • 怪异模式:浏览器使用自己的模式解析文档,不加doctype时默认为怪异模式
  • 标准模式:浏览器以W3C的标准解析文档

3. 几种前端储存以及它们之间的区别

  • cookies: HTM~ L = & A / N & YL5之前本地储存的N s C P B }主要方式,大小9 [ Q 2 = G & }只有4k,HTTP请求头会自动带上cookie,兼容性好
  • localStorage:HTML5新特性,持久性存储,即使页面关闭也不会被清除,以键值对的方式存储,大小为5M
  • sessionStorage:HTML5新特性,操作及大小同localStorage,和localStorage的区别在于sessiM s =onStorage在选项卡(页) N R N A S g面)被关闭时即清除,且不同选项卡之间的sessionStorage不互通
  • IndexedDB: NoSQL型数据库,类比Mongo– { [ U n YDB,使用键值对进行储存,异步操作数据库,支持事务,储存空间可以在250MB以上,但是IndexedDB受同源策略限制
  • Web SQL:是在浏览器上模拟的关系型数据库,开发者可以通过SQL语句来操作Wel H = ` Fb SQL,是HTML5以外一` g c t N 3 M x套独立的规范,兼容性差

4. href和src有什么区别

href(hyperReference)即超文本引用:当浏览器遇到href时,会并行的: n ,地下载资源,不会阻塞页面解析,例如我们使用<C z = F p # A ~ zlink>引入CSS,浏览器会并行地下载CSS而不阻塞页面解析. 因此我们在引入CSS时建议使用<link>而不z L N X S c G ;@impor % G P wt

<link href="style.css" rel="styll D 7 , H { c mesheet" />

src(resource)即资源,当浏览! U ; ` K o ; . C0 S ? ~ n g J遇到src时,会暂停页面解析,直到该资源下载或x ! .执行完毕,这也是script标签之所以放底部的原因

<script src="script.js` # E w"></script>

5. meta有哪些属性,作用是什么

meta标签用于描述网页的元信息z , r,如网站作者、描述、关键词,meta通过name=xxxcontent=xxx的形式来定义信息2 b I L T 4 f . G,常用设置如下:

  • charset:定义HTML文档的字符集
 <meta charset="UTF-8" >
  • http-equq F V B I u Oiv:可用于模拟http请求头,可设置过期时间、缓存、刷新
<meta h@ Z 9  m % ?ttp-equiv="expires" content="Wed, 20 Jun 2019 22:33:00 GMT"
  • viewport:视口,用于控制页面宽高及缩放比例
<meta
naz g K F 5 p ~me="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
>

6. vie– } S _ P o O ywpQ K : v & @ort有哪些参数,作用是什么

  • width/height,宽高,默认u q * b _ a Z v [宽度980px
  • i( Q a a 4nitial-scale,初始缩放比例,1~10
  • maximum-scale/minimum-scale,允许用户缩放的最大/小比例
  • user-scalable,用户是否可以缩放 (yes/no)

7. http-equive属性的作用和参数

  • expires,指定过期时间
  • progma,设置no-cache可以禁止缓存
  • refresh,定时刷新
  • set-cookie,可以设置cookie
  • X-UA-CompatW $ P , Iible,使用浏览器版本
  • apple-mobile-web-app-status-bar-style,针对WebApp全屏模式,隐藏状态栏/设置状态栏颜色

CSS相关

清除浮动的方法

为什么要清除浮) N 动:清除浮动是为了解决子元素浮动而导致父元素高度塌陷的问题

1.添加新元素

&Y O { #  .lt;div 3 h class="parent">_ / Y 2 / + 8
<div class="child"></div>
<!-- 添加一个空元素,利用css提供的clear:both清除浮动 -->
<div styh f 6 Ule="clear: both"></div>
</div>

2.使用伪元素

/* 对父元素添加伪元素 */
.parent::after{
coY x d g ! 0 Q jntent: "";
display: block;
height: 0;
clear:both;
}

3.触发父元素BFC} j ; . r v o r

/* 触发父元素BFC */
.parent {
overflow: hidden;
/* float: left; */
/* position: absolute; */
/* dis! ) ? B } (play: inline-block */
/* 以上属性均可触发BFC */
}

介绍一下flex布局

其实我本来还写了一节水平/垂直居中相关的,1 x @不过感觉内容过于基础还占长篇幅,所以删去了,作为一篇总结性的文章,这一小节也不应该从“flex是什么”开始讲,主轴、侧轴这些概念相信用过flex布局都知道,所以我们直接flex的几个属性讲起:

容器属性(使用在fleb a )x布局容器上的属性)

  • justify-content
    定义了子元素在主轴(横轴)上的对齐方式
.container {
just. m e * K o . 7ify-content: center |A b { ( ) u flex-start | flex-end | space-between | space-around;
/* 主轴对齐方式:居中 | 左对齐(默认值) | 右对齐 | 两M o _ / U & q a W端对齐(子元素间边距相等) | 周围对齐(每个子元素两侧margin相等) */
}
  • align-items
    定义了定义项目在交叉轴( f C ( g(竖轴)上对齐方式
.conS e z H V * 2 5tainer {
align-items: center | flex-start | flex-end | baseline |f { p S , n f , h stretch;
/* 侧轴对齐方式:居中 | 上对齐 | 下对5 M - E S齐 | 项目的第一行文字的基线对齐 | 如果子元素未设置高度,将占满整个容器的高度(默认值) */
}
  • flex-direction
    主轴(横轴)方向
.container {
flex-direction: row | row-reve& r qrse | column | column-reverse;
/* 主轴方向:水平由左至右排列t / ( + k : z %(默认值) | 水平由右向左 |F c m P . m  p c 垂直由上至下 | 垂直U ` K 7 F 0 a R由下至上 */
}
  • flex-wrap
    换行方式
.container {
flex-wf 7 { R : g 2rap: nowrap | wrap | wrap-reverse;
/* 换行方式:不换行(默认值) | 换行 | 反向换行 */
}
  • flex-flow
    flex-flow属性是flex-direction属性和fH q Z 5 c S [lex-wrap的简写
.container {
flex-flow: <flex-directioZ # on> || <flex-wrap>;
/* 默认值:row nowrap */
}
  • align-content
    定义多根轴线的对齐方式
.container {
align-content: center | flex-start | fl6 } q e 7ex-end | space-between | space-around | stre. t r - 1 +tch;
/* 默认值:与交叉轴的中点对齐 | 与交叉轴的起点对齐 | 与交叉轴的终点对齐 | 与交叉轴两端对齐 | 每根轴线两侧的间隔都相等 | (默认值):轴线占满整个交叉轴 */
}

项目属性(使用在容器内子元素上的属性)

  • flex-grow
    定义项目的放大比例,默认为0,即使有剩余空间也不放大。如果所有子元素fq 5 L N 9 ( m r 0lex-grow为U J s g 7 k Y1,; n N 0 d ; $ H ?那么将等分剩余空间,如果某个子元素flex-grow为2,那么这个子元素将占据2倍的剩余空间
.item {
flex( 7 1 8-grow: <number>; /* default 0 */
}
  • flex-L l ? ` F e #shrink
    定义项目的缩小比例,默认为1,即如果空间不足,子元素将缩小。如果所R % X有子元素flex-shrin! I x O Gk都为1,某个子元素flex-shrink为0,那么该子元素3 ; F将不4 $ j B :缩小
.item {
flex-shrink: <numbQ + ;er>? O } o k `;; /* default 1 */
}
  • flex-basis
    定义在分配多余空间之前( & ~ H k 4 $,项目占H 3 C z ?据的主轴空间,默认auto,即子元素本来的大小,如果设定为一个固定的值,那么子元素将占据固定空间
.item {
fle!  k | &x-basis: <length> | auto; /* default~ U B ; 0 C | auto */
}
  • flex
    flex属O X 3 : F A性是flex-grow, flex-shrin? s D ` Skflex-basis的简写,默认值为0 1 auto,即有剩余空间不放大,剩余空间不够将缩小,子元素占据自身大小
.item {
flex: none | [ <'flex-grow j  ; _ ] C # B'> <'flex-shrink'>? || <'flex-basis'> ]
}

flex有两个快捷值:autonone,分别代表1 1 auto(有剩余空间则平均分配,空间不够将等比缩小,子元素占据空间等于自身大小)和0 0 auto(有剩余空间也不分配,空间不够也不缩小,子元素占据空间等于自身大小V v 0 8 9 +

  • order
    定义项目的排列顺序。数值越小,排e 0 P m列越靠前,默认为0
.item {
order: <? Z ~ & W;integer>;
}
  • align-self
    定义单个子元素的排列方式,例如align-items设置了center,使得所有子元素居中对齐,那么可以通过给某个子元素设置align-self来单独设置子元素的排序方式
.item {
align-self: auto | flex-start | fle# n T % i - Vx-end | center | baseline | stretch;
}t L Q S + L

参考资料:阮一峰Flex布局

常见布局

编辑中,请稍等-_-||

什么是BFC

BFC全称 Block Formatting Context 即块级格式上下文,简单的说,BFC是页面上的一个隔离的独立容器,不受外界干扰或干扰外界

如何触发BFC

  • float不为 none
  • overfy ~ $ 1low的值不为 visible
  • position 为 absolute 或 fixed
  • display的值为 inline-block 或 table-cell 或 table-captio_ F 8 b N W hn 或 grid

BFC的渲染规则是什] . 2 7 z

  • BFC是页面上的一个隔离的独立容器,不受外界干扰或干扰外界
  • 计算BFC的高度时,浮动子元素也参与计算(即内部有浮动元素时也不会发生高度塌陷)
  • BFC的区域不会与float的元素区域重叠
  • BFC内部的元素会在垂直方向上放置
  • BFC内部两个相邻元素的margin会发生重叠

BFC的应用场景

  • 清除浮动:BFC内部的浮动元素会参与高度计算,因此可用于清除浮动,防止高度塌陷
  • 避免某元素被浮动元素覆盖:BFC的区域不会与浮动元素的区域重叠
  • 阻止外边距重叠:属于同一个BFC的两个相邻Bo? l h 8x的marF { h I 7 E Dgin会发生折叠,不同BFC不会发生折叠

总结m 9 – P H r L , [

对于前端基础知识的讲解,到这里就告一小段落。前端的世界纷繁复杂,远非笔者寥寥几笔所能勾画,笔t [ 7 ] y p 者就像在沙滩上拾取贝壳的孩童,有时侥幸拾取收集一二,就为之欢欣鼓舞,迫不及待与伙伴们分享。

最后还想可耻地抒(自)发(夸)一下(•‾⌣‾•)✧:
不知不觉,在6HU网已经水了半年有余,这半年来我写下了近6万字,不过其实一共只有5篇文章,这是因为我并不r x o h u $ . 8 r想写水文,不想把基础的东西水上几千字{ ^ 9 H o p几十篇来混赞升级。写下的文章,首4 O Q f * O / Q p先要能说服自己。要对自己写下的东西负责任,即使是一张图、一个标点。例如第一张图,我调整了不下十次,第一次我直接截取babel3 x N b p (的转化结果,觉得不好w K % + % _ %看,换成了代码块,还是不好看,又换成了carbon的代码图,第一次下载,发现两张图宽度不一样,填充宽度重新下载,又发现自己的& ^ : }代码少了一个空格,重新下载,为了实现两张图并排效果,写了一个HTML来调整两张图的样式,为了保证每张图的内容和边距一W u F 6致,我一边截图,一边记录下每次截图的尺寸和边距,每次截图都根据上一次的数据调整边距。

其实我并非提倡把时间花在这些细枝末节上,只是单纯觉得,文章没写好,就不能发出来,就像小野二郎先生说的? H q y那样} % M =:“菜做的不好,就不能拿给客人吃”,世间的大道理,往往都这样通俗简单。

往期文章:

1. 异步编程二三事 | i y X ? % ? Promise/async/Generator实现原理解析 | 9k字
2. 10行代码看尽redux实现 —— redux & react-redux & redux中间件设计实现 | 8k字
3.红黑树上红黑果,红黑树下你和我 —— 红黑树入门 | 6k字
4. 深入React服n ` ( a务端渲染原理 | 1W字

评论

发表回复