前端面经基础部分总结

在今年春招的时候,有幸面试了小米以及好未来前端工程师的实习岗位,运气也比较好,都成功的拿到了实习的offer。下面是总结的我面试所复习以及面试过程中遇到的一些面试点

html与css篇

html语义化标签

去掉或者是样式丢失的时候,还是能让页面呈现清晰的结构,怎样理解这句话呢在学完css之后可能会发现一个问题,就是标签的样式之间可以相互转化。
例如:使用一个div标签,就可以通过css样式实现一些标签的样式。那么这就是问题的所在了,html语义化的效果就是能够使得页面呈现出清晰的结构,当去掉样式之后
还是能看出整个页面的结果,这样的好处有利于seo,利于辨别各标签所包含的内容的作用等。

初始化页面样式

初始化页面样式,为什么要初始化页面的样式呢?其实这并不难解答,因为浏览器的原因,因为不同的浏览器其标签的默认样式有的是有差异的,在开发页面时,我们需要考虑的就是用户使用的不同浏览器,那么我们写的页面效果就要考虑到如果用户用不同的浏览器,我们写的样式是否还是能达到我们所想要的效果,因此我们就需要初始化页面样式。例如: ** {margin: 0; padding: 0;}*这种采用通配符选择器的写法,其权重最低,也能达到我们想要的效果。这种兼容样式的话,有reset.css一个重置样式的css文件。也可以自己去定义页面的初始样式。

html5

对于h5部分,其实主要就是为html新增了一些语义化的标签,如音频、视频、拖拽(这里与js实现的拖拽有区别),地理位置等一些标签,在这里就不过多的去叙述,
具体的话可以去参考https://www.w3school.com.cn/html5/index.asp,在这里就是关于这些标签的用法

css权重

对于css权重,主要说的就是对于css中选择器中的优先级,它们之间采用的256进制,具体的优先级如下:

选择器权重值
! important无限大 //它们之间采用的256进制
行间样式1000
id选择器100
类选择器 属性选择器 伪类选择器10
标签 伪元素1
通配符0

DocType

告知浏览器是以何种规范来解析页面,常见的规范有html、xhtml

盒模型

盒模型中有两种是需要去注意的,盒模型都包括以下部分width height border margin padding
区别:

  • 在标准盒模型中,其width包括的只是content区域
  • 在IE盒模型中,其width包含的就是content+border+padding部分

清除浮动

当使用float后,就会产生浮动流,浮动流会对之后的页面布局有影响,清除浮动常常使用clear: both;
通常清除浮动的方式,采用伪元素选择器代码如下:
元素:after{
display: 'block;
content: ‘’;
clear: both;
}

伪类与伪元素

两者的区别在于以下的几种情况:

  • 表示方法
    • css2中伪类、伪元素都是以单冒号(:)表示,css2.1后规定伪类用单冒号,伪元素用双冒号(::)表示,浏览器同样接受
    • css2时已经存在的伪元素(:before, :after, :first-line, :first-letter等)的单冒号写法。对于css2之后的所有新增的伪元素
      如(::selection),应该采用双冒号的写法,但因为兼容性问题。大部分还是采用单冒号
  • 定义不同
    • 伪类,通常可以添加类来达到效果;伪元素,需要通过添加元素才能达到效果。
    • 可以同时使用多个伪类,但是只能同时使用一个伪元素
  • 本质上
    • 伪类本质上是为了弥补方法常规css选择器的不足,比便于获得更多的信息
    • 伪元素本质上是创建了一个有内容的虚拟器

flex布局

在2009年提出的一种布局方案,可以简便、完整、响应式地实现各种页面的布局。flex是flexible box的缩写,翻译为弹性布局。
具体的可参照https://www.ruanyifeng.com/blog/2015/07/flex-grammar.html进行学习

常见的布局方式

  1. 静态布局:传统的web设计,网页上的所有元素的尺寸一律用px作为单位
  2. 流式布局:页面的宽度按照屏幕分辨率进行适配调整,但是整体的布局不变,网页中主要划分区域的尺寸使用百分数(搭配min_, max_)
  3. 自适应布局:分别为不同的屏幕分辨率定义布局,即创建多个静态布局,每一个静态布局对应一个分别率。改变屏幕分辨率可以切换不同的静态布局,但是在每个静态布局中,页面元素不会随窗口大小的调整而发生变化。
  4. 响应式布局:通常的作法是流式布局加上弹性布局再加上媒体查询来实现。分别为不同的屏幕分辨率定义布局,同时,在每个布局中应用流式布局的理念,即页面元素宽度随着窗口调整而自动适配

css hack

针对不同的浏览器写不同的css code的代码就是css hack

css sprites

简称:css精灵,是一种网页图片的应用处理返回时,其允许将一个页面涉及到的所有图片都包含到一张大图中,利用css的"background"的组合进行背景定位,访问页面时避免图片载入缓慢的现象
优点:

  1. 减少了http请求,从而提高了页面的性能
  2. 减少了图片的字节
  3. 解决图片命名困扰,只需要对一张集合的图片命名
  4. 只需要修改一张或者少张的颜色或样式来改变整个网页的风格

缺点:

  1. 合并麻烦,需要将多张图片合成一张
  2. 图片适应性差,在高分辨率的屏幕下自适应页面,若图片不够宽,会出现背景断裂
  3. 定位繁琐:在使用时需要对使用的图片进行精确的定位
  4. 可维护性差:页面背景需要少许改动时,可能要修改部分或者整张图片

bfc触发方式

  1. float的值不为none
  2. overflow的值不为visible
  3. position的值不为relative和static
  4. display的值为table-cell,table-caption,inline-block值中的一个

标准模式与严格模式

从Ie6开始引入了standards模式,在此模式中,浏览器尝试给符合标准的文档在规范上的正确下达到指定浏览器中的程度。在IE6之前css还不够成熟,所以IE5等之前的浏览器对css的支持性很差,IE6将对css提供更好的支持,但是之前的旧网站基于旧的样式所写,因为在IE6之后将DTD作为一个"参数",如果该参数存在,则代表用standrads标准,不存在则是Quirks模式(即怪异模式、诡异模式)

