前端面试总结二

1.关于同步加载、异步加载、延迟加载、预加载(form和ajax之间的区别)
同步加载:

<script src="http://yourdomain.com/script.js"></script> 

(阻塞模式)会阻止浏览器的后续处理,停止对后续文件的解析、执行,如图像的渲染。浏览器之所以会采用同步模式,是因为加载的js文件中对dom的操作、重定向、输出document等默认行为,所以同步才是最安全的。通常会把要加载的js放到body结束标签之前,使得js可在页面最后加载,尽量减少阻塞页面的渲染。这样可以先让页面显示出来。

异步加载:

(function() { 
var s = document.createElement('script'); 
s.type = 'text/javascript'; 
s.async = true; 
s.src = 'http://yourdomain.com/script.js'; 
var x = document.getElementsByTagName('script')[0]; 
 x.parentNode.insertBefore(s, x); 
})();

(非阻塞模式)浏览器在下载js的同时,还会执行后续的页面处理。在script标签内,用js创建一个script元素并插入到document中,这种就是异步加载js文件了。同步加载流程是瀑布模型,异步加载流程是并发模型。

延迟加载:
有些 js 代码并不是页面初始化的时候就立刻需要的,而是稍后的某些情况才需要的。延迟加载就是一开始并不加载这些暂时不用的js,而是在需要的时候或稍后再通过js 的控制来异步加载。也就是将 js 切分成许多模块,页面初始化时只加载需要立即执行的 js ,然后其它 js 的加载延迟到第一次需要用到的时候再加载。特别是页面有大量不同的模块组成,很多可能暂时不用或根本就没用到。就像图片的延迟加载,在图片出现在可视区域内时(在滚动条下拉)才加载显示图片。

预加载:
预加载是一种浏览器机制,使用浏览器空闲时间来预先下载/加载用户接下来很可能会浏览的页面/资源,当用户访问某个预加载的链接时,如果从缓存命中,页面就得以快速呈现。

同步和异步的区别:
同步是阻塞模式,异步是非阻塞模式。
同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;
异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。

2.同源策略
同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制以下几种行为:

1) Cookie、LocalStorage 和 IndexDB 无法读取
2) DOM 和 Js对象无法获得
3) AJAX 请求不能发送

3.js中几种实用的跨域方法
这里说的js跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同域的框架中(iframe)的数据。只要协议、域名、端口有任何一个不同,都被当作是不同的域。
要解决跨域的问题,我们可以使用以下几种方法:
(1)通过jsonp跨域
在js中,我们直接用XMLHttpRequest请求不同域上的数据是不可以的。但是,在页面上引入不同域上的js脚本文件却是可以的,jsonp正是利用这个特性来实现的。
比如,有个a.html页面,它里面的代码需要利用ajax获取一个不同域上的json数据,假设这个json数据地址是http://example.com/data.php,那么a.html中的代码就可以这样:

<script>
    function dosomething (jsondata) {
    //处理获得的json数据
    }
</script>
<script src = "http://example.com/data.php?callback=dosomething"></script>

我们看到获取数据的地址后面还有一个callback参数,按惯例是用这个参数名,但是用其他的也一样。当然如果获取数据的jsonp地址页面不是你自己能控制的,就得按照提供数据的那一方的规定格式来操作了。
因为是当做一个js文件来引入的,所以http://example.com/data.php返回的必须是一个能执行的js文件。
jsonp的原理就是通过script标签引入一个js文件,这个js文件载入成功后会执行我们在url参数中指定的函数,并且会把我们需要的json数据作为参数传入。所以jsonp是需要服务器端的页面进行相应的配合的。

<script>
$.getJSON('http://example.com/data.php?callback=?',function(jsondata){
    //处理获得的json数据
});
</script>

原理是一样的,只不过我们不需要手动的插入script标签以及定义回调函数。jquery会自动生成一个全局函数来替换callback=?中的问号,之后获取到数据后又会自动销毁,实际上就是起一个临时代理函数的作用。$.getJSON方法会自动判断是否跨域,不跨域的话,就调用普通的ajax方法;跨域的话,则会以异步加载js文件的形式来调用jsonp的回调函数。
jsonp缺点:只能实现get一种请求。
(2)通过修改document.domain来跨子域
浏览器都有一个同源策略,其限制之一就是第一种方法中我们说的不能通过ajax的方法去请求不同源中的文档。它的第二个限制是浏览器中不同域的框架之间是不能进行js的交互操作的。有一点需要说明,不同的框架之间(父子或同辈),是能够获取到彼此的window对象的,但是却不能使用获取到的window对象的属性和方法(html5中的postMessage方法是一个例外,还有些浏览器比如ie6也可以使用top、parent等少数几个属性),总之,你可以当做是只能获取到一个几乎无用的window对象。比如,有一个页面,它的地址是http://www.example.com/a.html,在这个页面里面有一个iframe,它的src是http://example.com/b.html, 很显然,这个页面与它里面的iframe框架是不同域的,所以我们是无法通过在页面中书写js代码来获取iframe中的东西的:

<script>
function onLoad (){
    var iframe = document.getElementById('iframe');
    var win = iframe.contentWindow;//这里是能够获取到iframe里的window对象,但该window对象的属性和方法几乎是不可用的
    var doc = win.document;//这里是获取不到iframe里的document对象的
    var name = win.name;//这里同样获取不到window对象的name属性的
}
</script>
<iframe id="iframe" src="http://exampe.com/b.html" onload="onLoad()"></iframe>

这个时候,document.domain就可以派上用场了,我们只要把http://www.example.com/a.html 和 http://example.com/b.html这两个页面的document.domain都设成相同的域名就可以了。但要注意的是,document.domain的设置是有限制的,我们只能把document.domain设置成自身或更高一级的父域,且主域必须相同。例如:a.b.example.com 中某个文档的document.domain 可以设成a.b.example.com、b.example.com 、example.com中的任意一个,但是不可以设成 c.a.b.example.com,因为这是当前域的子域,也不可以设成baidu.com,因为主域已经不相同了。
在页面 http://www.example.com/a.html 中设置document.domain:

<iframe src="http://exampe.com/b.html" id="iframe" onload="test()"></iframe>
<script>
document.domain = 'example.com';//设置成主域
function test (){
    alert(document.getElementById('iframe').contentWindow);
}
</script>

在页面 http://example.com/b.html 中也设置document.domain,而且这也是必须的,虽然这个文档的domain就是example.com,但是还是必须显示的设置document.domain的值:

<script>
document.domain = 'example.com';//在iframe载入的这个页面也设置document.domain,使之与主页面的document.domain相同
</script>

这样我们就可以通过js访问到iframe中的各种属性和对象了
不过如果想在http://www.example.com/a.html页面中通过ajax直接请求http://example.com/b.html页面,即使设置了相同的document.domain也还是不行的,所以修改document.domain的方法只适用于不同子域的框架间的交互。如果想通过ajax的方法去与不同子域的页面交互,除了使用jsonp的方法外,还可以用一个隐藏的iframe来做一个代理。原理就是让这个iframe载入一个与你想要通过ajax获取数据的目标页面处在相同的域的页面,所以这个iframe中的页面是可以正常使用ajax去获取你要的数据的,然后就是通过我们刚刚讲得修改document.domain的方法,让我们能通过js完全控制这个iframe,这样我们就可以让iframe去发送ajax请求,然后收到的数据我们也可以获得了。
(3)location.hash + iframe跨域
实现原理: a域与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
a.html:(http://www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

b.html:(http://www.domain2.com/b.html)

<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

c.html:(http://www.domain1.com/c.html)

<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

(4)使用window.name来进行跨域
window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。
比如:有一个页面a.html,它里面有这样的代码:

<script>
window.name = '我是页面a设置的值';//设置window.name的值
setTimeout(function(){
    window.location = 'b.html'
},3000);//3秒后把一个新页面b.html载入到当前的window
</script>

再看看b.html页面的代码:

<script>
alert(window.name);//读取window.name的值
</script>

a.html页面载入后3秒,跳转到了b.html页面,结果为:
在这里插入图片描述
我们看到在b.html页面上成功获取到了它的上一个页面a.html给window.name设置的值。如果在之后所有载入的页面都没对window.name进行修改的话,那么所有这些页面获取到的window.name的值都是a.html页面设置的那个值。当然,如果有需要,其中的任何一个页面都可以对window.name的值进行修改。注意,window.name的值只能是字符串的形式,这个字符串的大小最大能允许2M左右甚至更大的一个容量,具体取决于不同的浏览器,但一般是够用了。
上面的例子中,我们用到的页面a.html和b.html是处于同一个域的,但是即使a.html与b.html处于不同的域中,上述结论同样是适用的,这也正是利用window.name进行跨域的原理。
下面就来看一看具体是怎么样通过window.name来跨域获取数据的。还是举例说明。
比如有一个www.example.com/a.html页面,需要通过a.html页面里的js来获取另一个位于不同域上的页面www.cnblogs.com/data.html里的数据。
data.html页面里的代码很简单,就是给当前的window.name设置一个a.html页面想要得到的数据值。data.html里的代码:

<script>
window.name = '我是页面a.html想要的数据,所有可以转化成字符串来传递的数据都可以在这里使用,比如可以传递一个json数据';
</script>

那么在a.html页面中,我们怎么把data.html页面载入进来呢?显然我们不能直接在a.html页面中通过改变window.location来载入data.html页面,因为我们想要即使a.html页面不跳转也能得到data.html里的数据。答案就是在a.html页面中使用一个隐藏的iframe来充当一个中间人角色,由iframe去获取data.html的数据,然后a.html再去得到iframe获取到的数据。
充当中间人的iframe想要获取到data.html的通过window.name设置的数据,只需要把这个iframe的src设为www.cnblogs.com/data.html就行了。然后a.html想要得到iframe所获取到的数据,也就是想要得到iframe的window.name的值,还必须把这个iframe的src设成跟a.html页面同一个域才行,不然根据前面讲的同源策略,a.html是不能访问到iframe里的window.name属性的。这就是整个跨域过程。
看下a.html页面的代码:

<!Doctype html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>window.name跨域</title>
    <script>
    function getData(){//iframe载入data.html页面后会执行此函数
        var iframe = document.getElementById('proxy');
        iframe.onload = function(){//这个时候a.html与iframe已经是处于同一源了,可以互相访问
            var data = iframe.contentWindow.name;//获取iframe里的window.name,也就是data.html页面给它设定的数据
            alert(data);//成功获取到了data.html里的数据
        }
        iframe.src = 'b.html';//这里的b.html为随便一个页面,只要与a.html同源就行了,目的是让a.html能访问到iframe里的东西,设置成about:blank也行
    }
    </script>
</head>
<body>
    <iframe id="proxy" src="http://www.cnblogs.com/data.html" style="display:none" onload="getData()"></iframe>
</body>
</html>

上面的代码只是最简单的原理演示代码,你可以对使用js封装上面的过程,比如动态的创建iframe,动态的注册各种事件等等,当然为了安全,获取完数据后,还可以销毁作为代理的iframe。网上也有很多类似的现成代码,有兴趣的可以去找一下。
通过window.name来进行跨域,就是这样子的。
(5)使用HTML5中新引进的window.postMessage方法来跨域传送数据
window.postMessage(message,targetOrigin)方法是html5新引进的特性,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源,目前IE8+、FireFox、Chrome、Opera等浏览器都已经支持window.postMessage方法。
调用postMessage方法的window对象是指要接收消息的那一个window对象,该方法的第一个参数message为要发送的消息,类型只能为字符串;第二个参数targetOrigin用来限定接收消息的那个window对象所在的域,如果不想限定域,可以使用通配符 * 。
需要接收消息的window对象,可通过监听自身的message事件来获取传过来的消息,消息内容储存在该事件对象的data属性中。
上面所说的向其他window对象发送消息,其实就是指一个页面有几个框架的那种情况,因为每一个框架都有一个window对象。在讨论第二种方法的时候,我们说过,不同域的框架间是可以获取到对方的window对象的,而且也可以使用window.postMessage这个方法。下面看一个简单的示例,有两个页面

<script>
	function onLoad(){
		var iframe = document.getElementById('iframe');
		var win = iframe.contentWindow;//获取window对象
		win.postMessage('哈哈,我是来自页面a的消息','*');//向不同域的http://www.test.com/b.html页面发送消息
	}
</script>
<iframe id="iframe" src="http://www.test.com/b.html" onload="onLoad()"></iframe>
<script>	
window.onmessage = function(e){//注册message事件用来接收消息
	e = e||event;//获取事件对象
	alert(e.data);//通过data属性得到传送的消息
}
</script>

我们运行a页面后得到的结果:
在这里插入图片描述
我们看到b页面成功的收到了消息。
使用postMessage来跨域传送数据还是比较直观和方便的,但是缺点是IE6、IE7不支持,所以用不用还得根据实际需要来决定。
(6)跨域资源共享(CORS)
普通跨域请求:服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。
需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。如果想实现当前页cookie的写入,可参考下文:(7)nginx反向代理中设置proxy_cookie_domain 和 (8)NodeJs中间件代理中cookieDomainRewrite参数的设置。
目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。
前端设置:
1)原生ajax

// 前端设置是否带cookie
xhr.withCredentials = true;

示例代码:

var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie
xhr.withCredentials = true;

xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');

xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};

2)jQuery ajax

