JavaScript笔记04

一、面向对象

1.1 面向对象之super关键字

让我们先分析一段错误代码

<script>
	class Father {
        // 构造器
		constructor(x, y) {
			this.x = x
            this.y = y
        }
        sum() {
            console.log(this.x + this.y)
        }
        say() {
            return '我是爸爸'
        }
	}
    // 继承父类
    class Son extends Father{
        // 构造器
        constructor(x, y) {
            this.x = x
            this.y = y
        }
    }
    let son = new Son(1, 2)
    son.sum()
</script>

以上代码会报如下错:Uncaught ReferenceError: Must call super constructor in derived class before accessing ‘this’ or returning from derived constructor。

因为当调用子类Son时传递的参数调用的是子类构造函数中的this,而父类的函数sum()中的this使用的是父类构造函数的this,因此会报错。要想解决这种报错就需要使用的super关键字。

super关键字用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数。

将上述代码的子类做如下修改即可

Class Son extends Father{
	constructor(x, y) {
		super(x, y)  //调用了父类中的构造函数
	}
}

使用super调用父类的普通函数演示

Class Son extends Father{
	say() {
		console.log(super.say() + '的儿子')
		// super.say()就是调用父类中的普通函数say()
	}
}
var son = new Son()
son.say()  // 我是爸爸的儿子

子类 继承父类求和方法(需要使用super),同时 扩展求差方法

<script>
    class Father {
        // 构造器
        constructor(x, y) {
            this.x = x
            this.y = y
        }
        sum() {
            console.log(this.x + this.y)
        }
    }
    // 继承父类
    class Son extends Father{
        // 构造器
        constructor(x, y) {
            // 利用super 调用父类的构造函数
            super(x, y)
            this.x = x
            this.y = y
        }
        substract() {
            console.log(this.x - this.y)
        }
    }
    let son = new Son(5, 3)
    son.sum()  // 8
    son.substract()  // 2 
</script>

使用super的一个注意点:子类在构造函数中使用super,必须放到this前面(必须先调用父类的构造方法,再使用子类构造方法)

使用类的两个注意点:

  1. 在 ES6 中类没有变量提升,所以必须先定义类,才能通过类实例化对象
  2. 类里面的共有属性和方法一定要加this使用
<button>点击</button>
<script>
    class Bangumi {
        constructor(uname, age) {
            this.uname = uname;
            this.age = age;
            this.btn = document.querySelector('button')
            this.btn.addEventListener('click', () => {
                this.sing()
            });
        }
        sing() {
            console.log(this.uname)
        }
    }
    let any = new Bangumi('阿尼亚')
</script>

案例

需求:面向对象版tab栏切换

抽象对象:Tab对象

  1. 该对象具有切换功能
  2. 该对象具有添加功能
  3. 该对象具有删除功能
  4. 该对象具有修改功能

在这里插入图片描述

