美文网首页
3.JavaScript

3.JavaScript

作者: helloyoucan | 来源:发表于2019-11-25 18:03 被阅读0次

    原文链接:https://github.com/helloyoucan/knowledge

    JavaScript相关

    1、声明相关
    • var变量提升,function函数提升,function >var(优先级)
    • 块级作用域:withtry/catchletconst
    • let ,不可重复声明,暂时性死区
    • const,不可重复声明、修改,暂时性死区
    2、JS引擎
    任务队列
    • JS分为同步任务、异步任务。
    • 同步任务在主线程上执行,形成一个Call Stack(执行栈)。
    • 主线程之外,事件触发线程管理着一个Task Queue(任务队列),当异步任务完成时,会指定对应的事件(回调)进入Task Queue。
    • 一旦Call Stack(执行栈)中所有同步任务执行完毕(此时JS引擎空闲),系统就会读取Task Queue,将Task Queue中可运行的事件添加到Call Stack(执行栈)中,开始执行。
    • 事件循环是通过任务队列机制来进行协调的。
    • 一个Event Loop中,可以有一个或者多个任务队列(task queue),一个任务队列就是一系列有序任务(task)的集合
    • 每个任务都要一 个任务源(task source),源自同一个任务源的task必须放到同一个任务队列,从不同源来的则被添加到不同队列。
    • setTimeout/Promise等API便是任务源,而进入任务队列的是他们指定的具体执行任务。

    <img src="./images/task.png" style="width:500px"/>

    宏任务
    • (macro)task(宏任务),可以理解为每次执行栈执行的代码就行一个宏任务(包括每次从Task Queue中获取一个事件(回调)并放到执行栈中执行)。
    • macrotask中的事件(回调)都是放在一个事件队列中(Task Queue)的,而这个队列由事件触发线程维护。
    • 浏览器为了能够使得JS内部(macro)task与DOM任务能够有序地执行,会在一个task执行结束后,在下一个task执行开始前,对页面进行渲染(task->渲染->task->......)。
    • macrotask主要包含:script(整体代码)、setTimeout/setInterval、I/O、UI交互事件、postMessage、MessageChannel(创建消息通道传递消息的api,可用在iframe上)、setImmediate(Node.js环境)
    微任务
    • microtask(又称为微任务),可以理解是当前task执行结束后立刻执行的任务(在当前macrotask后,渲染前,下一个macrotask前)。
    • 产生的microtask会添加到微任务队列(Microtask Queues)中,该队列由JS引擎线程维护。
    • 响应速度比macrotask(例如setTimeout)会更快,因为无需等渲染。(某个macrotask执行后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前))。
    • microtask主要包含:Promise.then(原生的,部分实行可能是task)、MutationObserver(监听DOM变化的api)、process.nextTick(Node.js环境)。

    微任务和宏任务皆为异步任务,主要区别在于他们的执行顺序,Event Loop的走向和取值。

    运行机制

    在事件循环(Event Loop)中,每进行一次循环操作成为tick,每一次tick的任务处理模型是比较复制的,但关键步骤如下:

    • 执行Call Stack(执行栈)一个宏任务(栈中没有就从事件队列(Task Queue)中获取)
    • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
    • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
    • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
    • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

    <img src="/images/执行机制.jpg" style="width:300px;"/>

    3、 闭包

    有权访问另外一个函数作用域中的变量的函数

    用途
    • 设计私有方法和变量
    • 单例模式
    • 模块化——使用闭包模块化代码,减少全局变量的使用
    缺陷
    • 闭包会常驻在内存中,不会被垃圾回收机制主动回收,增大内存使用量,使用不当容易泄露
    • 闭包对脚本性能有负面效果,比如处理速度和内存消耗
    4、异步执行
    • 动态插入脚本
    • defer="defer"——IE支持,页面解释完成时执行,按照加载顺序执行
    • async="async"——加载完成则执行
    5、数据类型
    • 基本类型

      Number、String、Boolean、Undefined、Null、Symbol

    • 引用类型

      Objcet、Array、Function

    主要要点:

    • typeof {} 或 null 或 [ ] === "object"
    6、原型链

    <img src="/images/原型链.png" style="width:500px;"/>

    实例、构造函数、原型对象

    • 实例 = new 构造函数
    • 实例.__proto__ === 原型对象
    • 构造函数.prototype === 原型对象
    • 原型对象.constructor === 构造函数
    • 原型对象.__proto__ === 原型链上一级的原型对象
    instanceof
    • 判断 实例.__proto__ === 原型链上任意的构造函数(原型对象).prototype
    • 实例.__proto__.constructor === 实例的构造函数
    New运算符

    new做了什么?

    1. 创建了一个全新的对象。
    2. 这个对象会被执行[[Prototype]](也就是__proto__)链接。
    3. 生成的新对象会绑定到函数调用的this
    4. 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
    5. 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。
    function Func(name){this.name = name};
    var o1 = new Func('name1')
    //模拟new
    function new2(func,...param){
       /*
        //Object.create的作用
        function F(){}
        F.prototype = func.prototype
        var o =new F()
       */
        var o = Object.create(func.prototype)
        var ret = func.apply(o,param)
        return ret instanceof Object ? ret : o; //构造函数return了内容可以这样
    }
    var o2 = new2(Func,'name2')
    
    7、this
    • this是一个关键字
    • 代表函数运行时,自动生成的一个内部对象,只能在函数内部使用
    • this是在执行时确定是什么值,定以时无法确定
    • this指向的是,调用函数的那个对象
    • bind(this)、call(this,p1,p2,...)、apply(this,[p1,p2,....])
    8、继承
    1. 借助原型链实现继承

      /*
      缺点:
      1.父类中的属性(也就是父类this上的属性)是子类共用
      2.子类实例时无法向父类传参(因为父类早已实例化)
      */
      function P(){}
      function C(){}
      C.prototype = new P()
      
    2. 构造函数实现继承

      /*
      缺点:
      1.子类无法继承父类的prototype
      2.若父类方法绑定在this上则每个子类都会copy一份,违背代码复用
      */
      function P(){}
      P.prototype.say = function(){}//子类无法继承
      function C(){ P.call(this,arguments) } //继承父类属性
      
    3. 组合式继承

      /*
      缺点
      1.父类构造函数调用两次
      */
      function P(){}
      function C(){ P.call(this,arguments) } //P执行了1次
      C.prototype = new P() // P执行了2次
      
    4. 寄生组合继承

      function P(){}
      function C(){ P.call(this,arguments) }
      //实现方式
      C.prototype = Object.create(P.prototype) //通过浅拷贝继承父类方法,不需要调用一次父类构造函数
      C.prototype.constructor = C // 上一行代码对子类原型进行了拷贝,因此子类的constructor属性被重写,所以需要这样修复
      
    5. extends继承

      class P(){}
      class C extends P(){
        constructor(){
          super() //获得this对象
        }
      }
      
    ES5的继承 vs ES6的继承

    ES5

    • ES5的继承是通过prototype或构造函数机制来实现
    • ES5继承的实质是先创建子类实例,再将父类方法添加到this上。

    ES6

    • ES6是先创建父类实例对象this(先调用super()方法),再用子类构造函数修改this。
    • ES6通过class关键字定以类,里面有构造方法,类之间通过extend关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例失败。因为子类没有自己的this对象,而是继承父类的this对象,然后对齐加工。如果不调用super方法,子类得不到this对象。
    9、Promise
    • Promise是一种用于解决异步问题的思路、方案或者对象方式。

    • 状态:pendingfulfilledrejected

    • 写法:

      new Promise((resolve,rejected)=>{
        //success
        setTimeout(()=>{
            resolve('success')
        },2000)
        //fail
        rejected()
      })
      .then(()=>{})
      .catch(()=>{})
      .finally(()=>{})
      
    • 主要要点:

      1. 一旦执行resolve或rejected,后面的再执行rejected或resolve则无效
      2. 执行了resolve或rejected,Promise后面的代码还会执行下去
    • 静态方法

      Promise.all([]) // 数组内 所有 状态都为fulfilled,则状态变为fulfilled,数组内其中一个状态变为rejected,则状态为rejected
      Promise.race([]) // 数组内 其中一个 状态都为fulfilled,则状态变为fulfilled
      Promise.resolve() // 转化为Promise对象,等价于new Promise(resolve => resolve())
      Promise.reject() // 转化为Promise对象
      
    10、Ajax
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
        if(xhr.readyState === 4){
            if(xhr.status===200||
               xhr.status===304||
               xhr.status===206){//媒体资源体积大,分步传输,状态码为206
                res = xhr.responseText
                if(typeof res==='string'){
                    res = JSON.parse(res)
                    console.log(res)
                }else{
                    console.log(res)
                }
            }else{
                console.log(new Error(res))
            }
        }
    }
    xhr.open('GET','url',true)
    xhr.send()
    /*
    xhr.open('POST','url',true)
    xhr.setRequestHeader('content-type','application/json;charset=UTF-8')
    xhr.send(JSON.stringify({}))
    */
    
    • 状态码
    readState

    0-(未初始化)还没有调用send方法
    1-(载入)已调用send()方法,正在发生请求
    2-(载入完成)send方法执行完成,已经接收到前部响应内容
    3-(交互)正在解析响应内容
    4-(完成)响应内容解析完成,可以在客户端调用了

    status

    2xx - 表示成功处理请求,如200
    3xx - 需要重定向,浏览器直接跳转
    4xx - 客户端请求错误,如404
    5xx - 服务器端错误

    11、跨域

    同源策略:非同源(域名、协议、端口)的脚本不能访问或者操作其它域的页面对象。

    由于同源策略,非同源之间需要通讯,所以就需要跨域访问。

    跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。

    同源策略限制了一下行为:

    • Cookie、LocalStorage 和 IndexDB 无法读取
    • DOM 和 JS 对象无法获取
    • Ajax请求发送后response会被浏览器拦截
    跨域通讯
    • JSONP(动态插入js文件,文件中调用预先定义好的函数并传入参数)
    • Hash+ iframe
    • document.domain + iframe跨域 (适用于不同子域通讯)
    • postMessage(HTML5新增)
    • window.name + iframe(窗口载入所有页面共享)
    • webScoket(协议不受同源策略限制)
    • 跨域资源共享CROS(服务端支持+绝对路径访问接口)
    • 代理服务器(nginx、node等)
    12、优化
    页面加载资源优化
    • 静态资源压缩合并(减少http请求次数、减少文件体积)
    • 缓存资源
    • CDN
    • 按需加载资源(动态加载脚本,图片懒加载,数据下拉加载等)
    • 预加载(preload:强制浏览器不阻塞document.onload情况下请求资源;prefetch:告知浏览器将来可能需要,什么时候加载由浏览器决定)
    • 减少DOM的数量
    • 延迟加载脚本
    • 合理放置脚本位置(CSS放在前面加载,js文件放到页面底部加载)
    JavaScript执行优化
    • 缓存DOM查询(减少DOM查询,可对DOM查询做缓存)
    • 合并DOM操作(减少DOM操作,可将多个操作合并为一个)
    • 事件委托(使用事件委托的方式绑定事件)
    • 事件节流(一定时间内任务执行的次数。每次触发事件时都取消之前的延时调用方法)
    • 事件防抖(在任务高频率触发的时候,只有触发间隔超过制定间隔的任务才会执行。每次触发事件时都判断当前是否有等待执行的延时函数)
    • 慎用闭包(减少不必要的闭包的使用,及时释放无用的内存占用)
    • web worker执行耗时代码(耗时的代码块可移动到web worker执行)
    雅虎军规

    网页内容

    • 减少http请求
    • 减少DNS查询
    • 避免重定向
    • 缓存Ajax请求
    • 延迟加载
    • 预加载
    • 减少DOM元素数量
    • 划分内容到不同域名(接口和静态资源)
    • 减少iframe的使用
    • 避免404错误

    服务器

    • CDN
    • 静态内容添加Expires;动态内容设置Cache-Control报响应头
    • 启用Gzip
    • 配置Etag
    • 尽早输出(flush)缓冲
    • Ajax使用Get请求
    • 避免空的图片src

    cookie

    • 减少Cookie大小
    • 页面静态资源使用无Cookie域名

    CSS

    • 样式表放在<head>中
    • 避免css表达式
    • 使用<link>代替@import
    • 避免使用filters

    JavaScript

    • 脚本置底
    • 外部js和css文件
    • 精简JavaScript和css
    • 去除重复脚本
    • 减少DOM访问
    • 使用高效事件处理

    图片

    • 压缩图片
    • 优化CSS Sprite
    • 不在HTML中缩放图片
    • 使用小且可缓存的ico

    ETags:是服务器和浏览器用来决定浏览器缓存中组件与源服务器中的组件是否匹配的一种机制(“实体”也就是组件:图片,脚本,样式表等等)。添加ETags可以提供一种实体验证机制,比最后修改日期更加灵活。一个ETag是一个字符串,作为一个组件某一具体版本的唯一标识符。唯一的格式约束是字符串必须用引号括起来,源服务器用相应头中的ETag来指定组件的ETag。

    然后,如果浏览器必须验证一个组件,它用If-None-Match请求头来把ETag传回源服务器。如果ETags匹配成功,会返回一个304状态码,这样就减少了12195个字节的响应体。Etag 通过文件版本标识,方便服务器判断请求的内容是否有更新,如果没有就响应 304,避免重新下载。

    13、事件绑定
    种类
    • DOM0级(onclick=function(){})
    • DOM2级(addEventListener)
    • DOM3级(新增事件类型)
    事件模型
    • 捕获
    • 冒泡
    事件流
    • 捕获阶段(从window对象自上而选向目标节点传播)
    • 目标阶段(目标节点处理事件)
    • 冒泡阶段(事件从目标节点自下而上向window对象传播)
    事件绑定

    addEventListener(event,function,useCapture)

    event:事件类型

    function:执行函数

    useCapture:可选;true:捕获阶段执行;false(默认):冒泡阶段执行

    常用对象
    event.preventDefault() //阻止默认事件
    event.stopPropagation() //阻止冒泡
    event.stoplmmediatePropagation() //阻止其它事件的响应(当注册了多个事件时)
    event.currentTarget //其监听器触发事件的节点(绑定事件的元素),即当前处理该事件的元素、文档或窗口。
    event.target //当前被点击的元素(旧版本的ie是sourceElement)
    
    自定义事件
    • Event(IE不支持)

      var event = new Event(custome' //事件名称
          {//可选
              bubbles:false,//是否冒泡
              cancelable:false,//能否被取消
              composed:false//是否会在影子DOM根节点之外触发侦听器
          })
      dom.addEventListener('custome',function(){})
      dom.dispatchEvent(event)
      
    • CustomEvent(IE不完全支持)

      var event = new CustomEvent('custome',
          {//可选
              bubbles:false,//是否冒泡
              cancelable:false,//能否被取消
              detail:{a:1,b:2} //当事件初始化时传递的数据
          })
      dom.addEventListener('custome',function(e){ 
        console.log(e.detail.a,e.detail.b) 
      })
      dom.dispatchEvent(event)
      
    • document.createEvent(仅IE支持)

      var event = document.createEvent('HTMLEvents');
      event.hello='hello,I am event';
      event.initEvent('customEvent', true, true);//事件名称;事件是否冒泡;事件是否可以被取消
      dom.addEventListener('customEvent', function(e){
          console.log(e.hello)
      }, false);
      dom.dispatchEvent(event)
      
    14、本地存储
    1. cookie

      • 4kb(不同浏览器存放大小不一样,一般为4kb)

      • 不同浏览器存放位置(磁盘)不一样

      • http请求会带上

      • 字符串形式存在

      • 仅以域名区分(不同协议、端口、子域可共享)

      • 可设过期时间,默认是会话结束则销毁

      • document.cookie = "username=cfangxu; domain=qq.com; path=/" // 添加cookie,设置生效子域及路径
        
    2. sessionStorage

      • 同源策略限制
      • 本质是读取字符串,存储过多会消耗内存,页面变卡
      • 一般为5M
      • 非IE可本地打开,IE需要在服务器中打开
      • 通过跳转打开新页面会复制源页面当前会话的sessionStorage
      • 会话结束时删除
    1. localStorage

      • 同源策略限制
      • 本质是读取字符串,存储过多会消耗内存,页面变卡
      • 一般为5M
      • 非IE可本地打开,IE需要在服务器中打开
      • 修改时会触发其它页面storage事件
      • 同一域中共享
      • 持久化本地存储,需要主动删除,不会过期
    15、安全
    Cross-site requset forgery(CSRF,跨站请求伪造)
    • 原理:

      用户已登录A网站,同时用户在浏览B网站,在B网站中诱导用户点击操作A网站的连接(例如一个关注某用户的GET请求的连接)

    • 防御

      • Token认证

      • Referer验证(页面来源认证)

      • 隐藏令牌(例如把令牌存放在页面的<head>)

      • 验证码

    Cross Site Script(XSS,跨站脚本工具)
    • 原理:

      攻击者在网站恶意注入的客户端代码,通过恶意脚本对网页进行篡改,在用户浏览网页时,对用户浏览器进行控制或者获取用户隐私数据。

    • 防御

      • HttpOnly 防止劫取 Cookie(设置cookie是否能通过 js 去访问)
      • 对用户的任何输入进行检查、过滤和转义(XSS Filter)
      • 对服务端的输出进行检查、过滤和转义
    16、函数式编程
    • 不可变性 (不能更改数据,如需更改则复制数据副本来更改,并返回新数据)
    • 纯函数 (没有副作用,例如不要设置全局状态,不更改应用程序状态,函数参数不可变)
    • 数据转换 (不改变现有数据,返回新的数组或对象,例如Array.prototype.join)
    • 高阶函数 (将函数作为参数或者返回函数,或两者都有;可操纵其它函数)
    • 递归 (满足一定条件之前调用自身的技术)
    • 组合 (将较小的函数组合成更大的函数,最终得到一个应用程序)
    17、Set、WeakSet、Map、WeakMap
    • Set
      • 成员唯一、无序且不重复
      • [value, value],键值与键名是一致的(或者说只有键值,没有键名)
      • 可以遍历,方法有:add、delete、has
    • WeakSet
      • 成员都是对象
      • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏
      • 不能遍历,方法有add、delete、has
    • Map
      • 本质上是键值对的集合,类似集合
      • 可以遍历,方法很多可以跟各种数据格式转换
      • 键与内存地址绑定,简单类型(number、string、boolean)的值严格相等则同一键,undefined与null不同键,NaN不严格等于自身但Map视为同一键
    • WeakMap
      • 只接受对象作为键名(null除外),不接受其他类型的值作为键名
      • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
      • 不能遍历,方法有get、set、has、delete

    18、布尔类型转换

    数据类型 转换成true的取值 转换成false的取值
    Undefined undefined
    Boolean true false
    Object 非null时都为true null
    Number 任何非零数字值(包括无穷大) 0和NaN
    String 非空字符串 ""(空字符串)

    19、位运算符

    &与,|或,~取反,^异或,>>左移,<<右移。

    20、ES5的类 vs ES6 class

    1. 提升
      • class声明会提升,但不会初始化赋值。Foo进入暂时性死区,类似let、const声明变量
      • es5的函数会进行函数提升
    2. class声明内部会启动严格模式
    3. class的所有方法(包括静态方法和实例方法)都是不可枚举
    4. class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用
    5. 必须使用 new 调用 class
    6. class 内部无法重写类名。
    7. 继承
      • ES5是通过call或者apply回调方法调用父类
      • ES6的继承实现在于使用super关键字调用父类

    21、async/await

    • await是一个让出线程标志。
    • await后面的表达式会先执行一遍。
    • 然后将await后面的代码加入到microtask中
    • 然后跳出整个async函数。

    async本身是promise+generator的语法糖。所以await后面的代码是miscrotask。

    22、异步编程

    1. 回调函数
    2. 事件监听
    3. Promise对象
    4. Generator
    5. async/await

    23、call & apply

    作用一样,区别在于传参的不同

    • 第一个参数都是指定函数体内this的指向
    • 第二个参数开始不同
      • apply是传入带下标的集合、数组或类数组,apply把它传给函数作为参数
      • call从第二个开次传入的参数是不固定的,都会传给函数作为参数
      • call比apply的性能都要好,call传参数正式内部所需要的格式(在ES6中可用解构语法替代apply的传参方式)
    实现bind、call、apply
    Function.prototype.myBind = function(ctx,...arg1){
        var fn = this
        return function(...arg2){
            return fn.call(ctx,...arg1,...arg2);
        }
    
    }
    Function.prototype.myCall = function(ctx,...args){
        const fn = Symbol('fn');
        ctx[fn] = this
        const result = ctx[fn](...args)
        Reflect.deleteProperty(ctx,fn);
        return result
    }
    Function.prototype.myApply = function(ctx,args){
        const fn = Symbol('fn');
        ctx[fn] = this
        const result = ctx[fn](...args)
        Reflect.deleteProperty(ctx,fn);
        return result
    }
    

    24、箭头函数

    • 没有this,从作用域链的上一层继承this(无法使用call/apply/bind)
    • 不绑定arguments
    • 不可使用yield(不能作为Generator)
    • 不可使用new命令(无this,无prototype:new命令执行时需要将构造函数的prototype复制给新的对象__proto__)

    25、判断浏览器

    function myBrowser() {
      var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串  
      var isOpera = userAgent.indexOf("Opera") > -1;
      if (isOpera) {
        return "Opera"
      }; //判断是否Opera浏览器  
      if (userAgent.indexOf("Firefox") > -1) {
        return "Firefox";
      }  //判断是否Firefox浏览器  
      if (userAgent.indexOf("Chrome") > -1) {
        return "Chrome";
      }   //判断是否Google浏览器  
      if (userAgent.indexOf("Safari") > -1) {
        return "Safari";
      } //判断是否Safari浏览器  
      if (userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1 && !isOpera) {
        return "IE";
      }; //判断是否IE浏览器  
    }
    

    26、垃圾回收机制

    没有被引用的对象就是垃圾,要被清除。几个对象引用形成一个环,相互引用,但是访问不到的,也是垃圾。

    • 标记清除
      • 标记阶段:把所有活动对象做上标记
      • 清除阶段:把没有标记(非活动对象)销毁
    • 引用计数
      • 跟踪记录每个值被引用的次数
      • 当引用次数0时,垃圾回收器下次运行时,就会是否次数为0的值所占用的内存

    27、块级作用域与函数声明

    // 浏览器的 ES6 环境
    function f() { console.log('I am outside!'); }
    
    (function () {
      if (false) {
        // 重复声明一次函数f
        function f() { console.log('I am inside!'); }
      }
    
      f();
    }());
    // Uncaught TypeError: f is not a function
    

    为了兼容旧代码,浏览器的实习可以不遵守规定(块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用):

    • 允许在块级作用域内声明函数。
    • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
    • 同时,函数声明还会提升到所在的块级作用域的头部。

    相当于如下:

     浏览器的 ES6 环境
    function f() { console.log('I am outside!'); }
    (function () {
      var f = undefined;
      if (false) {
        function f() { console.log('I am inside!'); }
      }
    
      f();
    }());
    // Uncaught TypeError: f is not a function
    
    28、表示一个字符的方式(6种)
    '\z' === 'z'  // true
    '\172' === 'z' // true
    '\x7A' === 'z' // true
    '\u007A' === 'z' // true
    '\u{7A}' === 'z' // true
    

    相关文章

      网友评论

          本文标题:3.JavaScript

          本文链接:https://www.haomeiwen.com/subject/dpyjwctx.html