JS的面向对象

对象是JS中一个非常重要的概念,这是因为对象可以将多个相关联的数据封装到一起,更好的描述一个事物:

  • 比如我们可以描述一辆车:Car,具有颜色、速度、品牌、价格、行驶里程等等
  • 描述一个人:Person,具有姓名、年龄、身高、爱好、跑步等等

用对象来描述事物,更有利于我们将现实的事物,抽离成代码中某个数据结构:

  • 所以有一些编程语言就是纯面向对象的编程语言,比如Java
  • 你在实现任何现实抽象时都需要先创建一个类,根据类再去创建对象

JS支持多种编程范式,包括函数式编程和面向对象编程:

  • JS中的对象被设计成一组属性的无序集合,像是一个哈希表,有key和value组成
  • key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型
  • 如果值是一个函数,那么我们可以称之为是对象的方法

    如何创建对象

    早期使用创建对象的方式最多的是使用Object类,并且使用new关键字来创建一个对象:

  • 这是因为早期JS很多开发者是从JAVA过来的,习惯于JAVA中通过new来创建对象

后来很多开发者为了方便期间,都是直接通过字面量的形式来创建对象:

  • 这种形式看起来更加简洁,并且对象和属性之间的内聚性也更强,所以这种方式后来就流行了起来

    对属性操作的控制

    前面我们的属性都是直接定义在对象内部,或者直接添加到对象内部:

  • 但是这样来做的时候我们就不能对这个属性进行一些限制:比如这个属性是否可以通过delete删除?这个属性是否可以被for-in遍历?

如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符:

  • 通过属性描述符可以精准的添加或修改对象的属性
  • 属性描述符需要使用Object.defineProperty来对属性进行添加或者修改

    Objecet.defineProperty

    Objecet.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

    Object.defineProperty(obj, prop, descriptor)

可接收三个参数:

  • obj要定义属性的对象
  • prop要定义或修改的属性的名称或Symbol
  • descriptor要定义或修改的属性描述符

返回值:

  • 被传递给函数的对象

    属性描述符分类

    属性描述符的类型有两种:

  • 数据属性(Data Properties)描述符(Descriptor)

  • 存取属性(Accessor访问器Properties)描述符(Descriptor)

image.png

创建多个对象的方案

如果我们现在希望创建一些列的对象:比如Personm对象

  • 包括张三、李四、王五、李雷等等,他们的信息各不相同
  • 那么采用什么方式来创建比较好呢?

目前我们有两种方式:

  • new Object方式
  • 字面量创建的方式

    • image.pngimage.png
    • 字面量方式创建对象非常麻烦

      创建对象的方案-工厂模式

      工厂模式是一种常见的设计模式:
  • 通常我们会有一个工厂方法,通过该工厂方法我们可以生产想要的对象

image.png

工厂函数的缺点

工厂函数的缺点在于,实例不知道自身的类型,或者说只能知道是Object类型,虽然这并没有错误,但是这过于宽泛了,就好像人是动物,狗是动物,编程中过于宽泛可能会带来很多的问题。
image.png
我们想要的是Person类的实例person能够知道自己是Person类型的,而不仅仅知道自己是Object类型就够了。

创建对象的方案-构造函数

构造函数也称之为构造器(constructor),通常是在我们创建对象时会调用的函数。
在其他的编程语言中,构造函数是存在于类中的一个方法,称之为构造方法。但是JS中各构造函数有些不一样。

JS中的构造函数是什么样的?

  • 构造函数也是一个普通的函数,从表现形式来说,和普通函数没有区别。
  • 那么如果一个普通的函数被使用new操作符来调用,这个函数就称之为构造函数。

那么被new调用有什么特殊的呢?
如果一个函数被使用new操作符调用了,那么它会执行如下操作:

  1. 在内存中创建一个新的对象(空对象)
  2. 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性
  3. 构造函数内部的this会指向创建出来的新对象
  4. 执行函数的内部代码(函数体代码)
  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象

image.png
规范:因为构造函数和普通函数看起来没有区别,所以为了区分,一般约定俗成构造函数使用大驼峰命名

构造函数的缺点

构造函数的缺点在于,每个实例上的来自于构造函数的方法都会占据额外的内存空间,虽然它们是相同的。这就导致构造函数创建出来的实例会占据大量的内存,造成不必要的浪费,因为它们是同一个方法,只要占用一处内存空间即可。

