知识点都是搜集各种大佬们的,如有冒犯,请告知!
目录
原型链
New关键字的执行过程
ES6——class
constructor方法
类的实例对象
不存在变量提升
super 关键字
ES6——...(展开/收集)运算符
面向对象的理解
关于this对象
箭头函数
匿名函数
闭包
内存泄露
JavaScript垃圾回收机制
引用计数算法
循环引用
解决方法:
标记清除算法
如何写出对内存管理友好的JS代码?
提升性能有关
移动开发
数组方法集合
js小技巧
类型强制转换
string 强制转换为数字
object强制转化为string
使用 Boolean 过滤数组中的所有假值
双位运算符 ~~
取整 |0
判断奇偶数 &1
函数
强制参数
惰性载入函数
一次性函数
精确到指定位数的小数
数组
reduce 方法同时实现 map 和 filter
统计数组中相同项的个数
使用解构来交换参数数值
接收函数返回的多个结果
代码复用
Object [key]
解读HTTP/1、HTTP/2、HTTP/3之间的关系
HTTP 协议
HTTP之请求消息Request
HTTP之响应消息Response
HTTP之状态码
HTTP/1.x 的缺陷
HTTPS 应声而出(安全版的http)
SPDY 协议
HTTP2.0 的前世今生
HTTP2.0 的新特性
HTTP2.0 的升级改造
HTTP/2 的缺点
HTTP/3简介
3.QUIC新功能
七、总结
Session
Token
MVC、MVP和MVVM
MVC:Model+View+Controller
MVP
MVVM(以VUE.JS来举例)
# View 层
# Model 层
# ViewModel 层
VUE.JS
vue生命周期(钩子函数)
computed中的getter和setter
v-for循环key的作用
$nextTick
$set
组件间的传值
为什么要是用sass这些css预处理器
优势
如何防止XSS攻击?
跨域
什么是同源策略及其限制内容
跨域解决方案
三、总结
web语义化
分为html语义化、css语义化和url语义化
nodeJS
模块加载机制
CommonJS
AMD规范与CommonJS规范的兼容性
AMD
原型链
(总结:每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( __proto__ ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
而提到原型对象,就涉及到了两个属性:_proto_和prototype,前者是实例或者说是对象都会有的属性,他指向了实例的构造函数上的prototype属性,也就是:实例._proto_ = 构造函数.prototype,而这个prototype上就保存着需要实例间共享的属性和方法。
为什么要这样操作呢,因为js他没有想java那样有class类,而ES6上的class更多的是提供语法糖,所以js就利用函数来模拟类
当你创建一个函数(构造函数)的时候,js会自动为该函数添加prototype属性,他不是一个空对象,他有个constructor属性,指向了构造函数,而当你new一个实例的时候,该实例就会默认继承构造函数上的prototype上的所有属性和方法以达到继承目的。)
- 每个JS对象一定对应一个原型对象,并从原型对象继承属性和方法
- 实例.__proto__ = 构造函数.prototype(只有函数才有prototype属性。)
- 对象(实例)__proto__([[prototype]])属性的值就是它的构造函数所对应的原型对象(prototype):
var one = {x: 1};
var two = new Object();
one.__proto__ === Object.prototype // true
two.__proto__ === Object.prototype // true
one.toString === one.__proto__.toString // true
JS不像其它面向对象的语言,它没有类(
class,ES6引进了这个关键字,但更多是语法糖)的概念。JS通过函数来模拟类。- 当你创建函数时,JS会为这个函数自动添加
prototype属性,值是空对象值是一个有 constructor 属性(是一个指向prototype属性所在函数的指针)的对象,不是空对象。- 而一旦你把这个函数当作构造函数(
constructor)调用(即通过new关键字调用),那么JS就会帮你创建该构造函数的实例,实例继承构造函数prototype的所有属性和方法(实例通过设置自己的__proto__指向构造函数的prototype来实现这种继承)。
- 构造函数,通过
prototype来存储要共享的属性和方法,也可以设置prototype指向现存的对象来继承该对象。(Object.getPrototypeOf()可以方便的取得一个对象的原型对象)原型最初只包含constructor属性,而该属性也是共享的,因此可以通过对象实例访问,如果原型被覆盖,属性查询就会往之上一直查询
补充:
·Person.prototype.constructor = person.__proto__.constructor
·Object.prototype(root)<---Function.prototype<---Function|Object|Array...
选读:
Object本身是构造函数,继承了Function.prototype ;
Function也是对象,继承了Object.prototype。这里就有一个_鸡和蛋_的问题:
Object instanceof Function // trueFunction instanceof Object // true
Function本身就是函数,Function.__proto__是标准的内置对象Function.prototype。
Function.prototype.__proto__是标准的内置对象Object.prototype。
- Function.prototype和Function.__proto__都指向Function.prototype,这就是鸡和蛋的问题怎么出现的。
- Object.prototype.__proto__ === null,说明原型链到Object.prototype终止。
最后总结:先有Object.prototype(原型链顶端),Function.prototype继承Object.prototype而产生,最后,Function和Object和其它构造函数继承Function.prototype而产生。
参考:从__proto__和prototype来深入理解JS对象和原型链
New关键字的执行过程
即通过new创建对象经历4个步骤
- 创建一个新对象;[var o = {};]
- 链接该对象(即设置该对象的构造函数)到另一个对象 (构造函数的原型对象上);【更改实例(p)的_proto_指向构造函数(P)的prototype,这个步骤会弄丢构造函数原型对象上的constructor,改正构造函数(P)的在prototype上的constructor指向他自己(P)】
- 将新创建的对象作为this的上下文 ,执行构造函数中的代码(为这个新对象添加属性);【[Person.apply(o)] [Person原来的this指向的是window]】
- 如果该函数没有返回对象,则返回this(即使用步骤1创建的对象。)。
创建一个用户自定义的对象需要两步:
- 通过编写函数来定义对象类型。
- 通过 new 来创建对象实例。
function Person(name, age, job) {this.name = name;this.age = age;this.job = job;this.sayName = function () {alert(this.name);};}let New = function (P) {let o = {};let arg = Array.prototype.slice.call(arguments,1);o.__proto__ = P.prototype;P.prototype.constructor = P; // 先将环境配置好_proto_、constructorP.apply(o,arg); // 将参数赋予给新对象return o;};let p1 = New(Person,"Ysir",24,"stu");let p2 = New(Person,"Sun",23,"stus");console.log(p1.name);//Ysirconsole.log(p2.name);//Sunconsole.log(p1.__proto__ === p2.__proto__);//trueconsole.log(p1.__proto__ === Person.prototype);//true
当代码 new Foo(...) 执行时,会发生以下事情:
- 一个继承自 Foo
.prototype的新对象被创建。- 使用指定的参数调用构造函数
Foo,并将 this 绑定到新创建的对象(所以new关键字可以改变this的指向,将这个this指向实例对象)。newFoo 等同于new Foo(),也就是没有指定参数列表,Foo不带任何参数调用的情况。- 由构造函数返回的对象就是
new表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)
ES6——class
(总结:ES6的class类其实就是等同于ES5的构造函数,作用都是生成新对象,只不过class在结构形式上会和传统的面向对象语言如c++,java这些更类似、切合。因为传统构造方法在方法实现上与面向对象编程差别很大,为了让对象原型的写法更加清晰、更像面向对象编程,故引入class(类)这个概念,以作为对象的模板。 Class的内部固定搭配constructor方法,即构造方法,他默认返回实例对象,而直接在内部定义的方法会被绑定到构造函数的prototype对象上。而且不可直接调用class声明的构造函数,会抛出错误,同样地需要new一个实例里面的方法和属性才能被调用)
- JavaScript语言的传统方法是通过构造函数定义来生成新对象。但是构造函数跟传统的面向对象语言(比如C++和Java)写法上差异很大,很容易让新学习这门语言的程序员感到困惑。
- ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过
class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
function Point(x, y) { //构造函数this.x = x;this.y = y;
}Point.prototype.toString = function () {return '(' + this.x + ', ' + this.y + ')';
};var p = new Point(1, 2);
//定义类
class Point {
// ES5的构造函数Point,对应ES6的Point类的构造方法constructor(x, y) { //构造方法this.x = x; // this关键字则代表实例对象this.y = y;}toString() {return '(' + this.x + ', ' + this.y + ')';}
}
- ES6的类,完全可以看作构造函数的另一种写法。
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
constructor方法
constructor 方法是类的构造函数,是一个默认方法,通过 new 命令创建对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个默认的 consructor 方法会被默认添加。所以即使你没有添加构造函数,也是会有一个默认的构造函数的。一般 constructor 方法返回实例对象 this ,但是也可以指定 constructor 方法返回一个全新的对象,让返回的实例对象不是该类的实例。
class Foo {constructor() {return Object.create(null);}
}new Foo() instanceof Foo
// false
类的构造函数,不使用new是没法调用的,会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
类的实例对象
与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
//定义类
class Point {constructor(x, y) {this.x = x;this.y = y;}toString() {return '(' + this.x + ', ' + this.y + ')';}}var point = new Point(2, 3);point.toString() // (2, 3)point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
上面代码中,x和y都是实例对象point自身的属性(因为定义在this变量上),所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Point类上),所以hasOwnProperty方法返回false。这些都与ES5的行为保持一致。
不存在变量提升
Class不存在变量提升(hoist),这一点与ES5完全不同。
new Foo(); // ReferenceError
class Foo {}
super 关键字
super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求在 constructor 中必须调用 super 方法,因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工,而 super 就代表了父类的构造函数。super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B,因此 super() 在这里相当于 ```A.prototype.constructor.call(this, props)``。
class A {}class B extends A {constructor() {super();}
}
上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。
注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,因此super()在这里相当于A.prototype.constructor.call(this)。
class A {constructor() {console.log(new.target.name);}
}
class B extends A {constructor() {super();}
}
new A() // A
new B() // B
上面代码中,new.target指向当前正在执行的函数。可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B。
作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。
参考:理解 es6 class 中 constructor 方法 和 super 的作用
(可忽略!)第二种情况,super作为对象时,指向父类的原型对象。这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。如果属性定义在父类的原型对象上,super就可以取到。
class A {constructor() {this.a = 'aa'}p() {return 2;}}class B extends A {constructor() {//super === class A//super作为函数调用时,代表父类的构造函数//super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,//因此super()在这里相当于A.prototype.constructor.call(this)。super(); // super() === constructor A():A === B {a: "aa"}console.log(super.a,'super')//undefined 只有实例才会继承那些this的自身属性console.log(super.p()); // 2}}let b = new B();
console.log(b.a,'a')//aaconsole.log(b.p(),'p')//2
参考:ES6 Class
ES6——...(展开/收集)运算符
简而言之就是,... 运算符可以展开一个可迭代对象的所有项。
可迭代的对象一般是指可以被循环的,包括:string, array, set 等等。
基础用法 1: 展开
const a = [2, 3, 4]
const b = [1, ...a, 5]
b; // [1, 2, 3, 4, 5]
基础用法 2: 收集
function foo(a, b, ...c) {console.log(a, b, c)
}foo(1, 2, 3, 4, 5); // 1, 2, [3, 4, 5]
如果没有命名参数的话,... 就会收集所有的参数:
function foo(...args) {console.log(args)
}foo(1, 2, 3, 4, 5); // [1, 2, 3, 4, 5]
作为收集运算符时候,一定是在最后一个参数的位置,也很好理解,就是“收集前面剩下的参数”。
基础用法 3: 把 类数组 转换为 数组
const nodeList = document.getElementsByClassName("test");
const array = [...nodeList];console.log(nodeList); //Result: HTMLCollection [ div.test, div.test ]
console.log(array); //Result: Array [ div.test, div.test ]
基础用法 5: 合并数组/对象
const baseSquirtle = {name: 'Squirtle',type: 'Water'
};const squirtleDetails = {species: 'Tiny Turtle Pokemon',evolution: 'Wartortle'
};const squirtle = { ...baseSquirtle, ...squirtleDetails };
console.log(squirtle);
//Result: { name: 'Squirtle', type: 'Water', species: 'Tiny Turtle Pokemon', evolution: 'Wartortle' }
注意:当对像或者数组中有嵌套引用数据时,用...运算符进行赋值或者合并时属于浅复制,原数据改了,用到他的地方也会跟着改。
- 浅克隆之所以被称为浅克隆,是因为对象只会被克隆最外部的一层,至于更深层的对象,则依然是通过引用指向同一块堆内存.
- 深复制:他会把引用类型指针所指的那块内存地址复制出一块新空间,也就是新对象和旧对象所指的内存空间是不一样的,也就不存在原数据改了,复制对象也会跟着被修改
深复制的方法有:
JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,这两个方法结合起来就能产生一个便捷的深克隆.
const newObj = JSON.parse(JSON.stringify(oldObj));
确实,这个方法虽然可以解决绝大部分是使用场景,但是却有很多坑.
- 他无法实现对函数 、RegExp等特殊对象的克隆
- 会抛弃对象的constructor,所有的构造函数会指向Object
- 对象有循环引用,会报错
详细请看:面试官:请你实现一个深克隆
补充:
Object.create(proto[, propertiesObject])
===等同于下面
function object(o){function F(){}F.propotype = o return new F()
}
参数
proto——新创建对象的原型对象。
propertiesObject——可选。如果没有指定为 undefined,则是要添加到新创建对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()的第二个参数。
返回值
一个新对象,带着指定的原型对象和属性。
也就是说,Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
参考:深入了解 ES6 强大的 ... 运算符
ES6——promise
含义
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署Promise更好的选择。
基本用法
ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。
下面代码创造了一个Promise实例。
const promise = new Promise(function(resolve, reject) {// ... some codeif (/* 异步操作成功 */){resolve(value);} else {reject(error);}
});
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
romise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
promise.then(function(value) {// success
}, function(error) {// failure
});
Promise 新建后就会立即执行。
let promise = new Promise(function(resolve, reject) {console.log('Promise');resolve();
});promise.then(function() {console.log('resolved.');
});console.log('Hi!');// Promise
// Hi!
// resolved
上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。
下面是一个用Promise对象实现的 Ajax 操作的例子。
const getJSON = function(url) {const promise = new Promise(function(resolve, reject){const handler = function() {if (this.readyState !== 4) {return;}if (this.status === 200) {resolve(this.response);} else {reject(new Error(this.statusText));}};const client = new XMLHttpRequest();client.open("GET", url);client.onreadystatechange = handler;client.responseType = "json";client.setRequestHeader("Accept", "application/json");client.send();});return promise;
};getJSON("/posts.json").then(function(json) {console.log('Contents: ' + json);
}, function(error) {console.error('出错了', error);
});
resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样。
const p1 = new Promise(function (resolve, reject) {// ...
});const p2 = new Promise(function (resolve, reject) {// ...resolve(p1);
})
上面代码中,p1和p2都是 Promise 的实例,但是p2的resolve方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。
注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。
const p1 = new Promise(function (resolve, reject) {setTimeout(() => reject(new Error('fail')), 3000)
})const p2 = new Promise(function (resolve, reject) {setTimeout(() => resolve(p1), 1000)
})p2.then(result => console.log(result)).catch(error => console.log(error))
// Error: fail
上面代码中,p1是一个 Promise,3 秒之后变为rejected。p2的状态在 1 秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。
注意,调用resolve或reject并不会终结 Promise 的参数函数的执行。
new Promise((resolve, reject) => {resolve(1);console.log(2);
}).then(r => {console.log(r);
});
// 2
// 1
上面代码中,调用resolve(1)以后,后面的console.log(2)还是会执行,并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
一般来说,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。
new Promise((resolve, reject) => {return resolve(1);// 后面的语句不会执行console.log(2);
})
Promise.prototype.then()
Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。
getJSON("/post/1.json").then(function(post) {return getJSON(post.commentURL);
}).then(function (comments) {console.log("resolved: ", comments);
}, function (err){console.log("rejected: ", err);
});
上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为resolved,就调用第一个回调函数,如果状态变为rejected,就调用第二个回调函数。
如果采用箭头函数,上面的代码可以写得更简洁。
getJSON("/post/1.json").then(post => getJSON(post.commentURL)
).then(comments => console.log("resolved: ", comments),err => console.log("rejected: ", err)
);
Promise.prototype.catch()
Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获
如果 Promise 状态已经变成resolved,再抛出错误是无效的。所以一般都是直接return (resolve或者是reject回调函数)
const promise = new Promise(function(resolve, reject) {resolve('ok');throw new Error('test');
});
promise.then(function(value) { console.log(value) }).catch(function(error) { console.log(error) });
// ok
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
getJSON('/post/1.json').then(function(post) {return getJSON(post.commentURL);
}).then(function(comments) {// some code
}).catch(function(error) {// 处理前面三个Promise产生的错误
});
一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。
跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。(即错误会抛出,但是不影响运行,之后的代码照常运行)
const someAsyncThing = function() {return new Promise(function(resolve, reject) {// 下面一行会报错,因为x没有声明resolve(x + 2);});
};someAsyncThing().then(function() {console.log('everything is great');
});setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123
上面代码中,someAsyncThing()函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined,但是不会退出进程、终止脚本执行,2 秒之后还是会输出123。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。
一般总是建议,Promise 对象后面要跟catch()方法,这样可以处理 Promise 内部发生的错误。catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()方法。
const someAsyncThing = function() {return new Promise(function(resolve, reject) {// 下面一行会报错,因为x没有声明resolve(x + 2);});
};someAsyncThing()
.catch(function(error) {console.log('oh no', error);
})
.then(function() {console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
上面代码运行完catch()方法指定的回调函数,会接着运行后面那个then()方法指定的回调函数。如果没有报错,则会跳过catch()方法。
Promise.prototype.finally()
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
它的实现也很简单。
Promise.prototype.finally = function (callback) {let P = this.constructor;return this.then(value => P.resolve(callback()).then(() => value),reason => P.resolve(callback()).then(() => { throw reason }));
};
Promise.all()
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
Promise.race()
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。
!!!!下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve。
const p = Promise.race([fetch('/resource-that-may-take-a-while'),new Promise(function (resolve, reject) {setTimeout(() => reject(new Error('request timeout')), 5000)})
]);p
.then(console.log)
.catch(console.error);
上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。
Promise.resolve()
有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。
Promise.resolve()等价于下面的写法。
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
Promise.resolve方法的参数分成四种情况。
(1)参数是一个 Promise 实例
如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
(2)参数是一个thenable对象
thenable对象指的是具有then方法的对象,比如下面这个对象。
let thenable = {then: function(resolve, reject) {resolve(42);}
};
Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。
let thenable = {then: function(resolve, reject) {resolve(42);}
};let p1 = Promise.resolve(thenable);
p1.then(function(value) {console.log(value); // 42
});
上面代码中,thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出 42。
(3)参数不是具有then方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。
const p = Promise.resolve('Hello');p.then(function (s){console.log(s)
});
// Hello
上面代码生成一个新的 Promise 对象的实例p。由于字符串Hello不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved,所以回调函数会立即执行。Promise.resolve方法的参数,会同时传给回调函数。
(4)不带有任何参数
Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。
所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法。
const p = Promise.resolve();p.then(function () {// ...
});
上面代码的变量p就是一个 Promise 对象。
需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。
setTimeout(function () {console.log('three');
}, 0);Promise.resolve().then(function () {console.log('two');
});console.log('one');// one
// two
// three
上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是立即执行,因此最先输出。
Promise.try()
由于Promise.try为所有操作提供了统一的处理机制,所以如果想用then方法管理流程,最好都用Promise.try包装一下。这样有许多好处,其中一点就是可以更好地管理异常。
function getUsername(userId) {return database.users.get({id: userId}).then(function(user) {return user.name;});
}
上面代码中,database.users.get()返回一个 Promise 对象,如果抛出异步错误,可以用catch方法捕获,就像下面这样写。
database.users.get({id: userId})
.then(...)
.catch(...)
但是database.users.get()可能还会抛出同步错误(比如数据库连接错误,具体要看实现方法),这时你就不得不用try...catch去捕获。
try {database.users.get({id: userId}).then(...).catch(...)
} catch (e) {// ...
}
上面这样的写法就很笨拙了,这时就可以统一用promise.catch()捕获所有同步和异步的错误。
Promise.try(() => database.users.get({id: userId})).then(...).catch(...)
事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。
参考:https://es6.ruanyifeng.com/#docs/promise#Promise-%E7%9A%84%E5%90%AB%E4%B9%89
js报错类型
SyntaxError
SyntaxError 对象代表解析到语法上不合法的代码的错误
// SyntaxError: 语法错误
// 1) 变量名不符合规范
var 1 // Uncaught SyntaxError: Unexpected number
var 1a // Uncaught SyntaxError: Invalid or unexpected token
// 2) 给关键字赋值
function = 5 // Uncaught SyntaxError: Unexpected token =
// 3) 错误写法
var a =
ReferenceError
ReferenceError(引用错误) 对象代表当一个不存在的变量被引用时发生的错误。(这玩意儿不存在)
// ReferenceError:引用错误(要用的变量没找到)
// 1) 引用了不存在的变量
a() // Uncaught ReferenceError: a is not defined
console.log(b) // Uncaught ReferenceError: b is not defined
// 2) 给一个无法被赋值的对象赋值
console.log("abc") = 1 // Uncaught ReferenceError: Invalid left-hand side in assignment
TypeError
TypeError(类型错误) 对象用来表示值的类型非预期类型时发生的错误。(瞎几把调用)
// TypeError: 类型错误(调用不存在的方法)
// 变量或参数不是预期类型时发生的错误。比如使用new字符串、布尔值等原始类型和调用对象不存在的方法就会抛出这种错误,因为new命令的参数应该是一个构造函数。
// 1) 调用不存在的方法
123() // Uncaught TypeError: 123 is not a function
var o = {}
o.run() // Uncaught TypeError: o.run is not a function
// 2) new关键字后接基本类型
var p = new 456 // Uncaught TypeError: 456 is not a constructor
RangeError
RangeError对象标明一个错误,当一个值不在其所允许的范围或者集合中。
// RangeError: 范围错误(参数超范围)
// 主要的有几种情况,第一是数组长度为负数,第二是Number对象的方法参数超出范围,以及函数堆栈超过最大值。
// 1) 数组长度为负数
[].length = -5 // Uncaught RangeError: Invalid array length
// 2) Number对象的方法参数超出范围
var num = new Number(12.34)
console.log(num.toFixed(-1)) // Uncaught RangeError: toFixed() digits argument must be between 0 and 20 at Number.toFixed
// 说明: toFixed方法的作用是将数字四舍五入为指定小数位数的数字,参数是小数点后的位数,范围为0-20.
EvalError
与eval()相关的错误。此异常不再会被JavaScript抛出,但是EvalError对象仍然保持兼容性.
// EvalError: 非法调用 eval()
// 在ES5以下的JavaScript中,当eval()函数没有被正确执行时,会抛出evalError错误。例如下面的情况:
var myEval = eval;
myEval("alert('call eval')");
// 需要注意的是:ES5以上的JavaScript中已经不再抛出该错误,但依然可以通过new关键字来自定义该类型的错误提示。以上的几种派生错误,连同原始的Error对象,都是构造函数。开发者可以使用它们,认为生成错误对象的实例。
new Error([message[fileName[lineNumber]]])
// 第一个参数表示错误提示信息,第二个是文件名,第三个是行号。
URIError
给 encodeURI或 decodeURl()传递的参数无效
// URIError: URI不合法
// 主要是相关函数的参数不正确。
decodeURI("%") // Uncaught URIError: URI malformed at decodeURI
// jzz
面向对象的理解
(总结:面向对象的三大特征就是封装、继承和多态。所谓封装是说你把你需要创建的某一类型对象的特征(属性和方法)抽象出来封装成到一个超类中并暴露接口。而继承就是说与某一超类为模板创造出子类,子类会具有超类的特征,继承超类的方法和属性,多态就是子类不仅有超类的属性和方法,也会有自己特有的属性和方法,扩展子类的实现。比如说women类和men类,因为他们都是可以属于people类的,你可以先把people都有的特征抽象成类,women类和门类就可以继承people类,节约代码量并提高代码的复用性,然而就算都属于women类,每个实例,也就是具体到个人,他们也都会有自己特有的特性,有的人敲代码特厉害,有的人唱歌特厉害,所以每个对象都可以有自己的可扩展属性和方法以实现多态性,也就是每个人一样(整体)又都不一样(局部),大同小异.)
假设我是女娲,我准备捏一些人,
首先,人应该有哪些基本特征:
- 1.有四肢 2.有大脑 3.有器官 4.有思想 我们就有了第一个模型,这就是抽象。
- 其次,我和西方上帝是好友,我想我的这个想法能够提供给他用,但是我不想让他知道里面细节是怎么捏出来的,用的什么材料,他也不用考虑那么多,只要告诉我他要捏什么样的人就可以了。这就是封装。
- 然后,我之后创造的人都以刚才的模型做为模板,我创造的人都有我模型的特征 这就是继承。
- 最后,我觉得为了让人更丰富多彩,暗合阴阳之原理,可以根据模型进行删减,某些人上半身器官多突起那么一丢丢,下面少那么一丢丢。某些人,下半身多突起那么一丢丢。这就是多态。
面向对象的三大特征就是封装,继承,多态。
- 封装:就是把一个对象的特征,封装到一个类中,并暴露接口。
- 继承:为了代码的复用性,主要是让你不用重复造轮子了。
- 多态:就是继承的一个类,感觉类的特征不全面,可以扩展一些类的实现,实现特有的属性和方法。
“人”是类。
“人”有姓名、出生日期、身份证号等属性。
“人”有约会、么么哒、啪啪啪等功能(方法)。
“男人”、“女人”是“人”的子类。继承“人”的属性和功能。但也有自己特有的属性和功能。
你、我是对象。
关于this对象
(总结:this对象是在运行时基于函数的执行环境绑定的,this如果是在全局环境中通常指代的是全局对象,而如果是在函数中,this通常指最后调用this所在函数的那个对象。如果函数中有多个内嵌对象且每一层都有this,那么被调用的函数所指向的this只会是上一级的this对象,而不会像原型链一样层层寻找this值。而且可以使用Function.prototype.call或者apply方法绑定一个对象而达到改变this指向问题;还有一点需要提起:在箭头函数中this永远指向封闭词法环境的this,所谓封闭词法环境通常就是指函数,函数拥有自己的作用域,在内部定义的变量外环境是无法访问得到的;而js没有块级作用域,所以对象或者想if语句他们都没有自己的作用域,因此呢,用apply或者call都无法修改this指向,参数通常是对象,无法封住箭头函数的this,没有块级作用域)
- 无论是否在严格模式下,在全局执行环境中(在任何函数体外部)
this都指向全局对象。- 在函数内部,
this的值取决于函数被调用的方式。- this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象(this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的)
var j = o.b.fn;j();//不行
如果函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象
var a = '33'var o = {a: 10,b: {// a:12,fn: function () {console.log(this.a); //undefined}} }o.b.fn();
当一个函数在其主体中使用 this 关键字时,可以通过使用函数继承自Function.prototype 的 call 或 apply 方法将 this 值绑定到调用中的特定对象。
箭头函数
在箭头函数中,this与封闭词法环境的this保持一致。他不会创建自己的this和arguments。
在全局代码中,它将被设置为全局对象:
var globalObject = this;var foo = (() => this);console.log(foo() === globalObject); // true
注意:如果将this传递给call、bind、或者apply,它将被忽略。不过你仍然可以为调用添加参数,不过第一个参数(thisArg)应该设置为null。
所谓封闭词法环境是指函数在JavaScript中被解析为一个闭包,从而创建了一个作用域,使其在一个函数内定义的变量不能从函数外访问或从其他函数内访问。(js函数没有块级作用域)
var fullname = "aaa";
var obj = {fullname: "bbb",getFullName: () => this.fullname,prop: {fullname: "ccc",getFullName: function() {return this.fullname;}}};console.log(obj.prop.getFullName());//ccc,止在上一层作用域console.log(obj.getFullName());//aaavar func1 = obj.prop.getFullName;console.log(func1());//aaavar func2 = obj.getFullName;console.log(func2());//aaa
只有函数才能创建作用域, 不管是字面量还是 new 出来的对象(块级作用域)都是无法创建作用域的
匿名函数
(总结:匿名函数就是没有函数名、在运行时动态声明且是通过函数表达式而不是函数声明法定义的函数。匿名函数作用之一就是他比普通函数更节省内存空间,因为普通函数在定义时就会创建函数对象和作用域对象,即使没有调用也在占用着空间;而匿名函数仅在调用时候才会临时创建函数对象和作用域链对象;调用完,立即释放。而且匿名函数还可以构建命名空间和私有作用域,可以减少全局变量的污染,减低网页的内存压力和提高安全性;匿名函数如果在外层包围一个括号可以变成一个函数表达式,直接后面在跟个括号就直接可以调用了)
匿名函数:就是没有函数名的函数
匿名函数的执行环境具有全局性,因此其this对象通常指向window
var name = "The Window";var object = {name : "My Object",getNameFunc : function(){return function(){return this.name;};}
};alert(object.getNameFunc()()); //"The Window"(在非严格模式下)
以上代码先创建了一个全局变量name,又创建了一个包含name属性的对象。这个对象还包含一个方法——getNameFunc(),它返回一个匿名函数,而匿名函数又返回this.name。由于getNameFunc()返回一个函数,因此调用object.getNameFunc()()就会立即调用它返回的函数,结果就是返回一个字符串。然而,这个例子返回的字符串是"The Window",即全局name变量的值。为什么匿名函数没有取得其包含作用域(或外部作用域)的this对象呢?
因为每个函数在被调用时都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量;不过,把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,如下所示。
var name = "The Window";var object = {name : "My Object",getNameFunc : function(){var that = this;return function(){return that.name;};}
};alert(object.getNameFunc()()); //"My Object"
匿名函数最大的用途是创建闭包(这是JavaScript语言的特性之一),并且还可以构建命名空间、创建私有作用域,以减少全局变量的使用。
var oEvent = {};
(function(){ var addEvent = function(){ /*代码的实现省略了*/ };function removeEvent(){}oEvent.addEvent = addEvent;oEvent.removeEvent = removeEvent;
})();
在这段代码中函数addEvent和removeEvent都是局部变量,但我们可以通过全局变量oEvent使用它,这就大大减少了全局变量的使用,减少了网页的内存压力,增强了网页的安全性。
var rainman = (function(x , y){return x + y;
})(2 , 3);加上()表示函数表达式
/*** 也可以写成下面的形式,因为第一个括号只是帮助我们阅读,但是不推荐使用下面这种书写格式。* var rainman = function(x , y){* return x + y;* }(2 , 3);//先初始化*/
在这里我们创建了一个变量rainman,并通过直接调用匿名函数初始化为5,这种小技巧有时十分实用。
function(){console.log(1);
}
// 报错
- 因为ECAMScript规定函数的声明必须要有名字,如果没有名字的话,我们就没有办法找到它了,对于为什么自执行函数为什么就可以不带名字后面会讲。
- 如果没有名字必须要有一个依附体,如:将这个匿名函数赋值给一个变量。
如果按照上面的说法js报错也是应该的,那么我们用的下面这种代码为什么就能够正常运行?
(function(){console.log(1);
})() //1
之所以可以是因为我们将这个函数包含在了一个小括号中,why?小括号为什么这么神奇?
按照ECAMScript的规定,函数声明是必须要有名字的,但是我们用括号扩起来那么这个函数就不再是一个函数声明了,而是一个函数表达式,你可以理解成下面这段代码。
var a = function(){console.log(1);
}(); //1
将一个匿名函数赋值给一个变量或者对象属性就是函数表达式,函数表达式是可以不需要名字的,所以我们就可以直接通过这种方式来自动的执行这个函数。
再说一句
(function(){....
})()
第一个括号是个运算符,它会返回这个匿名函数,然后最后一个小括号会执行这个函数。
随便出一题:
var a = {n : 1};
var b = a;
a.x = a = {n : 2};
console.log(a.x);
console.log(b.x);解答过程:
var a = {n : 1};
var b = a;
// 此时b = {n:1};a.x = a = {n : 2};
// 从右往左赋值,a = {n:2}; 新对象
// b = {n:2}// a.x 中的a是{n:1}; {n:1}.x = {n:2}; 旧对象
// 因为b和a是引用的关系所以b.x也等于 {n:2}console.log(a.x); undefined
// 此时的a是新对象,新对象上没有a.x 所以是undefinedconsole.log(b.x); {n:2}
闭包
闭包的含义:闭包说白了就是函数的嵌套,内层的函数可以使用外层函数的所有变量,即使外层函数已经执行完毕(这点涉及JavaScript作用域链)。 所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包可以用在许多地方。它的最大用处有两个,一个是,另一个就是
1、【封装变量】—— 闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。减少全局污染
2、【延续局部变量的寿命】——让局部变量的值始终保持在内存中。由于函数外部可以读取或者改动函数内部的变量,只需一次初始化变量。该变量可以长时间地保存函数调用时产生的改变。
参考:https://www.cnblogs.com/shiyou00/p/10598010.html
var outer = null;(function(){var one = 1;function inner (){one += 1;alert(one);}outer = inner;
})();outer(); //2
outer(); //3
outer(); //4
使得外部可以访问函数的局部变量。因为内层的函数可以使用外层函数的所有变量,将这个内层函数赋值给一个外部变量便可以在外部访问。而且函数one变量一直储存在内存中,并没有因为函数被调用后而被清除。这段代码中的变量one是一个局部变量(因为它被定义在一个函数之内),因此外部是不可以访问的。但是这里我们创建了inner函数,inner函数是可以访问变量one的;又将全局变量outer引用了inner,所以三次调用outer会弹出递增的结果。
注意:闭包允许内层函数引用父函数中的变量,但是该变量是最终值,可以用匿名函数传参来解决,因为js没有块级作用域,但函数都拥有自己的作用域,且ECMAScript中所有函数的参数都是按值来传递的,所以当变量是原始数据类型值得时候,通过参数传进去的变量都是独立的个体,在栈中拥有自己的空间,所以彼此互不干扰。他传进去的确实是个值,而不是变量名了
内存泄露
不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
JavaScript垃圾回收机制
JavaScript不需要手动地释放内存,它使用一种自动垃圾回收机制(garbage collection)。当一个对象无用的时候,即程序中无变量引用这个对象时,就会从内存中释放掉这个变量。
垃圾回收机制怎么知道,哪些内存不再需要呢?
引用计数算法
最常使用的方法叫做"引用计数"(reference counting):语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。
如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。
const arr = [1, 2, 3, 4];
console.log('hello world');
上面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存。
如果增加一行代码,解除arr对[1, 2, 3, 4]引用,这块内存就可以被垃圾回收机制释放了。
let arr = [1, 2, 3, 4];
console.log('hello world');
arr = null;
上面代码中,arr重置为null,就解除了对[1, 2, 3, 4]的引用,引用次数变成了0,内存就可以释放出来了。
因此,并不是说有了垃圾回收机制,程序员就轻松了。你还是需要关注内存占用:那些很占空间的值,一旦不再用到,你必须检查是否还存在对它们的引用。如果是的话,就必须手动解除引用。
循环引用
引用计数算法是个简单有效的算法。但它却存在一个致命的问题:循环引用。如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。
var a={"name":"zzz"};var b={"name":"vvv"};a.child=b;b.parent=a;
因为有这个严重的缺点,这个算法在现代浏览器中已经被下面要介绍的标记清除算法所取代了。但绝不可认为该问题已经不再存在了,因为还占有大量市场的IE老祖宗们使用的正是这一算法。在需要照顾兼容性的时候,某些看起来非常普通的写法也可能造成意想不到的问题:
var div = document.createElement("div");
div.onclick = function() {console.log("click");
};
上面这种JS写法再普通不过了,创建一个DOM元素并绑定一个点击事件。那么这里有什么问题呢?请注意,变量div有事件处理函数的引用,同时事件处理函数也有div的引用!(div变量可在函数内被访问)。一个循序引用出现了,按上面所讲的算法,该部分内存无可避免地泄露哦了。 现在你明白为啥前端程序员都讨厌IE了吧?拥有超多BUG并依然占有大量市场的IE是前端开发一生之敌!亲,没有买卖就没有杀害。
function outer(){var obj = {};function inner(){ //这里引用了obj对象}obj.inner = inner;
}
这是一种及其隐蔽的循环引用,。当调用一次outer时,就会在其内部创建obj和inner两个对象,obj的inner属性引用了inner;同样inner也引用了obj,这是因为obj仍然在innerFun的封闭环境中,准确的讲这是由于JavaScript特有的“作用域链”。
因此,闭包非常容易创建循环引用,幸运的是JavaScript能够很好的处理这种循环引用。
解决方法:
- 置空dom对象
如果我们需要将dom对象返回,可以用如下方法:
- 构造新的context
把function抽到新的context中,这样,function的context就不包含对el的引用,从而打断循环引用。
标记清除算法
- 现代的浏览器已经不再使用引用计数算法了。现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
- 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
- 从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。但反之未必成立。因此当div(IE例子)与其时间处理函数不能再从全局对象出发触及的时候,垃圾回收器就会标记并回收这两个对象(在匿名函数中调用,div就不会出现在全局环境了)。
如何写出对内存管理友好的JS代码?
如果还需要兼容老旧浏览器,那么就需要注意代码中的循环引用问题。或者直接采用保证兼容性的库来帮助优化代码。
对现代浏览器来说,唯一要注意的就是明确切断需要回收的对象与根部的联系。有时候这种联系并不明显,且因为标记清除算法的强壮性,这个问题较少出现。最常见的内存泄露一般都与DOM元素绑定有关:
参考:JavaScript 内存泄漏教程、JavaScript 内存机制(前端同学进阶必备)
提升性能有关
- 阻塞式脚本:合并文件(减少http请求),将script标签放在body尾部(减少页面css,html的下载阻塞,减少界面的空白时间(浏览器在解析到script标签之前,不会渲染页面的任何部分))
目前流行的构建工具,如webpack,gulp,都有打包、合并文件的功能。
- 延迟脚本:defer和async属性:都是并行下载,下载过程不阻塞,区别在于执行时机,async是下载完成后立即执行;defer是等页面加载完成后再执行。defer仅当src属性声明时才生效(HTML5的规范)
- 适当将 DOM 元素保存在局部变量中——访问 DOM 会很慢。如果要多次读取某元素的内容,最好将其保存在局部变量中。但记住重要的是,如果稍后你会删除 DOM 的值,则应将变量设置为“null”,不然会导致内存泄漏。
- 减少使用全局变量——因为全局变量总是存在于执行环境作用域链的最末端,所以,访问全局变量是最慢的,访问局部变量是最快的。且如果全局变量太多,内存压力会变大,因为这些变量会得不到及时的回收而一直占用内存
- 减少闭包的使用——闭包会影响性能(作用域链加深)和可能导致内存泄漏,循环引用(IE中)
- 使用DocumentFragment优化多次append——一旦需要更新较多数量的DOM,请考虑使用文档碎片来存储DOM结构,然后再将其添加到现存的文档中。
-
使用一次innerHTML赋值代替构建dom元素——对于大的DOM更改,使用innerHTML要比使用标准的DOM方法创建同样的DOM结构快得多。
-
通过模板元素clone,替代createElement——而如果文档中存在现成的可以复制的样板节点,应该是用cloneNode()方法,因为使用createElement()方法之后,你需要设置多次元素的属性,使用cloneNode()则可以减少属性的设置次数——同样如果需要创建很多元素,应该先准备一个样板节点。
-
查询子元素的html节点时,使用children比childNodes更好——childNodes 属性,标准的,它返回指定元素的子元素集合,包括HTML节点,所有属性,文本。而children 属性,非标准的,它返回指定元素的子元素集合。经测试,它只返回HTML节点,甚至不返回文本节点。且在所有浏览器下表现惊人的一致。
-
删除DOM节点——删除dom节点之前,一定要删除注册在该节点上的事件,不管是用observe方式还是用attachEvent方式注册的事件,否则将会产生无法回收的内存。另外,在removeChild和innerHTML=’’二者之间,尽量选择后者. 因为在sIEve(内存泄露监测工具)中监测的结果是用removeChild无法有效地释放dom节点。
-
优化循环——减值迭代、简化终止条件(由于每次循环过程都会计算终止条件,所以必须保证它尽可能快,也就是说避免属性查找或者其它的操作,最好是将循环控制量保存到局部变量中,也就是说对数组或列表对象的遍历时,提前将length保存到局部变量中,避免在循环的每一步重复取值。)、使用后测试循环(while循环的效率要优于for(;;),最常用的for循环和while循环都是前测试循环,而如do-while这种后测试循环,可以避免最初终止条件的计算,因此运行更快。)
- 减少重绘和重排、事件委托、防抖和节流,动画用translate3d来加速绘制、window.Unload事件解除引用、switch语句相对if较快、巧用||和&&布尔运算符转换条件判断
参考:JS性能优化38条"军规",2019年呕心力作
移动开发
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" />设备像素比(DPR) = 设备像素个数 / 理想视口像素个数(device-width)rem是相对尺寸单位,相对于html标签字体大小的单位@media screen and (min-width: 321px) and (max-width:400px) {body {font-size:17px}}
数组方法集合

js小技巧
类型强制转换
string 强制转换为数字
- 可以用
*1来转化为数字(实际上是调用.valueOf方法)

- 也可以使用
+来转化字符串为数字。

object强制转化为string
- object->string:JSON.stringify()
- string->object:JSON.parse()
使用 Boolean 过滤数组中的所有假值
我们知道 JS 中有一些假值:false, null, 0, "", undefined, NaN,怎样把数组中的假值快速过滤呢?可以使用 Boolean 构造函数来进行一次转换。
const compact = arr => arr.filter(Boolean)compact([0, 1, false, 2, '', 3, 'a', 'e' * 23, NaN, 's', 34]) // [ 1, 2, 3, 'a', 's', 34 ]
双位运算符 ~~
可以使用双位操作符来替代正数的 Math.floor(),替代负数的 Math.ceil()。双否定位操作符的优势在于它执行相同的操作运行速度更快。
Math.floor(4.9) === 4 //true
// 简写为:
~~4.9 === 4 //true
~~-4.5 // -4
Math.floor(-4.5) // -5
Math.ceil(-4.5) // -4
取整 |0
对一个数字 |0 可以取整,负数也同样适用, num|0
1.3 | 0 // 1
-1.9 | 0 // -1
判断奇偶数 &1
const num=3;
!!(num & 1) // true
!!(num % 2) // true
函数
强制参数
默认情况下,如果不向函数参数传值,那么 JS 会将函数参数设置为 undefined。其它一些语言则会发出警告或错误。要执行参数分配,可以使用 if 语句抛出未定义的错误,或者可以利用 强制参数。
mandatory = ( ) => {
throw new Error('Missing parameter!');
}
foo = (bar = mandatory( )) => { // 这里如果不传入参数,就会执行manadatory函数报出错误
return bar;
}
惰性载入函数
在某个场景下我们的函数中有判断语句,这个判断依据在整个项目运行期间一般不会变化,所以判断分支在整个项目运行期间只会运行某个特定分支,那么就可以考虑惰性载入函数。
function foo() {if (a !== b) {console.log('aaa')}else {console.log('bbb')}
}
// 优化后
function foo() {if (a != b) {foo = function () {console.log('aaa')}}else {foo = function () {console.log('bbb')}}return foo();
}
那么第一次运行之后就会覆写这个方法,下一次再运行的时候就不会执行判断了。当然现在只有一个判断,如果判断很多,分支比较复杂,那么节约的资源还是可观的
一次性函数
跟上面的惰性载入函数同理,可以在函数体里覆写当前函数,那么可以创建一个一次性的函数,重新赋值之前的代码相当于只运行了一次,适用于运行一些只需要执行一次的初始化代码。
var sca = function() {console.log('msg')//做初始化sca = function() {console.log('foo')}
}sca() // msg
sca() // foo
sca() // foo
精确到指定位数的小数
numObj.toPrecision(precision)——以定点表示法或指数表示法表示的一个数值对象的字符串表示,四舍五入到
numObj.toPrecision(precision)
precision
可选。一个用来指定有效数个数的整数。var numObj = 5.123456;
console.log("numObj.toPrecision() is " + numObj.toPrecision()); //输出 5.123456
console.log("numObj.toPrecision(5) is " + numObj.toPrecision(5)); //输出 5.1235
console.log("numObj.toPrecision(2) is " + numObj.toPrecision(2)); //输出 5.1
console.log("numObj.toPrecision(1) is " + numObj.toPrecision(1)); //输出 5// 注意:在某些情况下会以指数表示法返回
console.log((1234.5).toPrecision(2)); // "1.2e+3"
数组
reduce 方法同时实现 map 和 filter
假设现在有一个数列,你希望更新它的每一项(map 的功能)然后筛选出一部分(filter 的功能)。如果是先使用 map 然后 filter 的话,你需要遍历这个数组两次。
在下面的代码中,我们将数列中的值翻倍,然后挑选出那些大于 50 的数。

统计数组中相同项的个数
很多时候,你希望统计数组中重复出现项的个数然后用一个对象表示,那么你可以使用 reduce 方法处理这个数组。
下面的代码将统计每一种车的数目然后把总数用一个对象表示

使用解构来交换参数数值
有时候你会将函数返回的多个值放在一个数组里,我们可以使用数组解构来获取其中每一个值。
let param1 = 1;
let param2 = 2;
[param1, param2] = [param2, param1];
console.log(param1) // 2
console.log(param2) // 1
当然我们关于交换数值有不少其他办法:
var temp = a; a = b; b = temp
b = [a, a = b][0]
a = a + b; b = a - b; a = a - b
接收函数返回的多个结果
在下面的代码中,我们从 /post 中获取一个帖子,然后在 /comments 中获取相关评论。由于我们使用的是 async/await,函数把返回值放在一个数组中,而我们使用数组解构后就可以把返回值直接赋给相应的变量。

fetch(Fetch 是浏览器提供的原生 AJAX 接口。使用 window.fetch 函数可以代替以前的 $.ajax、$.get 和 $.post;即Fetch API 就是浏览器提供的用来代替 jQuery.ajax 的工具)可转换成axios(axios是基于promise封装的网络请求(http)库,在多处框架中被使用) 其特点为:
- 支持浏览器和node.js
- 支持promise
- 能拦截请求和响应
- 能转换请求和响应数据
- 能取消请求
- 自动转换JSON数据
- 浏览器端支持防止CSRF(跨站请求伪造)
补充:ajax即“Asynchronous Javascript And XML”(异步 JavaScript 和 XML),ajax技术实现了在无需重新加载整个网页的情况下刷新局部数据,通过在后台与服务器进行少量数据交换,ajax可以使网页实现异步更新。ajax的原则是“按需取数据”,可以最大程度的减少冗余请求和响应对服务器造成的负担。
var xhttp = new XMLHttpRequest();xhttp.onreadystatechange = function() {if (this.readyState === 4 && this.status === 200) {console.log(this.responseText);}};xhttp.open("GET", "/", true);xhttp.send();
参考:ajax、axios、fetch之间的详细区别以及优缺点
代码复用
Object [key]
虽然将 foo.bar 写成 foo['bar'] 是一种常见的做法,但是这种做法构成了编写可重用代码的基础。许多框架使用了这种方法,比如 element 的表单验证。

上面的函数完美完成验证工作,但是当有很多表单,则需要应用验证,此时会有不同的字段和规则。如果可以构建一个在运行时配置的通用验证函数,会是一个好选择。
const schema = {first: {required: true},last: {required: true}
}
// universal validation function
const validate = (schema, values) => {for (field in schema) {if (schema[field].required) {if (!values[field]) {return false}}}return true;
}
console.log(validate(schema, {first: 'Bruce'
}));
// false
console.log(validate(schema, {first: 'Bruce',last: 'Wayne'
}));
// true
现在有了这个验证函数,我们就可以在所有窗体中重用,而无需为每个窗体编写自定义验证函数。
参考:JS 中可以提升幸福度的小技巧
解读HTTP/1、HTTP/2、HTTP/3之间的关系
HTTP 协议
HTTP 协议是 HyperText Transfer Protocol(超文本传输协议)的缩写,它是互联网上应用最为广泛的一种网络协议。所有的 WWW 文件的传输都必须遵守这个标准。伴随着计算机网络和浏览器的诞生,HTTP1.0 也随之而来,处于计算机网络中的应用层,HTTP 是建立在 TCP 协议之上,所以HTTP 协议的瓶颈及其优化技巧都是基于 TCP 协议本身的特性,例如 tcp 建立连接的 3 次握手和断开连接的 4 次挥手以及每次建立连接带来的 RTT 延迟时间。
HTTP之请求消息Request
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:
请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。
Host :请求头指明了被请求服务器的域名(用于虚拟主机),以及(可选的)服务器监听的TCP端口号。(Host: <host>:<port>)
Host: developer.cdn.mozilla.net
例如: 我们在浏览器中输入:http://www.hzau.edu.cn,浏览器发送的请求消息中,就会包含Host请求报头域,如下:
Host:www.hzau.edu.cn,此处使用缺省端口号80,若指定了端口号,则变成:Host:指定端口号。
(由于一个IP地址可以对应多个域名,比如假设我有这么几个域名www.qiniu.com,www.taobao.com和www.jd.com然后在域名提供商那通过A记录或者CNAME记录的方式最终都和我的虚拟机服务器IP 111.111.111.111关联起来,那么我通过任何一个域名去访问最终解析到的都是IP 111.111.111.111。我怎么来区分每次根据域名显示出不同的网站的内容呢,其实这就要用到请求头中Host的概念了,每个Host可以看做是我在服务器111.111.111.111上面的一个站点,每次我用那些域名访问的时候都是会解析同一个虚拟机没错,但是我通过不同的Host可以区分出我是访问这个虚拟机上的哪个站点。
参考:网络---一篇文章详解请求头Host的概念
Referer:包含了当前请求页面的来源页面的地址,即表示当前页面是通过此来源页面里的链接进入的。服务端一般使用 Referer 请求头识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。(Referer: <url>)
表示这个请求是从哪个URL过来的,假如你通过google搜索出一个商家的广告页面,你对这个广告页面感兴趣,鼠标一点发送一个请求报文到商家的网站,这个请求报文的Referer报文头属性值就是http://www.google.com。
Referer 请求头可能暴露用户的浏览历史,涉及到用户的隐私问题。
Referer: https://developer.mozilla.org/en-US/docs/Web/JavaScript
Origin:请求首部字段 Origin 指示了请求来自于哪个站点。该字段仅指示服务器名称,并不包含任何路径信息。该首部用于 CORS 请求或者 POST 请求。除了不包含路径信息,该字段与 Referer 首部字段相似。(<scheme>协议 "://" <host>域名或者IP地址 [ ":" <port> 端口])eg:
Origin: https://developer.mozilla.org
Origin字段的方式比Referer更人性化,因为它尊重了用户的隐私。——Origin字段里只包含是谁发起的请求,并没有其他信息 (通常情况下是方案,主机和活动文档URL的端口)。跟Referer不一样的是,Origin字段并没有包含涉及到用户隐私的URL路径和请求内容,这个尤其重要。且Origin字段只存在于POST请求,而Referer则存在于所有类型的请求。
参考:Origin字段
HTTP之响应消息Response
一般情况下,服务器接收并处理客户端发过来的请求后会返回一个HTTP的响应消息。
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
HTTP之状态码
状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别:
- 1xx:指示信息--表示请求已接收,继续处理
- 2xx:成功--表示请求已被成功接收、理解、接受
- 3xx:重定向--要完成请求必须进行更进一步的操作
- 4xx:客户端错误--请求有语法错误或请求无法实现
- 5xx:服务器端错误--服务器未能实现合法的请求
以下是几个常见的状态码:
- 200 OK —— 你最希望看到的,即处理成功!
- 303 See Other —— 我把你redirect到其它的页面,目标的URL通过响应报文头的Location告诉你。
- 304 Not Modified —— 告诉客户端,你请求的这个资源至你上次取得后,并没有更改,你直接用你本地的缓存吧,我很忙哦,你能 不能少来烦我啊!
- 403 Forbidden —— 服务器收到请求,但是拒绝提供服务
- 404 Not Found —— 你最不希望看到的,即找不到页面。如你在google上找到一个页面,点击这个链接返回404,表示这个页面已 经被网站删除了,google那边的记录只是美好的回忆。
- 500 Internal Server Error —— 服务器发生不可预期的错误
- 503 Server Unavailable —— 服务器当前不能处理客户端的请求,一段时间后可能恢复正常
- URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。
- 而URL是uniform resource locator,统一资源定位器,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。
- 而URN,uniform resource name,统一资源命名,是通过名字来标识资源,比如mailto:java-net@java.sun.com。
- 也就是说,URI是以一种抽象的,高层次概念定义统一资源标识,而URL和URN则是具体的资源标识的方式。URL和URN都是一种URI。
- 在Java的URI中,一个URI实例可以代表绝对的,也可以是相对的,只要它符合URI的语法规则。而URL类则不仅符合语义,还包含了定位该资源的信息,因此它不能是相对的,schema必须被指定。
- URL是一种具体的URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。URI是一种语义上的抽象概念,可以是绝对的,也可以是相对的,而URL则必须提供足够的信息来定位,所以,是绝对的,而通常说的relative URL,则是针对另一个absolute URL,本质上还是绝对的。
URI抽象结构 [scheme:]scheme-specific-part[#fragment]
[scheme:][//authority][path][?query][#fragment]
authority为[user-info@]host[:port]
- URI 属于父类,而 URL 属于 URI 的子类。URL 是 URI 的一个子集
- URI 表示请求服务器的路径,定义这么一个资源。而 URL 同时说明要如何访问这个资源(http://)。
- URI可以分为URL,URN,或同时具备locators 和names特性的一个东西。URN作用就好像一个人的名字,URL就像一个人的地址。换句话说:URN确定了东西的身份,URL提供了找到它的方式。”
- URL是一种具体的URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。
- URI是一种语义上的抽象概念,可以是绝对的,也可以是相对的,而URL则必须提供足够的信息来定位(即绝对)。
参考:URI和URL的区别
HTTP/1.x 的缺陷
- 连接无法复用:连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对大量小文件请求影响较大(没有达到最大窗口请求就被终止)。
HTTP/1.0 传输数据时,每次都需要重新建立连接,增加延迟。
HTTP/1.1 虽然加入 keep-alive 可以复用一部分连接,但域名分片等情况下仍然需要建立多个 connection,耗费资源,给服务器带来性能压力。
- Head-Of-Line Blocking(HOLB):导致带宽无法被充分利用,以及后续健康请求被阻塞。HOLB是指一系列包(package)因为第一个包被阻塞;当页面中需要请求很多资源的时候,HOLB(队头阻塞)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。
HTTP 1.0:下个请求必须在前一个请求返回后才能发出,request-response对按序发生。显然,如果某个请求长时间没有返回,那么接下来的请求就全部阻塞了。
HTTP 1.1:尝试使用 pipeling 来解决,即浏览器可以一次性发出多个请求(同个域名,同一条 TCP 链接)。但 pipeling 要求返回是按序的,那么前一个请求如果很耗时(比如处理大图片),那么后面的请求即使服务器已经处理完,仍会等待前面的请求处理完才开始按序返回。所以,pipeling 只部分解决了 HOLB。
如上图所示,红色圈出来的请求就因域名链接数已超过限制,而被挂起等待了一段时间。
- 协议开销大: HTTP1.x 在使用时,header 里携带的内容过大,在一定程度上增加了传输的成本,并且每次请求 header 基本不怎么变化,尤其在移动端增加用户流量。
- 安全因素:HTTP1.x 在传输数据时,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份,这在一定程度上无法保证数据的安全性
- 虽然 HTTP1.x 支持了 keep-alive,来弥补多次创建连接产生的延迟,但是 keep-alive 使用多了同样会给服务端带来大量的性能压力,并且对于单个文件被不断请求的服务 (例如图片存放网站),keep-alive 可能会极大的影响性能,因为它在文件被请求之后还保持了不必要的连接很长时间。
HTTPS 应声而出(安全版的http)
- HTTPS 协议需要到 CA 申请证书,一般免费证书很少,需要交费。
- HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,HTTPS 运行在 SSL/TLS 之上,SSL/TLS 运行在 TCP 之上,所有传输的内容都经过加密的。(HTTPS 是与 SSL 一起使用的;在 SSL 逐渐演变到成TLS (其实两个是一个东西,只是名字不同而已的安全传输协议))
- HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443。
- HTTPS 可以有效的防止运营商劫持,解决了防劫持的一个大问题。
SPDY 协议
【(读作“SPeeDY”)是Google开发的基于TCP的会话层 协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。 SPDY并不是一种用于替代HTTP的协议,而是对HTTP协议的增强。】
因为 HTTP/1.x 的问题,我们会引入雪碧图、将小图内联、使用多个域名等等的方式来提高性能。不过这些优化都绕开了协议,直到 2009 年,谷歌公开了自行研发的 SPDY 协议,主要解决 HTTP/1.1 效率不高的问题。谷歌推出 SPDY,才算是正式改造 HTTP 协议本身。降低延迟,压缩 header 等等,SPDY 的实践证明了这些优化的效果,也最终带来 HTTP/2 的诞生。
SPDY 协议在 Chrome 浏览器上证明可行以后,就被当作 HTTP/2 的基础,主要特性都在 HTTP/2 之中得到继承。
SPDY 可以说是综合了 HTTPS 和 HTTP 两者有点于一体的传输协议,主要解决:
- 降低延迟,针对 HTTP 高延迟的问题,SPDY 优雅的采取了多路复用(multiplexing)。多路复用通过多个请求 stream 共享一个 tcp 连接的方式,解决了 HOL blocking 的问题,降低了延迟同时提高了带宽的利用率。
- 请求优先级(request prioritization)。多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY 允许给每个 request 设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的 html 内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
- header 压缩。前面提到 HTTP1.x 的 header 很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。
- 基于 HTTPS 的加密协议传输,大大提高了传输数据的可靠性。
- 服务端推送(server push),采用了 SPDY 的网页,例如我的网页有一个 sytle.css 的请求,在客户端收到 sytle.css 数据的同时,服务端会将 sytle.js 的文件推送给客户端,当客户端再次尝试获取 sytle.js 时就可以直接从缓存中获取到,不用再发请求了。
SPDY 位于 HTTP 之下,TCP 和 SSL 之上,这样可以轻松兼容老版本的 HTTP 协议 (将 HTTP1.x 的内容封装成一种新的 frame 格式),同时可以使用已有的 SSL 功能
HTTP2.0 的前世今生
顾名思义有了 HTTP1.x,那么 HTTP2.0 也就顺理成章的出现了。HTTP/2 是现行 HTTP 协议(HTTP/1.x)的替代,但它不是重写,HTTP 方法/状态码/语义都与 HTTP/1.x 一样。HTTP/2 基于 SPDY3,专注于性能,最大的一个目标是在用户和网站间只用一个连接(connection)。
HTTP2.0 可以说是 SPDY 的升级版(其实原本也是基于 SPDY 设计的),但是,HTTP2.0 跟 SPDY 仍有不同的地方,主要是以下两点:
- HTTP2.0 支持明文 HTTP 传输,而 SPDY 强制使用 HTTPS
- HTTP2.0 消息头的压缩算法采用 HPACK,而非 SPDY 采用的 DEFLATE
HTTP2.0 的新特性
- 新的二进制格式(Binary Format),HTTP1.x 的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认 0 和 1 的组合。基于这种考虑 HTTP2.0 的协议解析决定采用二进制格式,实现方便且健壮。
- 多路复用(MultiPlexing),即连接共享,即每一个 request 都是是用作连接共享机制的。一个 request 对应一个 id,这样一个连接上可以有多个 request,每个连接的 request 可以随机的混杂在一起,接收方可以根据 request 的 id 将 request 再归属到各自不同的服务端请求里面。(单连接+帧、分包传输、打散,在客户端进行重组),1.0是最大6个连接,等连接都完成后再进行下一次的6个连接,即HOLB(队头阻塞)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。
- header 压缩,如上文中所言,对前面提到过 HTTP1.x 的 header 带有大量信息,而且每次都要重复发送,HTTP2.0 使用 encoder 来减少需要传输的 header 大小,通讯双方各自 cache 一份 header fields 表,既避免了重复 header 的传输,又减小了需要传输的大小。
- 服务端推送(server push),同 SPDY 一样,HTTP2.0 也具有 server push 功能。目前,有大多数网站已经启用 HTTP2.0,例如 YouTuBe,淘宝网等网站
HTTP2.0 的升级改造
对比 HTTPS 的升级改造,HTTP2.0 或许会稍微简单一些,你可能需要关注以下问题:
- 前文说了 HTTP2.0 其实可以支持非 HTTPS 的,但是现在主流的浏览器像 chrome,firefox 表示还是只支持基于 TLS 部署的 HTTP2.0 协议,所以要想升级成 HTTP2.0 还是先升级 HTTPS 为好。
- 当你的网站已经升级 HTTPS 之后,那么升级 HTTP2.0 就简单很多,如果你使用 NGINX,只要在配置文件中启动相应的协议就可以了,可以参考 NGINX 白皮书,NGINX 配置 HTTP2.0 官方指南。
- 使用了 HTTP2.0 那么,原本的 HTTP1.x 怎么办,这个问题其实不用担心,HTTP2.0 完全兼容 HTTP1.x 的语义,对于不支持 HTTP2.0 的浏览器,NGINX 会自动向下兼容的。
HTTP/2 的缺点
虽然 HTTP/2 解决了很多之前旧版本的问题,但是它还是存在一个巨大的问题,主要是底层支撑的 TCP 协议造成的。HTTP/2的缺点主要有以下几点:
TCP 以及 TCP+TLS建立连接的延时:
HTTP/2都是使用TCP协议来传输的,而如果使用HTTPS的话,还需要使用TLS协议进行安全传输,而使用TLS也需要一个握手过程,这样就需要有两个握手延迟过程:
①在建立TCP连接的时候,需要和服务器进行三次握手来确认连接成功,也就是说需要在消耗完1.5个RTT之后才能进行数据传输。
②进行TLS连接,TLS有两个版本——TLS1.2和TLS1.3,每个版本建立连接所花的时间不同,大致是需要1~2个RTT。
总之,在传输数据之前,我们需要花掉 3~4 个 RTT。
TCP的队头阻塞并没有彻底解决:
上文我们提到在HTTP/2中,多个请求是跑在一个TCP管道中的。但当出现了丢包时,HTTP/2 的表现反倒不如 HTTP/1 了。因为TCP为了保证可靠传输,有个特别的“丢包重传”机制,丢失的包必须要等待重新传输确认,HTTP/2出现丢包时,整个 TCP 都要开始等待重传,那么就会阻塞该TCP连接中的所有请求(如下图)。而对于 HTTP/1.1 来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。
HTTP/3简介
Google 在推SPDY的时候就已经意识到了这些问题,于是就另起炉灶搞了一个基于 UDP 协议的“QUIC”协议,让HTTP跑在QUIC上而不是TCP上。而这个“HTTP over QUIC”就是HTTP协议的下一个大版本,HTTP/3。它在HTTP/2的基础上又实现了质的飞跃,真正“完美”地解决了“队头阻塞”问题。
QUIC 虽然基于 UDP,但是在原本的基础上新增了很多功能,接下来我们重点介绍几个QUIC新功能。不过HTTP/3目前还处于草案阶段,正式发布前可能会有变动,所以本文尽量不涉及那些不稳定的细节。
3.QUIC新功能
上面我们提到QUIC基于UDP,而UDP是“无连接”的,根本就不需要“握手”和“挥手”,所以就比TCP来得快。此外QUIC也实现了可靠传输,保证数据一定能够抵达目的地。它还引入了类似HTTP/2的“流”和“多路复用”,单个“流"是有序的,可能会因为丢包而阻塞,但其他“流”不会受到影响。具体来说QUIC协议有以下特点:
-
实现了类似TCP的流量控制、传输可靠性的功能。
虽然UDP不提供可靠性的传输,但QUIC在UDP的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥塞控制以及其他一些TCP中存在的特性。
-
实现了快速握手功能。
由于QUIC是基于UDP的,所以QUIC可以实现使用0-RTT或者1-RTT来建立连接,这意味着QUIC可以用最快的速度来发送和接收数据,这样可以大大提升首次打开页面的速度。0RTT 建连可以说是 QUIC 相比 HTTP2 最大的性能优势。
-
集成了TLS加密功能。
目前QUIC使用的是TLS1.3,相较于早期版本TLS1.3有更多的优点,其中最重要的一点是减少了握手所花费的RTT个数。
-
多路复用,彻底解决TCP中队头阻塞的问题
和TCP不同,QUIC实现了在同一物理连接上可以有多个独立的逻辑数据流(如下图)。实现了数据流的单独传输,就解决了TCP中队头阻塞的问题。
七、总结
-
HTTP/1.1有两个主要的缺点:安全不足和性能不高。
-
HTTP/2完全兼容HTTP/1,是“更安全的HTTP、更快的HTTPS",头部压缩、多路复用等技术可以充分利用带宽,降低延迟,从而大幅度提高上网体验;
-
QUIC 基于 UDP 实现,是 HTTP/3 中的底层支撑协议,该协议基于 UDP,又取了 TCP 中的精华,实现了即快又可靠的协议。
参考:HTTP,HTTP2.0,SPDY,HTTPS 你应该知道的一些事、解读HTTP/2与HTTP/3 的新特性、一文读懂 HTTP/2 及 HTTP/3 特性
cookie、session、token
cookie 是什么
简单地说,cookie 就是浏览器储存在用户电脑上的一小段文本文件。cookie 是纯文本格式,不包含任何可执行的代码。仅仅是浏览器实现的一种数据存储功能。
cookie由服务器生成,发送给浏览器,浏览器把cookie以kv形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该cookie发送给服务器。由于cookie是存在客户端上的,所以浏览器加入了一些限制确保cookie不会被恶意使用,同时不会占据太多磁盘空间,所以每个域的cookie数量是有限的。
创建 cookie
Web 服务器通过发送一个称为 Set-Cookie 的 HTTP 消息头来创建一个 cookie,Set-Cookie消息头是一个字符串,其格式如下(中括号中的部分是可选的):
Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure; HttpOnly]
domain,指定了 cookie 将要被发送至哪个或哪些域中。默认情况下,domain会被设置为创建该 cookie 的页面所在的域名,所以当给相同域名发送请求时该 cookie 会被发送至服务器。例如,本博中 cookie 的默认值将是bubkoo.com。domain选项可用来扩充 cookie 可发送域的数量 。domain选项的值必须是发送Set-Cookie消息头的主机名的一部分- secure,只有当一个请求通过 SSL 或 HTTPS 创建时,包含
secure选项的 cookie 才能被发送至服务器。 HTTP-Only,背后的意思是告之浏览器该 cookie 绝不能通过 JavaScript 的document.cookie属性访问。设计该特征意在提供一个安全措施来帮助阻止通过 JavaScript 发起的跨站脚本攻击 (XSS) 窃取 cookie 的行为
参考:HTTP cookies 详解
Session
session 从字面上讲,就是会话。这个就类似于你和一个人交谈,你怎么知道当前和你交谈的是张三而不是李四呢?对方肯定有某种特征(长相等)表明他就是张三。
session 也是类似的道理,服务器要知道当前发请求给自己的是谁。为了做这种区分,服务器就要给每个客户端分配不同的“身份标识”,然后客户端每次向服务器发请求的时候,都带上这个“身份标识”,服务器就知道这个请求来自于谁了。至于客户端怎么保存这个“身份标识”,可以有很多种方式,对于浏览器客户端,大家都默认采用 cookie 的方式。
客户端对服务端请求时,服务端会检查请求中是否包含一个session标识( 称为session id ).
- 如果没有,那么服务端就生成一个随机的session以及和它匹配的session id,并将session id返回给客户端.
- 如果有,那么服务器就在存储中根据session id 查找到对应的session.
当浏览器禁止Cookie时,可以有两种方法继续传送session id到服务端:
- 第一种:URL重写(常用),就是把session id直接附加在URL路径的后面。
- 第二种:表单隐藏字段,将sid写在隐藏的表单中。
Cookie和Session的区别:
1、cookie数据存放在客户的浏览器上,session数据放在服务器上。
2、cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。
3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。
4、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
5、所以个人建议:
将登陆信息等重要信息存放为session
其他信息如果需要保留,可以放在cookie中
session的缺点:
1.Seesion:每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加。
2.可扩展性:在服务端的内存中使用Seesion存储登录信息,伴随而来的是可扩展性问题。
3.CORS(跨域资源共享):当我们需要让数据跨多台移动设备上使用时,跨域资源的共享会是一个让人头疼的问题。在使用Ajax抓取另一个域的资源,就可以会出现禁止请求的情况。
4.CSRF(跨站请求伪造):用户在访问银行网站时,他们很容易受到跨站请求伪造的攻击,并且能够被利用其访问其他的网站。
Token
在Web领域基于Token的身份验证随处可见。在大多数使用Web API的互联网公司中,tokens 是多用户下处理认证的最佳方式。
1、Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。
2、Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token(将用户数据包装成token)便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
3、使用Token的目的:Token的目的是为了减轻服务器的压力(不需要保存所有人的sessionId,对比起session),减少频繁的查询数据库,使服务器更加健壮。
即基于Token的身份验证是无状态的,我们不将用户信息存在服务器或Session中。
以下几点特性会让你在程序中使用基于Token的身份验证
1.无状态、可扩展
2.支持移动设备
3.跨程序调用
4.安全(每个请求都有签名还能防止监听以及重放攻击)
那些使用基于Token的身份验证的大佬们
大部分你见到过的API和Web应用都使用tokens。例如Facebook, Twitter, Google+, GitHub等。
Token是用户的验证方式,最简单的token组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,由token的前几位+盐以哈希算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接token请求服务器)。
举例:
##JWT+HA256验证
实施 Token 验证的方法挺多的,还有一些标准方法,比如 JWT,读作:jot ,表示:JSON Web Tokens 。JWT 标准的 Token 有三个部分:
header
payload
signature
中间用点分隔开,并且都会使用 Base64 编码,所以真正的 Token 看起来像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
###Header
header 部分主要是两部分内容,一个是 Token 的类型,另一个是使用的算法,比如下面类型就是 JWT,使用的算法是 HS256,就是SHA-256,和md5一样是不可逆的散列算法。
{"typ": "JWT","alg": "HS256"
}
上面的内容要用 Base64 的形式编码一下,所以就变成这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
###Payload
Payload 里面是 Token 的具体内容,这些内容里面有一些是标准字段,你也可以添加其它需要的内容。下面是标准字段:
iss:Issuer,发行者
sub:Subject,主题
aud:Audience,观众
exp:Expiration time,过期时间
nbf:Not before
iat:Issued at,发行时间
jti:JWT ID
比如下面这个 Payload ,用到了 iss 发行人,还有 exp 过期时间。另外还有两个自定义的字段,一个是 name ,还有一个是 admin 。
{ "iss": "ninghao.net","exp": "1438955445","name": "wanghao","admin": true
}
使用 Base64 编码以后就变成了这个样子:
eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ
###Signature
JWT 的最后一部分是 Signature ,这部分内容有三个部分,先是用 Base64 编码的 header.payload ,再用加密算法加密一下,加密的时候要放进去一个 Secret ,这个相当于是一个密码,这个密码秘密地存储在服务端。
var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
HMACSHA256(encodedString, 'secret');
处理完成以后看起来像这样:
SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
最后这个在服务端生成并且要发送给客户端的 Token 看起来像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
客户端收到这个 Token 以后把它存储下来,下会向服务端发送请求的时候就带着这个 Token 。服务端收到这个 Token ,然后进行验证,通过以后就会返回给客户端想要的资源。
验证的过程就是
根据传过来的token再生成一下第三部分Signature,然后两个比对一下,一致就验证通过。
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录(有就直接到数据库查,没有就直接打回,也就不需要再去查数据库然后返回无信息)。大概的流程是这样的:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token(header里)
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,(利用密匙和数据解出签名,和token的签名对比,如果相同则通过)如果验证成功,就向客户端返回请求的数据
参考:JWT产生和验证Token、Cookie、Session、Token那点事儿(原创)、彻底理解cookie,session,token(转)、会话(Cookie,Session,Token)管理知识整理(一)
MVC、MVP和MVVM
MVC:Model+View+Controller
M-Model 模型:数据保存
V-View 视图: 用户界面
C-Controller 控制器: 业务逻辑
MVC ,用户操作> View (负责接受用户的输入操作)>Controller(业务逻辑处理)>Model(数据持久化)>View(将结果通过View反馈给用户)
各部分的通信方式是单向传递。
view向controller传递指令,controller完成业务逻辑后,要求Model改变状态,Model将新的数据发送到view,用户得到反馈。
MVC接受用户指令时可以通过两种方式:一种是通过view接收指令,传递给controller;一种是直接通过controller接收指令。
在MVC里,View是可以直接访问Model的!从而,View里会包含Model信息,不可避免的还要包括一些 业务逻辑。 在MVC模型里,更关注的Model的不变,而同时有多个对Model的不同显示,及View。所以,在MVC模型里,Model不依赖于View,但是 View是依赖于Model的。不仅如此,因为有一些业务逻辑在View里实现了,导致要更改View也是比较困难的,至少那些业务逻辑是无法重用的。
Model层
Model层 是服务端数据在客户端的映射,是薄薄的一层,完全可以用struct表征。下面看一个实例:
可以看到,Model 层通常是服务端传回的 JSON数据的映射,对应为一个一个的属性。不过现在也有很多人将网络层(Service层)归到Model中,也就是MVC(S)架构。同时,大部分时候数据的持久化操作也会放在Model层中。
总结一下,Model层的职责主要有以下几项:HTTP请求、进行字段验证、持久化等。
View层
View层是展示在屏幕上的视图的封装,在 iOS 中也就是UIView以及UIView的子类。下面是UIView的继承层级图:
View层的职责是展示内容和接受用户的操作与事件。
Controller层
看了Model层和View层如此简单清晰的定义,如果你以为接下来要讲的Controller层的定义也跟这两层一样,那你就要失望了。
粗略总结了一下,Controller层的职责包括但不限于:管理根视图以及其子视图的生命周期、展示内容和布局、处理用户行为(如按钮的点击和手势的触发等)、储存当前界面的状态(例如分页加载的页数、是否正在进行网络请求的布尔值等)、处理界面的跳转、作为UITableView以及其它容器视图的代理以及数据源、业务逻辑和各种动画效果等。
按照传统的MVC定义,分割了小部分到Model层和View层,剩下的代码都没有其他地方可以去了,于是被统统的丢到了Controll层中。
庞大的Controller层带来的问题就是难以维护、难以测试。而且其中充斥着大量的状态值,一个任务的完成依赖于好几个状态值,而一个状态值又同时参与到多个任务中,这样复杂的多对多关系带来的问题就是开发效率低下,需要花费大量的时间周旋在各个状态值之间,对以后的功能拓展、业务添加也造成了障碍。
MVP
MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。作为一种新的模式,MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会从直接Model中读取数据而不是通过 Controller。
在MVP里,Presenter完全把Model和View进行了分离,主要的程序逻辑在Presenter里实现。而且,Presenter与具体的 View是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更View时候可以保持Presenter的不变,即重用!
不仅如此,我们还可以编写测试用的View,模拟用户的各种操作,从而实现对Presenter的测试--而不需要使用自动化的测试工具。 我们甚至可以在Model和View都没有完成时候,就可以通过编写Mock Object(即实现了Model和View的接口,但没有具体的内容的)来测试Presenter的逻辑。
在MVP里,应用程序的逻辑主要在Presenter来实现,其中的View是很薄的一层。因此就有人提出了Presenter First的设计模式,就是根据User Story来首先设计和开发Presenter。在这个过程中,View是很简单的,能够把信息显示清楚就可以了。在后面,根据需要再随便更改View, 而对Presenter没有任何的影响了。 如果要实现的UI比较复杂,而且相关的显示逻辑还跟Model有关系,就可以在View和Presenter之间放置一个Adapter。由这个 Adapter来访问Model和View,避免两者之间的关联。而同时,因为Adapter实现了View的接口,从而可以保证与Presenter之 间接口的不变。这样就可以保证View和Presenter之间接口的简洁,又不失去UI的灵活性。 在MVP模式里,View只应该有简单的Set/Get的方法,用户输入和设置界面显示的内容,除此就不应该有更多的内容,绝不容许直接访问 Model--这就是与MVC很大的不同之处。
目前我们提倡的MVC已经与MVP没有太大区别,View依然是很薄的一层,不进行与Model的逻辑处理,只进行简单的页面显示的逻辑处理。
MVVM(以VUE.JS来举例)
MVVM在概念上是真正将页面与数据逻辑分离的模式,在开发方式上,它是真正将前台代码开发者(JS+HTML)与后台代码开发者分离的模式(asp,asp.net,php,jsp)。 ViewModel负责连接 View 和 Model,保证视图和数据的一致性,这种轻量级的架构让前端开发更加高效、便捷。
MVVM 的出现促进了 GUI 前端开发与后端业务逻辑的分离,极大地提高了前端开发效率。MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用。如下图所示:
MVVM模式
MVVM 已经相当成熟了,主要运用但不仅仅在网络应用程序开发中。KnockoutJS 是最早实现 MVVM 模式的前端框架之一,当下流行的 MVVM 框架有 Vue,Angular 等。
简单画了一张图来说明 MVVM 的各个组成部分:
分层设计一直是软件架构的主流设计思想之一,MVVM 也不例外。
# View 层
View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建,为了更方便地展现 ViewModel 或者 Model 层的数据,已经产生了各种各样的前后端模板语言,比如 FreeMarker、Marko、Pug、Jinja2等等,各大 MVVM 框架如 KnockoutJS,Vue,Angular 等也都有自己用来构建用户界面的内置模板语言。
# Model 层
Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,主要围绕数据库系统展开。后端的处理通常会非常复杂:
前后端对比
后端:我们这里的业务逻辑和数据处理会非常复杂!
前端:关我屁事!
后端业务处理再复杂跟我们前端也没有半毛钱关系,只要后端保证对外接口足够简单就行了,我请求api,你把数据返出来,咱俩就这点关系,其他都扯淡。
# ViewModel 层
ViewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,那一块展示什么这些都属于视图状态(展示),而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。由于实现了双向绑定,ViewModel 的内容会实时展现在 View 层,这是激动人心的,因为前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新,真正实现数据驱动开发。看到了吧,View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。
针对于VUE的MVVM:
- 从 M 到 V 的映射(Data Binding),这样可以大量节省你人肉来 update View 的代码(也就是所说的 UI 逻辑)
- 从 V 到 M 的事件监听(DOM Listeners),这样你的 Model 会随着 View 触发事件而改变
针对上图,个人更倾向于将VUE文件里的script里export default 对象里的部分规划为Model层,即Model层可以包含数据模型和页面交互逻辑,应用逻辑全部是数据操作,虽然上图描绘Model层是纯js对象(即使用Object.prototype.toString.call(data) === '[object Object]')可能让人觉得是data对象,但是前者同样是会导出纯js对象的。View 代表UI 组件,它负责将数据模型转化成UI 展现出来,ViewModel 是一个同步View 和 Model的对象。
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
针对VUE.JS的MVVM'模式的优点:
- 最主要的双向绑定技术——核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel 的状态改变可以自动传递给 View,view层的变化也能及时在VM得到响应,MVVM的设计思想:关注Model(数据)的变化,让MVVM框架去自动更新DOM的状态,从而把发者从操作DOM的繁琐步骤中解脱出来!
- 低耦合(不是无耦合)。由于View 和 Model 之间并没有直接的联系,视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
- 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
- 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计
- 可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写易用灵活高效
参考:前后端分手大师——MVVM 模式、MVVM架构~mvc,mvp,mvvm大话开篇、MVC与MVVM的区别、响应式编程与MVVM架构—理论篇、Vue.js 和 MVVM 小细节、MVVM实现原理
VUE.JS
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式JavaScript框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,方便与第三方库或既有项目整合。
如下图所示,在具有响应式系统的Vue实例中,DOM状态只是数据状态的一个映射 即 UI=VM(State) ,当等式右边State改变了,页面展示部分UI就会发生相应改变。很多人初次上手Vue时,觉得很好用,原因就是这个。不过需要注意的是,Vue的核心定位并不是一个框架[3],设计上也没有完全遵循MVVM模式,可以看到在图中只有State和View两部分, Vue的核心功能强调的是状态到界面的映射,对于代码的结构组织并不重视, 所以单纯只使用其核心功能时,它并不是一个框架,而更像一个视图模板引擎,这也是为什么Vue开发者把其命名成读音类似于view的原因。
所谓“渐进式”
上文提到,Vue的核心的功能,是一个视图模板引擎,但这不是说Vue就不能成为一个框架。如下图所示,这里包含了Vue的所有部件,在声明式渲染(视图模板引擎)的基础上,我们可以通过添加组件系统、客户端路由、大规模状态管理来构建一个完整的框架。更重要的是,这些功能相互独立,你可以在核心功能的基础上任意选用其他的部件,不一定要全部整合在一起。可以看到,所说的“渐进式”,其实就是Vue的使用方式,它可能有些方面是不如React,不如Angular,但它是渐进的,没有强主张,你可以在原有大系统的上面,把一两个组件改用它实现,当jQuery用;也可以整个用它全家桶开发,当Angular用;还可以用它的视图,搭配你自己设计的整个下层用。你可以在底层数据逻辑的地方用OO和设计模式的那套理念,也可以函数式,都可以,它只是个轻量视图而已,只做了自己该做的事,没有做不该做的事,仅此而已。不干扰开发者的业务逻辑的思考方式。
渐进式的含义,我的理解是:没有多做职责之外的事,主张最少。也就是“Progressive”——这个词在英文中定义是渐进,一步一步,不是说你必须一竿子把所有的东西都用上。
有自己的配套工具,核心虽然只解决一个很小的问题(非常专注的只做状态到界面映射,以及组件),但它们有生态圈及配套的可选工具,当你把他们一个一个加进来的时候,就可以组合成非常强大的栈,就可以涵盖其他的这些更完整的框架所涵盖的问题。
这样的一个配置方案,使得在你构建技术栈的时候有可弹性伸缩的工具复杂度:当所要解决的问题内在复杂度很低的时候,可以只用核心的这些很简单的功能;当需要做一个更复杂的应用时,再增添相应的工具。例如做一个单页应用的时候才需要用路由;做一个相当庞大的应用,涉及到多组件状态共享以及多个开发者共同协作时,才可能需要大规模状态管理方案。
Vue.js 的优势
- Vue.js 在可读性、可维护性和趣味性之间做到了很好的平衡。Vue.js 处在 React 和 Angular 1 之间,而且如果你有仔细看 Vue 的指南,就会发现 Vue.js 从其它框架借鉴了很多设计理念。
- Vue.js 从 React 那里借鉴了组件化、prop、单向数据流、性能、虚拟渲染,并意识到状态管理的重要性。
- Vue.js 从 Angular 那里借鉴了模板,并赋予了更好的语法,以及双向数据绑定(在单个组件里)。
- 从我们团队使用 Vue.js 的情况来看,Vue.js 使用起来很简单。它不强制使用某种编译器,所以你完全可以在遗留代码里使用 Vue,并对之前乱糟糟的 jQuery 代码进行改造。
vue特点:
- 核心只关注视图(view)
- 易学,轻量,灵活
- 适用于移动端项目
- 渐进式框架
两个核心点:
1.响应的数据变化当数据发生变化----视图自动更新2.组合的视图组件UI页面映射为组件树划分组件可维护、可复用、可测试
虚拟DOM
Vue.js(2.0版本)使用了一种叫'Virtual DOM'的东西。所谓的Virtual DOM基本上说就是它名字的意思:虚拟DOM,DOM树的虚拟表现。它的诞生是基于这么一个概念:改变真实的DOM状态远比改变一个JavaScript对象的花销要大得多。
Virtual DOM是一个映射真实DOM的JavaScript对象,如果需要改变任何元素的状态,那么是先在Virtual DOM上进行改变,而不是直接改变真实的DOM。当有变化产生时,一个新的Virtual DOM对象会被创建并计算新旧Virtual DOM之间的差别。之后这些差别会应用在真实的DOM上。·
例子如下,我们可以看看下面这个列表在HTML中的代码是如何写的:
<ul class="list"><li>item 1</li><li>item 2</li>
</ul>
而在JavaScript中,我们可以用对象简单地创造一个针对上面例子的映射:
//code from http://caibaojian.com/vue-vs-react.html
{type: 'ul', props: {'class': 'list'}, children: [{ type: 'li', props: {}, children: ['item 1'] },{ type: 'li', props: {}, children: ['item 2'] }]
}
真实的Virtual DOM会比上面的例子更复杂,但它本质上是一个嵌套着数组的原生对象。
当新一项被加进去这个JavaScript对象时,一个函数会计算新旧Virtual DOM之间的差异并反应在真实的DOM上。计算差异的算法是高性能框架的秘密所在,React和Vue在实现上有点不同。
Vue宣称可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
小结:如果你的应用中,交互复杂,需要处理大量的UI变化,那么使用Virtual DOM是一个好主意。如果你更新元素并不频繁,那么Virtual DOM并不一定适用,性能很可能还不如直接操控DOM。
在学习中,我们没必要一上来就搞懂Vue的每一个部件和功能,先从核心功能开始学习,逐渐扩展。 同时,在使用中,我们也没有必要把全部件能都拿出来,需要什么用什么就是了,而且也可以把Vue很方便的与其它已有项目或框架相结合。
vue生命周期(钩子函数)
beforeCreate、created(此时需说明可以在created中首次拿到data中定义的数据)、beforeMount、mounted(此时需说明dom树渲染结束,可访问dom结构)、beforeUpdate、updated、beforeDestroy、destroyed
computed中的getter和setter
很多情况,我问到这个问题的时候对方的回答都是vue的getter和setter、订阅者模式之类的回答,我就会直接说问的并不是这个,而是computed,直接让对方说computed平时怎么使用,很多时候得到的回答是computed的默认方式,只使用了其中的getter,就会继续追问如果想要再把这个值设置回去要怎么做,当然一般会让问到这个程度的这个问题他都答不上来了。
<!--直接复制的官网示例-->
computed: {fullName: {// getterget: function () {return this.firstName + ' ' + this.lastName},// setterset: function (newValue) {var names = newValue.split(' ')this.firstName = names[0]this.lastName = names[names.length - 1]}}
}
v-for循环key的作用
key的作用就可以给他一个标识,让状态跟着数据渲染。
所以一句话,key的作用主要是为了高效的更新虚拟DOM。另外vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。
$nextTick
在下次 DOM 更新循环 结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。(官网解释)
解决的问题:有些时候在改变数据后立即要对dom进行操作,此时获取到的dom仍是获取到的是数据刷新前的dom,无法满足需要,这个时候就用到了$nextTick。
$set
向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性 (比如 this.myObject.newProperty = 'hi')(官方示例)
我自己的理解就是,在vue中对一个对象内部进行一些修改时,vue没有监听到变化无法触发视图的更新,此时来使用$set来触发更新,使视图更新为最新的数据。
组件间的传值
- provide / inject
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
- Vue.observable
让一个对象可响应。Vue 内部会用它来处理 data 函数返回的对象。
返回的对象可以直接用于渲染函数和计算属性内,并且会在发生改变时触发相应的更新。也可以作为最小化的跨组件状态存储器,用于简单的场景:
const state = Vue.observable({ count: 0 })const Demo = {render(h) {return h('button', {on: { click: () => { state.count++ }}}, `count is: ${state.count}`)}
}
- $attrs
包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。
- $listeners
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。
- props
- $emit
- eventbus
- vuex
- $parent / $children / ref
参考:Vue学习看这篇就够 、Vue 2.0,渐进式前端解决方案-尤雨溪、 Vue2.0 中,“渐进式框架”和“自底向上增量开发的设计”这两个概念是什么?-徐飞、一句话理解Vue核心内容、Vue与React两个框架的区别和优势对比、Vue2.0 v-for 中 :key 到底有什么用?
为什么要是用sass这些css预处理器
Sass(Syntactically Awesome Style Sheets)是一个相对新的编程语言,Sass为web前端开发而生,可以用它来定义一套新的语法规则和函数,以加强和提升CSS。通过这种新的编程语言,你可以使用最高效的方式,以少量的代码创建复杂的设计。它改进并增强了CSS的能力,增加了变量,局部和函数这些特性。
优势
- 易维护,更方便的定制
- 对于一个大型或者稍微有规模的UI来说,如果需要替换下整体风格,或者是某个字体的像素值,比如我们经常会遇到panel,window以及portal共用一个背景色,这个时候按照常规的方式,我们需要一个个定位到元素使用的class,然后逐个替换,SASS提供了变量的方式,你可以把某个样式作为一个变量,然后各个class引用这个变量即可,修改时,我们只需修改对应的变量。
- 对于编程人员的友好
- 对于一个没有前端基础的编程人员,写css样式是一件非常痛苦的事情,他们会感觉到各种约束,为什么我不能定一个变量来避免那些类似“变量”的重复书写?为什么我不能继承上个class的样式定义?。。。SASS/SCSS正是帮编程人员解开了这些疑惑,让css看起来更像是一门编程语言。
- 效率的提升
- 对于一个前端开发人员来说,我不熟悉编程,也不关注css是否具有的一些编程语言特性,但这不是你放弃他的理由,css3的发展,加之主流浏览器的兼容性不一,很多浏览器都有自己的兼容hack,很多时候我们需要针对不同的浏览器写一堆的hack,这种浪费时间的重复劳动就交给SASS处理去吧!
如何防止XSS攻击?
什么是XSS?
“XSS是跨站脚本攻击(Cross Site Scripting),为不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的(攻击者可获取用户的敏感信息如 Cookie、SessionID、劫持流量实现恶意跳转 等)。”(即页面被注入了恶意代码)
XSS 的本质是:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。
危害:
而由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,或者利用这些信息冒充用户向网站发起攻击者定义的请求。
XSS 有哪些注入的方法:
- 在 HTML 中内嵌的文本中,恶意内容以 script 标签形成注入。
- 在内联的 JavaScript 中,拼接的数据突破了原本的限制(字符串,变量,方法名等)。
- 在标签属性中,恶意内容包含引号,从而突破属性值的限制,注入其他属性或者标签。
- 在标签的 href、src 等属性中,包含
javascript:等可执行代码。 - 在 onload、onerror、onclick 等事件中,注入不受控制代码。
- 在 style 属性和标签中,包含类似
background-image:url("javascript:...");的代码(新版本浏览器已经可以防范)。 - 在 style 属性和标签中,包含类似
expression(...)的 CSS 表达式代码(新版本浏览器已经可以防范)。
总之,如果开发者没有将用户输入的文本进行合适的过滤,就贸然插入到 HTML 中,这很容易造成注入漏洞。攻击者可以利用漏洞,构造出恶意的代码指令,进而利用恶意代码危害数据安全。(输入过滤)
用户是通过哪种方法“注入”恶意脚本的呢?
不仅仅是业务上的“用户的 UGC 内容”可以进行注入,包括 URL 上的参数等都可以是攻击的来源。在处理输入时,以下内容都不可信:
- 来自用户的 UGC 信息
- 来自第三方的链接
- URL 参数
- POST 参数
- Referer (可能来自不可信的来源)
- Cookie (可能来自其他子域注入)
XSS 分类
根据攻击的来源,XSS 攻击可分为存储型、反射型和 DOM 型三种。
|类型|存储区|插入点| |-|-|
|存储型 XSS|后端数据库|HTML|
|反射型 XSS|URL|HTML|
|DOM 型 XSS|后端数据库/前端存储/URL|前端 JavaScript|
- 存储区:恶意代码存放的位置。
- 插入点:由谁取得恶意代码,并插入到网页上。
存储型 XSS
存储型 XSS 的攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中。
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
反射型 XSS
反射型 XSS 的攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。(邮件发送)
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。
由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。
DOM 型 XSS
DOM 型 XSS 的攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL。
- 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。(直接插入到页面中)
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。
XSS 攻击的预防
通过前面的介绍可以得知,XSS 攻击有两大要素:
- 攻击者提交恶意代码。
- 浏览器执行恶意代码。
输入过滤
在用户提交时,由前端过滤输入,然后提交到后端。这样做是否可行呢?
答案是不可行。一旦攻击者绕过前端过滤,直接构造请求,就可以提交恶意代码了。
那么,换一个过滤时机:后端在写入数据库前,对输入进行过滤,然后把“安全的”内容,返回给前端。这样是否可行呢?
我们举一个例子,一个正常的用户输入了 5 < 7 这个内容,在写入数据库前,被转义,变成了 5 < 7。
问题是:在提交阶段,我们并不确定内容要输出到哪里。
这里的“并不确定内容要输出到哪里”有两层含义:
- 用户的输入内容可能同时提供给前端和客户端,而一旦经过了
escapeHTML(),客户端显示的内容就变成了乱码(5 < 7)。 -
在前端中,不同的位置所需的编码也不同。
当<div title="comment">5 < 7</div>5 < 7作为 HTML 拼接页面时,可以正常显示:
当 5 < 7 通过 Ajax 返回,然后赋值给 JavaScript 的变量时,前端得到的字符串就是转义后的字符。这个内容不能直接用于 Vue 等模板的展示,也不能直接用于内容长度计算。不能用于标题、alert 等。
所以,输入侧过滤能够在某些情况下解决特定的 XSS 问题,但会引入很大的不确定性和乱码问题。在防范 XSS 攻击时应避免此类方法。
当然,对于明确的输入类型,例如数字、URL、电话号码、邮件地址等等内容,进行输入过滤还是必要的。
既然输入过滤并非完全可靠,我们就要通过“防止浏览器执行恶意代码”来防范 XSS。这部分分为两类:
- 防止 HTML 中出现注入。
- 防止 JavaScript 执行时,执行恶意代码。(输出转义,HTML 的编码是十分复杂的,在不同的上下文里要使用相应的转义规则。)
预防存储型和反射型 XSS 攻击
存储型和反射型 XSS 都是在服务端取出恶意代码后,插入到响应 HTML 里的,攻击者刻意编写的“数据”被内嵌到“代码”中,被浏览器所执行。
预防这两种漏洞,有两种常见做法:
- 改成纯前端渲染,把代码和数据分隔开。
- 对 HTML 做充分转义。
纯前端渲染
纯前端渲染的过程:
- 浏览器先加载一个静态 HTML,此 HTML 中不包含任何跟业务相关的数据。
- 然后浏览器执行 HTML 中的 JavaScript。
- JavaScript 通过 Ajax 加载业务数据,调用 DOM API 更新到页面上。
在纯前端渲染中,我们会明确的告诉浏览器:下面要设置的内容是文本(.innerText),还是属性(.setAttribute),还是样式(.style)等等。浏览器不会被轻易的被欺骗,执行预期外的代码了。
但纯前端渲染还需注意避免 DOM 型 XSS 漏洞(例如 onload 事件和 href 中的 javascript:xxx 等,请参考下文”预防 DOM 型 XSS 攻击“部分)。
在很多内部、管理系统中,采用纯前端渲染是非常合适的。但对于性能要求高,或有 SEO 需求的页面,我们仍然要面对拼接 HTML 的问题。
转义 HTML
如果拼接 HTML 是必要的,就需要采用合适的转义库,对 HTML 模板各处插入点进行充分的转义。
常用的模板引擎,如 doT.js、ejs、FreeMarker 等,对于 HTML 转义通常只有一个规则,就是把 & < > " ' / 这几个字符转义掉,确实能起到一定的 XSS 防护作用,但并不完善:
|XSS 安全漏洞|简单转义是否有防护作用| |-|-| |HTML 标签文字内容|有| |HTML 属性值|有| |CSS 内联样式|无| |内联 JavaScript|无| |内联 JSON|无| |跳转链接|无|
所以要完善 XSS 防护措施,我们要使用更完善更细致的转义策略。
预防 DOM 型 XSS 攻击
DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。
在使用 .innerHTML、.outerHTML、document.write() 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent、.setAttribute() 等。
如果用 Vue/React 技术栈,并且不使用 v-html/dangerouslySetInnerHTML 功能,就在前端 render 阶段避免 innerHTML、outerHTML 的 XSS 隐患。
DOM 中的内联事件监听器,如 location、onclick、onerror、onload、onmouseover 等,<a> 标签的 href 属性,JavaScript 的 eval()、setTimeout()、setInterval() 等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。
注意:
- 防范存储型和反射型 XSS 是后端 RD 的责任。而 DOM 型 XSS 攻击不发生在后端,是前端 RD 的责任。防范 XSS 是需要后端 RD 和前端 RD 共同参与的系统工程。 * 转义应该在输出 HTML 时进行,而不是在提交用户输入时。
- 不同的上下文,如 HTML 属性、HTML 文字内容、HTML 注释、跳转链接、内联 JavaScript 字符串、内联 CSS 样式表等,所需要的转义规则不一致。 业务 RD 需要选取合适的转义库,并针对不同的上下文调用不同的转义规则。
虽然很难通过技术手段完全避免 XSS,但我们可以总结以下原则减少漏洞的产生:
- 利用模板引擎 开启模板引擎自带的 HTML 转义功能。例如: 在 ejs 中,尽量使用
<%= data %>而不是<%- data %>; 在 doT.js 中,尽量使用{{! data }而不是{{= data }; 在 FreeMarker 中,确保引擎版本高于 2.3.24,并且选择正确的freemarker.core.OutputFormat。 - 避免内联事件 尽量不要使用
onLoad="onload('{{data}}')"、onClick="go('{{action}}')"这种拼接内联事件的写法。在 JavaScript 中通过.addEventlistener()事件绑定会更安全。 - 避免拼接 HTML 前端采用拼接 HTML 的方法比较危险,如果框架允许,使用
createElement、setAttribute之类的方法实现。或者采用比较成熟的渲染框架,如 Vue/React 等。 - 时刻保持警惕 在插入位置为 DOM 属性、链接等位置时,要打起精神,严加防范。
- 增加攻击难度,降低攻击后果 通过 CSP、输入长度配置、接口安全措施等方法,增加攻击的难度,降低攻击的后果。
- 主动检测和发现 可使用 XSS 攻击字符串和自动扫描工具寻找潜在的 XSS 漏洞
参考:前端安全系列(一):如何防止XSS攻击?、实现基于 Nuxt.js 的 SSR 应用(SEO、SPA、SSR、首屏渲染、Nuxt.js)
跨域
什么是同源策略及其限制内容
同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制内容有:
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM 节点
- AJAX 请求发送后,结果被浏览器拦截了
但是有三个标签是允许跨域加载资源:
<img src=XXX><link href=XXX><script src=XXX>
当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。
跨域解决方案
1.jsonp
1) JSONP原理
利用 <script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。
2) JSONP和AJAX对比
JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)
3) JSONP优缺点
JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。
4) JSONP的实现流程
- 声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
- 创建一个
<script>标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。 - 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是
show('我不爱你')。 - 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。
在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP函数。
// index.html
function jsonp({ url, params, callback }) {return new Promise((resolve, reject) => {let script = document.createElement('script')window[callback] = function(data) {resolve(data)document.body.removeChild(script)}params = { ...params, callback } // wd=b&callback=showlet arrs = []for (let key in params) {arrs.push(`${key}=${params[key]}`)}script.src = `${url}?${arrs.join('&')}`document.body.appendChild(script)})
}
jsonp({url: 'http://localhost:3000/say',params: { wd: 'Iloveyou' },callback: 'show'
}).then(data => {console.log(data)
})
复制代码
上面这段代码相当于向http://localhost:3000/say?wd=Iloveyou&callback=show这个地址请求数据,然后后台返回show('我不爱你'),最后会运行show()这个函数,打印出'我不爱你'
// server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {let { wd, callback } = req.queryconsole.log(wd) // Iloveyouconsole.log(callback) // showres.end(`${callback}('我不爱你')`)
})
app.listen(3000)
2.CORS
全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
两种请求
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
(2)HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的。
简单请求
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。
GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8
上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。
(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。
Access-Control-Allow-Credentials: true
另一方面,开发者必须在AJAX请求中打开withCredentials属性。
var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。
xhr.withCredentials = false;
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
非简单请求
预检请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
下面是一段浏览器的JavaScript脚本。
var url = 'http://api.alice.com/cors'; var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('X-Custom-Header', 'value'); xhr.send();
上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header。
浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。
OPTIONS /cors HTTP/1.1 Origin: http://api.bob.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。
除了Origin字段,"预检"请求的头信息包括两个特殊字段。
(1)Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。
HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain
上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。
Access-Control-Allow-Origin: *
如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。
XMLHttpRequest cannot load http://api.alice.com. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
服务器回应的其他CORS相关字段如下。
Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000
(1)Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
(2)Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
(3)Access-Control-Allow-Credentials
该字段与简单请求时的含义相同。
(4)Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
浏览器的正常请求和回应
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
下面是"预检"请求之后,浏览器的正常CORS请求。
PUT /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com X-Custom-Header: value Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
上面头信息的Origin字段是浏览器自动添加的。
下面是服务器正常的回应。
Access-Control-Allow-Origin: http://api.bob.com Content-Type: text/html; charset=utf-8
上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。
与JSONP的比较
CORS与JSONP的使用目的相同,但是比JSONP更强大。
JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
参考:跨域资源共享 CORS 详解
3.postMessage
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一(所以双方都需要在浏览器端才能相互跨域连接?),它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
otherWindow.postMessage(message, targetOrigin, [transfer]);
- message: 将要发送到其他 window的数据。
- targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
- transfer(可选):是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
接下来我们看个例子: http://localhost:3000/a.html页面向http://localhost:4000/b.html传递“我爱你”,然后后者传回"我不爱你"。
// a.html<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件//内嵌在http://localhost:3000/a.html<script>function load() {let frame = document.getElementById('frame')frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据window.onmessage = function(e) { //接受返回数据console.log(e.data) //我不爱你}}</script>
复制代码
// b.htmlwindow.onmessage = function(e) {console.log(e.data) //我爱你e.source.postMessage('我不爱你', e.origin)}
复制代码
4.websocket
Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
我们先来看个例子:本地文件socket.html向localhost:3000发生数据和接受数据
// socket.html
<script>let socket = new WebSocket('ws://localhost:3000');socket.onopen = function () {socket.send('我爱你');//向服务器发送数据}socket.onmessage = function (e) {console.log(e.data);//接收服务器返回的数据}
</script>
复制代码
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {ws.on('message', function (data) {console.log(data);ws.send('我不爱你')});
})
复制代码
5. Node中间件代理(两次跨域)
实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。代理服务器,需要做以下几个步骤:
- 接受客户端请求 。
- 将请求 转发给服务器。
- 拿到服务器 响应 数据。
- 将响应转发给客户端。
我们先来看个例子:本地文件index.html文件,通过代理服务器http://localhost:3000向目标服务器http://localhost:4000请求数据。
// index.html(http://127.0.0.1:5500)<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script><script>$.ajax({url: 'http://localhost:3000',type: 'post',data: { name: 'xiamen', password: '123456' },contentType: 'application/json;charset=utf-8',success: function(result) {console.log(result) // {"title":"fontend","password":"123456"}},error: function(msg) {console.log(msg)}})</script>
复制代码
// server1.js 代理服务器(http://localhost:3000)
const http = require('http')
// 第一步:接受客户端请求
const server = http.createServer((request, response) => {// 代理服务器,直接和浏览器直接交互,需要设置CORS 的首部字段response.writeHead(200, {'Access-Control-Allow-Origin': '*','Access-Control-Allow-Methods': '*','Access-Control-Allow-Headers': 'Content-Type'})// 第二步:将请求转发给服务器const proxyRequest = http.request({host: '127.0.0.1',port: 4000,url: '/',method: request.method,headers: request.headers},serverResponse => {// 第三步:收到服务器的响应var body = ''serverResponse.on('data', chunk => {body += chunk})serverResponse.on('end', () => {console.log('The data is ' + body)// 第四步:将响应结果转发给浏览器response.end(body)})}).end()
})
server.listen(3000, () => {console.log('The proxyServer is running at http://localhost:3000')
})
复制代码
// server2.js(http://localhost:4000)
const http = require('http')
const data = { title: 'fontend', password: '123456' }
const server = http.createServer((request, response) => {if (request.url === '/') {response.end(JSON.stringify(data))}
})
server.listen(4000, () => {console.log('The server is running at http://localhost:4000')
})
复制代码
上述代码经过两次跨域,值得注意的是浏览器向代理服务器发送请求,也遵循同源策略,最后在index.html文件打印出{"title":"fontend","password":"123456"}
6.nginx反向代理
实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。
使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
7.window.name + iframe
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
其中a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000
// a.html(http://localhost:3000/b.html)<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe><script>let first = true// onload事件会触发2次,第1次加载跨域页,并留存数据于window.namefunction load() {if(first){// 第1次onload(跨域页)成功后,切换到同域代理页面let iframe = document.getElementById('iframe');iframe.src = 'http://localhost:3000/b.html';first = false;}else{// 第2次onload(同域b.html页)成功后,读取同域window.name中数据console.log(iframe.contentWindow.name);}}</script>
复制代码
b.html为中间代理页,与a.html同域,内容为空。
// c.html(http://localhost:4000/c.html)<script>window.name = '我不爱你' </script>
复制代码
总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
8.location.hash + iframe
实现原理: a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现步骤:一开始a.html给c.html传一个hash值,然后c.html收到hash值后,再把hash值传递给b.html,最后b.html将结果放到a.html的hash值中。 同样的,a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000
// a.html<iframe src="http://localhost:4000/c.html#iloveyou"></iframe><script>window.onhashchange = function () { //检测hash的变化console.log(location.hash);}</script>
复制代码
// b.html<script>window.parent.parent.location.hash = location.hash //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面</script>
复制代码
// c.htmlconsole.log(location.hash);let iframe = document.createElement('iframe');iframe.src = 'http://localhost:3000/b.html#idontloveyou';document.body.appendChild(iframe);
复制代码
9.document.domain + iframe
该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。 只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
我们看个例子:页面a.zf1.cn:3000/a.html获取页面b.zf1.cn:3000/b.html中a的值
// a.html
<body>helloa<iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe><script>document.domain = 'zf1.cn'function load() {console.log(frame.contentWindow.a);}</script>
</body>
复制代码
// b.html
<body>hellob<script>document.domain = 'zf1.cn'var a = 100;</script>
</body>
复制代码
三、总结
- CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
- JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
- 不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。
- 日常工作中,用得比较多的跨域方案是cors和nginx反向代理
参考:九种跨域方式实现原理(完整版)
web语义化
分为html语义化、css语义化和url语义化
1)HTML语义化——使用恰当的标签结构来规范内容,使有利于开发人员以及屏幕阅读器(访客有视障)以及SEO读懂和识别
<html><body><article><header><h1>h1 - WEB 语义化</h1></header><nav><ul><li>nav1 - HTML语义化</li><li>nav2 - CSS语义化</li></ul></nav><section>section1 - HTML语义化</section><section>section2 - CSS语义化</section><time datetime="2018-03-23" pubdate>time - 2018年03月23日</time><footer> footer - by 小维</footer></article></body>
</html>
- header代表“网页”或者“section”的页眉,通常包含h1-h6 元素或者 hgroup, 作为整个页面或者一个内容快的标题。
hgroup元素代表“网页”或“section”的标题组,当元素有多个层级时,该元素可以将h1到h6元素放在其内,譬如文章的主标题和副标题组合footer元素代表“网页”或任意“section”的页脚- nav 元素代表页面的导航链接区域。用于定义页面的主要导航部分。
- article 代表一个在文档,页面或者网站中自成一体的内容,其目的是为了让开发者独立开发或重用。
文章内section是独立的部分,但是它们只能算是组成整体的一部分,从属关系,article是大主体,section是构成这个大主体的一个部分。
注意事项:
·自身独立情况下:用article
·是相关内容: 用section
·没有语义的: 用div
section元素代表文档中的“节”或“段”,“段”可以是指一片文章里按照主题的分段;“节”可以是指一个页面里的分组。section通常还带标题,虽然html5中section会自动给标题h1-h6降级,但是最好手动给他们降级。aside元素被包含在article元素中作为主要内容的附属信息部分,其中的内容可以是与当前文章有关的相关资料,标签,名词解释等。
在article元素之外使用作为页面或站点全局的附属信息部分。最典型的是侧边栏,其中的内容可以是日志串连,其他组的导航,甚至广告,这些内容相关的页面。
aside 在 article 内表示主要内容的附属信息。
在article之外侧可以做侧边栏,没有article与之对应,最好不用
如果是广告,其他日志链接或者其他分类导航也可以用。
- figure元素包含图像、图表和照片。figure标记可以包含figcaption,figcaption表示图像对应的描述文字,与图片产生对应关系。
<figure><img src="/figure.jpg" width="304" height="228" alt="Picture"><figcaption>Caption for the figure</figcaption>
</figure>
- 一些常用的媒体元素包含:audio/video/source/embed
<audio id="audioPlay"><source src="../h5/music/act/iphonewx/shakeshake.mp3" type="audio/mpeg">您的浏览器不支持 audio 标签。
</audio>
总之,HTML语义化是反对大篇幅使用无语义化的div+span+class,而鼓励使用HTML定义好的语义化标签。
当然,如果需要兼容低版本的IE浏览器,比如说IE8以及以下,那就需要考虑一些HTML5标签兼容性解决方案了。
2)css语义化——就是class和id命名的规范,如果说HTML语义化标签是给机器看的,那么CSS命名的语义化就是给人看的。良好的CSS命名方式减少沟通调试成本,易于理解。
- 看到这里,问题来了。既然CSS class和ID命名的语义化可以便于阅读理解和减少沟通调试成本,那么我们是不是可以用div 结合class和ID语义化命名的方式来代替html的语义化?
- 从代码的层面上来看,使用CSS class语义化的命名也是能够便于阅读和维护的,但是这样子并不利于SEO和屏幕阅读器识别
3)URL语义化——可以使得搜索引擎或者爬虫更好的理解·当前URL所在目录所要表达的意思,而对于用户来说,通过url也可以判断上一级目录或者下一级目录想要表示的内容,可以提高用户体验。
例如我司的搜索品类的url:
url语义化可以从以下标准来衡量:
- url简化,规范化:url里的名词如果包含两个单词,那么就用下划线_ 连接。
- 结构化,语义化:此处的品类搜索我们用语义化单词category表示
- 采用技术无关的url:第一个链接中的index.php这种就不应该出现在用户侧的url里。
参考:如何理解Web语义化、HTML 5的革新之一:语义化标签一节元素标签
HTML 5的革新——语义化标签(二)
nodeJS
什么是nodeJS
三个特性:
- 服务器端JavaScript处理:server-side JavaScript execution
- 非阻断/异步I/O:non-blocking or asynchronous I/O
- 事件驱动:Event-driven
简单的说 Node.js 就是运行在服务端的 JavaScript。
Node.js是一个基于ChromeV8引擎的JavaScript运行环境。Node.js使用了一个事件驱动、非阻塞式I/O的模型,使其轻量又高效。
为什么是js
JavaScript 是一个单线程的语言,单线程的优点是不会像 Java 这些多线程语言在编程时出现线程同步、线程锁问题同时也避免了上下文切换带来的性能开销问题,那么其实在浏览器环境也只能是单线程,可以想象一下多线程对同一个 DOM 进行操作是什么场景?不是乱套了吗?那么单线程可能你会想到的一个问题是,前面一个执行不完,后面不就卡住了吗?当然不能这样子的,JavaScript 是一种采用了事件驱动、异步回调的模式,另外 JavaScript 在服务端不存在什么历史包袱,在虚拟机上由于又有了 Chrome V8 的支持,使得 JavaScript 成为了 Node.js 的首选语言。
Node.js 架构
Node.js 由 Libuv、Chrome V8、一些核心 API 构成,如下图所示:
以上展示了 Node.js 的构成,下面做下简单说明:
- Node Standard Library:Node.js 标准库,对外提供的 JavaScript 接口,例如模块 http、buffer、fs、stream 等
- Node bindings:这里就是 JavaScript 与 C++ 连接的桥梁,对下层模块进行封装,向上层提供基础的 API 接口。
- V8:Google 开源的高性能 JavaScript 引擎,使用 C++ 开发,并且应用于谷歌浏览器。如果您感兴趣想学习更多的 V8 引擎知识,请访问 What is V8?
- Libuv:是一个跨平台的支持事件驱动的 I/O 库。它是使用 C 和 C++ 语言为 Node.js 所开发的,同时也是 I/O 操作的核心部分,例如读取文件和 OS 交互。来自一份 Libuv 的中文教程
- C-ares:C-ares 是一个异步 DNS 解析库
- Low-Level Components:提供了 http 解析、OpenSSL、数据压缩(zlib)等功能。
Node.js 特点
在这之前不知道您有没有听说过,Node.js 很擅长 I/O 密集型任务,应对一些 I/O 密集型的高并发场景还是很有优势的,事实也如此,这也是它的定位:提供一种简单安全的方法在 JavaScript 中构建高性能和可扩展的网络应用程序。
- 单线程
Node.js 使用单线程来运行,而不是向 Apache HTTP 之类的其它服务器,每个请求将生产一个线程,这种方法避免了 CPU 上下文切换和内存中的大量执行堆栈,这也是 Nginx 和其它服务器为解决 “上一个 10 年,著名的 C10K 并发连接问题” 而采用的方法。
- 非阻塞 I/O
Node.js 避免了由于需要等待输入或者输出(数据库、文件系统、Web服务器…)响应而造成的 CPU 时间损失,这得益于 Libuv 强大的异步 I/O。
- 事件驱动编程
事件与回调在 JavaScript 中已是屡见不鲜,同时这种编程对于习惯同步思路的同学来说可能一时很难理解,但是这种编程模式,确是一种高性能的服务模型。Node.js 与 Nginx 均是基于事件驱动的方式实现,不同之处在于 Nginx 采用纯 C 进行编写,仅适用于 Web 服务器,在业务处理方面 Node.js 则是一个可扩展、高性能的平台。
- 跨平台
起初 Node.js 只能运行于 Linux 平台,在 v0.6.0 版本后得益于 Libuv 的支持可以在 Windows 平台运行。
Node.js 的优势主要在于事件循环,非阻塞异步 I/O,只开一个线程,不会每个请求过来我都去创建一个线程,从而产生资源开销。
模块加载机制
面试中可能会问到能说下 require 的加载机制吗?
在 Node.js 中模块加载一般会经历 3 个步骤,路径分析、文件定位、编译执行。
按照模块的分类,按照以下顺序进行优先加载:
- 系统缓存:模块被执行之后会会进行缓存,首先是先进行缓存加载,判断缓存中是否有值。
- 系统模块:也就是原生模块,这个优先级仅次于缓存加载,部分核心模块已经被编译成二进制,省略了
路径分析、文件定位,直接加载到了内存中,系统模块定义在 Node.js 源码的 lib 目录下,可以去查看。 - 文件模块:优先加载
.、..、/开头的,如果文件没有加上扩展名,会依次按照.js、.json、.node进行扩展名补足尝试,那么在尝试的过程中也是以同步阻塞模式来判断文件是否存在,从性能优化的角度来看待,.json、.node最好还是加上文件的扩展名。 - 目录做为模块:这种情况发生在文件模块加载过程中,也没有找到,但是发现是一个目录的情况,这个时候会将这个目录当作一个
包来处理,Node 这块采用了 Commonjs 规范,先会在项目根目录查找 package.json 文件,取出文件中定义的 main 属性("main": "lib/hello.js")描述的入口文件进行加载,也没加载到,则会抛出默认错误: Error: Cannot find module 'lib/hello.js' - node_modules 目录加载:对于系统模块、路径文件模块都找不到,Node.js 会从当前模块的父目录进行查找,直到系统的根目录
require 模块加载时序图
参考:Node.js 是什么?我为什么选择它?、浅谈 Node.js 模块机制及常见面试问题解答
CommonJS
我们知道Node.js的实现让js也可以成为后端开发语言,
但在早先Node.js开发过程中,它的作者发现在js中并没有像其他后端语言一样有包引入和模块系统的机制。
这就意味着js的所有变量,函数都在全局中定义。这样不但会污染全局变量,更会导致暴露函数内部细节等问题。
CommonJS组织也意识到了同样的问题,于是 CommonJS组织创造了一套js模块系统的规范。我们现在所说的CommonJS指的就是这个规范。
Node 应用由模块组成,采用 CommonJS 模块规范。
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
CommonJS使用 require 来加载代码,而 exports 和 module.exports 则用来导出代码。
- module.exports 初始值为一个空对象 {}
- exports 是指向的 module.exports 的引用
- require() 默认返回的是 module.exports 而不是 exports
exports 相当于 module.exports 的快捷方式如下所示:
const exports = modules.exports;
但是要注意不能改变 exports 的指向,我们可以通过 exports.test = 'a' 这样来导出一个对象, 但是不能向下面示例直接赋值,这样会改变 exports 的指向
// 错误的写法 将会得到 undefined
exports = {'a': 1,'b': 2
}// 正确的写法
modules.exports = {'a': 1,'b': 2
}
CommonJS模块的特点如下。
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。(同步)
AMD规范与CommonJS规范的兼容性
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
AMD规范使用define方法定义模块,下面就是一个例子:
define(['package/lib'], function(lib){function foo(){lib.log('hello world!');}return {foo: foo};
});
AMD规范允许输出的模块兼容CommonJS规范,这时define方法需要写成下面这样:
define(function (require, exports, module){var someModule = require("someModule");var anotherModule = require("anotherModule");someModule.doTehAwesome();anotherModule.doMoarAwesome();exports.asplode = function (){someModule.doTehAwesome();anotherModule.doMoarAwesome();};
});
AMD
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:
require(['math'], function (math) {
math.add(2, 3);
});
math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。
我们经常看到这样的写法:
exports = module.exports = somethings
上面的代码等价于:
module.exports = somethings
exports = module.exports
原理很简单,即 module.exports 指向新的对象时,exports 断开了与 module.exports 的引用,那么通过 exports = module.exports 让 exports 重新指向 module.exports 即可。
参考:CommonJS规范、 exports 和 module.exports 的区别