$.ajax({
    ...
   xhrFields: {
       withCredentials: true    // 前端设置是否带cookie
   },
   crossDomain: true,   // 会让请求头中包含跨域的额外信息,但不会含cookie
    ...
});

3)vue框架
axios设置:

axios.defaults.withCredentials = true

vue-resource设置:

Vue.http.options.credentials = true

服务端设置:
若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。
1)Java后台:

/*
 * 导入包:import javax.servlet.http.HttpServletResponse;
 * 接口参数中定义:HttpServletResponse response
 */

// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com"); 

// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true"); 

// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");

2)Nodejs后台示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var postData = '';

    // 数据块接收中
    req.addListener('data', function(chunk) {
        postData += chunk;
    });

    // 数据接收完毕
    req.addListener('end', function() {
        postData = qs.parse(postData);

        // 跨域后台设置
        res.writeHead(200, {
            'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
            'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
            /* 
             * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
             * 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
             */
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
        });

        res.write(JSON.stringify(postData));
        res.end();
    });
});

server.listen('8080');
console.log('Server is running at port 8080...');

(7)nginx代理跨域
1)nginx配置解决iconfont跨域
浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}

2)nginx反向代理接口跨域
跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨域问题。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
nginx具体配置:

#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

1)前端代码示例

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();

2)Nodejs后台示例

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));

    // 向前台写cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
    });

    res.write(JSON.stringify(params));
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

(8) Nodejs中间件代理跨域
node中间件实现跨域代理,原理大致与nginx相同,都是通过开启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。
非vue框架的跨域(2次跨域)
利用node + express + http-proxy-middleware搭建一个proxy服务器。
1)前端代码示例:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();

2)中间件服务器:

var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
    // 代理跨域目标接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,

    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },

    // 修改响应信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
}));

app.listen(3000);
console.log('Proxy server is listen at port 3000...');

3)Nodejs后台同((6)nginx)

vue框架的跨域(1次跨域)
利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。
webpack.config.js部分配置:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

(9)WebSocket协议跨域
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
1)前端代码:

<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

2)Nodejs socket后台:

var http = require('http');
var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});

4.三种本地存储方式
(1)Cookie
作用:
cookie是纯文本,没有可执行代码。存储数据,当用户访问了某个网站(网页)的时候,我们就可以通过cookie来向访问者电脑上存储数据,或者某些网站为了辨别用户身份、进行session跟踪而储存在用户本地终端上的数据(通常经过加密)。
如何工作:
当网页要发http请求时,浏览器会先检查是否有相应的cookie,有则自动添加在request header中的cookie字段中。这些是浏览器自动帮我们做的,而且每一次http请求浏览器都会自动帮我们做。这个特点很重要,因为这关系到“什么样的数据适合存储在cookie中”。
存储在cookie中的数据,每次都会被浏览器自动放在http请求中,如果这些数据并不是每个请求都需要发给服务端的数据,浏览器设置自动处理无疑增加了网络开销;但如果这些数据是每个请求都需要发给服务端的数据(比如身份认证信息),浏览器设置自动处理就大大免去了重复添加操作。所以对于那种设置“每次请求都要携带的信息(最典型的就是身份认证信息)”就特别适合放在cookie中,其他类型的数据就不适合了。

特征:
1)不同的浏览器存放的cookie位置不一样,也是不能通用的;
2)cookie的存储是以域名形式进行区分的,不同的域下存储的cookie是独立的;
3)我们可以设置cookie生效的域(当前设置cookie所在域的子域),也就是说,我们能够操作的cookie是当前域以及当前域下的所有子域;
4)一个域名下存放的cookie的个数是有限制的,不同的浏览器存放的个数不一样,一般为20个;
5)每个cookie存放的内容大小也是有限制的,不同的浏览器存放大小不一样,一般为4KB;
6)cookie也可以设置过期的时间,默认是会话结束的时候,当时间到期自动销毁

读取:
我们通过 document.cookie 来获取当前网站下的cookie的时候,得到的字符串形式的值,它包含了当前网站下所有的cookie(为避免跨域脚本(xss)攻击,这个方法只能获取非 HttpOnly 类型的cookie)。它会把所有的cookie通过一个分号+空格的形式串联起来,例如 username=chenfangxu;job=codin

修改cookie:
要想修改一个cookie,只需要重新赋值就行,旧的值会被新的值覆盖。但要注意一点,在设置新cookie时,path/domain这几个选项一定要与旧cookie 保持一样。否则不会修改旧值,而是添加了一个新的 cookie。

删除:
把要删除的cookie的过期时间设置成已过去的时间,path/domain/这几个选项一定要旧cookie 保持一样。

cookie的属性(可选项):
过期时间:
如果我们想长时间存放一个cookie。需要在设置这个cookie的时候同时给他设置一个过期的时间。如果不设置,cookie默认是临时存储的,当浏览器关闭进程的时候自动销毁。
注意:document.cookie = ‘名称=值;expires=’ + GMT(格林威治时间)格式的日期型字符串
一般设置天数: newDate().setDate(oDate.getDate()+5);比当前时间多5天。

cookie的域概念:
domain指定了 cookie 将要被发送至哪个或哪些域中。默认情况下,domain 会被设置为创建该 cookie 的页面所在的域名,所以当给相同域名发送请求时该 cookie 会被发送至服务器。
浏览器会把 domain 的值与请求的域名做一个尾部比较(即从字符串的尾部开始比较),并将匹配的 cookie 发送至服务器

document.cookie = "username=cfangxu;path=/;domain=qq.com"

如上:“www.qq.com" 与 “sports.qq.com” 公用一个关联的域名"qq.com",我们如果想让 “sports.qq.com” 下的cookie被 “www.qq.com” 访问,我们就需要用到 cookie 的domain属性,并且需要把path属性设置为 “/”。
服务端设置:

Set-Cookie:username=zxy;path=/;domain=qq.com;

注:一定的是同域之间的访问,不能把domain的值设置成非主域的域名。

cookie的路径概念(path选项):
cookie 一般都是由于用户访问页面而被创建的,可是并不是只有在创建 cookie 的页面才可以访问这个 cookie。 因为安全方面的考虑,默认情况下,只有与创建 cookie 的页面在同一个目录或子目录下的网页才可以访问。即path属性可以为服务器特定文档指定cookie,这个属性设置的url且带有这个前缀的url路径都是有效的。

客户端设置:
最常用的例子就是让 cookie 在根目录下,这样不管是哪个子页面创建的 cookie,所有的页面都可以访问到了。

document.cookie="username=zxy;path=/"

domain和path总结:
domain是域名,path是路径,两者加起来就构成了 URL,domain和path一起来限制 cookie 能被哪些 URL 访问。
所以domain和path两个选项共同决定了cookie何时被浏览器自动添加到请求头部中发送出去。如果没有设置这两个选项,则会使用默认值。domain的默认值为设置该cookie的网页所在的域名,path默认值为设置该cookie的网页所在的目录。

cookie的安全性(secure选项):
通常 cookie 信息都是使用HTTP连接传递数据,这种传递方式很容易被查看,所以 cookie 存储的信息容易被窃取。假如 cookie 中所传递的内容比较重要,那么就要求使用加密的数据传输。
secure选项用来设置cookie只在确保安全的请求中才会发送。当请求是HTTPS或者其他安全协议时,包含 secure 选项的 cookie 才能被发送至服务器。

document.cookie="username=zxy;secure"

把cookie设置为secure,只保证 cookie 与服务器之间的数据传输过程加密,而保存在本地的 cookie文件并不加密。就算设置了secure 属性也并不代表他人不能看到机器本地保存的 cookie 信息。机密且敏感的信息绝不应该在 cookie 中存储或传输,因为 cookie 的整个机制原本就是不安全的。
注意:如果想在客户端即网页中通过 js 去设置secure类型的 cookie,必须保证网页是https协议的。在http协议的网页中是无法设置secure类型cookie的。

httpOnly:
这个选项用来设置cookie是否能通过 js 去访问。默认情况下,cookie不会带httpOnly选项(即为空),所以默认情况下,客户端是可以通过js代码去访问(包括读取、修改、删除等)这个cookie的。当cookie带httpOnly选项时,客户端则无法通过js代码去访问(包括读取、修改、删除等)这个cookie。
在客户端是不能通过js代码去设置一个httpOnly类型的cookie的,这种类型的cookie只能通过服务端来设置。

cookie的编码:
cookie其实是个字符串,但这个字符串中等号、分号、空格被当做了特殊符号。所以当cookie的 key 和 value 中含有这3个特殊字符时,需要对其进行额外编码,一般会用escape进行编码,读取时用unescape进行解码;当然也可以用encodeURIComponent/decodeURIComponent或者encodeURI/decodeURI,查看关于编码的介绍。

第三方cookie:
通常cookie的域和浏览器地址的域匹配,这被称为第一方cookie。那么第三方cookie就是cookie的域和地址栏中的域不匹配,这种cookie通常被用在第三方广告网站。为了跟踪用户的浏览记录,并且根据收集的用户的浏览习惯,给用户推送相关的广告。

(2)localStorage(本地存储)
HTML5新方法,不过IE8及以上浏览器都兼容。

特点:
1)生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的
2)存储的信息在同一域中是共享;
3)当本页操作(新增、修改、删除)了localStorage的时候,本页面不会触发storage事件,但是别的页面会触发storage事件;
4)大小:据说是5M(跟浏览器厂商有关系);
5)在非IE下的浏览器中可以本地打开,IE浏览器要在服务器中打开;
6)localStorage本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡;
7)localStorage受同源策略的限制。

设置:

localStorage.setItem('username','zxy');

获取:

localStorage.getItem('username');

删除:

localStorage.remove('username');

也可以一次清除所有存储

localStorage.clear();

storage事件:
当storage发生改变的时候触发。
注意:当前页面对storage的操作会触发其他页面的storage事件。

(3)sessionStorage
其实跟localStorage差不多,也是本地存储,会话本地存储。
特点:
用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储。也就是说只要这个浏览器窗口没有关闭,即使刷新页面或进入同源另一页面,数据仍然存在。关闭窗口后,sessionStorage即被销毁,或者在新窗口打开同源的另一个页面,sessionStorage也是没有的。

cookie,localStorage,sessionStorage区别:
相同:
在本地(浏览器端)存储数据。
不同:
1)localStorage只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据;
2)sessionStorage比localStorage更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下;
3)localStorage是永久存储,除非手动删除;
4)sessionStorage当会话结束(当前页面关闭的时候,自动销毁);
5)cookie的数据会在每一次发送http请求的时候,同时发送给服务器而localStorage、sessionStorage不会。

5.BFC(Block formatting contexts)块级格式上下文
w3c规范中的BFC定义:
浮动元素和绝对定位元素,非块级盒子的块级容器(例如 inline-blocks, table-cells, 和 table-captions),以及overflow值不为“visiable”的块级盒子,都会为他们的内容创建新的BFC(块级格式上下文)。
在BFC中,盒子从顶端开始垂直地一个接一个地排列,两个盒子之间的垂直的间隙是由他们的margin 值所决定的。在一个BFC中,两个相邻的块级盒子的垂直外边距会产生折叠。
在BFC中,每一个盒子的左外边缘(margin-left)会触碰到容器的左边缘(border-left)(对于从右到左的格式来说,则触碰到右边缘)。