<script>
    // 全局变量
	let that
    class Tab {
        constructor(id){
            // 全局变量指向this
            that = this
            this.main = document.querySelector(id)  // 获取总页面
            this.add = this.main.querySelector('.tabadd')  // 获取加号
            // 获取li的父元素,便于插入新的选项卡
        	this.ul = this.main.querySelector('.firstnav ul:first-child')
            // 获取section的父元素,便于插入新的页面
            this.section = this.main.querySelectot('.tabscon')
            this.init()  // 初始化
        }
    }
    // init 初始化操作让相关的元素绑定事件
    init() {
        this.updateNode()
        // 给加号绑定添加新选项卡事件
        this.add.onclick = this.addTab
        for(let i = 0; i < this.lis.length; i++) {
            this.lis[i].index = i
            this.lis[i].onclick = this.toggleTab  // 点击触发切换功能
            this.remove[i].onclick = this.removeTab
            this.spans[i].ondblclick = this.editTab  // 双击触发编辑功能
        }
    }
    // 清除类,切换页面前清除所有页面的类
    clearClass() {
        for (var i = 0; i < this.lis.length; i++){
            this.lis[i].className = ''
            this.sections[i].className = ''
        }
    }
    // 更新元素 因为我们动态的添加元素,因此需要重新获取对应的元素
    updateNode() {
        this.lis = this.main.querySelectorAll('li')  // 获取选项卡标签
        this.sections = this.main.querySelectorAll('section')  // 获取页面标签
        this.remove = this.main.querySelectorAll('.icon-guanbi')  // 获取选项卡的叉号
        this.spans = this.main.querySelectorAll('.firstnav li span:first-child')
    }
    // 1.切换功能
    toggleTab() {
        // this.clearClass() 这里使用this将会报错 因为在toggleTable中的this指向的是它的调用者,
        // 也就是init()中的小li,而要调用clearClass()就必须要使用指向clearClass()的调用者的this,
        // 因此就必须要用构造器中指向this的全局变量that
        that.clearClass()  // 清除所有样式
        // 切换选项卡(将样式添加到所要切换的选项卡)
        this.className = 'liactive'
		// 切换页面(将样式添加到所要切换的页面)
        this.sections[this.index].className = 'conactive'
    }
    // 2.添加功能
    addTab() {
        // 清除类
        this.clearClass()
        // (1) 创建li元素和section元素
        let li = '<li class="liactive"><span>新选项卡</span><span class="iconfont icon-guanbi"></span></li>'
        let section = '<section class="conactive">新页面</section>'
        // (2) 把这两个元素追加到对应的父元素里面
        that.ul.insertAdjacentHTML('beforeend', li)
        that.fsection.insertAdjacentHTML('beforeend', section)
        // 重新初始化
        that.init()
    }
    // 3.删除功能
    removeTab(e) {
        // 注意:删除时点击叉号也会触发叉号标签的父标签li的点击事件,因此要阻止冒泡
        e.stopPropagation()  // 阻止冒泡 防止触发li的切换事件
        let index = this.parentNode.index  // 得到叉号标签的父标签li的索引  可以认为这个索引就是叉号的索引
    	// 根据索引号删除对应的li和section remove()方法可以直接删除指定的元素
        that.lis[index].remove()
        that.sections[index].remove()
        that.init() 
        // 当我们删除的不是选中状态的li 的时候,原来的选中状态li保持不变
        if (document.querySelector('.liactive')) re
        // 当我们删除了选中状态的这个li的时候,让它的前一个li 处于选定状态
        index--
        // 手动调用我们的点击事件 不需要鼠标触发
        that.lis[index] && that.lis[index].click()  // 一个很巧妙的短路 当索引为空时就不触发点击事件 非空触发
    }
    // 4.修改功能
    editTab() {
        let str = this.innerHTML  // 保存一份选项卡默认文字
        // 双击禁止选定文字
        window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty()
        // 双击生成input表单
        this.innerHTML = '<input type="text" />'
        let input = this.children[0]
        input.value = str  // 选项卡默认文字给文本框
        input.select()  // 默认双击后选定文本框里面的所有文字
        // 当我们离开文本框就把文本框里面的值给span
        input.onblur = function(){
            this.parentNode.innerHTML = this.value
        }
        // 按下回车键也可以把文本框里面的值给span
        input.onkeyup = function(e) {
            if (e.Key === 'Enter') {
                // 手动调用表单失去焦点事件 不需要鼠标离开操作
                this.blur()
            }
        }
    }
</script>

二、构造函数和原型

2.1 构造函数创建对象

<script>
	// 1.利用 new Object() 创建对象
    var obj1 = new Object()
    // 2.利用 对象字面量创建对象
    var obj2 = {}
    // 3.利用构造函数创建对象
    function Star(uname, age){
        this.uname = uname
        this.age = age
        this.sing = function() {
            console.log('我会唱个歌')
        }
    }
    
    // 实例化对象
    var any = new Star('阿尼亚', 18)
</script>