对象的原型理解

每个对象都有自己特殊的内置属性[[prototype]],这个特殊的对象可以指向另外一个对象。
那么这个对象有什么用呢?

  • 当我们通过引用对象的属性key来获取一个value时,会触发[[Get]]操作(Get是对象的内置操作)
  • 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它
  • 如果对象中没有该属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性

那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?

  • 答案是有的,只要是对象都会有这样的一个内置属性

获取的方式有两种:

  • 方式一:通过对象的“proto”属性可以获取到(但是这个是早期浏览器自行添加的,存在兼容性问题)
  • 方式二:通过“Object.getPrototypeOf”方法可以获取到

    函数的原型prototype

    知道了对象都有原型以后,我们继续学习。

这里引入新的概念:所有的函数都有一个prototype属性:
image.png
你可能会感到疑惑,是不是因为函数是一个对象,所以它有prototype属性呢?

  • 并不是,因为它是一个函数,它才有这个特殊的属性
  • 而不是因为它是一个对象,所以有这个特殊的属性

image.png
对象没有这个属性

再看new操作符

我们知道new关键字的步骤如下:

  1. 在内存中创建一个新的对象(空对象)
  2. 这个对象内部的“[[prototype]]”属性会被赋值为该构造函数的prototype属性

image.png
所以当我们的实例来自同一个构造函数时,它们的proto属性值相同,都是构造函数的prototype值,以上面代码为例,f1和f2的proto都等于Foo.prototype

创建对象方案-原型和构造函数

学习了原型对象,我们就可以用它来解决构造函数创建对象的缺点——每个实例继承的构造函数的方法都会占用单独的内存空间,这会造成比必要的内存浪费。
image.png
像这样将构造函数想要实例继承的方法写在构造函数的原型对象上,实例就会因为自身“proto”属性指向构造函数的“prototype”的缘故获取到prototype的值。

面向对象的特性-继承

面向对象有三大特性:继承、封装、多态

  • 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程
  • 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中)
  • 多态:不同的对象在执行时表现出不同的形态

继承是做什么的呢?

  • 继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。

那么如何实现继承呢?

  • 可以使用原型链来实现继承

    原型链实现继承

    image.png
    这里最关键的就是“Student.prototype = new Person()”,这里将Person的实例作为Student的prototype,那么Student的实例就能访问到Person和Person.prototype的值,从而实现了继承。

    原型链继承的缺点

  1. 无法看到继承的属性

image.png
我们通过原型链继承创建的对象,直接打印无法看到其继承的属性

  1. 创建出来的对象会相互影响

image.png
我们明明只修改了stu1.friends,但是stu2.friends也受到了影响,这是因为stu1和stu2都是从Person获取的引用类型的值,所以修改会互相影响;而“stu1.name = ‘lisi’”则是给stu内部赋值name属性,值为lisi,所以不会影响到stu2。

  1. 在前面实现类的过程中没有传递参数

image.png
当我们给构造函数传递参数,想要实现该功能就需要修改子类,我们想要的是在父类实现该功能,而原型链继承无法实现该效果。

借用构造函数实现继承

为了解决原型链继承中存在的问题,社区中提供了一种新的技术:constructor stealing(有很多名称:借用构造函数或者经典函数或者伪造对象)

  • steal是本意是偷窃,这里意译为借用

image.png
原型链继承的缺点一就被补足了,直接打印就可以看到属性了。
并且当我们修改stu1的friends数组,也不会影响到stu2了,原型链继承的缺点二也被补足了。
同时传参的问题也得到了解决,关键在于“Person.call(this, name, age, friends)”这一行代码,也是借用构造函数实现继承的重点。这样原型链继承的缺点三就被补足了。

借用构造函数继承的缺点

  1. Person构造函数至少被调用了两次

image.png

  1. Student的原型对象也就是Person的实例上会多出一些属性,就是我们实际在stu1内部创建的那些,但是对于Person实例来说是没有必要的

image.png

原型式继承

有一个知名前端开发者提出的一种实现继承的方式。

  1. var obj = {
  2. name: "zx",
  3. age: 18,
  4. };
  5. function createObject(o) {
  6. var newObj = {};
  7. Object.setPrototypeOf(newObj, o);
  8. return newObj;
  9. }
  10. var info = createObject(obj);