BFC的通俗理解:
首先BFC是一个名词,是一个独立的布局环境,可以理解为一个箱子(实际上是看不见摸不着的),箱子里面物品的摆放是不受外界的影响的。转换为BFC的理解则是:BFC中的元素的布局是不受外界的影响(我们往往利用这个特性来消除浮动元素对其非浮动的兄弟元素和其子元素带来的影响。)并且在一个BFC中,块盒与行盒(行盒由一行中所有的内联元素所组成)都会垂直的沿着其父元素的边框排列。

特性:
1)内部的Box会在垂直方向,从顶部开始一个接一个地放置;
2)Box垂直方向的距离由margin决定,属于同一个BFC的两个相邻Box的margin会发生叠加;
3)每个元素的margin box的左边,与包含块border box的左边相接触(对于从左往右的格式化,否则相反),即使存在浮动也是如此;
4)BFC的区域不会与float box叠加;
5)BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然;
6)计算BFC的高度时,浮动元素也参与计算。

如何触发BFC:
1)float 除了none以外的值 overflow 除了visible 以外的值(hidden,auto,scroll );
2)display (table-cell,table-caption,inline-block, flex, inline-flex);
3)position值为(absolute,fixed) fieldset元素;
4)fieldset元素。

可以解决的问题:
1)margin叠加的问题,我们将某个元素放到我们新建的BFC里面就可以避免margin叠加;
2)对于左右布局的元素,我们可以给右侧的元素添加overflow:hidden或者auto,左侧的是float:left;
3)可以清除浮动,计算BFC高度,浮动元素不会撑开父元素的高度,我们可以让父元素触发BFC,即使用overflow:hidden 。

6.HTML与XHTML区别
(1)XHTML 元素必须被正确地嵌套;
(2)XHTML 元素必须被关闭,空标签也必须被关闭,如 <br>必须写成<br />
(3)XHTML 标签名必须用小写字母;
(4)XHTML 文档必须拥有根元素;
(5)XHTML 文档要求给所有属性赋一个值;
(6)XHTML 要求所有的属性必须用引号""括起来;
(7)XHTML 文档需要把所有 < 、>、& 等特殊符号用编码表示;
(8)XHTML 文档不要在注释内容中使“–”;
(9)XHTML 图片必须有说明文字;
(10)XHTML 文档中用id属性代替name属性。

7.浮动,清除浮动
浮动的原理:
浮动的框可以左右移动,直至它的外边缘遇到包含框或者另一个浮动框的边缘。浮动框不属于文档中的普通流(文档流),即脱离了文档流,当一个元素浮动之后,不会影响到块级框的布局而只会影响内联框(通常是文本)的排列,文档中的普通流就会表现得和浮动框不存在一样,当浮动框高度超出包含框的时候,也就会出现包含框不会自动伸高来闭合浮动元素(“高度塌陷”现象)。顾名思义,就是漂浮于普通流之上,像浮云一样,但是只能左右浮动。正是因为浮动的这种特性,导致本属于普通流中的元素浮动之后,包含框内部由于不存在其他普通流元素了,也就表现出高度为0(高度塌陷)。在实际布局中,往往这并不是我们所希望的,所以需要闭合浮动元素,使其包含框表现出正常的高度。

浮动元素引起的问题:

  • 父元素的高度无法被撑开,影响与父元素同级的元素;
  • 与浮动元素同级的非浮动元素会跟随其后;
  • 若非第一个元素浮动,则该元素之前的元素也需要浮动,否则会影响页面显示的结构。

清除浮动:
(1)通过设置父元素 overflow 或者display:table或者为父元素也添加float 属性来清除浮动,但这样做会出现一系列其他的问题,不推荐这样做。
(2)通过在浮动元素的末尾添加一个空元素,设置 clear:both属性,after伪元素其实也是通过 content 在元素的后面生成了内容为一个点的块级元素;具体看如下代码:

<div style="clear:both;"></div>

这种方法代优点是码量少,而且易懂,缺点是多了个没有任何语义的空标签,不利于后期维护。
(3)

.clearfix:after {content:"."; display:block; height:0; visibility:hidden; clear:both; } 
        .clearfix { *zoom:1; } //为了适配ie6
  • display:block 使生成的元素以块级元素显示,占满剩余空间;
  • height:0 避免生成内容破坏原有布局的高度;
  • visibility:hidden 使生成的内容不可见,并允许可能被生成内容盖住的内容可以进行点击和交互;
  • 通过 content:".“生成内容作为最后一个元素,至于content里面是点还是其他都是可以的,例如oocss里面就有经典的 content:“XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX”,有些版本可能content 里面内容为空,不推荐这样做的,firefox直到7.0 content:”” 仍然会产生额外的空隙;
  • zoom:1 触发IE hasLayout。
    这个方法利用了after伪类通过 content 在元素的后面生成了内容为一个点的块级元素,优点是结构和语义化完全正确,代码量居中,缺点是复用方式不当会造成代码量增加。

8.元素居中
水平居中:
1)inline-block + text-align

<div class="parent">
    <div class="child">child</div>
</div>
.parent {
    text-align: center;
}
.child {
    display: inline-block;
}

2)table + margin

<div class="parent">
    <div class="child">child</div>
</div>
.child {
    display: table;
    margin: 0 auto;
}

3)absolute + transform

<div class="parent">
    <div class="child">child</div>
</div>
.parent {
    position: relative;
}
.child {
    position: absolute;
    left: 50%;
    top: translateX(-50%);
}

4)flex + justify-content

<div class="parent">
    <div class="child">child</div>
</div>
.parent {
    display: flex;
    justify-content: center;
}

垂直居中:
1)line-height = height (只适用于单行内行内元素)

.parent {
    height: 100px;
}
.child {
    line-height: 100px;
}

2)table-cell + vertical-align(单行,多行都可居中)

.parent {
    display: table-cell;
    vertical-align: middle;
}

3)absolute + transform

.parent {
    position: relative;
}
.child {
    position: absolute;
    top: 50%;
    transfrom: translateY(-50%);
}

4)flex + align-item

.parent {
    display: flex;
    align-items: center;
}

水平垂直居中:
1)inline-block + text-align + table-cell + vertical-align

.parent {
    diaplay: teable-cell;
    text-align: center;
    vertical-align: middle;
}
.child {
    display: inline-block;
}

2)margin: auto

.parent {
    positon: relative;
}
.child {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    bottom: 0;
    margin: auto;
}
  1. transform + translate
.parent {
    positon: relative;
}
.child {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

4)flex

.parent {
    display: flex;
    justify-content: center;
    align-items: center;
}

9.圣杯布局、双飞翼布局、Flex布局和绝对定位布局的几种经典布局
题目要求:针对如下DOM结构,编写CSS,实现三栏水平布局,其中left、right分别位于左右两侧,left宽度为200px,right宽度为300px,main处在中间,宽度自适应。
要求:允许增加额外的DOM节点,但不能修改现有节点顺序。

<div class="container"> 
  <div class="main">main</div> 
  <div class="left">left</div> 
  <div class="right">right</div> 
</div>

圣杯布局与双飞翼布局针对的都是三列左右栏固定中间栏边框自适应的网页布局(想象一下圣杯是主体是加上两个耳朵;鸟儿是身体加上一对翅膀),圣杯布局是Kevin Cornell在2006年提出的一个布局模型概念,在国内最早是由淘宝UED的工程师(传说是玉伯)改进并传播开来,在中国也有叫法是双飞翼布局,它的布局要求有几点:

  • 三列布局,中间宽度自适应,两边定宽;
  • 中间栏要在浏览器中优先展示渲染;
  • 允许任意列的高度最高.

方法一:圣杯布局

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>实现三栏水平布局之圣杯布局</title>
    <style type="text/css">
    .container {
        padding: 0 300px 0 200px;
    }
    .left, .main, .right {
        position: relative;
        min-height: 130px;
        float: left;
    }
    .left {
        left: -200px;
        margin-left: -100%;
        background: green;
        width: 200px;
    }
    .right {
        right: -300px;
        margin-left: -300px;
        background-color: red;
        width: 300px;
    }
    .main {
        background-color: blue;
        width: 100%;
    }
    </style>
</head>
<body>
<div class="container"> 
  <div class="main">main</div> 
  <div class="left">left</div> 
  <div class="right">right</div> 
</div>
</body>
</html>

方法二:双飞翼布局
  圣杯布局和双飞翼布局解决问题的方案在前一半是相同的,也就是三栏全部float浮动,但左右两栏加上负margin让其跟中间栏div并排,以形成三栏布局。不同在于解决 “中间栏div内容不被遮挡”问题的思路不一样。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>实现三栏水平布局之双飞翼布局</title>
    <style type="text/css">
    .left, .main, .right {
        float: left;
        min-height: 130px;
        text-align: center;
    }
    .left {
        margin-left: -100%;
        background: green;
        width: 200px;
    }

    .right {
        margin-left: -300px;
        background-color: red;
        width: 300px;
    }
    .main {
        background-color: blue;
        width: 100%;
    }
    .content{
        margin: 0 300px 0 200px;
    }
    </style>
</head>
<body>
<div class="container"> 
  <div class="main">
      <div class="content">main</div> 
    </div>
  <div class="left">left</div> 
  <div class="right">right</div> 
</div>
</body>
</html>

