[复习笔记-02] 02

381 阅读59分钟

(一) 前置知识

(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更加房便快捷
  • 生成generate
    • @babel/generate用来将转换后得抽象语法树转化为javascript字符串
      • 将经过转换的AST通过babel-generator再转换为js代码
      • 过程及时深度遍历整个AST,然后构建转换后的代码字符串。

(三) 手写 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-urlencodedmultipart/form-datatext/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中指定
        • 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:表示该资源仅仅属于发出请求的最终用户,这将禁止中间服务器(如代理服务器)缓存此类资源,对于包含用户个人信息的文件,可以设置private
    • public:允许所有服务器缓存该资源
    • 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可以指定privatepublic,表示是否允许中间服务器缓存该资源
  • 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
    • 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 都拿不到该属性
  • 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) 数字证书认证机构的业务流程

  • 前置知识:
    • 服务器有一个密钥:一个公钥,一个私钥
    • 证书颁发机构也有一对密钥:一个公钥,一个私钥,公钥是提前内置在浏览器中的
    • 证书颁发机构用 (自己的私钥) 对 ( 服务器的公钥 ) 进行加密,做数字签名,并生成公钥证书
  • 具体流程
      1. 服务器把自己的 ( 公钥 ) 向证书认证机构申请证书
      1. 证书颁发机构用自己的 ( 私钥 ) 对服务器的 ( 公钥 ) 进行数字签名,并生成 ( 公钥证书 )
      1. ( 服务器 ) 向 ( 客服端 ) 发送证书颁发机构颁发的 ( 公钥证书 )
      1. ( 客服端 ) 收到公钥证书后,利用内置在自己的 ( 证书颁发机构的公钥 ) 解密 (公钥证书中,证明服务器的公钥的真实性 )
      1. 如果是真实的服务的公钥证书,那么 ( 客户端就会用服务器的公钥加密之后在对称加密才会用到的密钥 ) 并发送给服务器
      1. 服务器收到 ( 加密后的信息后 ) 用自己的私钥 ( 解密 ),解密后服务端就获取到了 ( 对称加密的密钥了 )
      1. 接下来,通信双发就可以进行 ( 对称加密通信了 ),即可以建立通信,交换报文了

(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, ACK=1, 序号Seq=y, 确认号Ack=x+1 ) 的 ( 确认包 ) 给 ( 客户端 )
      • 标志位 ACK=1 表示确认序号有效
      • Ack = Seq + 1 ( 确认号 = 序号 + 1 )
      • 服务器状态:由 COLSED状态 => SYN_RCVD状态
  • 第三次握手
    • ( 客户端 ) 发送一个 ( 标志位ACK=1, 序号Seq=x+1, 确认号Ack=y+1 ) 的 ( 确认包 ) 给 ( 服务器 )
      • 服务器和客户端的状态:都变成 ESTABLISHED 状态,表示已连接
      • established:是建立的意思

TCP建立链接 - 为什么需要第三次握手

为什么只有三次握手,才能确认双方的发送和接收能力是否正常,而两次却不可以??

  • 第一次握手:
    • 客户端发送连接包,服务端收到了
    • ( 服务端 ) 就能得出结论:( 客户端的发送能力,服务端的接收能力是正常的 )
  • 第二次握手
    • 服务端发送确认包,客服端收到了
    • ( 客户端 ) 就能得出结论:( 服务端的接收、发送能力,客户端的接收、发送能力是正常的 )
    • 注意:此时服务端并不能确认客户端的接收能力是否正常
  • 第三次握手
    • 客户端发送确认包,服务端收到了
    • ( 服务端 ) 就能得出结论:( 客户端的接收、发送能力,和服务端的接收、发送能力都是正常的 )
  • 总结为什么需要三次握手
    • 为了实现可靠数据传输, TCP 协议的通信双方都必须维护一个序列号, 标识发送出去的数据包哪些已经被对方收到
    • 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤
    • 如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认,也就是上面每次握手分析的,如果两次握手,服务端是没法确认客户端的接收能力是正常的
    • 防止已失效的连接请求又传送到服务器端,因而产生错误

(2) 四次挥手

  • 第一次挥手
    • ( 客户端 ) 发送一个 ( 标志位FIN=1, 序号Seq=u ) 的 ( 释放包 ) 到 ( 服务器 )
      • 标志位 FIN=1 表示释放连接
      • 客户端状态:由 ESTABLISHED状态 => FIN_WAIT1状态
      • 表明的是:主动方(客户端)的报文发送完了,但是主动方(客户端)还是可以 ( 接收报文 )
  • 第二次挥手
    • ( 服务器 ) 返回一个 ( 标志位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已失败
    • 只有异步操作的结果可以决定是哪一种状态,任何其他操作都无法改变这个状态
  • 状态一旦改变就不会再变,任何时候都可以得到这个结果
    • 状态的改变只有两种可能:从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的参数
        • 第二个参数可选

(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
会输出310,说明 ( 循环体中的变量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 ) 中只有 ( 全局作用域 ) 和 ( 函数作用域 ) 因此造成了以下问题
      • 内层变量可能会覆盖外层变量
      • 用来计数的循环变量,泄漏为全局变量
(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.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,箭头函数中也不存在 argumentssupernew.target
  • 箭头函数中的 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=函数名或者函数定义,不需要执行
    1. 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标签进行转换
<  --------------------- &lt
>  --------------------- &gt
&  --------------------- &amp
'' --------------------- &quot
空格 ------------------- &nbsp


(2) JavascriptEncode:对js一些特殊符号进行转码
\n --------------------- \\n
\r --------------------- \\r
"  --------------------- \\"

(2) CSRF - 跨站请求伪造

  • ( Cross Site Request Forgery ) 跨站请求伪造
  • forgery:伪造的意思 ( forgeries伪造 )
  • CSRF是一种劫持受信任用户向服务器发送非预期请求的攻击方式
  • CSRF的原理
    • 主要是通过获取用户在目标网站的cookie,骗取目标网站的服务器的信任,在用户已经登录目标站的前提下,访问到了攻击者的钓鱼网站,攻击者直接通过 url 调用目标站的接口,伪造用户的行为进行攻击,通常这个行为用户是不知情的。
    • 即获取了cookie,就可以做很多事情:比如以你的名义发送邮件、发信息、盗取账号、购买商品、虚拟货币转账等等
案例:

CSRF攻击的思想:(核心231、用户浏览并登录信任网站(如:淘宝)
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 攻击而拒绝该请求

资料