深入JS面向对象

139 阅读13分钟

面向对象是现实的抽象方式

  • 对象可以将多个关联数据封装到一起,更好的描述一个事物
  • 对象来描述事物,有利于我们将现实的事物,抽离成代码中某个数据结构

JavaScript的面向对象

JS支持多种编程范式

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

如何创建对象

  • new Object()
  • 字面量的形式

对属性的操作

属性描述符

  • 可以精准的添加或修改对象的属性
  • 需要使用Object.defineProperty来进行操作

Object.defineProperty()

  • 会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回对象

  • 参数

    • obj 要定义属性的对象
    • prop 要定义或修改的属性的名称Symbol
    • descriptor 要定义或修改的属性描述符 (是一个对象)
  • 返回值

    • 被传递给函数的对象

属性描述符分类

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

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

    configurableenumerablevaluewritablegetset
    数据描述符可以可以可以可以不可以不可以
    存取描述符可以可以不可以不可以可以可以

数据属性描述符

  • Configurable:表示属性是否可以通过delete删除属性,是否可以修改它的特性,是否可以将它修改存取属性描述符

    • 直接在对象上定义某个属性时,这个属性的Configurabletrue
    • 通过属性描述符定义某个函数时,这个属性的Configurablefalse
  • Enumerable:表示属性是否可以通过for-in或者**Object.keys()**返回该属性

    • 直接在对象上定义某个属性时,这个属性的Enumerabletrue
    • 通过属性描述符定义某个函数时,这个属性的Enumerablefalse
  • Writable:表示是否可以修改属性的

    • 直接在对象上定义某个属性时,这个属性的Writabletrue
    • 通过属性描述符定义某个函数时,这个属性的Writablefalse
  • Value:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改

    • 默认情况下这个值是undefined

存取属性描述符

  • Configurable:表示属性是否可以通过delete删除属性,是否可以修改它的特性,是否可以将它修改数据属性描述符
    • 直接在对象上定义某个属性时,这个属性的Configurabletrue
    • 通过属性描述符定义某个函数时,这个属性的Configurablefalse
  • Enumerable:表示属性是否可以通过for-in或者**Object.keys()**返回该属性
    • 直接在对象上定义某个属性时,这个属性的Enumerabletrue
    • 通过属性描述符定义某个函数时,这个属性的Enumerablefalse
  • get获取属性时会执行的函数。默认值undefined
  • set设置属性时会执行的函数。默认值undefined
  • 应用场景
    • 隐藏某一个私有属性不希望直接外界使用赋值
    • 截获某一个属性它访问和设置值的过程

对象方法补充

getOwnPropertyDescriptor(obj, prop)

  • 获取对象的特定属性描述符

getOwnPropertyDescriptors(obj)

  • 获取对象的所有属性描述符

Object.preventExtensions(obj)

  • 禁止对象扩展新属性

  • 给一个对象添加新的属性会失败(严格模式下会报错

Object.seal(obj)

  • 密封对象,禁止对象配置/删除属性

  • 实际是调用preventExtensions

  • 并将现有属性configurable: false

  • // for循环的方法来密封对象
    for (var key in obj) {
        Object.defineProperty(obj, key, {
            configurable: false,
            enumerator: false,
            writable: true,
            value: obj[key]
        })
    }
    

Object.freeze(obj)

  • 冻结对象,禁止属性修改
  • 实际上是调用seal
  • 并将现有属性writable:false

创建多个对象的方案

对象字面量 / new Object

  • 缺点重复代码太多

工厂模式

  • function createPerson(name, age, height, address) {
        var p = {}
        p.name = name
        p.age = age
        p.height = height
        p.address = address
        p.eating = function () {
            console.log(this.name + '在吃东西');
        }
        return p
    }
    
    var p1 = createPerson("zzy", 22, 1.88, "广州")
    var p2 = createPerson("lll", 24, 1.78, "北京")
    
  • 缺点:对象的类型都是Object,获取不到对象最真实的类型

构造函数

  • 也成为构造器constructor),通常会在创建对象时调用
  • 在其它面向对象的语言中,构造函数是存在于中的一个方法,称之为构造方法

JS中的构造函数

  • 一个普通函数被使用new操作符来调用,那么这个函数就被称为构造函数
  • 函数名首字母大写,多个单词采用驼峰

new操作符调用的作用

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

缺点

  • 如果构造函数中有函数,那么执行构造函数时都会创建函数对象,浪费空间