双飞翼布局比圣杯布局多使用了1个div,少用大致4个css属性(圣杯布局container的 padding-left和padding-right这2个属性,加上左右两个div用相对布局position: relative及对应的right和left共4个属性;而双飞翼布局子div里用margin-left和margin-right共2个属性,比圣杯布局思路更直接和简洁一点。简单说起来就是:双飞翼布局比圣杯布局多创建了一个div,但不用相对布局了。

方法三:Flex布局
  Flex 是 Flexible Box 的缩写,意为”弹性布局”,用来为盒状模型提供最大的灵活性。
  任何一个容器都可以指定为 Flex 布局,所以Flex 布局将成为未来布局的首选方案。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>实现三栏水平布局之Flex布局</title>
    <style type="text/css">
    .container{
        display: flex;
        min-height: 130px;
    }
    .main{
        flex-grow: 1;
        background-color: blue;
    }
    .left{
        order: -1;
        flex-basis: 200px;
        background-color: green;
    }
    .right{
        flex-basis: 300px;
        background-color: red;
    }
    </style>
</head>
<body>
<div class="container"> 
  <div class="main">main</div> 
  <div class="left">left</div> 
  <div class="right">right</div> 
</div>
</body>
</html>

方法四:绝对定位布局
  绝对定位使元素的位置与文档流无关,因此不占据空间。这一点与相对定位不同,相对定位实际上被看作普通流定位模型的一部分,因为元素的位置相对于它在普通流中的位置。
提示:因为绝对定位的框与文档流无关,所以它们可以覆盖页面上的其它元素。可以通过设置 z-index 属性来控制这些框的堆放次序。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>实现三栏水平布局之绝对定位布局</title>
    <style type="text/css">
    .container{
        position: relative;
    }
    .main,.right,.left{
        top: 0;
        height: 130px;
    }
    .main{
        margin: 0 300px 0 200px;
        background-color: blue;
    }
    .right{
        position: absolute;
        width: 300px;
        right: 0;
        background-color: red;
    }
    .left{
        position: absolute;
        width: 200px;
        background-color: green;
        left: 0;
    }
    </style>
</head>
<body>
<div class="container"> 
  <div class="main">main</div> 
  <div class="left">left</div> 
  <div class="right">right</div> 
</div>
</body>
</html>

10.flex布局
简要介绍
在这里插入图片描述
使用flex布局的容器(flex container),它内部的元素自动成为flex项目(flex item)。容器拥有两根隐形的轴,水平的主轴(main axis),和竖直的交叉轴。主轴开始的位置,即主轴与左边框的交点,称为main start;主轴结束的位置称为main end;交叉轴开始的位置称为cross start;交叉轴结束的位置称为cross end。item按主轴或交叉轴排列,item在主轴方向上占据的宽度称为main size,在交叉轴方向上占据的宽度称为cross size。
  此外,需注意使用flex容器内元素,即flex item的float,clear、vertical-align属性将失效。

属性总结表:
下图是关于flex的属性总结表,后面会详细介绍每个属性的意义和用法。
在这里插入图片描述

容器属性详述:
flex-direction:
决定主轴的方向,即项目排列的方向,有四个可能的值:row(默认)|row-reverse|column|column-reverse
    row:主轴为水平方向,项目沿主轴从左至右排列
    column:主轴为竖直方向,项目沿主轴从上至下排列
    row-reverse:主轴水平,项目从右至左排列,与row反向
    column-reverse:主轴竖直,项目从下至上排列,与column反向

在这里插入图片描述
flex-wrap:
默认情况下,item排列在一条线上,即主轴上,flex-wrap决定当排列不下时是否换行以及换行的方式,可能的值nowrap(默认)|wrap|wrap-reverse
    nowrap:自动缩小项目,不换行
    wrap:换行,且第一行在上方
    wrap-reverse:换行,第一行在下面
在这里插入图片描述
flex-flow:
是flex-direction和flex-wrap的简写形式,如:row wrap|column wrap-reverse等。默认值为row nowrap,即横向排列 不换行。
justify-content:
决定item在主轴上的对齐方式,可能的值有flex-start(默认),flex-end,center,space-between,space-around。当主轴沿水平方向时,具体含义为
      flex-start:左对齐
      flex-end:右对齐
      center:居中对齐
      space- between:两端对齐
      space-around:沿轴线均匀分布
    效果如下图
在这里插入图片描述
align-items:
决定了item在交叉轴上的对齐方式,可能的值有flex-start|flex-end|center|baseline|stretch,当主轴水平时,其具体含义为
    flex-start:顶端对齐
    flex-end:底部对齐
    center:竖直方向上居中对齐
    baseline:item第一行文字的底部对齐
    stretch:当item未设置高度时,item将和容器等高对齐
  效果图如下:
在这里插入图片描述
align-content:
该属性定义了当有多根主轴时,即item不止一行时,多行在交叉轴轴上的对齐方式。注意当有多行时,定义了align-content后,align-items属性将失效。align-content可能值含义如下(假设主轴为水平方向):
      flex-start:左对齐
      flex-end:右对齐
      center:居中对齐
      space- between:两端对齐
      space-around:沿轴线均匀分布
      stretch:各行将根据其flex-grow值伸展以充分占据剩余空间
  效果图如下
在这里插入图片描述

flex item属性详述:
item的属性在item的style中设置。item共有如下六种属性
order:
order的值是整数,默认为0,整数越小,item排列越靠前,如下图所示代码如下

<div class="wrap">
    <div class="div" style="order:4"><h2>item 1</h2></div>
    <div class="div" style="order:2"><h2>item 2</h2></div>
    <div class="div" style="order:3"><h2>item 3</h2></div>
    <div class="div" style="order:1"><h2>item 4</h2></div>
</div>

效果图为
在这里插入图片描述
flex-grow:
定义了当flex容器有多余空间时,item是否放大。默认值为0,即当有多余空间时也不放大;可能的值为整数,表示不同item的放大比例,如

<div class="wrap">
    <div class="div" style="flex-grow:1"><h2>item 1</h2></div>
    <div class="div" style="flex-grow:2"><h2>item 2</h2></div>
    <div class="div" style="flex-grow:3"><h2>item 3</h2></div>
</div>

即当有多余空间时item1、item2、和item3以1:2:3的比例放大。
flex-shrink:
定义了当容器空间不足时,item是否缩小。默认值为1,表示当空间不足时,item自动缩小,其可能的值为整数,表示不同item的缩小比例。flex-grow
flex-basis:
表示项目在主轴上占据的空间,默认值为auto。如下代码

<div class="wrap">
    <div class="div" style="flex-basis:80px"><h2>item 1</h2></div>
    <div class="div" style="flex-basis:160px"><h2>item 2</h2></div>
    <div class="div" style="flex-basis:240px"><h2>item 3</h2></div>
</div>

其效果图为
在这里插入图片描述
flex:
flex属性是flex-grow、flex-shrink和flex-basis三属性的简写总和。
align-self:
align-self属性允许item有自己独特的在交叉轴上的对齐方式,它有六个可能的值。默认值为auto
      auto:和父元素align-self的值一致
      flex-start:顶端对齐
      flex-end:底部对齐
      center:竖直方向上居中对齐
      baseline:item第一行文字的底部对齐
      stretch:当item未设置高度时,item将和容器等高对齐
在这里插入图片描述

11.原型与原型链:
构造函数:
构造函数模式的目的就是为了创建一个自定义类,并且创建这个类的实例。构造函数模式中拥有了类和实例的概念,并且实例和实例之间是相互独立的。
构造函数就是一个普通的函数,创建方式和普通函数没有区别,不同的是构造函数习惯上首字母大写。另外就是调用方式的不同,普通函数是直接调用,而构造函数需要使用new关键字来调用。

function Person(name, age, gender) {
  this.name = name
  this.age = age
  this.gender = gender
  this.sayName = function () {
    alert(this.name);
  }
}
var per = new Person("孙悟空", 18, "男");
function Dog(name, age, gender) {
  this.name = name
  this.age = age
  this.gender = gender
}
var dog = new Dog("旺财", 4, "雄")
console.log(per);//当我们直接在页面中打印一个对象时,事件上是输出的对象的toString()方法的返回值
console.log(dog);

在这里插入图片描述
每创建一个Person构造函数,在Person构造函数中,为每一个对象都添加了一个sayName方法,也就是说构造函数每执行一次就会创建一个新的sayName方法。这样就导致了构造函数执行一次就会创建一个新的方法,执行10000次就会创建10000个新的方法,而10000个方法都是一摸一样的,为什么不把这个方法单独放到一个地方,并让所有的实例都可以访问到呢?这就需要原型(prototype)

原型:
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,至于其他方法,则都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。

使用hasOwnPrototype()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从Object继承来的)只在给定属性存在于对象实例中时,才会返回true。

function Person(){
}
Person.prototype.name="Nicholas";
Person.prototype.age=29;
Person.prototype.job="Software engineer";
Person.prototype.sayName=function(){
	alert(this.name);
};

var person1=new Person();
var person2=new Person();

alert(person1.hasOwnPrototype("name"));//false

person1.name="Greg";
alert(person1.name);//"Greg"----来自实例
alert(person1.hasOwnPrototype("name"));//true

alert(person2.name);//"Nicholas"----来自原型
alert(person2.hasOwnPrototype("name"));//false

delete person1.name;
alert(person1.name);//"Nicholas"----来自原型
alert(person1.hasOwnPrototype("name"));//false

由于in操作符只要通过对象能够访问到属性就返回true,hasOwnPrototype()只在属性存在于实例中时才会返回true,因此只要in操作符返回true而hasOwnPrototype()返回false就可以确定属性是原型中的属性。

原型对象的问题:
原型模式省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。
原型中所有属性时被很多实例共享的,这种共享对于函数非常合适,对于那些包含基本值的属性倒也说得过去。毕竟,通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。

function Person(){
}
Person.prototype={
	constructor:Person,
	name:"Nicholas",
	age:29,
	job:"SOftWare Engineer",
	friends:["Shelby","Court"],
	sayName:function(){
		alert(this.name);
	}
};

var person1=new Person();
var person2=new Person();

person1.friends.push("Van");

alert(person1.friends);//"Shelby,Court,Van"
alert(person2.friends);//"Shelby,Court,Van"
alert(person1.friends===person2.friends);//true

在此,Person.prototype对象有一个名为friends的属性,该属性包含一个字符串数组。然后创建了Person的两个实例。接着,修改了person1.friends引用的数组,向数组中添加了一个字符串。由于friends数组存在于Person.prototype而非person1中,所以刚刚提到的修改也会通过person2.friends(与person1.friends指向同一个数组)反映出来。实例一般都是要有属于自己的属性的,而这个问题正是我们很少看到有人单独使用原型模式的原因所在。

原型链:
ES中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如让一个原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
实现原型链有一种基本模式,其代码大致如下:

function SuperType(){
	this.prototype=true;
}
SuperType.prototype.getSuperValue=function(){
	return this.property;
};

function SubType(){
	this.subproperty=false;
}

// 继承了SuperType
SubType.prototype=new SuperType();

SubType.protptype.getSubValue=function(){
	return this.subproperty;
};

var instance=new SubType();
alert(instance.getSuperValue());//true

12.闭包
闭包是指有权访问另一个函数作用域中的变量的函数。

闭包与变量:
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。

function createFunction(){
	var result=new Array();

	for(var i=0;i<10;i++){
		result[i]=function(){
			return i;
		};
	}
	return result;
}

用处:

  • 读取函数内部的变量;
  • 这些变量的值始终保持在内存中,不会在外层函数调用后被自动清除。

优点:

  • 变量长期驻扎在内存中;
  • 避免全局变量的污染;
  • 私有成员的存在 。

特性:

  • 函数套函数;
  • 内部函数可以直接使用外部函数的局部变量或参数;
  • 变量或参数不会被垃圾回收机制回收。

缺点:
常驻内存会增大内存的使用量使用不当会造成内存泄露,详解:
(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

13.JS继承的六种方式
1)构造继承
基本思想:
通过使用call、apply方法可以在新创建的对象上执行构造函数,用父类的构造函数来增加子类的实例。
具体实现:

// 子类
function Sub(){
  Super.call(this)
  this.property = 'Sub Property'
}

优缺点:
优点:
简单明了,直接继承超类构造函数的属性和方法。
缺点:
无法继承原型链上的属性和方法。

2)原型链继承
基本思想:
利用原型链来实现继承,超类的一个实例作为子类的原型。
具体实现:

// 子类
function Sub(){
  this.property = 'Sub Property'
}
Sub.prototype = new Super()
// 注意这里new Super()生成的超类对象并没有constructor属性,故需添加上
Sub.prototype.constructor = Sub

优缺点
优点:

  • 简单明了,容易实现;
  • 实例是子类的实例,实际上也是父类的一个实例;
  • 父类新增原型方法/原型属性,子类都能访问到。

缺点:

  • 所有子类的实例的原型都共享同一个超类实例的属性和方法;
  • 无法实现多继承。

3)组合继承
基本思想:
利用构造继承和原型链组合
具体实现:

// 子类
function Sub(){
  Super.call(this)
  this.property = 'Sub Property'
}
Sub.prototype = new Super()
// 注意这里new Super()生成的超类对象并没有constructor属性,故需添加上
Sub.prototype.constructor = Sub

优缺点:
优点:
解决了构造继承和原型链继承的两个问题.
缺点:
实际上子类上会拥有超类的两份属性,只是子类的属性覆盖了超类的属性.

4)原型式继承
基本思想:
采用原型式继承并不需要定义一个类,传入参数obj,生成一个继承obj对象的对象。
具体实现:

function objectCreate(obj){
  function F(){}
  F.prototype = obj
  return new F()
}

优缺点:
优点:
直接通过对象生成一个继承该对象的对象.
缺点:
不是类式继承,而是原型式基础,缺少了类的概念.

5)寄生式继承
基本思想:
创建一个仅仅用于封装继承过程的函数,然后在内部以某种方式增强对象,最后返回对象。
具体实现:

function objectCreate(obj){
  function F(){}
  F.prototype = obj
  return new F()
}
function createSubObj(superInstance){
  var clone = objectCreate(superInstance)
  clone.property = 'Sub Property'
  return clone
}

优缺点:
优点:
原型式继承的一种拓展。
缺点:
依旧没有类的概念。

6)寄生组合式继承
基本思想:
结合寄生式继承和组合式继承,完美实现不带两份超类属性的继承方式。
具体实现:

function inheritPrototype(Super,Sub){
  var superProtoClone = Object.Create(Super.prototype)
  superProtoClone.constructor = Sub
  Sub.prototype = Super
}
function Sub(){
  Super.call()
  Sub.property = 'Sub Property'
}
inheritPrototype(Super,Sub)

