本文整理前端常见的知识要点,方便随时复盘。内容主要涵盖JS基础、CSS、HTML,JS面向对象,渲染机制,网络HTTP/通信,前端安全,设计模式,算法等方面。
注:文章内容相对而言有一定的难度,所以需要读者有一定的基础。另外,若文章有何不妥之处或者你有任何疑问,欢迎留言讨论。
一.JS基础
1.手写实现call()
call()、apply()和bind()这是前端初学者比较头疼的三个方法,也是从初级向中级进阶所必需掌握的。通过使用这些方法,我们可以修改函数绑定的this,使其成为我们指定的对象。
在实现call()之前,先来看下如何使用它。(注:由于call()和apply()作用类似,所以这里一起介绍)
func.call(thisArg, arg1, arg2, ...)
func.apply(thisArg, [arg1, arg2, ...])
我们可以看到,call()和apply()方法的区别在于其传递参数的格式不同,一个是参数列表,一个是数组。
再看个实际的例子
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
return sum.call(this, num1, num2);
// return sum.apply(this, [num1, num2]); // apply 方式调用
}
console.log(callSum(10, 10)); // 20
传递参数并非call()和apply()真正的用武之地;它们真正强大的地方是能够扩充函数赖以运行的作用域。
window.color = 'red';
var o = { color: 'blue' };
function sayColor() {
console.log(this.color)
}
// 注:下面代码中的call也可以换成apply
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue
这里通过传入o对象,使sayColor()方法中的this指向了o,所以最后输出了blue。
好了,了解了call()的使用和原理,开始实现call()吧
// 手写call()方法
Function.prototype.myCall = function(thisArg, ...args) {
thisArg.fn = this // this指向调用call的对象,即我们要改变this指向的函数
return thisArg.fn(...args) // 执行函数并return其执行结果
}
// 尝试第一个例子
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
return sum.myCall(this, num1, num2);
}
console.log(callSum(10, 10)); // 20
// 尝试第二个例子
window.color = 'red';
var o = { color: 'blue' };
function sayColor() {
console.log(this.color)
}
sayColor(); // red
sayColor.myCall(this); // red
sayColor.myCall(window); // red
sayColor.myCall(o); // blue
可以看到,我们自己实现的myCall()方法与原生的call()方法执行的效果完全一样。
上面的call()实现还能进一步优化
Function.prototype.myCall = function(thisArg, ...args) {
var fn = Symbol('fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
thisArg = thisArg || window // 若没有传入this, 默认绑定window对象
thisArg[fn] = this // this指向调用call的对象,即我们要改变this指向的函数
var result = thisArg[fn](...args) // 执行当前函数
delete thisArg[fn] // 删除我们声明的fn属性
return result // 返回函数执行结果
}
2.手写实现apply()
基于上面call()的例子,我们这里就直接实现apply()方法
// 手写实现apply方法
Function.prototype.myApply = function(thisArg, args) {
var fn = Symbol('fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
thisArg = thisArg || window // 若没有传入this, 默认绑定window对象
thisArg[fn] = this // this指向调用apply的对象,即我们要改变this指向的函数
var newArgs = args ? args : [] // 这里判断是否传递了参数
var result = thisArg[fn](...newArgs) // 执行当前函数(此处说明一下:虽然apply()接收的是一个数组,
// 但在调用原函数时,依然要展开参数数组。可以对照原生apply(),原函数接收到展开的参数数组)
delete thisArg[fn] // 删除我们声明的fn属性
return result // 返回函数执行结果
}
// 尝试第一个例子
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
return sum.myApply(this, [num1, num2]);
}
console.log(callSum(10, 10)); // 20
// 尝试第二个例子
window.color = 'red';
var o = { color: 'blue' };
function sayColor() {
console.log(this.color)
}
sayColor(); // red
sayColor.myApply(this); // red
sayColor.myApply(window); // red
sayColor.myApply(o); // blue
3.手写实现bind()
bind()方法,这个方法会创建一个函数的实例,其this值会被绑定到传递给bind()函数的值(也就是第一个参数)。
window.color = 'red';
var o = { color: 'blue' };
function sayColor() {
console.log(this.color)
}
var objectSayColor = sayColor.bind(o);
objectSayColor(); // blue
其实使用apply()/call()方法就可以实现一个bind():
// 手写bind
Function.prototype.myBind = function(...rest1) {
var self = this
var context = rest1.shift() // 取得第一个参数(即执行环境),并删除
return function(...rest2) {
return self.apply(context, [...rest1, ...rest2])
}
}
// 测试
window.color = 'red';
var o = { color: 'blue' };
function sayColor() {
console.log(this.color)
console.log(arguments)
}
var objectSayColor = sayColor.myBind(o, '1', '2');
objectSayColor('3'); // 输出 "blue" 和 "1", "2", "3" 相关的数据
4.手写Promise
Promise 的实现有一定难度,这里推荐一篇文章:9k字 | Promise/async/Generator实现原理解析
5.闭包
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式:在一个函数内部创建另一个函数。
一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅会保存全局作用域(全局执行环境的变量对象)。但闭包则有所不同。
一个闭包的例子:
function createFunction(width, height) {
return function(object) {
var area = object[width] * object[height]
return area
}
}
// 创建函数
var areaFunc = createFunction('width', 'height');
// 调用函数
var area = areaFunc({ width: 3, height: 2 })
这里的 areaFunc 方法的作用域链中,包含了闭包的活动对象(arguments,object)、createFunction()的活动对象(arguments,width,height)和全局变量对象(createFunction,area)。当createFunction()的函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然留在内存中,这时需要我们手动销毁匿名函数,以释放内存。
// 解除对匿名函数的引用(释放内存)
areaFunc = null;
注:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。所以,我们最好在必要时才使用闭包。
6.函数柯里化
函数柯里化,用于创建已经设置好了一个或多个参数的函数。
通过下面这个例子能很好的理解其概念。
function add(num1, num2) {
return num1 + num2;
}
function curriedAdd(num2) {
return add(5, num2);
}
console.log(add(2, 3)); // 5
console.log(curriedAdd(3)); // 8
这个例子从技术上讲并非柯里化的函数,但很好的展示了其概念。
创建柯里化函数的步骤:调用另一个函数并为它传入要柯里化的函数和必要参数。通用方式如下:
function curry(fn) {
var args = Array.prototype.slice.call(arguments, 1); // 删除传入的function,获取其余参数
return function() {
var innerArgs = Array.prototype.slice.call(arguments); // 取得参数
var finalArgs = args.concat(innerArgs);
return fn.apply(null, finalArgs); // 这里不需要考虑执行环境,所以null
}
}
// 测试
function add(num1, num2) {
return num1 + num2;
}
var curriedAdd = curry(add, 5);
console.log(curriedAdd(3)); // 8
// 也可以第一次就直接传递完参数
var curriedAdd2 = curry(add, 5, 3);
console.log(curriedAdd2()); // 8
其实bind()方法中就实现了函数柯里化,只要在this值之后再传入另一个参数即可。 柯里化函数提供了强大的动态函数创建功能,但不要滥用,因为会带来额外的开销。
7.节流函数
防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间就执行一次,实现原理为设置一个定时器,约定xx毫秒后执行事件,如果时间到了,那么执行函数并重置定时器,和防抖的区别在于,防抖每次触发事件都重置定时器,而节流在定时器到时间后再清空定时器。
如果上面的介绍没懂,那举个例子。比如,窗口的onresize事件,我们分别使用防抖和节流,时间设为500毫秒。使用防抖:若你一直在改变窗口大小,则你的处理方法不会执行,只有你停止改变窗口大小后的500毫秒后,才会执行你的处理方法。使用节流:若你一直在改变窗口大小,则每500毫秒就会执行一次你的处理方法。
理解完概念,我们在看节流函数如何写的吧:
function throttle(func, wait) {
let timeout = null
return function() {
const context = this
const args = arguments
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
func.apply(context, args)
}, wait)
}
}
}
注:只要代码是周期性执行的,都可以考虑使用节流。
8.防抖函数
防抖函数如下
function debounce (func, wait) {
let timeout = null
return function() {
const context = this
const args = arguments
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
应用场景:比如,搜索框搜索。如果在工作中想直接使用防抖和节流方法,可以使用Lodash.js插件(这个插件整合很多常用的工具方法)。
9.数组扁平化
比如要将 [1, [1,2], [1,2,3]] 展开为 [1, 1, 2, 1, 2, 3]
(1)ES6中的flat()
const arr = [1, [1,2], [1,2,3]].flat()
(2)使用正则
const arr = [1, [1,2], [1,2,3]]
const str = `[${JSON.stringify(arr).replace(/(\[|\])/g, '')}]`
JSON.parse(str)
上面这两种方式比较简单实用,当然,还可以使用其他方法,比如循环递归。这里介绍一个reduce()的方式
(3)使用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)
10.深拷贝,浅拷贝
深浅拷贝都是针对引用类型而言的,浅拷贝只是复制对象的引用,如果拷贝后的对象发生变化,原对象也会发生变化。只有深拷贝才是真正地对对象的拷贝。
【浅拷贝】
浅拷贝只是复制了引用,并没有实现真正的复制。
var arr = [1,2,3];
var obj = {a:'a', b:[1,2], c:{cc:'cc'}};
var cloneArr = arr;
var cloneObj = obj;
cloneArr.push(4);
cloneObj.a = 'aaa';
console.log(arr); // [1, 2, 3, 4]
console.log(cloneArr); // [1, 2, 3, 4]
console.log(obj); // {a:'aaa', b:[1,2], c:{cc:'cc'}}
console.log(cloneObj); // {a:'aaa', b:[1,2], c:{cc:'cc'}}
这里,我们无论改变原来的值还是克隆的值,它们都会相互影响,并没有实现隔离。
【深拷贝】
深拷贝则是完完全全的拷贝,它们之间彼此隔离,互不影响。
实现深拷贝的方法主要有两种:
- 利用 JSON 对象中的 parse 和 stringify
- 利用递归来实现每一层都重新创建对象并赋值
(1)JSON 对象中的 parse 和 stringify
var arr = [1,2,3];
var obj = {a:'a', b:[1,2], c:{cc:'cc'}};
var cloneArr = JSON.parse(JSON.stringify(arr));
var cloneObj = JSON.parse(JSON.stringify(obj));
cloneArr.push(4);
cloneObj.a = 'aaa';
cloneObj.b.push(3);
cloneObj.c = 'ccc';
console.log(arr); // [1, 2, 3]
console.log(cloneArr); // [1, 2, 3, 4]
console.log(obj); // {a:'a', b:[1,2], c:{cc:'cc'}}
console.log(cloneObj); // {a:'aaa', b:[1,2,3], c:{cc:'ccc'}}
这个方法确实实现了深拷贝,也是我们最常用的一种方法,但对于一些复杂的引用类型,就存在问题
var obj = {
name: 'Tom',
sayName: function(){
console.log(this.name)
}
}
var cloneObj = JSON.parse(JSON.stringify(obj));
console.log(obj); // {name: "Tom", sayName: ƒ}
console.log(cloneObj); // {name: "Tom"}
我们发现,它并没有将方法复制下来。原因是:undefined、function、symbol 会在转换过程中被忽略。。。
(2)递归实现
利用递归实现深拷贝的思想是每一层都重新创建对象并赋值
function deepClone(source){
const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
for(let keys in source){
if(source.hasOwnProperty(keys)){ // 判断属性是否存在于实例中
if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
targetObj[keys] = source[keys].constructor === Array ? [] : {};
targetObj[keys] = deepClone(source[keys]);
}else{ // 如果不是,就直接赋值
targetObj[keys] = source[keys];
}
}
}
return targetObj;
}
// 测试
var obj = {
name: 'Tom',
sayName: function(){
console.log(this.name)
}
}
var cloneObj = deepClone(obj);
console.log(obj); // {name: "Tom", sayName: ƒ}
console.log(cloneObj); // {name: "Tom", sayName: ƒ}
在来测测最开始的例子
var arr = [1,2,3];
var obj = {a:'a', b:[1,2], c:{cc:'cc'}};
var cloneArr = deepClone(arr);
var cloneObj = deepClone(obj);
cloneArr.push(4);
cloneObj.a = 'aaa';
cloneObj.b.push(3);
cloneObj.c = 'ccc';
console.log(arr); // [1, 2, 3]
console.log(cloneArr); // [1, 2, 3, 4]
console.log(obj); // {a:'a', b:[1,2], c:{cc:'cc'}}
console.log(cloneObj); // {a:'aaa', b:[1,2,3], c:{cc:'ccc'}}
我们发现,都是ok的。
【JavaScript中的拷贝方法】
JavaScript中的有的方法也能实现拷贝,比如:concat()和slice(),ES6中的Object.assgin()和...展开运算符。这里就不一一测试了,直接给出结论吧。
- concat 只是对数组的第一层进行深拷贝
- slice 只是对数组的第一层进行深拷贝
- Object.assign() 拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值
- ... 实现的是对象第一层的深拷贝。后面的只是拷贝的引用值
11.JS 中的 this
(1)this的指向
this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式(调用位置)。
(2)this的绑定规则
- 默认绑定
- 隐式绑定
- 显示绑定
- new绑定
默认绑定
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
foo 函数调用位置是在全局,因此 this 指向全局对象。foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定。如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定到 undefined。
隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
显示绑定
通过 call、apply 和 bind 方法进行绑定。
new绑定
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。
(3)绑定优先级
new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
二.JS面向对象
1.模拟new过程
new的执行过程如下:
- 创建一个新对象,它继承foo.prototype。目的是继承构造函数原型上的属性和方法;
- 执行构造函数,执行时传入相应的参数,同时上下文(this)会被指定为这个新实例。目的是执行构造函数内的赋值操作;
- 如果构造函数返回了一个对象,那么这个对象会取代整个new出来的结果。如果构造函数没有返回对象,那么new出来的结果为步骤1创建的对象。
具体代码如下:
function myNew(func, ...args) {
const obj = Object.create(func.prototype); // 第一步,创建一个对象
const res = func.apply(obj, args); // 第二步,执行构造函数
return typeof res === 'object' ? res : obj // 第三步,返回对象
}
// 测试
function Name(name) {
this.name = name
}
const res = myNew(Name, 'Tom');
console.log(res) // Name {name: "Tom"}
console.log(res instanceof Name) // true
2.继承(ES5实现)
(1)原型链方式
ECMAScript中将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
// 父类
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
// 子类
function SubType() {
this.subproperty = false;
}
// 实现继承
SubType.prototype = new SuperType();
// 添加新方法(注:需要放在继承之后,不然会被覆盖。当然,也不能以字面量的形式修改原型,否则继承无效)
SubType.prototype.getSubValue = function() {
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); // true
实现继承的本质是重写原型对象,代之一个新类型的实例。
原型链的问题:
问题一:引用类型值共享的问题
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType() {
}
// 继承
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push('black');
alert(instance1.colors); // ['red', 'blue', 'green', 'black']
var instance2 = new SubType();
alert(instance2.colors); // ['red', 'blue', 'green', 'black']
我们发现在实例instance1中添加成员“black”,结果在实例instance2中也存在,这往往不是我们想要的效果。
问题二:在创建子类型的实例时,不能向超类型的构造函数中传递参数
基于上述两个问题,导致我们在实践中很少会单独使用原型链来实现继承。
(2)借用构造函数
借用构造函数的思想:在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType() {
// 继承
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push('black');
alert(instance1.colors); // ['red', 'blue', 'green', 'black']
var instance2 = new SubType();
alert(instance2.colors); // ['red', 'blue', 'green']
我们在创建SubType实例时,调用了SuperType构造函数,所以,SubType实例都具有自己的colors属性副本。
传递参数
function SuperType(name) {
this.name = name;
}
function SubType() {
// 继承
SuperType.call(this, 'Tom');
// 实例属性
this.age = 2;
}
var instance = new SubType();
alert(instance.name); // "Tom"
alert(instance.age); // 2
借用构造函数的问题
在超类型的原型中定义的方法,对子类型而言是不可见的,结果所有类型都只能使用构造函数模式。而使用构造函数模式存在一个问题——每个方法都要在实例上重新创建一遍。所以,借用构造函数的技术也是很少单独使用的。
(3)组合继承
组合继承,又叫伪经典继承,是JavaScript中最常用的继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
alert(this.name);
};
function SubType(name, age) {
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType; // 修改构造器
SubType.prototype.sayAge = function() {
alert(this.age);
};
var instance1 = new SubType('Tom', 3);
instance1.colors.push('black');
alert(instance1.colors); // ['red', 'blue', 'green', 'black']
instance1.sayName(); // "Tom"
instance1.sayAge(); // 3
var instance2 = new SubType('Jerry', 2);
alert(instance2.colors); // ['red', 'blue', 'green']
instance2.sayName(); // "Jerry"
instance2.sayAge(); // 2
当然,还有其它几种继承方法:原型式继承、寄生式继承和寄生组合式继承,篇幅有限,这里就不在介绍了。
3.继承(ES6实现)
ES6中的Class可以通过extends关键字实现继承,这比ES5通过修改原型链更容易理解。
class Point {
constructor(x, y) {
this.x = x
this.y = y
}
toString() {
return this.x
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y) // 需要先调用父类的 constructor(x, y),不然下面的 this 会报错
this.color = color
}
toString() {
return this.color + ' ' + super.toString()
// 调用父类的 toString()
}
}
let cp = new ColorPoint(1, 2, 'red')
console.log(cp instanceof ColorPoint) // true
console.log(cp instanceof Point) // ture
console.log(cp.x) // 1
console.log(cp.toString()) // red 1
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。
ES5与ES6实现继承的差别:
ES5的继承实质是先创建子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创建父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
三.设计模式
设计模式有很多种,这里只讲几个常见的模式。
在开始了解设计模式前,有必要先了解一下设计原则。
1.设计原则
设计模式的原则有:单一职责原则、开放封闭原则(也称开闭原则)、里氏代换原则、合成复用原则、接口隔离原则和迪米特法则(也称为最小知识原则)。这里主要介绍三种:单一职责原则、开放封闭原则和最小知识原则。
(1)单一职责原则
就一个类而言,应该仅有一个引起它变化的原因。在JavaScript中,由于类使用得并不多,所以这更多体现在对象和方法上。
结论:一个对象或方法只做一件事情。
(2)开放封闭原则
开放封闭原则是最重要的一个原则,是指对扩展开放,对修改封闭。这能增加可维护性,避免因为修改给系统带来的不稳定性。
(3)最小知识原则
最少知识原则说的是一个软件实体应当尽可能少地与其他实体发生相互作用。
遵守设计原则的目的是实现高内聚低耦合的代码。但在实际开发中也不必刻板的去遵守这些原则,应根据实际情况灵活应用。
2.单例模式
单例模式定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的模式,有些对象往往只需要一个,比如线程池、全局缓存 、浏览器中的window对象等。
实现一个单例模式很简单,只需用一个变量来表示当前是否已经创建了某个类的实例,如果是,则在下一次获取该类实例时,返回之前创建的对象。
var Singleton = function(name) {
this.name = name;
}
Singleton.instance = null;
Singleton.prototype.getName = function() {
alert(this.name);
}
Singleton.getInstance = function(name) {
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
var a = Singleton.getInstance('sven1');
var b = Singleton.getInstance('sven2');
alert(a === b); // true
常用单例模式之惰性单例【推荐】
以登录弹框为例
// 将结果缓存起来,这里使用了闭包
var getSingle = function(fn) {
var result;
return function() {
return result || (result = fn.apply(this, arguments)); // 执行fn
}
};
var createLoginLayer = function() {
var div = document.createElement('div');
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
var createSingleLoginLayer = getSingle(createLoginLayer);
// 使用单例,让创建的结果(div)缓存起来,即createLoginLayer方法只执行了一次
document.getElementById('loginBtn').onclick = function() {
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};
上面代码的好处在于创建对象和管理单例的逻辑都分开了,这让我们的代码能更加复用。
下面我们可以在试试创建唯一的iframe用于加载第三方页面:
var createSingleIframe = getSingle(function() {
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
return iframe;
});
document.getElementById('loginBtn').onclick = function() {
var loginLayer = createSingleIframe();
loginLayer.src = 'https://baidu.com';
}
3.工厂模式
工厂模式 (Factory Pattern),根据不同的输入返回不同类的实例,一般用来创建同一类对象。工厂方式的主要思想是将对象的创建与对象的实现分离。
工厂模式有多种:简单工厂模式、工厂方法模式、抽象工厂模式,这里介绍简单工厂模式。
function createFood(menu) {
switch(menu) {
case '回锅肉': return new Food1();
case '红烧肉': return new Food2();
default: throw new Error('此菜本店没!')
}
}
function Food1() {
this.name = '回锅肉'
}
function Food2() {
this.name = '红烧肉'
}
var food1 = createFood('回锅肉'); // Food1 {name: "回锅肉"}
var food2 = createFood('粉蒸肉'); // Error: 此菜本店没!
4.策略模式
策略模式的定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
策略模式也是比较常用的模式,常用来改写 switch 语句。
下面例子使用策略模式实现计算奖金
// S, A, B 代表绩效等级
// salary 代表月薪
// 定义策略对象,封装算法
var strategies = {
'S': function(salary) {
return salary * 4;
},
'A': function(salary) {
return salary * 3;
},
'B': function(salary) {
return salary * 2;
}
}
// Context
var calculateBonus = function(level, salary) {
return strategies[level](salary)
}
console.log(calculateBonus('S', 20000)); // 80000
console.log(calculateBonus('A', 10000)); // 30000
策略模式的优点:
- 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
- 策略模式提供了对开放封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换,易于理解,易于扩散。
- 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
- 在策略模式中利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的代替方案。
策略模式的缺点:
- 要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy。此时strategy要向客户暴露它的所有实现,这是违反最少知识原则的。
5.代理模式
代理模式又称委托模式,它为目标对象创造了一个代理对象,以控制对目标对象的访问。
JavaScript中常用的代理模式有虚拟代理和缓存代理。
(1)虚拟代理
虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。
例子:用虚拟代理实现图片预加载
// 目标对象
var myImage = (function() {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function(src) {
imgNode.src = src;
}
}
})();
// 代理对象
var proxyImage = (function() {
var img = new Image;
// 图片加载完成后再替换
img.onload = function() {
myImage.setSrc(this.src);
}
return {
setSrc: function(src) {
myImage.setSrc('./loading.gif'); // 先设置loading图占位
img.src = src;
}
}
})();
proxyImage.setSrc('https://img1.sycdn.imooc.com/5d2446e9000175aa06400359.jpg');
当然,不使用代理模式也能实现上述效果,使用的好处在于更加符合单一职责原则,开闭原则。
(2)缓存代理
缓存代理可以为一些开销很大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回前面存储的运算结果。
例子:假如计算乘积是一个复杂的计算
// 计算方法
var mult = function() {
console.log('计算乘积');
var a = 1;
for(var i = 0, len = arguments.length; i < len; i++) {
a = a * arguments[i];
}
return a;
}
var proxyMult = (function() {
var cache = {};
return function() {
var args = Array.prototype.join.call(arguments, ','); // 将传递的参数拼接为逗号分隔的字符串
if (args in cache) {
return cache[args]; // 返回缓存中的值
}
return cache[args] = mult.apply(this, arguments); // 返回值并存入缓存
}
})();
proxyMult(1,2,3,4); // 24
proxyMult(1,2,3,4); // 24 此次并未计算,而是从缓存中获取
6.发布订阅模式
发布订阅模式也叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。DOM 事件就是发布订阅模式。再比如 Vue 中视图与数据的双向绑定也是发布订阅模式。
这里以购房者去售楼处买房为例。购房者在售楼处留下电话,当新楼盘开售后,售楼处就会给购房者发短信通知。
售楼处充当发布者,购房者充当订阅者。
var salesOffices = {}; // 定义售楼处
salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数
salesOffices.listen = function(fn) { // 增加订阅者
this.clientList.push(fn); // 订阅的消息添加进缓存列表
};
salesOffices.trigger = function() { // 发布消息
for (var i = 0, fn; fn = this.clientList[i++];) {
fn.apply(this, arguments); // arguments 是发布消息时带上的参数
}
};
// 测试
salesOffices.listen(function(price, squareMeter) { // 小明订阅消息
console.log('价格 ' + price);
console.log('面积 ' + squareMeter);
});
salesOffices.listen(function(price, squareMeter) { // 小红订阅消息
console.log('价格 ' + price);
console.log('面积 ' + squareMeter);
});
salesOffices.trigger(2000000, 88);
// 价格 2000000
// 面积 88
// 价格 2000000
// 面积 88
salesOffices.trigger(3000000, 100);
// 价格 3000000
// 面积 100
// 价格 3000000
// 面积 100
不难看出,这个例子还有很多问题。比如,这里每个订阅者都会接受到所有消息,但小明只买88平米的房子,其他消息就没有必要发送了。
下面 ,看看一个通用的发布订阅模式。
var event = {
clientList: {},
listen: function(key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn); // 订阅消息添加进缓存
},
trigger: function() {
var key = Array.prototype.shift.call(arguments), // 取得第一个参数
fns = this.clientList[key];
if (!fns || fns.length === 0) { // 如果没有绑定对应的消息
return false;
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments); // arguments 是执行 trigger 时的参数
}
}
};
// 给对象安装发布订阅模式的方法
var installEvent = function(obj) {
for (var i in event) {
obj[i] = event[i];
}
};
// 测试
var salesOffices = {};
installEvent(salesOffices);
salesOffices.listen('squareMeter88', function(price) { // 小明订阅
console.log('价格 ' + price);
});
salesOffices.listen('squareMeter100', function(price) { // 小红订阅
console.log('价格 ' + price);
});
salesOffices.trigger('squareMeter88', 2000000); // 价格 2000000
salesOffices.trigger('squareMeter100', 3000000); // 价格 3000000
7.装饰者模式
装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。
以飞机大战为例,飞机在升级过程中会发射不同子弹。
var plane = {
fire: function(){
console.log( '发射普通子弹' );
}
}
var missileDecorator = function(){
console.log( '发射导弹' );
}
var atomDecorator = function(){
console.log( '发射原子弹' );
}
var fire1 = plane.fire;
plane.fire = function(){
fire1();
missileDecorator();
}
var fire2 = plane.fire;
plane.fire = function(){
fire2();
atomDecorator();
}
plane.fire(); // 分别输出:发射普通子弹、发射导弹、发射原子弹
再看个实际例子,比如我们想给 window 绑定 onload 事件,但是又不确定这个事件是不是已经被其他人绑定过,为了避免覆盖掉之前的 window.onload 函数中的行为,我们一般都会先保存好原先的 window.onload,把它放入新的 window.onload 里执行:
window.onload = function(){
console.log(1);
}
var _onload = window.onload || function(){};
window.onload = function(){
_onload();
console.log(2);
}
这样,代码就符合了开闭原则。
四.算法
这里主要介绍排序算法
1.冒泡排序
function bubbleSort (arr) {
// 冒泡排序
for (let i = arr.length - 1, tmp; i > 0; i--) {
for (let j = 0; j < i; j++) {
// 由小到大,升序排序
if (arr[j] > arr[j + 1]) {
tmp = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = tmp
}
}
}
return arr
}
let arr = [3, 2, 4, 9, 1, 5, 7, 6, 8]
let arrSorted = bubbleSort(arr)
console.log(arrSorted)
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
2.优化冒泡排序
function bubbleSort (arr) {
// 冒泡排序
for (let i = arr.length - 1, tmp; i > 0; i--) {
let flag = true
for (let j = 0; j < i; j++) {
// 由小到大,升序排序
if (arr[j] > arr[j + 1]) {
flag = false
tmp = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = tmp
}
}
if (flag) break; // 没交换元素,排序完成
}
return arr
}
let arr = [3, 2, 4, 9, 1, 5, 7, 6, 8]
let arrSorted = bubbleSort(arr)
console.log(arrSorted)
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
3.选择排序
function SelectionSort(arr) {
// 选择排序,每次选择最小值
for (let i = 0; i < arr.length - 1; i++) {
let minIndex = i
for (let j = i + 1; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex
}
// 由小到大排序,把最小值放在前面
let temp = arr[i]
arr[i] = arr[minIndex]
arr[minIndex] = temp
}
return arr
}
let arr = [3, 2, 4, 9, 1, 5, 7, 6, 8]
let arrSorted = SelectionSort(arr)
console.log(arrSorted)
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 待完善...
五.HTTP网络/通信
1.GET和POST的区别
- GET在浏览器回退时是无害的,而POST会再次提交请求
- GET产生的URL地址可以被收藏,而POST不可以
- GET请求会被浏览器主动缓存,而POST不会,除非手动设置
- GET请求只能进行url编码,而POST支 持多种编码方式
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留
- GET请求在URL中传送的参数是有长度限制的,而POST没有限制对参数的数据类型
- GET只接受ASCII字符,而POST没有限制
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息
- GET参数通过URL传递,POST放在Request body中
2.HTTP方法
- GET 获取资源
- POST 传输资源
- PUT 更新资源
- DELETE 删除资源
- HEAD 获得报文首部
3.HTTP状态码
- 1xx:指示信息-表示请求已接收,继续处理
- 2xx:成功-表示请求已被成功接收
- 3xx:重定向-要完成请求必须迸行更迸一步的操作
- 4xx:客戸端错误-请求有语法错误或请求无法实现
- 5xx:服各器错误-服务器未能实现合法的请求
常见状态码
- 200 OK:客户端请求成功
- 206 Partial Content:客户发送了一个带有Range头的GET请求,服务器完成了它
- 301 Moved Permanently:所请求的页面已经转移至新的url
- 302 Found:所请求的页面已经临时转移至新的url
- 304 Not Modified:客户端有缓冲的文档并发出了一个条件性的请求,服务器告诉客户,原来缓冲的文档还可以继续使用
- 400 Bad Request:客户端请求有语法错误,不能被服务器所理解
- 401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
- 403 Forbidden:对被请求页面的访问被禁止
- 404 Not Found:请求资源不存在
- 500 Internal Server Error:服务器发生不可预期的错误,原来缓冲的文档还可以继续使用
- 503 Server Unavailable:请求未完成,服务器临时过载或当机,一段时间后可能恢复正常
4.前后端如何通信
- Ajax
- WebSocket
- CORS
Ajax
Ajax 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。
Ajax 常见应用:运用 XMLHttpRequest 或新的 Fetch API 与网页服务器进行异步资料交换。
这里我们手写一个简单的 XHR 请求过程
function request() {
// 1.获取一个 XHR 实例
const xhr = new XMLHttpRequest()
// 2.初始化请求
xhr.open('POST', 'http://192.168.3.195:8088/setUsername', true)
// 3.事件处理器 在每次 readyState 属性变化时被自动调用
xhr.onreadystatechange = function() {
// 请求完成且状态为 200
if (xhr.readyState == 4 && xhr.status == 200) {
// 成功
// 得到服务端返回的数据
let res = JSON.parse(xhr.responseText)
} else {
// 其他情况
}
}
// 设置请求头
xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8')
xhr.setRequestHeader('Accept', 'application/json, text/plain, */*')
// 允许跨域
xhr.withCredentials = true
// 设置发送的数据
const requestData = JSON.stringify({
username: 'Tom'
})
// 4.发送请求
xhr.send(requestData)
}
WebSocket
是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
常见应用:客服系统、物联网数据传输系统等。
推荐教程:WebSocket 教程 - 阮一峰
CORS
CORS 全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了AJAX只能同源使用的限制。
5.跨域通信及其方式
跨域通信
协议、IP和端口只要其中之一不同就算跨域。
跨域通信方式
- Jsonp
- Hash
- postMessage
- WebSocket
- CORS
// 待完善...
六.前端安全
1.CSRF,跨站请求伪造
定义
CSRF即Cross-site request forgery(跨站请求伪造),是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
防御
- 添加校验token。系统开发人员可以在HTTP请求中以参数的形式加入一个随机产生的token,并在服务端进行token校验,如果请求中没有token或者token内容不正确,则认为是CSRF攻击而拒绝该请求。
- 尽量使用POST,限制GET。POST请求有一定作用,但并不一定完全安全。
- 检查Referer字段。通过验证请求头的Referer来验证来源站点,但请求头很容易伪造。
2.XSS,跨域脚本攻击
定义
跨站脚本攻击是指恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。
防御
- 输入检查:对输入内容中的
<script><iframe>
等标签进行转义或者过滤 - 设置httpOnly:很多XSS攻击目标都是窃取用户cookie伪造身份认证,设置此属性可防止JS获取cookie
- 开启CSP,即开启白名单,可阻止白名单以外的资源加载和运行
七.渲染机制
1.浏览器渲染原理与过程
主要包括以下五步:
- 浏览器将获取的HTML文档解析成DOM树
- 处理CSS标记,构成层叠样式表模型CSSOM(CSS Object Model)
- 将DOM和CSSOM合并为渲染树(
rendering tree
) - 渲染树的每个元素包含的内容都是计算过的,它被称之为布局
layout
。浏览器使用一种流式处理的方法,只需要一次绘制操作就可以布局所有的元素 - 将渲染树的各个节点绘制到屏幕上,这一步被称为绘制
paintin
2.重排 Reflow(也叫回流)
定义
DOM结构中的各个元素都有自己的盒子(模型),这些都需要浏览器根据各种样式来计算并根据计算结果将元素放到它该出现的位置,这个过程称之为reflow。
触发Reflow
- 当你增加、删除、修改DOM结点时,会导致Reflow或Repaint
- 当你移动DOM的位置,或是搞个动画的时候
- 当你修改CSS样式的时候
- 当你Resize窗口的时候(移动端没有这个问题), 或是滚动的时候
- 当你修改网页的默认字体时
3.重绘 Repaint
定义
当我们改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸和位置没有发生改变,这个称为repaint。重绘的性能优于重排(回流)。
4.减少reflow、repaint触发次数
- 用 transform 做形变和位移可以减少 reflow
- 避免逐个修改节点样式,尽量一次性修改
- 使用 DocumentFragment 将需要多次修改的 DOM 元素缓存,最后一次性 append 到真实 DOM 中渲染
- 可以将需要多次修改的 DOM 元素设置 display:none,操作完再显示。(因为隐藏元素不在 render 树内,因此修改隐藏元素不会触发回流重绘)
- 避免多次读取某些属性
- 通过绝对位移将复杂的节点元素脱离文档流,形成新的 Render Layer,降低回流成本
八.性能优化
1.从输入 URL 到页面加载完成,发生了什么?
(1)DNS(域名解析系统)将 URL 解析为对应的 IP 地址,建立起 TCP 网络连接(三次握手)
(2)发送 HTTP 请求
(3)接受 HTTP 响应
(4)渲染页面,构建DOM树
(5)关闭TCP连接(四次挥手)
2.前端性能优化比较常见的一些点:
- 使用缓存(最重要的一点),cookie与WebStorage
- 减少http请求
- 文件压缩合并:html,js,css压缩。删除一些无用代码:注释
- 图片无损压缩,安卓下可以使用webp格式图片
- 使用字体图标,矢量图svg,雪碧图,base64
- js文件一般放在页面底部,若放在head里一般要在 script 标签上加 async 或者 defer 进行异步加载
- 懒加载,预加载
- 服务端渲染(SSR),增强页面的呈现速度,同时能增强SEO,但页面切换不如单页应用(SPA)流畅
- 减少dom操作,规避重绘与回流;更改css属性较多时,以类名的形式操作dom;有复杂动画的DOM,可以考虑使其脱离文档流,从而减少重绘与回流的范围
- 事件的节流与防抖
- 事件委托。利用事件冒泡,通过指定一个事件处理程序,来管理某一类型的所有事件。这样能减少页面消耗的内存
- 启用DNS预解析。遇到网页中的超链接,DNS prefetching从中提取域名并将其解析为IP地址,这些工作在用户浏览网页时,使用最少的CPU和网络在后台进行解析
- 若使用了闭包,当功能完成后,最好将其置null,以释放内部变量占用的内存
九.CSS
1.CSS 选择器及优先级
- !important
- 内联样式
- ID选择器
- 类选择器/属性选择器/伪类选择器
- 元素选择器/伪元素选择器
- 关系选择器/通配符选择器
2.清除浮动
目的:解决子元素浮动导致父元素高度塌陷的问题
(1)伪类 after 和 zoom
.clear-float:after {
display: block;
clear: both;
content: "";
}
.clear-float {
zoom: 1;
}
推荐,兼容性好
(2)before 和 after
.clear-float:before,
.clear-float:after{
content: '';
display: block;
}
.clear-float:after {
clear: both;
}
(3)结尾处添加一个空 div
<div class="parent">
<div class="left"></div>
<!-- 通过在末尾添加元素的方式, 利用 clear:both 清除浮动 -->
<div style="clear:both"></div>
</div>
(4)父级 overflow: hidden
通过给父级添加 overflow: hidden 属性, 触发 BFC
3.flex布局
flex布局非常强大好用,在移动端我们会大量使用,pc端支持也越来越好了。
我们需要掌握:
容器的属性
flex-direction,justify-content,align-items,flex-wrap,flex-flow,align-content
容器中项目的属性
order,flex-grow,flex-shrink,flex-basis,flex,align-self
还需要明白什么是主轴,什么是交叉轴.
相关文章可看我以前的文章:
4.常用居中布局
这里就介绍几种常用的水平垂直居中布局。
(1)使用定位
.div1 {
width: 300px;
height: 300px;
border: 1px solid red;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
margin: auto;
}
(2)使用定位加margin
.div1 {
width: 300px;
height: 300px;
border: 1px solid red;
position: absolute;
top: 50%;
left: 50%;
margin-top: -150px;
margin-left: -150px;
}
(3)使用定位加translate
.div1 {
width: 300px;
height: 300px;
border: 1px solid red;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
}
(4)使用flex布局
.div1 {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 500px;
border: 1px solid red;
}
.div2 {
width: 300px;
height: 300px;
border: 1px solid red;
}
5.BFC
BFC(Block formatting context)块级格式化上下文。具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,也不会受外部元素的影响。
BFC是一种边距重叠的解决方案,比如我们在写代码的时候会发现左右或者上下两个元素都设有边距,但往往出现边距之间取最大值作为两个元素之间的边距,而不是取两者的边距之和。
如何创建BFC ?
(1)float不为none
(2)position设置为absolute,fixed
(3)display设置为table-cell,table-caption,inline-block, flex, inline-flex
(4)overflow设置为除了visible以外的值,即hidden,auto,scroll
(5)body根元素
BFC的应用
(1)清除浮动,BFC可以包含浮动的元素,清除浮动只需要将父元素变成一个BFC即可,常用的方法是给父元素设置overflow:hidden
(2)解决边距重叠,同一BFC下外边距会发生合并,若想避免外边距合并,可以将其放在不同的BFC容器中
(3)阻止元素被浮动元素覆盖,创建自适应两栏布局,左边宽度固定,右边自适应宽度
6.盒模型
标准模型
标准模型的宽高就是内容 content 部分的宽高。
IE模型(怪异模型)
IE模型中的宽高包含padding(内边距)和border(边框),这是两者的区别所在。
标准模型与IE模型之间的转换
标准模型 ==> IE模型 :box-sizing: border-box;
IE模型 ==> 标准模型 :box-sizing: content-box;
7.三栏布局/圣杯布局/双飞翼布局
这里给了五种实现方法
初始代码
<style media="screen">
html * {
padding: 0;
margin: 0;
}
.layout article div{
min-height: 100px;
}
.layout{
margin-bottom: 10px;
}
</style>
(1)浮动解决方案
<section class="layout float">
<style media="screen">
.layout.float .left{
float: left;
width: 300px;
background: red;
}
.layout.float .right{
float: right;
width: 300px;
background: blue;
}
.layout.float .center{
background: yellow;
}
</style>
<article class="left-right-center">
<div class="left"></div>
<div class="right"></div>
<div class="center">
<h1>浮动解决方案</h1>
1.这是三栏布局的中间部分
</div>
</article>
</section>
(2)绝对定位解决方案
<section class="layout absolute">
<style>
.layout.absolute .left-center-right>div{
position: absolute;
}
.layout.absolute .left{
left: 0;
width: 300px;
background: red;
}
.layout.absolute .center{
left: 300px;
right: 300px;
background: yellow;
}
.layout.absolute .right{
right: 0;
width: 300px;
background: blue;
}
</style>
<article class="left-center-right">
<div class="left"></div>
<div class="center">
<h1>绝对定位解决方案</h1>
1.这是三栏布局的中间部分</div>
<div class="right"></div>
</article>
</section>
(3)flexbox解决方案
<section class="layout flexbox">
<style>
.layout.flexbox{
margin-top: 120px;
}
.layout.flexbox .left-center-right{
display: flex;
}
.layout.flexbox .left{
width: 300px;
background: red;
}
.layout.flexbox .center{
flex: 1;
background: yellow;
}
.layout.flexbox .right{
width: 300px;
background: blue;
}
</style>
<article class="left-center-right">
<div class="left"></div>
<div class="center">
<h1>flexbox解决方案</h1>
1.这是三栏布局的中间部分
</div>
<div class="right"></div>
</article>
</section>
(4)表格布局解决方案
<section class="layout table">
<style>
.layout.table .left-center-right{
width: 100%;
display: table;
height: 100px;
}
.layout.table .left-center-right>div{
display: table-cell;
}
.layout.table .left{
width: 300px;
background: red;
}
.layout.table .center{
background: yellow;
}
.layout.table .right{
width: 300px;
background: blue;
}
</style>
<article class="left-center-right">
<div class="left"></div>
<div class="center">
<h1>表格布局解决方案</h1>
1.这是三栏布局的中间部分
</div>
<div class="right"></div>
</article>
</section>
(5)网格布局解决方案
<section class="layout grid">
<style>
.layout.grid .left-center-right{
display: grid;
width: 100%;
grid-template-rows: 100px;
grid-template-columns: 300px auto 300px;
}
.layout.grid .left{
background: red;
}
.layout.grid .center{
background: yellow;
}
.layout.grid .right{
background: blue;
}
</style>
<article class="left-center-right">
<div class="left"></div>
<div class="center">
<h1>网格布局解决方案</h1>
1.这是三栏布局的中间部分
</div>
<div class="right"></div>
</article>
</section>
简单分析: 一二两种方法(浮动,绝对定位)是比较容易想到的,但需要高度做支撑,这里的绝对定位方式会导致父级高度为零;第三种方法(flex布局)是移动端布局经常用到的一种布局方式,pc端要考虑兼容性;第四种方法(表格布局)是一种比较老的方法了,虽然用得少了,但效果不错;第五种方法(网格布局)是新的一种布局方式,需要多了解。
十.浏览器
1.浏览器内核
- IE:trident 内核
- Firefox:gecko 内核
- Safari:webkit 内核
- Opera:以前是 presto 内核,现在为 Blink 内核
- Chrome:Blink 内核(基于 webkit)
后续
知识并不是平白无故产生的,我在整理这篇文章时也参考和学习了一些优秀的文章,书籍,视频。
【参考文章】
【参考书籍】
《JavaScript高级程序设计(第三版)》
《JavaScript设计模式与开发实践》
《ES6标准入门(第三版)》
《你不知道的JavaScript(上册)》
【参考视频】
前端跳槽面试必备技巧(可惜下架了)
// 还有很多内容还没来得及补充和完善,后续再加上...