Js 面向对象-继承实现
上篇讲了用 构造函数 + 原型 的方式, 通过 new 关键字去创建一个对象. 这个 new 关键字会做 4个事情:
- 创建一个新对象 obj
- 让 obj 的隐式原型 [[prototype]] 去指向其构造函数的 显示原型 prototype
- 绑定 this 为创建的对象, 并执行构造函数代码
- 若构造函数没有返回对象, 则自动返回刚创建的对象
// 构造函数
fuction Person() {}
var p1 = new Person()
var p2 = new Person()
console.log(p1.__proto__ === Person.prototype) // true
console.log(p1.__proto__ === Person.prototype) // true
// constructor 属性, 也是引用
cosole.log(Person.prototype.constructor === Person) // true
最为关键的就是第二步的理解, 涉及到了 原型, 我们将其类比理解为 父类 应该也是行的. 然后对于 js 函数的理解:
- 所有对象都有内置的隐式原型 [[prototype]] 引用, 指向一个对象 (向上)
- 函数(除箭头) 都有一个显示的 prototype 属性 (引用), 也指向一个对象 (向下)
- 函数也是对象, 因此函数既有显示原型对象 和 隐式原型对象 (二者不等)
在 js 中, 上述的 Person() 被称为构造函数, 从面向对象的编程范式来看, Person 也可以称为 类.
类继承
面相对象的三大特性: 封装, 继承, 多态, 本篇就是来学习在 js 中是如何实现继承的, 这个非常关键, 因为 js 更多是以脚本语言著称的, 不想 Java, C++ 这种内置就用, 直接就用, 因此 js 在实现面向对象特性则需要做更大的巧妙设计.
这里用 Java 来做一个类比:
// Animal.java
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void speak() {
System.out.println(name + " 发出声音。");
}
}
// Dog.java
class Dog extends Animal {
public Dog(String name) {
super(name); // 调用父类构造函数
}
@Override
public void speak() {
System.out.println(name + " 汪汪叫。");
}
}
// Main.java
public class Main {
public static void main(String[] args) {
Animal a = new Animal("动物");
Dog d = new Dog("旺财");
a.speak(); // 输出:动物 发出声音。
d.speak(); // 输出:旺财 汪汪叫。
}
}
这里的 Dog 类 通过 extends 关键字继承了父类 Animal, 然后它就天然能获取到父类的属性, 方法等, 同时也能进行拓展和重写.
这看上去似乎非常直观, 我们甚至觉得本就该如此, 但在 js 中要实现类似的效果, 则是非常曲折的过程, 也经历了好多年的发展呢.
Js 原型链
上篇再说对象原型的时候知道, 当从一个对象上获取属性时, 若当前对象没有, 则会从它的原型上去获取.
// 原型链查找
var obj = {
name: "youge",
age: 18
}
// [[GET]] 操作
// 1. 在对象当前属性中查找
// 2. 若找不到, 则会从原型对象上去找 (__proto__)
// 3. 若还找不到, 则会继续沿着 obj.__proto__.__proto__.__proto__ ...查找
// 4. 这个查找的节点则构成了所谓的原型链, 知道顶层原型对象也没找到则返回 undefined
// 原型链
console.log(obj.__proto__ === Object.prototype) // true, 到顶啦
console.log(obj.__proto__.__proto__ === null) // true
console.log(obj.city) // undefined
// 在原型链条的任意节点添加上 city 属性都能被找到
obj.__proto__.city = '长安镇'
console.log(obj.city) // 长安镇
顶层原型
原型链的尽头就是 [Object: null prototype] {}. 它是一个 object 类型, 只是长得有点奇怪.
// 顶层原型
const obj = Object.create(null)
console.log(obj) // [Object: null prototype] {}
typeof obj // object
- {}: 表示它是一个空的对象字面量, 没有自己的可枚举属性
- [Object: null prototype] : 表示它的原型 [[prototype]] 是
null, 而非 Object.prototype
顶层原型的特殊在于它的原型是 null, 且该对象上有很多默认的属性和方法, 方便给后代们继承用的. 现在来理解一下这个它是如何产生的.
// 构造函数创建对象
function Person() {}
var p = new Person()
// new 会做4件事:
// 1. 创建一个新对象 p
// 2. this -> p; p.__proto__ -> Person.prototype
// 3. 执行 Person 函数
// 4. 若函数无返回对象, 则返回 p 对象
这里补充一下, 普通函数对象的隐式原型是内置的 Function 函数, 即它是所有函数的 "父类".
那对于普通对象来说, 它的隐式原型也是内置的 Object 函数, 它是所有类的 "父类".
// 普通对象的构造函是 Object() 函数
var obj1 = {}
var obj2 = new Object()
// new 同样会做4件事:
// 1. 创建一个新对象 obj1
// 2. this -> obj1; obj1.__proto__ -> Object.prototype
// ...
这里的 Object.prototype 对象, 就是顶层的 [Object: null prototype] {} 对象原型, 就到头了.
Object 和 Function
在 js 中, 它俩都是内置的函数, 函数也是特殊对象. Object 是所有类, 普通对象的 父类; Function 则是所有函数的 "父类", 二者有点像 元 概念.
// Object 和 Function 都是 js 内置的函数, 函数也是对象
console.log(typeof Object) // function
console.log(typeof Function) // function
下面的代码如果都能理解, 则说明对于对象, 函数, 原型就理解得差不多啦.
var foo = new Function()
console.log(typeof foo) // function
var obj = new Object()
console.log(typeof obj) // object
// 作为对象, prototype 是给 其子类 共享的显示原型对象
console.log(foo.__proto__ === Function.prototype) // true
console.log(obj.__proto__ === Object.prototype) // true, 顶层原型
console.log(Function.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true
// 但并非所有对象的 __proto__ 指向顶层原型, 比如函数就是指向 Function.prototype
// 作为函数, 所有函数的 __proto__ 都指向 Function, 包括它自身
console.log(Object.__proto__ === Function.prototype) // true
console.log(Function.__proto__ === Function.prototype) // 自指, true
//
console.log(Function.prototype === Object.prototype) // false
console.log(Function.__proto__ === Object.prototype) // false
console.log(Object.__proto__ === Function.prototype) // true
-
Function 是
所有函数的 "父类", 因此**任意函数的 __ proto __ 都指向 Function.prototype; ** -
Object 是函数; Function 也是函数 (自指)
-
Object 是
部分对象(普通)的 "父类", 因此 存在特殊对象(函数)的 __ proto __ 不指向 Object.prototype; -
函数是特殊对象, 函数的 __ proto __ ! == Object.prototype 而是指向 Function.prototype
从
构造关系看, Function 和 Object 是 "平级" 的内部构造器从
原型链看, Function 的原型链, 包含了 Object.prototype
感觉还是 Function 要更厉害一些呀, 因为它是所有函数的爸爸, Object 也是函数, 但 Function 却只是特殊对象.
继承实现-by 原型链
前面铺垫了那么多关于函数, 对象, 原型, 原型链的东西, 最终目的就是来通过原型链来实现 js 的继承, 只要真正了解了 js 的继承实现, 那对于原型链和面向对象就完全拿捏了, 直接起飞.
// 不手动实现继承是不能直接用的, 毕竟没有关联吗
// 1. 父类, 公共的属性和方法
function Person() {
this.name = 'youge'
}
// 公共方法添加到原型, 实现共享
Person.prototype.eating = function() {
console.log(this.name + ' is eating')
}
// 2. 子类, 特殊属性和方法
function Student() {
this.sno = 123
}
// 学生类, 也有给下面学生的共用方法, 加到原型
Student.prototype.studying = function() {
console.log(this.name + " is studying")
}
// 学生实例 -> 学生类 -> 人类
var stu = new Student()
console.log(stu.name) // undefined
stu.eating() // 报错, 因为找不到, 为 null, 调用 null 不行的
这里继承的关键在于, 如何让 Student类的 实例对象 stu 能获取到 Person 类 的属性和方法.
- Var p = new Person(), 则 p 对象是可以访问 Person 及其原型的, 且 p.__ proto __ === Person.prototype
- Var s = new Student(), 则 s 对象是可以访问 Student 及其原型的, 且 s.__ proto __ === Stdent.prototype
- s 要访问 p 的显示原型, 则可通过 Student.prototype = new Person() === p,
间接访问 Person显示原型啦
// 2. 子类, 特殊属性和方法
function Student() {
this.sno = 123
}
// 方法: 在创建 stu 之前, 将其默认原型对象 Student.prototype -> new Person()
// 因为: new Person() 的 [[prototype]] -> Person.prototype
// 所以: new Student() -> Student.prototype -> Person.prototype
Student.prototype = new Person()
// 学生类, 也有给下面学生的共用方法, 加到原型
Student.prototype.studying = function() {
console.log(this.name + " is studying")
}
就是这个关键的一步指向Student.prototype = new Person() 就实现 s 访问 p显示实现, 即 S类 继承 P类.
注意:
千万不能写成 Studnet.prototype = Person.prototye, 这会让 S 和 P 共享原型那就乱伦啦!!!
放的位置一定要在子类 Student 添加原型方法之前, 不让就失效了.
这种通过原型链的巧妙设计就能实现类之间的继承, 别看只有一行代码, 要真能理解, 说明对象和原型确实是理解到为了的.
但是这种方式会存在几个较大的弊端:
- 不能给父类的构造函数传参, 那有个屁用!
- 原型上的引用会被共享, 无法互相隔离!
- 父类构造函数被过早执行, 产生副作用!
// 原型链实现继承的弊端
// 1. 打印 stu 对象, 某些属性是看不到的
console.log(stu) // Person { sno: 123 }, 继承的属性看不到
// 2. 创建出来两个 stu 对象
var stu1 = new Student()
var stu2 = new Student()
// 父类 Person 上的属性被继承后变不能互相独立
stu1.friends.push('cj') // stu2 也会受到影响
console.log(stu1.friends) // [ 'cj' ]
console.log(stu2.friends) // ['cj' ]
// 换个写法就能隔离, 即加在当前对象上, 而非原型
stu1.friends = ['aa', 'bb']
console.log(stu1.friends) // [ 'aa', 'bb' ]
console.log(stu2.friends) // ['cj' ]
// 3. 不好传参数
// 这个参数要在 Person 中处理比较麻烦
var stu3 = new Student('cjj')
继承实现-by 借用构造函数
用原型链实现继承的弊端在于, 无法传参, 原型引用会被共享, 父类构造函数重复执行, 实例对象属性不全等.
原型链实现: Student.prototype = new Person()
可以通过一种叫 constructor stealing 的技术来解决原型链继承的弊端. 首先来解决不能传参的问题.
// 2. 子类, 特殊属性和方法
function Student(name, age, friends, sno) {
this.name = name
this.age = age
this.friends = friends
this.sno = 123
}
这样直接加参数在 子类 是不行的, 这里的 name, age, friends 应属于公共属性, 应该放在父类 Person 处理才好.
// name, age, friends 是共用的, 不能放 Student 写死
Person -> Student
Person -> Teacher
关键一步, 将这个初始化放在父类 Person 执行, 则通过 call 的方式调用 Person() 此时 this 为 Student 呀
// by借用构造函数, 实现继承
// 1. 父类, 公共的属性和方法
function Person(name, age, friends) {
// 在 Student 中执行 Person.call(this), 这个 this -> stu
this.name = name
this.age = age
this.friends = friends
}
// 2. 子类, 特殊属性和方法
function Student(name, age, friends, sno) {
// 通过 call 方式调用 Person, 此时 this 是 stu
Person.call(this, name, age, friends, sno)
this.sno = 123
}
关键就是这精妙一步呀:
// 在 Students 中调用父类 Person, 则 this 是 stu
// 这样传递参数过去的时候, 这些参数是绑定在 stu = new Studnet 之上的
Person.call(this, name, age, friends, sno)
这样就巧妙解决了实例化对象时不好传参的弊端, 同时也解决了对象间数据隔离的问题和原型属性访问不到的问题.
// by借用构造函数, 实现继承
// 1. 父类, 公共的属性和方法
function Person(name, age, friends, sno) {
// 在 Student 中执行 Person.call(this), 这个 this -> stu
this.name = name
this.age = age
this.friends = friends
this.sno = sno
}
// 2. 子类, 特殊属性和方法
function Student(name, age, friends, sno) {
// 通过 call 方式调用 Person, 此时 this 是 stu
Person.call(this, name, age, friends, sno)
}
Student.prototype = new Person()
// 公共方法添加到原型, 实现共享
Person.prototype.eating = function() {
console.log(this.name + ' is eating')
}
// 学生类, 也有给下面学生的共用方法, 加到原型
Student.prototype.studying = function() {
console.log(this.name + " is studying")
}
// 学生实例 -> 学生类 -> 人类
// 1. 可以传参
var stu1 = new Student('cj', 30, ['yaya'], 111)
var stu2 = new Student('zs', 20, ['moni'], 123)
// 2. 互不影响, 加在了实例对象上
stu1.friends.push('jack')
// 3, 父类属性也可见
console.log(stu1)
console.log(stu2)
Person { name: 'cj', age: 30, friends: [ 'yaya', 'jack' ], sno: 111 }
Person { name: 'zs', age: 20, friends: [ 'moni' ], sno: 123 }
// 借用构造函数也有弊端:
// 1. Person 函数至少被调用 2 次
// Student.prototype = new Person(); Person.call()
// 2. stu 的原型对象 p = new Person(), 会多出来这些 name , age 等属性
相较于原型链方式的大弊端, 基于它之上的借用版本的弊端则算是不致命, 更多是浪费内存而已.
- 父类会被重复调用: 之类原型对象指向 和 在子类中调用父类, 都会执行
- 实例对象的原型对象上会多出一些 "已有" 的属性, 因为使用了借用吗, 有留痕的啦
那如果我们直接将 子类的原型赋值给父类的原型, 上面两个问题就解决啦, 但是坚决不能这样做的哈.
// 绝对不行的哦
Student.prototype = Person.prototype
这样子类和父类就共享一个原型对象了, 这样就乱伦了, 给子类原型加东西, 父类原型也会同步加, 这是不对的.
小结一下
通过上面的原型链 + 借用构造函数的方式已不断完善 js 类继承的方式, 做的是事情大致是这样:
- Person 类 / 构造函数, 有自己的原型 [[prototype]] 对象
- Student 类 / 构造函数, 有自己的原型 [[prototype]] 对象
为了要 Student 类 能够 继承 Person 类, 我们先创建一个新的对象, 比如叫 obj
- 先让 obj 作为 Student 类的原型 (
- 再让 Person 类, 作为 obj 对象的原型
// 创建对象
var stu = new Student()
var obj = new Person()
// new 内置实现
stu.__proto__ === Stuent.prototype
obj.__proto__ === Person.prototype
// 构造链条关系
Stuent.prototype = obj
// 于是:
stu.__proto__.__proto__ === Person.prototype
// 这样就 stu 就能访问整个链条上的属性啦
继承实现-by 原型式继承函数
回顾 js 想要实现继承的目的是 重复利用另外一个对象的属性和方法
在 2006年, 一个前端大佬道格拉斯.克罗克福德 (JSON 的创立者) 写的一篇文章中提到了一种继承方法, 通过原型式函数来实现继承, 而非通过构造函数.
// 原型式继承函数
var parentObj = {
name: 'youge',
age: 18,
eating: function() {
console.log(this.name + ' is eating')
}
}
// 一个对象 A 实现继承的目的, 重复利用 B 对象的属性和方法
// 对于上面的 parentObj 对象来说, 我们现在要创建一个新对象 childObj
// 并实现让 childObj 的原型是 parentObj, 即 childObj.__proto__ === parentObj
// 实现这个 createChildObj 的函数:
// 传入一个对象A, 返回一个对象 B, 让 B.__proto__ === A 即可
function createChildObj(parentObj) {
var childObj = {}
// 如何让 childObj 的原型 是 parentObj 呢?
return childObj
}
关键就是如何来实现这个 createChildObj(parentObj) 函数. 我们之前学过, 创建对象可以通过 new 关键字实现
function Foo() {}
var obj = new Foo()
// new 会帮我实现 obj.__proto__ === Foo.prototye
因此, 可先创建 Foo 构造函数, 先让它的原型设置为 parentObj, 然后再通过 Foo() 来 new 一个新对象 obj
那这样不就是实现了 obj 的原型是 parentObj 了嘛, 哇真的是有点高级呀!
function createChildObj(parentObj) {
// 如何让 childObj 的原型 是 parentObj 呢?
// 可利用 new 的特性!
function Foo() {}
Foo.prototype = parentObj
var childObj = new Foo() // childObj.__proto__ === Foo.protoytpe
return childObj
}
太过于巧妙啦!!!
当然, 从 es5, es6 发展以来, 则有更直接的内置方法来实现 Object.setPrototypeOf()
function createChildObj2(parentObj) {
var childObj = {}
// 直接设置 childObj 对象的原型 为 parentObj 即可
Object.setPrototypeOf(childObj, parentObj)
return childObj
}
随着 ECMA 的发展, 还有内置更先进的方法 Object.create(obj) , 返回一个对象, 并将其原型设置为 obj
function createChildObj3(parentObj) {
// 返回一个对象, 并设置对象的原型为 parentObj
return Object.create(parentObj)
}
完整实例如下:
// 原型式继承函数
var parentObj = {
name: 'youge',
age: 18,
eating: function() {
console.log(this.name + ' is eating')
}
}
function createChildObj(parentObj) {
function Foo() {}
Foo.prototype = parentObj
var childObj = new Foo()
return childObj
}
function createChildObj2(parentObj) {
var childObj = {}
// 直接设置 childObj 对象的原型 为 parentObj 即可
Object.setPrototypeOf(childObj, parentObj)
return childObj
}
function createChildObj3(parentObj) {
// 返回一个对象, 并设置对象的原型为 parentObj
return Object.create(parentObj)
}
// 验证
// var obj2 = createChildObj(parentObj)
// var obj2 = createChildObj2(parentObj)
var obj2 = createChildObj2(parentObj)
console.log(obj2) // {}
// { name: 'youge', age: 18, eating: [Function: eating] }
console.log(obj2.__proto__)
console.log(obj2.name) // yoluge
原型式继承函数, 就是通过类似用 Object.cretate(obj) 的方式来实现 普通对象的继承.
var parentObj = {name: 'youge', age: 18}
// 内部: childObj.__proto__ -> childObj
var childObj = Object.create(parentObj)
// 这样 childObj 对象 就可以重复利用 parentObj 对象的属性方法啦, 从而实现继承
但我们最终想要实现的是, 通过构造函数的方式实现完美继承.
继承实现-by 寄生式继承函数
寄生式 (Parasitic) 继承也是上面这个大佬提出来的, 大致思路是结合 原型类继承+工厂模式 的方法, 即通过封装一个继承过程的函数, 在内部一某种很是来增强对象, 最后将这个对象返回, 但这种方法已经没有啥人用了的哈, 了解为主.
// 寄生式继承函数
var personObj = {
name: "youge",
running: function() {
console.log(this.name + ' is running')
}
}
function createStudent(name) {
// 创建新对象, 并设置其原型为 personObj
var stu = Object.create(personObj)
// 然后针对这个 stu 对象进行额外增强
stu.age = 18
stu.studying = function() {
console.log(name + ' isstudying...')
}
return stu
}
var stu1 = createStudent('cj')
var stu2 = createStudent('yaya')
console.log(stu2) // { age: 18, studying: [Function (anonymous)] }
这种方式的弊端和原型方式是一样的, 就不能区分类型, 方法被重复创建等. 虽然这种方式并不可取, 但是它依然提供了一个思路就是我们最终要搞一个工厂函数的方式, 并结合原型链方式实现最终方案
继承实现-by 寄生组合是继承函数
寄生组合式继承相当于是结合前面的东西, 最终完美实现继承的标准方案啦.
// 继承最佳方式: 寄生组合式继承
function Person(name, age, friends) {
this.name = name
this.age = age
this.friends = friends
}
Person.prototype.running = function() {
console.log('running~')
}
// 子类
function Student(name, age, friends, sno, score) {
Person.call(this, name, age, friends)
// 子类特有的
this.sno = sno
this.score = score
}
// 要继承 Person, 创建一个对象, 让 Student.prototype -> obj -> Person
Student.prototype = Object.create(Person.prototype)
Student.prototype.studying = function() {
console.log('studying~')
}
// 验证
var stu = new Student('youge', 18, ['cj'], 123, 80)
console.log(stu)
stu.studying()
stu.running()
关键就是这句: Student.prototype = Object.create(Person.prototype)
这里: newObj = Object.create(obj); 会内置 newObj.__ proto __ === obj.prototype
Person {
name: 'youge',
age: 18,
friends: [ 'cj' ],
sno: 123,
score: 80
}
studying~
running~
这里还有一个问题是类型不对, 这里是 Person, 但其实应该是 Student, 它是打印的时候拼接的, 自动找
// Person
stu.constructor.name
// Student 原型对象改变了, 且这个新的对象, 没有 constructor,
// 就会沿着原型链继续找, 然后从 Person 中找到了 constructor,
// 但它的值是 Person
要解决这个问题也简单, 即将这个 construcotr 属性添加到 Student 新的原型对象中即可
Student.prototype.constructor = Student
当然可以用 Object.defineProperty() 添加得更精准控制一些哈:
Object.defineProperty(Student.prototype, 'constructor', {
configurable: true,
enumerable: false, // 不让被遍历
writable: true, // 可以改
value: Student // 值设置为 Student, 作为类型
})
至于就是最佳继承方案啦, 寄生组合式继承. 但还是有能优化的地方, 可以将下面这这坨核心逻辑封装为工具函数:
// 核心逻辑
Student.prototype = Object.create(Person.prototype)
Object.defineProperty(Student.prototype, 'constructor', {
configurable: true,
enumerable: false, // 不让被遍历
writable: true, // 可以改
value: Student // 值设置为 Student, 作为类型
})
封装这个继承的工具函数的思路:
- 参数要传入一个子类, 一个父类
- 无返回值
- 内部将子类的原型对象, 去
指向父类的原型对象 - 子类添加上
constructor属性, 指向之类, 修正类型
function inherit(SubType, SuperType) {
// 修改子类原型对象 -> 父类显示原型对象
SubType.prototype = Object.create(SuperType.prototype)
// 子类添加 constructor 属性
Object.defineProperty(SubType.prototype, "constructor", {
configurable: true,
enumerable: false,
writable: true,
value: SubType
})
}
当然这里的 Object.create() 方法也是可以自己实现的, 上面也提到很多次啦!
function createChildObj(parentObj) {
function Foo() {}
Foo.prototype = parentObj
return new Foo()
}
兜兜转转, 终于算是最终推演了这个 js 实现继承的最佳方案啦:
// 继承最佳方式: 寄生组合式继承
// 继承的工具函数
function inherit(SubType, SuperType) {
// 修改子类原型对象 -> 父类显示原型对象
SubType.prototype = Object.create(SuperType.prototype)
// 子类添加 constructor 属性
Object.defineProperty(SubType.prototype, "constructor", {
configurable: true,
enumerable: false,
writable: true,
value: SubType
})
}
// 案例-父类
function Person(name, age, friends) {
this.name = name
this.age = age
this.friends = friends
}
Person.prototype.running = function() {
console.log('running~')
}
// 案例-子类
function Student(name, age, friends, sno, score) {
Person.call(this, name, age, friends)
// 子类特有的
this.sno = sno
this.score = score
}
// 让 Student 继承 Person
inherit(Student, Person)
Student.prototype.studying = function() {
console.log('studying~')
}
// 验证
var stu = new Student('youge', 18, ['cj'], 123, 80)
console.log(stu)
stu.studying()
stu.running()
name: 'youge',
age: 18,
friends: [ 'cj' ],
sno: 123,
score: 80
}
studying~
running~
补充-等效的 class 语法糖
而这个实现的逻辑, 就是 ES6 中的 class 语法糖的等效实现:
// 等效 es6 的 class 语法糖
class Person {
constructor(name, age, friends) {
this.name = name
this.age = age
this.friends = friends
}
running() {
console.log('running...')
}
}
// 继承
class Student extends Person {
constructor(name, age, friends, sno, score) {
super(name, age, friends) // 对应 Person.call(this, name...)
this.sno = sno
this.score = score
}
studying() {
console.log('studying...')
}
}
// 验证
var stu = new Student('youge', 18, ['cj'], 123, 80)
console.log(stu) // Student {...}
stu.studying() // studying...
stu.running() // running...
至此, 关于 js 实现继承的内容就差不多啦, 得出最佳的方式就是使用这个寄生组合式继承. 即将子类的显示原型对象, 去指向父类的显示原型对象, 然后再补充上 constructor 属性即可. 要理解这些实现方式, 背后要对 js 函数和对象的原型, 原型链深度理解, 这样就能轻松掌握啦.

浙公网安备 33010602011771号