优缺点:
优点:
完美实现继承,解决了组合式继承带两份属性的问题。
缺点:
过于繁琐,故不如组合继承。

14.Vue生命周期
在这里插入图片描述
Vue实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、销毁等一系列过程,我们称这是Vue的生命周期。通俗说就是Vue实例从创建到销毁的过程,就是生命周期。
每一个组件或者实例都会经历一个完整的生命周期,总共分为三个阶段:初始化、运行中、销毁。
1)在beforeCreate和created钩子函数之间的生命周期
在这个生命周期之间进行初始化事件、进行数据的观测。可以看到created的时候数据已经和data属性进行绑定(放在data中的属性当值发生改变时视图也会发生改变)。在beforeCreate的时候千万不要去修改data里面赋值的数据,最早也要放在created里面去做(添加一些行为)。
执行created函数后,这个时候已经可以使用到数据,也可以更改数据,在这里更改数据不会触发updated函数,在这里可以在渲染前倒数第二次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取。
2)在created和beforeMount之间的生命周期
在这一段发生的事情还是比较多的,首先会判断对象是否有el,如果有的话就继续向下编译,否则停止编译。也就意味着停止了生命周期,直到在该vue实例上调用vm.$mount(el)
此时注释掉代码中:el: ‘#app’,然后运行可以看到到created的时候就停止了,如果我们在后面继续调用vm.$mount(el),可以发现代码继续向下执行了。
然后,我们往下看,template参数选项的有无对生命周期的影响。
(1)如果vue实例对象中有template参数选项,则将其作为模板编译成render函数。
(2)如果没有template选项,则将外部HTML作为模板编译。
(3)可以看到template中的模板优先级要高于outer HTML的优先级。
修改代码如下, 在HTML结构中增加了一串html,在vue对象中增加了template选项:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>vue生命周期学习</title>
  <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
</head>
<body>
  <div id="app">
    <!--html中修改的-->
    <h1>{{message + '这是在outer HTML中的'}}</h1>
  </div>
</body>
<script>
  var vm = new Vue({
    el: '#app',
    template: "<h1>{{message +'这是在template中的'}}</h1>", //在vue配置项中修改的
    data: {
      message: 'Vue的生命周期'
    }
</script>
</html>

执行后的结果可以看到在页面中显示的是:
在这里插入图片描述
那么将vue对象中template的选项注释掉后打印如下信息:
在这里插入图片描述
这下就可以想想什么el的判断要在template之前了,是因为vue需要通过el找到对应的outer template。
在vue对象中还有一个render函数,它是以createElement作为参数,然后做渲染操作,而且我们可以直接嵌入JSX。

new Vue({
    el: '#app',
    render: function(createElement) {
        return createElement('h1', 'this is createElement')
    }
})

可以看到页面中渲染的是:
在这里插入图片描述
所以综合排名优先级:
render函数选项 > template选项 > outer HTML.
执行beforeMount钩子函数后,在这个函数中虚拟dom已经创建完成,马上就要渲染,在这里也可以更改数据,不会触发updated,在这里可以在渲染前最后一次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取。
3)beforeMount和mounted 钩子函数间的生命周期
可以看到此时是给vue实例对象添加$el成员,并且替换掉挂载的DOM元素。因为在之前console中打印的结果可以看到beforeMount之前el上还是undefined。
4)mounted
在mounted之前h1中还是通过{{message}}进行占位的,因为此时还没有挂载到页面上,还是JavaScript中的虚拟DOM形式存在的。在mounted之后可以看到h1中的内容发生了变化。
此时,组件已经出现在页面中,数据、真实dom都已经处理好了,事件都已经挂载好了,可以在这里操作真实dom等事情
5)beforeUpdate钩子函数和updated钩子函数间的生命周期
当vue发现data中的数据发生了改变,会触发对应组件的重新渲染,先后调用beforeUpdate和updated钩子函数。我们在console中输入:

vm.message = '触发组件更新'

发现触发了组件的更新。
当组件或实例的数据更改之后,会立即执行beforeUpdate,然后vue的虚拟dom机制会重新构建虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染。当更新完成后,执行updated,数据已经更改完成,dom也重新render完成,可以操作更新后的虚拟dom。
6)beforeDestroy和destroyed钩子函数间的生命周期
beforeDestroy钩子函数在实例销毁之前调用。在这一步,实例仍然完全可用。destroyed钩子函数在Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。一般在这里做一些善后工作,例如清除计时器、清除非指令绑定的事件等等。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <aaa></aaa>
    </div>

    
    <template id="aaa">
        <div>
            <p class="myp">A组件</p>
            <button @click="destroy">destroy</button>
            <input type="text" v-model="msg">
            <p>msg:{{msg}}</p>
        </div>
    </template>



</body>
<script src="./vue.js"></script>

<script>
    //生命周期:初始化阶段 运行中阶段 销毁阶段
    Vue.component("aaa",{
        template:"#aaa",
        data:function(){
            return {msg:'hello'}
        },
        timer:null,
        methods:{
            destroy:function(){
                this.$destroy()//
            }
        },
        beforeCreate:function(){
            console.log('beforeCreate:刚刚new Vue()之后,这个时候,数据还没有挂载呢,只是一个空壳')           
            console.log(this.msg)//undefined
            console.log(document.getElementsByClassName("myp")[0])//undefined
        },
        created:function(){
            console.log('created:这个时候已经可以使用到数据,也可以更改数据,在这里更改数据不会触发updated函数')
            this.msg+='!!!'
            console.log('在这里可以在渲染前倒数第二次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取')
            console.log('接下来开始找实例或者组件对应的模板,编译模板为虚拟dom放入到render函数中准备渲染')
        },
        beforeMount:function(){
            console.log('beforeMount:虚拟dom已经创建完成,马上就要渲染,在这里也可以更改数据,不会触发updated')
            this.msg+='@@@@'
            console.log('在这里可以在渲染前最后一次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取')
            console.log(document.getElementsByClassName("myp")[0])//undefined
            console.log('接下来开始render,渲染出真实dom')
        },
        // render:function(createElement){
        //     console.log('render')
        //     return createElement('div','hahaha')
        // },
        mounted:function(){ 
            console.log('mounted:此时,组件已经出现在页面中,数据、真实dom都已经处理好了,事件都已经挂载好了')
            console.log(document.getElementsByClassName("myp")[0])
            console.log('可以在这里操作真实dom等事情...')

        //    this.$options.timer = setInterval(function () {
        //        console.log('setInterval')
        //         this.msg+='!'  
        //    }.bind(this),500)
        },
        beforeUpdate:function(){
            //这里不能更改数据,否则会陷入死循环
            console.log('beforeUpdate:重新渲染之前触发')
            console.log('然后vue的虚拟dom机制会重新构建虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染')         
        },
        updated:function(){
            //这里不能更改数据,否则会陷入死循环
            console.log('updated:数据已经更改完成,dom也重新render完成')
        },
        beforeDestroy:function(){
            console.log('beforeDestory:销毁前执行($destroy方法被调用的时候就会执行),一般在这里善后:清除计时器、清除非指令绑定的事件等等...')
            // clearInterval(this.$options.timer)
        },
        destroyed:function(){
            console.log('destroyed:组件的数据绑定、监听...都去掉了,只剩下dom空壳,这里也可以善后')
        }
    })


    
    new Vue({
    }).$mount('#app')


</script>
</html>

15.promise
promises 的概念是由 CommonJS 小组的成员在 Promises/A 规范中提出来的。

then()方法介绍:
根据 Promise/A 规范,promise 是一个对象,只需要 then 这一个方法。then 方法带有如下三个参数:

  • 成功回调;
  • 失败回调;
  • 前进回调(规范没有要求包括前进回调的实现,但是很多都实现了)。

一个全新的 promise 对象从每个 then 的调用中返回。

Promise对象状态:
Promise 对象代表一个异步操作,其不受外界影响,有三种状态:

  • Pending(进行中、未完成的);
  • Resolved(已完成,又称 Fulfilled);
  • Rejected(已失败)。

(1)promise 从未完成的状态开始,如果成功它将会是完成态,如果失败将会是失败态。
(2)当一个 promise 移动到完成态,所有注册到它的成功回调将被调用,而且会将成功的结果值传给它。另外,任何注册到 promise 的成功回调,将会在它已经完成以后立即被调用。
(3)同样的,当一个 promise 移动到失败态的时候,它调用的是失败回调而不是成功回调。
(4)对包含前进特性的实现来说,promise 在它离开未完成状态以前的任何时刻,都可以更新它的 progress。当 progress 被更新,所有的前进回调(progress callbacks)会被传递以 progress 的值,并被立即调用。前进回调被以不同于成功和失败回调的方式处理;如果你在一个 progress 更新已经发生以后注册了一个前进回调,新的前进回调只会在它被注册以后被已更新的 progress 调用。
(5)注意:只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

Promise/A规范图解:
在这里插入图片描述
目前支持Promises/A规范的库:

  • Q:可以在NodeJS 以及浏览器上工作,与jQuery兼容,可以通过消息传递远程对象;
  • RSVP.js:一个轻量级的库,它提供了组织异步代码的工具;
  • when.js:体积小巧,使用方便;
  • NodeJS的Promise;
  • jQuery 1.5:据说是基于“CommonJS Promises/A”规范;
  • WinJS / Windows 8 / Metro。

使用promises的优势:

  1. 解决回调地狱(Callback Hell)问题
    (1)有时我们要进行一些相互间有依赖关系的异步操作,比如有多个请求,后一个的请求需要上一次请求的返回结果。过去常规做法只能 callback 层层嵌套,但嵌套层数过多的话就会有 callback hell 问题。比如下面代码,可读性和维护性都很差的。
firstAsync(function(data){
    //处理得到的 data 数据
    //....
    secondAsync(function(data2){
        //处理得到的 data2 数据
        //....
        thirdAsync(function(data3){
              //处理得到的 data3 数据
              //....
        });
    });
});

(2)如果使用 promises 的话,代码就会变得扁平且更可读了。前面提到 then 返回了一个 promise,因此我们可以将 then 的调用不停地串连起来。其中 then 返回的 promise 装载了由调用返回的值。

firstAsync()
.then(function(data){
    //处理得到的 data 数据
    //....
    return secondAsync();
})
.then(function(data2){
    //处理得到的 data2 数据
    //....
    return thirdAsync();
})
.then(function(data3){
    //处理得到的 data3 数据
    //....
});
  1. 更好地进行错误捕获
    多重嵌套 callback 除了会造成上面讲的代码缩进问题,更可怕的是可能会造成无法捕获异常或异常捕获不可控。
    (1)比如下面代码我们使用 setTimeout 模拟异步操作,在其中抛出了个异常。但由于异步回调中,回调函数的执行栈与原函数分离开,导致外部无法抓住异常
function fetch(callback) {
    setTimeout(() => {
        throw Error('请求失败')
    }, 2000)
}
 
try {
    fetch(() => {
        console.log('请求处理') // 永远不会执行
    })
} catch (error) {
    console.log('触发异常', error) // 永远不会执行
}
 
// 程序崩溃
// Uncaught Error: 请求失败

(2)如果使用 promises 的话,通过 reject 方法把 Promise 的状态置为 rejected,这样我们在 then 中就能捕捉到,然后执行“失败”情况的回调。

function fetch(callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
             reject('请求失败');
        }, 2000)
    })
}
 
 
fetch()
.then(
    function(data){
        console.log('请求处理');
        console.log(data);
    },
    function(reason, data){
        console.log('触发异常');
        console.log(reason);
    }
);

当然我们在 catch 方法中处理 reject 回调也是可以的。

function fetch(callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
             reject('请求失败');
        }, 2000)
    })
}
 
 
fetch()
.then(
    function(data){
        console.log('请求处理');
        console.log(data);
    }
)
.catch(function(reason){
    console.log('触发异常');
    console.log(reason);
});

then()方法:
简单来讲,then 方法就是把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。
而 Promise 的优势就在于这个链式调用。我们可以在 then 方法中继续写 Promise 对象并返回,然后继续调用 then 来进行回调操作。