src与href之间的区别

src用于替换当前元素,href用于在当前文档和引用资源之间确定联系,src是srouce的缩写,指定外部资源的位置,指定的内容将会嵌入到文档当前标签所在位置,在请求src资源时,会将其指向的资源下载并应用到文档中。当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到该资源加载编译,执行完毕,图片和框架等元素也如此,类似于将所指向资源嵌入到当前标签内部,这也是为什么将js资源放在底部而不是头部的原因

js部分

js数据类型

基本数据类型:boolean string number null undefined Symbol()
引用数据类型:object(Array / Date / RegExp)

类型判断方式

  1. 基本数据类型部分
    对于null: 可以使用String(null)判断,如果使用typeof对其进行判定返回会是object,这是因为历史遗留原因对于其余的基本类型:使用typeof进行判断,返回的格式就是字符串类型的变量的类型
  2. 引用类型部分:
    可以使用Object.prototype.toString.call()方法来进行判定

闭包

对于闭包,通俗来说,闭包产生于多个函数嵌套之间,当内层函数被保存到外面时,就会产生。这样说有点抽象看一个实际例子

funciton foo(){ 
   var a = 'hello word'
   function bar(){
     console.log(a);
   }
   return bar;
}
foo()

可以参考https://juejin.im/post/5b081f8d6fb9a07a9b3664b6博客来理解
闭包的优点:

  • 函数累加
  • 用作缓存结构
  • 封装
  • 模块化

闭包的坏处:

  • 造成原有作用域链不释放,造成内存泄漏

隐式类型转化

与隐式类型转化相对的就是显示类型转化,常使用ParseInt() ParseFloat() Number()方法,对于这几种,可以参考具体的方法来理解
对于隐式类型转化,其实常存在于下面的这几种情况

  1. 使用 == 符号时,使用+ - 符号来计算值时,需要记住的几个常用的案列有null == undefined 这个打印的结果为true
  2. 12 == ‘12’ 对于这种表示的形式其结果会为true,因为等式右边的这个字符串常量都会被Number()先将这个字符串类型的数据,转为数值,然后才来进行比较
    [ ] == ![ ] 这个表示的结果为true,类似于这种你就可以记住
    推荐一篇博客https://juejin.im/post/5a7172d9f265da3e3245cbca,在这上面有具体的关于隐式类型转化的方式与案列

变量提升

对于变量提升,发生的情况主要是用了var这个声明变量的方式,在es6之后出现的let, const这两种声明变量的方式,并不会出现这种变量声明。
那么对于变量提升,我们需要掌握的其实就是js中预编译的过程,当你了解到预编译之后,对于整个的变量提升就非常的清楚明白了。
掌握预编译首先要掌握的就是在js当中的全局变量与局部变量

  • 全局变量
    1. 在全局显示的定义: var(关键字) + (标识符),如 var t = 2;
    2. 隐式的定义:无论你在函数的内部还是在全局中写 t = 2,类似与这种形式的方式,都可以称t为全局变量,具体例子
      如: function (){ var a = b = 3;}
      此时的b为全局变量,而这里的a就为局部变量
  • 局部变量
    在function内部,定义变量时,采用上面所说的显示定义全局变量的方式,如 var a = 3;则此表示的就是局部变量,如上面所举出的那个例子,a为局部变量,b为全局变量。

当了解完这两个概念之后,那么就来理解函数的预编译过程,首先讨论是在函数中讨论函数的预编译发生过程,总共有四步:

  1. 生成一个AO对象(AO为执行期上下文)
  2. 找形参和变量声明,作为AO对象的属性,其值为underfine
  3. 形参和实参值相统一
  4. 在函数中找函数声明

举个栗子,来加深对方面的四句话的理解

function bar(a){
    console.log(a,b); // funtion a() {}, undefined
    var b = 3;
    console.log(a,b);//function a() {}, 3
    var a = 1;
    console.log(a,b); // 1,3
    function a(){}
    console.log(a); //1
}
bar(3);

对于上面的例子,首先先看到第一句话,生成了一个AO对象,然后,再来看第二句,在整个函数的形参和变量声明,在这个例子中只有a和b两个变量。在此时,两者的值都为undefined,然后再来看第三句话,形参和实参相统一,这时a由于传入的实参的值为3,那么此时a的值为3,b的值还是undefined,再来到第四句话,此时有个a的函数声明,那么此时a的值就变为了function,所以此时打印的结果就如上面注释后打印出的结果一样

全局中的预编译过程

  1. 生成一个GO对象(GO为执行期上下文)
  2. 找形参和变量声明,作为GO对象的属性,其值为undefined
  3. 在函数中找函数声明

在全局中预编译的过程和在函数中,类似,只是少了一个形参和实参相统一的步骤。这里就不多余的复述

作用域链与原型链

  • 作用域链:

    • 作用域链,对于作用域链,首先需要认识什么是自由变量,如果当前的作用域中想要输出一个变量,但是该变量在当前的作用域并没有声明。这种就叫做自由变量,举个栗子:
    var a = 100;
    function test(){
       var b = 1;
       console.log(a,b); //这里的a就是自由变量
    }
    test();
    
    • 作用域链含义:如果当前变量是自由变量,那么就会再向上一层寻找该自由变量,直到找到全局作用域还没有找到,就会宣布放弃,这种一层一层的关系就是作用域链。
  • 原型链

    • 原型对象:原型对象也是普通的对象,其带有一个自带的隐式的_proto_属性,原型也又可能是有自己的原型,如果一个对象的原型不为null的话,就称为原型链。原型链是由一些用来继承的共享的对象组成的(有限的)对象链。

    • 构造函数,实例原型以及实例之间的关系:

      1. 构造函数通过prototype指向实例原型,如:Person构造函数通过prototype指向Person.prototype
      2. 实例原型通过constructor指向构造函数,如:Person.prototype通过constructor指向Person构造函数
      3. 实例通过__proto__隐式的指向实例原型,如: person实例通过__proto__指向实例原型

数组与字符串常用方法

数组