上面的代码就是实现,不过他提出这个方法时还没有Object.setPrototypeOf()方法,所以其实代码是这样的

  1. function createObject (o) {
  2. frunction Fn(){ }
  3. Fn.prototype = o
  4. return new Fn()
  5. }

核心原理就是更改实例的原型对象,让它能够访问别的对象,从而实现继承,其实跟下面的代码是一样的。

  1. function createObject (o) {
  2. var newObj = {}
  3. newObj.__proto__ = o
  4. return newObj
  5. }

不过因为“proto”属性在不同的浏览器得到的支持不同,所以采用上面修改函数原型对象的代码。
另外,在ES6中,给Object添加了一个create的方法,可以直接实现该功能。

  1. function createObject(o) {
  2. var newObj = {};
  3. newObj.__proto__ = o;
  4. return newObj;
  5. }
  6. var info = createObject(obj);
  7. var info = Object.create(obj);

原型式继承的缺点

原型式继承依然存在属性共享的问题, 就像使用原型链一样。也就是说只要在原型上定义引用类型的值,就会导致所有实例都会互相影响。

寄生式继承

寄生式继承是原型式继承的开发者提出的另一种继承的实现方式,寄生式继承是与原型式继承紧密相关的一种思想。
寄生式继承的思路是结合原型类继承和工厂模式的一种方式:

  • 创建一个封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再将这个对象返回。 ```javascript // 封装object函数 function object(o) { function F() {} F.prototype = o return new F() }

// 封装创建新对象的函数 function createAnother(original) { var clone = object(original) clone.sayHello = function () { alert(“Hello JavaScript”) } return clone }

  1. <a name="HcWfT"></a>
  2. ### 寄生式继承的缺点
  3. 因为寄生式继承仅仅是对原型式继承的一种封装,所以原型式继承的属性共享问题依然存在。
  4. <a name="qUbUC"></a>
  5. ## 寄生组合式继承
  6. ```javascript
  7. function Person(name, age, friends) {
  8. this.name = name;
  9. this.age = age;
  10. this.friends = friends;
  11. }
  12. Person.prototype.running = function () {
  13. console.log("running~");
  14. };
  15. Person.prototype.eating = function () {
  16. console.log("eating~");
  17. };
  18. function Student(name, age, friends, sno, score) {
  19. Person.call(this, name, age, friends);
  20. this.sno = sno;
  21. this.score = score;
  22. }
  23. Student.prototype = Object.create(Person.prototype);
  24. Object.defineProperty(Student.prototype, "constructor", {
  25. enumerable: false,
  26. configurable: true,
  27. writable: true,
  28. value: Student,
  29. });
  30. Student.prototype.studying = function () {
  31. console.log("studying~");
  32. };
  33. var stu = new Student("zx", 18, ["kobe"], 111, 100);
  34. console.log(stu);
  35. stu.studying();
  36. stu.running();
  37. stu.eating();
  38. console.log(stu.constructor.name); // Student

寄生组合式继承的关键在于第22行到第27行的代码,之所以这样做是因为第21行的代码更改了Student的prototype,导致Student创建的实例的构造函数为Person,而22行的代码作用就是重新修改Student的constructor,这样当我们查看Student的实例的constructor时能够访问到正确的构造函数,让我们知道这个对象是哪个构造函数的实例。
不过上面的代码适用性并不高,因为它的value值是写死的,遇到别的构造函数需要继承就无法实现了,不够灵活,所以我们要对其进行封装,使其能够使用所有的情况。

  1. Student.prototype = Object.create(Person.prototype);
  2. Object.defineProperty(Student.prototype, "constructor", {
  3. enumerable: false,
  4. configurable: true,
  5. writable: true,
  6. value: Student,
  7. });
  8. // 替换为
  9. function inheritPrototype(SubType, SuperType) {
  10. SubType.prototype = Object.create(SuperType.prototype);
  11. Object.defineProperty(SubType.prototype, "constructor", {
  12. enumerable: false,
  13. configurable: true,
  14. writable: true,
  15. value: SubType,
  16. });
  17. }

很简单,只要把1-7行的代码替换为下面的11行的函数就可以实现了,里面封装了替换的操作。

原型的判断方法

hasOwnProperty