对象的原型

  • JS每个对象都有一个特殊的内置属性[[prototype]],这个特殊的属性可以指向另一个对象
  • prototype对象原型)也被称之为隐式原型
  • 早期ECMA没有规范如何去查看prototype对象原型

查看对象原型

  • 浏览器/node提供 __proto__ 属性去查看prototype(对象原型)
  • ES5以后官方提供 Object.getPrototypeOf()去查看prototype(对象原型)

对象原型的作用

  • 当我们从一个对象获取某个属性时,会触发get操作

    1. 当前对象中去查找对应的属性,如果找到就直接使用

    2. 如果没有找到,那么就会沿着它的原型链查找[[prototype]]

函数的原型

函数的原型

  • 函数作为对象也有[[prototype]](隐式原型)

  • 函数还会多出来一个显式原型属性:prototype

  • 对象的隐式原型指向函数的显式原型

  • // 函数也是一个对象
    function foo() {}
    console.log(foo.__proto__); // 函数作为对象也有[[prototype]](隐式原型)      
    console.log(foo.prototype); //函数还会多出来一个显示原型属性:prototype    
    var f1 = new foo()
    f1.__proto__ === foo.prototype // 对象的隐式原型指向函数的显示原型
    

函数的原型内存图

image.png

函数原型上的属性

constructor

  • foo.prototype这个对象中有一个constructor的属性

  • prototype.constructor = 构造函数本身

  • foo.prototype.constructor.name === foo.name
    

添加自己的属性

  • function foo() {}
    foo.prototype.name = 'zzy'
    var f1 = new foo()
    console.log(f1.name, f1.age); // zzy 
    

修改整个prototype对象

  • 真实开发中通过Object.defineProperty()方法添加constructor

  • foo.prototype = {
        // constructor: foo, //真实开发中通过Object.defineProperty()方法添加constructor
        name: 'zzy',
        age: 22,
    }
    Object.defineProperty(foo.prototype, "constructor", {
        enumerable: false,
        writable: true,
        configurable: true,
        value: foo
    })
    
    var f0 = new foo()
    console.log(f0.name); // zzy
    
  • 赋值为新的对象内存图

  • image.png

创建多个对象的方案

原型加构造函数

  • 一般属性放在函数自身内

  • 函数需要放到原型

  • function Person(name, age, height) {
        this.name = name
        this.age = age
        this.height = height
    }
    
    Person.prototype.eating = function () {
        console.log(this.name + '在吃东西');
    }
    
    var p1 = new Person('zzy', 22, 1.88)
    var p2 = new Person('lll', 18, 1.70)
    
    p1.eating() // zzy在吃东西
    p2.eating() // lll在吃东西
    

JavaScript原型链

  • 从一个对象获取属性,如果在当前对象中没有获取到就会去它的原型上面获取
  • image.png
  • image.png
  • image.png

Object的原型

Object.prototype

  • 是最顶层的原型
  • Object直接创建出来的对象的原型都是[Object:null prototype] {}
    • 该对象的原型属性指向null,也就是最顶层的原型
    • 该对象上有很多默认属性方法

Object是所有类的父类

  • 原型链最顶层原型对象就是Object的原型对象
  • image.png

面向对象的特性

封装

  • 属性方法封装到一个中,称之为封装过程

继承

  • 继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提纯面向对象中)
  • 继承可以帮我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可

多态

  • 不同对象在执行时表现出不同形态

抽象(有争议)

  • 现实的事物抽象代码的过程

继承