构造函数中的属性和方法我们称为成员,成员可以添加

  • 实例成员就是构造函数内部通过this添加的成员 uname age sing 就是实例成员

    console.log(any.uname)
    any.sing()
    

    实例成员只能通过实例化的对象来访问

  • 静态成员 在构造函数本身上添加的成员

    Star.sex = '男'
    console.log(Star.sex)  // 男
    console.log(any.sex)  // undefined  静态成员不能通过对象访问 
    

    静态成员只能通过构造函数来访问,不能通过对象访问

2.2 构造函数原型 prototype

构造函数通过原型分配的函数是所有对象所共享的

JavaScript规定,每一个构造函数都有一个prototype属性,指向另一个对象。注意这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。

我们把那些不变的方法,直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法。

prototype是构造函数抽象出来的一个供各个实例调用的共用方法。

Star.prototype.sing = function() {
	console.log('我会唱歌')
}

var any = new Star('阿尼亚', 5)
var hh = new Star('昏爹', 27)

any.sing()  // 我会唱歌
hh.sing()  // 我会唱歌

原型是什么?

一个对象,我们也称为prototype为原型对象

原型的作用是什么?

共享方法

一般情况下,我们的公共属性定义到构造函数里面,公共的方法我们放到原型对象身上。

2.3 对象原型__ proto __

对象都会有一个属性__ proto __ 指向构造函数prototype原型对象,之所以我们对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有 __ proto __原型的存在。

  • __ proto __对象原型和原型对象prototype是等价
  • prototype是函数身上的,__ proto __ 是构造函数实例化对象身上的, __ proto __又指向prototype,所以共享方法
console.log(any.__proto__ === Stat.prototype)  // true

方法查找规则:

  1. 先看any对象身上是否有sing方法,如果有就执行这个对象上的sing
  2. 如果没有sing这个方法,因为有__ proto __的存在,就去构造函数原型对象prototype身上去查找sing这个方法。

2.4 原型constructor构造函数

如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数。

Star.prototype = {
	constructor: Star  // 指回原来的构造函数
	sing: function(){
		console.log('我会唱歌')
	}
	movie: function() {
		console.log('我会看电影')	
	}
}
console.log(Star.prototype)
console.log(Star.__proto__)

给原型对象做了修改后会覆盖掉原来对象的constructor属性,要想重新得到constructor属性就必须要在原型对象中添加constructor:构造函数名,让原型对象重新指回原来的构造函数。

2.5 原型链

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CtK5Nj1m-1668424673355)(C:\Users\22706\AppData\Roaming\Typora\typora-user-images\image-20221114093451709.png)]

JavaScript的成员查找机制(规则):

  1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
  2. 如果没有就查找它的原型(也就是 __ proto __指向的prototype原型对象)。
  3. 如果还没有就查找原型对象的原型(Object的原型对象)。
  4. 依次类推一直找到Object为止(null)。
  5. __ proto __对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

2.6 原型对象this指向

  1. 在构造函数中,里面this指向的是对象实例 any
  2. 原型对象函数里面的this 指向的是 实例对象 any

2.7 扩展内置对象

可以通过原型对象,对原来的内置对象进行扩展自定义的方法,比如给数组增加自定义求偶和的功能

<script>
	// 原型对象的应用 扩展内置对象方法
	console.log(Array.prototype)  // 可以看到原型对象里并没有sum()方法
    // 自定义在Array内置对象的原型对象中添加一个sum()
    Array.prototype.sum = function() {
        let sum = 0
        for (var i = 0; i < this.length; i++) {
            sum += this[i]
        }
        return sum
    }
    
    let arr = [1, 2, 3]
    console.log(arr.sum())   // 6
    console.log(Array.prototype)  // 可以看到内置对象的原型对象上有sum() 新增的深色 内置的浅色
</script>

注意:数组和字符串内置对象不能给原型对象覆盖操作Array.prototype = {}, 只能是Array.prototype.xxx = function(){}的方式


三、继承

ES6之前并没有给我们提供extends继承,我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承

3.1 call方法的作用