对于数组,其基本的方法有

  • pop()
  • push()
  • shift()
  • unshift()
  • forEach()
  • filter()
  • map()
  • includes()
  • indexOf()
  • reduce()
  • reduceRight()
  • join()
  • slice()
  • splice()

举一个简单的例子,这个很简单的例子也有在面试中被要求写过
如:一个数组为[1,2,5,7,10,46,90],选出大于5的数并用’-'连接起来,如这个例子中输出 7-10-46-90
其实这个很简单先创建一个空串,用for循环,然后字符串拼接。实现的方式很多这里用一种较为简单的方法实现

  function getStr(arr){
     return arr.filter(item => item > 5).join('-');
  }

在日常写程序的过程中,数组的使用场景非常的多,对于里面的方法,都需要去掌握,具体关于每个方法参数以及性质可以参考https://developer.mozilla.org/zhCN/docs/Web/JavaScript/Reference/Global_Objects/Array这个网站,该网站比w3c上更新的更加的快并且健全,建议去参考api时,就可以在里面去参考

字符串

常用的一些方法如下:

  • split()
  • indexOf()
  • slice()
  • sub()
  • substr()
  • toString()

具体这些方法的使用,可以参考上面数组中提供的那个网站上去参考

事件循环

EventLoop:即事件循环,指浏览器或Node的一种解决js单线程不会阻塞的机制,其是一种执行模型,在不同的地方有不同的实现,浏览器和NodeJs都是基于不同的
技术实现了各自的EventLoop,可以概述为以下两点:

  1. 浏览器的EventLoop是在h5规范中所写
  2. noeJs的EventLoop是基于libvue实现的

理解事件循环,首先需要区分两个任务

  • 宏任务(macrotask),也叫tasks,一些异步执行的任务的回调会依次的进入macro task queue,等待后续被调用,这些任务包括
  1. setTimeout
  2. setInterval
  3. setImmediate(node独有)
  4. requestAnimation(浏览器独有)
  5. I/O
  6. UI rendering(浏览器独有)
  • 微任务(microtasks),也叫jobs,这些异步任务包括
  1. process.nextTick(node独有)
  2. Promise.then()
  3. object.observe()
  4. MutationObserve(注意: promise构造函数里的代码是同步执行的)

上面的这些任务,只需要记住就行了

浏览器中事件循环的机制

在浏览器中,只有一个执行栈和一个任务队列,在任务队列中放的是宏任务浏览器中事件循环的执行方式为:每从事件队列中取出一个事件时,有微任务就把微任务执行完,然后才开始执行事件(即从任务队列中去拿一个宏任务)
举个栗子

console.log(1)
setTimeout(function(){
    console.log(2);
},0)
new Promise(() => {
    console.log(3);
    setTimeout(() => {
        console.log(4);
    })
})
console.log(5);

具体的结果可以自己试试,然后结合在浏览器中的事件循环的执行方式来理解对于node中的事件循环机制,会涉及到六个队列,执行的方式也不一样,具体可以在网上看看博客

this指向

对于this指向只需要记住下面的几个点

  • 函数预编译过程中指向的就是window对象。
  • 全局作用域——>指向window
  • call/apply会改变函数运行的指向
  • obj.function(); function()里面的this指向的就是obj.谁调用了这个方法,this就指向谁。

上面的四点只需要记住就是了,这里就举一个例子说明下

var a = 5;
function test(){
  a = 0;
  console.log(a); 
  console.log(this.a);
  var a;
  console.log(a);
}
test();
new test();

上面这两种打印出的结果分别为 0 5 0 和 0 undefined 0;对于test()执行的这种方式,就是一个方法的调用,此时并没有某个对象去调用这个方法,因此此时this.a指的就是全局变量中的a,因此打印出的就是5。而在new test()就是新建一个对象的过程,在新建对象的过程中,this.a并没有指向的是window对象,也没有指明是当前的实例对象,因此在这里输出的就是undefined

数组去重

对于数组的去重方式有很多种,这里列举了其中的一些方法,关于去重,自己可以想一下或者去实现自己所能够想到的数组去重的方式

  • 利用Set数据结构进行去重
function unique(arr){
   return Array.from(new Set(arr));
}

这种方法所实现的去重无法去掉空对象

  • 使用双重for循环,然后使用splice去重
function unique(arr){
   for(let i = 0; i < arr.length - 1; i++){
       for(let j = i + 1; j < arr.length; j++){
           if(arr[i] == arr[j]){
                arr.splice(j, 1);
                j--;
           }
       }
   }
}
  • 使用indexOf去重
function unique(arr){
    if(! Array.isArray(arr)){
       console.log('type error');
       return;
    }
    var array = [ ];
    for(let i = 0; i < arr.length; i++){
       if(array.indexOf(arr[i]) === -1){
          array.push(arr[i]);
       }
     }
     return array;
}
  • 利用sort()
function unique(arr){
    if(! Array.isArray(arr)){
       console.log('type error');
       return;
    }
    arr = arr.sort(); //使用此方法的目的是为了将重复的内容放在一起
    let array = [arr[0]];
    for(let i = 1; i < arr.length; i++){
        if(arr[i] !== arr[i - 1]){
           array.push(arr[i]);
        }
    }
    return array;
}
  • 利用includes方法
function unique(arr){
  if(! Array.isArray(arr)){
       console.log('type error');
       return;
    }
    let array = [ ];
    for(let i = 0; i < arr.length; i++){
       if(!array.includes(arr[i])){
             array.push(arr[i]);
       }
    }
    return array;
}
  • 利用hasOwnproperty
function unique(arr){
   let obj = { };
   return arr.filter(function(item, index, arr){
        return obj.hasOwnProperty(typeof, item + item) ? false : (obj[typeof item + item] = true)
   })
}

使用hasOwnProperty判断是否存在对象属性

  • 利用filter
   function unique(arr){
        return arr.filter((item, index, arr) => {
            return arr.indexOf(item, 0) === index;
        })
 }

这里就不全部举例完了,还有一些方式,可以自行参考一些博客

深浅克隆