原型链继承

  • 弊端

    • 原型上面的属性无法枚举继承属性在stu对象中看不到
    • 获取引用,修改引用中的会相互影响(向生成的对象添加新的属性,会影响之后生成的对象,会修改原型)(push
    • 没有传递参数
  • // 父类: 公共属性和方法========================
    function Person(name = 'zzy') {
        this.name = name
        this.friends = []
    }
    Person.prototype.eating = function () {
        console.log(this.name + '在吃饭');
    }
    
    // 子类:特有属性和方法==========================
    function Stu(sno = '03171242') {
        this.sno = sno
    }
    
    // 通过原型链的方式,将p中的属性和函数赋值给Stu的prototype
    var p = new Person()
    Stu.prototype = p
    
    Stu.prototype.learning = function () {
        console.log(this.name + '在学习');
    }
    var stu = new Stu()
    console.log(stu.name);
    stu.eating();
    
    // 原型链方式的弊端:
    // 1. 第一个弊端:继承的属性在stu对象中看不到,原型上面的属性无法枚举
    // console.log(stu.name); // undefined
    // 2.创建两个stu对象
    var stu1 = new Stu()
    var stu2 = new Stu()
    stu1.friends.push('lll') // 第二个弊端:会导致之后生成的对象的friends都是'lll' 获取引用,修改引用中的值会相互影响
    stu1.name = 'aaa' // 不会影响新生成的对象的name  不会改原型,在本对象内添加属性
    console.log(stu1.name); // ['aaa']
    console.log(stu2.name); // ['zzy']
    // 3.第三个弊端 在前面实现类的过程中都没有传递参数
    

借用构造函数继承

  • constructor stealing(借用构造函数继承/经典继承/伪造对象)

  • 子类型构造函数的内部调用父类型构造函数

    • 函数可以在任意时刻调用

    • 可以通过apply()call()方法在创建的对象执行构造函数

    • function Stu(name, age, friends, sno) {
          Person.call(this, name, age, friends)
          this.sno = sno
      }
      
  • // 借用构造函数继承方案============================================================
    // 父类: 公共属性和方法========================
    function Person(name, age, friends) {
        //这里的this = stu
        this.name = name
        this.age = age
        this.friends = friends
    }
    Person.prototype.eating = function () {
        console.log(this.name + '在吃饭');
    }
    
    // 子类:特有属性和方法==========================
    function Stu(name, age, friends, sno) {
      Person.call(this, name, age, friends)
      this.sno = sno
    }
    
    // 通过原型链的方式,将p中的属性和函数赋值给Stu的prototype
    // var p = new Person()
    // Stu.prototype = p
    
    // Stu.prototype.learning = function () {
    //   console.log(this.name + '在学习');
    // }
    
    var stu = new Stu("zzy", 22, ['kobe'], 12)
    // console.log(stu.name);
    // stu.eating();
    
    // 原型链方式的弊端的解决:
    // 1. 解决第一个弊端:继承的属性在stu对象中看不到,原型上面的属性无法枚举
    console.log(stu); // Person { name: 'zzy', age: 22, friends: [ 'kobe' ], sno: '211' }
    // 2.创建两个stu对象
    var stu1 = new Stu('aaa', 12, ['xiaoa'], 123)
    var stu2 = new Stu('bbb', 21, ['ddd'], 321)
    stu1.friends.push('lucy') // 解决第二个弊端:会导致之后生成的对象的friends都是'lll' 获取引用,修改引用中的值会相互影响
    console.log(stu1); // Person { name: 'aaa', age: 12, friends: [ 'xiaoa', 'lucy' ], sno: 123 }
    console.log(stu2); // Person { name: 'bbb', age: 21, friends: [ 'ddd' ], sno: 321 }
    stu1.name = 'woai' // 不会影响新生成的对象的name  不会改原型,在本对象内添加属性
    console.log(stu1.name); // ['woai']
    console.log(stu2.name); // ['bbb']
    // 3.解决第三个弊端 在前面实现类的过程中都没有传递参数
    var stu3 = new Stu('ccc', 24, ['eee'], 021)
    
    // 借用构造函数的弊端==================
    // 1. Person函数至少被调用两次
    // 2. stu的原型对象上会多出一部分属性
    
  • 组合继承是JS最常用继承模式之一

  • 组合继承最大的问题是无论什么情况下,都会调用两次父类构造函数

    • 在创建子类原型的时候
    • 子类构造函数内部(每次创建子类实例的时候)
  • 所有子类实例拥有两份父类属性,默认访问实例本身这部分

    • 当前的实例里面(Person本身)
    • 子类对应的原型对象中(Person.__proto__

父类原型赋值给子类继承

  • 这种做法是不符合面向对象规范的,
  • 修改子类原型对象的某个引用类型的时候,父类原型对象的引用类型也会被修改

原型式继承函数(针对对象)

  • JSON创立者(道格拉斯·可罗克福德Douglas Crockford),06年写了一篇文章:《Prototypal Inheritance in JavaScript》(在JS中使用原型式继承

  • obj = {
        name: "zzy",
        age: 22,
    };
    
    // 原型式继承函数,setPrototypeOf()
    function createObject1(o) {
        var newObj = {}
        newObj.__proto__ = o // 真实开发环境不建议使用__proto__
        Object.setPrototypeOf(newObj, o)
        return newObj
    }
    var info = createObject2(obj);
    console.log(info.__proto__); // { name: 'zzy', age: 22 }
    
    // 道格拉斯·可罗克福德的原型式继承函数,还没有setPrototypeOf()
    function createObject2(o) {
        function Fn() {}
        Fn.prototype = o
        var newObj = new Fn(); // newObj.__proto__ = Fn.prototype
        return newObj;
    }
    var info = createObject2(obj);
    console.log(info.__proto__); // { name: 'zzy', age: 22 }
    
    
    // 最新ECMA提供了方法可以直接实现原型式继承函数的功能,Object.create()
    var info = Object.create(obj)
    console.log(info.__proto__); // { name: 'zzy', age: 22 }
    

寄生式继承函数(针对对象)

  • 寄生式Parasitic)继承是与原型式Prototypal )继承紧密相关的一种思想,由道格拉斯·可罗克福德Douglas Crockford)提出和推广

  • 寄生式继承的思想是结合原型式继承工厂模式的一种方式

    • 创建一个封装继承过程函数,该函数在内部以某种方式来增强对象,最后再将这个对象返回
  • var personObj = {
        running: function () {
            console.log("running~");
        },
    };
    
    //弊端:1.stu的函数会每次都重复创建;2.无法明确对象类型
    function createsStudent(name) {
        var stu = Object.create(personObj);
        stu.name = name;
        stu.studying = function () {
            console.log("studying~");
        };
        return stu;
    }
    
    var stuObj1 = createsStudent("aaa");
    var stuObj2 = createsStudent("bbb");
    console.log(stuObj1.name); // aaa
    stuObj1.studying(); // studying~
    stuObj2.running(); // running~
    
    