判断一个属性是属于自己的还是自己从父类继承来的

  1. var obj = {
  2. name: "why",
  3. age: 18,
  4. };
  5. var info = Object.create(obj, {
  6. address: {
  7. value: "北京市",
  8. enumerable: true,
  9. },
  10. });
  11. console.log(info.hasOwnProperty("address")); // true
  12. console.log(info.hasOwnProperty("name")); // false

instanceof

用于检测构造函数的prototype,是否出现在某个对象的原型链上

  1. function inheritPrototype(SubType, SuperType) {
  2. SubType.prototype = Object.create(SuperType.prototype);
  3. Object.defineProperty(SubType.prototype, "constructor", {
  4. enumerable: false,
  5. configurable: true,
  6. writable: true,
  7. value: SubType,
  8. });
  9. }
  10. function Person() {}
  11. var p = new Person();
  12. console.log(p instanceof Person); // true
  13. function Student() {}
  14. var stu = new Student();
  15. console.log(stu instanceof Person); // false
  16. inheritPrototype(Student, Person);
  17. var stu1 = new Student();
  18. console.log(stu1 instanceof Person); // true

可以看到,当我们让Student构造函数继承自Person构造函数之后,实例stu1也会被检测出继承自Person

isPrototypeOf

用于检测某个对象,是否出现在某个实例对象的原型链上

  1. function Person() {}
  2. var p = new Person();
  3. console.log(p instanceof Person); // true
  4. console.log(Person.prototype.isPrototypeOf(p)); // true

跟instanceof的区别在于,instanceof只能判断一个构造函数是否出现在对象的原型链上,而isPrototypeOf能够判断对象是否出现在对象的原型链上。

对象-函数-原型之间的关系

class

按照前面的构造函数形式创建类,不仅和编写普通函数过于类似,而且代码不容易理解,所以,ES6标准中提供了新的class创建类的方法。

  • 但是class类本质上依然是前面所讲的构造函数、原型链的语法糖而已

    class的定义方式

    那么,如何通过class来定义类呢?

  • 可以使用两种方式来声明类:类声明和类表达式 ```javascript // 类的声明 class Person {}

// 类的表达式 var Animal = class {};

  1. <a name="DhaUW"></a>
  2. ## class的构造方法的定义
  3. ```javascript
  4. class Person {
  5. // 类的构造方法
  6. // 注意:一个类只能有一个构造函数
  7. constructor(name, age) {
  8. this.name = name;
  9. this.age = age;
  10. }
  11. }
  12. var p1 = new Person("why", 18);
  13. var p2 = new Person("zx", 28);
  14. console.log(p1); // Person { name: 'why', age: 18 }
  15. console.log(p2); // Person { name: 'zx', age: 28 }

当我们通过new关键字操作类的时候,会调用constructor函数,并且执行如下操作:

  1. 在内存中创建一个新的对象
  2. 这个对象内部的[[prototype]]属性会被赋值为该类的prototype属性
  3. 构造函数内部的this,会指向创建出来的新对象
  4. 执行构造函数的内部代码
  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象

    class中定义方法

    在ES5的构造函数中,我们如果将方法定义在构造函数内部,而不是构造含函数的prototype中,就会导致每个构造函数的实例都会在内存中占用一块空间存储相同的方法,而class创建类可以直接将方法定在class内部,它会自行将其在prototype中创建,并不会重复。

    1. class Person {
    2. constructor(name, age) {
    3. this.name = name;
    4. this.age = age;
    5. }
    6. eating() {
    7. console.log(this.name + " eating~");
    8. }
    9. running() {
    10. console.log(this.name + " running~");
    11. }
    12. }

    class实现继承

    ```javascript class Person { constructor(name, age) { this.name = name; this.age = age; } }

class Student extends Person { constructor(sno) { super(); this.sno = sno; } }

  1. 上面的代码就等价于ES5中下面的代码
  2. ```javascript
  3. function Person(name, age) {
  4. this.name = name;
  5. this.age = age;
  6. }
  7. function Student(name, age, sno) {
  8. Person.call(name, age);
  9. this.sno = sno;
  10. }
  11. Student.prototype = Person.prototype
  12. Student.prototype = Object.create(Person.prototype);
  13. Object.defineProperty(Student.prototype, "constructor", {
  14. enumerable: false,
  15. configurable: true,
  16. writable: true,
  17. value: Student,
  18. });