对于深度克隆与浅度克隆,无非就是对于对象的拷贝问题进行的一个探讨,对于基本类型的赋值就是值传递,对于特殊类型对象的赋值,是将对象地址的引用赋值,这时候修改对象中的属性或者值,会导致所有引用的这个对象的值得改变,如果想要真正的赋值一个新的对象,而不是复制对象的引用就会用到深拷贝

  • 浅拷贝:
  1. 使用=符号
let str1 = {
    a: 1,
    b: 2,
    c: 3,
    d: {
        e: 5
    }
}
let str2 = str1;
str2.d.e = 4;
console.log(str1,str2)

对于这种形式的对象的拷贝,当更改掉str2中的内容后,会导致str1中的改变

  1. Object.assign()
    该方法实际上是一个浅拷贝的过程,与=的区别在与Object.assign()可以处理一层深拷贝
  • 深拷贝
  1. 使用JSON.parse(JSON.stringify(str))进行拷贝,但是该方法对于function不进行任何拷贝
  2. 使用for in进行深拷贝具体实现方式如下
function deepclone(target, origin){
    var target = target || {},
    toStr = Object.prototype.toString;
    arrStr = '[object Array]';
    for(var item in origin){
        if(origin.hasOwnProperty(item)){
            if(origin[item] !== null && typeof(origin[item]) == 'object'){
                if(toStr.call(origin[item]) === arrStr){
                    target[item] = [];
                }else{
                    target[item] = {};
                }
                deepclone(target[item],origin[item]);
            }else{
                target[item] = origin[item];
            }
        }
    }
}

内存泄漏

  • 内存泄漏指得是任何对象在不再拥有或需要它之后缓存在的情况
  • 垃圾回收机制,会定时扫描对象,并计算引用每个对象的其他对象的数量,如果一个对象的引用数量为0(即没有其他对象引用该对象),或该对象的唯一引用是循环的,那么该对象的内存即可回收
  • 造成内存泄漏的几种情况
  1. setTimeout中的第一个参数使用字符串而不是函数时
  2. 闭包
  3. 控制台日志
  4. 循环(两个对象彼此引用,并且彼此保留时,就会产生一个循环)

函数柯里化

即通过多次的传参使得某个函数的参数达到饱和,具体实现思想如下

  1. 首先实现将函数的参数拆分为两个部分,即下面的FixedParame方法的实现
  2. 然后再采用递归的思想,将函数传入的参数进行多次划分,直到达到最大化
function FixedParame(fn){
   var _args = [].slice.call(arguments, 1);
   return function(){
       var newArgs = _args.concat([].slice.call(arguments, 0));
       return fn.apply(this, newArgs);
   }
}
function newCurry(fn, length){
   var length = length || fn.length;
   return function(){
       if(arguments.length < length){
           var cobined = [fn].concat([].slice.call(arguments, 0));
           return newCurry(FixedParame.apply(this, cobined),length - arguments.length);
       }else{
           return fn.apply(this, arguments);
       }
   }
}

组合函数

其目的就是将几个函数的功能给组合在一起,下面封装一个组合函数,在使用函数时,只需要将函数传入即可

function compose(){
    //将类数组转化为数组,这样才能使用数组方法
    var args = Array.prototype.slice.call(arguments);
    var len = args.length - 1;
    return function(x){
        var result = args[len](x);
        while(len--){
            result = args[len](result);
        }
        return result;
    }
}

抖动与节流

  • 节流
    对于节流要明白两个点:
  1. 节流的目的是为了防止网站遭到恶意的攻击
  2. 节流函数的封装过程

下面就是封装的一个节流函数

function throttle(handler, wait){
    var initTime = 0;
    return function (e){
        var nowTime = new Date().getTime();
        if(nowTime - initTime > wait){
            handler.apply(this, arguments);
            initTime = nowTime;
        }
    }
}
  • 抖动
    对于抖动函数明白两点:
  1. 抖动函数的目的函数在频繁发生时,在等待一段时间后去执行的行为
  2. 抖动函数的封装过程

下面就是抖动函数的封装实现

function debounce(handler, delay){
    var timer = null;
    return function(){
        var _self = this, _args = arguments;
        clearTimeout(timer);
        timer = setTimeout(function(){
            handler.apply(_self, _args);
        },delay);
    }
}

函数扁平化

函数的扁平化,其目的其实就是将一个多维数组变为一个一维数组的过程。在Array中有一个flat(),其作用就是如此,接下来就手动实现falt()函数

function flatten(){
    var arr = arr || [];
    var toStr = Object.prototype.toString;
    this.forEach(item => {
        return toStr.call(item) == '[object Array]' ? arr = arr.concat(item.flatten()): arr.push(item);
    })
}

事件冒泡与事件捕获

对于了解事件冒泡和事件捕获,其实就是事件流的一个接收事件顺序,而事件流描述的就是从页面中去接收事件的顺序
事件流分为三类:

  • 事件冒泡流:从内到外进行事件的捕获 (IE事件流)
  • 事件捕获流:从外到内进行事件的捕获(NetScape事件流)
  • DOM事件流:事件捕获阶段——目标——事件冒泡阶段

对于这里需要去加一个问题进行讨论,也是在面试中可能被问道的问题
IE和DOM事件流之间的区别:

  1. 执行顺序不一样
  2. 事件参数和this指向不一样
  3. 事件加不加on

ajax

是一种用于创建快速动态网页的技术,通过在后台与服务器进行少量数据交换,ajax可以使网页实现异步更新,可以在不重新加载整个页面的情况下,对页面的某个部分进行更新。
以下的部分是ajax原理实现过程

  • XMLHttpRequest对象
    当前现代浏览器中均支持XMLHttpRequest对象(IE5和IE6使用ActiveXObject),因此为了应对所有的浏览器,包括(IE5和IE6),则在创建xmlhttp时,需要通过下面的这种方式来创建
var xmlHttp;
if(window.XMLHttpRequest){
    xmlHttp = new XMLHttpRequest();
}else{
    xmlHttp = new ActiveXObject("Microsoft.XMlHTTP");
}