寄生组合式继承(最终方案)

  • 封装子类和父类类型继承的两个方法(核心函数

    • // 社区中用的较多,没有Object.create()方法时
      function createObject(o) {
      function Fn() {}
      Fn.prototype = o;
      return new Fn();
      }
      function inheritPrototype(subType, supType) {
      subType.prototype =createObject(supType.prototype);
      Object.defineProperty(subType.prototype, "constructor", {
      enumerable: false,
      writable: true,
      configurable: true,
      value: subType,
      });
      }
      
    • // 使用Object.create()
      function inheritPrototype(subType, supType) {
          subType.prototype = Object.create(supType.prototype);
          Object.defineProperty(subType.prototype, "constructor", {
              enumerable: false,
              writable: true,
              configurable: true,
              value: subType,
          });
      }
      
  • 实现继承的例子

    • // 实现继承
      function Person(name, age, friends) {
          this.name = name;
          this.age = age;
          this.friends = friends;
      }
      
      Person.prototype.running = function () {
          console.log(this.name + "running~");
      };
      
      function Student(name, age, friends, sno, score) {
          Person.call(this, name, age, friends);
          this.sno = sno;
          this.score = score;
      }
      
      inheritPrototype(Student, Person);
      
      Student.prototype.studying = function () {
          console.log(this.name + "studying~");
      };
      
      var stu = new Student("zzy", 22, ["aa", "bb", "cc"], 111, 100);
      console.log(stu);
      stu.running();
      stu.studying();
      

原型的判断方法

hasOwnProperty

  • 对象是否一个属于自己属性不是原型上的属性)
  • 当前对象中返回true只在原型中的属性返回false

in/for in 操作符

  • 判断某个属性是否在某个对象或者对象的原型
  • 不管在当前对象还是原型返回的都是true
var obj = {
  name: 'zzy',
  age: 22
}

var info = Object.create(obj, {
  address: {
    value: '广州市',
    enumerable: true
  }
})
console.log(info); // { address: '广州市' }
console.log(info.__proto__); // { name: 'zzy', age: 22 }

// hasOwnProperty方法判断 在当前对象中返回true,只在原型的中的属性返回false
console.log(info.hasOwnProperty('address')); // true
console.log(info.hasOwnProperty('name')); // false

// in 操作符 不管在当前对象还是原型中返回的都是true
console.log('address' in info); // true
console.log('name' in info); // true

for(var key in info) {
  console.log(key); // address, name, age
}

instanceof

  • 用于检测构造函数prototype,是否出现在某个实例对象原型链
  • 自动调用构造函数prototype

isPrototypeOf

  • 用于检测某个对象,是否出现在某个实例对象原型链
function Person(params) {}
var p = new Person();
// 判断Person的prototype是否出现在p的原型链上
console.log(p instanceof Person); // true
// 判断p有没有出现在Person.prototype上
console.log(Person.prototype.isPrototypeOf(p)); // true 

var obj = {
    name: 'zzy'
}
var info = Object.create(obj)
// console.log(info instanceof obj); // 报错 obj不是构造函数,没有prototype属性
console.log(obj.isPrototypeOf(info)); // true

原型继承关系

  • image.png