(一) 前置知识
(1) 一些单词
settled:固定的,稳定的,自由自在的
accept:接受
credentials:证书,凭证
expires:期限
preflight:预检系统
expose:揭露,露出
location:定位,为止
stable:稳定的
secure:安全的,保护
certificate:证书
Cipher:密码,暗号
poll:轮询
enumerable:可枚举的
delegation:代理
abstract:抽象的
(二) babel的原理
- babel编译的过程:解析parse => 转换transform => 生成generate
- 解析parse
- @babel/parse:将字符串转成AST,Babylon( 现在是@babel/parser ) 是 Babel 中使用的 JavaScript 解析器
- 解析过程分为两个阶段:
- 语法分析:字符流 -> token流
- 词法分析:token流 -> AST
- 转换transform
- @babel/traverse:主要用来遍历AST
- Babel接收解析得到的AST并通过 ( babel-traverse ) 对其进行 ( 深度优先遍历 )
- 在此遍历过程中对节点进行 ( 添加 )、( 更新 ) 及 ( 移除 ) 操作
- traverse:是遍历的意思,吐槽下百度翻译垃圾得一笔
- @babel/types:主要用来操作AST,比如 ( 添加 )、( 更新 ) 及 ( 移除 ) 操作
- 除了手动替换,可以使用@babel/types更加房便快捷
- @babel/traverse:主要用来遍历AST
- 生成generate
- @babel/generate:用来将转换后得抽象语法树转化为javascript字符串
- 将经过转换的AST通过babel-generator再转换为js代码
- 过程及时深度遍历整个AST,然后构建转换后的代码字符串。
- @babel/generate:用来将转换后得抽象语法树转化为javascript字符串
(三) 手写 webpack 中的 Compiler 类
- Compiler类主要作用就是用来打包
- 源码仓库 - 手写webpack-Compiler
- 我的掘金文章 - 手写webpack-Compiler
- 能获得的知识点:
- AST
- @babel/core @babel/parser @babel/travers @babel/types @babel/generator
- tapable的各种发布订阅模式中的钩子函数
- loader 和 plugin 的原理,以及在webpack中的执行时机和具体过程
- webpack中的配置,不如module-rules-{test, use}
- fs.readFileSync,fs.writeFileSync,process.cwd() 等各种nodejs中的api
(1) 前置知识
- process.cwd()
process.cwd()
返回node进程的 (当前工作目录
)
- fs.readFileSync(path[, options]) --------------- 读文件
- 作用:读path对应的文件内容,返回path对应的内容
- 参数
- path:路径
- optios:配置对象,
注意 {encoding: 'utf8'} 一定要是utf8才能返回源码字符串
- fs.writeFileSync(path, data[, options]) ---------- 写文件
- 参数
- path:文件写到的路径
- data: 源码
- 参数
- path.relative(from, to)
- 根据 ( 当前工作目录 ) 返回 ( from ) 到 ( to ) 的 ( 相对路径 )
- RegExp.prototype.test(string)
- 作用:表示当前模式是否匹配 ( 参数字符串 )
- 返回值:boolean
- 例子:
/cat/.test('cats and dogs') // true
(2) 需要用到得babel插件
- @babel/core 核心文件
- @babel/parser 将源码字符串解析成AST
- @babel/traverse 遍历AST
- @babel/types 操作AST,修改,添加,删除AST
- @babel/generator 将修改后的AST,再转成源码string
(3) Compiler类完整代码
const path = require("path");
const fs = require("fs");
const babelParser = require("@babel/parser");
const babelTraverse = require("@babel/traverse").default;
const babelTypes = require("@babel/types");
const babelGenerator = require("@babel/generator").default;
const ejs = require("ejs");
const { SyncHook } = require("tapable");
// config 是webpack配置文件
const config = require(path.resolve(__dirname, "webpack.config.js"));
class Compiler {
constructor(config) {
this.config = config;
this.entryId = null; // 保存入口文件的路径 './scr/index.js'
this.modules = {}; // 保存所有模块依赖
this.entry = config.entry; // 入口文件的 ( 相对路径 )
this.root = process.cwd(); // node.js进程的当前工作路径
this.hooks = {
entryOption: new SyncHook(),
afterPlugins: new SyncHook(),
run: new SyncHook(),
compile: new SyncHook(),
afterCompile: new SyncHook(),
emit: new SyncHook(),
done: new SyncHook(),
};
let plugins = this.config.plugins
if (Array.isArray(plugins)) {
plugins.forEach(plugin => {
plugin.apply(this) // this是compiler实例对象
})
}
this.hooks.afterPlugins.call()
}
// (一) 获取源码
// - 1. 将引用的每个模块都通过路径,读取其源码,返回源码字符串
// - 2. 需要处理 ( loader ),即通过 ( fs.readFileSync ) 获取模块的源码字符串后,如果有loader,要再用loader来处理各种资源
// getSource
getSource = (moduleAbsolutePath) => {
// moduleAbsolutePath => C:\Users\Administrator\Desktop\7-compiler\src\index.js
// 获取源码
let content = fs.readFileSync(moduleAbsolutePath, { encoding: "utf8" }); // 记得一定要utf8格式才会返回源码字符串,不然可能返回 buffer 类型
const rules = this.config.module.rules;
for (let i = 0; i < rules.length; i++) {
// 遍历 rules 数组,成员是对象
const { test, use } = rules[i];
let backLoaderIndex = use.length - 1;
if (test.test(moduleAbsolutePath)) {
// module -> rules -> { test, use } 匹配每个依赖的绝对路径的话,需要用对应的loader来转化
function runLoader() {
const currentLoader = require(use[backLoaderIndex--]); // a-- 是先赋值整个表达式,然后再 a-1
content = currentLoader(content); // use数组从后往前遍历
if (backLoaderIndex >= 0) {
runLoader(); // 如果use数组中的每个loader都执行过了,就结束递归
}
}
runLoader();
}
}
return content;
};
// (二) 解析
// parse()
// 参数:(1)sourse: 源码字符串 (2)parentPath: 父路径
// 返回值: (1)解析过后的源码字符串 (2)依赖列表
parse = (source, parentPath) => {
// ( 源码string ) => ( AST ) => ( 遍历AST ) => ( 转换AST ) => ( 获取新的源码字符串 )
const dependencies = []; // 依赖数组
// AST
const AST = babelParser.parse(source);
// 遍历
babelTraverse(AST, {
CallExpression(p) {
const node = p.node;
if (node.callee.name === "require") {
node.callee.name = "__webpack_require__";
let modulePath = node.arguments[0].value;
modulePath =
"./" +
path.join(parentPath, modulePath).replace(/\\/g, "/") +
(path.extname(modulePath) ? "" : ".js"); // 后缀存在就加空字符串即不做操作,不存在加.js
dependencies.push(modulePath);
// 转换
node.arguments = [babelTypes.stringLiteral(modulePath)]; // 把AST中的argumtns中的Literal修改掉 => 修改成最新的modulePath
}
},
});
// 生成
const sourceCode = babelGenerator(AST).code;
// 返回
return { sourceCode, dependencies };
};
// buildModules
// 参数 (1)moduleAbsolutePath: 模块的 ( 绝对路径 ) (2)isEntry: 是否是入口文件
buildModules = (moduleAbsolutePath, isEntry) => {
const source = this.getSource(moduleAbsolutePath); // 获取 ( 模块 )的 ( 源码字符串 )
const moduleRelativePath = `./${path
.relative(this.root, moduleAbsolutePath)
.replace(/\\/g, "/")}`; // 获取相对路径,( ./src/index.js )
const parentPath = path.dirname(moduleRelativePath); // 父路径 ( './src/index.js' => './src' )
if (isEntry) {
// 是入口文件,单独保存入口文件的路径
this.entryId = moduleRelativePath;
}
const { sourceCode, dependencies } = this.parse(source, parentPath);
console.log("1111", sourceCode, dependencies);
this.modules[moduleRelativePath] = sourceCode;
if (dependencies.length) {
dependencies.forEach(
(dep) => this.buildModules(path.join(this.root, dep)),
false
); // 递归调用 buildModules
}
};
emitFile = () => {
// 用 modules 对象渲染我们的模板
// 输出到哪些目录下
const mainAbsolutePath = path.join(
this.config.output.path,
this.config.output.filename
); // 输出路径
const templateStr = this.getSource(path.join(__dirname, "main.ejs"));
const code = ejs.render(templateStr, {
entryId: this.entryId,
modules: this.modules,
});
this.assets = {};
// map
// path <=> code
this.assets[mainAbsolutePath] = code;
// 写文件
fs.writeFileSync(mainAbsolutePath, this.assets[mainAbsolutePath]);
};
run() {
this.hooks.run.call()
const entryAbsolutePath = path.resolve(this.root, this.entry);
this.hooks.compile.call() // compile钩子
this.buildModules(entryAbsolutePath, true); // 创建模块的依赖关系
this.hooks.afterCompile.call() // afterCompile钩子
console.log("this.modules :>> ", this.modules);
console.log("this.entryId :>> ", this.entryId);
this.emitFile(); // 反射文件,即打包后的文件
this.hooks.emit.call()
this.hooks.done.call()
}
}
const compiler = new Compiler(config);
compiler.hooks.entryOption.call();
compiler.run();
(四) 跨域
- cors
- nginx
- jsonp
- 前端工程中的 proxy
(1) cors
- cors的全称 - 跨域资源共享 ( Cross-origin resource sharing )
- cors需要浏览器和服务器的共同支持,后端需要实现cors接口
- cors请求的分类:
简单请求simpleRequest
和非简单请求not-so-simple request
- 浏览器对简单请求和非简单请求的处理不一样
1. 简单请求
- 是简单请求有 ( 两个条件 ):
- 请求方法:
HEAD
GET
POST
- HTTP头信息不超过以下字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
:只限于三个值application/x-www-form-urlencoded
、multipart/form-data
、text/plain
- 请求方法:
- 简单请求的基本流程
- 对于简单请求,浏览器直接发出CORS请求,会在头信息中增加 ( Origin ) 字段
- Origin
- Origin字段用来表示:本次请求来自哪个源,即(协议+域名+端口),浏览器根据这个值决定是否同意这次请求
- 如果Origin指定的源 ( 在许可的范围 ),响应头会多出几个头信息字段
Access-Control-Allow-Origin
- ( 必须字段 ),值要么是请求时
Origin
的值,要么是一个*
*
表示接受任意域名的请求- ( Access-Control-Allow-Origin ) 翻译是 ( 访问控制允许同源 )
- ( 必须字段 ),值要么是请求时
Access-Control-Allow-Credentials
- ( 可选字段 ),是一个布尔值,表示是否允许发送 ( Cookie )
- 该字段 ( 只能为true ),表示Cookie可以包含在CORS请求中
- 如果不让在请求中包含Cookie,直接 ( 删除该字段 )
- 默认情况下,Cookie不包括在CORS请求中
- 允许发送Cookie的条件
- 响应头中的 ( Access-Control-Allow-Credentials: true )
- 在XMLHttpRequest请求中设置 ( xhr.withCredentials = true )
- 如果要发送Cookie,则 ( Access-Control-Allow-Origin的值不能为* ),必须指定和请求网页一样的域名
Access-Control-Expose-Headers
- ( 可选字段 ),CORS请求时,XMLHttpRequest 对象的 getResponseHeader() 只能拿到 ( 6 ) 个基本字段
- Cache-Control
- Content-Language
- Content-Type
- Expires
- Last-Modified
- Pragma
- 想拿到除了以上6个字段的其他字段,就必须在Access-Control-Expose-Headers中指定
- ( 可选字段 ),CORS请求时,XMLHttpRequest 对象的 getResponseHeader() 只能拿到 ( 6 ) 个基本字段
Content-Type
(2) 非简单请求
- 非简单请求是对服务器有特殊要求的请求
- 比如请求方法:
PUT
DELETE
- 比如
Content-Type: application/json
- 比如请求方法:
- 非简单请求的Cors请求,会在正式请求之前,增加一次 HTTP 查询请求,称为
预检请求 ( preflight )
- 预检请求
- 预检请求的头信息
Origin
Access-Control-Request-Method
- ( 必须字段 )
- 用来列举HTTP请求会用到哪些请求方法
- PUT DELETE 都属于非简单请求
Access-Control-Request-Headers
- Access-Control-Request-Headers 是一个用 ( 逗号 ) 分割的 ( 字符串 ),指定浏览器会额外发送的头信息字段
- 预检请求的回应
Access-Control-Allow-Methods
- ( 必须字段 ),值是用 ( 逗号分割的字符串 ),表示服务器支持的 ( 所有跨域请求方法 )
Access-Control-Allow-Headers
- 如果请求包括
Access-Control-Request-Headers
,那么Access-Control-Allow-Headers
- 是一个逗号分割的字符串,表示服务器支持的 ( 所有头信息字段 )
- 如果请求包括
Access-Control-Allow-Credentials
Access-Control-Max-Age
- ( 可选字段 ),用来指定本次预检请求的有效期,单位是秒
- 浏览器的正常请求和回应
- 一旦服务器通过了预检请求,以后浏览器正常的CORS请求,就和简单请求一样,请求会有Origin字段,回应会有Access-Control-Allow-Orgin字段
- 预检请求的头信息
(2) nginx
- 设置 (
nginx.conf => http => server => location => proxy_pass
) 字段实现反向代理 - stable:稳定的
- legacy:历史,遗产
http {
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
proxy_pass http://localhost:3000;
}
}
}
上面表示访问:http://localhost:80 会被反向代理到 http://localhost:3000 上
(3) jsonp
- 具有
src
属性的标签,都具有跨域的能力,比如script
img
iframe
link的href属性
jsonp
只能发送get
请求,也就是说jsonp只能跨域发起get请求- jsonp参考资料
- JSONP 的原理
- 允许 ( 客户端 ) 传递一个 (
callback=foo
) 参数给 ( 服务端 ),callback参数对应的值foo是一个 ( 函数 ),然后服务端返回数据时会用这个callback参数对应的值包裹住要返回的 ( JSON ) 数据,客户端就能通过参数获取到数据
- 允许 ( 客户端 ) 传递一个 (
客户端
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script>
// 动态插入 script 标签到 html 中
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.setAttribute('src', src); // 添加src属性
document.getElementsByTagName('head')[0].appendChild(script); // 插入文档
}
// 获取 jsonp 文件,加载完成时,执行addScriptTag => 利用script的src属性发起请求
window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
// 执行本地的 js 逻辑,这个要跟获取到的 jsonp 文件的函数要一致
function foo(data) { // data参数就是服务端返回的数据
console.log('Your public IP address is: ' + data.ip);
};
</script>
</head>
<body>
</body>
</html>
服务端
JSONP看起来与JSON差不多,只不过是被包含在函数调用中的JSON
服务端返回一个函数,包裹住json
例如:
foo({ “name”: “Nicholas” });
注意:
这里的foo要和script中的src中的'http://example.com/ip?callback=foo中的foo保持一致
(五) 浏览器缓存
- 浏览器缓存的分类
- ( 强缓存 ) 和 ( 协商缓存 )
- 通用首部字段
- 请求和响应都能用的字段
- Cache-Control
- 请求首部字段
- If-Modified-Since
- If-None-Match
- 响应首部字段
- Etag
- 实体首部字段
- Expiress
- Last-Modified
(1) 强缓存
- Expires, Cache-Control
- 返回的状态码 200
- network => size => 会显示
from-cache (from-disk-cache),(from-memory-cache)
- 强缓存的实现:通过( Expires ) 或者 ( Cache-Control ) 这两个 ( http response header ) 来实现的,他们用都是用来表示资源在客服端存在的 有效期
1. Expires - 绝对时间点,http1.0
- http1.0提出,响应头中的一个字段,绝对时间,用GMT格式的字符串表示
- 注意:expires是和浏览器本地的时间做对比,是一个绝对时间点,是一个GMT时间
- Expires是优化中最理想的情况,因为它根本不会产生请求,所以后端也就无需考虑查询快慢
- Expires的原理
Expires的原理
1. 浏览器第一次向服务器请求资源,浏览器在请求资源的同时,在responder响应头中加上Expires字段
2. 浏览器在接收到这个资源后,将这个资源和所有response header一起缓存起来
- 所以,缓存命中的请求返回的header并不是来自服务器,而是来自之前缓存的header
3. 浏览器再次请求这个资源时,先从缓存中寻找,找到这个资源后,拿出Expires跟当前的请求时间做比较
- 如果当前请求时间,在Expires指定的时间之前,就能命中强缓存,否则不能
- 注意:Expires是和浏览器本地时间作对比
4. 如果未命中缓存,则浏览器直接从服务器获取资源,并更新response header中的Expires
- expires是较老的强缓存管理header,是服务器返回的一个绝对时间,在服务器时间与客服端时间相差较大时,Expires缓存管理容易出问题(比如:随便修改客户端时间,就能影响命中结果),所以在http1.1中,提出了新的header => Cache-Control,一个相对时间,以秒为单位,用数值表示
2. Cache-Control - 相对时间段,http1.1
- http1.1提出,响应头中的一个字段,相对时间,以秒为单位,用数值表示
- 注意:Cache-Control也是和浏览器本地时间做对比,以秒为单位的时间段
Cache-Control
的相关设置private
:表示该资源仅仅属于发出请求的最终用户,这将禁止中间服务器(如代理服务器)缓存此类资源
,对于包含用户个人信息的文件,可以设置privatepublic
:允许所有服务器缓存该资源max-age
: 123123 // 一个时间段,单位是s- no-cache:使用协商缓存
- no-store:不使用缓存
Cache-Control的原理
1. 浏览器第一次向服务器请求资源,服务器在返回资源的同时,在responder的header中加上Cache-Control字段
2. 浏览器在接收到这个资源后,会将这个资源和所有的response header一起缓存起来
- 所以,缓存命中的请求返回的header并不是来自服务器,而是来自之前缓存的header
3. 浏览器再次请求这个资源时,先从缓存中寻找,找到这个资源后,拿出Cache-Control和当前请求的时间做比较
- 如果当前请求时间,在Cache-Control表示的时间段内,就能命中强缓存,否则不能
4. 如果缓存未命中,则浏览器直接从服务器获取资源,并更新response header中的 Cache-Control
强缓存Expires和Cache-Control总结
- Expires和Cache-Control可以开启一个,也可以同时开启
- 当Expires和Cache-Control同时开启时,Cache-Control优先级高于Expires (
Cache-Control > Expires
) Cache-Control
可以指定private
和public
,表示是否允许中间服务器缓存该资源Expires
是一个用GMT时间表示的时间点
,Cach-Control
是用秒表示的时间段
,都是和浏览器本地时间做对比
(2) 协商缓存
- Last-Modified(If-Modified-Since),ETag(If-None-Match)
- 返回状态码 304 ( not-modified:资源未被修改 )
- 协商缓存的原理:当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的http状态为304,并且会显示一个Not Modified的字符串表示资源未被修改
- modified: 是修改的意思
1. Last-Modified 和 ( If-Modified-Since )
- 响应头:Last-Modified
- 请求头:If-Modified-Since
原理
Last-Modified If-None-Match
-----------
1. 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在response的header加上Last-Modified的header
- 这个header表示这个资源在服务器上的最后修改时间
2. 浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header
- 这个header的值就是上一次请求时返回的Last-Modified的值
3. 服务器再次收到资源请求时,根据浏览器传过来If-Modified-Since和资源在服务器上的最后修改时间判断资源是否有变化
- 如果没有变化则返回304 Not Modified,但是不会返回资源内容;
- 如果有变化,就正常返回资源内容。
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// 当服务器返回304 Not Modified的响应时,response header中不会再添加Last-Modified的header
// 因为既然资源没有变化,那么Last-Modified也就不会改变
4. 浏览器收到304的响应后,就会从缓存中加载资源
5. 如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified Header在重新加载的时候会被更新
- 下次请求时,If-Modified-Since会启用上次返回的Last-Modified值
2. Etag 和 ( If-None-Match )
- 只要资源有变化ETag这个字符串就不一样,和修改时间没有关系,所以很好的补充了Last-Modified的问题
- 响应头:ETag
- 请求头:If-None-Match
原理
Etag 和 If-None-Match
--------
1. 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在response的header加上ETag的header
- 这个header是服务器根据当前请求的资源生成的一个唯一标识,这个唯一标识是一个字符串
- 只要资源有变化这个串就不同,跟最后修改时间没有关系,所以能很好的补充Last-Modified的问题
2. 浏览器再次跟服务器请求这个资源时,在request的header上加上If-None-Match的header,
- 这个header的值就是上一次请求时返回的ETag的值
3. 服务器再次收到资源请求时,根据浏览器传过来If-None-Match然后再根据资源生成一个新的ETag
- 如果没有变化则返回304 Not Modified,但是不会返回资源内容
- 如果有变化,就正常返回资源内容。
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// 与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时
// 由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化
4. 浏览器收到304的响应后,就会从缓存中加载资源。
(3) 强缓存 和 协商缓存 的区别
- 协商缓存跟强缓存不一样,强缓存不发请求到服务器,所以有时候资源更新了浏览器还不知道,但是协商缓存会发请求到服务器,所以资源是否更新,服务器肯定知道。
- 大部分web服务器都默认开启协商缓存,而且是同时启用Last-Modified,If-Modified-Since和ETag、If-None-Match
- Last-Modified,If-Modified-Since和ETag、If-None-Match一般都是同时启用,这是为了处理Last-Modified不可靠的情况
(六) Cookie
- 定义:Cookie是服务器保存在浏览器的一小段文本信息
- 大小:每个cookie的大小不超过 4KB
- 作用:
- 浏览器每次请求都会附带上cookie
- 分辨两个请求是否来之同一个服务器
- 保存一些状态信息
window.navigator.cookieEnabled
返回一个布尔值,表示浏览器是否打开Cookie功能document.cookie
返回当前网页的Cookie- 共享:
- 只要 ( 域名 ) 和 ( 端口 ) 相同,就可以共享cookie
- 不需要 ( 协议 ) 一样,也就是说 A(www.baidu.com) 和 B(www.baidu.com) 可以共享cookie,也就是说A设置的Cookie可以被B访问到
- Cookie有HTTP协议生成,也主要供HTTP消费
- 生成Cookie
- HTTP响应头中设置 ( Set-Cookie ) Set-Cookie:foo=bar
- HTTP响应可以包含 ( 多个 ) Set-Cookie字段
- ( 一个Set-Cookie ) 字段可以设置 ( 多个属性 )
- 修改Cookie
- 修改之前设置的cookie需要满足4个条件:
key,domain,path,secure
都匹配
- 修改之前设置的cookie需要满足4个条件:
- 发送Cookie
- HTTP请求时自动携带
- ( Cookie ) 字段可以包含 ( 多个Cookie ),使用 ( ; ) 分割
- 服务器收到浏览器传来的cookie时,有两点无法知道
- cookie的各种属性,比如过期时间
- 哪个域名设置的cookie,一级域名还是二级域名设置的cokie
- Cookie的属性
- Expires
- 如果不设置Expires或者设置为null,则 Cookie只在当前会话Session有效
- 浏览器是根据对比 ( 本地时间 ),决定Cookie是否过期
- Max-Age
- 从现在开始,Cookie存在的秒数
- 同时执行Max-Age和Expires,优先级Max-Age更高,则Max-Age有效
- 对比 ( 强缓存Expires和Cache-Control:Max-Age )
- Domain
- Domain指定哪些 ( 域名 ) 要携带这个Cookie
- Path
- Path执行哪些 ( 路径 ) 要携带这个Cookie
Secure------只能用在HTTPS
- Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器
HttpOnly------不能通过js获取cookie
- HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到
- Document.cookie属性
- XMLHttpRequest对象
- Request API 都拿不到该属性
- Expires
- document.cookie
- 读cookie:一次性读取所有cookie
- 写cookie:一次只能设置一个cookie,不是覆盖,是添加
- 读写的差异和HTTP通信有关
- 发送请求时:Cookie字段一次将所有的cookie发送
- 响应请求时:Set-Cookie只能一个一个设置Cookie
(七) http和https
(1) https
- http的缺点
- 通信使用 ( 明文 ) 传播,内容可能被 ( 窃听 )
- 不验证通信双方的 ( 身份 ),可能会遭遇 ( 伪装 )
- 无法证明报文的 ( 完整性 ),可能会被 ( 篡改 )
- HTTP使用明文传播,可能会被 ( 窃听 )
- 加密的对象可以分为
- 通信的加密
- HTTP + SSL = HTTPS
- HTTP + 加密 + 认证 + 完整性保护 = HTTPS
- 内容的加密
- 通信的加密
- 加密的对象可以分为
- HTTP不验证通信双方的身份,可能会遭遇 ( 伪装 )
- 任何人都可以发起请求
- 查明对手的证书
- 证书:由值得信任的第三方机构颁发,用来证明 ( 客户端 ) 和 ( 服务端 ) 真实存在
- 伪造证书:从技术角度是非常困难的
- 证明:通过证书,可以确定通信方是意料中的服务器
- HTTP无法证明报文的完整性,可能会被 ( 篡改 )
- 完整性:指信息的准确度,无法证明完整性,通常也说明无法判断 ( 信息是否准确 )
- 接收到的内容可能有误
(1) 对称加密 - 共享密钥加密
- 一把密钥
- 加密和解密用 ( 同一个密钥 ) 的方式叫做,( 对称密钥加密 ) 或者叫 ( 共享密钥加密 )
- 共享密钥加密方式 ( 对称加密 ) 必须把 ( 密钥 ) 发给对方
- 如何解决密钥的发送问题
(2) 非对称加密 - 公开密钥加密
- 两把密钥
- 公开密钥加密方式:使用一对非对称的密钥
- 一把叫做,( 私有密钥 ) private key - 自己知道
- 另一把叫,( 公开密钥 ) public key - 所有人都知道
- 过程:
- ( 发送密文的一方 ) 使用 ( 对方 ) 的 ( 公开密钥 ) 进行 ( 加密处理 ),( 对方 ) 收到被加密的信息后,再使用 ( 自己的私有密钥 ) 进行解密,利用这种方式不需要发送 ( 用来解密的私钥 ),则不必要担心密钥被攻击
(3) HTTPS使用混合加密方式
- HTTPS采用共享加密和公开加密两者组合加密方式进行加密
- 在 ( 交换密钥的过程 ) 使用 ( 公开加密方式 ),在之后的 ( 建立通信,交换报文 ) 阶段使用 ( 共享加密方式 )
- 公开密钥的真实性?????????
- 数字证书认证机构
(4) 数字证书认证机构的业务流程
- 前置知识:
- 服务器有一个密钥:一个公钥,一个私钥
- 证书颁发机构也有一对密钥:一个公钥,一个私钥,公钥是提前内置在浏览器中的
- 证书颁发机构用 (自己的私钥) 对 ( 服务器的公钥 ) 进行加密,做数字签名,并生成公钥证书
具体流程
-
- 服务器把自己的 ( 公钥 ) 向证书认证机构申请证书
-
- 证书颁发机构用自己的 ( 私钥 ) 对服务器的 ( 公钥 ) 进行数字签名,并生成 ( 公钥证书 )
-
- ( 服务器 ) 向 ( 客服端 ) 发送证书颁发机构颁发的 ( 公钥证书 )
-
- ( 客服端 ) 收到公钥证书后,利用内置在自己的 ( 证书颁发机构的公钥 ) 解密 (公钥证书中,证明服务器的公钥的真实性 )
-
- 如果是真实的服务的公钥证书,那么 ( 客户端就会用服务器的公钥加密之后在对称加密才会用到的密钥 ) 并发送给服务器
-
- 服务器收到 ( 加密后的信息后 ) 用自己的私钥 ( 解密 ),解密后服务端就获取到了 ( 对称加密的密钥了 )
-
- 接下来,通信双发就可以进行 ( 对称加密通信了 ),即可以建立通信,交换报文了
- 接下来,通信双发就可以进行 ( 对称加密通信了 ),即可以建立通信,交换报文了
-
(5) HTTPS的安全通信机制
- cipher:密码,暗号
- spec:规格
- 1.客户端:发送 Client Hello 报文,开始ssl通信
- 2.服务器:可以ssl通信时,Server Hello 报文进行应答
- 3.服务器:发送 Certificate 报文,报文中包含 ( 公钥证书 )
- 4.服务器:发送 Server Hello Done 通知客服端,最初阶段的 SSL 握手协商部分结束
- 5.客户端:SSL第一次握手结束后,客户端以 Client Key Exchange 报文作为回应,报文中包含通信加密中使用的一种被称为Pre-mastersecret 的随机密码串。该报文已用步骤 3 中的公开密钥进行加密
- 6.客户端: 接着客户端继续发送 Change Cipher Spec 报文,该报文会提示服务器,在此报文之后的通信会采用 Pre-master secret 密钥加密
- 7.客户端: 客户端发送 Finished 报文。该报文包含连接至今全部报文的整体校验值。这次握手协商是否能够成功,要以服务器是否能够正确解密该报文作为判定标准
- 8.服务器:服务器同样发送 Change Cipher Spec 报文
- 9.服务器:服务器同样发送 Finished 报文
- 10.客户端:服务器和客户端的 Finished 报文交换完毕之后,SSL 连接就算建立完成。当然,通信会受到 SSL 的保护。从此处开始进行应用层协议的通信,即发送 HTTP 请求
- 11.服务端: 应用层协议通信,即发送 HTTP 响应
- 12.客户端:最后由客户端断开连接。断开连接时,发送 close_notify 报文
(2) HTTP
(1) 三次握手
三次握手:指的是建立tcp连接时,需要客户端和服务端发送的三个包
- 第一次握手
- ( 客户端 ) 发送一个 ( 标志位SYN=1, 序号Seq=x ) 的 ( 连接包 ) 到 ( 服务器 )
标志位 SYN=1 表示新建连接
- 序号 Seq 是随机的,这里随便设置为 Seq=x
客户端状态:由 CLOSED状态 => SYN_SENT状态
- ( 客户端 ) 发送一个 ( 标志位SYN=1, 序号Seq=x ) 的 ( 连接包 ) 到 ( 服务器 )
- 第二次握手
- ( 服务器 ) 发送一个 ( 标志位SYN=1, ACK=1, 序号Seq=y, 确认号Ack=x+1 ) 的 ( 确认包 ) 给 ( 客户端 )
标志位 ACK=1 表示确认序号有效
- Ack = Seq + 1 ( 确认号 = 序号 + 1 )
服务器状态:由 COLSED状态 => SYN_RCVD状态
- ( 服务器 ) 发送一个 ( 标志位SYN=1, ACK=1, 序号Seq=y, 确认号Ack=x+1 ) 的 ( 确认包 ) 给 ( 客户端 )
- 第三次握手
- ( 客户端 ) 发送一个 ( 标志位ACK=1, 序号Seq=x+1, 确认号Ack=y+1 ) 的 ( 确认包 ) 给 ( 服务器 )
服务器和客户端的状态:都变成 ESTABLISHED 状态,表示已连接
- established:是建立的意思
- ( 客户端 ) 发送一个 ( 标志位ACK=1, 序号Seq=x+1, 确认号Ack=y+1 ) 的 ( 确认包 ) 给 ( 服务器 )
TCP建立链接 - 为什么需要第三次握手
为什么只有三次握手,才能确认双方的发送和接收能力是否正常,而两次却不可以??
- 第一次握手:
- 客户端发送连接包,服务端收到了
- ( 服务端 ) 就能得出结论:( 客户端的发送能力,服务端的接收能力是正常的 )
- 第二次握手
- 服务端发送确认包,客服端收到了
- ( 客户端 ) 就能得出结论:( 服务端的接收、发送能力,客户端的接收、发送能力是正常的 )
- 注意:
此时服务端并不能确认客户端的接收能力是否正常
- 第三次握手
- 客户端发送确认包,服务端收到了
- ( 服务端 ) 就能得出结论:( 客户端的接收、发送能力,和服务端的接收、发送能力都是正常的 )
- 总结为什么需要三次握手:
- 为了实现可靠数据传输, TCP 协议的通信双方都必须维护一个序列号, 标识发送出去的数据包哪些已经被对方收到
- 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤
- 如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认,也就是上面每次握手分析的,如果两次握手,服务端是没法确认客户端的接收能力是正常的
- 防止已失效的连接请求又传送到服务器端,因而产生错误
(2) 四次挥手
- 第一次挥手
- ( 客户端 ) 发送一个 ( 标志位FIN=1, 序号Seq=u ) 的 ( 释放包 ) 到 ( 服务器 )
- 标志位 FIN=1 表示释放连接
客户端状态:由 ESTABLISHED状态 => FIN_WAIT1状态
- 表明的是:主动方(客户端)的报文发送完了,但是主动方(客户端)还是可以 ( 接收报文 )
- ( 客户端 ) 发送一个 ( 标志位FIN=1, 序号Seq=u ) 的 ( 释放包 ) 到 ( 服务器 )
- 第二次挥手
- ( 服务器 ) 返回一个 ( 标志位ACK=1, 序号Seq=v, 确认号Ack=u+1 ) 的 ( 确认包 ) 到 ( 客户端 )
服务器状态:由 ESTABLISHED状态 => COLSE_WAIT状态
- 第三次挥手
- ( 服务器 ) 发送一个 ( 标志位FIN=1, ACK=1, 序号Seq=w, 确认号Ack=u+1 ) 的 ( 释放包 ) 到 ( 客户端 )
服务器状态:由 COLSE_WAIT状态 => LAST_ACK状态
- 表明的时:主动方(服务器)的报文发送完了,但是主动方(服务器)还可以 ( 接收报文 )
- 第四次挥手
- ( 客户端 ) 发送一个 ( 标志位ACK=1, 序号Seq=u+1, 确认号Ack=w+1 ) 的 ( 确认包 ) 到 ( 服务器 )
客户端状态:由 FIN_WAIT2状态 => TIME_WAIT状态
- 注意:
- 客户端通常是主动关闭,进入TIME_WAIT状态
- 服务端通常是被动关闭,不会进入TIME_WAIT状态
TIME_WAIT状态 - 2MSL状态
- TIME_WAIT状态也称为2MSL等待状态
- 每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。
为什么第四次挥手后,客户端要进入TIME_WAIT状态,而不是直接关闭
- 因为要确保服务器是否收到了Ack确认报文,即在2MSL时间内没有再收到服务端的FIN报文,证明已经收到ACK
- 如果没有收到的话,服务器会重新发 FIN 报文给客户端,客户端再次收到 ACK 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文。
- TIME_WAIT 持续的时间至少是一个报文的来回时间。一般会设置一个计时,如果过了这个计时没有再次收到 FIN 报文,则代表对方成功就是 ACK 报文,此时处于 CLOSED 状态。
(3) HTTP1.0 和 HTTP1.1 的区别
(1) HTTP1.0
无状态
:服务器不跟踪,不记录请求过的状态无连接
:浏览器每次请求,都要重新建立连接
HTTP1.0
(1) 无状态
1. 服务器不跟踪记录请求过的状态
2. 对于无状态的特性可以借助 ( cookie/session ) 机制来做 ( 身份认证 ) 和 ( 状态记录 )
(2) 无连接
- 无连接导致的性能缺陷主要有两种:
- 无法复用链接
- 每次发送请求,都需要进行tcp链接,即三次握手和四次挥手,使得网络的利用率极低
- 对头阻塞
- http1.0规定,在前一个请求响应到达之后,下一个请求才能发送,如何前一个请求阻塞,后面的就都会阻塞
(2) HTTP1.1
HTTP1.1解决HTTP1.O的性能缺陷
- 长连接:新增
Connection
字段,可以设置keep-alive
使保持链接不断开 - 管道化:基于长连接,管道化可以不等第一个请求响应,继续发送后面的请求,但是响应的顺序还是要按请求的顺序返回
- 缓存处理:新增
cache-control
字段Max-Age, public private
- 断点续传
HTTP1.1
(1) 长连接
- HTTP1.1默认保持长连接,数据传输完成,保持tcp链接不断开,继续使用这个通道传输数据
(2) 管道化
- http1.0
- 请求1 > 响应1 --> 请求2 > 响应2 --> 请求3 > 响应3
- http1.1
- 请求1 --> 请求2 --> 请求3 > 响应1 --> 响应2 --> 响应3
- 虽然管道化一次可以发送多个请求,但响应仍然是顺序返回,仍然无法解决对头阻塞的问题
(3) 缓存处理
- HTTP1.1新增 Cache-Control 字段
- http1.0 => expires => 是一个绝对时间点,用GMT时间格式
- http1.1 => Cache-Control => 是一个相时时间段,以秒为单位
- Cache-control: no-cache,private,max-age=123123
- no-cache:不使用强缓存,使用协商缓存
- max-age: 一个时间段,单位是秒
- public:允许所有服务器缓存该资源
- private:表示该资源仅仅属于发出请求的最终用户,这将禁止中间服务器(如代理服务器)缓存此类资源
对于包含用户个人信息的文件,可以设置private
- Expires 和 Cache-Control 对比
- 如果同时开启,Cache-Control 的优先级高于 Expires
- expires是一个用GMT时间表示的时间点,Cach-Control是用秒表示的时间段,都是和浏览器本地时间做对比
- Cache-Control 比 Expires 更加精确
(4) 断点续传
- 请求头:Range
- 响应头:Content-Range
- 原理
- 在上传/下载资源时,如果资源过大,将其分割为多个部分,分别上传/下载
- 如果遇到网络故障,可以从已经上传/下载好的地方继续请求,不用从头开始,提高效率
(4) HTTP2.0
- 二进制分帧
- 多路复用:在共享TCP链接的基础上同时发送请求和响应
- 头部压缩
- 服务器推送:服务器可以额外的向客户端推送资源,而无需客户端明确的请求
HTTP2.0
(1) 二进制分帧
- 将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码
(2) 多路复用
- 基于二进制分帧
- 在同一域名下所有访问都是从同一个tcp连接中走,http消息被分解为独立的帧,乱序发送,服务端根据标识符和首部将消息重新组装起来
(5) TCP 和 UDP 的区别
- TCP面向连接,可靠性高,但传输效率低,传输慢
- UDP无连接,可靠性低,但传输效率高,传输快
(6) url到页面显示的过程
url到页面显示的过程
1. DNS域名解析
- DNS是 ( domain name system ) 域名系统的缩写
- 将域名解析成ip地址
- 一个域名对应一个以上的ip地址
- 为什么要将域名解析成ip地址?
- 因 ( 为TCP/IP网络 ) 是通过 ( ip地址 ) 来确定 ( 通信对象 ),不知道ip就无法将消息发送给对方
- DNS域名解析的过程:// 递归查询和迭代查询
1. ( 浏览器 ) 中查询 DNS 缓存,有则进入建立tcp链接阶段,下面同理
2. ( 本机的系统 )中查询 DNS 缓存
3. ( 路由器 ) 中查询 DNS 缓存
4. ( 运营商服务器 ) 中查询 DNS 缓存
5. 递归查询 // 根域名/一级域名/二级域名 ....blog.baidu.com
- .com
- .baidu
- blog
- 还未找到就报错
- DNS域名解析优化 // 2023-05-16
- 当第一次访问结束后,会 ( 缓存 ) ( 域名和IP的映射 )
- 但是一个项目足够大时,可能 ( img的src是不同的域名的url ) ( style的link是不同域名的url ),这些都是要做 DNS域名 解析的
- 1. DNS预解析
- 1. meta标签: 用meta信息来告知浏览器, 当前页面要做DNS预解析 <meta http-equiv="x-dns-prefetch-control" content="on" />
- 2. link标签: 在页面 header 中使用 link 标签来强制对 DNS 预解析 <link rel="dns-prefetch" href="http://..." />
- 2. DNS缓存优化
- 1. 加本地 DNS 缓存的大小
- 2. 优化本地 DNS 缓存的清理策略
- 3. CDN域名加速
- CDN服务缩短了用户查看内容的访问延迟
- 将静态资源通过 CDN 进行加速,减少IP地址切换带来的影响,解决了网络带宽小、用户访问量大、网点分布不均等问题
2. 建立tcp链接 // 三次握手
- 第一次握手
- 客服端发送一个 标志位SYN=1,序号Seq=x的链接包给服务端
- SYN:表示发起一个新链接,( Synchronize Sequence Numbers )
- Seq:序号是随机的
- 第二次握手
- 服务端发送一个 标志位SYN=1,ACK=1,确认号Ack=x+1,序号Seq=y的确认包给客户端
- 标志位 ACK 表示响应
- 第三次握手
- 客户端发送一个 SYN=0,ACK=1,确认号Ack=y+1,序号Seq=x+1的确认包给服务器
- 为什么需要三次握手
- 之所以要第三次握手,主要是因为避免无效的连接包延时后又发送到服务器,造成服务器以为又要建立链接的假象,造成错误
3. 客户端发送http请求
4. 服务端处理请求,并返回http响应报文
5. 浏览器解析渲染
- 遇见HTML标记,浏览器调用HTML解析器,解析成Token并构建DOM树
- 遇见style/link标记,浏览器调用css解析器,解析成CSSOM树
- 遇见script标记,浏览器调用js解析器,处理js代码(绑定事件,可能会修改DOM tree 和 CSSOM tree)
- 将DOM 和 CSSOM 合并成 render tree
- 根据render tree计算布局(布局)
- 将各个节点的颜色绘制到屏幕上(渲染)
6. 断开TCP链接 // 四次挥手,( FIN : 表示释放链接 )
- 第一次挥手:浏览器发起,告诉服务器我请求报文发送完了,你准备关闭吧
- 第二次挥手:服务器发起,告诉浏览器我请求报文接收完了,我准备关闭了,你也准备吧
- 第三次挥手:服务器发起,告诉浏览器,我响应报文发送完了,你准备关闭吧
- 第四次挥手:浏览器发起,告诉服务器,我响应报文接收完了,我准备关闭了,你也准备吧
- 先是服务器先关闭,再是浏览器关闭
(八) nodejs 事件轮询机制
nodejs事件轮询机制一共分为 6 个阶段
(1) timers 定时器阶段
- ( 计时 ) 和 ( 执行到点的定时器回调函数 )
(2) pending callbacks
- 某些系统操作(tcp错误类型)的回调函数
(3) idle, prepare
- 准备工作
(4) poll 轮询阶段,是一个轮询队列
- 1. 如果轮询队列不为空,就依次取出执行,直到轮询队列为空或者达到系统最大限制
- 2. 如果轮询队列为空
- 1. 如果之前设置过 ( setImmediate ) 函数,则直接进入下一个阶段即 ( 进入check阶段 )
- 2. 如果之前没有设置过setImmediate函数,就会在当前poll阶段 ( 等待 ),
- 直到 ( 轮询队列 ) 添加进了新的回调函数,那么就会进入4阶段1的判断,继续执行
- 或者,( 定时器 ) 到点了,也会去下一个 ( check阶段 )
(5) check 查阶段
- 执行 setImmediate 回调函数
(6) close callbacks 关闭阶段
- 执行 close 事件回调函数
注意点:
- process.nextTick可以在任意阶段优先执行
- nodejs时间循环先后顺序案例
// setImmediate要在第4个阶段,poll阶段清空轮询队列后,之前设置过setImmediate函数,就就如第5个阶段,执行setImmediate回调
setImmediate(function() {
console.log('setImmediate')
})
setTimeout(function() { // nodejs的第一个阶段就是timers阶段,计时和执行到定时器,这里是0,到点,会立即执行
console.log('setTimeout')
}, 0)
process.nextTick(function() { // process.nextTick() 可以在任意阶段优先执行
console.log('process.nextTick')
})
// 所以输出顺序是
// process.nextTick() => setTimeout(0) => setImmediate()
(九) ES6复习
(一) Promise
(1) promise的特点
- 对象的状态不受外界影响
- promise对象代表异步操作,有三种状态
pending进行中
fulfilled已成功
rejected已失败
- 只有异步操作的结果可以决定是哪一种状态,任何其他操作都无法改变这个状态
- promise对象代表异步操作,有三种状态
- 状态一旦改变就不会再变,任何时候都可以得到这个结果
- 状态的改变只有两种可能:从
pending变为fulfilled
和从pending变为rejected
- promise和事件的区别?
- 事件:事件错过了再去监听,得不到想要得结果
- promise:如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果
- 状态的改变只有两种可能:从
(2) promise的优点和缺点
- 优点:
- promise可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
- Promise对象提供统一的接口,使得控制异步操作更加容易
- 缺点:
- promise无法取消,一旦新建就会立即执行,中途无法取消
- promise内部抛出的错误,不会反映到外部,需要设置回调函数去接收。也不会终止程序的执行
- promise处于pending状态时,无法得知promise当前发展到哪一个阶段,是刚刚开始还是即将完成
(3) 基本用法
- Promise是一个构造函数,用来生成promise实例
- (
Promise构造函数
),接受一个 (函数
) 作为参数,该参数函数又有两个参数 (resolve函数
) 和 (reject函数
)- resolve函数的作用
- 将promise的状态从 pending 变为 resolved(fulfilled)
- 在异步操作成功时调用
- 并将 ( 异步操作的结果 ) 作为参数传递出去
- reject函数的作用
- 将promise的状态从 pending 变为 rejected
- 在异步操作失败时调用
- 并将 ( 异步操作抛出的错误 ) 作为参数传递出去
- reject函数的参数,通常时 Error 对象的实例,表示抛出的错误
- 注意
- 调用resolve()和reject()并不会终结promise参数函数的执行,一般情况下resolve()和reject()应该是promise函数函数的结束
- 所以为了避免一些错误的发生,使用 return resolve() 和 return reject()
- then
- ( promise实例生成后 ),可以分别用 ( then ) 方法指定 ( resolved状态 ) 和 ( rejected状态 ) 的 ( 回调函数 )
- then方法的参数
- then方法接受两个 ( 回调函数作为参数 )
- 第一个参数,在promise状态变成resolved时被调用,该参数函数的参数是 (promsie对象传出的值,即resolve的参数)
- 第二个参数,在promise状态变成rejected时被调用,该参数函数的参数是 (promise对象传出的值,即reject的参数
- 第二个参数可选
- resolve函数的作用
(4) Promise.prototype.then()
- then的作用
- 为promise对象添加状态改变后的回调
- then的返回值
- ( then ) 方法 ( 返回 ) 的是一个 ( 新的promise实例对象 )
- 可以 ( 链式 ) 调用
(5) Promise.prototype.catch()
- ( catch ) 方法是 .then(undefined, rejection) 或 .then(null, rejection) 的 ( 别名 )
- ( promise回调内部的错误 ) 和 ( then方法中的错误 ) 都会被 ( .catch ) 所捕获
- 如果 promise 状态已经变成了 resolved,再抛出错误是无效的
- promise对象的错误具有冒泡性值,会一直向后传递,直到被捕获为止
- 一般,不要在 then() 方法里面定义react状态的回调,多使用 catch(),因为catch更接近同步写法,同时能捕获then中的方法
- catch的返回值
- ( catch ) 方法 ( 返回 ) 的是一个 ( 新的promise实例对象 )
- 可以 ( 链式 ) 调用
(6) Promise.prototype.finally()
- finally()方法用于指定不管Promise对象最后的状态如何,都会执行的操作
- finally()函数,不接受任何参数,这即意味着没有办法知道前面的promise的状态是fulfilled还是rejected,即finally()方法的操作与状态无关,不依赖promise执行的结果
- finally()总是返回原来的值
(7) Promise.all() ---- 对比Promise.any()
- Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例
- 参数
- 是一个 ( 数组 ),成员都是 ( Promise实例 )
- 如果 ( 不是promise实例 ),会先调用 ( Promise.resolve ) 将参数 ( 转为promise实例 )
- 可以不是数组,但是必须具有 Iterator 接口,且返回的每个成员都是 promise 实例
- 返回值状态
- 都fulfilled,整个状态才会变成fulfilled
- 一个reject,整个状态变成rejected
- 注意点
- 如果Promise.all()参数数组成员promise对象中自己定义了catctch方法,那么它一旦被rejected,并不会触发Promise.all的catch方法
(8) Promise.race()
- Promise.race()谁先改变,整个状态就发生改变
- 率先改变的promise对象的返回值,会传递给Promise.race()返回函数的参数
(9) Promise.allSettled()
- 等到所有参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束
(10) Promise.any() ---- 对比Promise.all()
- 只要参数实例有一个变成fulfilled,包装实例就会变成fulfilled'
- 所有参数实例都变成rejected,包装实例就会变成 reject
(11) Promise.resolve()
- 将现有对象转成promise对象
- 分为几种情况
- 参数是一个promise实例 => 不做任何修改,原封不动得返回这个实例
- 参数是一个 thenable 对象 => 会将thenable对象转成promise对象,然后立即执行thenable对象中得then方法
- 参数不是 thenable对象或者根本不是对象 => 返回一个新得promise对象
- 不带任何参数 => 直接返回一个带有 resolved 状态得promise对象
(12) Promise.reject()
- 返回一个rejected状态的promise实例
(13) Promise.try()
- 让同步代码同步执行,异步代码异步执行
promise案例
- 前置知识:
- 1.队列是先进先出
- 2.event loop
- 3.宏任务 微任务
- 案例 - 宏任务和微任务的相互嵌套
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
console.log(1)
// A promise
new Promise((resolve) => {
console.log(2)
resolve()
console.log(3)
}).then(res => { // E then
console.log(4)
// C 定时器
setTimeout(() => console.log(5))
})
console.log(6)
// B 定时器
setTimeout(() => {
console.log(7)
// D promise
new Promise((resolve) => {
console.log(8)
resolve()
}).then(() => console.log(9)) // F then
})
console.log(10)
/**
* 第一轮 Event loop
* 1 => 同步任务,进入函数调用栈,立即执行
* A => A的回调立即执行
* 2 => 同步任务,立即执行
* E => 微任务,进入微任务队列
* 3 => 同步任务,立即执行
* 6 => 同步任务,立即执行
* B => 宏任务,B的回调进入宏任务队列
* 10 => 同步任务,立即执行
* 此时执行情况如下:
* 输出:1,2,3,6,10
* 微任务:[E]
* 宏任务:[B]
*
* 第二轮 Event loop
* 清空微任务队列,取出宏任务队列的第一个成员
* E => 4 同步任务,立即执行
* C 宏任务,进入宏任务队列,此时的宏任务队列 [B, C]
* B => 7 同步任务,立即执行
* D promise的回调立即执行
* => 8 同步任务,立即执行
* => F 微任务,进入微任务队列,此时的微任务队列 [F]
* 此时执行情况如下:
* 输出:4,7,8
* 微任务:[F]
* 宏任务:[C]
*
* 第三轮 Event loop
* 清空微任务队列,取出宏任务队列的第一个成员
* F => 9 同步任务,立即执行
* C => 5 同步任务,立即执行
*
* 总的输出顺序:1,2,3,6,10,4,7,8,9,5
*/
</script>
</body>
</html>
(二) let const var
- let const 是块级作用域
- for循环中使用let声明变量,那么i只在for循环体内有效
(1) for循环 ( 循环变量 ) 和 ( 循环体 ) 分别是不同的作用域
- 循环变量部分所在的作用域是父作用域,循环体是单独的子作用域
for(let i = 0; i<3; i++) {
let i = 10;
console.log(10)
}
// 10
// 10
// 10
会输出3个10,说明 ( 循环体中的变量i ) 和 ( 循环变量中的i ) 分别在不同的 ( 作用域 )
(2) let const 不存在变量提升
console.log('a', a)
console.log('b', b)
var a = 1
let b = 10
// undefined
// Uncaught ReferenceError: Cannot access 'b' before initialization
(3) 暂时性死区
- 在代码块内,使用let命令声明变量之前,该变量都是不可用的,这在语法上成为 (暂时性死区TDZ)
var a = 10
if (true) {
a = 100 // Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 20
}
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
- 暂时性死区也意味着,
typeof
不再是一个百分百安全的操作 - 注意:
如果一个变量根本没有声明,使用 typeof 反而不会报错,而是会返回 undefined
- 注意:
在没有引入let关键字之前,使用typeof是百分百安全的,但是有了let之后,这种操作不再变得安全
typeof x; // ReferenceError
let x;
// 在 let 声明变量 x 之前,都是 x 的死区,使用变量 x 就会报错
typeof undeclared_variable // "undefined"
// 如果一个变量根本没有声明,使用 typeof 反而不会报错,而是会返回 undefined
- 一些比较隐蔽的死区
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
分析:x的默认值是y,x赋值默认值时,y还没有声明就使用到了y,是y的死区,所以报错
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
// 分析:y的默认值是x,y赋值默认值时,x已经声明并赋值了,所以不会报错
var x = x; // 不报错 undefined
let x = x; // 报错 ReferenceError: x is not defined
// 在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“,是x的死区
(4) 不允许重复声明
- let不允许在, ( 相同作用域内 ),( 重复声明 ) ( 同一个变量 )
// 报错
function func() {
let a = 10; // 不允许重复声明,等价于:var a = undefined; let a = 10; a = 1
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1; // let不允许在相同作用域内,重复声明同一个变量
}
function func(arg) {
let arg; // 报错,因为等价于:var arg = undefined; let arg
}
func() // 报错
function func(arg) {
// 等价于:var arg = undefined;
{
let arg; // 不报错,因为在不同的作用域内
}
}
func() // 不报错
(5) 块级作用域
- 为什么要使用块级作用域
- ( es5 ) 中只有 ( 全局作用域 ) 和 ( 函数作用域 ) 因此造成了以下问题
- 内层变量可能会覆盖外层变量
- 用来计数的循环变量,泄漏为全局变量
- ( es5 ) 中只有 ( 全局作用域 ) 和 ( 函数作用域 ) 因此造成了以下问题
(1) 变量覆盖
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
上面等价于:
var tmp = new Date();
function f() {
var tmp = undefined; // 变量提升
console.log(tmp, '1') // undefined
if(false) {
tmp = 'hello world' // 根本没有机会执行
}
console.log(tmp, '2') // undefined
}
console.log(tmp, '3') // 时间
f()
console.log(tmp, '4') // 时间
(2) 用来计数的循环变量,泄漏为全局变量
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5,因为var声明的变量i存在变量提升,提升到父级作用域,for循环执行完后,全局只有一个i值为5
- 块级作用域,声明同一个变量时, ( 外层代码 ) 不受 ( 内层代码 ) 的影响
- 块级作用域 ( 内层作用域 ) 可以访问 ( 外层作用域的变量 )
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
- 块级作用于的出现,实际上使得 IIFE 匿名立即执行函数不再必要了
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
(6) 块级作用域与函数声明
- ES5规定,函数只能在顶层作用域和函数作用域中声明,不能在块级作用域中声明
ES6中,明确规定函数可以在块级作用域中声明
(7) const 命令
- const一旦声明变量,就必须立即初始化,必须立即赋值
- const声明,只在所在的块级作用域内有效
- const声明的常量也不提升,也存在暂时性死区
- const一样不能重复声明
const保证的是指向值得指针不能改变,并不能保证指针指向得数据不能改变
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
(8) ES6 声明变量的 6 种方法
var
function
let
const
import
class
(9) 顶层对象的属性
- ES5中,( 顶层对象的属性 ) 和 ( 全局变量 ) 挂钩相等
let const 声明的全局变量不属于顶层对象的属性
var xx = 1;
let yy = 2;
window.xx // 1
window.yy // undefined
说明:let const import class 声明的全局变量,不再是顶层对象的属性
(10) globalThis
- 统一的顶层对象
const let var 总结
- const let 块级作用域
- const let 不存在变量提升
- const let 存在暂时性死区
- const let 不能重复声明
- const let 声明的全局变量,不再是顶层对象的属性,即和window/global脱离关系
- const常量,声明后必须立即赋值,只能保证指向数据的指针不变,不能保证数据本身不可修改
- let变量
- ES6声明变量的6种方式:var const let function class import
(三) Set
- Set类似数组,但是成员的值唯一
- Set本身就是一个构造函数,用来生成Set数据结构
- 参数
- Set接受一个 ( 数组 ),或者具有 iterable 接口的其他数据结构,作为参数
- 注意点
- Set会认为
NaN和NaN相等
两个对象总不相等
- Array.from()可以把Set结构转成数组
- Set会认为
- set实例的属性和方法
- Set.prototype.constructor:构造函数,默认就是Set函数
- Set.prototype.size:返回Set实例的成员总数
- Set.prototype.
add
(value):添加某个值,返回 Set 结构本身,可以链式调用
- Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功
- Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员
- Set.prototype.clear():清除所有成员,没有返回值
- Set.prototype.keys():返回键名的遍历器
- Set.prototype.values():返回键值的遍历器
- Set.prototype.entries():返回键值对的遍历器
- Set.prototype.forEach():使用回调函数遍历每个成员
- 应用
- 数组去重
[...new Set([a,a,b,c])]
// [a,b,c] - 去除重复字符串
[... Set('aabbcdef')].join('')
// 'abcedf'
- 数组去重
(四) Map
- Map类似于对象,但是键不限于字符串,可以是各种类型
- Map是真正意义上的值值对应
- 参数
- Map的参数可以是一个数组,( 参数数组 ) 的每个成员 ( 是一个表示键值对的数组 )
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
---
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
(五) parseInt
- parseInt(string, radix) 解析string字符串,并返回指定基数radix的十进制数,radix在2-36之间
- 参数
- string:被解析的值,不是字符串会被转化为字符串
- radix:基数,值在2-36之间
- 返回值
- ( 数字 ) 或者 ( NaN )
一道面试题
['1', '2', '3'].map(parseInt)
相当于
['1', '2', '3'].map((value, index, arr) => parseInt(value, index))
相当于
parseInt('1', 0) => 1 , 当radix是0时,radix会被当成10进制来处理
parseInt('2', 1) => NaN , 1进制不可能有2
parseInt('3', 2) => NaN ,2进制不可能有3,最大是2
最终结果
[1, NaN, NaN]
--------------
[1, 2, 3].map(Number)
相当于
['1', '2', '3'].map((value, index, arr) => Number(value))
最终
[1, 2, 3]
(6) 箭头函数
- 注意点 ( 一共有 4 点 )
- 箭头函数中的this,是父级作用域中的this,即定义时所在的对象,即 ( 外部代码块中的this )
- 因为箭头函数根本没有自己的this,而是外层代码块中的this
- 对比:
箭头函数中的this,始终固定函数在 ( 定义 ) 时所在的对象,和 ( 函数本身的作用域 ) 保持一致了
普通函数中的this,对象中的方法(非箭头函数写法)都需要在 ( 运行 ) 时确定指向
- 箭头函数不能作为 ( 构造函数 ),即不能使用 ( new命令 )
- 因为没有自己的this,因此不能作为构造函数
- 箭头函数不能使用 ( arguments ) 对象,可以用 ( rest ) 参数代替
- 不是不能使用arguments对象,而是父级作用域的arguments
- 父级上下文中的this是对象或者window时,不存在arguments就会报错
- 父级上下文中的this是函数时,arguments就是父级函数的arguments
- 箭头函数不可以使用 ( yield ) 命令,即箭头函数不能用作 ( Generator ) 函数
- 除了this,箭头函数中也不存在 arguments,super,new.target
- 箭头函数中的this,是父级作用域中的this,即定义时所在的对象,即 ( 外部代码块中的this )
箭头函数中的 this 说明
- 箭头函数没有自己的this,而是父级作用域中的this
- 因为箭头函数没有自己的this,所以不能作为构造函数,所以不能使用bind,call,apply等改变this的执行
(1) 案例
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
父级作用域 foo 函数内
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
(7) Class
- class本质上是一个function,类本身就指向构造函数
- typeof classA => 'function'
- classA = classA.prototype.constructor
- 类的所有方法都定义在 prototype 上
- 一个类必须要有 constructor 方法,如果没有显示定义,就会自动添加一个空的 constructor 函数
- 类必须只用 new 来调用
- name属性:classA.name === classA
- this指向:类的方法内部如果含有this,默认指向 ( 类的实例 )
- 类相当于实例的原型,所有在类中定义的方法,都会被实例所继承
- 实例属性可以定义在类的最顶层,好处是比较整齐,一眼能看出哪些是类的实例属性
- 类的私有方法通常通过加下划线,表示私有,比如:( _bar )
- 类的私有方法提案:使用 ( # ) 号表示
- 类的私有方法和私有属性前面还可以使用静态关键字,表示 ( 静态的私有属性 ) ( static #a = 10 )
- 什么是 ( 私有方法 ) 和 ( 私有属性 ) ???
- 只有在类的 ( 内部 ) 访问的属性和方法,外部不能访问
- new.target
- new.target返回通过 ( new ) 命令作用于的那个 ( 构造函数 ),而不是别的方法调用的,用在 ( 构造函数内部 )
- new.target可以用在 ( 构造函数中 ) ,也可以用在 ( Class的Constructor ) 中
- new.target子类继承父类时,返回子类
(1) 类内部定义的所有方法,都是不可枚举的
- 可以设置
enumerable
属性来设置对象的属性是否可以枚举 - 哪些遍历器可以遍历对象的可枚举属性???
- for...in()
- object.kes()
- JSON.stringify()
- 如何设置一个对象可枚举???
Object.defineProperty(obj, property, { enumerable: true})
Reflect.defineProperty(obj, property, { enumerable: true})
对象的枚举属性相关
- 以下三种方法都能遍历 可枚举 属性
- for...in()
- Oject.keys()
- JSON.stringify()
const obj = {
name: 'woow_wu7'
}
// Reflect.defineProperty(对象, 属性, 描述对象)
Reflect.defineProperty(obj, 'age',{
value: 100,
enumerable: true
})
// for...in
for(let i in obj) {
console.log('for in()', i)
}
// Object.keys
let keys = Object.keys(obj)
console.log('Object.keys()', keys)
// JSON.stringify
const jsonString = JSON.stringify(obj)
console.log('JSON.stringify()', jsonString)
(2) 静态方法 static
- 类相当于实例的原型,所有在类中定义的方法,都会被实例所继承
- 如果在一个方法前加上 ( static ) ,就表示该方法 ( 不会被实例所继承 ),而是直接通过 ( 类来调用 ), 被称为 ( 静态方法 )
- 静态方法中的this => 指向 ( 类本身 ),而不是类的实例
- 静态方法可以和非静态方法同名
- 父类的 ( 静态方法 ) 可以被子类所 ( 继承 )
- 静态方法可以从 super 对象上调用
(3) 静态属性
- 静态属性值class本身的属性,而不是定义在实例对象this上的属性
- 注意:Class内部只有静态方法,没有静态属性
- 新的提案:可以把 静态属性定义在Class内部,同样在class顶层属性前加上static
Class的静态方法,静态属性,实例方法,实例属性
- 静态方法和静态属性通过Class本身来调用
- 实例属性和实例方法通过实例来调用
- 静态方法可以被子类所继承
- 静态方法中的this指向类本身
class A {
static a = '静态属性a'
a = '实例属性a'
static b() {
console.log('静态方法b')
}
b() {
console.log('实例方法b')
}
}
console.log('A.a', A.a) // 静态属性a
console.log('A.b()', A.b()) // 静态方法b
const a = new A()
console.log('a.a', a.a) // 实例属性a
console.log('a.b()', a.b()) // 实力方法
(8) Class的继承
- Class可以通过 ( extends ) 关键字,实现继承
- ( 子类 ) 必须在 constructor 中调用 super 方法
- 因为子类自己的this对象,必须通过符类的construtor完成塑造,得到父类同样的属性和方法,然后对其加工,加上子类自己的属性和方法,如果不调用 super ,就得不到 this 对象
- ES5和ES6的继承的不同???
- ES5:先创建实例this,再将父类的属性方法添加到this上
- ES6:先将父类实例属性和方法添加到this上,然后用子类的构造函数修改this
- 子类的构造函数,只有在先调用super后,才能使用this关键字
- 因为只有 super 方法才能调用父类的实例
- 父类的静态方法也能被子类所继承
(1) super关键字
super 可以作为 ( 函数 ),也可以作为 ( 对象 )
- super作为函数
- super作为 ( 函数 ) 时,只能用在 ( 子类的构造函数中 ),表示 ( 父类的构造函数 )
- super内部的this,指向 ( 子类的构造函数 )
- super作为对象
- 在 ( 普通方法 ) 中,指向 ( 父类的原型 ),( this ) 指向 ( 当前的子类实例 )
- 在 ( 静态方法 ) 中,指向 ( 父类 ),( this ) 指向 ( 当前子类 )
- 注意点
- super作为对象时,在静态方法中表示父类的原型,所以super无法调用父类实例上的属性和方法
super作为对象时的经典案例
- super在普通方法中,表示父类的原型,this指向当前子类的实例
- super在静态方法中,表示父类,this指向当前子类
class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2
(2) 类的 prototype
和 __proto__
Class同时存在两条继承线,即Class同时拥有 prototype
和 __proto__
属性
- 子类的
__proto__
属性,表示构造函数的继承
,总是指向父类
- 子类的
prototype.__proto__
表示方法的继承
,总是指向父类的prototype属性
(9) Symbol 新的数据类型
- ( symbol ) 是新的 ( 原始数据类型 ),表示 ( 独一无二 ) 的值
- (
Symbol值
) 是通过 (Symbol()
) 函数生成 - Symbol(参数)
- 参数
- 参数相当于描述信息,便于区分每个独一无二的Symbol类型
- 参数是 ( 对象 ),会先调用 ( toString ) 函数转成字符串,然后生成一个Symbol类型的值
- 参数相同的两个Symbol()函数生成的值,并不相等
- Symbol值不能和其他类型的值进行计算
- 参数
- Symbol值作为对象属性名时,不能使用 ( . ) 进行访问
(1) 数据类型 ( 一共7种 )
- 基础数据类型 ( 6种 ):
number string boolean null undefined symbol
- 引用数据类型:
Object
- Object包括很多,比如
plainObject Array RegExp Error Date Form ......
(2) 对象的属性名
- 可以是 ( 字符串 ) 或者 ( Symbol类型 )
(3) Symbol() 函数前为什么不能加 new ???
- 前置:首先得搞清楚new返回得是什么:new 返回得始终是一个对象,要么是this对象,要么是return后跟的对象
- 原因:
因为 Symbol() 函数生成的原始类型的值,而不是一个对象,new始终会返回一个对象
(4) 那为什么可以使用 new Number(1) ???
- 原因:因为
new Number(1)
是把 number 类型的1转成了 (包装对象
)
(5) 对象属性名的遍历
- Symbol作为对象属性名时,不会出现在
- for...in中
- for...of中
- 也不会被以下函数返回
- Object.keys()
- Object.getOwnPropertyNames()
- JSON.stringify()
(十) 对象深拷贝
(1) JSON.parse(JSON.stringify())
- 缺点:只能深拷贝对象和数组,但不能拷贝函数,循环引用,原型链上的属性和方法(Date, RegExp, Error等)
(2) 基础版本 - for in循环
- 缺点:
- 仍然只能拷贝对象数组
- 不能拷贝 ( 函数 ),( 循环引用 ),( 原型链上的属性和方法,比如Date,RegExp,Error )
const obj = {
name: 'woow_wu7',
number: 1, // Number
string: 'string', // String
boolean: true, // Boolean
null: null, // null
undefined1: undefined, // undefined
symbol: Symbol('symbol'), // Symbol
arr: [1, 2, 3, 4], // Array
plainObj: { [Symbol()]: 1111 }, // plain Object
address: {
city: 'chongqing',
town: 'yubei',
detail: ['chongqing', 'yubei']
},
fn: function () { console.log('this is function') }, // Function
err: new Error(), // Error
date: new Date(), // Date
reg: new RegExp(), // RegExp
}
function deepClone(args) {
// '[object Object]'
// slice(a, b) 从字符串的a位置开始截取字符串,截取到b位置
// 包含a位置,但不包含b位置
const argsType = Object.prototype.toString.call(args).slice(8, -1)
let obj = null;
switch (argsType) {
case 'Object':
obj = {}
break
case 'Array':
obj = []
break
default: // 其实永远执行不到这里
return args
}
for (let i in args) {
if (args.hasOwnProperty(i)) { // 是否是自身属性
if (typeof args[i] === 'object') {
// 对象或数组继续递归判断,结束条件就是
obj[i] = deepClone(args[i])
}
else {
obj[i] = args[i]
}
}
}
return obj
}
const res = deepClone(obj)
console.log('res', res)
(3) Map解决循环引用 - for...in循环递归2
- 需求:
- 可以拷贝对象和数组
- 能解决循环引用的问题
(1) 什么是循环引用?
const obj = {name: 'wang'}
obj.circle = obj
// obj新增circle属性,值是obj对象本身
// 这样的情况,像上面的代码例子中,for..in循环中deepClone(parameter[key])会不断重复执行
// 最终造成内存溢出
----------
(2) 如何解决循环引用?
1. 检查map实例中是否有克隆过的对象
2. 如果存在,直接返回
3. 如果不存在,就赋值键值对,key是传入的对象,value是克隆的对象
var objComplex = {
address: {
city: 'chongqing',
town: 'jiazhou',
},
score: [100, 200],
}
objComplex.circular = objComplex
function deepClone(objComplex, mapx = new Map()) { // 默认值,是一个空的map实例
if (typeof objComplex !== 'object') {
// 不是对象和数组直接返回
return objComplex
}
const objClone = Array.isArray(objComplex) ? [] : {}
if (mapx.get(objComplex)) {
// 存在被克隆的对象,直接返回
return mapx.get(objComplex)
}
// 不存在,就添加键值对,将被克隆对象作为key,克隆的对象作为value
mapx.set(objComplex, objClone)
for(let key in objComplex) {
objClone[key] = deepClone(objComplex[key], mapx)
// 注意:mapx要传入做判断
// 不管objComplex[key]是什么类型,都调用deepClone(),因为在deepClone()中会判断
}
return objClone
}
const res = deepClone(objComplex) // 这样就不会内存溢出了
console.log(res, 'res')
(4) Reflect 解决Symbol数据类型复制 - Reflect.ownKeys()循环递归(3)
- 要求: 可以拷贝对象和数组,并解决循环引用问题,并解决Symbol数据类型
- 解决 Symbol 数据类型
- Symbol不能用 new 去调用,参数可以是数组
- Reflect.ownKeys(obj)返回参数对象的所有属性,包括symbol数据类型的属性
- 缺点:Reflect不能取到原型链上的属性和方法
用 Reflect 解决 Symbol类型数据的复制
var objComplex = {
address: {
city: 'chongqing',
town: 'jiazhou',
},
score: [100, 200],
}
objComplex.circular = objComplex
objComplex[Symbol()] = 'symbol'
function deepClone(objComplex, mapx = new Map()) {
if (typeof objComplex !== 'object') {
return objComplex
}
const objClone = Array.isArray(objComplex) ? [] : {}
if (mapx.get(objComplex)) {
return mapx.get(objComplex)
}
mapx.set(objComplex, objClone)
// for(let key in objComplex) {
// objClone[key] = deepClone(objComplex[key], mapx)
// }
Reflect.ownKeys(Array.isArray(objComplex) ? [...objComplex] : { ...objComplex }).forEach(key => {
// Reflect.ownKeys(obj)返回对象参数的所有属性,包括symbol类型的属性
objClone[key] = deepClone(objComplex[key], mapx)
})
return objClone
}
const res = deepClone(objComplex)
console.log(res, 'res')
(5)结构化克隆算法解决其他对象的拷贝 (4)
- 要求: 可以拷贝对象和数据,并解决循环引用问题(Map),并解决Symbol数据类型(Reflect),解决其他对象的拷贝(结构化克隆),构造函数生成实例的原型对象的拷贝
- 比如:Date,Regexp,构造函数生成的实例的原型对象的属性拷贝
- 缺点:不能处理Error,不能处理Function,不能处理DOM节点
function Message() {
this.sex = 'man'
}
Message.prototype.age = 1000
var objComplex = {
address: {
city: 'chongqing',
town: 'jiazhou',
},
score: [100, 200],
[Symbol()]: 'symbol',
date: new Date(),
reg: new RegExp(),
fn: function () { },
err: new Error(),
message: new Message()
}
objComplex.circle = objComplex
function deepClone(objComplex, mapx = new Map()) {
if (typeof objComplex !== 'object') {
return objComplex
}
let objClone = Array.isArray(objComplex) ? [] : {}
if (mapx.get(objComplex)) {
return mapx.get(objComplex)
}
mapx.set(objComplex, objClone)
// for(let key in objComplex) {
// objClone[key] = deepClone(objComplex[key], mapx)
// }
// Reflect.ownKeys(Array.isArray(parameter) ? [...parameter] : { ...parameter }).forEach(key => {
// objClone[key] = deepClone(parameter[key], mapx)
// })
switch (objComplex.constructor) { // 传入的参数对象的构造函数
case Date:
case RegExp:
case Message: // 自定义的构造函数
objClone = new objComplex.constructor(objComplex)
break
// 如果是Date,RegExp,Message构造函数的请求
// 就分别用这些构造函数生成实例,然后再赋值给拷贝对象
default:
Reflect.ownKeys(Array.isArray(objComplex) ? [...objComplex] : {...objComplex}).forEach(key => {
objClone[key] = deepClone(objComplex[key], mapx)
})
}
return objClone
}
const res = deepClone(objComplex)
console.log(res, 'res')
console.log(res.message.age, '克隆的对象')
console.log(objComplex.message.age, '原对象')
(十一) 事件模型
- addEventListener:绑定事件的监听函数
- removeEventListener:移除事件的监听函数
- dispatchEvent:触发事件
(1) addEventListener(type, listener[, useCapture])
- 参数
- type:事件名称,大小写敏感
- listener:监听函数
- useCapture:是否在捕获阶段触发,默认是false在冒泡阶段触发0
- 第二个参数
- ( 第二个参数 ) 除了是 ( 监听函数 ),还可以是一个 ( handleEvent ) 方法的 ( 对象 )
- 第三个参数
- ( 第三个参数 ) 除了是 ( useCapture ) 布尔值以外,还可以是 ( 属性配置对象 )
- 第三个参数是配置对象时的属性
capture
:boolean,表示是否在捕获阶段触发监听函数once
:boolean,表示监听函数是否只触发一次passive
:布尔值,表示监听函数不会调用 preventDefault 方法
- ( addEventListener ) 可以针对 ( 当前对象 ) 的 ( 同一事件 ),添加 ( 多个不同监听函数 ),但是如果是为同一个事件多次添加同一个监听函数,该函数只会执行一次
- this
- 监听函数中的this,指向当前事件所在的对象,即 ( addEventListener所绑定的对象 )
(2) removeEventListener()
- 删除的需要满足的条件
- 同一个DOM元素
- 同一个监听函数
- 第三个参数也必须一样
(3) EventTarget.dispatchEvent(event)
- 作用:在当前节点触发指定事件,从而触发监听函数的执行
- 参数:是一个 ( Event ) 对象的 ( 实例 )
- 返回值:boolean,如果调用Event.preventDefault()则返回false
(4) 监听函数 - 3种方法
- js中一共有三种方法为 ( 事件 ) 绑定 ( 监听函数 )
- 1.HTML中的
on-事件名
属性- 属性的值:是 ( 将要执行的代码 )
- 只会在 ( 冒泡阶段 ) 触发
- 直接设置
on-事件名
,和通过元素节点的setAttribute
方法设置on-事件名
属性,效果是一样的
- 2.元素节点的事件属性
- 只会在 ( 冒泡阶段 ) 触发
window.onClick=函数名或者函数定义
,不需要执行
-
- EventTarget.addEventListener()
- 总结
- 第一种违反了HTML和JS分离的原则,并且只能在冒泡阶段触发
- 第二种同一个事件,只能定义一个监听函数,并且只能在冒泡阶段触发
- 第三种推荐
- 1.同一个事件,可以添加多个监听函数
- 2.能够指定在哪个阶段触发监听函数,捕获阶段还是冒泡阶段
- 3.除了DOM对象,其他对象也有这个接口,比如window,XMLHttpRequest,统一接口
(5) this 的指向 - 和Event.currentTarget一样,都指向监听函数所绑定的节点
- 监听函数中的this,指向触发事件的那个元素节点,即监听函数所绑定的那个节点,始终保持不变
(6) 事件的传播
- 捕获阶段:从window传导到目标节点
- 目标阶段:在目标节点上触发
- 冒泡阶段:从目标阶段传到回window
- 注意点:默认是目标节点就是嵌套最深的节点,也就是说捕获的结束节点和冒泡的开始节点
(7) 事件代理
- 因为事件会在冒泡阶段向上传播到父节点,所以看可以把节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方法叫做事件代理
- 如果希望事件到某个节点为止,不再传播,可以使用事件对象的 stopProgation 方法
- propagation:是传播的意思
阻止事件传播
:event.stopProgation()
- 注意点:只会阻止事件的传播,不会阻止该节点的其他事件的触发
彻底阻止传播
:event.stopImmediatePropagation()
(8) Event.target 和 Event.currentTarget
- Event.currentTarget:返回事件当前所在的节点,即监听函数绑定的那个节点
- Event.target:返回最先触发事件的节点
(9) Event.preventDefault()
- 取消浏览器对当前事件的默认行为
- 比如,点击链接后,浏览器默认会跳转到另一个页面,使用这个方法以后,就不会跳转了
- 比如,按一下空格键,页面向下滚动一段距离,使用这个方法以后也不会滚动了
该方法生效的前提是,事件对象的cancelable属性为true,如果为false,调用该方法没有任何效果
(十二) XMLHttpRequest
- 如何获取response???
- xhr.response
- xhr.responseText
- xhr.responseXML
- xhr.responseText
- 在 xhr.responseType = 'text' , '',不设置时,xhr实例对象上才有此属性,此时才能调用
- xhr.response
- 在 xhr.responseType = 'text' ,'' 时,值是 ( '' )
- 在 xhr.resposneType 是其他值时,值是 ( null )
- xhr.responseType
- text
- document
- json
- blob
- arrayBuffer
const xhr = new XMLHttpRequest()
// new 命令总是返回一个对象,要么是this对象,要么是return后面跟的对象
// new 调用的是构造函数,说明XMLHttpRequest是一个构造函数
(1) xhr.open()
- 初始化 HTTP 请求参数,比如url,http请求方法等,但并 ( 不发送请求 )
- xhr.open() 方法主要供 xhr.send() 方法使用
xhr.open(method, url, async, username, password)
- 参数
- method:http请求的方法,包括 GET POST HEAD
- url:请求的地址
- async:是否异步
- true,默认值,异步请求,通常需要调用 onreadystatechange() 方法
- false,对 send() 方法的调用将阻塞,直到响应完全接收
(2) xhr.send()
- 发送一个http请求
xhr.send(body)
- get请求:get请求的参数可以直接写在 open() 方法中
- post请求:post请求的参数写在 send() 方法中
- 注意:
- body参数的数据类型会影响 requestHeader 中的 Content-Type 的默认值,如何手动指定则会覆盖默认值
- 如果data是 Document 类型,同时也是HTML Document类型,则content-type默认值为text/html;charset=UTF-8;否则为application/xml;charset=UTF-8;
- 如果data是 DOMString 类型,content-type默认值为text/plain;charset=UTF-8;
- 如果data是 FormData 类型,content-type默认值为multipart/form-data; boundary=[xxx]
- 如果data是其他类型,则不会设置content-type的默认值
(3) xhr.setRequestHeader()
- 指定一个http请求的头部,只有在 readState = 1 时才能调用
- setRequestHeader可以调用的时机
- 1. 在 readyStaet = 1 时
- 2. 在 open() 方法之后,send() 方法之前
- 3. 其实 1 2 是一个意思
xhr.setRequestHeader('name', 'value')
- 参数
- name:头部的名称
- value:头部的值
- 注意
- setRequestHeader() 方法可以 ( 多次调用 ) ,值不是 ( 覆盖override ) 而是 ( 追加append )
- setRequestHeader() 只有在 readyState = 1 时才能调用,即 open() 方法之后,send() 方法之前
(4) xhr.getResponseHeader()
- 指定http响应头部的值
(5) xhr.abort()
- 取消当前响应,关闭连接并且结束任何未决的网络活动
- xhr.abort()会将 readyState 重置为0
- 应用:取消请求,在请求耗时太长,响应不再有必要时,调用该方法
- abort:是终止的意思
(6) xhr.onreadystatecange()
- 在 readyState 状态改变时触发
- xhr.onreadystatechange() 在 readyState = 3 时,可能多次调用
- onreadystatechange 都是小写
- readyState 驼峰
readyState状态
0 UNSENT ------------- xhr对象成功构造,open() 方法未被调用
1 OPEND ------------- open() 方法被调用,send() 方法未被调用,setRequestHeader() 可以被调用
2 HEADERS_RECEIVED --- send() 方法已经被调用,响应头和响应状态已经返回
3 LOADING ------------ 响应体 ( response entity body ) 正在下载中,此状态下通过 xhr.response 可能已经有了响应数据
4 NODE ------------- 整个数据传输过程结束,不管本次请求是成功还是失败
(7) xhr.onload
- 请求成功时触发,此时 readyState = 4
- 注意:重点!!!
- 1.除了在 xhr.onreadystatechange 指定的回调函数的 readyState = 4 时取值
- 2.还可以在 xhr.onload事件中取值
xhr.onload = function() {
// 请求成功
if (xhr.status === 200 ) {
// do successCallback
}
}
- 3.判断 xhr.status === 200 是有坑的,因为成功时返回的状态码不只有200,下面的写法更靠谱
xhr.onload = function () {
//如果请求成功
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
// 304 not modified 资源未被修改 协商缓存
// 2开头的状态码,表示请求成功
//do successCallback
}
}
(8) xhr.timeout
- 设置过期时间
- 问题1:请求的开始时间怎么确定?是 ( xhr.onloadstart ) 事件触发的时候,也就是xhr.send()调用的时候
- 解析:因为xhr.open()只是创建了链接,当并没有真正传输数据,只有调用xhr.send()时才真正开始传输
- 问题2:什么时候是请求结束?
- 解析:( xhr.loadend ) 事件触发时结束
(9) xhr.onprogress 下载进度信息
(10) xhr.upload.onprogress 上传进度信息
xhr.upload.onprogress = function(e) {
if ( e.lengthComputable ) {
const present = e.loaded / e.total * 100;
}
}
- XMLHttpRequest请求案例
XMLHttpRequest请求案例
----
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="buttonId">点击,请求数据</button>
<script>
const button = document.getElementById('buttonId')
button.addEventListener('click', handleClick, false)
function handleClick() {
const xhr = new XMLHttpRequest()
xhr.open('GET', 'http://image.baidu.com/channel/listjson?pn=0&rn=30&tag1=明星&tag2=全部&ie=utf8', true) // open()方法
xhr.setRequestHeader('Content-Type', 'application/json') // setRequestHeader必须在open()方法后,send()方法前调用,即 readyState === 1时
xhr.responseType = 'text'
xhr.timeout = 10000
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
// 这里通过 this 代替 xhr 其实是一样的
// 因为 this 在运行时确定指向,xhr实例在调用onreadystatechange方法,所以this指向xhr实例
console.log(JSON.parse(this.responseText)) // 等价于 console.log(JSON.parse(xhr.responseText))
}
}
xhr.onload = function () {
if ((xhr.status >= 200 && xhr.status < 300) || (xhr.status === 304)) {
console.log(JSON.parse(xhr.responseText), 'xhr.onload是在请求完成时触发的回调')
}
}
xhr.send() // 发送请求
}
</script>
</body>
</html>
(十三) 前端安全
(1) xss - 跨站脚本攻击
- ( Cross Site Script ) 跨站脚本攻击
- cross site script原本的缩写是css,但是为了区分层叠样式表css,改写为XSS
- XSS攻击是指攻击者在网站上注入恶意的客户端代码,通过 恶意脚本 对客服端网页进行篡改,从而在用户浏览网页时,对用户浏览器就行控制,或者获取用户隐私数据一种攻击方式
- 恶意脚本:主要指 javascrip代码,有时也指 html 和 flash
- 攻击方式:有多种,共同的特征是:窃取用户的隐私数据
- 攻击类型:可以分为三类,反射型(非持久型),储存型(持久型),基于DOM
- 危害:
- 利用虚假的输入表单,骗取用户的个人信息
- 利用脚本获取用户的cookie
- 显示伪造的图片和文章
防御XSS攻击
- 设置 httpOnly
- 阻止通过script脚本获取cookie
- httpOnly可以让脚本获取不到cookie,包括js,XMLHttpRequest等
- javascript获取cookie
document.cookie
- XMLHttpRequest获取cookie
- 通过
xhr.getResponseHeader('Set-Cookie')
// 不能获取cookie,返回null - 通过xhr.getAllResponseHeader()获取所有的simple response header,并不包括Set-Cookie字段
注意:这两种方法都只能获取simple response heade,而不能获取Set-Cookie字段
- 通过
- 过滤检查
- 对 input, textArea, form 表单做特殊符号的过滤检查
- HtmlEncode:某些情况下,不能对用户数据进行严格过滤,需要对标签进行转换
- JavaScriptEncode
(1) HtmlEncode:对html标签进行转换
< --------------------- <
> --------------------- >
& --------------------- &
'' --------------------- "
空格 -------------------  
(2) JavascriptEncode:对js一些特殊符号进行转码
\n --------------------- \\n
\r --------------------- \\r
" --------------------- \\"
(2) CSRF - 跨站请求伪造
- ( Cross Site Request Forgery ) 跨站请求伪造
- forgery:伪造的意思 ( forgeries伪造 )
- CSRF是一种劫持受信任用户向服务器发送非预期请求的攻击方式
- CSRF的原理
主要是通过获取用户在目标网站的cookie,骗取目标网站的服务器的信任,在用户已经登录目标站的前提下,访问到了攻击者的钓鱼网站,攻击者直接通过 url 调用目标站的接口,伪造用户的行为进行攻击,通常这个行为用户是不知情的。
- 即获取了cookie,就可以做很多事情:比如以你的名义发送邮件、发信息、盗取账号、购买商品、虚拟货币转账等等
案例:
CSRF攻击的思想:(核心2和3)
1、用户浏览并登录信任网站(如:淘宝)
2、登录成功后在浏览器产生信息存储(如:cookie)
3、用户在没有登出淘宝的情况下,访问危险网站
// 注意:如果该cookie在没有设置过期时间或者为null,默认是会话时间session-cookie,关闭浏览器后cookie会被清除
// Expires,Max-Age可以设置cookie的过期时间
// 所以这里强调了是没有登出的情况,就有cookie被获取的风险
// 如果cookie设置了具体的过期时间,有效期内都可能被获取
4、危险网站中存在恶意代码,代码为发送一个恶意请求(如:购买商品/余额转账)
// 该请求,携带刚刚在浏览器产生的信息(cookie),进行恶意请求
5、淘宝验证请求为合法请求(区分不出是否是该用户发送)
// 用HTTP中的header头中的 Refer 来预防
// refer 可以检查请求源,只有合法的请求来源服务器才予以响应
6、达到了恶意目标
预防CSRF攻击
- 验证码:被认为是对抗CSRF攻击最简洁有效的防御方法
- CSRF往往是在用户不知情的情况下构建了网络请求,而验证码会强制用户必须与应用进行交互才能完成最终的请求
- CSRF往往是在用户不知情的情况下构建了网络请求,而验证码会强制用户必须与应用进行交互才能完成最终的请求
- refer检查
- HTTP中有 Refer 字段,表示请求来源地址,通过Refer可以检查请求是否来自合法的源,服务器只对合法的源予以响应
- refer:是参考的意思
- token
- CSRF主要就是获取cookie,所以要防御的话,就需要在请求中加入攻击者不能伪造的信息,并且该信息不能保存在cookie中
- 可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求