当创建好xmlHttp之后,如果需要将请求放到服务器,需要使用XMLHttpRequest对象的open()和send()方法
对于get和post方法,在使用时的情况是不一样的,大多数的情况下都可以使用get请求,但是在下面的情况中使用post请求

  1. 无法使用缓存文件(更新服务器上的文件或数据库中的文件时)
  2. 向服务器发送大量数据(post没有数据量的限制)
  3. 发送包含未知字符的用户输入时,post比get更稳定也更加的可靠

如果需要通过GET方法发送信息,并且向URl添加信息:

xmlhttp.open("GET","demo_get2.asp?fname=Bill&lname=Gates",true);
xmlhttp.send();

如果需要通过POST那样传送数据,还需要对传入的数据做设置,因此会设置请求头,如下所示:

setRequestHeader()来添加HTTP头
xmlhttp.open("POST","ajax_test.asp",true);
xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xmlhttp.send("fname=Bill&lname=Gates");
  • 说到这里需要停顿以下,这里GET和POST请求是什么呢?
    这个是应用层中http请求中的两种前后台数据交互的一种方式,在现如今的都是采用这种数据交互方式来实现前后端的交互,这样做的目的是为了前后端的分离,这样做的好处很多如上面所说的加载数据时,只需要更新一部分的内容即可,另外在开发时间上只要前后端做好数据规范,能做到前后端同时进行开发,大大减少开发项目的周期,另外在项目的可维护性上也有很大程度上的帮助等。在这里所涉及到的http协议部分,也是我们前端所需要了解的部分,这部分会在下面的网络部分进行讲解
  • 上面的部分是将前端的数据发送给服务器,那么对于服务器返回的数据应该怎样处理呢?
    这时就要用到onreadystatechange事件,当请求被发送到服务器时,需要执行一些基于响应的任务,每当readtState改变时,就会触发onreadystatechange事件。readyState属性存在XMLHttpRequest的状态信息。因此在这里就有关于XMLHttpRequest对象的属性
属性描述
onreadystatechange存储函数(或函数名),每当 readyState 属性改变时,就会调用该函数。
readyState存有 XMLHttpRequest 的状态。从 0 到 4 发生变化。0:请求未初始化1: 服务器连接已建立2: 请求已接收3: 请求处理中 4: 请求已完成,且响应已就绪
status200: “OK”,404: 未找到页面

当服务器响应已经做好被处理的准备时,即就有以下的部分

xmlhttp.onreadystatechange=function()
  {
  if (xmlhttp.readyState == 4 && xmlhttp.status ==  200)
    {
    document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
    }
  }

在上面的代码中if部分就是表示服务器已经准备好了的描述过程,document.getElementById(“myDiv”).innerHTML=xmlhttp.responseText;中的responseText
表示服务器返回给前端的数据。上面的所有部分就是ajax整个原理的过程,在jq中也有封装好的ajax技术参考https://www.w3school.com.cn/jquery/jquery_ajax_get_post.asp,又或者在react或者是vue中(其依赖包axios,在npm官网上可以查看其用法)

js继承实现方式:

  • 原型链继承
function Person(name, age){
    this.name = name;
    this.age = age;
}

function Student(school){
    this.school = school;
}

Student.prototype = new Person();

const student = new Student();

原型链继承缺点:多个实例对引用类型的操作会被篡改

  • 构造函数继承
function Car(name, color, size){
   this.name = name;
   this.color = color;
   this.size = size;
}

function Baoma(name, color, size, model){
   Car.call(this, name, color, size);
   this.model = model;
}

利用call方法,来实现继承
缺点:

  1. 只能继承父类的实例属性和方法,不能继承原型属性/方法
  2. 无法实现复用,每个子类都有父类实例函数的副本,影响性能
  • 组合函数继承
    用原型链实现对原型属性和方法的继承,用构造函数来实现实例属性的继承
function Animal(type, size, food){
    this.type = type;
    this.size = size;
    this.food = food;
}

Animal.prototype.sayName = funciton(){
    console.log(this.name);
}


function Dog(type, size, food, age){
    Animal.call(this, type, size, food);
    this.age = age;
}

Dog.prototype = new Animal();

缺点:在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法

  • 原型式继承
    利用一个空对象作为中介,将某个对象直接复制给空对象构造函数的原型
function object(obj){
    function F(){};
    F.prototype = obj;
    return new F();
}

即object()对传入其中的对象执行了一次浅复制,将构造函数F的原型直接指向传入的对象。
缺点:

  1. 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能
  2. 无法传递参数
  • 圣杯模式继承(寄生组合式继承)
    这样实现的方式好处在于,当改变继承对象的属性时,并不会更改父类的原型
var inherit = (function(){
    var F = function(){};
    return function(Target, Origin){
        F.prototype = Origin.prototype;
        Target.prototype = new F();
        Target.prototype.constructor = Target;
        Target.prototype.uper = Origin.prototype;
    }
})()

也可以使用下面的方式来实现,,借用构造函数传递参数和寄生模式实现继承

function inheritPrototype(target, origin){
    var prototype = Object.create(origin.prototype);
    prototype.constructor = target;
    target.prototype = prototype;
}
  • 使用es6中的extends

图片懒加载与预加载

对于图片的懒加载和预加载可参考博客https://juejin.im/post/5b0c3b53f265da09253cbed0

  • 懒加载:也叫延迟加载,指的是在长网页中延迟加载图想,是一种很好优化网页性能的方式
    优点:
  1. 能提升用户的体验
  2. 减少无效资源的加载
  3. 方式并发加载的资源过多会阻塞js的加载
  • 预加载:将所有所需资源提前请求加载到本地,之后再取用时,就直接从缓存中拿资源,具体原理以及实现方式参照上述博客

浏览器部分

浏览器中的缓存

cookie、sessionStorage和localstorage之间的区别

  • sessionStorage: 在sessionStorage中的数据,只能在同一个会话页面才能访问,并且当会话结束后数据也会随之销毁,因此其是一个会话级别的存储。
  • localStorage: 用于持久化的本地存储,除非主动删除数据,否则数据用于不会过期
  • cookie: cookie的作用是与服务器进行交互,作为http规范的一部分而存在,记录客户端的用户信息,其大小限制在4KB左右
  • sessionStorage和localStorage的大小大概为5M左右

