威海企业网站建设,造价师注册管理系统,新工商名录企业应用平台,建立旅游网站的目的知识点都是搜集各种大佬们的#xff0c;如有冒犯#xff0c;请告知#xff01; 目录
原型链
New关键字的执行过程 ES6——class
constructor方法
类的实例对象
不存在变量提升
super 关键字
ES6——...#xff08;展开/收集#xff09;运算符 面向对象的理解
关…知识点都是搜集各种大佬们的如有冒犯请告知 目录
原型链
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新功能
七、总结
cookie、session、token
cookie 是什么
创建 cookie
Session
Token
MVC、MVP和MVVM
MVCModelViewController
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]]属性的值就是它的构造函数所对应的原型对象prototypevar one {x: 1};
var two new Object();
one.__proto__ Object.prototype // true
two.__proto__ Object.prototype // true
one.toString one.__proto__.toString // trueJS不像其它面向对象的语言它没有类classES6引进了这个关键字但更多是语法糖的概念。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 // trueFunction本身就是函数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指向实例对象。new Foo 等同于 new Foo()也就是没有指定参数列表Foo 不带任何参数调用的情况。由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象则使用步骤1创建的对象。一般情况下构造函数不返回值但是用户可以选择主动返回对象来覆盖正常的对象创建步骤ES6——class
总结ES6的class类其实就是等同于ES5的构造函数作用都是生成新对象只不过class在结构形式上会和传统的面向对象语言如cjava这些更类似、切合。因为传统构造方法在方法实现上与面向对象编程差别很大为了让对象原型的写法更加清晰、更像面向对象编程故引入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是一个 Promise3 秒之后变为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的状态都变成fulfilledp的状态才会变成fulfilled此时p1、p2、p3的返回值组成一个数组传递给p的回调函数。
2只要p1、p2、p3之中有一个被rejectedp的状态就变成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 assignmentTypeError
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 constructorRangeError
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 33var 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.childb;b.parenta;因为有这个严重的缺点这个算法在现代浏览器中已经被下面要介绍的标记清除算法所取代了。但绝不可认为该问题已经不再存在了因为还占有大量市场的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中就是全局对象出发定时扫描内存中的对象。凡是能从根部到达的对象都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用稍后进行回收。从这个概念可以看出无法触及的对象包含了没有引用的对象这个概念没有任何引用的对象也是无法触及的对象。但反之未必成立。因此当divIE例子与其时间处理函数不能再从全局对象出发触及的时候垃圾回收器就会标记并回收这两个对象在匿名函数中调用div就不会出现在全局环境了。如何写出对内存管理友好的JS代码
如果还需要兼容老旧浏览器那么就需要注意代码中的循环引用问题。或者直接采用保证兼容性的库来帮助优化代码。
对现代浏览器来说唯一要注意的就是明确切断需要回收的对象与根部的联系。有时候这种联系并不明显且因为标记清除算法的强壮性这个问题较少出现。最常见的内存泄露一般都与DOM元素绑定有关
参考JavaScript 内存泄漏教程、JavaScript 内存机制前端同学进阶必备 提升性能有关
阻塞式脚本合并文件减少http请求将script标签放在body尾部减少页面csshtml的下载阻塞减少界面的空白时间浏览器在解析到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 nameviewport contentwidthdevice-width,initial-scale1.0,maximum-scale1.0,user-scalableno /设备像素比DPR 设备像素个数 / 理想视口像素个数device-widthrem是相对尺寸单位相对于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 num3;
!!(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.2e3
数组
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函数把返回值放在一个数组中而我们使用数组解构后就可以把返回值直接赋给相应的变量。 fetchFetch 是浏览器提供的原生 AJAX 接口。使用 window.fetch 函数可以代替以前的 $.ajax、$.get 和 $.post即Fetch API 就是浏览器提供的用来代替 jQuery.ajax 的工具可转换成axiosaxios是基于promise封装的网络请求http库在多处框架中被使用 其特点为 支持浏览器和node.js支持promise能拦截请求和响应能转换请求和响应数据能取消请求自动转换JSON数据浏览器端支持防止CSRF(跨站请求伪造)补充ajax即“Asynchronous Javascript And XML”异步 JavaScript 和 XMLajax技术实现了在无需重新加载整个网页的情况下刷新局部数据通过在后台与服务器进行少量数据交换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请求报头域如下
Hostwww.hzau.edu.cn此处使用缺省端口号80若指定了端口号则变成Host指定端口号。
由于一个IP地址可以对应多个域名比如假设我有这么几个域名www.qiniu.comwww.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这个资源。而URNuniform resource name统一资源命名是通过名字来标识资源比如mailto:java-netjava.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 BlockingHOLB导致带宽无法被充分利用以及后续健康请求被阻塞。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 强制使用 HTTPSHTTP2.0 消息头的压缩算法采用 HPACK而非 SPDY 采用的 DEFLATE
HTTP2.0 的新特性
新的二进制格式Binary FormatHTTP1.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 的但是现在主流的浏览器像 chromefirefox 表示还是只支持基于 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 以及 TCPTLS建立连接的延时
HTTP/2都是使用TCP协议来传输的而如果使用HTTPS的话还需要使用TLS协议进行安全传输而使用TLS也需要一个握手过程这样就需要有两个握手延迟过程
①在建立TCP连接的时候需要和服务器进行三次握手来确认连接成功也就是说需要在消耗完1.5个RTT之后才能进行数据传输。
②进行TLS连接TLS有两个版本——TLS1.2和TLS1.3每个版本建立连接所花的时间不同大致是需要1~2个RTT。
总之在传输数据之前我们需要花掉 34 个 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 消息头来创建一个 cookieSet-Cookie消息头是一个字符串其格式如下中括号中的部分是可选的
Set-Cookie: value[; expiresdate][; domaindomain][; pathpath][; 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请求服务器)。
举例
##JWTHA256验证 实施 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 的具体内容这些内容里面有一些是标准字段你也可以添加其它需要的内容。下面是标准字段 issIssuer发行者 subSubject主题 audAudience观众 expExpiration time过期时间 nbfNot before iatIssued at发行时间 jtiJWT 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 里客户端每次向服务端请求资源的时候需要带着服务端签发的 Tokenheader里服务端收到请求然后去验证客户端请求里面带着的 Token利用密匙和数据解出签名和token的签名对比如果相同则通过如果验证成功就向客户端返回请求的数据参考JWT产生和验证Token、Cookie、Session、Token那点事儿原创、彻底理解cookiesessiontoken转、会话(Cookie,Session,Token)管理知识整理(一) MVC、MVP和MVVM
MVCModelViewController
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在概念上是真正将页面与数据逻辑分离的模式在开发方式上它是真正将前台代码开发者JSHTML与后台代码开发者分离的模式asp,asp.net,php,jsp。 ViewModel负责连接 View 和 Model保证视图和数据的一致性这种轻量级的架构让前端开发更加高效、便捷。
MVVM 的出现促进了 GUI 前端开发与后端业务逻辑的分离极大地提高了前端开发效率。MVVM 的核心是 ViewModel 层它就像是一个中转站value converter负责转换 Model 中的数据对象来让数据变得更容易管理和使用该层向上与视图层进行双向数据绑定向下与 Model 层通过接口请求进行数据交互起呈上启下作用。如下图所示 MVVM模式
MVVM 已经相当成熟了主要运用但不仅仅在网络应用程序开发中。KnockoutJS 是最早实现 MVVM 模式的前端框架之一当下流行的 MVVM 框架有 VueAngular 等。
简单画了一张图来说明 MVVM 的各个组成部分 分层设计一直是软件架构的主流设计思想之一MVVM 也不例外。
# View 层
View 是视图层也就是用户界面。前端主要由 HTML 和 CSS 来构建为了更方便地展现 ViewModel 或者 Model 层的数据已经产生了各种各样的前后端模板语言比如 FreeMarker、Marko、Pug、Jinja2等等各大 MVVM 框架如 KnockoutJSVueAngular 等也都有自己用来构建用户界面的内置模板语言。
# 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 的状态改变可以自动传递给 Viewview层的变化也能及时在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状态只是数据状态的一个映射 即 UIVM(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基本上说就是它名字的意思虚拟DOMDOM树的虚拟表现。它的诞生是基于这么一个概念改变真实的DOM状态远比改变一个JavaScript对象的花销要大得多。
Virtual DOM是一个映射真实DOM的JavaScript对象如果需要改变任何元素的状态那么是先在Virtual DOM上进行改变而不是直接改变真实的DOM。当有变化产生时一个新的Virtual DOM对象会被创建并计算新旧Virtual DOM之间的差别。之后这些差别会应用在真实的DOM上。·
例子如下我们可以看看下面这个列表在HTML中的代码是如何写的
ul classlistliitem 1/liliitem 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$emiteventbusvuex$parent / $children / ref
参考Vue学习看这篇就够 、Vue 2.0渐进式前端解决方案-尤雨溪、 Vue2.0 中“渐进式框架”和“自底向上增量开发的设计”这两个概念是什么-徐飞、一句话理解Vue核心内容、Vue与React两个框架的区别和优势对比、Vue2.0 v-for 中 :key 到底有什么用 为什么要是用sass这些css预处理器
SassSyntactically Awesome Style Sheets是一个相对新的编程语言Sass为web前端开发而生可以用它来定义一套新的语法规则和函数以加强和提升CSS。通过这种新的编程语言你可以使用最高效的方式以少量的代码创建复杂的设计。它改进并增强了CSS的能力增加了变量局部和函数这些特性。
优势
易维护更方便的定制 对于一个大型或者稍微有规模的UI来说如果需要替换下整体风格或者是某个字体的像素值比如我们经常会遇到panelwindow以及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 lt; 7。
问题是在提交阶段我们并不确定内容要输出到哪里。
这里的“并不确定内容要输出到哪里”有两层含义
用户的输入内容可能同时提供给前端和客户端而一旦经过了 escapeHTML()客户端显示的内容就变成了乱码( 5 lt; 7 )。 在前端中不同的位置所需的编码也不同。 div titlecomment5 lt; 7/div当 5 lt; 7 作为 HTML 拼接页面时可以正常显示当 5 lt; 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。避免内联事件 尽量不要使用 onLoadonload({{data}})、onClickgo({{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 srcXXXlink hrefXXXscript srcXXX
当协议、子域名、主域名、端口号中任意一个不相同时都算作不同域。 跨域解决方案
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,还要在这个地址中向服务器传递该函数名可以通过问号传参:?callbackshow。服务器接收到请求后需要进行特殊的处理把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如传递进去的函数名是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 } // wdbcallbackshowlet 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?wdIloveyoucallbackshow这个地址请求数据然后后台返回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) 请求方法是以下三种方法之一 HEADGETPOST2HTTP的头信息不超出以下几种字段 AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-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; charsetutf-8上面的头信息之中有三个与CORS请求相关的字段都以Access-Control-开头。
1Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值要么是一个*表示接受任意域名的请求。
2Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值表示是否允许发送Cookie。默认情况下Cookie不包括在CORS请求之中。设为true即表示服务器明确许可Cookie可以包含在请求中一起发给服务器。这个值也只能设为true如果服务器不要浏览器发送Cookie删除该字段即可。
3Access-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;需要注意的是如果要发送CookieAccess-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字段预检请求的头信息包括两个特殊字段。
1Access-Control-Request-Method
该字段是必须的用来列出浏览器的CORS请求会用到哪些HTTP方法上例是PUT。
2Access-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; charsetutf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout2, max100
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 1Access-Control-Allow-Methods
该字段必需它的值是逗号分隔的一个字符串表明服务器支持的所有跨域请求的方法。注意返回的是所有支持的方法而不单是浏览器请求的那个方法。这是为了避免多次预检请求。
2Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers字段则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串表明服务器支持的所有头信息字段不限于浏览器在预检中请求的字段。
3Access-Control-Allow-Credentials
该字段与简单请求时的含义相同。
4Access-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; charsetutf-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.htmliframe srchttp://localhost:4000/b.html frameborder0 idframe onloadload()/iframe //等它加载完触发一个事件//内嵌在http://localhost:3000/a.htmlscriptfunction 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
scriptlet 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 srchttps://cdn.bootcss.com/jquery/3.3.1/jquery.min.js/scriptscript$.ajax({url: http://localhost:3000,type: post,data: { name: xiamen, password: 123456 },contentType: application/json;charsetutf-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 srchttp://localhost:4000/c.html frameborder0 onloadload() idiframe/iframescriptlet 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)scriptwindow.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.htmliframe srchttp://localhost:4000/c.html#iloveyou/iframescriptwindow.onhashchange function () { //检测hash的变化console.log(location.hash);}/script
复制代码 // b.htmlscriptwindow.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
bodyhelloaiframe srchttp://b.zf1.cn:3000/b.html frameborder0 onloadload() idframe/iframescriptdocument.domain zf1.cnfunction load() {console.log(frame.contentWindow.a);}/script
/body
复制代码
// b.html
bodyhellobscriptdocument.domain zf1.cnvar a 100;/script
/body
复制代码
三、总结
CORS支持所有类型的HTTP请求是跨域HTTP请求的根本解决方案JSONP只支持GET请求JSONP的优势在于支持老式浏览器以及可以向不支持CORS的网站请求数据。不管是Node中间件代理还是nginx反向代理主要是通过同源策略对服务器不加限制。日常工作中用得比较多的跨域方案是cors和nginx反向代理
参考九种跨域方式实现原理完整版 web语义化
分为html语义化、css语义化和url语义化
1HTML语义化——使用恰当的标签结构来规范内容使有利于开发人员以及屏幕阅读器访客有视障以及SEO读懂和识别
htmlbodyarticleheaderh1h1 - WEB 语义化/h1/headernavullinav1 - HTML语义化/lilinav2 - CSS语义化/li/ul/navsectionsection1 - HTML语义化/sectionsectionsection2 - CSS语义化/sectiontime datetime2018-03-23 pubdatetime - 2018年03月23日/timefooter 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标记可以包含figcaptionfigcaption表示图像对应的描述文字与图片产生对应关系。figureimg src/figure.jpg width304 height228 altPicturefigcaptionCaption for the figure/figcaption
/figure一些常用的媒体元素包含audio/video/source/embedaudio idaudioPlaysource src../h5/music/act/iphonewx/shakeshake.mp3 typeaudio/mpeg您的浏览器不支持 audio 标签。
/audio总之HTML语义化是反对大篇幅使用无语义化的divspanclass而鼓励使用HTML定义好的语义化标签。
当然如果需要兼容低版本的IE浏览器比如说IE8以及以下那就需要考虑一些HTML5标签兼容性解决方案了。
2)css语义化——就是class和id命名的规范如果说HTML语义化标签是给机器看的那么CSS命名的语义化就是给人看的。良好的CSS命名方式减少沟通调试成本易于理解。 看到这里问题来了。既然CSS class和ID命名的语义化可以便于阅读理解和减少沟通调试成本那么我们是不是可以用div 结合class和ID语义化命名的方式来代替html的语义化从代码的层面上来看使用CSS class语义化的命名也是能够便于阅读和维护的但是这样子并不利于SEO和屏幕阅读器识别3URL语义化——可以使得搜索引擎或者爬虫更好的理解·当前URL所在目录所要表达的意思而对于用户来说通过url也可以判断上一级目录或者下一级目录想要表示的内容可以提高用户体验。
例如我司的搜索品类的url
url语义化可以从以下标准来衡量 url简化规范化url里的名词如果包含两个单词那么就用下划线_ 连接。结构化语义化此处的品类搜索我们用语义化单词category表示采用技术无关的url第一个链接中的index.php这种就不应该出现在用户侧的url里。参考如何理解Web语义化、HTML 5的革新之一语义化标签一节元素标签HTML 5的革新——语义化标签(二)
nodeJS
什么是nodeJS
三个特性
服务器端JavaScript处理server-side JavaScript execution非阻断/异步I/Onon-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 LibraryNode.js 标准库对外提供的 JavaScript 接口例如模块 http、buffer、fs、stream 等
Node bindings这里就是 JavaScript 与 C 连接的桥梁对下层模块进行封装向上层提供基础的 API 接口。
V8Google 开源的高性能 JavaScript 引擎使用 C 开发并且应用于谷歌浏览器。如果您感兴趣想学习更多的 V8 引擎知识请访问 What is V8?
Libuv是一个跨平台的支持事件驱动的 I/O 库。它是使用 C 和 C 语言为 Node.js 所开发的同时也是 I/O 操作的核心部分例如读取文件和 OS 交互。来自一份 Libuv 的中文教程
C-aresC-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.jsnode_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 的区别