(1)下面通过样例作为演示,我们定义做饭、吃饭、洗碗(cook、eat、wash)这三个方法,它们是层层依赖的关系,下一步的的操作需要使用上一部操作的结果。(这里使用 setTimeout 模拟异步操作)

//做饭
function cook(){
    console.log('开始做饭。');
    var p = new Promise(function(resolve, reject){        //做一些异步操作
        setTimeout(function(){
            console.log('做饭完毕!');
            resolve('鸡蛋炒饭');
        }, 1000);
    });
    return p;
}
 
//吃饭
function eat(data){
    console.log('开始吃饭:' + data);
    var p = new Promise(function(resolve, reject){        //做一些异步操作
        setTimeout(function(){
            console.log('吃饭完毕!');
            resolve('一块碗和一双筷子');
        }, 2000);
    });
    return p;
}
 
function wash(data){
    console.log('开始洗碗:' + data);
    var p = new Promise(function(resolve, reject){        //做一些异步操作
        setTimeout(function(){
            console.log('洗碗完毕!');
            resolve('干净的碗筷');
        }, 2000);
    });
    return p;
}

(2)使用 then 链式调用这三个方法:

cook()
.then(function(data){
    return eat(data);
})
.then(function(data){
    return wash(data);
})
.then(function(data){
    console.log(data);
});

当然上面代码还可以简化成如下:

cook()
.then(eat)
.then(wash)
.then(function(data){
    console.log(data);
});

(3)运行结果如下:
在这里插入图片描述
reject()方法:
上面样例我们通过 resolve 方法把 Promise 的状态置为完成态(Resolved),这时 then 方法就能捕捉到变化,并执行“成功”情况的回调。
而 reject 方法就是把 Promise 的状态置为已失败(Rejected),这时 then 方法执行“失败”情况的回调(then 方法的第二参数)。

(1)下面同样使用一个样例做演示

//做饭
function cook(){
    console.log('开始做饭。');
    var p = new Promise(function(resolve, reject){        //做一些异步操作
        setTimeout(function(){
            console.log('做饭失败!');
            reject('烧焦的米饭');
        }, 1000);
    });
    return p;
}
 
//吃饭
function eat(data){
    console.log('开始吃饭:' + data);
    var p = new Promise(function(resolve, reject){        //做一些异步操作
        setTimeout(function(){
            console.log('吃饭完毕!');
            resolve('一块碗和一双筷子');
        }, 2000);
    });
    return p;
}
 
cook()
.then(eat, function(data){
  console.log(data + '没法吃!');
})

在这里插入图片描述
(2)如果我们只要处理失败的情况可以使用 then(null, …),或是使用接下来要讲的 catch 方法。

cook()
.then(null, function(data){
  console.log(data + '没法吃!');
})

catch()方法:
(1)它可以和 then 的第二个参数一样,用来指定 reject 的回调

cook()
.then(eat)
.catch(function(data){
    console.log(data + '没法吃!');
});

(2)它的另一个作用是,当执行 resolve 的回调(也就是上面 then 中的第一个参数)时,如果抛出异常了(代码出错了),那么也不会报错卡死 js,而是会进到这个 catch 方法中。

//做饭
function cook(){
    console.log('开始做饭。');
    var p = new Promise(function(resolve, reject){        //做一些异步操作
        setTimeout(function(){
            console.log('做饭完毕!');
            resolve('鸡蛋炒饭');
        }, 1000);
    });
    return p;
}
 
//吃饭
function eat(data){
    console.log('开始吃饭:' + data);
    var p = new Promise(function(resolve, reject){        //做一些异步操作
        setTimeout(function(){
            console.log('吃饭完毕!');
            resolve('一块碗和一双筷子');
        }, 2000);
    });
    return p;
}
 
cook()
.then(function(data){
    throw new Error('米饭被打翻了!');
    eat(data);
})
.catch(function(data){
    console.log(data);
});

运行结果如下:
在这里插入图片描述
这种错误的捕获是非常有用的,因为它能够帮助我们在开发中识别代码错误。比如,在一个 then() 方法内部的任意地方,我们做了一个 JSON.parse() 操作,如果 JSON 参数不合法那么它就会抛出一个同步错误。用回调的话该错误就会被吞噬掉,但是用 promises 我们可以轻松的在 catch() 方法里处理掉该错误。
(3)还可以添加多个 catch,实现更加精准的异常捕获。

somePromise.then(function() {
 return a();
}).catch(TypeError, function(e) {
 //If a is defined, will end up here because
 //it is a type error to reference property of undefined
}).catch(ReferenceError, function(e) {
 //Will end up here if a wasn't defined at all
}).catch(function(e) {
 //Generic catch-the rest, error wasn't TypeError nor
 //ReferenceError
});

all()方法:
Promise 的 all 方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调

(1)比如下面代码,两个异步操作是并行执行的,等到它们都执行完后才会进到 then 里面。同时 all 会把所有异步操作的结果放进一个数组中传给 then。

//切菜
function cutUp(){
    console.log('开始切菜。');
    var p = new Promise(function(resolve, reject){        //做一些异步操作
        setTimeout(function(){
            console.log('切菜完毕!');
            resolve('切好的菜');
        }, 1000);
    });
    return p;
}
 
//烧水
function boil(){
    console.log('开始烧水。');
    var p = new Promise(function(resolve, reject){        //做一些异步操作
        setTimeout(function(){
            console.log('烧水完毕!');
            resolve('烧好的水');
        }, 1000);
    });
    return p;
}
 
Promise
.all([cutUp(), boil()])
.then(function(results){
    console.log("准备工作完毕:");
    console.log(results);
});

(2)运行结果如下:
在这里插入图片描述
race()方法:
race 按字面解释,就是赛跑的意思。race 的用法与 all 一样,只不过 all 是等所有异步操作都执行完毕后才执行 then 回调。而race 的话只要有一个异步操作执行完毕,就立刻执行 then 回调
注意:其它没有执行完毕的异步操作仍然会继续执行,而不是停止。

(1)这里我们将上面样例的 all 改成 race

Promise
.race([cutUp(), boil()])
.then(function(results){
    console.log("准备工作完毕:");
    console.log(results);
});

在这里插入图片描述
(2)race 使用场景很多。比如我们可以用 race 给某个异步请求设置超时时间,并且在超时后执行相应的操作。

//请求某个图片资源
function requestImg(){
    var p = new Promise(function(resolve, reject){
    var img = new Image();
    img.onload = function(){
       resolve(img);
    }
    img.src = 'xxxxxx';
    });
    return p;
}
 
//延时函数,用于给请求计时
function timeout(){
    var p = new Promise(function(resolve, reject){
        setTimeout(function(){
            reject('图片请求超时');
        }, 5000);
    });
    return p;
}
 
Promise
.race([requestImg(), timeout()])
.then(function(results){
    console.log(results);
})
.catch(function(reason){
    console.log(reason);
});

上面代码 requestImg 函数异步请求一张图片,timeout 函数是一个延时 5 秒的异步操作。我们将它们一起放在 race 中赛跑。

  • 如果 5 秒内图片请求成功那么便进入 then 方法,执行正常的流程;
  • 如果 5 秒钟图片还未成功返回,那么则进入 catch,报“图片请求超时”的信息。
    在这里插入图片描述

Deferred对象及其方法:

  1. $.Deferred

jQuery 用 $.Deferred 实现了 Promise 规范;
$.Deferred() 返回一个对象,我们可以称之为 Deferred 对象,上面挂着一些熟悉的方法如:done、fail、then 等;
jQuery 就是用这个 Deferred 对象来注册异步操作的回调函数,修改并传递异步操作的状态。

下面我们定义做饭、吃饭、洗碗(cook、eat、wash)这三个方法(这里使用 setTimeout 模拟异步操作)

//做饭
function cook(){
    console.log('开始做饭。');
    var def = $.Deferred();
    //执行异步操作
    setTimeout(function(){
        console.log('做饭完毕!');
        def.resolve('鸡蛋炒饭');
    }, 1000);
    return def.promise();
}
 
//吃饭
function eat(data){
    console.log('开始吃饭:' + data);
    var def = $.Deferred();
    //执行异步操作
    setTimeout(function(){
        console.log('吃饭完毕!');
        def.resolve('一块碗和一双筷子');
    }, 1000);
    return def.promise();
}
 
//洗碗
function wash(data){
    console.log('开始洗碗:' + data);
    var def = $.Deferred();
    //执行异步操作
    setTimeout(function(){
        console.log('洗碗完毕!');
        def.resolve('干净的碗筷');
    }, 1000);
    return def.promise();
}
  1. then()方法
    通过 Deferred 对象的 then 方法我们可以实现链式调用。
    (1)比如上面样例的三个方法是层层依赖的关系,且下一步的的操作需要使用上一部操作的结果。我们可以这么写:
cook()
.then(function(data){
    return eat(data);
})
.then(function(data){
    return wash(data);
})
.then(function(data){
    console.log(data);
});

当然也可以简写成如下:

cook()
.then(eat)
.then(wash)
.then(function(data){
    console.log(data);
});

(2)运行结果如下:
在这里插入图片描述
2. reject()方法
上面样例我们通过 resolve 方法把 Deferred 对象的状态置为完成态(Resolved),这时 then 方法就能捕捉到变化,并执行“成功”情况的回调。
而 reject 方法就是把 Deferred 对象的状态置为已失败(Rejected),这时 then 方法执行“失败”情况的回调(then 方法的第二参数)。
(1)下面同样使用一个样例做演示

//做饭
function cook(){
    console.log('开始做饭。');
    var def = $.Deferred();
    //执行异步操作
    setTimeout(function(){
        console.log('做饭完毕!');
        def.reject('烧焦的米饭');
    }, 1000);
    return def.promise();
}
 
//吃饭
function eat(data){
    console.log('开始吃饭:' + data);
    var def = $.Deferred();
    //执行异步操作
    setTimeout(function(){
        console.log('吃饭完毕!');
        def.resolve('一块碗和一双筷子');
    }, 1000);
    return def.promise();
}
 
cook()
.then(eat, function(data){
  console.log(data + '没法吃!');
})

运行结果如下:
在这里插入图片描述
(2)Promise 规范中,then 方法接受两个参数,分别是执行完成和执行失败的回调。而 jQuery 中进行了增强,还可以接受第三个参数,就是在 pending(进行中)状态时的回调。

deferred.then( doneFilter [, failFilter ] [, progressFilter ] )
  1. done()与fail()方法
    done 和 fail 是 jQuery 增加的两个语法糖方法。分别用来指定执行完成和执行失败的回调。
    比如下面两段代码是等价的:
//then方法
d.then(function(){
    console.log('执行完成');
}, function(){
    console.log('执行失败');
});
 
//done方法、fail方法
d.done(function(){
    console.log('执行完成');
})
.fail(function(){
    console.log('执行失败');
});
  1. always()方法
    jQuery 的 Deferred 对象上还有一个 always 方法,不论执行完成还是执行失败,always 都会执行,有点类似 ajax 中的 complete。
cook()
.then(eat)
.then(wash)
.always(function(){
  console.log('上班去!');
})

与Promises/A规范的差异:
在开头讲到,目前 Promise 事实上的标准是社区提出的 Promises/A 规范,jQuery 的实现并不完全符合 Promises/A,主要表现在对错误的处理不同。

  1. ES6中对错误的处理
    下面代码我们在回调函数中抛出一个错误,Promises/A 规定此时 Promise 实例的状态变为 reject,同时该错误会被下一个 catch 方法指定的回调函数捕获。
cook()
.then(function(data){
    throw new Error('米饭被打翻了!');
    eat(data);
})
.catch(function(data){
    console.log(data);
});
  1. jQuery中对错误的处理
    同样我们在回调函数中抛出一个错误,jQuery 的 Deferred 对象此时不会改变状态,亦不会触发回调函数,该错误一般情况下会被 window.onerror 捕获。换句话说,在 Deferred 对象中,总是必须使用 reject 方法来改变状态。
cook()
.then(function(data){
    throw new Error('米饭被打翻了!');
    eat(data);
})
 