<script>
    // 定义一个普通函数
	function fn() {
        console.log('我想喝手磨咖啡')
        console.log(this)
    }
</script>
  • 作用1:call()可以调用函数
<script>
	fn()  // 我想和手磨咖啡 Window
    fn.call() // 我想和手磨咖啡 Window
</script>
  • 作用2:call()可以改变这个函数的this指向
<script>
	// 定义一个对象
    let o = {
        name: 'andy'
    }
    // 使用方法call()让函数中this指向对象o
    fn.call(o)  // 我想和手磨咖啡 {name: 'andy'} 即this指向对象o
    fn.call(o, 1, 2)  // 第一个参数改变this指向,第二第三个参数传参
</script>

3.2 借用构造函数继承属性

<script>
	// 借用父构造函数继承属性
    // 1. 父构造函数
    function Father(uname, age) {
        // this 指向父构造函数的对象实例
        this.uname = uname
        this.age = age
    }
    // 2. 子构造函数
    function Son(uname, age, score) {
        // this 指向子构造函数的对象实例
        // 父构造函数使用call函数让其的this指向子构造函数,并传递参数
    	Father.call(this, uname, age)
        this.score = score
    }
    let son = new Son('阿尼亚', 5, 100)
    console.log(son)   // son age: 5 uname: '阿尼亚' score: 100   子构造函数继承了父构造函数的两个属性
</script>

关于我对this的理解:this指向谁,谁就能用this调用其属性和方法,这里父构造函数的this指向了子构造函数的实例,所以子构造函数就能调用父构造函数的属性和方法。

3.3 借用构造函数继承方法

<script>
	// 借用父构造函数继承属性
    // 1. 父构造函数
    function Father(uname, age) {
        this.uname = uname
        this.age = age
    }
    // 父构造函数的共享方法  写在原型对象上
    Father.prototype.money = function() {
        console.log(10000)
    }
    // 2. 子构造函数
    function Son(uname, age, score) {
    	Father.call(this, uname, age)
        this.score = score
    }
    // 这样直接赋值会有问题,如果修改了子原型对象,父原型对象也会跟着一起变化,父构造函数的原型对象上也会	// 有exam这个方法,这样显然不对,因为这样直接赋值相当于让指向子构造函数原型对象地址的指针转向指向父     // 原型对象的地址。
    // Son.prototype = Father.prototype
    
    // 正确做法:新实例化一个Father对象,开辟新空间,根据原型链,新对象的__proto__会指向Father的原型对象,因此新对象能够继承Father的方法,而新对象赋给Son的原型对象就相当于Son继承了新对象,Son的专有方法只会在出现在新对象上,而不会出现在Father的原型对象上,这就解决了上述问题,可以把新对象当成一个中间商。
    Son.prototype = new Father()
    // 如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的构造函数
    Son.prototype.constructor = Son
    
    // 这个是子构造函数的专门的方法
    Son.prototype.exam = function() {
        console.log('孩子要考试')
    }
    let son = new Son('阿尼亚', 5, 100)
    console.log(son)
</script>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LGrXVKsN-1668424673356)(C:\Users\22706\AppData\Roaming\Typora\typora-user-images\image-20221114111537817.png)]


四、类

ES6 之前通过 构造函数 + 原型 实现面向对象编程,其特点如下:

  1. 构造函数有原型对象prototype
  2. 构造函数原型对象prototype里面有constructor指向构造函数本身
  3. 构造函数可以通过原型对象添加方法
  4. 构造函数创建的实例对象有__ proto __原型指向构造函数的原型对象

ES6 通过 实现面向对象编程

<script>
	class Star {
        
    }
    console.log(typeof Star)
    // 1. 类的本质其实还是一个函数, 我们也可以简单的认为,类就是构造函数的另外一种写法
    // (1) 类有原型对象prototype
    console.log(Star.prototype)
    // (2) 类原型对象prototype 里面有constructor指向类本身
    console.log(Star.prototype.constructor)
    // (3) 类可以通过原型对象添加方法
    Star.prototype.sing = function() {
        console.log('冰雨')
    }
    let any = new Star()
    console.dir(any)
    // (4) 类创建的实例对象有__proto__ 原型指向 类的原型对象
    console.log(any.__proto__ === Star.prototype)
