罗列一下我的学习规划
- ✨ Javascript 基础与进阶
- 🐨 Vue2.x 源码
- 🎉 Vue3.x 源码
- 🤯 React 源码
- 👺 前端工程化
- 👾 网络安全
- 🤖 浏览器原理
- 🎏 计算机原理
- 🤔 数据结构和算法
- 🥳 组件库开发
- 💬 NodeJS
- 🛠 单元测试
- 🗝 linux/docker
- 🐤 微前端(qiankun)
- 📱 Flutter
- 💡 Svelte
即将实现的DEMO
- 🎮 vue3.x版本的俄罗斯方块
- 🚀 vue3.x版本的飞机大战
- 📦 vue2.x 低代码平台
- 🔗 搭建个人持续集成
ヾ(◍°∇°◍)ノ゙JavaScript基础学习
强缓存、协商缓存的区别、以及应用场景
一、 概述
浏览器的缓存机制也就是HTTP缓存机制,其机制是根据HTTP报文的缓存标识进行的。
浏览器缓存是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。
一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。
二、 缓存位置
运行在浏览器背后的独立线程,一般可以用来实现缓存功能。
使用Service Worker的话,传输协议必须为HTTPS。因为Service Worker中涉及了请求拦截,所以必须使用HTTPS协议来保障安全。
内寸中的缓存,主要包含的是当前页面中已经抓取到的资源,例如页面已经下载的样式、脚本、图片等。
读取内存中的数据肯定比磁盘的快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放(一旦我们关闭Tab页面,内存中的缓存也就被释放了)。
内存缓存中有一块重要的缓存资源是preloader相关指令(例如 ),总所周知preloader的相关指令已经是页面优化的常见手段之一,他可以一边解析js/css文件,一边网络请求下一个资源。
存储在硬盘中的缓存,读取速度虽然慢点,但是什么都能存储到磁盘中, 与Memory Cache 相比,优势是容量和存储时效性。
在所有浏览器缓存中,Disk Cache覆盖面基本上是最大的。
它会根据HTTP Header中的字段判断哪些资源缓存,哪些资源不请求直接使用,哪些资源已经过期需要重新请求。并且即使在夸站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。
绝大部份的缓存都来自Disk Cache。
浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?
Push Cache(推送缓存)是HTTP/2中的内容,当以上三种缓存都没有命中时,它才会被使用。
它只在会话(session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂。
为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种: 强缓存和协商缓存,并且缓存策略都是通过设置HTTP Header来实现的。
三、 缓存过程分析
浏览器与服务器通信的方式为应答式的,即:浏览器发起HTTP请求 >> 服务器响应该请求。
那么浏览器怎么确定一个资源该不该缓存,如何去缓存呢?
浏览器第一次向服务器发起该请求后拿到请求结果后,将请求结果和缓存标识存入浏览器缓存,浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。
由图可知
以上两点是浏览器缓存机制的关键,它确保了每个请求的缓存存入与读取。
根据是否需要向服务器重新发起HTTP请求将缓存过程分为两个部分,分别是强缓存和协商缓存
四、 强缓存
定义
不会向服务器发起请求,直接从缓存中读取资源,在 chrome 控制台的 Network 选项中可以看到该请求返回 200 的状态码,并且size显示from disk cache或from memory cache。
强缓存可以通过设置两种HTTP Header实现: Expires 和 Cache-Control
Expires
缓存过期时间,用来指定资源到期的时间,是服务端的具体时间点。也就是说Expires = max-age + 请求时间,需要要和Last- modified结合使用
Expires是Web服务器响应消息头字段,在响应http请求是告诉浏览器在过期时间前浏览器可以直接从浏览器缓存存取数据,而无需再次请求。
Expires是HTTP/1的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失败
Cache-Control
在 HTTP/1.1 中,Cache-Control 是最重要的规则,主要用于控制网页缓存。
Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令:
Expires 和 Cache-Control 两者对比
其实这两者的差别不大,区别在于Expires 是 http1.0的产物, Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires
某些不支持HTTP1.1的环境下,Expires就会发挥作用,现阶段它的存在只是一种兼容性的写法。
弊端
强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务端最新的内容,那我们如何获知服务端内容是否已经发生了更新,此时需要用到协商缓存策略。
五、 协商缓存
定义
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:
协商缓存可以通过设置两种 HTTP Header 实现: Last-Modified 和 ETag
六、 缓存机制
强缓存优先于协商缓存进行,若强缓存生效则直接使用缓存,不生效则进行协商缓存(Last-Modified/If-modified-Since和Etag/If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失败,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识。再存入浏览器缓存中;生效则返回304,继续使用缓存。
七、 实际场景应用
对于频繁变动的资源,首先需要使用Cache-Contro: no-cache 使浏览器每次都请求服务器,然后配合ETag或者Last- Modified来验证资源是否有效。
这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小
通常在处理这类资源时,给他们的Cache-Control 配置一个很大的max-age=31536000(一年), 这样浏览器之后请求相同的URL会命中缓存。
而为了解决更新的问题,就需要在文件名(或者路径)中添加hash,版本号等动态字符,之后更改动态字符,从而达到更改引用URL的目的,让之前的强制缓存失效(其实并未立即失效,只是不再使用而已)。
八、 用户行为对浏览器缓存的影响
所谓用户行为对浏览器缓存的影响,指的是用户在浏览器如何操作时,会触发怎样的缓存策略。
打开网页,地址栏输入地址: 查找disk cache中是否有匹配,如果有则使用;没有则发送网络请求
普通刷新(F5): 因为TAB并没有关闭,因此memory cache是可用的,会被优先使用(如果匹配的话)。其次才是disk cache;
强制刷新(ctrl + F5): 浏览器不使用缓存,因此发送的请求头部均带有Cache-Control: no-cache(为了兼容,还带了Pragma: no-cache),服务器直接返回200和最新内容。
一、行内样式
使用style属性引入css样式
示例
<h1 style="color:red;">style属性的应用</h1>
<p style="color:red;font-size:30px;">我是p标签</p>
二、内部样式表
在style标签中书写CSS代码。style标签写在head标签中。
示例
<head>
<style type="text/css">
h3{ color:red; }
</style>
</head>
三、外表样式表
CSS代码保存在扩展名为.css的样式表中。 HTML文件引用扩展名为.css的样式表,有两种方式:链接式、导入式。
语法
<link type="text/css" rel="styleSheet" href="css文件路径" />
// 在html文件内导入
<style type="text/css">
@import url("css文件路径");
</style>
// 在css文件内导入
@import url(style.css);
两种的区别
link
四、 CSS中的优先级
样式优先级
行内样式>内部样式>外部样式(后两者是就近原则)
内部样式表和外部样式表使用就近原则,即谁写在下面以谁为准。
注意:导入式和链接式的优先级也是使用就近原则。
选择器优先级
优先级:id选择器>class选择器>属性选择器>标签选择器>通配符选择器
一、 跨域的定义
跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里的跨域是广义的
1. 资源跳转:a链接、重定向、表单提交
2. 资源嵌入:<link>、<script>、<img>、<frame>等dom标签,还有样式中background:url()、@font-face()等文件外链
3. 脚本请求:js 发起的ajax请求、dom和js的跨域操作等
是由浏览器同源策略限制的一类请求场景
二、 同源策略
同源策略/SOP是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能。
如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。
所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名是指向同一个ip地址,也非同源。
同源策略限制以下几种行为:
1. Cookie、LocalStorage、IndexDB 无法读取
2. DOM 和 JS 对象无法获得
3. AJAX请求不能发送
三、 常见跨越场景
URL 说明 是否允许通信
http://www.domain.com/a.js
http://www.domain.com/b.js 同一域名,不同文件或路径 允许
http://www.domain.com/lab/c.js
http://www.domain.com:8000/a.js
http://www.domain.com/b.js 同一域名,不同端口 不允许
http://www.domain.com/a.js
https://www.domain.com/b.js 同一域名,不同协议 不允许
http://www.domain.com/a.js
http://192.168.4.12/b.js 域名和域名对应相同ip 不允许
http://www.domain.com/a.js
http://x.domain.com/b.js 主域相同,子域不同 不允许
http://domain.com/c.js
http://www.domain1.com/a.js
http://www.domain2.com/b.js 不同域名 不允许
四、跨域解决方案
五、 通过jsonp跨域
通常为了减轻web服务器的负载,我们把js、css、img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器运行,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。
<script>
var script = document.createElment('script');
script.type = 'text/javascript';
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.scr = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
document.head.appenChild(script);
// 回调执行函数
function handleCallbacl(res){
alert(JSON.stringify(res);
}
</script>
服务器返回如下(返回时即执行全局函数):
handleCallback({"status": true, "user": "admin"})
$.ajax({
url: 'http://www.domain2.com:8080/login',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: "handleCallback", // 自定义回调函数名
data: {}
});
this.$http.jsonp('http://www.domain2.com:8080/login', {
params: {},
jsonp: 'handleCallback'
}).then((res) => {
console.log(res);
})
后端nodejs代码
var querystring = require('querystring');
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
var params = qs.parse(req.url.split('?')[1]);
var fn = params.callback;
// jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
jsonp缺点: 只能实现get一种请求
六、 document.domain + iframe 跨域
此方案仅限于主域相同、子域不同的跨域应用场景
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域
<!-- 父窗口 ->
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>
<!-- 子窗口 -->
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
alert('get js data from parent ---> ' + window.parent.user);
</script>
七、 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 -->
<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 -->
<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 -->
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
八、 window.name + iframe跨域
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
// a.html
var proxy = function(url, callback){
var state = 0;
var iframe = document.createElement('iframe');
// 加载跨域页面
iframe.src = url;
// onload 事件触发两次,第一次加载跨域页,并留存数据与window.name
iframe.onload = function(){
if(state === 1){
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if(state === 0 ) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
}
}
document.body.appendChild(iframe);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
}
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
alert(data);
});
中间代理页,与a.html同域,内容为空即可。
<script>
window.name = 'This is domain2 data!';
</script>
总结
通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
九、 postMessage 跨域
用法: postMessage(data, origin)方法接受两个参数
data: html5 规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参最好用JSON.stringify()序列化
origin: 协议+主机+端口号,也可以设置为"*",表示可以传递任意窗口,如果要指定和当前窗口同源的话设置为"/"
demo
// a.html
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;" />
<script>
var iframe = document.getElementById("iframe");
iframe.onload = function(){
var data = { name: 'aym" };
// 向domain2传递跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com/')
}
// 接受domain2返回的数据
window.addEventListener('message', function(e){
alert('data from domain2 -->' + e.data);
}, false)
</script>
// b.html
<script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>
十、跨域资源共享(CORS)
普通跨域请求:只服务端设置Access-Control-Origin即可,前端无须设置
若要带cookie请求:前后端都需要设置
需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。
如果想实现当前页cookie的写入,可参考下文
目前所有浏览器都支持该功能(IE8+/IE9需要使用XDomainRequest对象来支持CORS)
CORS也已经成为主流跨域解决方案。
// 前端设置是否携带cookie
xhr.withCredentials = true;
// 示例代码
// IE8/9需要用window.XDomainRequest兼容
const xhr = new XMLHttpRequest();
// 前端设置是否携带cookie
xhr.withCredentials = true;
xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlecoded');
xhr.send('user=admin');
xhr.inreadystatechange = function(){
if(xhr.readyState === 4 && xhr.status === 200) alert(xhr.responseText);
}
$.ajax({
...
xhrFields: {
// 前端设置是否携带cookie
withCredentials: true
},
crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie
})
// axios 设置
axios.defaults.withCredentials = true
// vue-resource设置
Vue.http.options.credentials = true
若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。
/*
* 导入包: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");
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...');
十一、 nginx代理跨域
浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体问题(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。
location / {
add_header Access-Control-Allow-Origin *;
}
同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
#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;
}
}
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
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...');
十二、 NodeJS中间件代理跨域
node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。
利用node + express + http-proxy-middleware搭建一个proxy服务器。
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();
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...');
利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。
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
}
}
十三、 WebSocket协议跨域
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/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>
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.');
});
});
var obj1 = { a: 1 };
function change(obj) {
obj.a = 2;
obj = {
a: 3,
};
return obj;
}
var obj2 = change(obj1);
console.log(obj1)
console.log(obj2);
var Foo = function () {
getName = function () {
console.log(1);
};
};
Foo.getName = function () {
console.log(2);
};
Foo.prototype.getName = function () {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
}
getName();
Foo.getName();
Foo().getName();
getName();
new Foo().getName();
#triangle-up {
width: 0;
height: 0;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
border-bottom: 100px solid red;
}
.arrow-tip {
width: 0;
height: 0;
border-left: 15px solid transparent;
border-right: 15px solid transparent;
border-bottom: 15px solid #222;
position: relative;
}
.arrow-tip:after {
content: "";
display: block;
width: 0;
height: 0;
position: absolute;
bottom: -16px;
left: -17px;
z-index: -1;
border-left: 17px solid transparent;
border-right: 17px solid transparent;
border-bottom: 17px solid red;
}
使用border-radius
div{
width: 100px;
height: 50px;
border:1px solid black;
background-color: blue;
border-radius: 100px 100px 0 0;
}
使用clip属性剪裁绝对定位元素
.demo {
/*clip 属性剪裁绝对定位元素。也就是说,只有 position:absolute 的时候才是生效的。*/
position:absolute;
width: 100px;
height: 100px;
border-radius: 50px;
/*唯一合法的形状值是:rect (top, right, bottom, left)*/
clip: rect(0px 50px 100px 0px);
}
使用clip-path属性
div{
width: 50px;
height: 50px;
border: 13px solid orange;
border-radius: 50px;
clip-path: polygon(50% 0%, 100% 0%, 100% 3600%, 50% 50%);
-webkit-clip-path: polygon(50% 0%, 100% 0%, 100% 3600%, 50% 50%);
}
使用borer-radius属性
.circle {
position: relative;
box-sizing: border-box;
}
.big {
width: 140px;
height: 140px;
border-radius: 50%;
position: absolute;
margin: auto;
/*以下五个属性为水平垂直居中的方法*/
left: 0;
top: 200px;
right: 0;
bottom: 0;
box-sizing: border-box;
}
.small {
width: 136px;
height: 136px;
border: 10px solid #9DDBE8;
border-radius: 50%;
position: absolute;
margin: auto;
/*以下五个属性为水平垂直居中的方法*/
left: 0;
top: 200px;
right: 0;
bottom: 0;
box-sizing: border-box;
border-bottom-color: transparent;
}
一、 content-type 的概念
content-type是http的实体首部字段,在request的请求行(或response的状态码)之后,也是首部的一部分。用于说明请求或返回的消息主体是用何种方式编码,在request header 和 response header里都存在。
请求信息和响应信息都可以包含实体信息,实体信息一般由实体头域和实体组成。实体头域包含关于实体的原信息,实体头包含Allow、Content-Base、Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5、Content-Range、Content-Type、Etag、Expires、Last-Modified、extension-header。
Content-Type是返回消息中非常重要的内容,表示后面的文档属于什么MIME类型。Content-Type:[type]/[subtype]: parameter。
例如最常见的就是text/html,它的意思是说返回的内容是文本类型,这个文本又是HTML格式的。
原则上浏览器会根据Content-Type来决定如何显示返回的消息体内容。
在http协议消息头中,使用Content-Type来表示具体请求中的媒体类型信息。
二、Type的形式
subtype用于指定type的详细形式。
content-type/subtype配对的集合和与此相关的参数,将随着时间而增长。为了确保这些值在一个有序且公开的状态下开发,MIME使用Internet Assigned Nunber Authority(IANA)作为中心的注册机制来管理这些值
parameter可以用来指定附加的信息,更多情况下是用于指定text/plain和text/html等文字编码方式的charset参数。
MIME根据type制定了默认的subtype,当客户端不能确定消息的subtyoe的情况下,消息被看作默认的subtype进行处理。
Text默认是text/plain、Application默认是application/octet-stream、Multipart默认是multipart/mixed
对于IE6浏览器来说,如果Content-Type中的类型和实际的消息体类型不一致,那么它会根据内容中的类型来分析实际应该是什么类型,对呀jpg、gif等常用图片格式都可以正确的识别出来,而不管Content-Type中写的是什么
如果Content-Type中指定的是浏览器可以直接打开的类型,那么浏览器就会直接打开其内容显示出来。
如果是被关联到其他应用程序的类型,这时候就要查找注册表中关于这种类型的注册情况。
如果是允许直接打开而不需要询问的,就会直接调出这个关联的应用程序来打开这个文件,但如果是不允许直接打开的,就会询问是否打开。
对于没有关联到任何应用程序的类型,IE浏览器不知道它该如何打开,此时IE6就会把它当成XML来尝试打开。
三、 Content-Type 与 Accept
Http报头分为通用报头,请求报头,响应报头和实体报头
请求方的Http报头结构: 通用报头|请求报头|实体报头
响应方的Http报头结构: 通用报头|响应报头|实体报头
Accept代表发送端(客户端)希望接受的数据类型
比如: Accept: text/xml; 代表客户端希望接受的数据类型是xml类型
Content-Type代表发送端(客服端|服务器)发送的实体数据的数据类型
比如: Content-Type: text/html; 代表发送端发送的数据格式是html
两者结合起来
Accept: text/xml;
Content-Type: text/html;
即代表希望接受的数据类型是xml格式,本次请求发送的数据的数据格式是html
四、 常用类型
text/html: HTML格式
text/plain: 纯文本格式
text/xml: XML格式
image/gif: gif图片格式
image/jpeg: jpg图片格式
image/png: png图片格式
application/xhtml+xml: XHTML格式
application/xml: XML数据格式
application/atom+xml: Atom XML聚合格式
application/json: JSON数据格式
application/pdf: pdf格式
application/msword: word文档格式
application/octet-stream: 二进制流数据(如常见的文件下载)
application/x-www-form-urlencoded: <form encType=””>中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)
audio/x-wav: wav文件
audio/x-ms-wma: wma文件
audio/mp3: mp3文件
video/x-ms-wmv: wmv文件
video/mpeg4: mp4文件
video/avi: avi文件
multipart/form-data: 需要在表单中进行文件上传时,就需要使用该格式
五、常见的Content-Type讲解
这应该是最常见的POST提交数据的方式了。
浏览器的原声form表单,如果不设置enctype属性,那么最终就会以application/x-www-form-urlencoded方式提交数据。
很多时候,我们用Ajax提交数据的时候,也是用这种方式。
例如jquery的ajax,Content-Type的默认值都是application/x-www-form-urlencoded;charset=utf-8
这又是一个常见的POST数据提交的方式。 我们使用表单上传文件的时候,必须让form的enctype等于这个值。
<form action="url" enctype="multipart/form-data" method="post"></form>
POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"
title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png
PNG ... content of chrome.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--
这种方式一般用来上传文件,各大服务端语言对它也有着良好的支持。
现在越来越多的人把它作为请求头,用来告诉服务端信息主体是序列化后的JSON字符串。
由于JSON规范的流行,除了低版本IE之外的各大浏览器都原生支持JSON.stringify。
服务端语言也都有处理JSON的函数,使用JSON不会遇上什么麻烦
JSON 格式支持比键值对复杂得多的结构化数据
var data = {'title':'test', 'sub' : [1,2,3]};
$http.post(url, data).success(function(result) {
...
});
最终发送的请求
POST http://www.example.com HTTP/1.1
Content-Type: application/json;charset=utf-8
{"title":"test","sub":[1,2,3]}
这种方案可以方便的提交复杂的结构化数据,特别适合RESTful的接口。
各大抓包工具如Chrome自带的开发者工具、fireBug、fiddler都会以树型结构展示JSON数据,非常友好。
它是一种使用 HTTP 作为传输协议,XML 作为编码方式的远程调用规范。典型的 XML-RPC 请求是这样的:
POST http://www.example.com HTTP/1.1
Content-Type: text/xml
<?xml version="1.0"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value><i4>41</i4></value>
</param>
</params>
XML-RPC协议简单、功能够用,各种语言的实现都有。
它的使用也很广泛,如WordPress的XML-RPC API,搜索引擎的ping服务等等。
JavaScript中也有现成的库支持以这种方式进行数据交互,能很好的支持已有的 XML-RPC 服务。
一、 loader 的工作原理
loader(加载器)是webpack的核心之一。它用于将不同类型的文件转换为webpack可识别的模块
webpack 只能直接处理JavaScript格式的代码。任何非js文件都必须呗预先处理转换为js代码,才可以参与打包。
loader就是这样一个代码转换器,它由webpack的loader runner
执行调用,接收原始资源数据作为参数(当多个加载器联合使用时,上一个loader的结果会传入下一个loader),最后输出JavaScript代码(和可选的source map)给webpack做进一步编译。
二、 loader 执行顺序
内联loader可以通过添加不同前缀,跳过其他类型的loader
!
跳过normal loader-!
跳过 pre loader、 normal loader!!
跳过 pre loader、normal loader、post loader三、 less-loader、css-loader、style-loader 插件作用
四、loader 在webpack的配置
npm i style-loader css-loader less less-loader -D
'use strict';
const path = require('path');
module.exports = {
mode: 'production',
entry: {
index: './src/index.js',
search: './src/search.js',
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
},
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
},
],
},
};
// 解析less主要是通过
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader'
]
}
解析的过程是链式的,所以在use数组中下面的部分会先执行,所以执行顺序其实是less-loader > css-loader > style-loader
Webpack 选择了 compose 方式, 而不是pipe的方式而已,在技术上实现从左往右也不会有难度
compose : require("style-loader!css-loader!sass-loader!./my-styles.sass");
pipe : require("./my-styles.sass!sass-loader!css-loader!style-loader");
函数组合是函数式编程中非常重要的**
函数组合有两种形式:一种是pipe、另一种是compose。前者从左往右组合函数,后者方向相反
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const add = n => n + 1;
const double = n => n * 2;
const addThenDouble = compose(double, add);
addThenDouble(2); // ((2 + 1) * 2) === 6
先执行了加1, 然后执行了double, 在compose中是采用了reduceRight,所以传入参数的顺序先传入double,后传入add
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const add = n => n + 1;
const double = n => n * 2;
const addThenDouble = compose(add,double);
add1ThenDouble(2); // ((2 + 1 = 3) * 2 === 6)
一、 静态布局
传统的Web设计,网页上的所有元素的尺寸一律使用px作为单位
不管浏览器尺寸具体是多少,网页布局始终按照最初写代码时的布局来显示。常规的pc的网站都是静态(定宽度)布局的,也就是设置了min-width,这样的话,如果小于这个宽度就会出现滚动条,如果大于这个宽度则内容居中外加背景,这种设计常见于pc端。
设计方法
PC
居中布局,所有样式使用绝对宽度、高度(px),设计了一个Layout,在屏幕宽高有调整时,使用横向和竖向的滚动条来查阅被遮掩部分
移动端
另外建立移动网站,单独设计一个布局,使用不同的域名如wap.或者m.
优缺点
二、 流式布局
流式布局(Liquid)的特点(也叫"Fluid") 是页面元素的宽度按照屏幕分辨率进行适配调整,但整体布局不变。代表作栅栏系统(网格系统)。
网页中主要的划分区域的尺寸使用百分数(搭配min-*、max-*属性使用),例如,设置网页主体的宽度为80%,min-width为960px。图片也作类似处理(width:100%, max-width一般设定为图片本身的尺寸,防止被拉伸而失真)。
屏幕分辨率变化时,页面里元素的大小会变化而但布局不变。【这就导致如果屏幕太大或者太小都会导致元素无法正常显示。
使用%百分比定义宽度,高度大都是用px来固定住,可以根据可视区域 (viewport) 和父元素的实时尺寸进行调整,尽可能的适应各种分辨率。往往配合 max-width/min-width 等属性控制尺寸流动范围以免过大或者过小影响阅读。
这种布局方式在Web前端开发的早期历史上,用来应对不同尺寸的PC屏幕(那时屏幕尺寸的差异不会太大),在当今的移动端开发也是常用布局方式,但缺点明显:主要的问题是如果屏幕尺度跨度太大,那么在相对其原始设计而言过小或过大的屏幕上不能正常显示。因为宽度使用%百分比定义,但是高度和文字大小等大都是用px来固定,所以在大屏幕的手机下显示效果会变成有些页面元素宽度被拉的很长,但是高度、文字大小还是和原来一样(即,这些东西无法变得“流式”),显示非常不协调
三、 自适应布局
自适应布局的特点是分别为不同的屏幕分辨率定义布局,即创建多个静态布局,每个静态布局对应一个屏幕分辨率范围。改变屏幕分辨率可以切换不同的静态局部(页面元素位置发生改变),但在每个静态布局中,页面元素不随窗口大小的调整发生变化。可以把自适应布局看作是静态布局的一个系列。
屏幕分辨率变化时,页面里面元素的位置会变化而大小不会变化。
使用 @media 媒体查询给不同尺寸和介质的设备切换不同的样式。在优秀的响应范围设计下可以给适配范围内的设备最好的体验,在同一个设备下实际还是固定的布局。
关于跨域的问题
一、 HTTP 和 HTTPS 协议的区别
二、 HTTP1.0 和 HTTP1.1 的区别
三、 HTTP1.1 和 HTTP2.0 的区别
一、 全局路由守卫
全局前置守卫
router.afterEach((to, from, next) => {})
全局解析守卫
router. beforeResolve((to, from) => {})
全局后置钩子
router.afterEach((to, from) => {})
二、路由独享守卫
beforeEnter: (to, from, next) => {}
三、组件内的守卫
beforeRouteEnter 在渲染该组件的对应路由被 confirm 前调用
beforeRouteEnter(to, from, next){
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
}
beforeRouteUpdate 在当前路由改变,但是该组件被复用时调用
beforeRouteUpdate(to, from, next){
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
}
beforeRouteLeave 导航离开该组件的对应路由时调用
beforeRouteLeave(to, from, next){
// 可以访问组件实例 `this`
}
四、 完整的导航解析流程
一、 单行省略
overflow: hidden; // 溢出隐藏
text-overflow: ellipsis; // 溢出用省略号显示
white-space: nowrap; // 规定段落中的文本不进行换行
二、 多行省略
display: -webkit-box; // 作为弹性伸缩盒子模型显示
overflow: hidden; // 溢出隐藏
text-overflow: ellipsis; // 溢出用省略号显示
-webkit-box-orient: vertical; // 设置伸缩盒子的子元素排列方式: 从上到下垂直排列
-webkit-line-clamp: 2; // 显示的行数
一、 原型
在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个prototype属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。
当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的prototype属性对应的值,在ES5中这个指针被称为对象的原型。
一般来说不应该能够获取这个值的,但是现在浏览器中都实现了proto属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5中新增了Object.getPrototypeOf()方法, 来获取对象的原型
二、 原型链
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。
原型链的尽头一般来说都是Object.prototype,所以这就是新建的对象为什么能够使用toString()等方法的原因。
三、 原型的特点
JavaScript对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。
当修改原型时,与之相关的对象以后继承这一改变。
一、 防抖
在事件被触发n秒后在执行回调,如果在这n秒内事件又被触发,则重新计时。
这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求
应用场景
实现
function debounce(fn, wait){
let timer = null;
return function(){
let context = this, args = [...arguments];
// 如果此时存在定时器的话,则取消之前的定时器重新计时
if(timer){
clearTimeout(timer);
timer = null;
}
// 设置定时器,使事件间隔指定事件后执行
timer = setTimeout(() => {
fn.apply(context, args);
}, warit);
}
}
二、 节流
指规定一个单位时间内,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件内被触发多次,只有一次能生效。
节流可以使用在scroll函数的事件监听上,通过事件节流来降低事件调用的频率
应用场景
实现
// 时间戳版
function throttle(fn, delay){
let preTime = Date.now();
return function(){
let context = this, args = [...arguments], nowTime = Date.now();
// 如果两次时间间隔超过了指定时间, 则执行函数
if( nowTime - preTime >= delay){
preTime = Date.now();
return fn.apply(context, args);
}
}
}
// 定时器版
function throttle(fun, wait){
let timeout = null;
return function(){
let context = this, args = [...arguments];
if(!timeout){
timeout = setTimeout(() => {
fun.apply(context, args);
timeout = null;
}, wait);
}
}
}
一、 基本概念
物理像素又被称为设备像素,它是显示设备中一个最微小的物理部件。每个像素可以根据操作系统设置自己的颜色和亮度。正是这些设备像素的微小距离欺骗了我们肉眼看到的图像效果。
设备独立像素也称为密度无关像素,可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用的虚拟像素(比如说CSS像素),然后由相关系统转换为物理像素。
CSS像素是一个抽像的单位,主要使用在浏览器上,用来精确度量Web页面上的内容。一般情况之下,CSS像素称为与设备无关的像素(device-independent pixel),简称DIPs。
屏幕密度是指一个设备表面上存在的像素数量,它通常以每英寸有多少像素来计算(PPI)。
设备像素比简称为dpr,其定义了物理像素和设备独立像素的对应关系。
它的值可以按下面的公式计算得到:
设备像素比 = 物理像素 / 设备独立像素
在Javascript中,可以通过 window.devicePixelRatio
获取到当前设备的dpr。
在CSS中可以通过 -webkit-device-pixel-ratio
,-webkit-min-device-pixel-ratio
和 -webkit-max-device-pixel-ratio
进行媒体查询,对不同dpr的设备,做一些样式适配。
或者使用 resolution | min-resolution | max-resolution 这些比较新的标准方式
一个位图像素是栅格图像(如:png, jpg, gif等)最小的数据单元。每一个位图像素都包含着一些自身的显示信息(如:显示位置,颜色值,透明度等)。
理论上,1个位图像素对应于1个物理像素,图片才能得到完美清晰的展示
如上图:对于dpr=2的retina屏幕而言,1个位图像素对应于4个物理像素,由于单个位图像素不可以再进一步分割,所以只能就近取色,从而导致图片模糊(注意上述的几个颜色值)。
所以,对于图片高清问题,比较好的方案就是两倍图片(@2x)。
如:200×300(css pixel)img标签,就需要提供400×600的图片。
缩放比:scale = 1/dpr
简单的理解,viewport是严格等于浏览器的窗口。在桌面浏览器中,viewport就是浏览器窗口的宽度高度。但在移动端设备上就有点复杂。
移动端的viewport太窄,为了能更好为CSS布局服务,所以提供了两个viewport:虚拟的visualviewport和布局的layoutviewport。
在开发移动端页面,我们可以设置meta标签的viewport scale来对视窗的大小进行缩放定义
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
二、 布局单位
相对长度单位,相对于当前对象内文本的字体尺寸, 根据父元素的大小变化而变化
相当于"倍",比如设置当前的div字体大小为1.5em,则当前的div字体大小为:当前div继承的字体大小 * 1.5倍
相对长度单位,相对于跟元素( 即 html 元素)font-size 的倍数, 不会被它的父元素影响。
如果有嵌套的关系,嵌套关系的元素的字体大小始终按照根节点的字体大小进行缩放。
使用rem单位,就按以下三个步骤:
确定基数
设置html的font-size
/**
* 百分数 = 基数/16
* 基数10 百分数 62.5%
* 基数14 百分数 87.7%
*/
html{ font-size: 百分数 };
px换算rem
rem 布局
核心: 设置好根html元素的font-size
一般来说,为了防止在高清屏幕下像素不够用导致模糊,设计稿是640px(iPhone5 设备宽为320px)或750px(iphone6 设备宽为375px)的两倍稿,按照设备宽度做了两倍的大小。
假设我们将屏幕宽度平均分为100份,每一份的宽度用rem表示, rem = 屏幕宽度 / 100,如果将rem作为单位,rem前面的数值就代表屏幕宽度的百分比。
如何把UI图中获取的像素单位的值,转化为以rem为单位的值
利用scss体验提供一系列的基础支持
/**移动端页面设计稿宽度*/
$design-width: 750;
/**移动端页面设计稿dpr基准值*/
$design-dpr: 2;
/**将移动端页面分为10块*/
$blocks: 10;
/**缩放所支持的设备最小宽度*/
$min-device-width: 320px;
/**缩放所支持的设备最大宽度*/
$max-device-width: 540px;
/**
* rem与px的对应关系
* 1rem代表在js中设置的html,font-size的值(为一块的宽度)
* $rem即为$px对应占多少块
*
* $px / $design-width === $rem / $blocks
*/
/**单位px转化为rem*/
@function px2rem($px){
@return #{ $px / $design-width * $blocks }rem;
}
/**单位rem转化为px, 可用于根据rem单位快速计算原px*/
@function rem2px($rem){
@return #{ $rem / $blocks * $design-width)px;
}
兼容性: 在移动端IOS 8以上Android 4.4以上获得支持
一、 数据类型检测的方式有哪些
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object
对象、数组、null都会被判断为object,其他判断都正确
instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型
instanceof 只能正确判断引用数据类型,而不能判断基本数据类型
instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function); // true
console.log((() => {}) instanceof Function); // true
instanceof 运算符用于判断构造函数的prototype属性是否出现在对象的原型链中的任何位置
function instanceof(left, right){
// 获取对象的原型
let proto = Object.getPrototypeOf(left);
// 获取构造函数的prototype对象
let prototype = right.prototype;
// 判断构造函数的prototype对象是否在对象的原型链上
while(true){
if(!proto) return false;
if(proto === prototype) return true;
// 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
proto = Object.getPrototypeOf(proto);
}
}
constructor 有两个作用, 一是判断数据的类型,二是对象实例通过constructor对象访问它的构造函数
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log([].constructor === Array); // true
console.log((function(){}).constructor === Function); // true
console.log(({}).constructor === Object); // true
需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了
function Fn(){};
Fn.prototype = new Array();
var f = new Fn();
console.log(f.constructor === Fn); // false
console.log(f.constructor === Array); // true
Object.prototype.toString.call() 使用Object对象的原型方法toString来判断数据类型
var a = Object.prototype.toString;
console.log(a.call(2)); // [object Number]
console.log(a.call(true)); // [object Number]
console.log(a.call('str')); // [object String]
console.log(a.call([])); // [object Array]
console.log(a.call(function(){})); // [object Function]
console.log(a.call({})); // [object Object]
console.log(a.call(undefined)); // [object Undefined]
console.log(a.call(null)); // [object Null]
因为toString是Object的原型方法,而Array、Function等类型作为Object的实例,都重写了toString方法。
不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应重写之后的toString方法( function 类型返回内容为函数体的字符串,Array类型返回元素的字符串),而不会去调用Object上原型的toString方法(返回对象的具体类型)。
所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此在想要得到对象的具体类型时,应该调用Object原型上的toString方法。
二、 判断数组的方式有哪些
Object.prototype.toString.call(arr).slice(8, -1) === 'Array';
arr.__proto__ === Array.prototype
Array.isArray(arr)
arr instanceof Array
Array.prototype.isPrototypeOf(arr)
一、 当需要一次性向页面中插入大量数据时,怎么样才能不卡主页面的情况下渲染数据?一般有两种做法
二、 粗暴法
首先使用最粗暴的做法,一次性将大量数据插入页面中
<ul id="container"></ul>
/**记录任务开始的时间*/
let now = Date.now();
/**插入十万条数据*/
const total = 100000;
/**获取容器*/
const ul = document.getElementById("container");
/**将数据插入容器中*/
for (let i = 0; i < total; i++) {
let li = document.createElement("li");
li.innerHTML = ~~(Math.random() * total);
ul.appendChild(li);
}
console.log("JS运行时间:", Date.now() - now); /**JS运行时间: 375*/
setTimeout(() => {
console.log("总时间", Date.now() - now); /**总时间 3267*/
}, 0);
如何两次console.log
的结果时间差异巨大,并且是如何简单统计JS运行时间和总渲染时间
在JS的Event Loop中,当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完毕后,才会触发渲染线程对页面进行渲染
第一个console.log的触发时间是在页面进行渲染之前,此时得到的间隔时间为js运行所需要的时间
第二个console.log是放到setTimeout中,它的触发时间是在渲染完成,在下一次Event Loop中执行
结论:对于大量数据的渲染,JS运算并不是性能的瓶颈,性能的瓶颈主要在于渲染阶段
三、 时间分片
页面卡顿是由于同事渲染大量DOM所引起的,所以考虑将渲染过程分批进行
/**获取容器*/
const ul = document.getElementById("container");
/**插入十万条数据*/
const total = 100000;
/**一次插入20条*/
const once = 20;
/**每条记录的索引*/
let idx = 0;
/**总页数*/
let page = total / once;
/**循环加载数据*/
function loop(curTotal, curIdx) {
if (curTotal <= 0) return false;
/**每页多少条*/
let pageCount = Math.min(curTotal, once);
setTimeout(() => {
/**将数据插入容器中*/
for (let i = 0; i < pageCount; i++) {
let li = document.createElement("li");
li.innerText = curIdx + i + " : " + ~~(Math.random() * total);
ul.appendChild(li);
}
loop(curTotal - pageCount, curIdx + pageCount);
}, 0);
}
loop(total, idx);
页面加载的时间已经非常快了,每次刷新可以很快看到第一屏的所有数据,但是当我们快速滚动页面的时候,发现页面出现闪屏或者白屏的现象。
需要明白的一些概念
为什么你感觉不到这个变化?
直观感受,不同帧率的体验:
以上两种情况都会导致setTimeout的执行步调和屏幕的刷新步调不一致。
在setTimeout中对dom进行操作,必须要等到屏幕下次绘制时才能更新到屏幕上,如果两者步调不一致,就可能导致中间某一帧的操作被跨越过去,而直接更新下一帧的元素,从而导致丢帧现象。
requestAnimationFrame
最大的优势是由系统来决定回调函数的执行时机
如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次
如果屏幕刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms
换句话说就是requestAnimationFrame
的步伐跟着系统的刷新步伐走。
他能保证回调函数在屏幕每一次的刷新间隔只被执行一次,这样就不会引起丢帧现象
<ul id="container"></ul>
/**获取容器*/
const ul = document.getElementById("container");
/**插入十万条数据*/
const total = 100000;
/**一次插入20条*/
const once = 20;
/**每条记录的索引*/
let idx = 0;
/**总页数*/
let page = total / once;
/**循环加载数据*/
function loop(curTotal, curIdx) {
if (curTotal <= 0) return false;
/**每页多少条*/
let pageCount = Math.min(curTotal, once);
window.requestAnimationFrame(() => {
/**将数据插入容器中*/
for (let i = 0; i < pageCount; i++) {
let li = document.createElement("li");
li.innerText = curIdx + i + " : " + ~~(Math.random() * total);
ul.appendChild(li);
}
loop(curTotal - pageCount, curIdx + pageCount);
}, 0);
}
loop(total, idx);
DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的Document使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为DocumentFragment不是真实DOM树的一部分,它的变化不会触发DOM树的(重新渲染),且不会导致性能等问题。
可以使用document.createDocumentFragment方法或者构造函数来创建一个空的DocumentFragment
从MDN的说明中,我们得知DocumentFragment是DOM节点,但并不是DOM树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流
当append元素到document中时,被append进去的元素的样式表的计算是同步发生的,此时调用get ComputedStyle可以得到样式的计算值。
而append元素到documentFragment中时,是不会计算元素的样式表,所以documentFragment性能更优。当然现在浏览器的优化已经做的很好了,当append元素到document中后,没有访问getComputedStyle之类的方法时,现代浏览器也可以把样式表的计算推迟到脚本执行之后。
<ul id="container"></ul>
/**获取容器*/
const ul = document.getElementById("container");
/**插入十万条数据*/
const total = 100000;
/**一次插入20条*/
const once = 20;
/**每条记录的索引*/
let idx = 0;
/**总页数*/
let page = total / once;
/**循环加载数据*/
function loop(curTotal, curIdx) {
if (curTotal <= 0) return false;
/**每页多少条*/
let pageCount = Math.min(curTotal, once);
window.requestAnimationFrame(() => {
const fragment = document.createDocumentFragment();
/**将数据插入容器中*/
for (let i = 0; i < pageCount; i++) {
let li = document.createElement("li");
li.innerText = curIdx + i + " : " + ~~(Math.random() * total);
fragment.appendChild(li);
}
ul.appendChild(fragment);
loop(curTotal - pageCount, curIdx + pageCount);
}, 0);
}
loop(total, idx);
通过时间分片的方式来同时加载大量简单DOM。
四、 虚拟列表
虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域的数据不渲染或部分渲染的技术,从而达到极高的渲染性能
假设有一万条记录同时渲染,我们屏幕的可见区域的高度为500px,而列表项的高度为50px,则此时我们在屏幕中最多只能看到10个列表项,那么在首次渲染的时候,我们只需要加载10条即可。
说完首次加载,再分析一下当滚动发生时,我们可以通过计算当前滚动值得知此时在屏幕可见区域应该显示的列表项
假设滚动发生,滚动条距顶部的位置为150px,则我们可得知可见区域内的列表项为第4项至第13项。
在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。
由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可以正常的处罚滚动,将html结构设计成如下结构
<div class="infinite-list-container">
<div class="infinite-list-phantom"></div>
<div class="infinite-list">
<!-- item-1 -->
<!-- item-2 -->
<!-- ...... -->
<!-- item-n -->
</div>
</div>
接着监听infinite-list-container的scroll事件,获取滚动条的scrollTop
则可推算出
当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移到可视区域中
虚拟列表component部分
<template>
<div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
<div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }" />
<div class="infinite-list" :style="{ transform: getTransform }">
<div
ref="items"
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemSize + 'px', lineHeight: itemSize + 'px' }"
>
{{ item.value }}
</div>
</div>
</div>
</template>
export default {
name: "Infinite",
props: {
/**所有列表数据 */
listData: {
type: Array,
default: () => [],
},
/**每项的高度 */
itemSize: {
type: Number,
default: 200,
},
},
computed: {
/**列表总高度 */
listHeight() {
return this.listData.length * this.itemSize;
},
/**可显示的列表项目 */
visibleCount() {
return Math.ceil(this.screenHeight / this.itemSize);
},
/**偏移量对应的style */
getTransform() {
return `translate3d(0, ${this.startOffset}px, 0)`;
},
/**获取真实显示列表数据 */
visibleData() {
return this.listData.slice(
this.start,
Math.min(this.end, this.listData.length)
);
},
},
data() {
return {
/**可视区域高度 */
screenHeight: 0,
/**偏移量 */
startOffset: 0,
/**起始索引 */
start: 0,
/**结束索引 */
end: null,
};
},
mounted() {
this.screenHeight = this.$el.clientHeight;
this.start = 0;
this.end = this.start + this.visibleCount;
},
methods: {
scrollEvent() {
/**当前滚动位置 */
const scrollTop = this.$refs.list.scrollTop;
/**此时的开始索引 */
this.start = Math.floor(scrollTop / this.itemSize);
/**此时的结束索引 */
this.end = this.start + this.visibleCount;
/**此时的偏移量 */
this.startOffset = scrollTop - (scrollTop % this.itemSize);
},
},
};
.infinite-list-container {
height: 100%;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
.infinite-list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.infinite-list {
left: 0;
right: 0;
top: 0;
position: absolute;
text-align: center;
}
.infinite-list-item {
padding: 10px;
color: #555;
box-sizing: border-box;
border-bottom: 1px solid #999;
}
父组件
<template>
<div id="app">
<Infinite :listData="data" :itemSize="100" />
</div>
</template>
import Infinite from "./components/Infinite";
let d = [];
for (let i = 0; i < 1000; i++) {
d.push({ id: i, value: i });
}
export default {
name: "App",
components: {
Infinite,
},
data() {
return {
data: d,
};
},
};
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
}
#app {
height: 100%;
}
实际应用的时候,当列表中包含文本之类的可变内容,会导致列表项的高度并不相同
在虚拟列表中应用动态高度的解决方案一般有如下三种
- 对组件数学itemSize进行扩展,支持传递类型为数字、数组、函数
这种方式虽然有比较好的灵活度,但仅适用于可以预先知道或通过计算得知列表项高度的情况,依然无法解决列表项高度由内容撑开的情况。
- 将列表项渲染到屏幕外,对其高度进行测量并缓存,然后再将其渲染至可视区域内
由于预先渲染至屏幕外,在渲染至屏幕内,这导致渲染成本增加一倍,这对于数百万用户在低端移动端设备上使用的产品来说是不切实际的
- 以预估高度先行渲染,然后获取真实高度并缓存
定义组件属性estimatedItemSize, 用来接收预告高度
props:{
// 预告高度
estimatedItemSize:{
type: Number
}
}
定义positions,用于列表项渲染后存储每一项的高度以及位置信息
this.positions = [
top: 0,
bottom: 100,
height: 100
]
并在初始化根据estimatedItemSize对positions进行初始化
initPositions(){
this.positions = this.listData.map((item, index) => {
return {
index,
height: this.estimatedItemSize,
top: index * this.estimatedItemSize,
bottom: (index + 1) * this.estimatedItemSize
}
})
}
由于列表项高度不定,并且我们维护了positions,用于记录每一项的位置,而列表高度实际就等于列表中最后一项的底部距离列表顶部的位置
// 列表总高度
listHeight(){
return this.positions[this.positions.length - 1].bottom
}
由于需要在渲染完成后,获取列表每项的位置信息并缓存,所以使用钩子函数updated来实现
updated(){
let nodes = this.$refs.items;
nodes.forEach(node => {
let rect = node.getBoundingClientRect();
let height = rect.height;
let index = +node.id.slice(1);
let oldHeight = this.positions[index].height;
let dValue = oldHeight - height;
// 存在差值
if(dValue){
this.positions[index].bottom = this.positions[index].bottom - dValue;
this.positions[index].height = height;
for(let k = index + 1; k < this.positions.length; k++){
this.positions[k].top = this.positons[k-1].bottom;
this.positions[k].bottom = this.positions[k-1].bottom - dValue;
}
}
})
}
滚动后获取列表开始索引的方法修改为通过缓存获取】
// 获取列表起始索引
getStartIndex(scrollTop = 0){
let item = this.positions.find(i => i && i.bottom > scrollTop);
return item.index
}
由于我们的缓存数据,本身就是有顺序的,所以获取开始索引的方法可以考虑通过二分查找的方式来降低检索次数
// 获取列表起始索引
getStartIndex(scrollTop = 0){
// 二分查找
return this.binarySearch(this.positions, scrollTop)
}
// 二分法查找
binarySearch(list, value){
let start = 0;
let end = list.length - 1;
let tempIndex = null;
while(start <= end){
let midIndex = parsetInt((start + end) / 2);
let midValue = list[midIndex].bottom;
if(midValue === value){
return midIndex + 1;
}else if(midValue < value){
start = midIndex + 1;
}else if(midValue > value){
if(tempIndex === null || tempIndex > midIndex) tempIndex = midIndex;
end = end -1;
}
}
return tempIndex
}
滚动后将偏移量的获取方式变更
scrollEvent(){
// ... 省略
if(this.start >= 1) this.startOffset = this.positions[this.start - 1].bottom
else this.startOffset = 0
}
通过faker.js来创建一些随机数据
let data = [];
for(let i = 0; i < 100000;i++){
data.push({
id: i,
value: faker.lorem.sentences(); // 长文本
})
}
为了使页面平滑滚动,我们还需要在可见区域的上方和下方渲染额外的项目,在滚动时给予一些缓存,所以将屏幕分为三个区域
定义组件属性bufferScale, 用于接收缓存区数据与可视区数据的比例
props:{
// 缓存区比例
bufferScale:{
type: Number;
default: 1
}
}
可视区上方渲染条数 aboveCount 获取方式如下
aboveCount(){
return Math.min(this.start, this.bufferScale * this.visibleCount)
}
可视区下方渲染条数 belowCount 获取方式如下
belowCount(){
return Math.min(this.listData.length - this.end, this.bufferScale * this.visibleCount)
}
真实渲染数据visibleData获取方式如下
visibleData(){
let start = this.start - this.abvoeCount;
let end = this.end + this.belowCount;
return this._listData.slice(start, end);
}
完整代码
虚拟列表组件
<template>
<div
ref="list"
:style="{ height }"
class="infinite-list-container"
@scroll="scrollEvent($event)"
>
<div ref="phantom" class="infinite-list-phantom" />
<div ref="content" class="infinite-list">
<div
class="infinite-list-item"
ref="items"
:id="item._index"
:key="item._index"
v-for="item in visibleData"
>
<slot ref="slot" :item="item.item" />
</div>
</div>
</div>
</template>
export default {
name: "VirtualList",
props: {
/**所有列表数据 */
listData: {
type: Array,
default: () => [],
},
/**预估高度 */
estimatedItemSize: {
type: Number,
required: true,
},
/**缓存区比例 */
bufferScale: {
type: Number,
default: 1,
},
/**容器高度 100px or 50vh */
height: {
type: String,
default: "100%",
},
},
data() {
return {
/**可视区域高度 */
screenHeight: 0,
/**开始索引 */
start: 0,
/**结束索引 */
end: 0,
};
},
computed: {
_listData() {
return this.listData.map((item, index) => {
return {
_index: `_${index}`,
item,
};
});
},
visibleCount() {
return Math.ceil(this.screenHeight / this.estimatedItemSize);
},
aboveCount() {
return Math.min(this.start, this.bufferScale * this.visibleCount);
},
belowCount() {
return Math.min(
this.listData.length - this.end,
this.bufferScale * this.visibleCount
);
},
visibleData() {
let start = this.start - this.aboveCount;
let end = this.end + this.belowCount;
return this._listData.slice(start, end);
},
},
created() {
this.initPositions();
window.vm = this;
},
mounted() {
this.screenHeight = this.$el.clientHeight;
this.start = 0;
this.end = this.start + this.visibleCount;
},
updated() {
this.$nextTick(function () {
if (!this.$refs.items || !this.$refs.items.length) return;
/**获取真实元素大小, 修改对应的尺寸缓存 */
this.updateItemSize();
/**更逊真实元素大小,修改对应的尺寸缓存 */
let height = this.positions[this.positions.length - 1].bottom;
this.$refs.phantom.style.height = height + "px";
/**更新真实偏移量 */
this.setStartOffset();
});
},
methods: {
initPositions() {
this.positions = this.listData.map((d, index) => ({
index,
height: this.estimatedItemSize,
top: index * this.estimatedItemSize,
bottom: (index + 1) * this.estimatedItemSize,
}));
},
/**获取列表起始索引 */
getStartIndex(scrollTop = 0) {
/**二分查找 */
return this.binarySearch(this.positions, scrollTop);
},
binarySearch(list, value) {
let start = 0,
end = list.length - 1,
tempIndex = null;
while (start <= end) {
let midIndex = parseInt((start + end) / 2);
let mindValue = list[midIndex].bottom;
if (mindValue === value) return midIndex + 1;
else if (mindValue < value) start = midIndex + 1;
else if (mindValue > value) {
if (tempIndex === null || tempIndex > midIndex) tempIndex = midIndex;
end = end - 1;
}
}
return tempIndex;
},
/**获取列表项的当前尺寸 */
updateItemSize() {
let nodes = this.$refs.items;
nodes.forEach((node) => {
let rect = node.getBoundingClientRect();
let height = rect.height;
let index = +node.id.slice(1);
let oldHeight = this.positions[index].height;
let dValue = oldHeight - height;
/**存在差值 */
if (dValue) {
this.positions[index].bottom = this.positions[index].bottom - dValue;
this.positions[index].height = height;
for (let k = index + 1; k < this.positions.length; k++) {
this.positions[k].top = this.positions[k - 1].bottom;
this.positions[k].bottom = this.positions[k].bottom - dValue;
}
}
});
},
/**获取当前的偏移量 */
setStartOffset() {
let startOffset = 0;
if (this.start >= 1) {
let size =
this.positions[this.start].top -
(this.positions[this.start - this.aboveCount]
? this.positions[this.start - this.aboveCount].top
: 0);
startOffset = this.positions[this.start - 1].bottom - size;
} else {
startOffset = 0;
}
this.$refs.content.style.transform = `translate3d(0,${startOffset}px,0)`;
},
/**滚动事件 */
scrollEvent() {
/**当前滚动位置 */
let scrollTop = this.$refs.list.scrollTop;
/**此时的开始索引 */
this.start = this.getStartIndex(scrollTop);
/**此时的结束索引 */
this.end = this.start + this.visibleCount;
/**此时的偏移量 */
this.setStartOffset();
},
},
};
<style scoped>
.infinite-list-container {
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
.infinite-list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.infinite-list {
left: 0;
right: 0;
top: 0;
position: absolute;
}
.infinite-list-item {
padding: 5px;
color: #555;
box-sizing: border-box;
border-bottom: 1px solid #999;
/* height:200px; */
}
</style>
Item组件
<template>
<p>
<span style="color: red">{{ item.id }}</span
>{{ item.value }}
</p>
</template>
export default {
props: {
item: {
type: Object,
default: () => {},
},
},
};
父组件
<template>
<div id="app">
<VirtualList :listData="data" :estimatedItemSize="100" v-slot="slotProps">
<Item :item="slotProps.item" />
</VirtualList>
</div>
</template>
import Item from "./components/Item";
import VirtualList from "./components/VirtualList.vue";
import faker from "faker";
let d = [];
for (let i = 0; i < 1000; i++) {
d.push({ id: i, value: faker.lorem.sentences() });
}
export default {
name: "App",
components: {
VirtualList,
Item,
},
data() {
return {
data: d,
};
},
};
一、 单例模式
二、 策略模式
三、 代理模式
四、 发布订阅模式
五、 迭代器模式
六、 命令模式
七、 组合模式
八、 模版方法模式
九、 享元模式
十、 指责链模式
十一、 中介者模式
十二、 装饰器模式
十三、 状态模式
十四、 适配器模式
十五、 外观模式
一、 预加载的概念
当页面打开图片提前加载,并且缓存在用户本地,需要用到时候直接进行渲染; 在浏览器图片较多的网页,可以有更好的用户体验
二、 预加载的三种方式
var num = 1;
var list = [];
loadImage();
function loadImage(){
var img = new Image(); // 创建一张图片
img.addEventListnener('load', loadHander); // 给这张图片添加事件监听load
img.src='./img/' + num + '.jpg';
}
function loadHeader(){
list.push(thos.cloneNode()); // 加载完成后,将加载进来的图片复制一个新的,放入数组中
num++; // 修改num, 大于100停止执行
if(num > 100) return;
// 给这个图片的地址赋予一个新地址,因为改变图片的地址就会重新出发load, 会继续进入loadHander函数,不断加载,直到加载完成
this.src = './img/' + num + '.jpg';
}
function loadImage(src){
return new Promise(function(resolve, reject){
// promise 中创建图片
const img = new Image();
img.onload = function(){
resolve(img); // 加载时执行resolve函数
}
img.onerror = function(){
reject(src + '地址错误'); // 抛出异常时执行reject函数
}
img.src = src;
})
}
function* fn(){
for(let i = 1; i<100; i++){
// 在执行器函数中一次性执行loadImage函数
yield loadImage('./img/'+ i + '.jpg');
}
}
const s = fn();
let value = s.next().value;
resume();
function resume(){
// 执行一次resume函数, 并在函数里面执行promise在resolve状态下的函数
value.then(img => {
// 反复执行s.next().value, 直到全部图片加载完成
value = s.next().value;
if(value) resume()
})
}
function loadImage(src){
const p = new Promise(function(resolve, reject){
const img = new Image();
img.onload = function(){ resolve(img) };
img.onerror = function(){ reject(src) };
img.src= src;
})
return p;
}
// 使用async表示这个函数是一个异步函数
async function fn(){
const arr = [];
for(let i = 3; i < 80; i++){
// 使用await, 作用就是让异步函数变为同步等待, 异步变成了阻塞式等待
// 当异步全部完成时,再继续向后进行
// async函数中的await后面跟的是promise对象
// async函数执行后返回一个promise对象
await loadImage(`./img/${i}.jpg`).then(img => arr.push(img));
}
return arr
};
fn().then(list => {console.log(list)});
.parent{
position: relative;
}
.children{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.parent{
position: relative;
}
.children{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
.parent{
position: relative;
}
.children{
position: absolute;
top: 50%;
left: 50%;
margin-left: -50px; /*自身height 的一半**/
margin-top: -50px; /*自身width的一半**/
}
.parent{
display: flex;
jusity-content: center;
align-items: center;
}
连接监听
一、 事件
事件是文档或者浏览器窗口中发生的,特定的交互瞬间
事件是由用户或浏览器自身执行的某种动作
事件是JavaScript和DOM之间交互的桥梁
二、事件流
事件流描述的是从页面中接收事件的顺序
事件发生时会在元素节点于根节点之间按照特定顺序的顺序传播,路径所经过的所有节点都会收到该事件,这个传播过程即DOM事件流
事件的传播是从最特定的事件目标到最不特定的事件目标(document对象)。即从DOM树的叶子到根,也就是说事件会从最内层的元素开始发生,一直向上传播,直到document对象。
事件的传播是从最不特定的事件目标(document对象)到最特定的事件目标。即从DOM树的根到叶子,事件会从最外层开始发生,直到最具体的元素。
DOM标准采用捕获 + 冒泡,两种事件流都会触发DOM的所有对象,从document对象开始,也在document对象结束
DOM标准规定事件流包括三个阶段: 事件捕获阶段、处于目标阶段、事件冒泡阶段
事件捕获阶段:实际目标在捕获阶段不会接收事件。也就是在捕获阶段,事件从document到再到就停止了。
处于目标阶段:事件在
冒泡阶段: 事件又传播回文档
用来为一个特定的元素绑定一个事件处理函数,是JavaScript中的常用方法,其传入三个参数,分别是没有on的事件类型、事件处理函数、控制事件阶段,第三个参数是boolean类型,默认是false,表示在事件冒泡阶段调用事件处理函数,传入true就表示在事件捕获的阶段调用事件处理函数。
事件委托是利用事件的冒泡原理来实现的,在元素最外层的元素绑定事件,里面的元素触发事件的时候,都会冒泡到最外层元素,所以都会被触发。这就是事件委托,委托它们父级代为执行事件。
(1) event.stopPropagation()
(2) window.event.cancelBubble = true (谷歌、IE8兼容,火狐不兼容)
(3) 合并取消 return false
在JavaScript的return false只会阻止默认行为,而用jquery的话既阻止了默认行为又防止对象冒泡
一、 变量
可以在任意位置访问的变量
var age = 20;
function a(){ console.log(age) };
a() // 20
函数中用var定义的变量,只能在函数中访问这个变量,函数外部访问不了
function a(){ var age = 20; }
a();
console.log(age); // Uncaught ReferenceError: age is not defined
二、 函数
function a(){
var name = 'test';
function b(){ console.log(name); }
b()
}
a(); // test
var a = 0;
(function(){ console.log(++a) })() // 1
三、 闭包
如果某个函数被它的父函数之外的一个变量引用,就形成了闭包
父对象的所有变量对子对象都是可见的,反之则不成立
所以闭包就是能够读取其他函数内部变量的函数,是将函数内部和函数外部连接起来的桥梁
function a(){
var num = 0;
console.log(++num);
}
a(); // 1
a(); // 1
上面代码都输出了1,为什么呢
因为函数执行完以后,里面的变量(即局部变量)就会销毁,下一次运行又会重新创建那个变量,所以虽然第一次++num了,但是这个变量在第一次执行完后就会被销毁了
怎么样才能确保第一次的变量不被销毁,那么就需要使用闭包了
function a(){
var b = 0;
function c(){
b++;
console.log(b);
}
return c;
}
var res = a();
res(); // 1
res(); // 2
里面的变量的值没有被销毁,因为函数a被外部res引用,所以变量b没有被回收
var bi = (function(){
var a = 0;
function b(){
a++;
console.log(a);
}
return b;
})();
bi(); // 1
bi(); // 2
执行过程分析
function b(){
a++;
console.log(a);
}
没有使用闭包的版本
window.onload = function(){
var ul = document.getElementsByTagName("ul")[0];
var li = ul.getElementsByTagName("li");
for(var i = 0; i < li.length; i++){
li[i].onclick = function(){ console.log(i) }; // 不管点那个都是返回6
}
}
使用了闭包的版本
window.onload = function(){
var ul = document.getElementsByTagName("ul")[0];
var li = ul.getElementsByTagName("li");
for(var i = 0; i < li.length; i++){
(function(i){
li[i].onclick = function(){ console.log(i) }; // 点击第几个就返回第几个
})(i)
}
}
var name = "The Windows";
var object = {
name: "My Object",
getNameFunc: function(){
return function(){ return this.name };
}
}
console.log(object.getNameFunc()()) // The Windows
var name = "The Windows";
var object = {
name: "My Object",
getNameFunc: function(){
var that = this
return function(){ return that.name };
}
}
console.log(object.getNameFunc()()); // My Object
var name = "The Windows";
var object = {
name: "My Object",
getNameFunc: function(){
var that = this
return () => this.name
}
}
console.log(object.getNameFunc()()); // My Object
function foo(x) {
var tmp = 3;
return function f2(y) {
alert(x + y + (++tmp));
};
}
var bar = foo(3); // bar 现在是一个闭包
bar(10); // 17
bar(1); // 9
// 解释arguments
function sum() {
var cache;
if (arguments.length === 1) {
cache = arguments[0];
return function (args) {
return cache + args;
};
} else {
return arguments[0] + arguments[1];
}
}
console.log(sum(1,2)) // 3
console.log(sum(2)(3)) // 5
<button class="btn">按钮</button>
<button class="btn">按钮</button>
<button class="btn">按钮</button>
<button class="btn">按钮</button>
<button class="btn">按钮</button>
<button class="btn">按钮</button>
<button class="btn">按钮</button>
const btnes = document.querySelectorAll('.btn');
for (var i = 0, len = btnes.length; i < len; i++) {
btnes[i].onclick = function () {
console.log(i); // 7
};
}
修改一下
// 使用let声明变量
const btnes = document.querySelectorAll('.btn');
for (let i = 0, len = btnes.length; i < len; i++) {
btnes[i].onclick = function () {
console.log(i);
};
}
// 参数的传递 桥梁
const btnes = document.querySelectorAll('.btn');
for (var i = 0, len = btnes.length; i < len; i++) {
btnes[i].idx = i
btnes[i].onclick = function () {
console.log(this.idx);
}
}
// 使用闭包
const btnes = document.querySelectorAll('.btn');
for (var i = 0, len = btnes.length; i < len; i++) {
(function (j) {
btnes[j].onclick = function () {
console.log(j);
};
})(i);
}
function fnnn(){
var arr = [];
for(var i = 0;i < 5;i ++){
arr[i] = function(){
return i;
}
}
return arr;
}
var list = fnnn();
for(var i = 0,len = list.length;i < len ; i ++){
console.log(list[i]()); // 55555
}
修改一下
function fnnn(){
var arr = [];
for(let i = 0;i < 5;i ++){
arr[i] = function(){
return i;
}
}
return arr;
}
var list = fnnn();
for(var i = 0,len = list.length;i < len ; i ++){
console.log(list[i]()); // 12345
}
或者
function fnnn(){
var arr = [];
for(var i = 0;i < 5;i ++){
arr[i] = ( function(j){
return j;
})(i)
}
return arr;
}
var list = fnnn();
for(var i = 0,len = list.length;i < len ; i ++){
console.log(list[i]); // 12345
}
function fun(n, o) {
console.log(o);
return {
fun: function (m) {
return fun(m, n);
},
};
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3); // undefined,0,0,0
var b = fun(0).fun(1).fun(2).fun(3); // undefined,0,1,2
var c = fun(0).fun(1); c.fun(2); c.fun(3); // undefined,0,1,1
注意:所有声明的匿名函数都是一个新的函数
// 这里的fun函数属于标准具名函数声明,是新创建的函数
function fun(n, o) {
console.log(o);
// 函数的返回值是一个对象字面量表达式,属于一个新的object
return {
// 这个新的对象内部包含一个也叫fun的属性,
// 通过上述介绍可得知,属于匿名函数表达式,
// 即fun这个属性中存放的是一个新创建匿名函数表达式。
// 所以第一个fun函数与第二个fun函数不相同,均为新创建的函数
fun: function (m) {
return fun(m, n);
},
};
}
在函数表达式内部能不能访问存放当前函数的变量
测试一,对象内部的函数表达式
var o = {
fn: function(){
console.log(fn);
}
}
o.fn(); // Uncaught ReferenceError: fn is not defined
测试二,非对象内部的函数表达式
var fn = function(){
console.log(fn);
}
fn(); // function(){ console.log(fn) }
结论是:使用var或是非对象内部的函数表达式内,可以访问到存放当前函数的变量,在对象内部的不能访问到。
原因是:因为函数作用域链的问题,采用var的是在外部创建了fn变量。函数内部当然在内部寻找不到fn后向上的作用域查找,而在创建对象内部时,因为没有在函数作用域内创建fn,所以无法访问。
综上可知,最内层return 出去的fun函数不是第二层fun函数,而是最外层的fun函数
所以三个fun函数的关系就是第一个等于第三个,他们都不等于第二个。
第一行a
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
第一个fun(0)是在调用第一层fun函数。第二个fun(1)是在调用前一个fun的返回值的fun函数,所以第后面几个fun(1),fun(2),fun(3),都是在调用第二层fun函数
所以在第一次调用fun(0)时,o为undefined
第二次调用fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1, 0),所以o为0
第三次调用fun(2)时m为2,但依然是调用a.fun,所以还是闭包了第一次调用时的n,所以内部调用第一层的fun(2, 0); 所以还是0
第四次同理
即得到最终答案为undefined, 0, 0, 0
第二行b
var b = fun(0).fun(1).fun(2).fun(3);
先从fun(0)开始看,肯定是调用的第一层fun函数
而它的返回值是一个对象,所以第二个fun(1)调用的是第二层fun函数,后面几个也是调用的第二层fun函数,后面几个也是调用的第二层fun函数。
所以在第一次调用第一层fun(0)时,o为undefined
第二次调用.fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1, 0);所以o为0;
第三次调用.fun(2)时m为2,此时当前的fun函数不是第一次执行的返回对象,而是第二次执行的返回对象。而在第二次执行第一层fun函数时fun(1,0),所以n=1,o=0,返回时闭包了第二次的n
所以在第三次调用第三层fun函数时,m=2,n=1,即调用第一层fun函数fun(2,1),所以o为1
第四次调用.fun(3)时m为3,闭包了第三次调用的n,同理最终调用第一层fun函数为fun(3,2);所以o为2
即最终答案: undefined, 0, 1, 2
第三行c
var c = fun(0).fun(1); c.fun(2); c.fun(3);
fun(0)为执行第一层fun函数,.fun(1)执行的是fun(0)返回的第二层fun函数,这里语句结束,这里语句结束。
所以c存放的是fun(1)的返回值,而不是fun(0)的返回值。
所以c中的闭包的也是fun(1)第二次执行的n的值。
c.fun(2)执行的是fun(1)返回的第二层函数。
c.fun(3)执行的也是fun(1)返回的第二层fun函数
所以在第一次调用第一层fun(0)时,o为undefined
第二次调用.fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0
第三次调用.fun(2)时m为2,此时fun闭包的是第二次调用的n=1,即m=2,n=1,并在内部调用第一层fun函数fun(2,1);所以o为1;
第四次调用.fun(3)时同理,但依然是调用的第二次的返回值,所以最终调用第一层fun函数fun(3,1),所以o还为1
即最终是undefined, 0, 1, 1
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.