JS
关于promise的使用:


加入状态后:

实现promise链式调用:
function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];
this.then = function (onFulfilled) {
return new Promise(function (resolve) {
handle({
onFulfilled: onFulfilled || null,
resolve: resolve
});
});
};
function handle(callback) {
if (state === 'pending') {
callbacks.push(callback);
return;
}
//如果then中没有传递任何东西
if(!callback.onResolved) {
callback.resolve(value);
return;
}
var ret = callback.onFulfilled(value);
callback.resolve(ret);
}
function resolve(newValue) {
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if (typeof then === 'function') {
then.call(newValue, resolve);
return;
}
}
state = 'fulfilled';
value = newValue;
setTimeout(function () {
callbacks.forEach(function (callback) {
handle(callback);
});
}, 0);
}
fn(resolve);
}promise关于面试题:

promise关于错误处理:
大多数时候我们希望发生错误的时候,promise处理当前的异常并中断后续的then操作。但是当使用reject处理异常的时候,会出现在下一个then里面的第二个函数里面传递下去,并不能中断后续then操作:
var promiseStart = new Promise(function(resolve, reject){
reject('promise is rejected');
});
promiseStart
.then(function(response) {
console.log('resolved');
return new Promise(function(resolve, reject){
resolve('promise is resolved');
});
},function (error){
console.log('rejected:', error);
// 如果这里不抛出error,这个error将被吞掉,catch无法捕获异常
// 但是如果抛出error,这个error会被下一个then的reject回调处理,这不是我们想要的
throw(error);
})
.then(function (response){
console.log('resolved:', response);
},function (error){
console.log('rejected:', error);
throw(error);
})
.catch(function(error) {
console.log('catched:', error);
})
/*
输出:
rejected: promise is rejected
rejected: promise is rejected
catched: promise is rejected
*/当用catch处理异常的时候,可以直接在catch中捕获:
var promiseStart = new Promise(function(resolve, reject){
reject('promise is rejected');
});
promiseStart
.then(function(response) {
console.log('resolved');
return new Promise(function(resolve, reject){
resolve('promise is resolved');
});
})
.then(function (response){
console.log('resolved:', response);
})
.catch(function(error) {
console.log('catched:', error);
})
/*
输出:
catched: promise is rejected
*/这样发生了错误后后续的then不会被调用,错误也只被catch处理一次。
事件循环(Event Loop)-js执行机制:
- 事件循环:
事件循环是js实现异步的一种方法,也是js的执行机制。
既然js是单线程,那就像只有一个窗口的银行,客户需要排队一个一个办理业务,同理js任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此聪明的程序员将任务分为两类:
- 同步任务
- 异步任务


- 关于setTimeout和setInterval:


上面说完了setTimeout,当然不能错过它的孪生兄弟setInterval。他俩差不多,只不过setInterval是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。这句话请读者仔细品味。
- 关于Promise与process.nextTick(callback):


- 复杂例子分析:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})最终结果: 完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。


- 额外的问题:
setTimeout(() => {
console.log(2)
}, 2)
setTimeout(() => {
console.log(1)
}, 1)
setTimeout(() => {
console.log(0)
}, 0)在Chrome和node中,这里打印顺序都是1, 0, 2。
说明0ms和1ms的延时效果是一致的。通过源码看出传入0和传入1结果都是oneMillisecond,即1ms。
变量和函数的提升:
js是自上而下逐行解释执行的,但是引擎会在解释JavaScript代码之前首先对齐进行编译,简单说就是在js代码执行前引擎会先进行预编译,预编译期间会将变量声明与函数声明提升至其对应作用域的最顶端。预编译指的是:在当前作用域中,JavaScript代码执行之前,浏览器首先会默认的把所有带var和function声明的变量进行提前的声明或者定义,和他前面有无return无关。函数声明的优先级高于变量声明
console.log(a);
var a = 3;
//预编译后的代码结构可以看做如下
var a; // 将变量a的声明提升至最顶端,赋值逻辑不提升。
console.log(a); // undefined
a = 3; // 代码执行到原位置即执行原赋值逻辑- 变量提升:
即全局作用域中声明的变量会提升至全局最顶层,函数内声明的变量只会提升至该函数作用域最顶层。
console.log(a);
var a = "a";
var foo = () => {
console.log(a);
var a = "a1";
}
foo();经过预编译成:
var a;
console.log(a); // undefined
a = "a";
var foo = () => {
var a; // 全局变量会被局部作用域中的同名变量覆盖
console.log(a); // undefined
a = "a1";
}
foo();- 函数提升:
console.log(foo1); // [Function: foo1]
foo1(); // foo1
console.log(foo2); // undefined
foo2(); // TypeError: foo2 is not a function
function foo1 () {
console.log("foo1");
};
var foo2 = function () {
console.log("foo2");
};var a = 1;
function foo() {
a = 10;
console.log(a);
return;
function a() {};
}
foo();
console.log(a);结果打印:10 1。会被预编译成
var a = 1; // 定义一个全局变量 a
function foo() {
// 首先提升函数声明function a () {}到函数作用域顶端, 然后function a () {}等同于 var a = function() {};最终形式如下
var a = function () {}; // 定义局部变量 a 并赋值。
a = 10; // 修改局部变量 a 的值,并不会影响全局变量 a
console.log(a); // 打印局部变量 a 的值:10
return;
}
foo();
console.log(a); // 打印全局变量 a 的值:1分割线----------------------
function bar(){
return foo;
foo=10;
var foo='11';
}
console.log(bar());//undefined
typeof bar();//"undefined"已上代码会被编译为:
function bar(){
var foo;
return foo;
foo=10;
foo='11';
}
console.log(bar());//undefined
typeof bar();//"undefined"分割线------------------
变量声明提升与函数声明提升混合:
function bar(){
return foo;
foo=10;
function foo(){}
var foo='11';
}
typeof bar(); // "function"已上会被编译为:
下面打印出的是foo函数function。首先会有函数和变量var的提升,函数被提升过后foo被赋值成函数了,此时变量被提升是不会被赋值的。第二次foo没有被赋值成功,所以foo他的值还是函数。
function bar(){
var foo = function(){};
var foo;
console.log(foo)
return foo;
foo=10;
foo='11';
}this用法:
先说总结:
this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象(这句话有些问题,后面会解释为什么会有问题,虽然网上大部分的文章都是这样说的,虽然在很多情况下那样去理解不会出什么问题,但是实际上那样理解是不准确的,所以在你理解this的时候会有种琢磨不透的感觉)
- 如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window,这里需要说明的是在js的严格版中this指向的不是window而是undefined。
- 如果一个函数中有this,这个函数被上一级的对象所调用,那么this指向的就是上一级的对象。
- 如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象。
- this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的
探索如下:
按照我们上面说的this最终指向的是调用它的对象,这里的函数a实际是被Window对象所点出来的:
function a(){
var user = "追梦子";
console.log(this.user); //undefined
console.log(this); //Window
}
a();等价于下面:
function a(){
var user = "追梦子";
console.log(this.user); //undefined
console.log(this); //Window
}
window.a();这里的this指向的是对象o,因为你调用这个fn是通过o.fn()执行的,那自然指向就是对象o,这里再次强调一点,this的指向在函数创建的时候是决定不了的,在调用的时候才能决定,谁调用的就指向谁 :
var o = {
user:"追梦子",
fn:function(){
console.log(this.user); //追梦子
}
}
o.fn();下面这样证明如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象(即this指向的是离他最近的上一级的那个对象)
var o = {
a:10,
b:{
a:12,
fn:function(){
console.log(this.a); //12
}
}
}
o.b.fn();尽管对象b中没有属性a,这个this指向的也是对象b,因为this只会指向它的上一级对象,不管这个对象中有没有this要的东西:
var o = {
a:10,
b:{
// a:12,
fn:function(){
console.log(this.a); //undefined
}
}
}
o.b.fn();这是一个比较特殊的情况:
this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的,下面虽然函数fn是被对象b所引用,但是在将fn赋值给变量j的时候并没有执行,所以最终指向的是window
var o = {
a:10,
b:{
a:12,
fn:function(){
console.log(this.a); //undefined
console.log(this); //window
}
}
}
var j = o.b.fn;
j();构造函数版本的this:
下面之所以对象a可以点出函数Fn里面的user是因为new关键字可以改变this的指向,将这个this指向对象a,为什么我说a是对象,因为用了new关键字就是创建一个对象实例,理解这句话可以看下面第二段代码,我们这里用变量a创建了一个Fn的实例(相当于复制了一份Fn到对象a里面),此时仅仅只是创建,并没有执行,而调用这个函数Fn的是对象a,那么this指向的自然是对象a,那么为什么对象a中会有user,因为你已经复制了一份Fn函数到对象a中,用了new关键字就等同于复制了一份。
function Fn(){
this.user = "追梦子";
}
var a = new Fn();
console.log(a.user); //追梦子var o = {
user:"追梦子",
fn:function(){
console.log(this.user); //追梦子
}
}
window.o.fn();当在构造函数中,this碰到return时:
如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。如下:
function fn()
{
this.user = '追梦子';
return function(){};
}
var a = new fn;
console.log(a.user); //undefinedfunction fn()
{
this.user = '追梦子';
return 1;
}
var a = new fn;
console.log(a.user); //追梦子call,apply,bind总结:
比如,b.call(a),就是把b放到a的环境中执行,b里面的this,就是a的this
call、 bind、 apply 这三个函数的第一个参数都是this的指向对象,第二个参数差别就来了:
- call 的参数是直接放进去的,第二第三第n个参数全都用逗号分隔,直接放到后面
- apply的所有参数都必须放在一个数组里面传进去。
- bind除了返回函数以外,它的参数和call 一样


undefined 、null、 undeclared 的区别:



作用域和作用域链:
- 作用域:
作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了‘块级作用域’,可通过新增命令 let 和 const 来体现。块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。如:
if (true) {
// 'if' 条件语句块不会创建一个新的作用域
var name = 'Hammad'; // name 依然在全局作用域中
}
console.log(name); // logs 'Hammad'块级作用域特点:
- let/const 声明并不会被提升到当前代码块的顶部,因此你需要手动将 let/const 声明放置到顶部,以便让变量在整个代码块内部可用:
function getValue(condition) {
if (condition) {
let value = "blue";
return value;
} else {
// value 在此处不可用
return null;
}
// value 在此处不可用
}- 禁止重复声明:
如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 let 声明就会导致抛出错误:
var count = 30;
let count = 40; // Uncaught SyntaxError: Identifier 'count' has already been declared count 变量被声明了两次:一次使用 var ,另一次使用 let 。因为 let 不能在同一作用域内重复声明一个已有标识符,此处的 let 声明就会抛出错误。但如果在嵌套的作用域内使用 let 声明一个同名的新变量,则不会抛出错误。
var count = 30;
// 不会抛出错误
if (condition) {
let count = 40;
// 其他代码
}- 循环中的绑定块作用域的妙用:
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = function () {
console.log('第' + (i + 1) + '个')
}
}- 作用域链:

关于自由变量的取值问题:要到创建这个函数的那个域”。作用域中取值,这里强调的是“创建”,而不是“调用”

fn()返回的是 bar 函数,赋值给 x。执行 x(),即执行 bar 函数代码。取 b 的值时,直接在 fn 作用域取出。取 a 的值时,试图在 fn 作用域取,但是取不到,只能转向创建 fn 的那个作用域中去查找,结果找到了,所以最后的结果是 30。
作用域与执行上下文:
许多开发人员经常混淆作用域和执行上下文的概念,误认为它们是相同的概念,但事实并非如此。
JavaScript 属于解释型语言,JavaScript 的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样:
解释阶段:
- 词法分析
- 语法分析
- 作用域规则确定
执行阶段:
- 创建执行上下文
- 执行函数代码
- 垃圾回收
JavaScript 解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是 this 的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。
作用域和执行上下文之间最大的区别是:
执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。
垃圾回收机制:
- 标记清除:
js中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在一个函数中声明一个变量,就将这个变量标记为"进入环境",从逻辑上讲,永远不能释放进入环境变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为"离开环境"。
function test(){
var a = 10; //被标记"进入环境"
var b = "hello"; //被标记"进入环境"
}
test(); //执行完毕后之后,a和b又被标记"离开环境",被回收垃圾回收机制在运行的时候会给存储再内存中的所有变量都加上标记(可以是任何标记方式),然后,它会去掉处在环境中的变量及被环境中的变量引用的变量标记(闭包)。而在此之后剩下的带有标记的变量被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后垃圾回收机制到下一个周期运行时,将释放这些变量的内存,回收它们所占用的空间。
到目前为止,IE、Firefox、Opera、Chrome、Safari的js实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。
- 引用计数:
低版本的IE用的引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。

闭包:
定义:
闭包是指有权访问另外一个函数作用域中的变量的函数.可以理解为(能够读取其他函数内部变量的函数)
作用:
正常函数执行完毕后,里面声明的变量被垃圾回收处理掉,但是闭包可以让作用域里的变量,在函数执行完之后依旧保持没有被垃圾回收处理掉。当一个函数执行完毕后,里面的局部变量是会被JS自带的垃圾回收机制给销毁的,从而释放内存。但是如果返回一个函数,而且函数里面有用到父级数声明的变量,那么此时,变量不会被回收,因为还有可能被用到,并且外界可以通过函数访问这段作用域下的变量。
如果想获取某个函数里面的变量:
此时,a是不是就获得了呢?并没有,我们得到的只是a的副本,假设我以某种方法修改了a的值,b不会发生任何变化。所以,我们并不是使用的foo内部的a。
function foo(){
var a = "我是foo里面的a";
return a;//将变量a给return出来
}
let b = foo();//此时b就获得foo里面的a了。然而闭包可以实现外部访问函数里面的变量:
下面按道理每次都会返回6呀,怎么会每次叠加呢。其实不然,我们执行的是函数b(从foo返回出来的),并没有重新执行foo,所以也就不会每次给num重新赋值5。至于为什么会变成这种累加的情况呢,这是因为函数foo执行完后,其内部的的A函数里面对num有引用,所以foo的作用域以及变量a被保留在了函数A中,返回给了b。
现在,我们就能在外界通过函数b来访问foo内部的变量num了。这就是闭包,我们通过返回一个函数打通了函数内部与外界的桥梁。
function foo(c){
var num = c;
return function A(){
num++;
return num;
}
}
var b = foo(5);//b = A
b();//6
b();//7典型问题:
循环绑定的问题,由于var声明的变量可以穿透作用域。
当你点击任意li时,都会打印出5。这是因为,我们绑定了五次onclik事件,无论你有没有触发后面的的函数,for循环都会执行,当你点击的时候,for循环已经执行完了,i 的值早已经变成了5
<ul class="list">
<li class="bloc">1</li>
<li class="bloc">2</li>
<li class="bloc">3</li>
<li class="bloc">4</li>
<li class="bloc">5</li>
</ul>
var ali = document.querySelectorAll('.wrap ul li')
for(var i = 0,l = ali.length;i < l;i++){
ali[i].onclick = function(){
console.log(i) //5 5 5 5 5
}
}闭包解决此问题:
我们是不是应该想办法把每次循环时的 i 值保存下来呢,就好像每个点击事件都重新给一个新的 i 值。于是,通过闭包可以保存每次循环的 i 值:
for(var i = 0,l = ali.length;i < l;i++){
ali[i].onclick = (function(j){
return function(){
console.log(j)
}
})(i)
}我们通过立即执行函数后返回了一个函数的形式,把每次循环的 i 值通过传参放到了不同的函数中,每个函数都是一个独立的作用域,每个函数都有了各自的i值,互相不影响。
ajax:
//创建 XMLHttpRequest 对象
var ajax = new XMLHttpRequest();
//规定请求的类型、URL 以及是否异步处理请求。
ajax.open('GET',url,true);
//发送信息至服务器时内容编码类型
ajax.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
//发送请求
ajax.send(null);
//接受服务器响应数据
ajax.onreadystatechange = function () {
if (obj.readyState == 4 && (obj.status == 200 || obj.status == 304)) {
}
};post和get请求区别:
###代码上的区别
1:get通过url传递参数,传参:open('GET', 'url?name=jack&age=22', true);
2:post设置请求头 规定请求数据类型,传参:xhr.send(JSON.stringify({name: 'jack', age: 22}));
###使用上的区别
1:post比get安全
(因为post参数在请求体中。get参数在url上面)
2:get传输速度比post快 根据传参决定的。
(post通过请求体传参,后台通过数据流接收。速度稍微慢一些。而get通过url传参可以直接获取)
3:post传输文件大理论没有限制 get传输文件小大概7-8k ie4k左右
4:get获取数据 post上传数据
(上传的数据比较多 而且上传数据都是重要数据。所以不论在安全性还是数据量级 post是最好的选择)跨域:
- jsonp 只能解决get跨域(问的最多)
动态创建一个script标签。利用script标签的src属性不受同源策略限制。因为所有的src属性和href属性都不受同源策略限制。可以请求第三方服务器数据内容。
步骤:- 去创建一个script标签
- script的src属性设置接口地址
- 接口参数,必须要带一个自定义函数名 要不然后台无法返回数据。
- 通过定义函数名去接收后台返回数据
//去创建一个script标签
var script = document.createElement("script");
//script的src属性设置接口地址 并带一个callback回调函数名称
script.src = "http://127.0.0.1:8888/index.php?callback=jsonpCallback";
//插入到页面
document.head.appendChild(script);
//通过定义函数名去接收后台返回数据
function jsonpCallback(data){
//注意 jsonp返回的数据是json对象可以直接使用
//ajax 取得数据是json字符串需要转换成json对象才可以使用。
}- CORS:跨域资源共享
限制:浏览器需要支持HTML5,可以支持POST,PUT等方法兼容ie9以上
需要后台设置
Access-Control-Allow-Origin: * //允许所有域名访问,或者
Access-Control-Allow-Origin: http://a.com //只允许所有域名访问- 用Apache做转发(逆向代理),让跨域变成同域
将 www.123.com 映射到 127.0.0.1:即将请求转发
server {
listen 80;
server_name www.123.com;
location / {
proxy_pass http://127.0.0.1:8080;
index index.html index.htm index.jsp;
}
}事件流(冒泡和捕获):
描述的是页面中接受事件的顺序,即冒泡和捕获。
事件处理分为dom0级和dom2级,dom0是以on开头,比如div.onclick=function(event){}。
dom2级不以on开头,而是addEventListener,true表示捕获,false冒泡,但是IE8和以下不支持addEventListener,只支持attachEvent, dom2级事件流包括:首先发生的是事件捕获,然后是实际的目标接收到事件,最后阶段是冒泡阶段。
深浅拷贝:
深复制和浅复制最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用
1)深复制在计算机中开辟了一块内存地址用于存放复制的对象
2)而浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。
对数组复制可以用 ... 或者from方法。
方法1:
下列代码在浏览器控制台执行:
1 2 3 4 5 | var arr = [1,2,3];var arr1 = arr;arr1.push(4);console.log(arr); //[1,2,3,4]console.log(arr1);//[1,2,3,4] |
通过JSON.stringfy()和JSON.parse()转换
1 2 3 4 5 6 7 | var arr = [1,2,3];var arr1 = JSON.stringify(arr);var arr2 = JSON.parse(arr1);arr2.push(4);console.log(arr); //[1, 2, 3]console.log(arr1);//字符串[1,2,3]console.log(arr2);//[1, 2, 3, 4] |
方法2:递归的方式
var json1={"name":"鹏哥","age":18,"arr1":[1,2,3,4,5],"string":'afasfsafa',"arr2":[1,2,3,4,5],"arr3":[{"name1":"李鹏"},{"job":"前端开发"}]};
var json2={};
function copy(obj1,obj2){
var obj2=obj2||{}; //最初的时候给它一个初始值=它自己或者是一个json
for(var name in obj1){
if(typeof obj1[name] === "object"){ //先判断一下obj[name]是不是一个对象
obj2[name]= (obj1[name].constructor===Array)?[]:{}; //我们让要复制的对象的name项=数组或者是json
copy(obj1[name],obj2[name]); //然后来无限调用函数自己 递归思想
}else{
obj2[name]=obj1[name]; //如果不是对象,直接等于即可,不会发生引用。
}
}
return obj2; //然后在把复制好的对象给return出去
}
json2=copy(json1,json2)
json1.arr1.push(6);
alert(json1.arr1); //123456
alert(json2.arr1); //12345几种for循环:
- for of:
只能循环数组
- for in:
可以循环对象,也可以是数组。不能保证for ... in将以任何特定的顺序返回索引
- forEach:
array.forEach(function(value,index,arr){
console.log(value)
})只能遍历数组。方法不能使用break,continue语句跳出循环,或者使用return从函数体返回,对于空数组不会执行回调函数.
观察模式和发布订阅模式:
观察者模式:
彩票中心里,管理员充当目标对象(被观察者),彩民充当观察者,当管理员说公布一等奖号码时,即给各个观察者发布了消息,然后彩民(观察者)就收到发布消息,进行自己的后续操作(兑奖)。
定义了对象间一种一对多的依赖关系,当目标对象 Subject 的状态发生改变时,所有依赖它的对象 Observer 都会得到通知。但却不能对事件通知进行细分管控,如 “筛选通知”,“指定主题事件通知” 。
如下:两个观察者接收目标者状态变更通知后,都执行了 update(),并无区分。
// 目标者类
class Subject {
constructor() {
this.observers = []; // 观察者列表
}
// 添加
add(observer) {
this.observers.push(observer);
}
// 删除
remove(observer) {
let idx = this.observers.findIndex(item => item === observer);
idx > -1 && this.observers.splice(idx, 1);
}
// 通知
notify() {
for (let observer of this.observers) {
observer.update();
}
}
}
// 观察者类
class Observer {
constructor(name) {
this.name = name;
}
// 目标对象更新时触发的回调
update() {
console.log(`目标者通知我更新了,我是:${this.name}`);
}
}
// 实例化目标者
let subject = new Subject();
// 实例化两个观察者
let obs1 = new Observer('前端开发者');
let obs2 = new Observer('后端开发者');
// 向目标者添加观察者
subject.add(obs1);
subject.add(obs2);
// 目标者通知更新
subject.notify();
// 输出:
// 目标者通知我更新了,我是前端开发者
// 目标者通知我更新了,我是后端开发者发布订阅模式:
每家每户向牛奶订购中心订购了牛奶,但是各家的牛奶品牌不一样,有燕塘、蒙牛等等。当燕塘牛奶来货了,订阅中心就给订购燕塘的各家各户派发燕塘牛奶。同理,当蒙牛到货时,订阅中心发布蒙牛的牛奶。 优点就是一方面实现了发布者与订阅者之间的解耦,中间者可在两者操作之间进行更细粒度的控制 。如下:各自订阅各自的,到时候发布的时候各自接受各自的
// 事件中心
let pubSub = {
list: {},
subscribe: function (key, fn) { // 订阅
if (!this.list[key]) {
this.list[key] = [];
}
this.list[key].push(fn);
},
publish: function(key, ...arg) { // 发布
for(let fn of this.list[key]) {
fn.call(this, ...arg);
}
},
unSubscribe: function (key, fn) { // 取消订阅
let fnList = this.list[key];
if (!fnList) return false;
if (!fn) {
// 不传入指定取消的订阅方法,则清空所有key下的订阅
fnList && (fnList.length = 0);
} else {
fnList.forEach((item, index) => {
if (item === fn) {
fnList.splice(index, 1);
}
})
}
}
}
// 订阅
pubSub.subscribe('onwork', time => {
console.log(`上班了:${time}`);
})
pubSub.subscribe('offwork', time => {
console.log(`下班了:${time}`);
})
pubSub.subscribe('launch', time => {
console.log(`吃饭了:${time}`);
})
// 发布
pubSub.publish('offwork', '18:00:00');
pubSub.publish('launch', '12:00:00');
// 取消订阅
pubSub.unSubscribe('onwork');==和===:
==:
- 先检查两个操作数的数据类型是否相同
- 如果相同,则比较两个数是否相等
- 如果不同,则先将两个数转换为相同数据类型,再进行比较