</script>

3.1 类的本质

  1. class 本质还是function
  2. 类的所有方法都定义在类的prototype属性上
  3. 类创建的实例,里面也有**__ proto__** 指向类的prototype原型对象
  4. 所以ES6的类它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向编程的语法而已
  5. 所以ES6的类其实就是语法糖
  6. 语法糖:就是一种便捷写法,简单理解,有两种方法可以实现同样的功能,但是一种写法更加清晰、方便,那么这个方法就是语法糖

五、ES5中的新增方法

  • 数组方法
  • 字符串方法
  • 对象方法

4.1 数组方法

4.1.1 forEach() 迭代(遍历)数组

语法:

array.forEach(function(currentValue, index, arr))
  • currentValue:每个数组元素
  • index:索引
  • arr: 数组本身

demo:

<script>
	let arr = [1, 2, 3]
    let sum = 0
    arr.forEach(function(value, index, array) {
        console.log('每个数组元素' + value)
        console.log('每个数组元素的索引号' + index)
        console.log('数组本身' + array)
        sum += value
    })
</script>

forEach比使用for循环更加方便,for循环能实现的功能都能通过forEach内置函数实现

4.1.2 filter() 筛选数组

filter()方法创建一个新数组,新数组中的元素是通过检查指定数组中符合条件的所有元素,主要用于根据条件筛选数组

语法:

array.filter(function(currentValue, index, arr))
  • 注意:它直接返回一个新数组

demo:

<script>
	let arr = [12, 52, 43, 81]
    let newArr = arr.filter(function(value, index) {
        return value >= 20  // 筛选大于20的数据
    })
    console.log(newArr)  // [52, 43, 81]
</script>

可以根据要求,在回调函数中书写条件

4.1.3 some() 查找数组中是否有满足条件的元素

语法:

array.some(function(currentValue, index, arr))
  • 注意它返回值是布尔值,如果查找到这个元素,就返回true,如果查找不到就返回false
  • 如果找到第一个满足条件的元素,则终止循环,不再继续查找

demo:

<script>
	let arr = [12, 52, 43, 81]
    let flag = arr.some(function(value) {
        return value >= 20  // 有大于20的数据返回true,否则false
    })
    console.log(flag)  // true
    
    let arr = ['red', 'pink', 'blue']
    let flag1 = arr.some(function(value) {
        return value == 'pink'  // 有pink返回true,否则false
    })
    console.log(flag1)  // true
</script>

filter()some()区别

  1. filter 查找满足条件的元素,返回的是一个数组,而且是把所有满足条件的元素返回回来
  2. some 也是查找满足条件的元素是否存在,返回的是一个布尔值,如果查找到第一个满足条件的元素就终止循环。

案例

需求:根据输入的价格区间筛选商品,根据输入的商品名称筛选商品

UI(User Interface)用户界面图如下:
在这里插入图片描述

代码实现:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        table {
            width: 400px;
            border: 1px solid #000;
            /* 清除格子之间的空隙 */
            border-collapse: collapse;
            margin: 0 auto;
        }
        td, th {
            border: 1px solid #000;
            text-align: center;
        }
        input {
            width: 50px;
        }
        .search {
            width: 600px;
            margin: 20px auto;
        }
    </style>