window.onerror = function(msg, url, line) {
    console.log("发生错误了:" + msg);
    return true; //如果注释掉该语句,浏览器中还是会有错误提示,反之则没有。
}

在这里插入图片描述

$.when方法:
jQuery 中,还有一个 $.when 方法。它与 ES6 中的 all 方法功能一样,并行执行异步操作,在所有的异步操作执行完后才执行回调函数。当有两个地方要注意:
$.when 并没有定义在 . D e f e r r e d 中 , 看 名 字 就 知 道 , .Deferred 中,看名字就知道, .Deferred.when 它是一个单独的方法。
$.when 与 ES6 的 all 的参数稍有区别,它接受的并不是数组,而是多个 Deferred 对象。

(1)比如下面代码,两个异步操作是并行执行的,等到它们都执行完后才会进到 then 里面。同时 all 会把所有异步操作的结果传给 then。

//切菜
function cutUp(){
    console.log('开始切菜。');
    var def = $.Deferred();
    //执行异步操作
    setTimeout(function(){
        console.log('切菜完毕!');
        def.resolve('切好的菜');
    }, 1000);
    return def.promise();
}
 
//烧水
function boil(){
    console.log('开始烧水。');
    var def = $.Deferred();
    //执行异步操作
    setTimeout(function(){
        console.log('烧水完毕!');
        def.resolve('烧好的水');
    }, 1000);
    return def.promise();
}
 
$.when(cutUp(), boil())
.then(function(data1, data2){
    console.log("准备工作完毕:");
    console.log(data1, data2);
});

Ajax函数与Deferred的关系:
jQuery 中我们常常会用到的 ajax, get, post 等 Ajax 函数,其实它们内部都已经实现了 Deferred。这些方法调用后会返回一个受限的 Deferred 对象。既然是 Deferred 对象,那么自然也有上面提到的所有特性。

  1. then方法
    比如我们通过链式调用,连续发送多个请求。
req1 = function(){
    return $.ajax(/*...*/);
}
req2 = function(){
    return $.ajax(/*...*/);
}
req3 = function(){
    return $.ajax(/*...*/);
}
 
req1().then(req2).then(req3).done(function(){
    console.log('请求发送完毕');
});
  1. success、error与complete方法
    success、error、complete是 ajax 提供的语法糖,功能与 Deferred 对象的 done、fail、always 一致。比如下面两段代码功能是一致的:
//使用success、error、complete
$.ajax(/*...*/)
.success(function(){/*...*/})
.error(function(){/*...*/})
.complete(function(){/*...*/})
 
//使用done、fail、always
$.ajax(/*...*/)
.done(function(){/*...*/})
.fai(function(){/*...*/})
.always(function(){/*...*/})

16.前端的安全问题
XSS(Cross-Site Scripting)跨站脚本攻击:
xss攻击又叫做跨站脚本攻击,主要是用户输入或通过其他方式,向我们的代码中注入了一下其他的js,而我们又没有做任何防范,去执行了这段js。

可能用户会写一个死循环,将我们的页面给弄崩了,但是也有可能通过这种方式,来获取我们的cookie,从而回去登陆态等信息。

xss攻击从来源可分为反射型和存储型
反射型:
将xss代码通过url来注入
index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>反射型</title>
</head>
<body>
<div id="test"></div>

<script>
    var $test = document.querySelector('#test');;
    $test.innerHTML = window.location.hash
</script>
</body>
</html>

在IE浏览器去访问该页面并在地址后面加上index.html#<img src="404.html" onerror="alert('xss')" />

这时页面一打开就会有个xss的弹窗,这就是最简单的反射型攻击
当然可能会觉得这样没有任何作用,但是修改一下代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>反射型</title>
</head>
<body>
<div id="test"></div>

<script>
  // 先向页面的cookie存储一个name=1的信息
    document.cookie = "name=1"
    var $test = document.querySelector('#test');;
    $test.innerHTML = window.location.hash
</script>
</body>
</html>

此时打开的地址修改为index.html#<img src="404.html" onerror="alert(document.cookie)" />
这里就会发现弹窗内容为我们存取的cookie。
注意:1.这里必须用IE打开这个链接,因为chrome和safari等浏览器,会主动将url里的一下字符串进行encode,保证了一定的安全性。 2.为什么我们这里用img的onerror来注入脚本呢?而不是直接用script标签来执行,我们修改一下访问的地址index.html#<script>alert(document.cookie)</script>,这时会发现,页面并没有执行这段代码,但是这段代码已经注入到了#test标签中了。所以,一般通过img的onerror来注入是最有效的方法

存储型
将xss代码发送到了服务器,在前端请求数据时,将xss代码发送给了前端。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>存储型</title>
</head>
<body>
<div id="test"></div>

<script>
  // 先向页面的cookie存储一个name=1的信息
    document.cookie = "name=1"
    // 这里假设是请求了后台的接口 response是我们请求回来的数据
    var response = '<img src="404.html" οnerrοr="alert(document.cookie)"'

    var $test = document.querySelector('#test');;
    $test.innerHTML = response
</script>
</body>
</html>

这里最常见的情况就是一个富文本编辑器下,由用户输入了一串xss代码,存储在了服务器中,我们在展示用户输入内容时,没有做防范处理。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>富文本</title>
</head>
<body>
  <div id="test"></div>
  <textarea name="" id="" cols="30" rows="10"></textarea>
  <button onclick="submit()">提交</button>
</body>
</html>
<script>
function submit() {
  var $test = document.querySelector('#test');
    $test.innerHTML = document.querySelector('textarea').value
}
</script>

xss的防范手段:
(1)encode
encode也分为html的encode和js的encode
html的encode: 就是将一些有特殊意义的字符串进行替换,比如:

& => &amp;

" => &quot;

' => &#39;

< => &lt;

> => &gt;

js的encode: 使用“\”对特殊字符进行转义,除数字字母之外,小于127的字符编码使用16进制“\xHH”的方式进行编码,大于用unicode(非常严格模式)。

(2)对于富文本的防范:filter
因为富文本是比较特殊的,在富文本中输入标签,我们需要展示出来,所以我们不能用之前的html的encode方法来执行。所以我们就得用一个叫白名单过滤的方式来防范。

原理就是:首先列举一下比较合法的标签,称为白名单,这些标签是不会对页面进行攻击的。之后对用户输入的内容进行白名单过滤。

其他的防御措施,例如设置CSP HTTP Header、输入验证、开启浏览器XSS防御等等都是可选项,原因在于这些措施都存在被绕过的可能,并不能完全保证能防御XSS攻击。不过它们和输出编码却可以共同协作实施纵深防御策略。

CSRF(Cross-sit request forgery)漏洞(跨站伪造请求):
CSRF就是利用你所在网站的登录的状态,悄悄提交各种信息, 是一种比xss还要恶劣很多的攻击。因为CSRF可以在我们不知情的情况下,利用我们登陆的账号信息,去模拟我们的行为,去执行一下操作,也就是所谓的钓鱼。比如我们在登陆某个论坛,但这个网站是个钓鱼网站,我们利用邮箱或者qq登陆后,它就可以拿到我们的登陆态,session和cookie信息。然后利用这些信息去模拟一个另外网站的请求,比如转账的请求。

防范措施:
(1)提交 method=Post 判断referer
HTTP请求中有一个referer的报文头,用来指明当前流量的来源参考页。如果我们用post就可以将页面的referer带入,从而进行判断请求的来源是不是安全的网站。但是referer在本地起的服务中是没有的,直接请求页面也不会有。这就是为什么我们要用Post请求方式。直接请求页面,因为post请求是肯定会带入referer,但get有可能不会带referer。

(2)利用Token
Token简单来说就是由后端生成的一个唯一的登陆态,并传给前端保存在前端,每次前端请求时都会携带着Token,后端会先去解析这个Token,看看是不是后台给我们的,已经是否登陆超时,如果校验通过了,才会同意接口请求

iframe安全隐患问题:
有时候前端页面为了显示别人的网站或者一些组件的时候,就用iframe来引入进来,比如嵌入一些广告等等。但是有些iframe安全性我们无法去评估测试,有时候会携带一些第三方的插件啊,或者嵌入了一下不安全的脚本啊,这些都是值得我们去考虑的。

防范措施:
(1)使用安全的网站进行嵌入;
(2)在iframe添加一个叫sandbox的属性,浏览器会对iframe内容进行严格的控制,详细了解可以看看相关的API接口文档。

点击劫持:
有个词叫做防不胜防,我们在通过 iframe 使用别人提供的内容时,我们自己的页面也可能正在被不法分子放到他们精心构造的iframe或者frame当中,进行点击劫持攻击。

这是一种欺骗性比较强,同时也需要用户高度参与才能完成的一种攻击。通常的攻击步骤是这样的:

1、攻击者精心构造一个诱导用户点击的内容,比如Web页面小游戏
2、将我们的页面放入到iframe当中
3、利用z-index等CSS样式将这个iframe叠加到小游戏的垂直方向的正上方
4、把iframe设置为100%透明度
5、受害者访问到这个页面后,肉眼看到的是一个小游戏,如果受到诱导进行了点击的话,实际上点击到的却是iframe中的我们的页面

点击劫持的危害在于,攻击利用了受害者的用户身份,在其不知情的情况下进行一些操作。如果只是迫使用户关注某个微博账号的话,看上去仿佛还可以承受,但是如果是删除某个重要文件记录,或者窃取敏感信息,那么造成的危害可就难以承受了。

防御措施:
有多种防御措施都可以防止页面遭到点击劫持攻击,例如Frame Breaking方案。一个推荐的防御方案是,使用X-Frame-Options:DENY这个HTTP Header来明确的告知浏览器,不要把当前HTTP响应中的内容在HTML Frame中显示出来。

错误的内容:
想象这样一个攻击场景:某网站允许用户在评论里上传图片,攻击者在上传图片的时候,看似提交的是个图片文件,实则是个含有JavaScript的脚本文件。该文件逃过了文件类型校验(这涉及到了恶意文件上传这个常见安全问题,但是由于和前端相关度不高因此暂不详细介绍),在服务器里存储了下来。接下来,受害者在访问这段评论的时候,浏览器会去请求这个伪装成图片的JavaScript脚本,而此时如果浏览器错误的推断了这个响应的内容类型(MIME types),那么就会把这个图片文件当做JavaScript脚本执行,于是攻击也就成功了。

问题的关键就在于,后端服务器在返回的响应中设置的Content-Type Header仅仅只是给浏览器提供当前响应内容类型的建议,而浏览器有可能会自作主张的根据响应中的实际内容去推断内容的类型。

在上面的例子中,后端通过Content-Type Header建议浏览器按照图片来渲染这次的HTTP响应,但是浏览器发现响应中其实是JavaScript,于是就擅自做主把这段响应当做JS脚本来解释执行,安全问题也就产生了。

防御措施:
浏览器根据响应内容来推断其类型,本来这是个很“智能”的功能,是浏览器强大的容错能力的体现,但是却会带来安全风险。要避免出现这样的安全问题,办法就是通过设置X-Content-Type-Options这个HTTP Header明确禁止浏览器去推断响应类型。

同样是上面的攻击场景,后端服务器返回的Content-Type建议浏览器按照图片进行内容渲染,浏览器发现有X-Content-Type-OptionsHTTP Header的存在,并且其参数值是nosniff,因此不会再去推断内容类型,而是强制按照图片进行渲染,那么因为实际上这是一段JS脚本而非真实的图片,因此这段脚本就会被浏览器当作是一个已经损坏或者格式不正确的图片来处理,而不是当作JS脚本来处理,从而最终防止了安全问题的发生。

本地存储数据问题:
很多开发者为了方便,把一些个人信息不经加密直接存到本地或者cookie,这样是非常不安全的,黑客们可以很容易就拿到用户的信息,所有在放到cookie中的信息或者localStorage里的信息要进行加密,加密可以自己定义一些加密方法或者网上寻找一些加密的插件,或者用base64进行多次加密然后再多次解码,这样就比较安全了。

第三方依赖安全隐患:
现如今的项目开发,很多都喜欢用别人写好的框架,为了方便快捷,很快的就搭建起项目,自己写的代码不到20%,过多的用第三方依赖或者插件,一方面会影响性能问题,另一方面第三方的依赖或者插件存在很多安全性问题,也会存在这样那样的漏洞,所以使用起来得谨慎。

