• JavaScript中类的学习


    一、JavaScript中的类

    1.什么是类

            类描述了一种代码的组织结构形式,不同的语言中对其实现形式各有差异。JavaScript中的类Class实际是一种描述对象之间引用关系语法糖

            在Class语法糖出现之前,我们想重用一个功能模块,通常是用一个函数来进行封装:

    1. //声明一个函数,用于功能的复用
    2. function Animal(name) {
    3. this.name = name;
    4. }
    5. //可以在函数的原型上进行属性的添加,便于复用
    6. Animal.prototype.walk = function () {
    7. return 5555
    8. };
    9. //通过new操作符创建一个新的函数
    10. //new的过程发生了什么:
    11. // 1.创建一个新的对象
    12. // 2.将构造函数的作用域赋值给这个新对象,从而this指向了这个新对象
    13. // 3.执行构造函数中的代码,为这个新对象添加属性
    14. // 4.返回新对象
    15. const dog = new Animal("小狗");//
    16. //通过new创建了一个新对象
    17. console.log(dog);//Animal { name: '小狗' }
    18. //改对象拥有其构造函数上的属性
    19. console.log(dog.walk());//5555
    20. //通过原型可以查看原型上绑定的属性,当你获取一个属性而对象本身没有时,会一层层的查找原型上是否存在该属性,最后找到Object对象本身,没有则返回undefined,有则返回该属性
    21. console.log(Animal.prototype);//Animal { walk: [Function] }
    22. //引用对象的隐式原型指向构造函数的显示原型
    23. console.log(dog.__proto__===Animal.prototype);//true

            为了更好的做成一个单独的模块,还有使用IIFE(立即执行函数)形式来增强模块的:

    1. //立即执行函数
    2. //立即执行函数模式是一种语法,可以让你的函数在定义后立即被执行
    3. //立即执行函数的组成:定义一个函数;将整个函数包裹在一对括号中,将函数声明转换成表达式;在函数尾部加上一对括号,让函数立即被执行
    4. //作用:页面加载完成后只执行一次的设置函数;将设置函数中的变量包裹在局部作用域中,不会泄漏成全局变量
    5. var Animal = (function () {
    6. function Animal(name) {
    7. this.name = name;
    8. console.log(this.name);
    9. }
    10. Animal.prototype.walk = function (data) {
    11. console.log(data);
    12. };
    13. console.log(666);
    14. return Animal;
    15. })();
    16. Animal('我是name')
    17. Animal.prototype.walk('执行方法')
    18. //依次输出为
    19. //666
    20. //我是name
    21. //执行方法

            由于这是一种很常见的需求,所以ECMAScript规范中加入了Class语法,简化了之前的使用形式:

    1. //通过class关键字声明一个Animal类
    2. //constructor构造函数用于将new的实例对象指回构造函数本身
    3. class Animal {
    4. constructor(name) {
    5. this.name = name;
    6. }
    7. walk() {
    8. console.log('执行了');
    9. }
    10. }
    11. //通过new关键字基于类生成实例对象
    12. let dog = new Animal('小狗')
    13. console.log(Animal);//[Function: Animal]
    14. console.log(dog);//Animal { name: '小狗' }
    15. dog.walk()//执行了

    2.静态属性

            到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。 在这个例子里,我们使用 static定义 origin,因为它是所有网格都会用到的属性。 每个实例想要访问这个属性的时候,都要在 origin前面加上类名。 如同在实例属性上使用 this.前缀来访问属性一样,这里我们使用 Grid.来访问静态属性。

    1. class Grid {
    2. //用于定义静态属性
    3. static origin = { x: 0, y: 0 };
    4. calculateDistanceFromOrigin(point) {
    5. let xDist = (point.x - Grid.origin.x);
    6. let yDist = (point.y - Grid.origin.y);
    7. console.log(this.scale);//声明实例对象时所传递的值
    8. return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    9. }
    10. constructor(scale) {
    11. this.scale = scale;
    12. }
    13. }
    14. let grid1 = new Grid(1.0); // 1x scale
    15. let grid2 = new Grid(5.0); // 5x scale
    16. console.log(grid1.calculateDistanceFromOrigin({ x: 10, y: 10 }));
    17. console.log(grid2.calculateDistanceFromOrigin({ x: 10, y: 10 }));

    3.存取器

            通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。

            下面来看如何把一个简单的类改写成使用 get和 set。 首先,我们从一个没有使用存取器的例子开始。

    1. class Employee {
    2. fullName;
    3. }
    4. let employee = new Employee();
    5. employee.fullName = "Bob Smith";
    6. if (employee.fullName) {
    7. console.log(employee.fullName);
    8. }

            我们可以随意的设置 fullName,这是非常方便的,但是这也可能会带来麻烦。

            下面这个版本里,我们先检查用户密码是否正确,然后再允许其修改员工信息。 我们把对 fullName的直接访问改成了可以检查密码的 set方法。 我们也加了一个 get方法,让上面的例子仍然可以工作。

    1. let passcode = "secret passcode";
    2. class Employee {
    3. _fullName=5;
    4. get fullName() {
    5. return this._fullName;
    6. }
    7. set fullName(newName) {
    8. if (passcode && passcode == "secret passcode") {
    9. this._fullName = newName;
    10. }
    11. else {
    12. console.log("验证失败");
    13. }
    14. }
    15. }
    16. let employee = new Employee();
    17. employee.fullName = "Bob Smith";
    18. if (employee.fullName) {
    19. console.log(employee.fullName);
    20. }

    4.继承与多态

            使用extends关键字可以很方便的实现类之间的继承。

    1. class Animal {
    2. move(distanceInMeters) {
    3. console.log(`Animal moved ${distanceInMeters}m.`);
    4. }
    5. }
    6. class Dog extends Animal {
    7. bark() {
    8. console.log('Woof! Woof!');
    9. }
    10. }
    11. const dog = new Dog();
    12. dog.bark();//Woof! Woof!
    13. dog.move(10);//Animal moved 10m.
    14. dog.bark();//Woof! Woof!

            这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里, Dog是一个 派生类,它派生自 Animal 基类,通过 extends关键字。 派生类通常被称作 子类,基类通常被称作 超类。

            因为 Dog继承了 Animal的功能,因此我们可以创建一个 Dog的实例,它能够 bark()和 move()。

            下面我们来看个更加复杂的例子:

    1. //新建Animal类
    2. class Animal {
    3. //用于将实例对象的this指向构造函数本身
    4. constructor(theName) { this.name = theName; }
    5. move(distanceInMeters) {
    6. console.log(`${this.name} moved ${distanceInMeters}m.`);
    7. }
    8. }
    9. //新建Snake类继承于Animal类
    10. class Snake extends Animal {
    11. //super关键字,代表父类对象,super(name)表示执行父类
    12. constructor(name) { super(name); }
    13. move(distanceInMeters = 5) {
    14. console.log("Slithering...");
    15. //触发父类方法
    16. super.move(distanceInMeters);
    17. }
    18. }
    19. //新建Horse类继承于Animal类
    20. class Horse extends Animal {
    21. //super关键字,代表父类对象,super(name)表示执行父类
    22. constructor(name) { super(name); }
    23. move(distanceInMeters = 45) {
    24. console.log("Galloping...");
    25. //触发父类方法
    26. super.move(distanceInMeters);
    27. }
    28. }
    29. let sam = new Snake("Sammy the Python");
    30. let tom = new Horse("Tommy the Palomino");
    31. sam.move();
    32. tom.move(34);
    33. // 输出为
    34. // Slithering...
    35. // Sammy the Python moved 5m.
    36. // Galloping...
    37. // Tommy the Palomino moved 34m.

            这个例子展示了一些上面没有提到的特性。 这一次,我们使用 extends关键字创建了 Animal的两个子类: Horse和 Snake。

            与前一个例子的不同点是,派生类包含了一个构造函数,它 必须调用 super(),它会执行基类的构造函数。 而且,可以用使用super关键字调用Animal中的方法。

            这个例子演示了如何在子类里可以重写父类的方法。 Snake类和 Horse类都创建了 move方法,它们重写了从 Animal继承来的 move方法,使得 move方法根据不同的类而具有不同的功能。 注意,即使 tom被声明为 Animal类型,但因为它的值是 Horse,调用 tom.move(34)时,它会调用 Horse里重写的方法。在继承链的不同层次中move方法被多次定义,当调用方法时会自动选择合适的定义,这就是多态的一种实现。

    5.类的本质

            文章的开头说过Class只是一种描述对象之间引用关系的语法糖。现在我们就来看看语法糖背后的本质是什么。

    1. class Animal {
    2. constructor(name) {
    3. this.name = name;
    4. }
    5. print() {
    6. console.log(this.name);
    7. }
    8. }
    9. const animal = new Animal('dog');
    10. animal.print(); // dog

            这里我们创建了一个Animal的实例,可以看到实例可以使用print中的方法。实现这一切的前提需要搞清楚new操作符到底做了什么,下面是一个new操作符的模拟实现:

    1. //模拟实现new操作符
    2. //创建一个Person函数,声明属性与方法
    3. function Person(name) {
    4. this.name = name;
    5. }
    6. Person.prototype.sayName = function () {
    7. console.log(this.name);
    8. }
    9. //创建一个createPerson函数用于模拟new Person实现逻辑
    10. function createPerson() {
    11. // 1 创建一个新的对象
    12. var o = {};
    13. // 2 获取构造函数,以便实现作用域的绑定
    14. //arguments的作用:当在js中调用一个函数的时候,我们经常会给这个函数传递一些参数,js把传入到这个函数的全部参数存储在arguments中。
    15. // Arguments是一个类数组对象,对象中的每一项分别对应传入的参数
    16. console.log(arguments);//[Arguments] { '0': [Function: Person], '1': 'ydb' }
    17. //因为arguments不是数组对象,是一个类数组对象,所以不能调用数组的方法,比如prototype属性不能使用
    18. console.log(arguments.prototype);//undefined
    19. //所以为了实现后续new实现对象之前的相关绑定需要使用prototype进行绑定,所以需要将arguments转换成数组对象
    20. // console.log([].shift.call(arguments));//[Function: Person]
    21. //通过[].shift.call(arguments)将arguments从类数组对象,转成数组对象
    22. //原理:
    23. //[].slice.call( arguments )相当于Array.prototype.slice.call( arguments )
    24. //在这里,slice()是数组上的一个方法,它不改变原数组,而是从原数组中返回指定的元素,也就是一个新的数组,当没有传入任何参数的时候也就是会返回整个数组(复制原数组,不改变原数组);然后是 call 函数,它和 apply 函数一样,他们两个都是改变函数的 this 指向,区别就是参数不一样,他们的第一个参数都是一个对象或者是 “this”,而 apply 的第二个参数是一个数组,call 后面可以继续跟多个参数,也就是 apply ( this,[ ] );call ( this , n1,n2,n3....)
    25. // 所以重点就是:
    26. // 因为slice内部实现是使用的this为代表调用对象,那么当[ ].slice.call() 传入 arguments 对象的时候,通过 call 函数改变原来 slice 方法的 this 指向, 使其指向 arguments,并对 arguments 进行复制操作,然后返回一个新数组。所以就达到了把 arguments 类数组转为数组的目的!
    27. // 当然,[ ].shift.call( arguments ) 也是如此,shift () 方法为删除数组的第一项,并返回删除项,所以这句我们可以理解为 “ 删除arguments的第一项并返回,也就是拿到 arguments 的第一项 ”。
    28. var _constructor = [].shift.call(arguments);
    29. //获取到传入的Person函数
    30. console.log(_constructor);//[Function: Person]
    31. // 3 由于通过new操作创建的对象实例内部的不可访问的属性[[Prototype]](有些浏览器里面为__proto__)
    32. //指向的是构造函数的原型对象的,所以这里实现手动绑定。
    33. // 实例对象的隐式原型指向构造函数的现实原型
    34. o.__proto__ = _constructor.prototype;
    35. // 4.作用域的绑定使用apply改变this的指向
    36. //作为参数传递
    37. console.log(arguments,'arguments');//[Arguments] { '0': 'ydb' } arguments
    38. console.log(o,'o')
    39. _constructor.apply(o, arguments);
    40. console.log(o,'o');//Person { name: 'ydb' } o
    41. return o;
    42. }
    43. var person1 = createPerson(Person, 'ydb');
    44. person1.sayName();
    45. console.log(person1);//Person { name: 'ydb' }

            注意这里说的构造函数并不是指的是"constructor",而是指的是Person函数,因为每一个函数的原型对象上有一个constructor属性指向函数本身:

    console.log(Person.prototype.constructor === Person); // true
    

            到这里可以很清楚的知道之所以person1可以使用sayName方法,完全是因为这句代码:

    o.__proto__ = _constructor.prototype;
    

            它们实现了同一个对象的引用,这个对象在这里指的是Person的原型对象,也就是Person.prototype。并不是Person把原型对象复制给了o对象上,它们之前只是一种对象引用的关系。

            再来看看继承是如何实现的:

    1. class Animal {
    2. //将new的实例对象指向构造函数本身
    3. constructor(name) {
    4. this.name = name;
    5. }
    6. print() {
    7. console.log(this.name);
    8. }
    9. }
    10. class Dog extends Animal {
    11. //将new的实例对象指向构造函数本身
    12. constructor(name) {
    13. super(name);
    14. }
    15. print() {
    16. console.log("dog");
    17. }
    18. walk() {
    19. console.log(`${this.name} is a dog`);
    20. }
    21. }
    22. const dog = new Dog('二哈');
    23. dog.print(); // dog
    24. dog.walk(); // 二哈 is a dog

            转换一下:

    1. var __extends = (this && this.__extends) || (function () {
    2. var extendStatics = function (d, b) {
    3. extendStatics = Object.setPrototypeOf ||
    4. ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
    5. function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
    6. return extendStatics(d, b);
    7. };
    8. return function (d, b) {
    9. extendStatics(d, b);
    10. function __() { this.constructor = d; }
    11. d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    12. };
    13. })();
    14. var Animal = /** @class */ (function () {
    15. function Animal(name) {
    16. this.name = name;
    17. }
    18. Animal.prototype.print = function () {
    19. console.log(this.name);
    20. };
    21. return Animal;
    22. }());
    23. var Dog = /** @class */ (function (_super) {
    24. __extends(Dog, _super);
    25. function Dog(name) {
    26. return _super.call(this, name) || this;
    27. }
    28. Dog.prototype.print = function () {
    29. console.log("dog");
    30. };
    31. Dog.prototype.walk = function () {
    32. console.log(this.name + " is a dog");
    33. };
    34. return Dog;
    35. }(Animal));
    36. var dog = new Dog('二哈');
    37. dog.print(); // dog
    38. dog.walk(); // 二哈 is a dog

            可以看出通过__extends方法实现了某些对象对其他对象的引用,达到功能模块复用的目的,并没有什么魔法。

            所以到现在你应该明白了,JavaScript中其实并没有类这个概念的,只不过是语法糖隐藏了内部实现,方便开发人员的实现对对象之间引用这种功能。JavaScript中也没有所谓的“原型继承”,本质都是对象之间的引用。

  • 相关阅读:
    读<深入理解Java虚拟机-第3版>
    Java之多线程综合练习小题一
    编程语言界再添新锐,Google 前工程师开源 Toit 语言
    香港闯关相关法律
    ALU,半加器,全加器,减法电路
    大喜国庆,聊聊我正式进入职场的这三个月...
    如何使用 LeiaPix 让照片动起来
    高并发架构设计经验
    Optional非空判断
    Linux中FTP安装
  • 原文地址:https://blog.csdn.net/ct5211314/article/details/133635985