</head>
<body>
    <div class="search">
        按照价格查询:<input type="text" class="start"> - <input type="text" class="end">
        <button class="search-price">搜索</button>
        按照商品名称查询:<input type="text" class="product">
        <button class="search-pro">查询</button>
    </div>
    <table>
        <thead>
            <tr>
                <th>id</th>
                <th>产品名称</th>
                <th>价格</th>
            </tr>
        </thead>
        <tbody>
        </tbody>
    </table>
    <script>
        // 利用新增数组方法操作数据
        let data = [{
            id: 1,
            pname: '小米',
            price: 3999
        }, {
            id: 2,
            pname: 'oppo',
            price: 999
        }, {
            id: 3,
            pname: '荣耀',
            price: 1299
        }, {
            id: 4,
            pname: '华为',
            price: 1999
        }, ];
        // 1. 获取相应的元素
        let tbody = document.querySelector('tbody')
        let search_price = document.querySelector('.search-price')
        let search_pro = document.querySelector('.search-pro')
        let price_start = document.querySelector('.start')
        let price_end = document.querySelector('.end')
        let product = document.querySelector('.product')

        // 2. 把数据渲染到页面中
        // 因为每次查询后都要重新渲染数据 所以将渲染数据封装成一个函数
        function setData(mydata){
            // 重新渲染数据前要先清空tbody中的数据
            tbody.innerHTML = ''
            mydata.forEach(function(value){  // 使用forEach渲染数据到表格中
            // 创建tr
            let tr = document.createElement('tr')
            // tr中写入内容
            tr.innerHTML = '<td>' + value.id + 
                '</td><td>' + value.pname + 
                '</td><td>' + value.price + '</td>'
            // 将tr加入tbody中
            tbody.appendChild(tr)
        });
        }
        // 调用渲染函数初始化渲染
        setData(data)
        // 3. 根据价格查询商品(使用filter来做)
        // 当我们点击了按钮,就可以根据我们的商品价格去筛选数组里面的对象
        search_price.addEventListener('click', function () {
            // 根据价格条件进行筛选 返回一个新数组
            let newData = data.filter(function (value) {
                return value.price > price_start.value && value.price <= price_end.value
            })
            // 重新渲染数据
            setData(newData)
        })
        // 4. 根据商品名称查找商品(使用some来做)
        // 如果查询数组中唯一的元素,用some方法更合适,因为它找到这个元素,就不再进行循环,效率更高
        search_pro.addEventListener('click', function () {
            let arr = []
            // 根据商品名称进行筛选
            let flag = data.some(function (value) {
                if (value.pname === product.value) {
                    arr.push(value)
                    return true  // return 后面必须写true
                }
            })
            // 渲染根据条件筛选的数据
            setData(arr)
        })
    </script>
</body>
</html>

4.2 对象方法

4.2.1 Object.defineProperty()定义对象中新属性或修改原有的属性

Object.defineProperty(obj, prop, descriptor)
  • obj:必需,目标对象

  • prop:必需,需定义或修改的属性的名字

  • descriptor:必需,目标属性所拥有的特性

    Object.defineProperty()第三个参数descriptor说明:以对象形式{}书写

    • value:设置属性的值,默认为undefined
    • writable:值是否可以重写,true | false 默认为true
    • enumerable:目标属性是否可以被枚举(遍历),true | false 默认为false
    • **configurable:**目标属性是否可以被删除或是否可以再次修改特性 true | false 默认为false
<script>
    var obj = {
        id: 1,
        pname: '小米',
        price: 1999
    }
    // 1. 以前的对象添加和修改属性
    obj.num = 1000
    obj.price = 99
    console.log(obj)
    // 2. Object.defineProperty() 定义新属性或修改原有的属性
    Object.defineProperty(obj, 'num', {  // 没有就添加 有就修改
        value: 1000
    })
    Object.defineProperty(obj, 'id', {  // 没有就添加 有就修改
        writable:false
    })
    obj.id = 2
    console.log(obj.id)  // 1  设置wirtable:false id将无法被修改 
</script>

4.2.2 Object.keys() 获取对象自身所有的属性

  • 效果类似于for … in
  • 返回一个属性名组成的数组
<script>
    var obj = {
        id: 1,
        pname: '小米',
        price: 1999
    }
    var arr = Object.keys(obj)
    console.log(arr)  // ['id', 'pname', 'price']
</script>
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值