===:
- 先检查两个操作数的数据类型是否相同
- 若不同,直接返回false
- 若相同,则比较二者是否相等
__proto__ 和prototype:
其他的参考有道云笔记上的:
1. __proto__是每个对象都有的一个属性,而prototype是函数才会有的属性。
2. __proto__指向的是当前对象的原型对象,而prototype指向的,是以当前函数作为构造函数构造出来的对象的原型对象。看起来有点绕,我 show you the code,下面我们用右手作为原型来给自己构造一个女朋友:
//在JavaScript的世界中,所有的函数都能作为构造函数,构造出一个对象
//下面我给自己构造一个女朋友
function GirlFriend () {
this.name = "Alice";
}
//现在我设置GirlFriend()这个函数的prototype属性
//一般来说直接用匿名的对象就行,我这里是为了方便理解,
//先定义一个hand对象再把hand赋值给GirlFriend()的prototype
var hand = {
whichOne: "right hand",
someFunction: function(){
console.log("not safe for work.");
}
};
GirlFriend.prototype = hand;
//这个时候,我们可以用GirlFriend()作为构造函数,构造出myObject对象
var myObject = new GirlFriend();
console.log(myObject.__proto__ === GirlFriend.prototype) //true好了,通过上面的代码,我们构建了一个女神对象myObject,而 myObject 的原型是 hand 对象,而刚好 myObject 的构造函数GirlFriend()的 prototype 属性也指向 hand 对象。现在我们知道,prototype 与__proto__ 的关系就是:你的__proto__来自你构造函数的prototype
还有,上面的例子中,myObject 是通过 new GirlFriend()创建的,而 hand 对象,则是赋值语句创建的,这有什么不同?
其实hand这种直接用赋值语句加花括号来创建的对象,叫做对象字面量,你可以想象JavaScript内置了一个叫Object()的构造函数,这个函数的prototype属性指向的是一个空对象:
console.log(Object.prototype) //输出{}
而所有对象字面量都是通过Object()构造出来的,换言之,对象字面量__proto__ 属性都指向Object.prototype, which is 一个空对象。
所以我们可以知道, hand.__proto__ 指向的是Object.prototype
再附送你一个fun fact:
Object.prototype这个对象,它的__proto__指向的是null,然后就没有然后了。
console.log(Object.prototype.__proto__);//输出nullCSS
盒模型:


CSS权值优先级:
!important > 行内样式>ID选择器 > 类选择器 > 标签 > 通配符 > 继承 > 浏览器默认属性
- 内联样式表的权值为 1000
- ID 选择器的权值为 100
- Class 类选择器的权值为 10
- HTML 标签选择器的权值为 1
相对单位:rem, em:
em相对于父元素,rem相对于根元素即html元素。
em:
- 子元素字体大小的em是相对于父元素字体大小
- 元素的width/height/padding/margin用em的话是相对于该元素的font-size
<div>
我是父元素div
<p>
我是子元素p
<span>我是孙元素span</span>
</p>
</div>div {
font-size: 40px;
width: 10em; /* 400px */
height: 10em;
border: solid 1px black;
}
p {
font-size: 0.5em; /* 20px */
width: 10em; /* 200px */
height: 10em;
border: solid 1px red;
}
span {
font-size: 0.5em;
width: 10em;
height: 10em;
border: solid 1px blue;
display: block;
}我猜你会说10px、100px,哈哈,其实逻辑上是正确的,但是如果你是chrome浏览器我不得不告诉你应该是12px、120px。因为chrome设置的最小我猜你会说10px、100px,哈哈,其实逻辑上是正确的,但是如果你是chrome浏览器我不得不告诉你应该是12px、120px。因为chrome设置的最小。
rem:
rem是全部的长度都相对于根元素,根元素是谁?<html>元素。通常做法是给html元素设置一个字体大小,然后其他元素的长度单位就为rem。
html {
font-size: 10px;
}
div {
font-size: 4rem; /* 40px */
width: 30rem; /* 300px */
height: 30rem;
border: solid 1px black;
}
p {
font-size: 2rem; /* 20px */
width: 15rem;
height: 15rem;
border: solid 1px red;
}
span {
font-size: 1.5rem; /* 15px */
width: 10rem;
height: 10rem;
border: solid 1px blue;
display: block;
} 当用rem做响应式时,直接在媒体中改变html的font-size那么用rem作为单位的元素的大小都会相应改变,很方便。
网页中我们很多时候都需要用到满屏,或者屏幕大小的一半等,尤其是移动端,屏幕大小各式各样,而这个时候我们现有的单位就显得有点捉襟见肘,于是就诞生了这四个单位。
vw:基于视窗的宽度计算,1vw 等于视窗宽度的百分之一
vh:基于视窗的高度计算,1vh 等于视窗高度的百分之一
vmin:基于vw和vh中的最小值来计算,1vmin 等于最小值的百分之一
vmax:基于vw和vh中的最大值来计算,1vmax 等于最大值的百分之一
HTML
react
redux小书:
huziketang.mangojuice.top/books/react…
虚拟dom和diff算法策略:
react 在内存中生成维护一个跟真实DOM一样的虚拟DOM 树,在改动完组件后,会再生成一个新得虚拟DOM,react 会把新虚拟DOM 跟原虚拟DOM 进行比对,找出两个DOM 不同的地方diff ,然后把diff放到队列里面,然后批量更新diff 到真实DOM 上。React会先将你的代码转换成一个 JavaScript对象,然后这个 JavaScript对象再转换成真实 DOM。这个 JavaScript对象就是所谓的虚拟 DOM。
优点:最终真实DOM 就只更新了diff 部分,提高了渲染速度
缺点:首次渲染DOM 时候由于多了一层虚拟DOM 计算,就比html 渲染慢
比如这样一段代码:
<div class="title">
<span>Hello ConardLi</span>
<ul>
<li>苹果</li>
<li>橘子</li>
</ul>
</div>会被React可能存储为这样的 JS代码:
const VitrualDom = {
type: 'div',
props: { class: 'title' },
children: [
{
type: 'span',
children: 'Hello ConardLi'
},
{
type: 'ul',
children: [
{ type: 'ul', children: '苹果' },
{ type: 'ul', children: '橘子' }
]
}
]
}当我们需要创建或更新元素时, React首先会让这个 VitrualDom对象进行创建和更改,然后再将 VitrualDom对象渲染成真实 DOM;
当我们需要对 DOM进行事件监听时,首先对 VitrualDom进行事件监听, VitrualDom会代理原生的 DOM事件从而做出响应。
用虚拟dom的目的:
- 提高开发效率:
- 提升性能:
直接操作 DOM是非常耗费性能的,这一点毋庸置疑。但是 React使用 VitrualDom也是无法避免操作 DOM的。
如果是首次渲染, VitrualDom不具有任何优势,甚至它要进行更多的计算,消耗更多的内存。
VitrualDom的优势在于 React的 Diff算法和批处理策略, React在页面更新之前,提前计算好了如何进行更新和渲染 DOM。实际上,这个计算过程我们在直接操作 DOM时,也是可以自己判断和实现的,但是一定会耗费非常多的精力和时间,而且往往我们自己做的是不如 React好的。所以,在这个过程中 React帮助我们"提升了性能"。
所以,我更倾向于这个说法: “VitrualDom帮助我们提高了开发效率,在重复渲染时它帮助我们计算如何更高效的更新,而不是它比 DOM操作更快。”
跨浏览器兼容:
React基于 VitrualDom自己实现了一套自己的事件机制,自己模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,抹平了各个浏览器的事件兼容性问题
跨平台兼容:
VitrualDom为 React带来了跨平台渲染的能力。以 ReactNative为例子。React根据 VitrualDom画出相应平台的 ui层,只不过不同平台画的姿势不同罢了。react Native是通过虚拟DOM实现跨平台,运行时将虚拟DOM转换为相应的web编码、android编号、ios编码进行运行的。
diff策略:将虚拟dom树转换成真实dom树的最少操作的过程 称为 调和 ,diff算法是调和的具体实现。针对不同情况,diff算法有三种优化策略.
- 树的优化策略(tree diff):