浏览器缓存机制

对于缓存其存在的意义在于可以加快相应事件,提高用户的体验,对于一般的html文件,浏览器会自动访问,对于ajax请求所发送的数据,有时也需要缓存,需要注意的是post请求浏览器是不会进行缓存的。

  • http状态码304
    该状态码代表的意思是服务器不会给我们数据,因此当前浏览器中有缓存的数据,此状态码代表使用缓存,当浏览器看到此状态码就应该去拿缓存中的数据
  • 浏览器中缓存方式
  1. 协商缓存:根据前后台状态来判定是否要进行缓存
    ETags和If-None-Match

    • ETags是响应头中的内容,If-None-Match是请求头中内容,首次请求结束后会将ETags设置在If-None-Match中,第二次请求时,则会将ETags传输到服务器中
      对比ETags如果相同(表示两次请求的内容相同),则服务器会返回304状态码,这时浏览器应该取缓存中的数据

    last-Modified和If-Modified-since

    • last-Modified是响应头中的内容,If-Modified-since是请求头中的内容,其发生的过程与上面的类似,如果对比两次结果相同,这时服务器就会返回304状态码
  • 强制缓存:强制设定缓存数据的存储时间,一到时间就清空缓存数据
  1. Cache-control: 请求头部和响应头部都可以包含该字段,此字段是用来设置浏览器缓存的保留时间,可设置为以下值:
  2. no-Cache: 不缓存
  3. no-Store: 用于防止重要的信息被无意的发布
  4. max-age: 指示客户机可以接收生存期不大于指定时间的响应
  5. min-fresh:指示客户机可接收响应时间小于当前时间加上指定时间的响应
  6. max-stale: 指示客户机可以接收超出超时间的响应时间
  7. Expires: 内容保质器,若其和max-age同在则会被覆盖掉,其参数值设定时,可直接设置时间

浏览器内核

  • 浏览器内核主要有以下部分组成
    • 渲染引擎
    • js引擎
      • 执行栈
    • 事件触发线程
      • 消息队列
    • 网络异步线程
    • 定时器线程

网络部分

http协议

http协议规定了浏览器怎样向万维网服务器请求万维网文档,以及服务器怎样把文档传送给浏览器,从层次角度看,其是面向事务的应用层协议,是万维网上能够可靠地传递文件(文本、声音、图像等各种多媒体文件)的重要基础。其协议本身是无连接的,但是其使用了面向连接的TCP作为运输层协议,保证了数据的可靠性传输。

因此http协议在发送请求时,首先需要和服务器建立TCP连接,当建立TCP连接的三报文握手的前两部分完成后,万维网客户就把请求报文,作为TCP连接的三报文握手
中的第三个数据发送给万维网服务器,服务器收到HTTP请求报文后,就把所有请求的文档作为响应报文返回客户。因此整个网络的请求过程可以描述为以下内容:

  • 网络请求的过程:

    • 浏览器通过DNS域名解析到服务IP
    • 客户端(浏览器)通过TCP协议建立到服务器的TCP连接(三次握手)
    • 客户端(浏览器)向web服务器端(HTTP服务器)发送HTTP协议包,请求服务器里的资源文档 (telnet模拟)
    • 服务器向客户端发送HTTP协议应答包
    • 客户端和服务器断开(四次挥手),客户端开始解释处理HTML文档
  • 对于get请求与post请求的常规理解

    • GET使用URL或Cookie传参,而POST将数据,放在BODY中。
      在GET请求中它的参数是要拼接到URl后面的,POST请求它的请求data是拼接在请求主体中

    • GET的URL会有长度上的限制,POST可以传输很多数据。
      GET在长度上有限制是因为请求的data放在URL后面,URL的输入框是有限制的
      POST传输的数据也会有一定的限度,因为这是为了安全性的考虑,为了防止恶意攻击

    • 可见性。
      GET请求中的数据在URL栏中是可见的,不要将用户名等私密信息放在GET请求中

http请求中请求报文与响应报文的内容

  • http(请求报文,响应报文)通过报文进行沟通
    请求报文:
    请求头 请求行 请求主体
    请求头: 在请求头中的内容包括 1. 请求方式 2.请求url 3. http协议及版本

  • 相应报文
    响应头 响应行 响应主体