防范措施:
手动去检查那些依赖的安全性问题基本是不可能的,最好是利用一些自动化的工具进行扫描过后再用,比如NSP(Node Security Platform),Snyk等等。

Https 也可能存在的风险:
为了保护信息在传输过程中不被泄露,保证传输安全,使用TLS或者通俗的讲使用HTTPS已经是当今的标准配置了。然而事情并没有这么简单,即使是服务器端开启了HTTPS,也还是存在安全隐患,黑客可以利用SSL Stripping这种攻击手段,强制让HTTPS降级回HTTP,从而继续进行中间人攻击。

问题的本质在于浏览器发出去的第一次请求就被攻击者拦截了下来并做了修改,根本不给浏览器和服务器进行HTTPS通信的机会。大致过程如下,用户在浏览器里输入URL的时候往往不是从https://开始的,而是直接从域名开始输入,随后浏览器向服务器发起HTTP通信,然而由于攻击者的存在,它把服务器端返回的跳转到HTTPS页面的响应拦截了,并且代替客户端和服务器端进行后续的通信。由于这一切都是暗中进行的,所以使用前端应用的用户对此毫无察觉。

解决这个安全问题的办法是使用HSTS(HTTP Strict Transport Security),它通过下面这个HTTP Header以及一个预加载的清单,来告知浏览器在和网站进行通信的时候强制性的使用HTTPS,而不是通过明文的HTTP进行通信:

Strict-Transport-Security: max-age=; includeSubDomains; preload

这里的“强制性”表现为浏览器无论在何种情况下都直接向服务器端发起HTTPS请求,而不再像以往那样从HTTP跳转到HTTPS。另外,当遇到证书或者链接不安全的时候,则首先警告用户,并且不再让用户选择是否继续进行不安全的通信。

缺失静态资源完整性校验:
出于性能考虑,前端应用通常会把一些静态资源存放到CDN(Content Delivery Networks)上面,例如Javascript脚本和Stylesheet文件。这么做可以显著提高前端应用的访问速度,但与此同时却也隐含了一个新的安全风险。

如果攻击者劫持了CDN,或者对CDN中的资源进行了污染,那么我们的前端应用拿到的就是有问题的JS脚本或者Stylesheet文件,使得攻击者可以肆意篡改我们的前端页面,对用户实施攻击。这种攻击方式造成的效果和XSS跨站脚本攻击有些相似,不过不同点在于攻击者是从CDN开始实施的攻击,而传统的XSS攻击则是从有用户输入的地方开始下手的。

防御这种攻击的办法是使用浏览器提供的SRI(Subresource Integrity)功能。顾名思义,这里的Subresource指的就是HTML页面中通过

每个资源文件都可以有一个SRI值,就像下面这样。它由两部分组成,减号(-)左侧是生成SRI值用到的哈希算法名,右侧是经过Base64编码后的该资源文件的Hash值。

<script src=“https://example.js” integrity=“sha384-eivAQsRgJIi2KsTdSnfoEGIRTo25NCAqjNJNZalV63WKX3Y51adIzLT4So1pk5tX”></script>

浏览器在处理这个script元素的时候,就会检查对应的JS脚本文件的完整性,看其是否和script元素中integrity属性指定的SRI值一致,如果不匹配,浏览器则会中止对这个JS脚本的处理。

17.vue底层实现原理
MVVM 是Model - View - ViewModel 的缩写。
Model 代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑;
View 代表UI组件,它负责将数据模型转化成UI 展现出来。
ViewModel 监听模型数据的改变和控制视图行为、处理用户交互、简单理解就是一个同步View和Model的对象,连接Model和View。
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,model 和 ViewModel 之间的交互是双向的,因此View 数据的变化会同步到Model 中,而Model数据的变化也会立即反应到View上。
ViewModel通过双向数据绑定把View 层和model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM,不需要关注数据状态的同步问题,复杂的数据状态维护完全由MVVM 来统一管理。
mvvm双向绑定

<div id="mvvm-app">
    <input type="text" v-model="word">
    <p>{{word}}</p>
    <button v-on:click="sayHi">change model</button>
</div>
 
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/mvvm.js"></script>
<script>
    var vm = new MVVM({
        el: '#mvvm-app',
        data: {
            word: 'Hello World!'
        },
        methods: {
            sayHi: function() {
                this.word = 'Hi, everybody!';
            }
        }
    });
</script>

几种实现双向绑定的做法:
实现数据绑定的做法有大致如下几种:

发布者-订阅者模式(backbone.js)
脏值检查(angular.js) 
数据劫持(vue.js)

发布者-订阅者模式:
一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set('property', value)

脏值检查:
angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:

  • DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
  • XHR响应事件 ( $http )
  • 浏览器Location变更事件 ( $location )
  • Timer事件( $timeout , $interval )
  • 执行 $digest() 或 $apply()

数据劫持:
vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

要实现mvvm的双向绑定,就必须要实现以下几点:

  • 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者;
  • 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数;
  • 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图;
  • mvvm入口函数,整合以上三者。

实现Observer:
将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化

var data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; // 哈哈哈,监听到值变化了 kindeng --> dmq
 
function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    // 取出所有属性遍历
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};
 
function defineReactive(data, key, val) {
    observe(val); // 监听子属性
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚举
        configurable: false, // 不能再define
        get: function() {
            return val;
        },
        set: function(newVal) {
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
        }
    });
}

这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法,代码改善之后是这样:

// ... 省略
function defineReactive(data, key, val) {
    var dep = new Dep();
    observe(val); // 监听子属性
 
    Object.defineProperty(data, key, {
        // ... 省略
        set: function(newVal) {
            if (val === newVal) return;
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
            dep.notify(); // 通知所有订阅者
        }
    });
}
 
function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

那么问题来了,谁是订阅者?怎么往订阅器添加订阅者?
没错,上面的思路整理中我们已经明确订阅者应该是Watcher, 而且var dep = new Dep();是在 defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以在 getter里面动手脚:

// Observer.js
// ...省略
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // ... 省略
});
 
// Watcher.js
Watcher.prototype = {
    get: function(key) {
        Dep.target = this;
        this.value = data[key];    // 这里会触发属性的getter,从而添加订阅者
        Dep.target = null;
    }
}

这里已经实现了一个Observer了,已经具备了监听数据和数据变化通知订阅者的功能。

实现Compile:
compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。

因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中。

function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}
Compile.prototype = {
    init: function() { this.compileElement(this.$fragment); },
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(), child;
        // 将原生节点拷贝到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    }
};

compileElement方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,详看代码及注释说明:

Compile.prototype = {
    // ... 省略
    compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;    // 表达式文本
            // 按元素节点方式编译
            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1);
            }
            // 遍历编译子节点
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },
    compile: function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 规定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令为 v-text
            var attrName = attr.name;    // v-text
            if (me.isDirective(attrName)) {
                var exp = attr.value; // content
                var dir = attrName.substring(2);    // text
                if (me.isEventDirective(dir)) {
                    // 事件指令, 如 v-on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    // 普通指令
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
            }
        });
    }
};
 
// 指令处理集合
var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    // ...省略
    bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        // 第一次初始化视图
        updaterFn && updaterFn(node, vm[exp]);
        // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
        new Watcher(vm, exp, function(value, oldValue) {
            // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
            updaterFn && updaterFn(node, value, oldValue);
        });
    }
};
 
// 更新函数
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    }
    // ...省略
};

这里通过递归遍历保证了每个节点及子节点都会解析编译到,包括了{{}}表达式声明的文本节点。指令的声明规定是通过特定前缀的节点属性来标记,如<span v-text=“content” other-attr中v-text便是指令,而other-attr不是指令,只是普通的属性。
监听数据、绑定更新函数的处理是在compileUtil.bind()这个方法中,通过new Watcher()添加回调来接收数据变化的通知

至此,一个简单的Compile就完成了。

实现Watcher:
Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。

function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    // 此处为了触发属性的getter,从而在dep添加自己,结合Observer更易理解
    this.value = this.get(); 
}
Watcher.prototype = {
    update: function() {
        this.run();    // 属性值变化收到通知
    },
    run: function() {
        var value = this.get(); // 取到最新值
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
        }
    },
    get: function() {
        Dep.target = this;    // 将当前订阅者指向自己
        var value = this.vm[exp];    // 触发getter,添加自己到属性订阅器中
        Dep.target = null;    // 添加完毕,重置
        return value;
    }
};
// 这里再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以可以在Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // ... 省略
});
Dep.prototype = {
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update(); // 调用订阅者的update方法,通知变化
        });
    }
};

实例化Watcher的时候,调用get()方法,通过Dep.target = watcherInstance标记订阅者是当前watcher实例,强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。

实现MVVM:
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data;
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

但是这里有个问题,从代码中可看出监听的数据对象是options.data,每次需要更新视图,则必须通过var vm = new MVVM({data:{name: ‘kindeng’}}); vm._data.name = ‘dmq’; 这样的方式来改变数据。

显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的:
var vm = new MVVM({data: {name: ‘kindeng’}}); vm.name = ‘dmq’;

所以这里需要给MVVM实例添加一个属性代理的方法,使访问vm的属性代理为访问vm._data的属性,改造后的代码如下:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data, me = this;
    // 属性代理,实现 vm.xxx -> vm._data.xxx
    Object.keys(data).forEach(function(key) {
        me._proxy(key);
    });
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}
 
MVVM.prototype = {
    _proxy: function(key) {
        var me = this;
        Object.defineProperty(me, key, {
            configurable: false,
            enumerable: true,
            get: function proxyGetter() {
                return me._data[key];
            },
            set: function proxySetter(newVal) {
                me._data[key] = newVal;
            }
        });
    }
};

这里主要还是利用了Object.defineProperty()这个方法来劫持了vm实例对象的属性的读写权,使读写vm实例的属性转成读写了vm._data的属性值,达到鱼目混珠的效果。

参考:Vue底层实现原理总结

18.Vue 组件间的参数传递
(1)父组件与子组件传值
父组件传给子组件: 子组件通过props方法接受数据;
子组件传给父组件:$emit方法传递参数;
(2)非父子组件间的数据传递,兄弟组件传值
EventBus,就是创建一个事件中心,相当于中转站,可以用它来传递事件和接受事件,项目比较小时,用这个比较合适;
VueX,创建一个数据仓库,整个项目全局都可以往这个仓库存放数据和读取数据。

19.vuex
vuex是一个专为vue.js应用程序开发的状态管理模式(它采用集中式存贮管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化)。
在这里插入图片描述
vuex五大核心属性:state,getter,mutation,action,module

  • state:存储数据,存储状态;在根实例中注册了store 后,用 this.$store.state 来访问;对应vue里面的data;存放数据方式为响应式,vue组件从store中读取数据,如数据发生变化,组件也会对应的更新。
  • getter:可以认为是 store 的计算属性,它的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
  • mutation:更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
  • action:包含任意异步操作,通过提交 mutation 间接更变状态。
  • module:将 store 分割成模块,每个模块都具有state、mutation、action、getter、甚至是嵌套子模块。
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  actions: {
    increment(context) {
      context.commit('increment')
    }
  }
})

项目特别复杂的时候,可以让每一个模块拥有自己的state,mutation,action,getters,使得结构非常清晰,方便管理

  const modulesA = {
    state: { ... },
    mutations: { ... },
    actions: { ... }
    }
  }

  const modulesB = {
    state: { ... },
    mutations: { ... },
    actions: { ... }
    }
  }

const store = new Vuex.store({
  modules: {
    a: modulesA,
    b: modulesB
  }
})

20.Vue的路由
hash模式 和 history模式

hash模式:
在浏览器中符号 “#” 以及#后面的字符称之为hash,用window.location.hash读取;
特点:hash虽然在URL中,但不被包括在HTTP请求中,用来指导浏览器动作,对服务端安全无用,hash不会重新加载页面。

history模式:
history 采用HTML5的新特性;且提供了两个新方法:pushState()、replaceState()可以对浏览器历史纪录栈进行修改,以及popState事件监听到状态变更。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值