- 组件的优化策略(component diff):
如果是同一个类型的组件,则按照原策略进行Virtual DOM比较。即tree diff比较。

- 元素的优化策略(element diff):




建议:
箴言一:在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
箴言二:使用 shouldComponentUpdate()方法节省diff的开销
箴言三:在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
不建议用下标index作为key的原因:
class App extends Component{
constructor(props) {
super(props)
this.state = {
list: [{id: 1,val: 'A'}, {id: 2, val: 'B'}, {id: 3, val: 'C'}]
}
}
click() {
this.state.list.reverse()
this.setState({})
}
render() {
return (
<ul>
{
this.state.list.map((item, index) => {
return (
<Li key={index} val={item.val}></Li>
)
})
}
<button onClick={this.click.bind(this)}>Reverse</button>
</ul>
)
}
}
class Li extends Component{
constructor(props) {
super(props)
}
componentDidMount() {
console.log('===mount===')
}
componentWillUpdate() {
console.log('===update====')
}
render() {
return (
<li>
{this.props.val}
<input type="text"></input>
</li>
)
}
}

传统diff和react diff:
在传统的diff算法下,对比前后两个节点,如果发现节点改变了,会继续去比较节点的子节点,一层一层去对比。就这样循环递归去进行对比,复杂度就达到了o(n3),n是树的节点数,想象一下如果这棵树有1000个节点,我们得执行上十亿次比较,这种量级的对比次数,时间基本要用秒来做计数单位了。
两棵树只会对同一层次的节点进行比较,忽略DOM节点跨层级的移动操作。同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个DOM树的比较。由此一来,最直接的提升就是复杂度变为线型增长而不是原先的指数增长
react和react-dom区别:
react只包含了Web和Mobile通用的核心部分,比如:包保函了生成虚拟dom的函数react.createElement,以及Component这个类,负责Dom操作的分到react-dom中,负责Mobile的包含在react-native中。
ReactDom只做和浏览器或DOM相关的操作,核心功能就是把这些虚拟Dom渲染到文档中变成实际dom,例如ReactDOM.render()和ReactDOM.findDOMNode()。如果是服务器端渲染,可以ReactDOM.renderToString()。除这些以外的其他所有的事情都是react做的。
react路由:
路由官网:react-guide.github.io/react-route…
react-router和react-router-dom的区别:
写法1:
import {Swtich, Route, Router, HashHistory, Link} from 'react-router-dom';写法2:
import {Switch, Route, Router} from 'react-router';
import {HashHistory, Link} from 'react-router-dom';- react-router:提供了router的核心api。如Router、Route、Switch等,但没有提供有关dom操作进行路由跳转的api;
- react-router-dom:提供了BrowserRouter、HashRouter、Route、Link等api,可以通过dom操作触发事件控制路由。
react-router-dom包含了react-router的依赖,因此我们在安装的时候只需要安装后者即可。
browserHistory 和 hashHistory的区别:
React Router 是建立在 history 之上的。 一个 history 知道如何去监听浏览器地址栏的变化, 并解析这个 URL 转化为 location 对象, 然后 router 使用它匹配到路由,最后正确地渲染对应的组件。
1)多页面模式(MPA Multi-page Application):
多页面跳转需要刷新所有资源,每个公共资源(js、css等)需选择性重新加载
页面跳转:使用window.location.href = "./index.html"进行页面间的跳转;
数据传递:可以使用path?account="123"&password=""路径携带数据传递的方式,或者localstorage、cookie等存储方式
2)单页面模式(SPA Single-page Application):
只有一个Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次
页面跳转:使用js中的append/remove或者show/hide的方式来进行页面内容的更换;
数据传递:可通过全局变量或者参数传递,进行相关数据交互
多页面模式,就是多个HTML页面之间的操作,浏览器会通过自身的history处理好页面间的操作,单页面模式,对于浏览器来说只有一个HTML页面,任何操作都在同一个页面内,浏览器无法监控到页面跳转(实际只是内容改变,路径没变)。
- browserHistory:
browserHistory 是使用 React-Router 的应用推荐的 history方案。它使用浏览器中的 History API 用于处理 URL,创建一个像example.com/list/123这样真实的 URL 。
在单页面模式下使用browserHistory 的问题是:只有一个真实的html页面,是无法体现出html页面之间跳转的效果的, 这时就需要使用服务器配合,模拟出多个HTML页面,从而实现浏览器真实的页面跳转效果。

在webpack 的 本地服务器模式下开启historyApiFallback可以解决browserHistory 的问题:
// 本地服务器 webpack-dev-server插件,开发中server,便于开发,可以热加载
devServer: {
contentBase: './dist', //默认本地服务器所在的根目录
historyApiFallback: true, //开启
inline: true, //源文件改变时刷新页面
port: 8084 //端口号,默认8080
},那在非本地模式(打出来的包)上面怎么解决这个问题:
通过服务器(无论Nginx,还是Node都可以)在浏览器方法该目录下(比如dist文件夹)的文件时,都返回 index.html。
比如访问 example.com/list/123 路径,但是服务器下没有这个文件可供访问,所以就需要通过设置服务器,当浏览器访问不存在的页面时,都给索引到 index.html。
- hashHistory:
使用hashHistory,浏览器的url是这样的:/#/user/liuna?_k=adseis,因为有 # 的存在,浏览器不会发送request,react-router 自己根据 url 去 render 相应的模块
react路由传参的几种方式:

react的class中每个方法都要绑定this原因:
合成事件。他是基于Virtual DOM 所实现的一套事件系统。我们在React Element中所定义的事件,会作为合成事件来处理,其对应的事件处理函数,会接收到一个SyntheticEvent的实例。
合成事件主要有如下的一些优势:
- 抹平各浏览器之间的事件差异,不存在兼容性问题,对开发者极为友好 。
- 合成事件利用冒泡机制,在顶层document完成事件注册和分发,避免直接操作DOM事件,减少内存开销,简化事件处理和回收机制。
- 内部使用事件池的概念,管理合成事件的创建,回收及其复用,提升性能
dom树上绑定事件是耗性能的,一个页面,如果你在dom节点上绑了很多事件,但是用户就点了几个按钮,那其他事件绑上去岂不是浪费了?
你在render里绑定的事件并不是真的绑在真实dom上的(绑在虚拟dom上),而是在document这个根节点上绑了个SyntheticEvent,当用户点击的时候事件冒泡到document上,SyntheticEvent收到后再在真实dom上通过回调(回调是重点)执行你在render上绑定的事件。
然而在JSX中传递的事件不是一个字符串,而是一个函数(如:onClick={this.handleClick}),此时onClick即是中间变量(参考js的this问题,不能有中间变量),所以处理函数中的this指向会丢失。解决这个问题就是给调用函数时bind(this),从而使得无论事件处理函数如何传递,this指向都是当前实例化对象 。或者用箭头函数,箭头函数体内的this对象是固定的,就是定义时所在的对象,而不是使用时所在的对象。
react组件class两种方法定义区别:
class Hello {
greet(){ },// 普通方法
say = () => { },// 箭头函数
}babel转换后,大致如下:
function Hello(){
this.say = () => { }
}
Hello.prototype.greet = function() { }通过转义后的代码,可看到区别:
普通函数被定义为类的原型方法
通过function创建的原型方法。
箭头函数被定义为类的实例方法, 即绑定在对象上
通过箭头方法创建的实例方法。
箭头函数定义的方法:
箭头函数定义方法时,this指向的是函数定义时的作用域。看例子:
function A(){
this.num = 2;
this.handleClick = () => { console.log(this.num) };
}
const B = new A();
// 第一种情况
B.handleClick(); //2
// 第二种情况
const fn = B.handleClick;
fn(); // 2普通函数定义的方法:
而普通函数的this,却是根据调用它时的对象有所关系。
function A(){
this.num = 2
this.handleClick = function(){console.log(this.num) }
}
const B = new A();
//以下都为严格模式下
B.handleClick(); // 2
const fn = B.handleClick;
fn(); // 报错,因为在严格模式下this为undefined无论是原型方法或者是实例方法,导致在调用时,this不同,原因在于他们的创建方式,当使用箭头函数创建,那么this将指向函数定义时的作用域;而当使用普通的function创建时,this的指向和函数的调用环境有关,(关于普通函数this的指向有所不了解,可通过《你不知道的javascript》熟悉)
所以,无论如何,箭头函数的this都不会发生改变,this指向的是组件;而普通函数的this随着调用场景的变化有所变化。
方法使用:
如果我们在需要将组件的一个方法绑定给一个子元素,为了保证函数中的this指向的是这个React组件(当函数需要借助组件的一些状态和属性时)。
class Hello extends React.PureComponent {
greet() {console.log(this)}
say = () => { console.log(this)}
render() {
return (
<div>
// 普通函数
<div onClIck={this.greet.bind(this)}>普通函数</div>
<div onClick={(e) => this.greet(e)}></div>
// 箭头函数
<div onClick={this.say}></div>
</div>
)
}
}普通函数的传递方法视觉上就觉得有点繁琐,除此之外,更重要的是性能方面。
React的生命周期中,shouldUpdateComponent,当shouldUpdateComponent返回false时,组件将不会重新render,子组件也可避免重新render。在 React 15.3.0 ,Reac可通过React.Purecomponent定义组件,当shouldUpdateComponent进行shallow compare时,避免一些不必要的render。而通过普通函数定义的方法,通过bind绑定后,每次父组件发生render时,传给子组件的 props.onClick 都会变, 方法就得重新bind(this),对于子组件而言,它的props、state发生了改变,则必须发生重新渲染。
关于setState:
1、setState 只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 中都是同步的。
2、setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
3、setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
关于ref:


不管ref设置值是回调函数还是字符串,都可以通过ReactDOM.findDOMNode(ref)来获取组件挂载后真正的dom节点。
但是对于html元素使用ref的情况,ref本身引用的就是该元素的实际dom节点,无需使用ReactDOM.findDOMNode(ref)来获取,该方法常用于React组件上的ref。


GIT
merge和rebase:
相同点:
git merge b # 将b分支合并到当前分支
同样 git rebase b,也是把 b分支合并到当前分支
区别:
两个使用场景是不一样的,merge只是合并另外一个分支的内容,rebase也合并另外一个分支的内容,但是会把本分支的commits顶到最顶端
假设我们现在有3个分支
- master分支:线上环境使用的分支
- testing分支:测试环境使用的分支
- my_feature分支:开发新功能的分支,也就是当前分支
A. 假设我在my_feature上开发了一段时间,之后另外的同事开发的功能正式上线到master分支了,那么我可以在当前的分支下rebase一下master分支,这样我这个分支的几个commits相对于master还是处于最顶端的,也就是说rebase主要用来跟上游同步,同时把自己的修改顶到最上面
B. 我在my_feature上开发了一段时间了,想要放到testing分支上,那就切到testing,然后merge my_feature进来,因为是个测试分支,commits的顺序无所谓,也就没必要用rebase (当然你也可以用rebase)。另外,单独使用rebase,还有调整当前分支上commits的功能(合并,丢弃,修改commites msg)
git pull和fetch区别:

git和SVN:

- SVN:
集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了,再放回图书馆。最大的毛病就是必须联网才能工作
- GIT:
分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上。分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个人的电脑坏掉了不要紧,随便从其他人那里复制一个就可以了。而集中式版本控制系统的中央服务器要是出了问题,所有人都没法干活了。分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。
Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。
文件往Git版本库里添加的时候,是分两步执行的:
第一步是用git add把文件添加进去,实际上就是把文件修改添加到暂存区;
第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支。
因为我们创建Git版本库时,Git自动为我们创建了唯一一个master分支,所以,现在,git commit就是往master分支上提交更改。
你可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改。

git fork和clone:
fork:
进入别人的项目右上角都有一个fork,相当于是把别人的项目复制到云端,注意是云端,本地电脑并没有下载,你可以在你GitHub主页的Repositories里面找到.
既然可以Download别人的源码为啥还要fork呢?
首先,我们有百度云也不一定会把百度云上的东西全都下载到本地硬盘对吧?有些东西我们需要存到云端,方便随时查看。
第二,Git可以多人协作完成项目,或者我写完一个项目可以开源到GitHub上,看到的小伙伴fork我的代码之后发现有BUG或者有一个地方有更好的算法可以解决,他可以在他自己的仓库里面修改源码,修改好之后他可以pull request,这样我就可以看到什么地方修改了,如果我觉得他的算法可行就可以把他的代码Merge到我的项目里面,简单说就帮我修复bug了,不用我自己动手。
clone:
直接down下来项目,属于自己的本地项目
HTTP(端口80)
特点:
- 简单:每个资源(比如图片、页面)都通过 url 来定位。这都是固定的,在http协议中,处理起来也比较简单,想访问什么资源,直接输入url即可。
- 灵活:http协议的头部有一个数据类型---通过http协议,就可以完成不同数据类型的传输。
- 无连接(重)--- http协议连接一次之后就会断开,不会保持连接
- 无状态(重)--- 客户端和服务器端是两种身份。第一次请求结束后,就断开了,第二次请求时,服务器端并没有记住之前的状态,也就是说,服务器端无法区分客户端是否为同一个人、同一个身份。有的时候,我们访问网站时,网站能记住我们的账号,这个是通过其他的手段(比如 session)做到的,并不是http协议能做到的。
HTTP报文组成:




HTTP get和post表面区别:

get是通过URL提交数据,因此get可提交的数据量就跟URL所能达到的最大长度有直接关系。URL不存在参数上限的问题,HTTP协议规范也没有对URL长度进行限制。这个限制是特定的浏览器和服务器对它的限制。即如果url太长,服务器可能会因为安全方面的设置从而拒绝请求或者发生不完整的数据请求。
post理论上来讲是没有大小限制的,HTTP协议规范也没有进行大小限制,但实际上post所能传递的数据量大小取决于服务器的设置和内存大小。实际中如果你上传文件的过程中可能会发现这样一个问题,即上传个头比较大的文件到服务器时候,可能上传不上去。
HTTP get和post本质没区别:
HTTP是什么?HTTP是基于TCP/IP的关于数据如何在万维网中如何通信的协议。HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的
但是有一个重大的区别:
GET产生一个TCP数据包;POST产生两个TCP数据包。
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
也就是说,GET只需要汽车跑一趟就把货送到了,而POST得跑两趟,第一趟,先去和服务器打个招呼“嗨,我等下要送一批货来,你们打开门迎接我”,然后再回头把货送过去。
因为POST需要两步,时间上消耗的要多一点,看起来GET比POST更有效。因此Yahoo团队有推荐用GET替换POST来优化网站性能。但这是一个坑!跳入需谨慎。为什么?
1. GET与POST都有自己的语义,不能随便混用。
2. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。
3. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。
网络基础(握手挥手):
- TCP/IP的分层管理
- TCP/IP最重要的一个特点就是分层管理,分别为:
- 应用层 --- 决定向用户提供应用服务时的通信活动,http、ftp、dns 都属于这一层
- 安全层(TSL/SSL) --- 如果是https的请求会存在这一层,http的请求则无此层,注意!!!!!!
- 传输层 --- 传输层对上层应用层提供处于网络连接中两台计算机之间的数据传输
- 网络层 --- 网络层用来处理网络上流动的数据包,数据包是网络传输的最小数据单位
- 链路层 --- 用来处理连接网络的硬件部分,包括控制操作系统、硬件的设备驱动等物理可见部分
- 一问:基于浏览器的连接处理(浏览器从输入url到页面渲染出来的过程)(-- HTTP权威指南)
- 浏览器从url中解析出服务器的主机名
- 浏览器将服务器的主机名转换成服务器的IP地址(DNS)
- 浏览器将端口号从url中解析出来
- 浏览器建立一条与web服务器的TCP连接
- 浏览器向服务器发送一条HTTP的请求报文
- 服务器向浏览器回送一条HTTP的响应报文
- 关闭连接,浏览器渲染
- 二问:浏览器建立一条与web服务器的TCP连接的时候多了哪些事(三次握手/四次挥手)?或者会追问浏览器的渲染过程是什么样?又或者是TCP的作用是啥
- 三次握手
- 客户端首先发送一个带有SYN标志的数据包给服务端
- 服务端接受SYN数据包之后,回传一个SYN/ACK标志的数据包以示传达确认连接信息
- 客户端收到SYN/ACK的确认数据包之后,再回传一个ACK标志的数据包给服务端,表示‘握手’结束
- 四次挥手
- 客户端向服务端先发送一个带有FIN标志的数据包给服务端
- 服务端接受FIN数据包之后,回传一个ACK的数据包给客户端以示传达确认关闭信息
- 服务端向客户端发送一个FIN标志的数据包,请求关闭连接
- 客户端收到FIN的数据包之后,回传一个ACK的数据包给服务端,以表示确认关闭
- TCP的作用是啥(个人觉得没几个变态公司会问,可以当作了解)
- 提供无差错的数据传输
- 按序传输(数据总是会按照发送顺序到达)
- 未分段的数据流(可以在任意时刻以任意尺寸将数据发送出去)
- 浏览器的渲染过程
- html元素转成DOM tree
- css 样式转成CSS tree
- DOM tree和CSS tree整合形成Render tree
- 通过layout计算DOM 元素要显示的位置和样式
- 浏览器进行绘制渲染
HTTP状态码:

持久链接/http长连接:

长连接中的管线化:

HTTP缓存:
强制缓存(不问服务器这个文件有没有更新,只要在缓存时间范围内,就使用缓存的文件。都是服务器端设置):- cache-control中的max-age保存一个相对时间。例如Cache-Control: max-age = 484200,表示浏览器收到文件后,缓存在484200s内均有效。 如果同时存在cache-control和Expires,浏览器总是优先使用cache-control。他有如下几个值可以设置:public、private、no-cache。
public:客户端和服务端都可以缓存;
private:只有客户端可以缓存;
no-cache:使用协商缓存;
- Expires是一个绝对时间,即服务器时间。浏览器检查当前时间,如果还没到失效时间就直接使用缓存文件。但是该方法存在一个问题:服务器时间与客户端时间可能不一致。因此该字段已经很少使用
- 第一种, 浏览器把缓存文件的最后修改时间通过 header ”If-Modified-Since“来告诉Web服务器。第一次请求资源的时候,服务端会在响应头里面设置该字段,表示该资源的最后修改时间,浏览器第二次请求该资源的时候,会在请求头里面加上一个字段If-Modified-Since,值为第一次请求的时候服务端返回的Last-Modified的值,服务端会判断资源当时的最后更改时间与请求头里面的If-Modified-Since字段是否相同,如果相同,则告诉客户端使用缓存,否则重新下载资源。这种方式有一个弊端,就是当服务器中的资源增加了一个字符,后来又把这个字符删掉,本身资源文件并没有发生变化,但修改时间发生了变化。当下次请求过来时,服务器也会把这个本来没有变化的资源重新返回给浏览器。
- 第二种, 浏览器把缓存文件的ETag, 通过header "If-None-Match", 来告诉Web服务器。
第一种:


第二种:



HTTP1.0和HTTP1.1区别:
1.长连接(持久连接):
HTTP流水线(即管道化):
是指,在一个TCP连接内,多个HTTP请求可以并行,客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能 够区分出每次请求的响应内容。在现实环境中,请求、响应链沿途不少的服务器和代理不支持流水线,会把后续的请求吃掉;此外,由于要保证响应返回的顺序,会造成队头阻塞 (Head-of-line blocking)的问题,即靠前的响应一旦阻塞,会耽误后续的响应发送。由于上述两个原因导致HTTP流水线技术对性能的提升并不明显(这个问题会在HTTP2.0中解决)。所以当代浏览器从未将流水线全面铺开,而是普遍把并发连接数从协 议规定的2提升到了6( HTTP1.1的RFC规定使用流水线技术一个用户最多两个连接 )。
2.HOST域:
HTTP1.1在Request消息头里头多了一个Host域,而且是必传的,HTTP1.0则没有这个域。
在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发 展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。
HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)
3.缓存:
HTTP1.1在1.0的基础上增加了新的特性:Cache-Control头域
4.宽带优化:
HTTP/1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了。又比如下载大文件时不支持断点续传功能,在发生断连后不得不重新下载完整的包。
HTTP/1.1中在请求消息中引入了range头域,它支持只请求资源的某个部分。在响应消息中Content-Range头域声明了返回的这部分对象的偏移值和长度。如果服务器相应地返回了对象所请求范围的内容,则响应码为206(Partial Content),它可以防止Cache将响应误以为是完整的一个对象。
另外一种浪费带宽的情况是请求消息中如果包含比较大的实体内容,但不确定服务器是否能够接收该请求(如是否有权限),此时若贸然发出带实体的请求,如果被拒绝也会浪费带宽。 HTTP/1.1加入了一个新的状态码100(Continue)。客户端事先发送一个只带头域的请求,如果服务器因为权限拒绝了请求,就回送响应码401(Unauthorized);如果服务器接收此请求就回送响应码100,客户端就可以继续发送带实体的完整请求了。具体用法为:客户端在Request头部中包含Expect: 100-continue Server看到之后呢如果回100 (Continue) 这个状态代码,客户端就继续发requestbody。(注意,HTTP/1.0的客户端不支持100响应码,这个是HTTP1.1才有的。)如果回401,客户端就知道是什么意思了。
HTTPS(端口443)
HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
我们都知道HTTPS能够加密信息,以免敏感信息被第三方获取,所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议。
1、客户端发起HTTPS请求
这个没什么好说的,就是用户在浏览器里输入一个https网址,然后连接到server的443端口。
2、服务端的配置
采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请,区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面(startssl就是个不错的选择,有1年的免费服务)。
这套证书其实就是一对公钥和私钥,如果对公钥和私钥不太理解,可以想象成一把钥匙和一个锁头,只是全世界只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。
3、传送证书
这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。
4、客户端解析证书
这部分工作是有客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。
如果证书没有问题,那么就生成一个随机值,然后用证书对该随机值进行加密,就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。
5、传送加密信息
这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。
6、服务段解密信息
服务端用私钥解密后,得到了客户端传过来的随机值(私钥),然后把内容通过该值进行对称加密,所谓对称加密就是,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。
7、传输加密后的信息
这部分信息是服务段用私钥加密后的信息,可以在客户端被还原。
8、客户端解密信息
客户端用之前生成的私钥解密服务段传过来的信息,于是获取了解密后的内容,整个过程第三方即使监听到了数据,也束手无策
webpack
热更新HMR:
- 第一步,当文件有变化,webpack 对文件系统进行 watch 打包到内存中:
webpack-dev-middleware 调用 webpack 的 api 对文件系统 watch,当文件发生改变后,webpack 重新对文件进行编译打包,然后保存到内存中。
webpack 将 bundle.js 文件打包到了内存中,不生成文件的原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销。
- 第二步,devServer 通知浏览器端文件发生改变:
在启动 devServer 的时候,sockjs 在服务端和浏览器端建立了一个 webSocket 长连接,以便将 webpack 编译和打包的各个阶段状态告知浏览器,最关键的步骤还是 webpack-dev-server 调用 webpack api 监听 compile的 done 事件,当compile 完成后,webpack-dev-server通过 _sendStatus 方法将编译打包后的新模块 hash 值发送到浏览器端。
- 第三步,webpack-dev-server/client 接收到服务端消息做出响应:
webpack-dev-server 修改了webpack 配置中的 entry 属性,在里面添加了 webpack-dev-client 的代码,这样在最后的 bundle.js 文件中就会接收 websocket 消息的代码了。
webpack-dev-server/client 当接收到 type 为 hash 消息后会将 hash 值暂存起来,当接收到 type 为 ok 的消息后对应用执行 reload 操作。
在 reload 操作中,webpack-dev-server/client 会根据 hot 配置决定是刷新浏览器还是对代码进行热更新(HMR)
- 第四步,webpack 接收到最新 hash 值验证并请求模块代码:
- 第五步,HotModuleReplacement.runtime 对模块进行热更新:
优化:
防抖和节流的相同点:
防抖和节流都是为了解决短时间内大量触发某函数而导致的性能问题,比如触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象
防抖:
应用场景:
(1) 用户在输入框中连续输入一串字符后,只会在输入完后去执行最后一次的查询ajax请求,这样可以有效减少请求次数,节约请求资源;
(2) window的resize、scroll事件,不断地调整浏览器的窗口大小、或者滚动时会触发对应事件,防抖让其只触发一次;)
(3)顾名思义,防止用户手抖,或者用户疯狂点击,只会执行最后一次
优化前代码:(如果用户不断点击,这样事件触发就不断延后,但这样的做法始终还是有他的不合理性)
function debounce(fn,wait){
var timer = null;
return function(){
if(timer !== null){
clearTimeout(timer);
}
timer = setTimeout(fn,wait);
}
}
function handle(){
console.log(Math.random());
}
window.addEventListener("resize",debounce(handle,1000));优化后代码:(这样的话事件触发就不会延后了,并且也起到了防抖的效果。
需要防抖的场景还有很多,比如resize,onscroll,click,input等。)
function debounce(fn,wait){
2 var timer = null;
3 return function(){
4 if(timer === null){
5 timer = setTimeout(()=>{
6 fn()
7 clearTimeout(timer)
8 timer = null
9 },wait)
10 }
11 }
12 }
13
14 function handle(){
15 console.log(Math.random());
16 }
17
18 window.addEventListener("click",debounce(handle,1000))节流:
不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数。
应用场景:
(1)鼠标连续不断地触发某事件(如点击),只在单位时间内只触发一次;
(2)在页面的无限加载场景下,需要用户在滚动页面时,每隔一段时间发一次 ajax 请求,而不是在用户停下滚动页面操作时才去请求数据;
(3)监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断;
function throttle(fn,delay){
2 let valid = true
3 return function() {
4 if(!valid){
5 //休息时间 暂不接客
6 return false
7 }
8 // 工作时间,执行函数并且在间隔期内把状态位设为无效
9 valid = false
10 setTimeout(() => {
11 fn()
12 valid = true;
13 }, delay)
14 }
15 }
16 /* 请注意,节流函数并不止上面这种实现方案,
17 例如可以完全不借助setTimeout,可以把状态位换成时间戳,然后利用时间戳差值是否大于指定间隔时间来做判定。
18 也可以直接将setTimeout的返回的标记当做判断条件-判断当前定时器是否存在,如果存在表示还在冷却,并且在执行fn之后消除定时器表示激活,原理都一样
19 */
20
21 function showTop () {
22 var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
23 console.log('滚动条位置:' + scrollTop);
24 }
25 window.onscroll = throttle(showTop,1000) 图片懒加载原理:
说明:
懒加载”说白了就是延迟加载,比如我们加载一个页面,这个页面很长很长,长到我们的浏览器可视区域装不下,那么懒加载就是优先加载可视区域的内容,其他部分等进入了可视区域在加载。
目的:
1.全部加载的话会影响用户体验,如果每次进入页面就请求所有的图片资源,那么可能等图片加载出来用户也早就走了
2.浪费用户的流量,有些用户并不想全部看完,全部加载会耗费大量流量。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
img {
display: block;
margin-bottom: 50px;
width: 400px;
height: 400px;
}
</style>
</head>
<body>
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<script>
let num = document.getElementsByTagName('img').length;
let img = document.getElementsByTagName("img");
let n = 0; //存储图片加载到的位置,避免每次都从第一张图片开始遍历
lazyload(); //页面载入完毕加载可是区域内的图片
window.onscroll = lazyload;
function lazyload() { //监听页面滚动事件
let seeHeight = document.documentElement.clientHeight; //可见区域高度
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop; //滚动条距离顶部高度
for (let i = n; i < num; i++) {
if (img[i].offsetTop < seeHeight + scrollTop) {
if (img[i].getAttribute("src") == "Go.png") {
img[i].src = img[i].getAttribute("data-src");
}
n = i + 1;
}
}
}
</script>
</body>
</html>减少HTTP请求,合理设置HTTP缓存:
资源合并与压缩:
尽可能的将外部的脚本、样式进行合并,多个合为一个。另外, CSS、 Javascript、Image 都可以用相应的工具进行压缩,压缩后往往能省下不少空间。
CSS Sprites:
合并 CSS图片,减少请求数的又一个好办法。
利用CDN加速:
本质仍然是一个缓存,而且将数据缓存在离用户最近的地方,使用户以最快速度获取数据。CDN缓存的一般是静态资源,如图片、文件、CSS、script脚本、静态网页等,但是这些文件访问频度很高,将其缓存在CDN可极大改善网页的打开速度。
先引入CSS再引入JS:
- js代码的下载是阻塞下载,不可以和其他代码并行下载和解析;CSS的加载不会阻塞DOM树的解析
- 页面加载时,是按照从上到下,从左到右的顺序加载的。如果将js放在前面,会立即执行,阻塞后续的资源下载和执行。如果外部脚本加载时间过长,会造成网页长时间失去响应,浏览器就会呈现“假死”状态(阻塞效应)
- 页面需要等到head中的js和css加载完成之后才开始绘制,当js放在body最后时,不需要等待,可以避免资源阻塞,同时使静态页面迅速显示
- 部分js的执行依赖于前面的样式。
- js一般是处理功能,所以不需要提前加载。先给用户观感,在给用户上手体验。
浏览器重绘(Repaint)和回流(Reflow):
回流必将引起重绘,重绘不一定会引起回流
重绘(Repaint):
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
回流(Reflow):
当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:
* 页面首次渲染
* 浏览器窗口大小发生改变
* 元素尺寸或位置发生改变元素内容变化(文字数量或图片大小等等)
* 元素字体大小变化
* 添加或者删除可见的 DOM 元素
* 激活 CSS 伪类(例如:hover)
* 查询某些属性或调用某些方法
* 一些常用且会导致回流的属性和方法
clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftscrollIntoView()、scrollIntoViewIfNeeded()、getComputedStyle()、
getBoundingClientRect()、scrollTo()JS性能优化(事件委托):
减少dom操作,在js中,添加到页面上的事件处理程序数量会直接关系到页面整体运行运行性能。导致这一问题的原因是多方面的。首先函数都是对象,都会占用内存;内存中对象越多,性能就越差。
//html代码
<ul id="myLinks">
<li id="goSomeWhere">Go somewhere</li>
<li id="sayHi">Say hi</hi>
</ul>
//分别加上事件处理-JS代码
let item1 = document.getElementById("goSomeWhere");
let item2 = document.getElementById("sayHi");
EventUtil.addHandler(item1, "click", function(event){
console.log("goSomeWhere")
}
EventUtil.addHandler(item2, "click", function(event){
console.log("sayHi");
}
//改善点即将click事件结合在一起
let list = document.getElementById("myLinks")
EventUtil.addHandler(list, "click", function(event){
event = EventUtil.getEvent(event);
let target = EventUtil.getTarget(event);
switch(target.id){
case "goSomeWhere":
console.log("goSomeWhere");
break;
case "sayHi":
console.log("sayHi");
break;
}
}JS性能优化(作用域链):
访问当前作用域之外的变量时间会增加,所以 访问全局变量总是比访问局部变量要慢,因为需要遍历作用域链。只要能减少花费在作用域链上的时间,就能增加脚本的整体性能。
浅谈SPA, SEO, SSR:
- SPA全称 single page application:
即单页面应用,在传统的网站中,不同的页面之间的切换都是直接从服务器加载一整个新的页面,而在SPA这个模型中,是通过动态地重写页面的部分与用户交互,而避免了过多的数据交换,响应速度自然相对更高,常见的SPA框架有react vue angluar。一般是将框架及网站页面代码发送到浏览器,然后在浏览器中生成和操作DOM(这里也是第一次访问SPA网站在同等带宽及网络延迟下比传统的在后端生成HTML发送到浏览器要更慢的主要原因)
优点:
页面之间的切换非常快,一定程度上减少了后端服务器的压力(不用管页面逻辑和渲染),后端程序只需要提供API,完全不用管客户端到底是Web界面还是手机等。
缺点:
首屏打开速度很慢,因为用户首次加载需要先下载SPA框架及应用程序的代码,然后再渲染页面。不利于SEO
- 搜索引擎优化SEO(Search Engine Optimization):
seo是通过总结搜索引擎的排名规律,对网站进行合理的优化是你的网站在搜索引擎的排名提高,从而利用搜索引擎给自己带来客户。优化:
(1)当前页面的title上出现这个关键字;
(2)当前页面的keywords和description中出现这个关键字;
(3)当前页面的内容里多次出现这个关键词,并且在第一次出现的时候,加粗;
(4)其他页面的锚文本里,出现这个关键词。
- SSR Server-Side Rendering(服务器端渲染):
常用的SSR框架有:React 的 Next,Vue.js 的 Nuxt 。因为SPA不利于SEO,因为搜索引擎对于异步数据的支持也还不足,如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容。所以有了SSR:将SPA应用打包到服务器上,在服务器上渲染出HTML,发送到浏览器
优点:
更快的响应时间,不用等待所有的JS都下载完成,浏览器便能显示比较完整的页面了。我们可以将SEO的关键信息直接在后台就渲染成HTML,而保证搜索引擎的爬虫都能爬取到关键数据
缺点:
相对于仅仅需要提供静态文件的服务器,SSR中使用的渲染程序自然会占用更多的CPU和内存资源; 一些常用的浏览器API可能无法正常使用,比如window、docment和alert等,如果使用的话需要对运行的环境加以判断; 开发调试会有一些麻烦,因为涉及了浏览器及服务器,对于SPA的一些组件的生命周期的管理会变得复杂; 可能会由于某些因素导致服务器端渲染的结果与浏览器端的结果不一致。
渲染篇介绍:
客户端渲染:
客户端渲染模式下,服务端会把渲染需要的静态文件发送给客户端,客户端加载过来之后,自己在浏览器里跑一遍 JS,根据 JS 的运行结果,生成相应的 DOM
<!doctype html>
<html>
<head>
<title>我是客户端渲染的页面</title>
</head>
<body>
<div id='root'></div>
<script src='index.js'></script>
</body>
</html>根节点下到底是什么内容呢?你不知道,我不知道,只有浏览器把 index.js 跑过一遍后才知道,这就是典型的客户端渲染。
页面上呈现的内容,你在 html 源文件里里找不到——这正是它的特点。
服务端渲染:
服务端渲染的模式下,当用户第一次请求页面时,由服务器把需要的组件或页面渲染成 HTML 字符串,然后把它返回给客户端。客户端拿到手的,是可以直接渲染然后呈现给用户的 HTML 内容,不需要为了生成 DOM 内容自己再去跑一遍 JS 代码。使用服务端渲染的网站,可以说是“所见即所得”,页面上呈现的内容,我们在 html 源文件里也能找到。
服务端渲染解决的问题:
事实上,很多网站是出于效益(seo)的考虑才启用服务端渲染,性能倒是在其次。
假设 A 网站页面中有一个关键字叫“前端性能优化”,这个关键字是 JS 代码跑过一遍后添加到 HTML 页面中的。那么客户端渲染模式下,我们在搜索引擎搜索这个关键字,是找不到 A 网站的——搜索引擎只会查找现成的内容,不会帮你跑 JS 代码。A 网站的运营方见此情形,感到很头大:搜索引擎搜不出来,用户找不到我们,谁还会用我的网站呢?为了把“现成的内容”拿给搜索引擎看,A 网站不得不启用服务端渲染。
但性能在其次,不代表性能不重要。服务端渲染解决了一个非常关键的性能问题——首屏加载速度过慢。在客户端渲染模式下,我们除了加载 HTML,还要等渲染所需的这部分 JS 加载完,之后还得把这部分 JS 在浏览器上再跑一遍。这一切都是发生在用户点击了我们的链接之后的事情,在这个过程结束之前,用户始终见不到我们网页的庐山真面目,也就是说用户一直在等!相比之下,服务端渲染模式下,服务器给到客户端的已经是一个直接可以拿来呈现给用户的网页,中间环节早在服务端就帮我们做掉了,用户岂不“美滋滋”?
服务端渲染本质上是本该浏览器做的事情,分担给服务器去做。这样当资源抵达浏览器时,它呈现的速度就快了
除非网页对性能要求太高了,以至于所有的招式都用完了,性能表现还是不尽人意,这时候我们就可以考虑向老板多申请几台服务器,把服务端渲染搞起来了~
浏览器内核:
浏览器内核可以分成两部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎。渲染引擎又包括了 HTML 解释器、CSS 解释器、布局、网络、存储、图形、音视频、图片解码器等等零部件。
渲染阻塞:
HTML、CSS 和 JS,都具有阻塞渲染的特性。
JS 引擎是独立于渲染引擎存在的。我们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。 因此与其说是 JS 把 CSS 和 HTML 阻塞了,不如说是 JS 引擎抢走了渲染引擎的控制权。
- CSS会阻塞dom树解析?(求证):
<!DOCTYPE html>
<html lang="en">
<head>
<title>css阻塞</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
h1 {
color: red !important
}
</style>
<script>
function h () {
console.log(document.querySelectorAll('h1'))
}
setTimeout(h, 0)
</script>
<link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet">
</head>
<body>
<h1>这是红色的</h1>
</body>
</html>假设: css加载会阻塞DOM树解析和渲染
假设结果: 在bootstrap.css还没加载完之前,下面的内容不会被解析渲染,那么我们一开始看到的应该是白屏,h1不会显示出来。并且此时console.log的结果应该是一个空数组。
实际结果:

由上图我们可以看到,当css还没加载完成的时候,h1并没有显示,但是此时控制台输出了h1,可以得知,此时DOM树至少已经解析完成到了h1那里,而此时css还没加载完成,也就说明,css并不会阻塞DOM树的解析。
- CSS会阻塞dom树渲染?(求证):
由上图,当css还没加载出来的时候,页面显示白屏,直到css加载完成之后,红色字体才显示出来,也就是说,下面的内容虽然解析了,但是并没有被渲染出来。所以,css加载会阻塞DOM树渲染。
原因:
这可能也是浏览器的一种优化机制。因为你加载css的时候,可能会修改下面DOM节点的样式,如果css加载不阻塞DOM树渲染的话,那么当css加载完之后,DOM树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。所以我干脆就先把DOM树的结构先解析完,把可以做的工作做完,然后等你css加载完之后,在根据最终的样式来渲染DOM树,这种做法性能方面确实会比较好一点。
由于 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,
所以他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成(或者 CSS 资源加载失败)后,才能开始渲染。因此,CSS 加载会阻塞 Dom 的渲染。
- CSS加载会阻塞js执行么(求证):
<!DOCTYPE html>
<html lang="en">
<head>
<title>css阻塞</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
console.log('before css')
var startDate = new Date()
</script>
<link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet">
</head>
<body>
<h1>这是红色的</h1>
<script>
var endDate = new Date()
console.log('after css')
console.log('经过了' + (endDate -startDate) + 'ms')
</script>
</body>
</html>假设: css加载会阻塞后面的js运行
预期结果: 在link后面的js代码,应该要在css加载完成后才会运行

由上图我们可以看出,位于css加载语句前的那个js代码先执行了,但是位于css加载语句后面的代码迟迟没有执行,直到css加载完成后,它才执行。这也就说明了,css加载会阻塞后面的js语句的执行。详细结果看下图(css加载用了5600+ms):
结论:
- css加载不会阻塞DOM树的解析
- css加载会阻塞DOM树的渲染
- css加载会阻塞后面js语句的执行、
因此,为了避免让用户看到长时间的白屏时间,我们应该尽可能的提高css加载速度,比如可以使用以下几种方法:
- 使用CDN(因为CDN会根据你的网络状况,替你挑选最近的一个具有缓存内容的节点为你提供资源,因此可以减少加载时间)
- 对css进行压缩(可以用很多打包工具,比如webpack,gulp等,也可以通过开启gzip压缩)
- 合理的使用缓存(设置cache-control,expires,以及E-tag都是不错的,不过要注意一个问题,就是文件更新后,你要避免缓存而带来的影响。其中一个解决防范是在文件名字后面加一个版本号)
- 减少http请求数,将多个css文件合并,或者是干脆直接写成内联样式(内联样式的一个缺点就是不能缓存)
其他:
淘系、华系、腾系等面试题:
安全:
- XSS攻击(Cross-Site Scripting)即跨站脚本攻击:
是一种代码注入攻击。比如在一个网站能输入的地方输入一些恶意脚本,由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,利用这些信息冒充用户向网站发起攻击者定义的请求。 攻击者向有 XSS 漏洞的网站中输入恶意的 HTML 代码,当其它用户浏览该网站时候,该段 HTML 代码会自动执行,从而达到攻击的目的,如盗取用户的 Cookie,破坏页面结构,重定向到其它网站等 。
防御:
利用正则检查输入内容进行过滤,对输入的内容进行转义,比如:
- & 替换 为:
& - < 替换为
<; - > 替换为
> - ‘’ 替换为
" - ’ 替换为
' - / 替换为
/
- CSRF攻击(Cross-site request forgery)即跨域请求伪造:
原理:
- 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
- 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
- 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
- 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
- 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行
防御:
CSRF攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token,来把正常的请求和攻击的请求区分开。跟验证码类似,只是用户无感知。
服务端给用户生成一个token,加密后传递给用户
用户在提交请求时,需要携带这个token
服务端验证token是否正确。
HTML5和CSS3常问:
MVVM中的双向绑定:
由视图(View)、视图模型(ViewModel)、模型(Model)三部分组成。vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。
1.当一个对象(或变量)的属性改变,那么调用这个属性的地方显示也应该改变,模型到视图(model => view)
2.当调用属性的这个地方改变了这个属性(通常是一个表单元素),那么这个对象(或变量)的属性也会改为最新的值 ,即视图到模型(view => model)
如下通过defineProperty实现双向绑定例子:
PS:加入了 用户输入中文的判断(用户输入中文时,频繁触发 keyup事件,但实际上输入并没有结束。)
语法: Object.defineProperty(obj, prop, descriptor)obj: 需要被操作的目标对象
prop: 目标对象需要定义或修改的属性的名称
descriptor: 将被定义或修改的属性的描述符
var obj = new Object();
Object.defineProperty(obj, 'name', {
configurable: false,
writable: true,
enumerable: true,
value: '张三'
})
console.log(obj.name) //张三<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>双向绑定</title>
</head>
<body>
手写一个简单双向绑定<br/>
<input type="text" id="model"><br/>
<div id="modelText"></div>
</body>
<script>
var model = document.querySelector("#model");
var modelText = document.querySelector("#modelText");
var defaultName = "defaultName";
var userInfo = {}
model.value = defaultName;
Object.defineProperty(userInfo, "name", {
get: function () {
return defaultName;
},
set: function (value) {
defaultName = value;
model.value = value;
console.log("-----value");
console.log(value);
modelText.textContent = value;
}
})
userInfo.name = "new value";
var isEnd = true;
model.addEventListener("keyup", function () {
if (isEnd) {
userInfo.name = this.value;
}
}, false)
//加入监听中文输入事件
model.addEventListener("compositionstart", function () {
console.log("开始输入中文");
isEnd = false;
})
model.addEventListener("compositionend", function () {
isEnd = true;
console.log("结束输入中文");
})
</script>
</html>每当通过userInfo.name ="***"; 这样的方式设置对象值得时候,就会触发defineProperty这个里面的set方法。每当通过userInfo.name这个方式获取对象的值得时候就会触发defineProperty这个里面的get方法。
MVC和MVVM:
MVC :
即 Model-View-Controller 的缩写,就是 模型—视图—控制器,也就是说一个标准的Web 应用程式是由这三部分组成的:
View :用来把数据以某种方式呈现给用户
Model :其实就是数据
Controller :接收并处理来自用户的请求,并将 Model 返回给用户
MVC同时也会带来的问题:
1、 开发者在代码中大量调用相同的 DOM API,处理繁琐 ,操作冗余,使得代码难以维护。
2、大量的DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。
3、 当 Model 频繁发生变化,开发者需要主动更新到View ;当用户的操作导致 Model 发生变化,开发者同样需要将变化的数据同步到Model 中,这样的工作不仅繁琐,而且很难维护复杂多变的数据状态。
MVVM:
MVVM 由 Model、View、ViewModel 三部分构成,Model 层代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI 展现出来,ViewModel 是一个同步View 和 Model的对象。
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
关于同源策略:
目的:出于信息安全考虑。
同源条件:
协议相同 域名相同 端口相同 举例:http://www.example.com/dir/page.html 这个网址,协议是http://,域名是www.example.com,端口是80(默认端口可以省略),同源情况如下:
http://www.example.com/dir2/other.html:同源(协议,域名,端口都相等)
http://example.com/dir/other.html:不同源(域名不同)
http://v2.www.example.com/dir/other.html:不同源(域名不同)
http://www.example.com:81/dir/other.html:不同源(端口不同)
https://www.example.com/dir/page.html:不同源(协议不同)
不同源的限制范围:
- 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB。
- 无法接触非同源网页的 DOM。
- 无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应)。
允许跨域行为实例:
<script src="..."></script>标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。<link rel="stylesheet" href="...">标签嵌入CSS。由于CSS的松散的语法规则,CSS的跨域需要一个设置正确的Content-Type消息头。不同浏览器有不同的限制: IE, Firefox, Chrome, Safari 和 Opera。<img>嵌入图片。支持的图片格式包括PNG,JPEG,GIF,BMP,SVG<video> 和 <audio>嵌入多媒体资源。<object>, <embed> 和 <applet>的插件。@font-face引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts)。<frame>和<iframe>载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互2、 document.domain + iframe跨域
3、 location.hash + iframe
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资源共享(CORS)
7、 nginx代理跨域
8、 nodejs中间件代理跨域
9、 WebSocket协议跨域
单点登录:
单点登录 SSO 全称 Singn Sign On 。SSO 指在多个应用系统中,用户只需要登录一次用户系统,就可以访问其他互相信任的应用系统。例如:在网易官网登录账户,那么再进入网易邮箱等其他业务的时候会自动登录。另外还有一个好处就是在一定时间内可以不用重复登录账户。
问题:
单点登录出现在二级域名的时候就不用跨域,可以通过cookie来共享登录信息。例如bbb.a.com 和 ccc.a.com 是二级域名。
单点登录出现在跨域里面比如(www.aaa.com 和 www.bbb.com):
一旦跨域就不能通过cookie来共享后台返回的登录信息。
这时就要通过postMessage来传递登录后返回的信息,比如token。得到来自不同域的信息后就可以实现单点登录了。
例子:
在a页面点击登陆过后,b页面直接影藏登录按钮,显示该用户已经登录。就不用再次登陆
a.html页面代码:
<title>A首页</title>
<meta charset="utf-8">
<p>
<button onclick="login();" id="login">同步登录</button>
<p id="msg" style="display: none;">该用户已经登录
<button onclick="localStorage.clear();" id="login">注销</button>
</p>
</p>
<iframe src="http://www.b.com" style="height: 0px;width: 0px;display: none;"></iframe>
<script>
// 是否显示“同步登录”按钮
var tokenData=localStorage.getItem('tokenData')
if(tokenData){
document.getElementById('login').style.display='none';
document.getElementById('msg').style.display='block';
}
// 同步登录
function login(){
let data=JSON.stringify({
msg:'建议以字符串形式传输字符串',
token:'这是从后台而来的token'
})
document.getElementsByTagName('iframe')[0].contentWindow.postMessage(data,'*');
}
// 有消息从父级传来时 存贮 tokenData
window.addEventListener('message',function(e){
if(e.source!=window.parent) return;
localStorage.setItem("tokenData",e.data);
},false);
</script>b.html页面代码:
<title>B首页</title>
<meta charset="utf-8">
<p>
<button onclick="login();" id="login">同步登录</button>
<p id="msg" style="display: none;">该用户已经登录
<button onclick="localStorage.clear();" id="login">注销</button>
</p>
</p>
<iframe src="http://www.a.com" style="height: 0px;width: 0px;display: none;"></iframe>
<script>
// 是否显示“同步登录”按钮
var tokenData=localStorage.getItem('tokenData')
if(tokenData){
document.getElementById('login').style.display='none';
document.getElementById('msg').style.display='block';
}
// 同步登录
function login(){
let data=JSON.stringify({
msg:'建议以字符串形式传输字符串',
token:'这是从后台而来的token'
})
document.getElementsByTagName('iframe')[0].contentWindow.postMessage(data,'*');
}
// 有消息从父级传来时 存贮 tokenData
window.addEventListener('message',function(e){
if(e.source!=window.parent) return;
localStorage.setItem("tokenData",e.data);
},false);
</script>各种上传问题:
掘金:
github:
我的关注/
别人的优秀代码:github.com/Bigerfe/fe-…