http各个版本之间的区别

  • 1.0和1.1之间的区别
  1. 缓存处理,在HTTP1.0中主要使用header里面If-Modified-Since,Expires来作为缓存判断的标准。HTTP1.1则引入了更多的缓存控制等策略,例如:Enity tag, If -unmodifiedSince,If-Match等更多可供选择的缓存头来控制缓存策略
  2. 带宽优化及网络连接使用
  3. 错误通知管理,在1.1中新增了324个错误状态码
  4. Host头处理,1.1中认为每台服务器都绑定了一个唯一的ip地址,在请求消息中的URL并没有传递主机名,但随着虚拟主机技术的发展,一台物理服务器上可存在多供虚拟主机,并且共享一个ip地址,
    HTTP1.1的请求消息和响应消息都支持Host头域,且如果请求消息中没有Host头域会有(400的错误码)
  5. 长连接,1.1协议的持续连接方式有两种,即是一个在TCP连接上可以传送多个请求和响应,在1.1种默认是打开了connection: keep-alive,一定程度上弥补了1.0每次都要创建连接的特点
  • https和http之间的一些差别
  1. https是需要到CA申请证书,需要进行交费,费用很少
  2. http运行在tcp上,所有传输的内容都是明文,https运行在SSL/TLS之上,SSL/TLS运行在TCP之上,所有的传输内容都经过加密
  3. HTTP和HTTPS使用的是完全不同的连接方式,HTTP默认端口80 HTTPS默认端口443
  4. HTTPS可以有效的防止运营商劫持,解决了防劫持的一个大问题(防劫持:在使用webApp,流量里既有通信数据又有程序代码与界面)
  • SPDY:HTTP1.x的优化
    SPDY方案优化了HTTP1.x的请求延迟,解决的了HTTP1.x的安全性
  1. 降低延迟,针对HTTP高延迟问题,SPDY采取了多路复用的方式,多路复用通过请求stream共享一个tcp连接方式,解决HOL Blocking的问题,降低了延迟的同时提高了带宽的利用率
  2. 请求优先级。由于多路复用带来的共享连接时。关键请求可能会导致请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应
  3. header压缩,HTTP1.x的header很多时候都是重复多余的,选择合适的压缩算法可以减小包的大小和数量
  4. 基于HTTPS的加密协议传输
  5. 服务器端推送,服务端可将文件推送给客户端,下次访问文件可以从缓存中获取。
  • HTTP2.0和SPDY方案之间的差异
  1. HTTP2.0支持明文传输,而SPDY强制使用HTTPS
  2. HTTP2.0消息头的压缩算法采用HPACk,而非SPDY采用的DEFLATE
  • HTTP2.0和HTTP1.x相比的新特性
  1. 新的二进制格式
  2. 多路复用
  3. header压缩
  4. 服务器端推送
  • HTTP2.0的升级改造
  1. HTTP2.0可以支持非HTTPS的。当当今主流的浏览器像chrom,firefox表示还是支持基于TLS部署的HTTP2.0协议所以想要升级为2.0,先升级https协议为好
  2. 当网络升级为https之后,升级2.0只要在nginx对应的配置文件中启动相应的协议就行
  3. 当网络升级为2.0之后,不用担心其不支持http1.x因为,2.0满足向下兼容的语义,对于不支持2.0的浏览器,NGINX会自动向下兼容
  • 2.0中的多路复用与1.x中的长连接复用之间的区别
  1. 1.*:一次请求一次响应,建立一个连接,用完关闭;每个请求都要建立一个连接
  2. 1.1:pipeling解决方案,若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某个请求超时等,后续请求只能被阻塞,毫无办法,也就是常说的线头阻塞
  3. 2.0多个请求同时在一个连接上并行执行时,某个请求任务耗时严重,不会影响到其他连接的正常执行
  • 为什么需要头部压缩(header压缩)?
    假定一个页面100供资源需要加载,而每次请求都有1kb的消息头,则至少需要消耗100kb来获取消息头,2.0可以维护一个字典,差量更新http头部,大大降低因为头部传输而产生的流量

  • 2.0中多路复用的好处?
    http性能优化的关键并不在于高带宽而是低延迟。TCP连接会随着时间进行自我“调谐”,起初会限制连接的最大速度,如果数据成功传输,会隋卓时间的推移,提高传输的速度,在这种调谐则被称为TCP慢启动。由于这种原因,让原本具有突发性和短时性的HTTP连接变得十分低效。2.0通过让所有数据流共用同一个连接,可以有效的使用TCP连接,让高带宽能真正的服务于http的性能

  • 服务器推送是什么?
    把客户端所需要的资源伴随着index.html文件一起发送到客户端,省去客户端重新请求的过程

同源策略

浏览器中有一个很重要的概念–同源策略,所谓的同源是指,域名,协议,端口相同。不同源的客户端脚本在没有明确授权的情况下不能读写对方的资源,只能去访问同源的文件
在https://www.baidu.com/中,其中的http指的是协议,www.baidu.com指的是域名,在.com后面加上:440。这个表示的就是默认的端口号,如果是默认的端口号,则不需要在访问网站的时候去写端口号。

  • http默认的端口号是:80

  • https的默认的端口号是:443 https是在http的基础上加了SSL层而形成的,其安全性更高

  • 域名解析,在进行域名解析时,其解析的过程是倒着解析的
    一级域名 .com 二级域名 baidu.com 三级域名 zhidao.baidu.com

相关域名代表的含义
com org net 属于顶级域名,是在全世界范围内解析的,cn hk是在一个地区解析的,如:

  • .cn 中国
  • .com(商业机构)
  • .net(从事互联网服务的机构)
  • .org(非盈利性组织)
  • .com.cn (国内商业机构)
  • .net.cn(国内从事互联网服务机构)
  • .net.org(国内非盈利性组织)

dns先根据顶级域名判断网络范围再根据域名查找主机ip地址,理论上www开头相当于占用为的 在国外一般不写www

同源策略的解决方式:

  1. flash现目前不常用,因此不做讨论
  2. 服务器代理中转
  3. jsonp
  4. document.domain(针对基础域名相同的情况)
    bj.58.com document.domain = ‘58.com’
    tj.58.com document.domain = ‘58.com’
各种跨域的实现原理与方法

服务器代理中转:

首先明白的一点就是同源策略是浏览器与服务器中间存在的,而服务器与服务器之间不存在同源策略问题。因此如果想要实现从浏览器跨域到其他服务器,可以采用的方式是先将浏览器中的请求发送给与自己端口、协议、域名相同的服务器当中,再通过这个服务器与其他服务器进行数据之间的请求,从而就能实现跨域的过程document.domain(针对基础域名相同的情况)采用这种方式去处理跨域问题时,必须有一个要求就是基础域名必须是相同的情况下,才能够用这种方式

JSONP原理:

  1. 在web页面上用script标签引入js文件时则不受是否跨域的影响。不仅如此,我们还发现,凡是拥有src这个属性的标签
    都拥有跨域的能力,比如 script img iframe
  2. 于是就可以把数据放在服务器上,并且数据为json形式(因为js可以轻松的处理json数据)
  3. 因为无法监控script的src属性是否把数据获取完成,所以我们需要做一个处理
  4. 实现定义好处理跨域获取数据的函数,如function doJSON(data){}
  5. 用src获取数据的时候提那家一个参数cb = ‘doJSON’(服务端会根据参数cn的值返回 对应的内容) 此内容为以cb对应的值doJSON为函数真实要传递的数据为函数的参数的一串字符

在这里需要注意就是,在使用src引入文件的时候,src其实不管文件的格式是什么类型,只要文件中含有需要的数据就可以进行引入。采用的jsonp的请求方式都是get请求

实现代码如下:

function addScriptTag(src) {
    var script = document.createElement('script');
    script.setAttribute('type', 'text/javascript');
    script.src = src;
    document.body.appendChild(script);
}

window.onload = function() {
    addScriptTag("http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=apple&callback=result");
}

//自定义回调函数,上面的http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=apple&callback=result中的result就是该函数
function result(data) {
    console.log(data);
}

跨域资源共享CORS:
注意:CORS需要浏览器和服务器同时支持,目前,所有的浏览器都支持该功能,IE浏览器不能低于IE10
整个CORS通信过程,都是浏览器自动完成,不用用户参与。其余AJAX通信没有区别,代码完全一样,浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,还会多一次附加的请求,但是用户不会有感觉。实现CORS通信的关键是服务器,只要服务器实现CORS接口,就可以实现跨源通信。

两种请求:
浏览器将CORS请求分成两类:简单请求和非简单请求

  • 简单请求
  1. 请求方法是以下三种方法之一
    head
    get post
  2. http的头信息不超出以下几种字段
    Accept
    Accept-Language
    Content-Language
    Last-Event-ID
    Content-Type: 只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

凡是不满足上面两个条件的就属于非简单请求

  • 简单请求实现基本流程:
    • 首先浏览器需要发送的是那个CORS请求,具体来说,就是在头信息中增加一个Origin字段用于说明本次请求来自哪个源,服务器根据这个值,决定是否同意这次请求

    • 如果Origin指定的源不在许可范围内,服务器会返回正常的Http回应,但是这个回应的头信息没有包含Access-Control-Allow-Origin字段,就会出错

    • 如果成功,那么服务器返回的响应会多出几个头信息字段

      • Access-Control-Allow-Origin:该字段是必须的,它的值要么是请求时Origin字段的值,要么就是*,表示接受任意域名的请求。
      • Access-Control-Allow-credentials:该字段可选,它的值是一个布尔值,表示是否允许发送cookie,默认情况下,Cookie不包括在CORS请求之中,设置为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值只能设置为true,如果服务器不要浏览器发送Cookie,删除该字段即可
      • Access-Control-Expose-Headers:该字段可选,CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段: Cache-Control Content-Type
      • Content-Language Expires Last-Modified Pragma 如果想要获取其他字段必须在Access-Control-Expose-Headers中指定字段的值
    • 非简单请求实现基本流程:
      非简单请求是对服务器有特殊要求的请求,比如请求方法是put或者delete,或者Content-Type字段的类型是application/json

      • 预检请求:

        非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,即浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词
        和头信息字段,只有得到肯定大于,浏览器才会正式的发送XMLHttpRequest请求。

        预检请求用的请求方法是options,表示这个请求是用来询问的,除了Origin字段,预检请求得头信息包括两个特殊字段

        • Access-Control-Request-Method:该字段是必须得,用来列出CORS请求会用到的哪些HTTP方法,
        • Access-Control-request-Headers:用来指定浏览器CORS请求会额外发送的头信息字段

        预检请求的回应:

        服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。如果否定了预检
        请求,会得到一个正常的Http回应,但是没有任何CORS相应的头信息字段,这时在浏览器就会报错,此时服务器回应的CORS字段如下

        • Access-Control-Allow-Methods:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
        • Access-Control-Allow-Headers:如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
        • Access-Control-Allow-Credentials:该字段与简单请求时的含义相同。
        • Access-Control-Max-Age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

        如果浏览器通过预检,浏览器就会和简单请求一样,会有Origin头信息

安全部分

在进行项目的开发时,作为一名前端开发人员也需要考虑到网络的安全,作为前端主要涉及到的安全问题就以下两种

xss攻击

可以将xss攻击分为以下两类

  • 反射性:通过url参数直接注入,一般是使用alert来探测站点是否防御,直接攻击使用src来引入脚本
  • 存储性:存储到DB(数据库)读取时注入(危害很大),主要是在输入框中输入script标签,这样数据就是存储在数据库中,如果该页面读取出script中的内容就会导致其他人也会受到攻击
  • xss攻击注入点:
    • html节点内容
    • html属性
    • js代码
    • 富文本

五种防御方式:

  • HTML节点内容的防御
    转义掉<>即转义掉<>即可,转义的时机有两种,一种是写入数据库时进行转义,另外一种是在解析的时候进行转义
  • HTML属性的xss防御
    转义“&quto”即转义掉双引号,转义掉单引号
    注意:实际上html属性可以不包括引号,因此严格的说,还需要对空格进行转义,但是这样会导致渲染的时候空格数不对,因此不转义空格,然后在写html属性的时候
    全部都带上引号
  • html转义函数
  • js转义
    注意掉""或者替换成json
  • 富文本
    由于需要完整的HTML,因此不太容易过滤,一般是按照白名单进行保留部分标签和属性来进行过滤,除了允许的标签和属性,其他的全部不允许(也有黑名单方式,但是由于html复杂效果比较差,其原理就是正则匹配)

csrf攻击

csrf:跨站点请求伪造,攻击者盗用你的身份,以你的名义发送恶意的请求,从而达到攻击者所期望的一个操作

csrf攻击原理及过程:

  1. 用户打开浏览器,访问受信任的网站A,输入账号登录进A
  2. 登录成功后,产生cookie返回给浏览器,此时用户登录A,再正常发送请求到网站A
  3. 用户并没有退出站点A之前,在同一浏览器中,通过tab页打开了网站B
  4. 网站B接收到用户请求后,返回一些攻击代码,并发送一个请求访问A
  5. 这样A就会接收到B的恶意代码执行

csrf的防御方式:

  • 验证Http中的Referer字段(该字段用于记录该HTTP请求的来源地址)
  • 在请求地址中添加taken并验证(当用户登录成功后可将产生的taken放在session中,然后服务器可创立一个拦截器来验证这个taken)
  • 在HTTP头中自定义属性并验证(通过XMLHttpRequest这个对象,可一次性给所有的请求都加上taken这个头部属性,并把taken放入其中)

上面大多都是基础部分,都是需要了解的地方。当然还要准备一下算法个人比较推荐的就是力扣还有剑指offer

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值